 |
|
 |
 |
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.
|
|
|
|
|
|