How to set up an AD backed OpenID Provider without direct communication

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 :)

Tags: , , ,

This entry was posted on Friday, July 9th, 2010 at 19:11 and is filed under easyXDM, programming. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

  • http://www.google.com/profiles/david.williware David Williams

    Sean – I love it. Very clever. I’m headed to the easyXDM group to gather more info.

  • http://www.mactonweb.com/ Web design London

    It does this without the Relying Party needing access to end user credentials .An end user can freely choose which OpenID Provider to use, and can preserve  service types built on top of this protocol to create a framework.

  • Nicholas Wilson

    Fascinating! I had exactly the reverse problem of RPs behind the firewall and needed to relay things the other way. Any comments on my spec appreciated (prototype only but works)… http.//nwilson.github.io/oidrelay/