I am trying to perform JWT validation for each action excepting the LogIn and Register actions, but I cannot find a way to do it in action filters, as i need the token and it's on this.Request.Headers.
This is the validation method which works fine:
try
{
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
string sToken = token.Substring(7, token.Length - 7);
if (!tokenHandler.CanReadToken(sToken))
{
return false;
}
JwtSecurityToken jwtToken = tokenHandler.ReadToken(sToken) as JwtSecurityToken;
if (jwtToken == null)
{
return false;
}
TokenValidationParameters parameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
RequireExpirationTime = true,
ValidAudience = "http://localhost",
ValidIssuer = "http://localhost",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.Default.GetBytes(StandardValues.SecretKey))
};
SecurityToken securityToken;
ClaimsPrincipal principal = tokenHandler.ValidateToken(sToken, parameters, out securityToken);
if (principal == null)
{
return false;
}
}
catch (Exception ex)
{
return false;
}
return true;
And I have multiple controllers on which I want to perform the validation.
Please let me know how can I perform this validation outside and before the actions, (into an action filter or another way) while sending the token as parameter for validation method.
In our ASP.Net WebApi we use the following for validating our tokens:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = $"https://{Configuration.GetValue<string>("AppServiceNameOutput")}",
ValidAudience = $"https://{Configuration.GetValue<string>("AppServiceNameOutput")}",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration.GetValue<string>("SigningKey"))),
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var tokenBlackList = context.HttpContext.RequestServices.GetRequiredService<ITokenBlackList>();
var tokenParser = context.HttpContext.RequestServices.GetRequiredService<ITokenParser>();
var bearer = context.HttpContext.Request.Headers["Authorization"];
if (String.IsNullOrEmpty(bearer))
{
bearer = context.Request.Query["access_token"];
}
var token = tokenParser.GetBearerTokenFromAuthHeaderString(bearer);
if (tokenBlackList.TokenIsBlackListed(token).Result)
{
context.Fail("Token has expired");
}
return Task.CompletedTask;
}
};
});
Then on each controller action we specify whether the endpoint should be authorised or not, and which policies are allowed access.
[Authorize(Policy = "ManagerOnly")]
[HttpPost]
public IActionResult Update([FromBody] UpdateAppRequest request)
Related
I'm able to successfully validate an access token issued by Azure AD B2C inside the controller using this code:
string discoveryEndpoint = "https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/<policyId>/v2.0/.well-known/openid-configuration";
ConfigurationManager<OpenIdConnectConfiguration> configManager = new(discoveryEndpoint, new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdconfig = configManager.GetConfigurationAsync().Result;
TokenValidationParameters validationParameters = new()
{
ValidateAudience = true,
ValidAudience = <ClientId>,
ValidateIssuer = true,
ValidIssuer = openIdconfig.Issuer,
IssuerSigningKeys = openIdconfig.SigningKeys,
ValidateLifetime = true
};
JwtSecurityTokenHandler tokenHandler = new();
tokenHandler.ValidateToken(accessToken, validationParameters, out SecurityToken validatedToken);
But can't make it work setting up authorization in Startup.cs like this
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg =>
{
<...the same code as above to get issuer and signing keys...>
cfg.TokenValidationParameters = new()
{
ValidateAudience = true,
ValidAudience = <ClientId>,
ValidateIssuer = true,
ValidIssuer = openIdconfig.Issuer,
IssuerSigningKeys = openIdconfig.SigningKeys,
ValidateLifetime = true
};
});
<...>
public void Configure(IApplicationBuilder appBuilder)
{
<...>
appBuilder.UseRouting();
appBuilder.UseAuthorization();
<...>
}
and using [Authorize] attribute in my controller method. In this case I get 401 response. What am I doing wrong here?
I will give you a tip.
Register events in AddJwtBearer options
.AddJwtBearer("JwtBearer", opts =>
{
opts.Authority = GetAuthority(configuration);
opts.Audience = configuration["Authentication:AzureAdB2C:ClientId"];
opts.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = configuration["Authentication:AzureAdB2C:Issuer"],
ValidAudiences = new string[] { opts.Audience },
ValidateLifetime = true,
NameClaimType = "name",
};
opts.Events = new JwtBearerEvents();
opts.Events.OnAuthenticationFailed = authFailedContext =>
{
return Task.CompletedTask;
};
opts.Events.OnMessageReceived = messageContext =>
{
return Task.CompletedTask;
};
opts.Events.OnTokenValidated = tokenValidatedContext =>
{
return Task.CompletedTask;
};
opts.Events.OnForbidden = forbiddenContext =>
{
return Task.CompletedTask;
};
});
and check/log azure validation context.
I'm using actionable messages (with Outlook web app) to call an Logic App. Therefore I am getting an Bearer token in the request:
"Action-Authorization": "Bearer eyJ0eXAi..."
Callstack:
Outlook web app -> Logic App -> my endpoint hosted in azure
Now I tried to validate the token with jwt.io but getting an Issue that the Signature is invalid.
So I tried to validate it in c# with the JwtSecurityTokenHandler.
I tried to add https://substrate.office.com/sts/ to the issuer list, but it seems like the validation don't even get there.
I'm using the following code to validate the jwt token issued by office.com:
bool IsAuthorized(HttpActionContext actionContext)
{
var valid = base.IsAuthorized(actionContext);
// Custom handle for Bearer token, when invalid from base-class
if (!valid && actionContext.Request.Headers.Authorization.Scheme == "Bearer")
{
var jwt = actionContext.Request.Headers.Authorization.Parameter;
var th = new JwtSecurityTokenHandler();
var sjwt = th.ReadToken(jwt) as JwtSecurityToken;
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = false,
//IssuerSigningToken = sjwt,
ValidateActor = false,
ValidateAudience = false,
ValidateIssuer = true,
ValidateLifetime = true,
ValidIssuers = new[] { "https://substrate.office.com/sts/" },
ValidAudiences = new[] {"https://XXX.logic.azure.com"}
};
SecurityToken validatedToken;
try
{
th.ValidateToken(jwt, validationParameters, out validatedToken);
}
catch (Exception ex)
{
return false;
}
}
return valid;
}
Here is my JWT token:
I am getting the exception:
IDX10500: Signature validation failed. Unable to resolve SecurityKeyIdentifier: 'SecurityKeyIdentifier
(
IsReadOnly = False,
Count = 2,
Clause[0] = X509ThumbprintKeyIdentifierClause(Hash = 0x818...),
Clause[1] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause
)
', ...
Even though I set ValidateIssuerSigningKey = false.
Is there a way to accept https://substrate.office.com/sts/ as a valid issuer?
The exception says that the "Signature validation failed".
To resolve this problem we can't just add the wanted valid issuer to ValidIssuers, we need the to verify that the token is issued from the issuer itself.
Especially for this case with office.com being the issuer I found the expected key (JWK - JSON Web Key) here:
https://substrate.office.com/sts/common/discovery/keys
(also https://substrate.office.com/sts/common/.well-known/openid-configuration)
Here is the working code:
bool IsAuthorized(HttpActionContext actionContext)
{
var valid = base.IsAuthorized(actionContext);
// Custom handle for Bearer token, when invalid from base-class
if (!valid && actionContext.Request.Headers.Authorization.Scheme == "Bearer")
{
var jwt = actionContext.Request.Headers.Authorization.Parameter;
var th = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new JsonWebKey(GetJWK()),
ValidIssuers = new[] { "https://substrate.office.com/sts/" }
};
Microsoft.IdentityModel.Tokens.SecurityToken validatedToken;
try
{
var claims = th.ValidateToken(jwt, validationParameters, out validatedToken);
valid = true;
}
catch (Exception ex)
{
valid = false;
}
}
return valid;
}
// Get the token from configuration
private string GetJWK()
{
return ConfigurationManager.AppSettings["ida:jwks_json"];
}
In the appsettings I put the RSA key from the website for validating the token, it looks like:
{"kty":"RSA","use":"sig","kid":"gY...","x5t":"gY...","n":"2w...","e":"AQAB","x5c":["MII..."]}
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.
services.AddAuthorization();
services.AddAuthentication(options =>
{
options.DefaultScheme = "bearer";
}).AddJwtBearer("bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = HelperLib.securityKey,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
context.Response.StatusCode = 408; // request timeout
}else if(context.Exception.GetType() == typeof(SecurityTokenSignatureKeyNotFoundException))
{
context.Response.StatusCode = 401; // security token signature is invalid
}else if(context.Response.GetType() == typeof(SecurityTokenInvalidSignatureException)){
context.Response.StatusCode = 408;
}
// if no token is set
context.Response.StatusCode = 402; // check if bearer has token
Console.WriteLine($"no token has been set {context.Response.StatusCode = 408}");
return Task.FromResult(0);
}
};
i am trying to add jwt authentication when the user request using jwt,
also for this i want to reject the request when the user session is expired and no token it should return 401 but for now what happen the user can request even though without token, is there any way I can do this?is policy is much better?
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;
}