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.
Related
I can't read token claims from Bearer JWT token.
Login is working, the HTTP request comes with a valid JWT token to the backend.
Login returns the token.
Here is my code on server side:
Program.cs
builder.Services.AddAuthentication(m => {
//m.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
m.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
m.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(conf => {
conf.RequireHttpsMetadata = false;
conf.SaveToken = true;
conf.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = Configuration["JWT-Issuer"],
ValidAudience = Configuration["JWT-Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT-Key"])),
ClockSkew = TimeSpan.Zero,
};
});
When I uncomment this line //m.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; postman says unauthorized. When it is kept commented, authorization succeeds.
Token is generated here.
GenerateToken Method:
private object GenerateJwtToken(string Id, string email, ApplicationUser appUser, string appUserRole, string FirstName)
{
List<Claim> claims = null;
claims = new List<Claim> {
new Claim(JwtRegisteredClaimNames.Email,email),
new Claim(JwtRegisteredClaimNames.Jti, appUser.Id),
new Claim("Role",appUserRole),
new Claim("UserName",appUser.UserName),
new Claim("TEMP", FirstName)
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT-Key"]));
var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expire = DateTime.Now.AddDays(Convert.ToDouble(_configuration["JWT-Expiry"]));
var token = new JwtSecurityToken(
issuer: _configuration["JWT-Issuer"],
audience: _configuration["JWT-Issuer"],
claims: claims,
expires: expire,
signingCredentials: cred
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
When JWT Bearer token is passed to the API call decorated with [Authorize] and tested with debugger it shows that custom claim like TEMP is not present in the User.Claims List.
Reading Claims From Token
string Email = User.Claims.SingleOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")).Value;
string FirstName= User.Claims.SingleOrDefault(x => x.Type.Equals("TEMP")).Value;
Here, the email can be read successfully but I can't read the FirstName from the token. In fact User.Claims doesn't have the FirstName claim (I mean all the custom claims leaving the Registered JWT Default ClaimTypes), it only has the default token parameters which are emailaddress, id, role etc.
What should I do?
should I need to create custom authentication scheme for this purpose?
Internally in AddJwtBearer, there are some remapping done by the Token hander, where claims are renamed, like
email -> http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
This mapping can be turned of using:
// Or set this flag to false
.AddJwtBearer(opt =>
{
...
opt.MapInboundClaims = false;
});
or setting:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
The actual mapping can be found here
However, I am not sure why the TEMP claim is not found. What claims do you actually see in the authenticated User in your controller? Some claims in some cases are also ignored.
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.
I am making custom Single-Sign-On service with asp.net core. I made login server which returns JWT token in the cookie with validation. I would like to connect my web application to the login server, so the web application recognize if the user is logged in or not by login server.
Is it possible to do it with startup.cs setting in the web application? And if so how can I make it? I don't want to put the token checking process in every actions.
Thanks in advance
As far as I know, in asp.net core we could provide the jwtbear authentication validation to check if the user login or not. All these things is provided at the web application not login server. If you send the token to the login server how the web application know the user is login or not.
Normally, we will directly check the user token is valid or not on the web application side.
We could set the login server and web application use the same Issuer, Audience and SecretKey.
Then we you could directly validate the user in the web application instead of login server.
More details, you could refer to below codes:
Login server generate token:
public static string GetToken3()
{
var key = Encoding.ASCII.GetBytes
("YourKey-2374-OFFKDI940NG7:56753253-tyuw-5769-0921-kfirox29zoxv");
//Generate Token for user
var JWToken = new JwtSecurityToken(
issuer: "http://localhost:45092/",
audience: "http://localhost:45092/",
claims: new List<Claim>
{
new Claim(ClaimTypes.Name, "Users2222" )
},
notBefore: new DateTimeOffset(DateTime.Now).DateTime,
expires: new DateTimeOffset(DateTime.Now.AddDays(1)).DateTime,
//Using HS256 Algorithm to encrypt Token
signingCredentials: new SigningCredentials
(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
);
string token = new JwtSecurityTokenHandler().WriteToken(JWToken);
return token;
}
web application validate token:
//Provide a secret key to Encrypt and Decrypt the Token
var SecretKey = Encoding.ASCII.GetBytes
("YourKey-2374-OFFKDI940NG7:56753253-tyuw-5769-0921-kfirox29zoxv");
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "http://localhost:45092/",
ValidAudience = "http://localhost:45092/",
IssuerSigningKey = new SymmetricSecurityKey(
SecretKey),
ClockSkew = TimeSpan.Zero
}
);
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.
I am generating a JWT token in my WindowsService using IdentityModel.Tokens.Jwt, like so:
private JwtSecurityToken GetJwtToken()
{
var symmetricSecurityKey = new SymmetricSecurityKey(Convert.FromBase64String(_secretKey));
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256Signature);
return new JwtSecurityToken(
"myIssuer",
expires: DateTime.Now.AddMinutes(15),
signingCredentials: signingCredentials
);
}
Then, I am writing that token with JwtSecurityTokenHandler and sending it in a request to a WebAPI controller:
//some code...
// _tokenHandler below is a JwtSecurityTokenHandler
_httpClient.DefaultRequestHeaders.Add("jwtToken", _tokenHandler.WriteToken(GetJwtToken()));
HttpResponseMessage response = await _httpClient.GetAsync(url);
//...
And on the API side, I am trying to validate the token:
public bool Authenticate(string token)
{
if (string.IsNullOrEmpty(token))
throw new ArgumentEmptyException(nameof(token));
TokenValidationParameters parameters = new TokenValidationParameters
{
ValidIssuer = "myIssuer",
ValidateIssuer = true,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(SecretKey))
};
try
{
new JwtSecurityTokenHandler().ValidateToken(token, parameters, out SecurityToken validatedToken);
return true;
}
catch (SecurityTokenException)
{
return false;
}
}
}
This throws an error below:
IDX12741: JWT: '[PII is hidden. For more details, see
https://aka.ms/IdentityModel/PII.]' must have three segments (JWS) or
five segments (JWE).'
And an example of a generated token, which actually looks like two tokens sent at once, which is baffling me:
eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzk2OTc3NzUsImlzcyI6Im15SXNzdWVyIn0.g9Mw7FijNzAzGofll5E44B8cJtOozln3nUjHKgnkdTs,
eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzk2OTc3ODAsImlzcyI6Im15SXNzdWVyIn0.Noc3lC0h_ryH6axlQJ2Kk2a8wcp5eQ0QhBqidfjuujo
Any advice?
The JWT token was generated correctly, the problem was in a shared instance of HttpClient. Each consecutive call added to the DefaultRequestHeaders jwtToken value.
When I added logic to reset the value before adding new token, it worked:
_httpClient.DefaultRequestHeaders.Remove("jwtToken"); // new
_httpClient.DefaultRequestHeaders.Add("jwtToken", _tokenHandler.WriteToken(GetJwtToken()));