In the context of a login page, I try to build a Jwt token in .NET Core.
The middleware is throwing an exception:
SecurityTokenNoExpirationException
Pretty sure everything is into place.
I followed : https://www.youtube.com/watch?edufilter=NULL&ab_channel=IAmTimCorey&v=9QU_y7-VsC8
here is the authentication controller part that creates the JWT:
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(settings.JwtSecret);
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, user.Email),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Role, user.RoleId.ToString()),
// token not valid before certain date, in our case, make it valid right away
new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeMilliseconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeMilliseconds().ToString()),
};
var header = new JwtHeader(
new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature
)
);
var payload = new JwtPayload(claims);
var securityToken = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();
here is the startup.cs part that initializes the JWT middleware:
var settings = settingsSection.Get<Settings>();
var key = Encoding.ASCII.GetBytes(settings.JwtSecret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
Tell me if you need more info.
Thanks for helping me on this.
The timestamps in a JWT are represented as seconds in UNIX epoch time, not as milliseconds.
RFC7519 defines the numeric date:
A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds.
Therefore use ToUnixTimeSeconds() instead of ToUnixTimeMilliseconds() when you create the claims.
Related
I have a .NET 5 project. I am trying to generate a JWT within. This line is giving me an error deployed to an IIS web server: JwtSecurityTokenHandler().WriteToken(token)
The encryption algorithm 'System.String' requires a key size of at least 'System.Int32' bits.
Key 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey', is of size: 'System.Int32'. (Parameter 'key')
There is commented code I have put in there to test that the values going into the token generation are OK, and the key is 16 characters long (I have tested with a lot more and still fails).
This works fine in my local environment.
Anyone know why this might be?
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtConfig.Value.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_jwtConfig.Value.Issuer,
_jwtConfig.Value.Audience,
claims,
expires: DateTime.UtcNow.AddDays(30),
signingCredentials: creds);
//return new JsonResult(new Dictionary<string, object>
// {
// { "secretkey", _jwtConfig.Value.SecretKey },
// { "creds", creds.ToString() },
// { "issuer", _jwtConfig.Value.Issuer },
// { "audience", _jwtConfig.Value.Audience },
// { "token", token.ToString() }
// });
return new JsonResult(new Dictionary<string, object>
{
{ "access_token", new JwtSecurityTokenHandler().WriteToken(token) },
});
Relevant section from Startup.cs:
services.Configure<JWTSettings>(Configuration.GetSection("JWTSettings"));
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.IncludeErrorDetails = true;
var secretKey = Configuration.GetSection("JWTSettings:SecretKey").Value;
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("JWTSettings:Issuer").Value,
ValidateAudience = true,
ValidAudience = Configuration.GetSection("JWTSettings:Audience").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
};
})
It seems you have error IDX10653.
But your exception message do not display real information. Please review this article for details. Could you add the code IdentityModelEventSource.ShowPII = true; and provide real exception message?
I think you will see real exception message and you can understand what happened.
Highly likely your SecretKey is less then 128 bits. In this case just try to use more long value for parameter JWTSettings:SecretKey in your configuration file. So similar scenario was described here.
It does not appear to be coding issue, more something on the .NET Configuration of the web host. Deploying the application as "Framework Dependent" results in the error, however "Self Contained" the code itself works.
Alright so I might have the solution but I'm not so sure so let me know if this works:
I've configured my JWT token directly in my Startup.cs like so:
Startup.cs
public static void AddIdentityServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
// Identity stuff
services.AddTransient<IAuthenticationService, AuthenticationService>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "JwtBearer";
options.DefaultChallengeScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidIssuer = configuration["JwtSettings:Issuer"],
ValidAudience = configuration["JwtSettings:Audience"],
ClockSkew = TimeSpan.FromMinutes(5),
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["JwtSettings:Key"]))
};
o.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.NoResult();
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
return c.Response.WriteAsync($"{c.Exception.ToString()} JwtBearerEvents in IdentityServiceRegistration");
},
OnChallenge = context =>
{
context.HandleResponse();
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject("401 Not authorized");
return context.Response.WriteAsync(result);
},
OnForbidden = context =>
{
context.Response.StatusCode = 403;
context.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject("403 Not authorized");
return context.Response.WriteAsync(result);
},
};
});
}
And here's my token generation method used by CreateUserAsync and other methods:
private async Task<JwtSecurityToken> GenerateToken(ApplicationUser user)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(JwtRegisteredClaimNames.Nbf,
new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp,
new DateTimeOffset(DateTime.Now.AddYears(10))
.ToUnixTimeSeconds().ToString())
}
.Union(userClaims);
var jwtSecurityToken = new JwtSecurityToken(
new JwtHeader(
new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("//secret key of 64 characters of capital letters and numbers")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
return jwtSecurityToken;
}
You should be able to generate a key from a website like this: SHA256 Generator
Let me know if it helps.
I couldn't understand why I always get 401 unauthorized where the user I logged in has a role of SuperAdmin. I tried looking at other solutions and projects and they seem identical to the code I have still does not work. I use Postman to test the API and in Authorization tab bearer token I pasted the token of the user I logged in and make a request on this API.
//API
[Route("create")]
[Authorize(Roles = "SuperAdmin")]
public async Task<IActionResult> RegisterUserAsync([FromBody] Request request)
{
return something;
}
//StartUp.cs
private void ConfigureAuth(IServiceCollection services)
{
services.AddIdentity<UserEntity, RoleEntity>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddRoles<RoleEntity>();
}
var key = Encoding.ASCII.GetBytes(jwtSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
//JWT
public string GenerateToken(UserEntity userEntity, IList<string> roles)
{
var token = string.Empty;
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.jwtOptions.GetJwtOptions().Secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, userEntity.UserName),
new Claim(ClaimTypes.GivenName, userEntity.FirstName),
new Claim(ClaimTypes.Surname, userEntity.LastName),
new Claim(ClaimTypes.NameIdentifier, userEntity.Id.ToString()),
new Claim(ClaimTypes.Role, roles.FirstOrDefault()) //SuperAdmin
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(this.jwtOptions.GetJwtOptions().ExpiresInMinutes),
SigningCredentials = credentials
};
token = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
return token;
}
You need to add app.UseAuthentication() before app.UseAuthorization() , the authentication middleware will handle the JWT bearer authentication , validate and decode token , at last write to user's principle.
Using .Net Core 3.1 WebApi with Jwt authentication seems to work fine unless we try to use ValidateIssuer and Validate Audience.
When we set these properties to true, we get an Unauthorized Http Status code.
We get the values for Audience and Issuer from our app settings, so we know they are the same.
Following is the code from our startup.cs:
//
// Configure JWT authentication from the 'jwtIssuerOptions' values in the appsettings.json file
//
Models.JwtIssuerOptions jwtSettings = _appConfiguration.GetSection("jwtIssuerOptions").Get<Models.JwtIssuerOptions>();
var keyBytes = Encoding.UTF8.GetBytes(jwtSettings.JwtSecret);
SymmetricSecurityKey symmetricSecurityKey = new SymmetricSecurityKey(keyBytes);
services.AddAuthentication(a =>
{
a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(b =>
{
b.RequireHttpsMetadata = false;
b.SaveToken = true;
b.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = jwtSettings.Audience ,
ValidIssuer = jwtSettings.Issuer ,
ValidateIssuerSigningKey = true,
IssuerSigningKey = symmetricSecurityKey,
TokenDecryptionKey = symmetricSecurityKey,
ValidateIssuer = true,
ValidateAudience = true
};
});
Following is the code from our Auth Helper that creates the Jwt:
private void CreateTheJWT(EndUserCredentials user)
{
var keyBytes = Encoding.UTF8.GetBytes(_jwtIssuerOptions.JwtSecret);
var symmetricSecurityKey = new SymmetricSecurityKey(keyBytes);
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256Signature);
var cryptoKey = new EncryptingCredentials(symmetricSecurityKey, JwtConstants.DirectKeyUseAlg, SecurityAlgorithms.Aes256CbcHmacSha512);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Name),
new Claim("EndUser", System.Text.Json.JsonSerializer.Serialize( user)),
}),
Expires = DateTime.UtcNow.AddMinutes(_jwtIssuerOptions.TimeoutMinutes),
Audience = _jwtIssuerOptions.Issuer,
Issuer = _jwtIssuerOptions.Audience,
NotBefore = DateTime.UtcNow.AddMinutes(-2),
IssuedAt = DateTime.UtcNow.AddMinutes(-1),
SigningCredentials = signingCredentials,
EncryptingCredentials = cryptoKey
};
foreach (string role in user.Roles)
{
tokenDescriptor.Subject.AddClaim(new Claim(ClaimTypes.Role, role));
}
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
}
When we set the offending properties to "false", everything works. Any help is appreciated!
it seems to me that your issuer and audiance are swapped. can you reassign in CreateTheJWT function?
Audience = _jwtIssuerOptions.Audience,
Issuer = _jwtIssuerOptions.Issuer,
If you are here like me and you didn't swap your issuer and audience.
Ensure you have this in your Startup.ConfigureServices.
app.UseAuthentication();
app.UseAuthorization();
I have following code in startup:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
};
});
In Authentication class of auth controller I have following code:
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_config.GetSection("AppSettings:Secret").Value);
var tokenDescriptor = new SecurityTokenDescripto
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim (ClaimTypes.NameIdentifier, user.Id.ToString ()),
new Claim (ClaimTypes.Name, user.Username)
}),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha512Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
This returns tokenString.
while passing url with token string string from postman returns 401 unauthorized.
The url is:
localhost:5000/api/hotel/?Authentication=Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwidW5pcXVlX25hbWUiOiJSdXBhayIsIm5iZiI6MTU3NTcxNDU2MCwiZXhwIjoxNTc1ODAwOTYwLCJpYXQiOjE1NzU3MTQ1NjB9.68L1K3cwRDz7CkL2MP6LdESYO0-2rG5wkyURLzvVIrkg_5XcPb1qVoP2pQgEB8DxbTNVCaBwLV_OsIg2GtTJXg
Can you just add your token to your request's header from postman, not from queryString.
key: Authentication value: Bearer {your jwt}
According this question, I'm using the snippet below, to sign and encrypt the JWToken.
var claims = new Claim[] { new SomeClaimes() };
var scKey = Encoding.UTF8.GetBytes("SOME KEY");
var ecKeyTemp = Encoding.UTF8.GetBytes("SOME OTHER KEY");
byte[] ecKey = new byte[256 / 8];
Array.Copy(ecKeyTemp, ecKey, 256 / 8);
var tokenDescriptor = new SecurityTokenDescriptor {
Subject = new ClaimsIdentity(claims),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(
scKey),
SecurityAlgorithms.HmacSha512),
EncryptingCredentials = new EncryptingCredentials(
new SymmetricSecurityKey(
ecKey),
SecurityAlgorithms.Aes256KW,
SecurityAlgorithms.Aes256CbcHmacSha512),
Issuer = "My Jwt Issuer",
Audience = "My Jwt Audience",
IssuedAt = DateTime.UtcNow,
Expires = DateTime.Now.AddDays(7),
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
var jwt = tokenHandler.WriteToken(token);
And here is my service registration:
services
.AddAuthentication(o => {
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(cfg => {
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters {
ValidIssuer = "My Jwt Issuer",
ValidAudience = "My Jwt Audience",
IssuerSigningKey = new SymmetricSecurityKey(SameKeyAsGenerating)),
TokenDecryptionKey = new SymmetricSecurityKey(SameKeyAsGenerating),
ClockSkew = TimeSpan.Zero,
RequireSignedTokens = true,
RequireExpirationTime = true,
SaveSigninToken = true,
ValidateActor = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidateTokenReplay = true,
};
});
The problem is that the authentication layer authenticates both encrypted and not-encrypted token as authorized. I mean when I create a token without the EncryptingCredentials (just a signed token, and not encrypted), the token is still valid and the request is authorized. The question is how to force authentication layer to only accept signed-encrypted tokens and reject just-signed-not-encrypted tokens?
UPDATE: The Solution:
Thanks to sellotape's answer, I implemented this JwtEncryptedSecurityTokenHandler:
public class JwtEncryptedSecurityTokenHandler : JwtSecurityTokenHandler {
[DebuggerStepThrough]
public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters, out SecurityToken validatedToken) {
if (string.IsNullOrWhiteSpace(token))
throw new ArgumentNullException(nameof (token));
if (validationParameters == null)
throw new ArgumentNullException(nameof (validationParameters));
if (token.Length > MaximumTokenSizeInBytes)
throw new ArgumentException(
$"IDX10209: token has length: '{token.Length}' which is larger than the MaximumTokenSizeInBytes: '{MaximumTokenSizeInBytes}'.");
var strArray = token.Split(new[] { '.' }, 6);
if (strArray.Length == 5)
return base.ValidateToken(token, validationParameters, out validatedToken);
throw new SecurityTokenDecryptionFailedException();
}
}
And used the new handler in Startup.ConfigureServices():
.AddJwtBearer(cfg => {
cfg.RequireHttpsMetadata = false;
// other configurations...
cfg.SecurityTokenValidators.Clear();
cfg.SecurityTokenValidators.Add(new JwtEncryptedSecurityTokenHandler());
});
For more explanation, see the accepted answer.
I'm sure there are a few ways to do this but how about:
Implement ISecurityTokenValidator, probably inheriting from SecurityTokenHandler; the required overrides are fairly simple and could largely be copied from JwtSecurityTokenHandler. Override ValidateToken() and throw if the JWT is not encrypted**.
Add the new handler/validator in the AddJwtBearer() configure-options action: cfg.SecurityTokenValidators.Add(new RequireEncryptedTokenHandler());
** If you look at the source for JwtSecurityTokenHandler.ValidateToken() (not sure exactly which version you're using but I think the message is the same), it seems that "is encrypted" is simply decided as "has 5 parts to it", which should be easy to implement (copy/paste) in your new handler.