I’ve currently got a project where I need to provide an OpenID Provider (OP) to authenticate users using Active Directory (AD), something that shouldn’t be to much of a hassle.
But there is a catch: the OP needs to be outside the firewall in order to talk to the Relying Parties (RP) and is unable to communicate directly with AD. So what can you do?
Well, in this case the OP is only going to be used by users that are sitting behind the firewall, and who have access to both internal resources and external resources over port 80, and what this means is that we can use the client (the browser) to relay messages between the OP and the asserting party. In order to make the communication going via the browser trusted, we can use a challenge combined with symmetric encryption to verify authenticity.
This means that the chain of trust needed for the identity assertion looks like the following
- The RP trust the assertion from the OP
- The RP and the OP shares can communicate directly and uses a shared secret to verify the authenticity of the message passed through the browser
- The RP trusts the assertion from the Internal Webserver (IW)
- The RP and the IW has a pre-shared encryption key that together with a challenge serves to verify the authenticity of the messages passed through the browser
In the following code DotNetOpenAuth is used to provide support for OpenID, and easyXDM is used to provide the Cross-Document Communication.
Codewise, it all starts once the OpenID request hits the OP, and at this point we only store away the request and render a blank page containing the following JavaScript
// server.aspx // This is called by ASP.NET Ajax once the page is ready function pageLoad() { // Set up a new easyXDM.Rpc object var rpc = new easyXDM.Rpc({ remote: "http://localhost:55192/Endpoint.aspx" }, { remote: { authenticate: {} } }); // Retrieve a challenge from the server PageMethods.GetChallenge(function (challenge) { // Relay the challenge to the internal webserver rpc.authenticate(challenge, function (response) { // Relay the response back to the server PageMethods.VerifyResponse(response, function (verified) { // If the response was verified, we redirect to the page that will complete the OpenID assertion if (verified) { location.href = "complete.aspx"; } else { alert("not authenticated"); } }); }); }); }
As you can see this uses PageMethods to talk to the server side code for retrieving the challenge, and for delivering the response
'server.aspx <WebMethod()> _ Public Shared Function GetChallenge() As Utilities.Challenge ' This method is called to create a challenge, and to get access to the claimed identifier that the internal webserver will have to verify ' Create and store away a challenge Dim challenge As New Utilities.Challenge challenge.Guid = System.Guid.NewGuid().ToString() HttpContext.Current.Session("guid") = challenge.Guid If providerEndpoint.PendingAuthenticationRequest IsNot Nothing Then challenge.Identifier = providerEndpoint.PendingAuthenticationRequest.ClaimedIdentifier.OriginalString End If ' Return a Plain Old CRL Object (POCO). This will be serialized into JSON Return challenge End Function <WebMethod()> _ Public Shared Function VerifyResponse(ByVal response As String) As Boolean ' This method will be called after the response returns from the internal webserver If providerEndpoint.PendingAuthenticationRequest IsNot Nothing Then Dim enc = New Utilities.SymmetricEncryption("my secret") ' Encrypt the claimed identity and the stored challenge, and see if it matches the response from the internal server If enc.Encrypt(providerEndpoint.PendingAuthenticationRequest.ClaimedIdentifier.OriginalString & "_" & HttpContext.Current.Session("guid")) = response Then ' If it matches we issue an authentication token FormsAuthentication.SetAuthCookie(providerEndpoint.PendingAuthenticationRequest.ClaimedIdentifier, False) Return True End If End If Return False End Function
On the internal server the setup is similar, we accept the claimed identity, verify it against the identity of the authenticated user, and return a signed response.
Here we expose a method whose only purpose is to relay the challenge back to the serverside code
// Endpoint.aspx // This is called by ASP.NET Ajax once the page is ready function pageLoad() { // Set up a new easyXDM.Rpc object var rpc = new easyXDM.Rpc({}, { local: { authenticate: function (challenge, fn) { // Relay the challenge to the serverside code PageMethods.Authenticate(challenge, function (response) { // Relay the response back to the OP fn(response); }); } } }); }
Here we extract the username from the claimed identifier and compare it to the known one. If it matches we return an encrypted response
'Endpoint.aspx <WebMethod()> _ Public Shared Function Authenticate(ByVal challenge As Utilities.Challenge) As String ' This is called to check wether the claimed identifier matches the known one Dim claimedUserName As String = New Uri(challenge.Identifier).Query.Substring(4) If HttpContext.Current.User.Identity.IsAuthenticated Then Dim username As String = HttpContext.Current.User.Identity.Name username = username.Substring(username.IndexOf("\") + 1) If username = claimedUserName Then ' If it matches we encrypt the identifier and the challenge, and return the result as the response Dim enc = New Utilities.SymmetricEncryption("my secret") Return enc.Encrypt(challenge.Identifier & "_" & challenge.Guid) End If End If Return String.Empty End Function
One of the interesting things here is how the instances of Utilities.Challenge
is serialized and deserialized going from the OP’s server side code to the client side code, and again serialized and deserialized going from the internal webservers client side code to the internal webservers server side code. Like magic right?
(Of course, we could easily have used a couple of redirects instead of the server-client-client-server communication, but that wouldn’t have been as fun now would it 🙂