Force the authentication middleware to only accept encrypted tokens - c#

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.

Related

JwtSecurityTokenHandler().WriteToken(token) throwing error in hosted environment

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.

ASP.NET Core 3.1 Web API Role based authorization not working

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.

.Net Core 3.1 Jwt Validate Issuer does not seem to work

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();

Postman returning with 401 unothorized when valid token is passed while working with asp.net core 3.0

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}

Ignoring signature in JWT

I have an web application that is using OpenId Connect. I created a self signed certificate but it is still not signed by a CA.
How can I ignore the signature validation?
This is what I have so far:
SecurityToken validatedToken = null;
var tokenHandler = new JwtSecurityTokenHandler {
Configuration = new SecurityTokenHandlerConfiguration {
CertificateValidator = X509CertificateValidator.None
},
};
TokenValidationParameters validationParams =
new TokenValidationParameters()
{
ValidAudience = ConfigurationManager.AppSettings["Audience"],
ValidIssuer = ConfigurationManager.AppSettings["Issuer"],
AudienceValidator = AudienceValidator,
ValidateAudience = true,
ValidateIssuer = true
};
return tokenHandler.ValidateToken(jwtToken, validationParams, out validatedToken);
It throws the following exception:
IDX10500: Signature validation failed. Unable to resolve
SecurityKeyIdentifier: 'SecurityKeyIdentifier\r\n (\r\n
IsReadOnly = False,\r\n Count = 1,\r\n Clause[0] =
System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause\r\n
)\r\n', \ntoken:
'{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"issuer_rsaKey\"}.{\"iss\":...
Don't ignore the signature, this is dangerous!
Even if you use a self-signed certificate, you will be able to use the public key for signature validation.
Since you are using OpenId Connect, you should be able to get the public key for your signing certificate by heading over to /.well-known/jwks.
Then you can setup your validation parameters like this:
var certificate = new X509Certificate2(Convert.FromBase64String(yourPublicKeyGoesHere));
var validationParameters = new TokenValidationParameters {
IssuerSigningTokens = new[] { new X509SecurityToken(certificate) }
};
After that, you can call ValidateToken:
SecurityToken token;
var claimsPrincipal = handler.ValidateToken(encodedToken, validationParameters, out token);
You really want to ignore the signature?
Remember, if you do, how do you know someone didn't tamper with the data inside the token? You could easily decode the base64 url encoded payload and change the subject. And if you rely on that in your application, you'll be in trouble (hint: someone accessing someone else data)
You REALLY, REALLY want to ignore it?
You can use ReadToken and just skip every validation there is:
var badJwt = new JwtSecurityTokenHandler()
.ReadToken(encodedMaliciousToken) as JwtSecurityToken;
DON'T do that though, it's bad practice.
public TokenValidationParameters CreateTokenValidationParameters()
{
var result = new TokenValidationParameters
{
ValidateIssuer = false,
ValidIssuer = ValidIssuer,
ValidateAudience = false,
ValidAudience = ValidAudience,
ValidateIssuerSigningKey = false,
//IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey)),
//comment this and add this line to fool the validation logic
SignatureValidator = delegate(string token, TokenValidationParameters parameters)
{
var jwt = new JwtSecurityToken(token);
return jwt;
},
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
};
result.RequireSignedTokens = false;
enter code here
return result;
}

Categories

Resources