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 }; } }
Hi,
How do you check if the “alg” has “none” and if the signature has changed. How can you check that manually?
Sorry for the late reply and thank you for your message. We are currently no longer using this auth method, so I unfortunately cannot help you on that. thank you for your understanding. Ronald
can we authorize the Azure AD JWT Token as [Authorize] filter ?
I actually never tried. Sry I cannot help here.