azuremicrosoft-graph-apiasp.net-core-webapisingle-sign-onmicrosoft-entra-id

Verify Microsoft Access token on my ASP.NET Core Web API


Attached is the screenshot of the tokenOn my client side (nextAuth) I am getting an access token once from Microsoft, now I want that token to be verified from backend which is on ASP.NET Core Web API. As far as I know the process is to get the keys from Microsoft and the one which has same "kid" use that to verify.

This is the header part of my JWT that I got from Microsoft:

{
    "nonce": "v9eWI-Nv75YgvN8IgSNSlHPK-UH8iOcEUz8KXqj6DeA",
    "alg": "RS256",
    "x5t": "H9nj5AOSswMphg1SFx7jaV-lB9w",
    "kid": "H9nj5AOSswMphg1SFx7jaV-lB9w"
}

Now after lots of trying I was forced to hardcode certificate value of key that I got from Microsoft to get some success

I visited the URL https://login.microsoftonline.com/{myTenantId}/discovery/keys?appid={clientId}

And got this

{
      "kty": "RSA",
      "use": "sig",
      "kid": "H9nj5AOSswMphg1SFx7jaV-lB9w",
      "x5t": "H9nj5AOSswMphg1SFx7jaV-lB9w",
      "n": "iheTlbqieo0KzaFBTz6dxt9LAENfe2d0ywnpvZx9khzurtApc4ThWBpeoTBI6UpReeCwyW6DQjJSGCqHwe-wqRdgoiMc0PV-danrh0px38x2KL_j7VoHR0hlQBYOpp5GBMdz-Nsc80wBtHAqxz7Nno3qYNTXUvwZ2LSbbvgoPXrh0zhLlSrn2gAroRv6Z8xSOVg3CSmZeVgZHJv4aMYQiBiIIZW68YN5ywHxOf6-LdrhqN24NPSLYUNPKGxCkUkWg-VV-iulUqIkDxn3SKiX4zVA-jevbuUrsK3MeRHoJLqXMf8KnpPkf4ZLCTLdUNFyLyxFvqnm6QUAgpDrQ_rxhw",
      "e": "AQAB",
      "x5c": [
        "MIIC/jCCAeagAwIBAgIJAJjMBdn72zEjMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwODE4MTkzMzIzWhcNMjkwODE4MTkzMzIzWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiheTlbqieo0KzaFBTz6dxt9LAENfe2d0ywnpvZx9khzurtApc4ThWBpeoTBI6UpReeCwyW6DQjJSGCqHwe+wqRdgoiMc0PV+danrh0px38x2KL/j7VoHR0hlQBYOpp5GBMdz+Nsc80wBtHAqxz7Nno3qYNTXUvwZ2LSbbvgoPXrh0zhLlSrn2gAroRv6Z8xSOVg3CSmZeVgZHJv4aMYQiBiIIZW68YN5ywHxOf6+LdrhqN24NPSLYUNPKGxCkUkWg+VV+iulUqIkDxn3SKiX4zVA+jevbuUrsK3MeRHoJLqXMf8KnpPkf4ZLCTLdUNFyLyxFvqnm6QUAgpDrQ/rxhwIDAQABoyEwHzAdBgNVHQ4EFgQUVOgUL8kBykezhhopW+Whb6mtqhowDQYJKoZIhvcNAQELBQADggEBAHqgxwjA+D7HzUT5mNRf8uizC9SkHdLXULyZTehFbSYEB3SYXZIbUWJcd88uaDUZ7CxPaojg6swmuB9XnzcZh8Gpa5y1ThSWvmPvUIDq1e03OJzhsC6dPmwoysmLvovngkGJ/AWURK7zRot53DLYWt0d/0KPDExPsovava0LeQjU8bLxHwQ9+8JFFxoN/aE85TQc8Whw+xZ4NX6BdTiucY8ku0smJLblwWui+mGyFSfDFW9r/r8LMUifgXTkaLYz4Z7sQdPFsdSoAR2jOzKhiz6acyQ1R4QUheN0EBmuPSsbu9M+E6tSYD+4q3GmQWtXjVagJoKPos0DIhe785YZjtI="
      ]
    }

As you can see both have same kid.

This is my backend code:

 private const string KeyString = @"MIIC/jCCAeagAwIBAgIJAJjMBdn72zEjMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwODE4MTkzMzIzWhcNMjkwODE4MTkzMzIzWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiheTlbqieo0KzaFBTz6dxt9LAENfe2d0ywnpvZx9khzurtApc4ThWBpeoTBI6UpReeCwyW6DQjJSGCqHwe+wqRdgoiMc0PV+danrh0px38x2KL/j7VoHR0hlQBYOpp5GBMdz+Nsc80wBtHAqxz7Nno3qYNTXUvwZ2LSbbvgoPXrh0zhLlSrn2gAroRv6Z8xSOVg3CSmZeVgZHJv4aMYQiBiIIZW68YN5ywHxOf6+LdrhqN24NPSLYUNPKGxCkUkWg+VV+iulUqIkDxn3SKiX4zVA+jevbuUrsK3MeRHoJLqXMf8KnpPkf4ZLCTLdUNFyLyxFvqnm6QUAgpDrQ/rxhwIDAQABoyEwHzAdBgNVHQ4EFgQUVOgUL8kBykezhhopW+Whb6mtqhowDQYJKoZIhvcNAQELBQADggEBAHqgxwjA+D7HzUT5mNRf8uizC9SkHdLXULyZTehFbSYEB3SYXZIbUWJcd88uaDUZ7CxPaojg6swmuB9XnzcZh8Gpa5y1ThSWvmPvUIDq1e03OJzhsC6dPmwoysmLvovngkGJ/AWURK7zRot53DLYWt0d/0KPDExPsovava0LeQjU8bLxHwQ9+8JFFxoN/aE85TQc8Whw+xZ4NX6BdTiucY8ku0smJLblwWui+mGyFSfDFW9r/r8LMUifgXTkaLYz4Z7sQdPFsdSoAR2jOzKhiz6acyQ1R4QUheN0EBmuPSsbu9M+E6tSYD+4q3GmQWtXjVagJoKPos0DIhe785YZjtI=";

 [HttpPost("validate-token3")]
 public IActionResult DecodeValidToken()
 {
     try
     {
         string accessToken = "MyTokenHereWhichICannotPasteHere";

         var certificate = new X509Certificate2(Convert.FromBase64String(KeyString));
         var rsa = certificate.GetRSAPublicKey();
         var rsaSecurityKey = new RsaSecurityKey(rsa);
         var tokenHandler = new JwtSecurityTokenHandler();

         var validationParameters = new TokenValidationParameters
         {
             ValidateIssuerSigningKey = true,
             IssuerSigningKey = rsaSecurityKey,
             ValidateIssuer = false,
             ValidateAudience = false
         };

         var principal = tokenHandler.ValidateToken(accessToken, validationParameters, out SecurityToken validatedToken);
         return Ok(principal.Claims);
     }
     catch (SecurityTokenInvalidSignatureException ex)
     {
         return Unauthorized(new { Message = "Invalid token signature", Details = ex.Message });
     }
     catch (Exception ex)
     {
         return StatusCode(500, new { Message = "Internal server error", Details = ex.Message });
     }
 }

Even though my token has kid still I am getting this error:

Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: 'IDX10503: Signature validation failed. Token does not have a kid. Keys tried: 'Microsoft.IdentityModel.Tokens.RsaSecurityKey, KeyId: '', InternalId: 'y16ADo8cPNxOAHfa262kqg7euKagSoVzzY5Wo4CzT5c'. , KeyId: '. Number of keys in TokenValidationParameters: '1'. Number of keys in Configuration: '0'. Exceptions caught:
''.
token: '[Security Artifact of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/SecurityArtifactLogging.]'. See https://aka.ms/IDX10503 for details.'

I am trying to solve this issue from last 4 days, but still no luck yet, even I have tried a different approach in which we are getting keys from Microsoft and passing all the keys to validate


Solution

  • I generated access token and tried to validate the token using the same code as below:

    enter image description here

    Got the same error:

    enter image description here

    Note that: Microsoft Graph API tokens are not meant to be validated.

    Hence to resolve the error, check the below:

    Expose an API and add a scope:

    enter image description here

    Grant API permission:

    enter image description here

    Make sure to generate access token by passing scope as api://***/ScopeName

    Now the token is valid and got the output successfully:

    class Program
    {
        private const string KeyString = @"xxx";
    
        static void Main(string[] args)
        {
            string accessToken = "MyTokenHereWhichICannotPasteHere"; 
    
            try
            {
                var certificate = new X509Certificate2(Convert.FromBase64String(KeyString));
                var rsa = certificate.GetRSAPublicKey();
                var rsaSecurityKey = new RsaSecurityKey(rsa);
                var tokenHandler = new JwtSecurityTokenHandler();
    
                var validationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = rsaSecurityKey,
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
    
                var principal = tokenHandler.ValidateToken(accessToken, validationParameters, out SecurityToken validatedToken);
                Console.WriteLine("Token is valid. Claims:");
                foreach (var claim in principal.Claims)
                {
                    Console.WriteLine($"{claim.Type}: {claim.Value}");
                }
            }
            catch (SecurityTokenInvalidSignatureException ex)
            {
                Console.WriteLine($"Unauthorized: Invalid token signature. Details: {ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }
    }
    

    enter image description here

    Refer this GitHub blog Azure AD Authentication, Next-Auth JWT and .NET Core Web API · nextauthjs/next-auth · Discussion #5996 · GitHub where the access token is generated in the frontend using Next-Auth.