So as stated in the title, I am having issues with the token validation in dotnet application. Here is the program.cs
code:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add configuration for Ocelot
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
// Add Ocelot service
builder.Services.AddOcelot(builder.Configuration);
builder.Services.AddHttpClient();
// Keycloak configuration
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer("KeyCloak", options =>
{
options.Authority = "http://localhost:5002/realms/test-realm";
options.Audience = "api-gw";
options.MetadataAddress = "http://keycloak:5002/realms/test-realm/.well-known/openid-configuration";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "http://localhost:5002/realms/test-realm",
ValidateAudience = true,
ValidAudience = "api-gw",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
Console.WriteLine("Token Validated.");
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
Console.WriteLine($"Token Validation Failed: {context.Exception.Message}");
return Task.CompletedTask;
}
};
});
// Add authorization
builder.Services.AddAuthorization();
var app = builder.Build();
// Use authentication middleware before Ocelot
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpsRedirection();
// Use Ocelot API Gateway
app.UseOcelot().Wait();
// Run the app
app.Run();
What I though would happen is that the Microsoft.AspNetCore.Authentication.JwtBearer
would automatically fetch the public keys from the KeyCloak URL from MetadataAddress
. Both the KeyCloak and the API Gateways are deployed as Docker containers. I have checked the connectivity, the containers can communicate. Additionally, when checking in local browser the http://localhost:5002/realms/test-realm/.well-known/openid-configuration
I am receiving the proper JSON object with a link to the jwks_uri
. However, I am getting the following error:
info: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0]
2024-12-16 20:44:49 requestId: 0HN8U187GSHNG:00000003, previousRequestId: No PreviousRequestId, message: 'The path '/api/weatherforecast' is an authenticated route! AuthenticationMiddleware checking if client is authenticated...'
2024-12-16 20:44:50 Token Validation Failed: IDX10500: Signature validation failed. No security keys were provided to validate the signature.
2024-12-16 20:44:50 warn: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0]
2024-12-16 20:44:50 requestId: 0HN8U187GSHNG:00000003, previousRequestId: No PreviousRequestId, message: 'Client has NOT been authenticated for path '/api/weatherforecast' and pipeline error set. Request for authenticated route '/api/weatherforecast' was unauthenticated;'
2024-12-16 20:44:50 warn: Ocelot.Responder.Middleware.ResponderMiddleware[0]
2024-12-16 20:44:50 requestId: 0HN8U187GSHNG:00000003, previousRequestId: No PreviousRequestId, message: 'Error Code: UnauthenticatedError Message: Request for authenticated route '/api/weatherforecast' was unauthenticated errors found in ResponderMiddleware. Setting error response for request path:/api/weatherforecast, request method: GET'
For the sake of completeness, here is the ocelot.json
. The /login
route goes through another small auth-service
container, but I am able to receive the token just fine. The problem is with the authentication for the /weatherforecast route.
{
"Routes": [
{
"DownstreamPathTemplate": "/login",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "auth-service",
"Port": 8080
}
],
"UpstreamPathTemplate": "/login",
"UpstreamHttpMethod": ["Post"]
},
{
"DownstreamPathTemplate": "/todos/{id}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "jsonplaceholder.typicode.com",
"Port": 443
}
],
"UpstreamPathTemplate": "/api/todos/{id}",
"UpstreamHttpMethod": ["Get"]
},
{
"DownstreamPathTemplate": "/weatherforecast",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "temp-service",
"Port": 8080
}
],
"UpstreamPathTemplate": "/api/weatherforecast",
"UpstreamHttpMethod": ["Get"],
"AuthenticationOptions": {
"AuthenticationProviderKey": "KeyCloak",
"AllowedScopes": []
}
}
],
"GlobalConfiguration": {
"BaseUrl": "https://localhost:5000"
}
}
Any ideas on what could be causing the problem? From what I can deduct, the keys are not correctly loaded from the KeyCloak URL, thus the JWT library cannot validate the token without any public key.
2
Answers
Finally, I have managed to find the issue. I will post it here for others if such a problem arises anytime soon for somebody.
The main problem was the mismatch of the URLs (especially the IPs) of the
openid-configuration
inside the KeyCloak endpoint. Beforehand I have specified theKC_HOSTNAME=localhost
for KeyCloak's Docker compose deployment. While it worked for the first call as specified in theoptions.MetadataAddress = "http://keycloak:5002/realms/test-realm/.well-known/openid-configuration";
, thejwks_uri
was pointing to thelocalhost
instead of thekeycloak
IP address (the KeyCloak's container name), thus the second HTTP call did not succeed. I managed to find this by deploying a distributed tracing telemetry inside the system using OpenTelemetry and Jaeger.So changing all values of the
KC_HOSTNAME
,options.Authority
and theValidIssuer
to a single IP address (the containers name)keycloak
fixed the error.I would try to figure out what the KID claim in the token is and what keys can be found via the JWKS endpoint and see if the key found in the tokens matches what is found in the JWKS endpoint.
I just wrote a blog post IdentityServer in Docker Containers that some of the troubleshooting steps you can do.
In it, I show how to add a BackChannelListener, to allow you to capture the traffic between the client and the auth service, like this:
Then you can add it to JwtBearer like