Writing an OpenID Connect Web Client from Scratch

OIDC is supposed to make things easier, so I thought it would be a good exercise to write a web application that uses OIDC to authenticate users – but without using any OIDC specific libraries.

I chose to use the implicit flow with the form post response mode – which is very similar to the WS-Federation or SAML2p POST profiles. Let’s see how much code it takes.

Initiating the Request
The first task is to create the authentication request for the OIDC authorize endpoint. This involves constructing a query string that contains the client id, scope, redirect URL, response type and response mode. In addition you need to create two random numbers for state and nonce. State is used to correlate the authentication response, nonce is used to correlate the identity token coming back. Both values need to be stored temporarily (I use a cookie for that).

public ActionResult SignIn()
{
    var state = Guid.NewGuid().ToString("N");
    var nonce = Guid.NewGuid().ToString("N");
 
    var url = Constants.AuthorizeEndpoint +
        "?client_id=implicitclient" +
        "&response_type=id_token" +
        "&scope=openid email" +
        "&redirect_uri=http://localhost:11716/account/signInCallback" +
        "&response_mode=form_post" +
        "&state=" + state +
        "&nonce=" + nonce;
            
        SetTempCookie(state, nonce);
        return Redirect(url);
}

 

Handling the Callback
When the OIDC provider is done with its work, it sends back an identity token as part of a form POST back to our application. We can fetch the token (and the state) from the forms collection, validate the token (more on that shortly) and if successful sign the user in locally using an authentication cookie.

[HttpPost]
public async Task<ActionResult> SignInCallback()
{
    var token = Request.Form["id_token"];
    var state = Request.Form["state"];
 
    var claims = await ValidateIdentityTokenAsync(token, state);
 
    var id = new ClaimsIdentity(claims, "Cookies");
    Request.GetOwinContext().Authentication.SignIn(id);
 
    return Redirect("/");
}

 

Validating the Token
There is an exact specification on how to validate identity tokens and the authentication response (see here) – this involves:

  • Validate the JWT
    • the audience must match the client id
    • the issuer must match the expected issuer name
    • the signing key must match the expected issuer signing key
    • the token must be in its validity time period
  • The state parameter on the response must match the state value that we sent to the provider on the initial request
  • The nonce claim inside the identity token must match the nonce that we sent to the provider on the initial request

Most of the work is done by the JWT token library from Microsoft (see here for alternatives on different platforms).

private async Task<IEnumerable<Claim>> ValidateIdentityTokenAsync(
string token, string
state)
{
    var result = await Request
        .GetOwinContext()
        .Authentication
        .AuthenticateAsync("TempCookie");
                
    if (result == null)
    {
        throw new InvalidOperationException("No temp cookie");
    }
 
    if (state != result.Identity.FindFirst("state").Value)
    {
        throw new InvalidOperationException("invalid state");
    }
 
    var parameters = new TokenValidationParameters
    {
        AllowedAudience = "implicitclient",
        ValidIssuer = "https://idsrv3.com",
        SigningToken = new X509SecurityToken(
           X509
           .LocalMachine
           .TrustedPeople
           .SubjectDistinguishedName
           .Find("CN=idsrv3test", false)
           .First())
    };
 
    var handler = new JwtSecurityTokenHandler();
    var id = handler.ValidateToken(token, parameters);
 
    if (id.FindFirst("nonce").Value != 
        result.Identity.FindFirst("nonce").Value)
    {
        throw new InvalidOperationException("Invalid nonce");
    }
 
    Request
       .GetOwinContext()
       .Authentication
       .SignOut("TempCookie");
                
    return id.Claims;
}

 

Done. This wasn’t too bad! The full sample using IdentityServer v3 as the provider can be found here.

In the next post I will show how you can simplify this by using the OpenID discovery document and OWIN middleware. Stay tuned.

This entry was posted in Uncategorized. Bookmark the permalink.

17 Responses to Writing an OpenID Connect Web Client from Scratch

  1. Hi,
    What about the validation of the redirect_url in ThinkTecture ?
    Don’t you think http was a lazy choice for a client ?
    Can you validate the client certificate before redirect the authorization token ?

    Stéphane POPOFF

  2. I don’t understand that sentence. Could you please explain?

    • Sorry for this poor translation. Don’t you think as an authorisation server you must have a proof of the client identity better than a simple URL ? So redirect only on https and verify the client certificate should be a minimal requirement when using Oauth.

  3. In implicit flow (hence the name) the redirect URL is the client “authentication” – and yes it should always be over SSL (or some other transport protection). Since the redirect is done via the browser – the authorization server cannot very a client certificate.

    There are other flows in OAuth/OIDC (e.g. code flow) that require client authentication. Check the spec.

  4. Axel says:

    Wouldn’t it be better to store state in the server session instead of sending it over the wire?

    • I need to compare the state I sent to the server with the state I kept locally. It is for correlating the request with the response.

      • Axel says:

        Sure that is the purpose of the state parameter.
        Are you really keeping it locally?
        “SetTempCookie(state, nonce);” sounds like you are sending it to the browser as a cookie instead of storing it locally in the session of the server.
        Then state is coming back from the browser in the cookie and you compare the state from the cookie with the state from the url.
        I think that state should be stored in the server’s session.

  5. One copy is stored in a cookie – one copy I send to the authorization server. What’s wrong with that?

    • Axel says:

      There is nothing wrong as long as the cookie is signed and the returning value is validated before it is compared with the returning state.

      • The OWIN cookie middleware signs and encrypts all cookies by default – that’s why I didn’t explicitly mention it. Thanks for pointing it out!

  6. Axel says:

    This magic is hidden in SetTempCookie then, right?
    Maybe it is short enough to be part of the post?

  7. Chris Simmons says:

    Just a note for others who read this excellent article and can’t find the samples (since they’ve been moved):
    https://github.com/thinktecture/Thinktecture.IdentityServer.v3.Samples/tree/master/source/Clients

    Thanks, Dominick, for your write-up here. Very helpful stuff.

  8. Vikr says:

    Hey Mate

    I am interested how SSO is done in OpenID connect endpoint. Assume Logon has happened and then you have a token( OpenIDtoken) and then you land to another company B2B , is there a case or spec in OpenID for that which it can handle?

Leave a comment