Validating JWT getting a strange “ Unable to match key kid” error - c#

I am trying to validate a valid JWT using this code below but am getting a strange error
"IDX10501: Signature validation failed. Unable to match key:
kid: 'System.String'.
Exceptions caught:
'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'."
Here is my validation method
ClaimsPrincipal principal = null;
var token = "JWT GOES HERE"
try
{
string sec = "000uVmTXj5EzRjlnqruWF78JQZMT";
var securityKey = new SymmetricSecurityKey(System.Text.Encoding.Default.GetBytes(sec));
var now = DateTime.UtcNow;
SecurityToken securityToken;
string tokenIssuer = "https://MyIssuer.com";
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidIssuer = tokenIssuer,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
principal = handler.ValidateToken(token, validationParameters, out securityToken); <---Errors here
}
Here is the value of my JWT. I am using the correct issuer
{
"alg": "RS256",
"kid": "dev",
"x5t": "Sm7aAUSt4Fdv7X1b9jQDf8XwbvQ",
"pi.atm": "xxe8"
}.{
"scope": [],
"client_id": "ClientABC",
"iss": "https://MyIssuer.com",
"jti": "1JLDz",
"sub": "ClientABC",
"exp": 1601609852
}.[Signature]
What am I missing here? Is the the SymmetricSecurityKey since this algorithm is RS256? Am I missing something in my TokenValidationParameter?
Update
After futher investigation I am getting the error of.
IDX10501: Signature validation failed. Unable to match key:
kid: 'dev'.
Exceptions caught:
'System.NotSupportedException: IDX10634: Unable to create the SignatureProvider.
Algorithm: 'RS256', SecurityKey: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '', InternalId: 'TdfWgWjCVeM60F3C5TOogJuka1aR5FA_xchwhY9MHH4'.'
is not supported. The list of supported algorithms is available here: https://aka.ms/IdentityModel/supported-algorithms
at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateSignatureProvider(SecurityKey key, String algorithm, Boolean willCreateSignatures, Boolean cacheProvider)

try to use SecurityAlgorithms.HmacSha256
Example when you issue the token:
Users user = _context.Users.FirstOrDefault(c => c.UserName == userName && c.Password == password);
if(user == null)
{
return Unauthorized();
}
Claim[] claims = new Claim[]
{
new Claim("Id", user.Id.ToString()),
new Claim("Name", user.Name),
new Claim("Email", user.Email),
};
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("000uVmTXj5EzRjlnqruWF78JQZMT"));
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var token = new
JwtSecurityToken(
"MyProject",
"MyClient",
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: signingCredentials);
return Ok(new JwtSecurityTokenHandler().WriteToken(token));
If you are using .net core app, then in Startup.cs, in ConfigureServices method write this code to validate the token:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = "MyClient",
ValidIssuer = "MyProject",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("000uVmTXj5EzRjlnqruWF78JQZMT"))
};
});
Also don't forget to add the following lines to the Configure method in Startup.cs
app.UseAuthentication();
app.UseAuthorization();

The issue is that you are trying to use a symmetric key with an asymmetric algorithm. The RSA algorithm requires both a public and a private key.
Try using a symmetric algorithm instead, like HS256 (HMAC-SHA256).

Having come here looking for answers myself, let me share another scenario which may crop up in a production setting. So we were having this same cryptic authentication error. In our case, the client application was using our development environment's Token service to generate their bearer token and then sending it to production.
I don't know why it didn't outright just deny instead of failing on "Unable to match Key kid", but using the production token service solved it for us. So be sure to verify that first, since it's a relatively quick and simple thing to do.

Related

Azure AD B2C JWT Token Signature Validation Failed

I am trying to validate a valid JWT using this code below but am getting a strange error
[Error] IDX10516: Signature validation failed. Unable to match key:
kid: '-KI3Q9nNR7bRofxmeZoXqbHZGew'.
Number of keys in TokenValidationParameters: '1'.
Number of keys in Configuration: '0'.
Exceptions caught:
'[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
token: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. Valid Lifetime: 'True'. Valid Issuer: 'False'
Code:
I had seen a lot of reference links on this issue but still not getting what am I missing?
I tried to test locally, its worked perfectly but after deployed to Azure Function its failing.
Configuration Settings:
"JwtSettings:TenantName": "OSHDev.onmicrosoft.com",
"JwtSettings:TenantId": "5492b240-96ee-44a1-bdcb-fa0ba0200111",
"JwtSettings:AadB2cInstance": "https://OSHDev.b2clogin.com/{0}/v2.0/",
"JwtSettings:OpeinConfigUrl":"https://OSHDev.b2clogin.com/OSHDev.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/v2.0/.well-known/openid-configuration",
public async Task<ClaimsPrincipal> ValidateAccessToken(string accessToken, JwtSettings jwtSettings, ILogger logger)
{
var audience = jwtSettings.Audience;
var tenant = jwtSettings.TenantName;
var tenantid = jwtSettings.TenantId;
var aadb2cInstance = jwtSettings.AadB2cInstance;
var openidconfigurl = jwtSettings.OpeinConfigUrl;
//Debugging purposes only, set this to false for production
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = false;
ConfigurationManager<OpenIdConnectConfiguration> configManager =
new ConfigurationManager<OpenIdConnectConfiguration>(
openidconfigurl,
new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config;
config = await configManager.GetConfigurationAsync();
//Microsoft Identity to override claim names . If we remove below code line, "sub" claim will not be visible. Its visible under "nameidentifier"
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler? tokenValidator = new JwtSecurityTokenHandler();
// Initialize the token validation parameters
TokenValidationParameters validationParameters = new TokenValidationParameters
{
// App Id URI and AppId of this service application are both valid audiences.
ValidateAudience = true,
ValidAudiences = new[] { audience },
ValidateIssuer= true,
ValidIssuers = new List<string>()
{
string.Format(CultureInfo.InvariantCulture, aadb2cInstance, tenantid),
string.Format(CultureInfo.InvariantCulture, aadb2cInstance, tenant)
},
ValidateIssuerSigningKey = true,
// Support Azure AD V1 and V2 endpoints.
IssuerSigningKeys = config.SigningKeys,
RequireSignedTokens = true,
//Debugging purposes only, set this to true for production
ValidateLifetime = true
};
try
{
//Validate JwTToken and return Claims Prinicpals
ClaimsPrincipal? claimsPrincipal = tokenValidator.ValidateToken(accessToken, validationParameters, out SecurityToken securityToken);
return claimsPrincipal;
}
catch (Exception ex)
{
logger.LogError(ex.Message);
}
return null;
}
Like the error said, your token validator try to find the public key that is used to sign your token in the jwks_uri, you can find it in your well-known URL.
In each token there is a header "KID", this header indicate the ID of the public key used to sign your token.
When i check your jwks_uri there is no KID with ID :
"-KI3Q9nNR7bRofxmeZoXqbHZGew"
But:
"veMP2TrHLgs4XvKYJhumvhPW6O-WpbdSGqKdetRmvxI"

Minimal API NET 7: JSON Web Token (JWT) generation failed

I am trying to use the minimal API in NET 7 and failing to generate JWT tokens.
I have collected the following from several sites and youtube videos. Unfortunately I couldn't find a JWT authentication for NET 7 in any post or video, so I had to combine something from NET 6 (maybe that's where the error comes from?).
to keep from building the config, I used the new function in NET 7:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();
in appsettings.json I then added the following:
"Jwt": {
"Key": "gfdsgf789087fgfdsgfds087807dfgfdsfg",
"Issuer": "https://localhost:7285",
"Audience": "https://localhost:7285"
}
Then I created an endpoint to request the token:
app.MapPost("/security/createToken",
[AllowAnonymous] (User user) =>.
{
if (user.UserName == "user" && user.Password == "123")
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.UserName)
};
var token = new JwtSecurityToken
(
issuer: builder.Configuration["Jwt:Issuer"],
audience: builder.Configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddDays(60),
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
SecurityAlgorithms.HmacSha256)
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
return Results.Ok(tokenString);
}
return Results.Unauthorized();
});
now when I run the API then I get the token via Swagger. Then if I want to use it on another endpoint:
app.MapGet("/secret",
[Authorize(AuthenticationSchemes =
JwtBearerDefaults.AuthenticationScheme)] () => $"Hello You. This is asecret!!!");
...then I get the error message in Swagger:
Error: response status is 401
www-authenticate: Bearer error="invalid_token",error_description="The signature key was not found".
When I look at tokens via jwt.io I get "Invalid Signature".
Under PAYLOAD:DATA I see my data but without KEY !!!!
{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "user",
"nbf": 1670946236,
"exp": 1676130236,
"iss": "https://localhost:7285",
"aud": "https://localhost:7285"
}
As I see, KEY is not inserted in the token!? I assume that I forgot something, but what?
Can anyone see what the error is and why my token is not valid?
Thanks
I have finally deleted the Jwt definition from appsettings.json and filled the options as they were given in NET 6 examples.
builder.Services.AddAuthentication().AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "https://localhost:7285",
ValidAudience = "https://localhost:7285",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey#345"))
};
});
Now it works.

Validate an externally created JWT with no key Id using System.IdentityModel.Tokens.Jwt

Given that there is no way to influence the JWT as it is created externally, how can I verify the signature of the token when it does not contain a kid.
This is the relevant code:
private bool ValidateToken(string authToken)
{
var tokenHandler = new JwtSecurityTokenHandler();
var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secretKey));
var validationParameters = new TokenValidationParameters()
{
ValidAudience = "clientid",
ValidIssuer = _issuer,
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKey = new SymmetricSecurityKey(hmac.Key),
};
try
{
tokenHandler.ValidateToken(authToken, validationParameters, out SecurityToken validatedToken);
}
catch (Exception ex)
{
//handle exception
}
return true;
}
However this throws an exception:
{
"IDX10503: Signature validation failed.
Token does not have a kid.
Keys tried: '[PII of type 'System.Text.StringBuilder' is hidden.
For more details, see https://aka.ms/IdentityModel/PII.]'.
Number of keys in TokenValidationParameters: '1'.
Number of keys in Configuration: '0'.
Exceptions caught:
'[PII of type 'System.Text.StringBuilder' is hidden.
For more details, see https://aka.ms/IdentityModel/PII.]'.
token: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden.
For more details, see https://aka.ms/IdentityModel/PII.]'."
}
Why does Microsoft insist on there being a key id in the token? Is there a way to ignore the missing kid?
Example token (edited to remove data):
header:
{
"alg": "HS256",
"typ": "JWT",
"ver": 1,
"typ_2": "ref"
}
payload:
{
"jti": "<token_id>",
"client_id": "<client_id>",
"client_name": "<client>",
"ref_token": "<ref_token>",
"ref_token_type": "Full",
"zone": "<zone>",
"endusertype": "system",
"nbf": 1643639617,
"exp": 1643643217,
"iat": 1643639617,
"iss": "<issuer>"
}
Edit: removed IssuerSigningKeys to avoid confusion.
After validating the encoding and scratching my head for quite some time I managed to get a hold of the external party and they confirmed that the key was indeed wrong - the "key" was only intended to be used in the api-call in which the consumer asks for a token, to "authenticate" the consumer. They did not include a kid in the jwt as they did not intend for consumers to validate the token.
This does however not answer the question.

JWT validation - sts.windows.net vs login.windows.net: Why are the issuers not consistent?

I am developing an application that is spread across multiple Function App running on .net5.
I need to authenticate HTTP calls between functions. To do so, I am using Azure Active Directory. I have created a registered application in my tenant and generated a new secret. Whenever Function1 needs to contact Function2, I retrieve an access token from AAD, like this:
var stringContent = new StringContent($"grant_type=client_credentials&client_id={Uri.EscapeUriString(clientId)}&client_secret={Uri.EscapeUriString(clientSecret)}&scope={Uri.EscapeUriString(scope)}", Encoding.UTF8, "application/x-www-form-urlencoded");
string tokenUrl = "https://login.microsoftonline.com/57cc008d-ba7c-4887-acfd-93089c705640/oauth2/v2.0/token";
HttpResponseMessage result = await _httpClient.PostAsync(tokenUrl, stringContent);
string content = await result.Content.ReadAsStringAsync();
With this call, I get a token which has the following information:
{
"typ": "JWT",
"alg": "RS256",
"x5t": "XXXXXXXXXXXXX_KXEg",
"kid": "XXXXXXXXXXXXX_KXEg"
}.{
"aud": "api://f87cc6ac-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"iss": "https://sts.windows.net/57cc008d-XXXX-XXXX-XXXX-XXXXXXXXXXXX/",
"iat": 1628443017,
"nbf": 1628443017,
"exp": 1628446917,
"aio": "E2ZgYOg4qv7qZsTRKv5v+XXXXXXXXXXX",
"appid": "f87cc6ac-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"appidacr": "1",
"idp": "https://sts.windows.net/57cc008d-XXXX-XXXX-XXXX-XXXXXXXXXXXX/",
"oid": "39b2e6b8-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"rh": "0.AVsAjQDMV3y6h0is_ZMInHBWQKzGfPhOtBZEj3l003jzIFFbAAA.",
"sub": "39b2e6b8-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"tid": "57cc008d-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"uti": "U7pMFAzw_XXXXXXXXXXXXX",
"ver": "1.0"
}.[Signature]
Now, the access token is used as a bearer token when Function2 is called. Function2 gets the token from the Authorization header and tries to validate it, like this:
ConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ("https://login.microsoftonline.com/57cc008d-ba7c-4887-acfd-93089c705640/v2.0/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConnectConfiguration = await configurationManager.GetConfigurationAsync();
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = true,
ValidIssuers = new[]
{
openIdConnectConfiguration.Issuer
},
ValidateIssuerSigningKey = true,
IssuerSigningKeys = openIdConnectConfiguration.SigningKeys,
RequireExpirationTime = true,
ValidateLifetime = true,
RequireSignedTokens = true,
};
JwtSecurityTokenHandler securityTokenHandler = new();
if (!securityTokenHandler.CanReadToken(cleanedBearerToken))
throw new ArgumentException("Unable to read the token. It is malformed.");
try
{
ClaimsPrincipal claimsPrincipal = securityTokenHandler.ValidateToken(cleanedBearerToken, validationParameters, out SecurityToken _);
return claimsPrincipal;
}
catch (Exception unhandledException)
{
throw new AuthenticationException("The token could not be validated.", unhandledException);
}
Aside from the issuer, the validation works. With this setup, the issuer can't be validated. The token indicates that the issuer is from sts.windows.net. However, the OpenID configuration states that the issuer must be login.microsoft.com.
To make it work, I have fall back to something like this which is less ideal since I have to ignore a parameter returned by the openid-configuration endpoint (which must know better that I):
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = true,
ValidIssuers = new[]
{
// --> Force the use of the following issuer!!
"https://sts.windows.net/57cc008d-ba7c-4887-acfd-93089c705640/"
},
ValidateIssuerSigningKey = true,
IssuerSigningKeys = openIdConnectConfiguration.SigningKeys,
RequireExpirationTime = true,
ValidateLifetime = true,
RequireSignedTokens = true,
};
Question
Is the override of issuer a good practice? If not, what can I do to get consistent issuer from Azure Active Directory and avoid specifying the issuer myself?
The issuer value depends on the Access token version. If you view the Sample Tokens in jwt.ms, you will see the issuer for Access token V1 is https://sts.windows.net/... and for Access token V2 it is https://login.microsoft.com/...
Also, if you check OIDC metadata endpoint v1 (https://login.microsoftonline.com/common/.well-known/openid-configuration), issuer will be sts.windows.net and for OIDC metadata endpoint v2 (https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration), issuer will be login.microsoft.com.
Update
In registered application's manifest, look for the value of accessTokenAcceptedVersion:
Change it to 2 and the issuer will become login.microsoft.com instead of sts.windows.net.

ASP.NET Core 3.1 JWT signature invalid when using AddJwtBearer()

Problem: AddJwtBearer() is failing, but verifying the token manually works.
I'm trying to generate and verify a JWT with an asymmetric RSA algo.
I can generate the JWT just fine using this demo code
[HttpPost("[action]")]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> JwtBearerToken() {
AppUser user = await userManager.GetUserAsync(User);
using RSA rsa = RSA.Create(1024 * 2);
rsa.ImportRSAPrivateKey(Convert.FromBase64String(configuration["jwt:privateKey"]), out int _);
var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
var jwt = new JwtSecurityToken(
audience: "identityapp",
issuer: "identityapp",
claims: new List<Claim>() {new Claim(ClaimTypes.NameIdentifier, user.UserName)},
notBefore: DateTime.Now,
expires: DateTime.Now.AddHours(3),
signingCredentials: signingCredentials
);
string token = new JwtSecurityTokenHandler().WriteToken(jwt);
return RedirectToAction(nameof(Index), new {jwt = token});
}
I'm also able to verify the token and it's signature using the demo code below
[HttpPost("[action]")]
[ValidateAntiForgeryToken]
public IActionResult JwtBearerTokenVerify(string token) {
using RSA rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(configuration["jwt:privateKey"]), out int _);
var handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = handler.ValidateToken(token, new TokenValidationParameters() {
IssuerSigningKey = new RsaSecurityKey(rsa),
ValidAudience = "identityapp",
ValidIssuer = "identityapp",
RequireExpirationTime = true,
RequireAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
}, out SecurityToken securityToken);
return RedirectToAction(nameof(Index));
}
But, verification fails (401) when hitting an endpoint protected with
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Error message from HTTP header: Bearer error="invalid_token", error_description="The signature is invalid"
My JWT bearer auth configuration is here
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
using var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(Configuration["jwt:privateKey"]), out int _);
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters() {
IssuerSigningKey = new RsaSecurityKey(rsa),
ValidAudience = "identityapp",
ValidIssuer = "identityapp",
RequireExpirationTime = true,
RequireAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
};
});
I can easily make it work using a symmetric key and HmacSha256 - but that's not what I'm looking for.
UPDATE
I've written the exception to the response, and this is what I get:
IDX10503: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.RsaSecurityKey, KeyId: '', InternalId: '79b1afb2-0c85-43a1-bb81-e2accf9dff38'. , KeyId:
'.
Exceptions caught:
'System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'RSA'.
at System.Security.Cryptography.RSAImplementation.RSACng.ThrowIfDisposed()
at System.Security.Cryptography.RSAImplementation.RSACng.GetDuplicatedKeyHandle()
at System.Security.Cryptography.RSAImplementation.RSACng.VerifyHash(ReadOnlySpan`1 hash, ReadOnlySpan`1 signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
at System.Security.Cryptography.RSAImplementation.RSACng.VerifyHash(Byte[] hash, Byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
at Microsoft.IdentityModel.Tokens.AsymmetricAdapter.VerifyWithRsa(Byte[] bytes, Byte[] signature)
at Microsoft.IdentityModel.Tokens.AsymmetricAdapter.Verify(Byte[] bytes, Byte[] signature)
at Microsoft.IdentityModel.Tokens.AsymmetricSignatureProvider.Verify(Byte[] input, Byte[] signature)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(Byte[] encodedBytes, Byte[] signature, SecurityKey key, String algorithm, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
'.
token: '{"alg":"RS256","typ":"JWT"}.{"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":"mail#mail.com","nbf":1582878368,"exp":1582889168,"iss":"identityapp","aud":"identityapp"}'.
UPDATE - Working solution
So, I guess I figured it out from the exception message. The RSA security key was being prematurely disposed.
I extracted the key creation from the AddJwtBearer(), and used dependency injection instead.
This seems to work just fine. But I'm unsure if this is good practice.
// Somewhere futher up in the ConfigureServices(IServiceCollection services) method
services.AddTransient<RsaSecurityKey>(provider => {
RSA rsa = RSA.Create();
rsa.ImportRSAPrivateKey(
source: Convert.FromBase64String(Configuration["jwt:privateKey"]),
bytesRead: out int _);
return new RsaSecurityKey(rsa);
});
// Chaining onto services.AddAuthentication()
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
SecurityKey rsa = services.BuildServiceProvider().GetRequiredService<RsaSecurityKey>();
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters() {
IssuerSigningKey = rsa,
ValidAudience = "identityapp",
ValidIssuer = "identityapp",
RequireExpirationTime = true,
RequireAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
};
});
While your solution apparently works, it has two issues, for which I'll provide solutions.
The first issue is that the RSA you create implements IDisposable, but the disposing is not handled properly within the life cycle (here: transient) since the RSA is not the immediate result of a factory. This results in a resource leak where undisposed RSA instances might accumulate throughout the running time of you host (and even beyond the "official" shutdown).
The second issue is that your use of BuildServiceProvider creates a whole new service provider additionally to the one implicitly used by the rest of the code. In other words, this creates a new dependency injection container in parallel to the "canonical" one.
The solution goes as follows. (Note I can't test your scenario perfectly, but I have something similar in my own application.) I'll start with the key part in the middle:
services
.AddTransient(provider => RSA.Create())
.AddTransient<SecurityKey>(provider =>
{
RSA rsa = provider.GetRequiredService<RSA>();
rsa.ImportRSAPrivateKey(source: Convert.FromBase64String(Configuration["jwt:privateKey"]), bytesRead: out int _);
return new RsaSecurityKey(rsa);
});
Note how the RSA gets its own factory. So it is disposed at the right time. The security key too has its own factory, which looks up the RSA when needed.
Somewhere above the code I just showed, you would do something like this:
services
.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
.Configure<SecurityKey>((options, signingKey) =>
{
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = signingKey,
ValidAudience = "identityapp",
ValidIssuer = "identityapp",
RequireExpirationTime = true,
RequireAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
};
});
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer();
Note how the TokenValidationParameters were moved inside a Configure method! The signingKey there will be the SecurityKey you registered on the dependency injection container! Thus we get rid of BuildServiceProvider.
Caution: Microsoft's IdentityModel seems to have a bug where using an RSA, disposing it, and then using another RSA fails under certain circumstances for the second RSA instance. This is for example the underlying issue behind this SO question. You might run into that issue independently of my solution. But you may sidestep that issue by adding your RSA (not necessarily the security key) with AddSingleton rather than AddTransient.

Categories