Home > Software design >  Manually validating a token from an OIDC provider without `well-known` metadata in Asp Net Core 3.1
Manually validating a token from an OIDC provider without `well-known` metadata in Asp Net Core 3.1

Time:09-23

I'm using a flow with Openid, where I redirect my user to another provider, to take care about login, and after this login I receive the code in my /login/callback?code=xxxx URL.

So, the JWT is generated using the code but I can't validate it. I don't have well-known endpoint in my STS, I need to configure manually in this way:

    services.AddAuthorization(cfg =>
        {
            cfg.AddPolicy("MyPolicy", cfgPolicy =>
            {
                cfgPolicy.AddRequirements().RequireAuthenticatedUser();
                cfgPolicy.AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme);
            });
        }).AddAuthentication(cfg =>
        {
            cfg.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            cfg.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie()
        .AddOpenIdConnect(cfg =>
        {
            cfg.ClientId = authenticationConfig.ClientId;
            cfg.ClientSecret = authenticationConfig.ClientSecret;
            cfg.ResponseType = "code";
            cfg.CallbackPath = "/login/callback";
            cfg.Scope.Clear();
            cfg.Scope.Add("openid");
            
            cfg.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = "https://myissuer"
            };

            cfg.Configuration = new OpenIdConnectConfiguration
            {
                AuthorizationEndpoint = "https://mysts/api/oauth/authorize",
                TokenEndpoint = "https://mysts/api/oauth/token",
                UserInfoEndpoint = "https://mysts/api/oauth/token_info"
            };
        });

Some important points:

  1. I have a token instrospection endpoint, to validate my token (token_info endpoint).
  2. I don't have an default endpoint to return public keys (jwks). My endpoint is always a concat from some values, something like that --> https://mysts/offline/jwks/{kid}/{clientid}, so this is dynamic and depends of the token.
  3. I don't have a well-known endpoint.

CodePudding user response:

You can't validate it because, ASP.NET Core tries to validate the JWT signature by default. However, since you've set up the metadata yourself, and not given a JwksUri, it has no way of getting the public key of the OIDC provider to validate the signature against.

You can specify the JwksUri, or turn off signature validation (NOT SECURE), or validate it yourself. If you have access to the public key, somehow, use options.SecurityTokenValidator and implement a ISecurityTokenValidator for custom validation.

Here's a (non-tested) implementation that fetches the JWKS dynamically and validates a token:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication()
        .AddOpenIdConnect(options => {
            options.SecurityTokenValidator = new MyJwtValidator(Configuration);
        });
}

class MyJwtValidator: ISecurityTokenValidator
{
    private readonly IConfiguration _configuration;
    private readonly HttpClient _httpClient;
    private readonly JwtSecurityTokenHandler _tokenHandler;

    public MyJwtValidator(IConfiguration configuration)
    {
        _configuration = configuration;
        _tokenHandler = new JwtSecurityTokenHandler();
        _httpClient = new HttpClient
        {
            BaseAddress = new Uri(_configuration.GetSection("OidcProvider").Get<string>())
        };
    }

    public bool CanReadToken(string securityToken) => true;

    public ClaimsPrincipal ValidateToken(
        string securityToken,
        TokenValidationParameters validationParameters,
        out SecurityToken validatedToken
    )
    {
        // parse the token (without validating) to extract a value
        var parsedToken = new JwtSecurityToken(securityToken);
        var keyId = parsedToken.Claims.First(c => c.Type == "kid").Value;
        
        // fetch JWKS and validate the token
        var clientId = _configuration.GetSection("OidcProvider:ClientId").Get<string>();
        var jwks = _httpClient.GetStringAsync($"url/to/jwks/{keyId}/{clientId}").Result;
        // jwks == "{ keys: [..."
        var signingKeys = new JsonWebKeySet(jwks).GetSigningKeys();

        return _tokenHandler.ValidateToken(securityToken, new TokenValidationParameters
        {
            IssuerSigningKeys = signingKeys
        }, out validatedToken);
    }

    public bool CanValidateToken { get; } = true;
    public int MaximumTokenSizeInBytes { get; set; } = int.MaxValue;
}

It's doing sync-over-async, which is frowned upon, but you might wanna cache the keys to avoid paying the penalty.

  • Related