45

The following code gives me Azure AD security token, I need to validate that token is valid or not. How to achieve this?

// Get OAuth token using client credentials 
string tenantName = "mytest.onmicrosoft.com";
string authString = "https://login.microsoftonline.com/" + tenantName;

AuthenticationContext authenticationContext = new AuthenticationContext(authString, false);

// Config for OAuth client credentials  
string clientId = "fffff33-6666-4888-a4tt-fbttt44444";
string key = "123v47o=";
ClientCredential clientCred = new ClientCredential(clientId, key);
string resource = "http://mytest.westus.cloudapp.azure.com";
string token;

Task<AuthenticationResult> authenticationResult = authenticationContext.AcquireTokenAsync(resource, clientCred);
token = authenticationResult.Result.AccessToken;
Console.WriteLine(token);
// How can I validate this token inside my service?                
krlzlx
  • 5,752
  • 14
  • 47
  • 55
Neo
  • 15,491
  • 59
  • 215
  • 405

4 Answers4

62

There are two steps to verify the token. First, verify the signature of the token to ensure the token was issued by Azure Active Directory. Second, verify the claims in the token based on the business logic.

For example, we need to verify the iss and aud claim if you were developing a single tenant app. And you also need to verify the nbf to ensure the token is not expired. For more claims you can refer here.

Below description is from here about the detail of signature verifying. (Note: The example below uses the Azure AD v2 endpoint. You should use the endpoint that corresponds to the endpoint the client app is using.)

The access token from the Azure AD is a JSON Web Token(JWT) which is signed by Security Token Service in private key.

The JWT includes 3 parts: header, data, and signature. Technically, we can use the public key to validate the access token.

First step – retrieve and cache the signing tokens (public key)

Endpoint: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration

Then we can use the JwtSecurityTokenHandler to verify the token using the sample code below:

 public JwtSecurityToken Validate(string token)
 {
     string stsDiscoveryEndpoint = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";

     ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);

     OpenIdConnectConfiguration config = configManager.GetConfigurationAsync().Result;

     TokenValidationParameters validationParameters = new TokenValidationParameters
     {
         ValidateAudience = false,
         ValidateIssuer = false,
         IssuerSigningTokens = config.SigningTokens,
         ValidateLifetime = false
     };

     JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

     SecurityToken jwt;

     var result = tokenHandler.ValidateToken(token, validationParameters, out jwt);

     return jwt as JwtSecurityToken;
 }

And if you were using the OWIN components in your project, it is more easy to verify the token. We can use the code below to verify the token:

app.UseWindowsAzureActiveDirectoryBearerAuthentication(
            new WindowsAzureActiveDirectoryBearerAuthenticationOptions
            {
                Audience = ConfigurationManager.AppSettings["ida:Audience"],
                Tenant = ConfigurationManager.AppSettings["ida:Tenant"]
            });

Then we can use the code below to verify the ‘scope’ in the token:

public IEnumerable<TodoItem> Get()
{
    // user_impersonation is the default permission exposed by applications in AAD
    if (ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value != "user_impersonation")
    {
        throw new HttpResponseException(new HttpResponseMessage {
          StatusCode = HttpStatusCode.Unauthorized,
          ReasonPhrase = "The Scope claim does not contain 'user_impersonation' or scope claim not found"
        });
    }
    ...
}

And here is a code sample which protected the web API with Azure AD:

Protect a Web API using Bearer tokens from Azure AD

derekbaker783
  • 8,109
  • 4
  • 36
  • 50
Fei Xue
  • 14,369
  • 1
  • 19
  • 27
  • 1
    I'm a little confused about the validation of the token. Let's say I'm running an asset exchange. I've got a user who obtained a security token at 12 PM. At 1 PM I discover that they're violating the exchange rules and I go into the AAD portal and block that user's sign-ins. Do I understand that it will take a day or two before that token will no longer work? Is there anything more immediate that will disable this user? – Quark Soup Feb 21 '17 at 13:09
  • 2
    At present, it is not able to revoke the access token already issued by Azure AD. However, we can disable the users sign-in for the app immaculately by enable the **User assignment required to access app** feature on the Azure portal and disable the user. If you want Azure AD to support revoking the access token, you can submit the feedback from [here](https://feedback.azure.com/forums/34192--general-feedback). – Fei Xue Feb 22 '17 at 05:54
  • 29
    @FeiXue please don't turn off important validation rules in your sample code (`ValidateAudience`, `ValidateIssuer`, `ValidateLifetime`) before somebody copy-pastes your sample and thinks it's good to go for production because it works. – Steven Liekens May 17 '18 at 07:58
  • Where do you put your `Validate` method and where do you call it? – krlzlx Aug 23 '18 at 07:59
  • 2
    I"m curious - who and how is managing the caching of the public key and in which cases the cache gets updated? – JustAMartin Aug 20 '19 at 06:40
  • But how to configure config.SigningTokens? Azure AD gives access to keys through dedicated Uri. – Bronek May 26 '20 at 10:31
  • 1
    See Ambrose Leung's answer below. This is currently out of date. SigningTokens are no longer used by Azure. – Patrick Knott Jul 24 '20 at 19:40
  • 3
    @Patrick Indeed, it's been changed to SigningKeys, also see https://stackoverflow.com/questions/45500752/how-to-use-configurationmanager-microsoft-identitymodel-protocols – Zimano Apr 23 '21 at 08:16
32

Just wanted to add to Fei's answer for people using .net Core 2.0

You'll have to modify 2 lines of the Validate(string token) method.

 var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
        stsDiscoveryEndpoint,
        new OpenIdConnectConfigurationRetriever()); //1. need the 'new OpenIdConnect...'

 OpenIdConnectConfiguration config = configManager.GetConfigurationAsync().Result;
 TokenValidationParameters validationParameters = new TokenValidationParameters
 {
     //decode the JWT to see what these values should be
     ValidAudience = "some audience",
     ValidIssuer = "some issuer",

     ValidateAudience = true,
     ValidateIssuer = true,
     IssuerSigningKeys = config.SigningKeys, //2. .NET Core equivalent is "IssuerSigningKeys" and "SigningKeys"
     ValidateLifetime = true
 };
Ambrose Leung
  • 3,704
  • 2
  • 25
  • 36
  • 5
    Please don't post answers that disable the three most important validation rules. Also you should probably not use that in production yourself. – Steven Liekens May 16 '18 at 15:37
  • 10
    good point, i changed it from 'false' to 'true' - I was just going off of the original answer – Ambrose Leung May 16 '18 at 18:23
  • Hi @StevenLiekens can you shed some light how it'd be in production or a link to get further insight? – Sergio Solorzano Mar 25 '20 at 13:50
  • @SergioSolorzano I had `ValidateXXX = false` initially (because I copied and pasted it from the marked answer), I've already corrected it to `ValidateXXX = true` which is what Steven was referring to (that it should be set to 'true' in production) – Ambrose Leung Mar 25 '20 at 21:41
  • But how to configure config.SigningTokens? Azure AD gives access to keys through dedicated Uri – Bronek May 26 '20 at 10:31
  • 1
    This is the way it works in .net Framework 4.7.2 now as well. Azure gives keys not signing tokens. – Patrick Knott Jul 24 '20 at 19:39
  • Where can I get/generate my own SigningKeys? – FritsJ Aug 06 '20 at 15:20
  • @FritsJ Use OpenSSL to generate your own keys. Sweet and to the point example is at https://stackoverflow.com/a/10176685/508681 – jklemmack Jan 13 '21 at 16:45
  • Trying to use this but I get a "Microsoft.IdentityModel.Tokens.SecurityTokenException: IDX10612: Decryption failed. Header.Enc is null or empty, it must be specified." Any idea how to deal with that? – Tomas Jansson Mar 08 '21 at 14:47
5

But if you are not using OWIN in your projects, it is going to be a little hard or at least time consuming.. This articleHere is great resource.

And because I do not have much to add on the above, except the detailed code.. Here is something that can be useful to you:

 public async Task<ClaimsPrincipal> CreatePrincipleAsync()
    {
        AzureActiveDirectoryToken azureToken = Token.FromJsonString<AzureActiveDirectoryToken>();
        var allParts = azureToken.IdToken.Split(".");
        var header = allParts[0];
        var payload = allParts[1];
        var idToken = payload.ToBytesFromBase64URLString().ToAscii().FromJsonString<AzureActiveDirectoryIdToken>();

        allParts = azureToken.AccessToken.Split(".");
        header = allParts[0];
        payload = allParts[1];
        var signature = allParts[2];
        var accessToken = payload.ToBytesFromBase64URLString().ToAscii().FromJsonString<AzureActiveDirectoryAccessToken>();

        var accessTokenHeader = header.ToBytesFromBase64URLString().ToAscii().FromJsonString<AzureTokenHeader>();
        var isValid = await ValidateToken(accessTokenHeader.kid, header, payload, signature);
        if (!isValid)
        {
            throw new SecurityException("Token can not be validated");
        }
        var principal = await CreatePrincipalAsync(accessToken, idToken);
        return principal;
    }



    private async Task<bool> ValidateToken(string kid, string header, string payload, string signature)
    {
        string keysAsString = null;
        const string microsoftKeysUrl = "https://login.microsoftonline.com/common/discovery/keys";

        using (var client = new HttpClient())
        {
            keysAsString = await client.GetStringAsync(microsoftKeysUrl);
        }
        var azureKeys = keysAsString.FromJsonString<MicrosoftConfigurationKeys>();
        var signatureKeyIdentifier = azureKeys.Keys.FirstOrDefault(key => key.kid.Equals(kid));
        if (signatureKeyIdentifier.IsNotNull())
        {
            var signatureKey = signatureKeyIdentifier.x5c.First();
            var certificate = new X509Certificate2(signatureKey.ToBytesFromBase64URLString());
            var rsa = certificate.GetRSAPublicKey();
            var data = string.Format("{0}.{1}", header, payload).ToBytes();

            var isValidSignature = rsa.VerifyData(data, signature.ToBytesFromBase64URLString(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
            return isValidSignature;
        }

        return false;
    }

There are some functions that I use in here that are not available for you, they are self descriptive.

Assil
  • 572
  • 6
  • 21
  • I am using MS OAuth 2 to sign to my web. I can't find my key in microsoftKeysUrl. Is there anywhere else that I can obtain the correct key? – s k Jun 15 '20 at 01:49
  • 1
    Are you saying that you know the keys id or name in the token but you are not able to find it https://login.microsoftonline.com/common/discovery/keys ??? That is strange, It should be found here. The problem however is that they have changed the version of signing algorithm and introduced the nonce which will make it hard for you to verify it. Which endpoint are you hitting to get the token V1 or V2 ? – Assil Jun 16 '20 at 01:23
  • 1
    After trying for 1 whole day, I managed to find my kid here. https://login.microsoftonline.com/common/discovery/v2.0/keys. But then I have to enable the implicit options inside AD Otherwise the key won't show. – s k Jun 16 '20 at 07:28
  • Nice to hear.. So yea, I see you specified the version to get the key. Things are changing fast. – Assil Jun 16 '20 at 10:03
0

While all very helpful, none of the prior answers actually enforce all of the key validation options -- most of them are disabled in the other code samples.

For example, how do you validate the Issuer? Well you have the core details available in the Open ID Configuration but you have to map it to your Azure Tenant ID.

This is tested and working in .NET 6.

Here is a code sample that validates them all and is actually async:

//Load from your configuration...
var tenantId = "00000000-0000-0000-0000-000000000000";
var clientId = "00000000-0000-0000-0000-000000000000";
var openIdConnectWellKnownConfigUri = new Uri("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration");

//With the Input token to be validated...
var jwtTokenToValidate = " . . . ";

//With the above information we can validate all key aspects of the Jwt Token...
try
{
    var openIdConfigManager = new ConfigurationManager<OpenIdConnectConfiguration>(
        openIdConnectWellKnownConfigUri.ToString(),
        new OpenIdConnectConfigurationRetriever()
    );

    OpenIdConnectConfiguration openIdConfig = await openIdConfigManager.GetConfigurationAsync().ConfigureAwait(false);
    TokenValidationParameters validationParams = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        ValidateAudience = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidateTokenReplay = true,
        //Valid values for Validation of the JWT...
        ValidAudience = clientId,
        ValidIssuer = openIdConfig.Issuer.Replace("{tenantid}", tenantId),
        //Set the Azure AD SigningKeys for Validation!
        IssuerSigningKeys = openIdConfig.SigningKeys,
    };

    var jwtTokenHandler = new JwtSecurityTokenHandler();
    jwtTokenHandler.ValidateToken(jwtTokenToValidate, validationParams, out SecurityToken validToken);

    return validToken as JwtSecurityToken
        ?? throw new SecurityTokenValidationException("Unexpected failure while parsing and validating the the JWT token specified.");
}
catch (Exception exc)
{
    //Handle the Token Validation Exception (one of many types may occur)...
}
CajunCoding
  • 720
  • 5
  • 9