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:
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"
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 = "***";
});