Manual JWT Validation against Azure Active Directory

For our current WebApi project we decided to implement OAuth2 authentication with Azure Active Directory. As our API needs to support other authentication mechanisms like Basic and Negotiate beside OAuth2 we implemented multiple authentication filters that, where each filter is responsible for a specific authentication mechanism. One of these authentication filters, the BearerAuthenticationFilter, is responsible to handle requests that contain a Bearer access token in the Authorization header. The bearer access token provided by Azure Active Directory is a JWT (JSON Web Token) signed with a certificate. The BearerAuthenticationFilter has to read the JWT and validate its signature with a certificate.

Register Application in Azure AD

To create access tokens for testing purposes, your application has to be registered with one of your AD tenants. Register your application in Azure with your Azure AD tenant is easy. Have a look at the documentation about Authorize access to web applications using OAuth 2.0 and Azure Active Directory. There you will find all necessary information about the following topics.

  • How to register your application
  • OAuth 2.0 authorization flow
  • Request authorization code
  • Request access token
  • Refresh access token
  • JWT claims

Furthermore Microsoft provides a lot of Azure code samples on GitHub.

Read JWT

To read and validate the JWT I suggest using the Microsoft library called System.IdentityModel.Tokens.Jwt, which is availble on NuGet. The library includes types for creating, serializing and validating JSON web tokens.

ATTENTION
System.IdentityModel.Tokens.Jwt version 5.x.x requires .NET Framework 5.x.
If the target framework is .NET Framework 4.5.x or 4.6.x take latest stable 4.x.x version of System.IdentityModel.Tokens.Jwt package.

The following code snippet illustrates how to easily read information from a JSON web token.

public class BearerAuthenticationFilter : AuthenticationFilter
{
    public override async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
    {
        AuthenticationHeaderValue authenticationHeaderValue = context.Request.Headers.Authorization;
        if (authenticationHeaderValue == null)
        {
            return;
        }

        if (!authenticationHeaderValue.Scheme.Equals("Bearer", StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        // read JWT
        var token = authenticationHeaderValue.Parameter;
        var jwtHandler = new JwtSecurityTokenHandler();
        var jwt = (JwtSecurityToken)jwtHandler.ReadToken(token);
        Contract.Assert(null != jwt, $"{ErrorCode.UNAUTHORIZED}Invalid JWT token");
        Contract.Assert(null != jwt.Payload, $"{ErrorCode.UNAUTHORIZED}Invalid JWT payload");

        var upn = jwt.Payload[Public.Constants.Authentication.JwtKey.UPN] as string;
        Contract.Assert(!string.IsNullOrWhiteSpace(upn), $"'{Public.Constants.Authentication.JwtKey.UPN}' is missing in JWT payload");

        var issuer = jwt.Payload.Iss;
        Contract.Assert(!string.IsNullOrWhiteSpace(issuer), $"'{Public.Constants.Authentication.JwtKey.ISSUER}' is missing in JWT payload");
    }

        ...
}

Validate JWT

The certificate to validate the JWT signature with can be fetched from an endpoint provided by Azure AD. The following code snippet illustrates how to validate a JWT.

IMPORTANT

When using OpenIdConnectConfiguration``configManager.GetConfigurationAsync(cancellationToken); has to be called in async context by using await (Implies that the surrounding method has to be async). Otherwise the method call GetConfigurationAsync(cancellationToken) did never return. Even when I tried to run the asynchronous method synchronously by calling .Result or using other mechanisms to run ansynchronous methods synchronous the method didn’t return.

public override async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
    ...

    // validate JWT
    try
    {
        await JwtValidator.ValidateJwtToken(token, cancellationToken);
    }
    catch (SecurityTokenException e)
    {
        var validationSucceeded = false;
        Contract.Assert(validationSucceeded, $"{ErrorCode.UNAUTHORIZED}JWT validation failed ({e.Message}).");
    }

    ...
}

public class JwtValidator
{
    private const string STS_DISCOVERY_ENDPOINT_SUFFIX = ".well-known/openid-configuration";
    private const string URI_DELIMITER = "/";

    public static async Task<SecurityToken> ValidateJwtToken(string token, CancellationToken cancellationToken)
    {
        var aadInstance = "https://login.microsoftonline.com/{0}";
        var tenant = "example.com";
        var audience = "853fb202-4201-4e20-97ae-4d5840d9490f";
        var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

        // Fetch configuration
        var stsDiscoveryEndpoint = string.Concat(authority, URI_DELIMITER, STS_DISCOVERY_ENDPOINT_SUFFIX);
        ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);
        var config = await configManager.GetConfigurationAsync(cancellationToken);

        // extract issuer and token for validation
        var issuer = config.Issuer;
        var signingTokens = config.SigningTokens.ToList();

        // validate token
        var validationParameters = CreateTokenValidationParameters(signingTokens, issuer, audience);

        var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();

        SecurityToken jwt;
        jwtSecurityTokenHandler.ValidateToken(token, validationParameters, out jwt);
        return jwt;
    }

    private static TokenValidationParameters CreateTokenValidationParameters(List<SecurityToken> signingTokens, string issuer, string audience)
    {
        Contract.Requires(null != signingTokens);
        Contract.Requires(!string.IsNullOrWhiteSpace(issuer));

        return new TokenValidationParameters()
        {
            ValidAudience = audience,
            ValidIssuer = issuer,
            IssuerSigningTokens = signingTokens,
            CertificateValidator = X509CertificateValidator.None,
            ValidateLifetime = true
        };
    }
}

Links

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: