Authenticating into an Iron Speed Designer Application Using XML Web Services


Help application users login to a public website automatically.
- Neil Ennis, Director of Expert1 Pty Ltd

March 18, 2009
Iron Speed Designer V6.X

Introduction

Imagine this scenario. You’ve built an Iron Speed Designer application for your client to use internally within their organization. They loved it so much that they also asked you to build an Iron Speed Designer application for their public website, which is on a totally different server.

You’ve now got both applications working perfectly, and your client thinks you’re amazing. But they have one problem. They want to be able to log in to their public website to view information, and they’re annoyed because they have to enter their credentials every time they want to see it.

The manager says “I just want to click on a button on our internal web site and view the summary information from the public site. I don’t want to have to key in my password every time.”

There are many variations to this problem. Each of them is based on a situation where you have two web sites that need to communicate with each other. Users at one web site need to view secure information on the other web site.

Here’s a simple way to solve this problem, but it might help to consider an example from every-day life first...

Going to the Movies: An Analogy

I go to the movies and buy my tickets at the ticket booth.

I give my ticket to the usher, who punches a hole in my ticket, and takes my lovely lady and me to our seats.

When I get home, my teenage son finds my punched ticket. He goes back to the movies the next day with my old ticket. He presents it to the usher, who raises an eyebrow at him and, instead of letting him in, politely directs him to the ticket booth, so he can buy his own ticket.

This analogy demonstrates the basic requirements of a ticketing system:

1. Issue unique tickets that can be presented to a security agent.
2. Invalidate the ticket once it has been used so it can’t be reused.
3. Provide a mechanism for causing the ticket to expire after a short period in case it gets lost or stolen.

Overview of the Ticketing System Example

A fully functional download of this example is available here.

The ticketing system example comes in two parts: a “Private” web site and a “Public” web site.

In the real world, each of these sites would reside on its own server, and they would only be able to communicate with each other via web services. The ticketing system will run fine, however, even if you install both systems on the same server.

The Public web site lets users log in and view a hypothetical “Activation Code” that belongs only to them.

The Private web site also lets users log in. But once they’re logged in, it lets them view their "Activation Code" on the public site by authenticating them via a web service.

The Public Website

I’m using Web Services in this example. If you’ve never used them before, please read this introductory article about adding web services to Iron Speed Designer Applications.

Add a Web Service to Your Application

The Public web site consists of a Users table which contains the user activation codes:

CREATE TABLE [Users] (
    [UserID] [int] IDENTITY (1, 1) NOT NULL ,
    [Name] [varchar] (50) NOT NULL ,
    [Password] [varchar] (50) NOT NULL ,
    [ActivattionKey] [varchar] (50) NOT NULL ,
    CONSTRAINT [PK_Users] PRIMARY KEY NONCLUSTERED
    (
        [UserID]     ) WITH FILLFACTOR = 90 ON [PRIMARY]
) ON [PRIMARY]
GO

It also contains related UserRoles and Roles table.

The other point of interest is the Tickets table. This table contains information about Tickets that have been issued by the system, who they were issued to, when they expire, and whether or not they have been used:

CREATE TABLE [Tickets] (
    [TicketID] [int] IDENTITY (1, 1) NOT NULL ,
    [TicketGUID] [uniqueidentifier] NOT NULL ,
    [IPAddress] [varchar] (50) COLLATE Latin1_General_CI_AS NOT NULL ,
    [HostName] [varchar] (50) COLLATE Latin1_General_CI_AS NOT NULL ,
    [IssueDate] [datetime] NOT NULL ,
    [ExpiryDate] [datetime] NOT NULL ,
    [UserID] [int] NOT NULL ,
    [IsVoid] [bit] NOT NULL CONSTRAINT [DF_Tickets_IsVoid] DEFAULT (0),
    CONSTRAINT [PK_Tickets] PRIMARY KEY CLUSTERED
    (
        [TicketID]
    ) WITH FILLFACTOR = 90 ON [PRIMARY] ,
    CONSTRAINT [FK_Tickets_Users] FOREIGN KEY
    (
        [UserID]
    ) REFERENCES [Users] (
        [UserID]
    )
) ON [PRIMARY]
GO

A new ticket is created via the “CreateTicket” stored procedure:

CREATE Procedure dbo.CreateTicket
/*
 * Create a new ticket, and return its details in the result set
 */
    (
        @UserName varchar(50),
        @Password varchar(50),
        @IPAddress varchar(50),
        @HostName varchar(50),
        @TicketGUID uniqueidentifier OUTPUT
    )
AS
BEGIN
    SET NOCOUNT ON
    DECLARE @UserHash varchar(500)
 
    -- Check to see that the user exists, and the password is valid
    DECLARE @UserID INT
    SELECT @UserID = U.UserID
        FROM Users U
        WHERE U.Name = @UserName
        AND U.Password = @Password
 
    -- do we have a valid user and password?
    IF @UserID IS NOT NULL
    BEGIN
        -- add the new ticket
        SET @TicketGUID = NEWID()
        INSERT INTO Tickets(TicketGUID, IPAddress, HostName,
                IssueDate, ExpiryDate, UserID)
        SELECT @TicketGUID,
                @IPAddress,
                @HostName,
                GetDate(),
                DATEADD(MINUTE, 30, GetDate()),
                -- by default set the expiry time of the ticlet to 30 minutes from now
                @UserID
    END
    ELSE
        SET @TicketGUID = NULL
 
    SELECT * FROM Tickets WHERE TicketGUID = @TicketGUID
END
GO

As you can see, in order to create a ticket, you must supply a valid user ID and password. This information is passed in via the web service. Since it’s transmitted over the Internet, you would be wise to host your web service on a secure connection that is SSL certificated.

I haven’t done it here, but as an added precaution you could also hash up the information you transmit to the web service. The .NET framework has some easy-to-use cryptography functions that make this a breeze. For further information, please see:

System.Security.Cryptography.MD5CryptoServiceProvider

This provides a “ComputeHash” function that makes it easy to hash up any information using the MD5 algorithm.

The CreateTicket procedure is called by a Web Method in our Ticket Service as per the following VB.NET code.

<WebMethod(Description:="Returns a ticket for authenticated users")> _
Public Function Authenticate(ByVal UserName As String, _
        ByVal Password As String, _
        ByVal IPAddress As String, _
        ByVal HostName As String) As Guid
    Dim Result As Guid
    Try
        ' note. We can't use Context.Request.UserHostAddress since that
        ' would return the IPAddress of the remote web server. We want
        ' the IPAddress of the user. The remote server therefore needs
        ' to tell us the users IP address and host name
        Result = CreateTicket(UserName, Password, IPAddress, HostName)
    Catch ex As Exception
        Throw New Exception("ApplicationsService.Authenticate: " & ex.Message, _
                ex.InnerException)
    End Try
    Return Result
End Function
 
Private Function MyConnectionString() As String
    ' -------------------------------------------------------------------------
    ' note - if you change the name of this application, you'll need to find
    ' out what the database connection string setting name is look in Web.Config.
    ' The key will be of the form DataBaseNNNNNNNN where NNNNNN is the name of
    ' your(Application)
    ' -------------------------------------------------------------------------
    Return (ConfigurationManager.AppSettings("DatabaseTicketDemoPublic1"))
End Function
 
Private Function CreateTicket(ByVal UserName As String, _
        ByVal Password As String, _
        ByVal IPAddress As String, _
        ByVal HostName As String) As Guid
    Dim MyCmd As New System.Data.SqlClient.SqlCommand
    Dim prmTicketGUID As New SqlClient.SqlParameter
    prmTicketGUID.ParameterName = "@TicketGuid"
    prmTicketGUID.Direction = ParameterDirection.Output
    prmTicketGUID.SqlDbType = SqlDbType.UniqueIdentifier
 
    MyCmd.CommandText = "CreateTicket"
    MyCmd.CommandType = System.Data.CommandType.StoredProcedure
    MyCmd.Parameters.Add(New System.Data.SqlClient.SqlParameter( _
        "@RETURN_VALUE", _
        System.Data.SqlDbType.Int, _
        4, _
        System.Data.ParameterDirection.ReturnValue, _
        False, CType(0, Byte), CType(0, Byte), "", _
        System.Data.DataRowVersion.Current, _
        Nothing))
 
    MyCmd.Parameters.AddWithValue("@UserName", UserName)
    MyCmd.Parameters.AddWithValue("@Password", Password)
    MyCmd.Parameters.AddWithValue("@IPAddress", IPAddress)
    MyCmd.Parameters.AddWithValue("@HostName", HostName)
    MyCmd.Parameters.Add(prmTicketGUID)
 
    MyCmd.Connection = New System.Data.SqlClient.SqlConnection(MyConnectionString)
 
    MyCmd.Connection.Open()
 
    MyCmd.ExecuteNonQuery()
    MyCmd.Connection.Close()
 
    If Not IsDBNull(prmTicketGUID.Value) Then
        Return prmTicketGUID.Value
    Else
        Throw New Exception("TicketService.CreateTicket: Unable to create ticket")
    End If
End Function

The Authenticate web method uses the IP Address and Host Name passed to it from the Private web site. I want to know the IP Address of the end user, not of the Private web server. I’m going to ensure that the ticket matches the end user’s IP address.

The IP Address of the Private web server might come in handy, though. The “Authenticate” method is central to the whole operation. It is important that you secure it. One approach would be for the “Authenticate” method to reject any requests that didn’t originate from a known server. For example:

If Context.Request.UserHostAddress <> MyAllowedIPAddress Then
    Throw New Exception("Access denied")
End If

A table of permissible IP addresses is even better.

The system can check the validity of an existing ticket presented to it via the dbo.IsValidTicket function:

CREATE FUNCTION IsValidTicket
-- Function which decides if a ticket is valid
(
    @TicketGUID UNIQUEIDENTIFIER,
    @IPAddress VARCHAR(50),
    @HostName VARCHAR(50)
)
RETURNS BIT
AS
BEGIN
    DECLARE @IsValidTicket BIT
 
    -- workaround coz SQL2000 doesn't like GetDate in functions
    DECLARE @CurrentDate DATETIME
    SELECT @CurrentDate = CurrentDate FROM CurrentDateView
 
    IF EXISTS(SELECT 1
        FROM Tickets
        WHERE TicketGuid = @TicketGuid
        AND IPAddress = @IPAddress
        AND HostName = @HostName
        AND @CurrentDate BETWEEN IssueDate AND ExpiryDate
        AND (IsVoid IS NULL OR IsVoid = 0))
            SET @IsValidTicket = 1
    ELSE
        SET @IsValidTicket = 0
 
    -- Return
    RETURN @IsValidTicket
END
 
GO

This function ensures the ticket is current, not used, and has been issued for the IP address and host name of the person presenting it. Comparing IP address and host name makes it harder for someone else to steal the ticket.

This function is accessed via the following VB.Net code:

Public Function IsValidTicket(ByVal TicketGuid As Guid, _
        ByVal IPAddress As String, _
        ByVal HostName As String) As Boolean
    Dim MyCmd As New System.Data.SqlClient.SqlCommand
    Dim Result As Boolean     Try         MyCmd.CommandText = "dbo.IsValidTicket"
        MyCmd.CommandType = System.Data.CommandType.StoredProcedure
 
        Dim prmReturnValue As New SqlClient.SqlParameter
        prmReturnValue.ParameterName = "@RETURN_VALUE"
        prmReturnValue.Direction = ParameterDirection.ReturnValue
        prmReturnValue.SqlDbType = SqlDbType.Bit
 
        MyCmd.Parameters.Add(prmReturnValue)
        MyCmd.Parameters.AddWithValue("@TicketGuid", TicketGuid)
        MyCmd.Parameters.AddWithValue("@IPAddress", IPAddress)
        MyCmd.Parameters.AddWithValue("@HostName", HostName)
 
        MyCmd.Connection = _
    New System.Data.SqlClient.SqlConnection(MyConnectionString)
        MyCmd.Connection.Open()
 
        MyCmd.ExecuteNonQuery()
        Result = CType(prmReturnValue.Value, Boolean)
 
    Catch ex As Exception
        Result = False
        Throw New Exception("TicketService.IsValidTicket: " & ex.Message, _
    ex.InnerException)
    Finally
        Try
                MyCmd.Connection.Close()
        Catch
        End Try
    End Try
    Return Result
End Function

Finally, the ticket is voided or has a “hole” punched in it via the VoidTicket stored procedure:

CREATE Procedure dbo.VoidTicket
/*
 * Void a ticket
 */
    (
        @TicketGUID uniqueidentifier,
        @IPAddress varchar(50),
        @HostName varchar(50)
    )
AS
BEGIN
    SET NOCOUNT ON
    IF DBO.IsValidTicket(@TicketGuid, @IPAddress, @HostName) = 1
        UPDATE Tickets
        SET IsVoid = 1
        WHERE TicketGUID = @TicketGuid
    ELSE
        RAISERROR('VoidTicket: Invalid Ticket', 16, 1)
END
GO

This function is accessed via the following VB.NET code:

Public Function VoidTicket(ByVal TicketGuid As Guid, _
        ByVal IPAddress As String, _
        ByVal HostName As String) As Boolean
    Dim MyCmd As New System.Data.SqlClient.SqlCommand
    Dim Result As Boolean = False
    Try
        MyCmd.CommandText = "VoidTicket"
        MyCmd.CommandType = System.Data.CommandType.StoredProcedure
        MyCmd.Parameters.Add( _
            New System.Data.SqlClient.SqlParameter("@RETURN_VALUE", _
                System.Data.SqlDbType.Int, _
                4, _
                System.Data.ParameterDirection.ReturnValue, _
                False, CType(0, Byte), CType(0, Byte), "", _
                System.Data.DataRowVersion.Current, _
                Nothing))
 
        MyCmd.Parameters.AddWithValue("@TicketGuid", TicketGuid)
        MyCmd.Parameters.AddWithValue("@IPAddress", IPAddress)
        MyCmd.Parameters.AddWithValue("@HostName", HostName)
        MyCmd.Connection = _
    New System.Data.SqlClient.SqlConnection(MyConnectionString)
        MyCmd.Connection.Open()
        MyCmd.ExecuteNonQuery()
        ' if we get this far, the ticket was voided, so return success
        Result = True
    Catch ex As Exception
        Throw New Exception("TicketService.CreateTicket: Unable to create ticket")
    Finally
        Try
            MyCmd.Connection.Close()
        Catch ex As Exception
        End Try
    End Try
 
    Return Result
 
End Function

The Private Website

You will need to add a reference to the Ticket Service to the Private Web Site. In Visual Studio, do this by selecting the “Add Web Reference…” option from the Project’s context menu:

When prompted, enter the location of the ticketing Web Service:

You will then be presented with a description of the service:

From here, choose a name for the web service when you reference it from within your project. Click the “Add Reference” button.

Once you’ve done this, the methods of the Web Service will be available to your application.

' get info about this user
Dim su As New Utils.SystemUtils(Me.Page)
Dim uRec As UsersRecord = UsersTable.GetRecord("UserID=" & su.GetUserID)
 
' get an instance of the ticketing web service
Dim ts As New TicketService.TicketService
 
' get a ticket Dim guidTicket As Guid = ts.Authenticate(uRec.Name, _
    uRec.Password, _
    Request.UserHostAddress, _
    Request.UserHostName)
 
' build a url Dim strURL As String = _
    ConfigurationManager.AppSettings( _
        "RemoteUserInfoURL") & _
        "?T=" & guidTicket.ToString
 
' go to the new url
Response.Redirect(strURL)

The Private web site gets the ticket via the ts.Authenticate method, and then passes this information as a QueryParameter in the URL when it displays the remote web page.

Note that I’ve stored the RemoreUserInfoUrl as a variable in the web.config file. This makes it easier to change addresses if the application needs to be moved.

Letting the Authenticated User in

Back on the public web site, you need to check the ticket, and let the users view their information.

Here’s the code to do this from a normal Iron Speed Designer ShowRecord page.

Put the code in the Page_Load_InitPageSettings method so that you can authenticate the web service user before the Iron Speed Designer security check functions kick in and force the user to go to the sign-in screen first.

Protected Overrides Sub Page_Load_InitPageSettings( _
ByVal sender As Object, ByVal e As System.EventArgs)
    Dim su As New Utils.SystemUtils(Me.Page)
 
    ' if we're logged in and a ticket was supplied, log out first,
    ' to make sure we're logged in as the right user
    If su.IsLoggedIn _
    And Not IsNothing(Request.QueryString("T")) Then
        SystemUtils.ClearLoginInfo()
    End If
 
    ' if we're not logged in, see if we have a ticket that we
    ' can authenticate from
    If Not su.IsLoggedIn Then
        Try
            ' have we got a ticket?
            If Not IsNothing(Request.QueryString("T")) Then
                      &bsp; ' hide the ok button, reveal the close button
                        ' get the app id
                        Dim AppID As Integer = _
                                CInt(Request.QueryString("Application"))
                        ' get the ticket
                        Dim TicketGuid As String = _
                                Request.QueryString("T")
                        Dim guidTicket As New Guid(TicketGuid)
 
                        ' validate the ticket
                        Dim SrvApp As New TicketService
                        If SrvApp.IsValidTicket(guidTicket, _
                                                                Request.UserHostAddress, _
                                Request.UserHostName) Then
                            ' void the ticket (do this as soon as possible
                            ' after validation, for security purposes
                            SrvApp.VoidTicket(guidTicket, _
                                Request.UserHostAddress, _
                                Request.UserHostName)
                            ' get a copy of the ticket so we can work out
                            ' who the user is
                            Dim tr As TicketsRecord = _
                                TicketsTable.GetRecord( _
                                                                "TicketGuid='" & TicketGuid & "'")
                            If Not IsNothing(tr) Then
                                ' SetLoginInfo will do the work of
                                ' authenticating the(Name And Password)
                                Dim ur As UsersRecord = _
                                                                UsersTable.GetRecord( _
                                                                "UserID=" & tr.UserId0)
                                ' have we got a user record?
                                If Not IsNothing(ur) Then
                                                                ' get credentials
                                                                Dim strUserName As String = _
                                                                    ur.Name
                                                                Dim strPassword As String = _
                                                                    ur.Password
                                                                Dim errMessage As String = ""
                                                                ' authenticate
                                                                Dim bSuccess As Boolean = _
                                                                    DirectCast(Me.Page, _
                                BaseApplicationPage).SystemUtils.SetLoginInfo( _
                                                                                                                                strUserName, _
                                                                                                                                strPassword, _
                                                                                                                                errMessage)
                                                                ' did we succeed?
                                                                If Not bSuccess Then
                                                                    ' login failed
                                                                    Throw New Exception("Login Failed")
                                                                Else
                                                                    ' we've authenticated with a ticket.
                                                                    ' Make sure we reember that this window
                                                                    ' is in a popup, so we can close it later
                                                                    Session("IsPopup") = True
                                                                    ' now we're logged in, re-load this
                                                                    ' page without the query string
                                                                    ' parameters()
                                                                    Response.Redirect( _
                                                                Request.Url.GetLeftPart( _
                                                                                                                                UriPartial.Path))
                                                                End If
                                Else
                                                                ' didn't find the user
                                                                Throw New Exception("User Not Found")
                                End If
                            Else
                                ' didn't find the ticket
                                Throw New Exception("Ticket Not Found")
                            End If
                        Else
                            ' ticket was not valid
                            Throw New Exception("Invalid Ticket")
                        End If
            Else
                        ' no ticket & not logged in, so get user to log in
                        Response.Redirect("../Security/Signin.aspx")
            End If
 
            MyBase.Page_Load_InitPageSettings(sender, e)
 
        Catch ex As Exception
            ' An security error has occured, display the error
            ' and bomb out
            Throw New Exception( _
                        "ERROR: ShowMyUserInfo (LoadData):" & ex.Message)
        End Try
 
    End If
End Sub

The actual line that does the authencicating is the one which says:

Dim bSuccess As Boolean = _
    DirectCast(Me.Page, _
      BaseApplicationPage).SystemUtils.SetLoginInfo( _
        strUserName, _
        strPassword, _
        errMessage)

This takes the User Credentials you extracted from the ticket, and passes them to the SetLoginInfo method.

The Ticketing System in Action

Here’s what the user sees. First after logging in to the Private Web Site:

The user sees their page, with a Show Record image button. When they click on the button, the system authenticates them on the Public web site behind the scenes, and then pops up a new window with their information in it:

About the Author

Neil Ennis has more than 25 years experience as a software developer. He graduated from the University of Queensland in 1981 with a Bachelor of Science degree.

He is the creator of the popular IBS Finance and Tango32 loan management systems. His software has been used by lenders throughout Australia, the South Pacific and the UK since the 1980s.

Neil founded Expert 1 Pty Ltd in 2004. It specializes in the development of loan origination and loan management software with interfaces to most major banking systems and credit agencies.

Neil is an enthusiastic cruiser and built the ShipWatcher.com website as a free service to lovers of cruise ships.

Contact the author.



  Privacy Statement