Celdus
Celdus

Reputation: 2110

How to validate jwt token from different issuer

I'm using actionable messages (with Outlook web app) to call an Logic App. Therefore I am getting an Bearer token in the request:

"Action-Authorization": "Bearer eyJ0eXAi..."

Callstack: Outlook web app -> Logic App -> my endpoint hosted in azure

Now I tried to validate the token with jwt.io but getting an Issue that the Signature is invalid. So I tried to validate it in c# with the JwtSecurityTokenHandler.

I tried to add https://substrate.office.com/sts/ to the issuer list, but it seems like the validation don't even get there.

I'm using the following code to validate the jwt token issued by office.com:

bool IsAuthorized(HttpActionContext actionContext)
        {
            var valid = base.IsAuthorized(actionContext);

            // Custom handle for Bearer token, when invalid from base-class
            if (!valid && actionContext.Request.Headers.Authorization.Scheme == "Bearer")
            {
                var jwt = actionContext.Request.Headers.Authorization.Parameter;
                var th = new JwtSecurityTokenHandler();
                var sjwt = th.ReadToken(jwt) as JwtSecurityToken;                

                var validationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = false,
                    //IssuerSigningToken = sjwt,
                    ValidateActor = false,
                    ValidateAudience = false,
                    ValidateIssuer = true,
                    ValidateLifetime = true,
                    ValidIssuers = new[] { "https://substrate.office.com/sts/" },
                    ValidAudiences = new[] {"https://XXX.logic.azure.com"}
        };

                SecurityToken validatedToken;
                try
                {
                    th.ValidateToken(jwt, validationParameters, out validatedToken);
                }
                catch (Exception ex)
                {
                    return false;
                }
            }

            return valid;
        }

Here is my JWT token:

JWT Token

I am getting the exception:

IDX10500: Signature validation failed. Unable to resolve SecurityKeyIdentifier: 'SecurityKeyIdentifier
    (
    IsReadOnly = False,
    Count = 2,
    Clause[0] = X509ThumbprintKeyIdentifierClause(Hash = 0x818...),
    Clause[1] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause
    )
', ...

Even though I set ValidateIssuerSigningKey = false.

Is there a way to accept https://substrate.office.com/sts/ as a valid issuer?

Upvotes: 3

Views: 13126

Answers (2)

wlscaudill
wlscaudill

Reputation: 525

If you're using an OAuth spec JWT then the key to validate the ticket can be retrieved via 'http://auth.myApplication.com/.well-known/jwks.json' and the built in JwtSecurityTokenHandler can be used even if you're using the older System.IdentityModel.Tokens.Jwt which does not have the calls natively built in.

OWIN Example:

public static class AppBuilderExtensions
{
    /// <summary>
    /// Use JWT Authentication.
    /// </summary>
    /// <param name="app">The <see cref="IAppBuilder"/> to use.</param>
    /// <param name="audiences">The expected audiences.  Will be validated in the token.</param>
    /// <returns>The <see cref="IAppBuilder"/> instance.</returns>
    public static IAppBuilder UseJwtTokenAuthentication(
        this IAppBuilder app,
        params string[] audiences)
    {
        var validationParameters = new TokenValidationParameters
                                   {
                                       ValidateLifetime = true,
                                       ValidateAudience = true,
                                       ValidateIssuer = true,
                                       ValidateActor = true,
                                       ValidateIssuerSigningKey = true,
                                       ValidAudiences = audiences,
                                       ValidIssuer = Constants.Issuer,
                                       IssuerSigningKeyResolver = Constants.GetSigningKey,
                                   };

        var tokenHandler = new JwtSecurityTokenHandler();
        app.UseJwtBearerAuthentication(
            new JwtBearerAuthenticationOptions
            {
                AuthenticationMode = AuthenticationMode.Active,
                AllowedAudiences = audiences,
                TokenHandler = tokenHandler,
                TokenValidationParameters = validationParameters,
            });

        return app;
    }
}

public static class Constants
{
    /// <summary>
    /// The authentication issuer.
    /// </summary>
    public const string Issuer = "https://auth.myApplication.com/"; // custom domain for Auth0

    private static readonly OpenIdConnectKeyResolver KeyResolver;

    static Constants() { KeyResolver = new OpenIdConnectKeyResolver(Issuer, TimeSpan.FromHours(1), TimeSpan.FromSeconds(10)); }

    /// <summary>
    /// Gets the <see cref="IssuerSigningKeyResolver" /> delegate to provide to <see cref="JwtSecurityTokenHandler" />.
    /// </summary>
    public static IssuerSigningKeyResolver GetSigningKey => KeyResolver.GetSigningKey;
}

Direct call Example:

        void ValidateToken(string authenticationToken)
        {
            var issuer = "https://auth.myApplication.com/"; // custom domain for Auth0
            var audiences = new[]
                            {
                                "https://secure.myApplication.com/",
                            };

            using (var signingKeyResolver = new OpenIdConnectKeyResolver(issuer, TimeSpan.Zero, TimeSpan.FromSeconds(10)))
            {
                var tokenHandler = new JwtSecurityTokenHandler();
                var validationParameters = new TokenValidationParameters
                                           {
                                               ValidateLifetime = true,
                                               ValidateAudience = true,
                                               ValidateIssuer = true,
                                               ValidateActor = true,
                                               ValidateIssuerSigningKey = true,
                                               ValidAudiences = audiences,
                                               ValidIssuer = issuer,
                                               IssuerSigningKeyResolver = signingKeyResolver.GetSigningKey,
                                           };

                var principal = tokenHandler.ValidateToken(authenticationToken, validationParameters, out var securityToken);

                if (principal == null || securityToken == null)
                {
                    throw new UnauthorizedAccessException();
                }
            }
        }

This requires a signing key resolver, the example is using the second option as that's what I needed:

It is baked into Microsoft.IdentityModel.Tokens (see example here: https://github.com/auth0/auth0-aspnet-owin/blob/master/src/Auth0.Owin.OpenIdConnectSigningKeyResolver/OpenIdConnectSigningKeyResolver.cs).

It is a little more work if using System.IdentityModel.Tokens.Jwt (see example here: https://github.com/NaosProject/Naos.Auth/blob/main/Naos.Auth.Recipes.Jwt/OpenIdConnectKeyResolver.cs) I wrote a piece of code to accomplish this which can be copied directly (MIT license) or installed as a mix-in using package 'Naos.Auth.Recipes.Jwt'.

Upvotes: 0

Celdus
Celdus

Reputation: 2110

The exception says that the "Signature validation failed". To resolve this problem we can't just add the wanted valid issuer to ValidIssuers, we need the to verify that the token is issued from the issuer itself.

Especially for this case with office.com being the issuer I found the expected key (JWK - JSON Web Key) here: https://substrate.office.com/sts/common/discovery/keys (also https://substrate.office.com/sts/common/.well-known/openid-configuration)

Here is the working code:

bool IsAuthorized(HttpActionContext actionContext)
        {
            var valid = base.IsAuthorized(actionContext);

            // Custom handle for Bearer token, when invalid from base-class
            if (!valid && actionContext.Request.Headers.Authorization.Scheme == "Bearer")
            {
                var jwt = actionContext.Request.Headers.Authorization.Parameter;
                var th = new JwtSecurityTokenHandler();

                var validationParameters = new TokenValidationParameters
                {
                    ValidateAudience = false,
                    ValidateLifetime = true,
                    ValidateIssuer = true,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new JsonWebKey(GetJWK()),
                    ValidIssuers = new[] { "https://substrate.office.com/sts/" }
                };

                Microsoft.IdentityModel.Tokens.SecurityToken validatedToken;
                try
                {
                    var claims = th.ValidateToken(jwt, validationParameters, out validatedToken);
                    valid = true;
                }
                catch (Exception ex)
                {
                    valid = false;
                }
            }

            return valid;
        }

        // Get the token from configuration
        private string GetJWK()
        {
            return ConfigurationManager.AppSettings["ida:jwks_json"];
        }

In the appsettings I put the RSA key from the website for validating the token, it looks like:

{"kty":"RSA","use":"sig","kid":"gY...","x5t":"gY...","n":"2w...","e":"AQAB","x5c":["MII..."]}

Upvotes: 2

Related Questions