Search code examples
.net-coreoauth-2.0asp.net-core-webapiopenid-connectasp.net-authorization

ASP.NET Core 6 Web Api Authorization missing signature key in access token


How can I configure an ASP.NET Core 6 Web API with OAuth2 authorization to use the openid system's jwks endpoint to validate an access token?

My API works fine with oauth2 when I validate the claims "manually", but fails with http 401 unauthorized response header WwwAuthenticate:

Bearer error="invalid_token",error_description="The signature key was not found"

I am using Microsoft.AspNetCore.Authentication.JwtBearer v6.0.20. I think the problem is that the ID token has a signature key id, but on the Access token is null:

enter image description here

According to the team that owns openid (I am in a corporate environment) the system conforms to "pingfederate" and it's by design the signature key is not present.

They suggested I use the jwks endpoint to validate the access token, but I don't know how to configure this in .NET

My program.main - truncated for brevity:

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

...
builder.Services.AddSwaggerGen(opt =>
{
    opt.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri($"{config.GetValue<string>("authority")}/authorization"),
                TokenUrl = new Uri($"{config.GetValue<string>("authority")}/token")
            }
        }
    });
    opt.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Id = "oauth2",
                    Type = ReferenceType.SecurityScheme
                }
            }, new string[]{ }
        }
    });
});

builder.Services.AddAuthentication(authOpt =>
{
    authOpt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, opt =>
    {
        opt.Authority = config.GetValue<string>("authority");
        opt.MetadataAddress = $"{config.GetValue<string>("authority")}/.well-known/openid-configuration";
        opt.Audience = "myapp";
    });
    
builder.Services.AddAuthorization(auth =>
{
    auth.AddPolicy("canread", pol => pol.RequireClaim("scope", "read:weather"));
});

var app = builder.Build();

app.UseSwagger();
// configure swagger ui auth option
app.UseSwaggerUI(opt =>
{
    opt.OAuthAppName("my app");
    opt.OAuthClientId(config.GetValue<string>("clientid"));
    opt.OAuthClientSecret(config.GetValue<string>("clientsecret"));
    opt.OAuthAdditionalQueryStringParams(new Dictionary<string, string> { { "audience", "myapp" } });
    opt.OAuthScopeSeparator(" ");
    opt.OAuthUsePkce();
});

...
// custom middleware to inspect access token is passed
app.UseMiddleware<MessageInspector>();
app.UseAuthentication();
app.UseAuthorization();

...

If I use AuthorizeAttribute, it fails with signature key missing error, but if I verify the claims "manually" it works fine:

//[Authorize]
//[Authorize("canread", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet(Name = "GetWeatherForecast")]
public ActionResult<IEnumerable<WeatherForecast>> Get()
{
    var tokenStr = HttpContext.Request.Headers.Authorization.FirstOrDefault();
    var handler = new JwtSecurityTokenHandler();
    // auth token is in form "Bearer abcdefg1234567890..."
    var jwt = handler.ReadJwtToken(tokenStr.Remove(0, 7));

    if (!jwt.Claims.Where(x => x.Type == "scope" && x.Value == "read:weather").Any()) { return StatusCode(401); }

    return Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray());
}

I can see that the access token is passed in format "Bearer xxx"

enter image description here


Solution

  • I followed this solution and used IdentityModel.AspNetCore.OAuth2Introspection pkg

    The only difference is I couldn't use AddJwtBearer because it gave me an InvalidOperationException:

    Scheme already exists: Bearer

    Then use AddOAuth2Introspection and everything worked like a charm:

    .AddOAuth2Introspection(OAuth2IntrospectionDefaults.AuthenticationScheme, opt =>
    {
        opt.Authority = "***";
        opt.ClientId = "***";
        opt.ClientSecret = "***";
    });