ASP.NET API not validating token

Hi, I am configuring as ASP.NET API, and I am having issues getting my token to validate. I have configured my validation like so:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = jwtIssuer;
options.Audience = jwtAudience;
});
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = jwtIssuer;
options.Audience = jwtAudience;
});
I get my token from a Nuxt front end and pass it as an authorization header on request to the ASP.NET backend. I have checked my JWT here: https://jwt.io/ and everything appears to be in order. I can use it just fine on my front end. When I run my controller, the HttpContext.User is always null. I have also tried this configuration setup:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidateIssuer = true,
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
options.IncludeErrorDetails = true;
});
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidateIssuer = true,
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
options.IncludeErrorDetails = true;
});
Any help would be greatly appreciated
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
7 Replies
skywalker-kiwi#02131
skywalker-kiwi#02131OP•7mo ago
I also have this service which can be used to validate the token:
public async Task<ClaimsPrincipal?> ValidateTokenAsync(string token, CancellationToken cToken = default)
{
try
{
ArgumentNullException.ThrowIfNullOrEmpty(token);

cToken.ThrowIfCancellationRequested();
string newToken = token.StartsWith("Bearer ") ? token[7..] : token;
OpenIdConnectConfiguration config = await _configurationManager.GetConfigurationAsync(cToken);
TokenValidationParameters validationParameters = new()
{
RequireSignedTokens = true,
ValidAudience = _configuration.GetSection("KindeSettings")["Audience"],
ValidateAudience = true,
ValidIssuer = _configuration.GetSection("KindeSettings")["Issuer"],
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("KindeSettings")["Key"]))
};

ClaimsPrincipal result = null;
int tries = 0;
while(result == null && tries <= 1)
{
try
{
JwtSecurityTokenHandler handler = new();
result = handler.ValidateToken(newToken, validationParameters, out _);
}
catch (SecurityTokenSignatureKeyNotFoundException)
{
_configurationManager.RequestRefresh();
tries++;
}
catch (SecurityTokenException)
{
return null;
}
}
return result;
}
catch(Exception ex)
{
throw;
}
}
public async Task<ClaimsPrincipal?> ValidateTokenAsync(string token, CancellationToken cToken = default)
{
try
{
ArgumentNullException.ThrowIfNullOrEmpty(token);

cToken.ThrowIfCancellationRequested();
string newToken = token.StartsWith("Bearer ") ? token[7..] : token;
OpenIdConnectConfiguration config = await _configurationManager.GetConfigurationAsync(cToken);
TokenValidationParameters validationParameters = new()
{
RequireSignedTokens = true,
ValidAudience = _configuration.GetSection("KindeSettings")["Audience"],
ValidateAudience = true,
ValidIssuer = _configuration.GetSection("KindeSettings")["Issuer"],
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("KindeSettings")["Key"]))
};

ClaimsPrincipal result = null;
int tries = 0;
while(result == null && tries <= 1)
{
try
{
JwtSecurityTokenHandler handler = new();
result = handler.ValidateToken(newToken, validationParameters, out _);
}
catch (SecurityTokenSignatureKeyNotFoundException)
{
_configurationManager.RequestRefresh();
tries++;
}
catch (SecurityTokenException)
{
return null;
}
}
return result;
}
catch(Exception ex)
{
throw;
}
}
The issue with this approach is that the OpenIdConnectConfiguration result has missing values: in particular the claims, response types, etc. It does return some values, and so it could be possible that the result isn't deserialising correctly?
string issuer = _configuration.GetSection("KindeSettings")["Issuer"];
_configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{issuer}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
string issuer = _configuration.GetSection("KindeSettings")["Issuer"];
_configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{issuer}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
This is how I have configured my ConfigurationManager
onderay
onderay•7mo ago
Thanks for all the detail. I will get more experienced team members to jump in. Are you able to do the following? Check the Authority and Audience URLs: Ensure that the jwtIssuer and jwtAudience values are correctly set to match the values used when the token was issued. These should match the values configured in your Kinde settings. Ensure the Signing Key is Correct: If you are using a symmetric key, make sure the jwtKey is correct. If you are using an asymmetric key, you will need to configure the IssuerSigningKeyResolver to fetch the public key from the Kinde JWKS endpoint.
skywalker-kiwi#02131
skywalker-kiwi#02131OP•7mo ago
The jwt issuer is my base kindle url. The jwt audience is base_url/api The signing key has been received from the .well-known/jwks endpoint *kinde
leo_kinde
leo_kinde•7mo ago
Hi @skywalker-kiwi#02131 , when you say HttpContext.User is null in the controller, is this on a controller/action which has auth enforced with something like the [Authorize] attribute? The issue is it is passing verification, but not populating? In terms of configuration, generally you do not need to explicitly specify the OpenID configuration, setting the Authority is sufficient for the JwtBearer handling to discover it (as it is on a standard path). Normally where JWKS is not specified it will request the config and the JWKS for you and cache the results, though you may want to specify them to avoid the initial requests. One thing you may want to specify on the JwtBearer config is:
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "sub";
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "sub";
This will populate the Name field o the User with the Kinde user identifier, though without it I've not seen it cause the User object to not be populated at all.
skywalker-kiwi#02131
skywalker-kiwi#02131OP•7mo ago
There is an Authorize attribute on the controller, yes. In terms of configuration, it has updated as per the suggestions made here: https://discord.com/channels/1070212618549219328/1181922100806680646 It looks like:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.Authority = jwtIssuer;
options.Audience = jwtAudience;
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "sub";
options.IncludeErrorDetails = true;
});
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.Authority = jwtIssuer;
options.Audience = jwtAudience;
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "sub";
options.IncludeErrorDetails = true;
});
When I send the request through both postman and swagger, I get a 401 unauthorised response, along with the message "signing key not found". I have checked my token to make sure the iss and aud values are set, and they are. My jwt issuer is https://summt.kinde.com and my audience is https://summt.kinde.com/api This is the error that occurs when I have an authorize attribute on the controller. When there isn't one, I just get a user object where the claims are empty and the IsAuthorized value is false The issue has been resolved: I added this method to my program.cs
async Task<RSA> GetRSA(string domain)
{
using (HttpClient client = new HttpClient())
{
string fullEndpoint = $"{domain}/.well-known/jwks.json";
string jwksJson = await client.GetStringAsync(fullEndpoint);
JsonDocument jwks = JsonDocument.Parse(jwksJson);

JsonElement key = jwks.RootElement.GetProperty("keys")[0];
byte[] n = Base64UrlEncoder.DecodeBytes(key.GetProperty("n").GetString());
byte[] e = Base64UrlEncoder.DecodeBytes(key.GetProperty("e").GetString());

RSAParameters rsaParameters = new RSAParameters { Modulus = n, Exponent = e };
RSA rsa = RSA.Create(rsaParameters);
return rsa;
}

}
async Task<RSA> GetRSA(string domain)
{
using (HttpClient client = new HttpClient())
{
string fullEndpoint = $"{domain}/.well-known/jwks.json";
string jwksJson = await client.GetStringAsync(fullEndpoint);
JsonDocument jwks = JsonDocument.Parse(jwksJson);

JsonElement key = jwks.RootElement.GetProperty("keys")[0];
byte[] n = Base64UrlEncoder.DecodeBytes(key.GetProperty("n").GetString());
byte[] e = Base64UrlEncoder.DecodeBytes(key.GetProperty("e").GetString());

RSAParameters rsaParameters = new RSAParameters { Modulus = n, Exponent = e };
RSA rsa = RSA.Create(rsaParameters);
return rsa;
}

}
This gets the RSA from the jwks endpoint. I realised that I had one-half of my signing key stored, hence the key issue. More so a nooby thing on my end, not being 100% on how JWTs work 🫠 I also removed the Authorize attribute from my controller as this still causes errors.... not sure if that is intended or not. But now I can hit my controller and the user is authenticated with valid claims returning builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(async options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtIssuer, ValidAudience = jwtAudience, IssuerSigningKey = new RsaSecurityKey(rsa) }; options.SaveToken = true; options.Authority = jwtIssuer; options.Audience = jwtAudience; options.MapInboundClaims = false; options.TokenValidationParameters.NameClaimType = "sub"; options.IncludeErrorDetails = true; }); This was the final config in Program.cs
leo_kinde
leo_kinde•7mo ago
I'm glad you have something working. However, normally you shouldn't need to do this as the libraries should do it for you. If you are receiving 401 The signature key was not found, there is a known issue where mismatched versions IdentityModel can cause this. If you have both Microsoft.AspNetCore.Authentication.JwtBearer installed with a version of System.IdentityModel.Tokens.Jwt beyond 7.3.1 this can happen. If this is the case you should either downgrade System.IdentityModel.Tokens.Jwt or install the same version of Microsoft.IdentityModel.Protocols.OpenIdConnect so JwtBearer is using the same version of all dependencies.
skywalker-kiwi#02131
skywalker-kiwi#02131OP•7mo ago
Awesome, thanks for that. I’ll give it a go and see if that helps too
Want results from more Discord servers?
Add your server