I am writing a service which receives POSTs from another service, which includes an Authorization header containing a bearer token. This token is obtained independently from an OpenID Connect server (Keycloak in our dev environment, but not necessarily in production). Our service does not need to obtain or issue tokens; it merely needs to be able to validate them.
We are using .NET Framework 4.8 with self-hosted ASP.NET WebApi (OWIN 4, etc).
Configuration-wise, the information we have is:
The intent is that we obtain the issuer public key dynamically, from the OpenID server's metadata endpoint 'http://keycloak:8080/auth/realms/demo/.well-known/openid-configuration'. Currently I have something like:
WebApp.Start(startOptions, builder => {
var config = ...
// ... Set up routes etc ...
config.Filters.Add(new HostAuthenticationFilter("Bearer"));
builder.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "js-client",
Authority = "http://keycloak:8080/auth/realms/demo/",
RequireHttpsMetadata = false,
SignInAsAuthenticationType = "Bearer",
});
builder.UseWebApi(config);
}));
The controller action looks like:
[HttpGet]
[HttpPost]
[Authorize]
public IHttpActionResult Receive([FromBody] string dto) => Ok();
Currently, it always returns 401 Unauthorized with a message 'Authorization has been denied for this request' irrespective of the validity of the token.
Wireshark reveals that our service never tries to contact the Keycloak server for OIDC metadata, so I guess that the authorisation handler is not even finding the token.
I've looked at UseJwtBearerAuthentication and UseOAuthAuthorizationServer too, but those seem to want more information than just an OIDC endpoint (unsurprising, really) or they need custom provider implementations.
This does not seem to be such an unusual use case that I need to implement my own validator, so presumably I'm missing something? Google searches turn up hundreds of examples which seem to relate only to ASP.NET Core or don't cover non-interactive use cases.
I managed to make progress on this by inspecting the source of OpenIdConnectAuthenticationMiddleware.
The JwtBearer middleware handles validation of the issuer, but needs to know the public key. Since I need to avoid configuring this directly, I need to ask the OIDC server for it.
This can be accomplished using a ConfigurationManager, which should deal with caching, etc for us:
private JwtBearerAuthenticationOptions GetJwtBearerTokenAuthenticationOptions(string issuer, IConfigurationManager<OpenIdConnectConfiguration> configurationManager)
{
return new JwtBearerAuthenticationOptions
{
Realm = "demo",
TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
// ... etc ...
IssuerSigningKeyResolver = (token, securitytoken, kid, validationparameters) =>
configurationManager.GetConfigurationAsync(CancellationToken.None).GetAwaiter().GetResult().SigningKeys,
ValidIssuer = issuer.TrimEnd('/'),
}
};
}
(The resolver delegate can't be async unfortunately, so I can't await this properly.)
The ConfigurationManager can be constructed like this (based on the internals of OpenIdConnectAuthenticationMiddleware):
private IConfigurationManager<OpenIdConnectConfiguration> GetOIDCConfigurationManager(string issuer)
{
var httpClient = new HttpClient(new WebRequestHandler());
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Demo OpenIdConnect middleware");
httpClient.Timeout = TimeSpan.FromMinutes(1);
httpClient.MaxResponseContentBufferSize = 10485760L;
var httpRetriever = new HttpDocumentRetriever(httpClient) { RequireHttps = false };
return new ConfigurationManager<OpenIdConnectConfiguration>($"{issuer}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever(), httpRetriever);
}
These can then be used as follows:
const string issuer = "http://keycloak:8080/auth/realms/demo/";
var configurationManager = GetOIDCConfigurationManager(issuer);
builder.UseJwtBearerAuthentication(GetJwtBearerTokenAuthenticationOptions(issuer, configurationManager));
It all seems to work, although I'd very much like to know if there's a simpler way...?
Obviously, anyone using this in production should RequireHttps = true
instead.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With