I have an asp net core api where I need a custom validation for the "audience" claim in a JWT token. This is possible via the AudienceValidator delegate (Microsoft.IdentityModel.Tokens) with signature:
bool CustumValidator(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
The audiences should, as I understand it, contain a list of audiences in the token. The validationParameters contains all the TokenValidationParameters you register at startup. However, for me this parameter is empty.
In the configure method of my startup class if have the following:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime applicationLifetime)
{
//...
// Setup JWT for the application.
var jwtAppSettingOptions = ConfigurationRoot.GetSection(nameof(JwtIssuerOptions));
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
//ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)], -- I have tried with both this property and the ValidAudiences one below
ValidAudiences = new[] { jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)] },
ValidateIssuerSigningKey = true,
IssuerSigningKey = _jwtSigningKey,
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
AudienceValidator = DoValidation // <-- my custom validation method, described below
};
//....
}
And then I have this little test method, that for now, does nothing but return true.
private bool DoValidation(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
var castedToken = securityToken as JwtSecurityToken;
// Do Nothing
return true;
}
As said, this token in the piece above has no value in its audience property, and the IEnumerable<string> audiences is empty as well. I do not understand why. If I let the default validator method be (by not setting a delegate), it is able to see the token's audience and Forbids access if the audience is incorrect. So why does my custom method not get the audience(s) passed in?
Here's how I make the token:
var jwt = new JwtSecurityToken(
_jwtIssuerOptions.Issuer,
String.IsNullOrWhiteSpace(_aud) ? _jwtIssuerOptions.Audience : _aud,
claims,
_jwtIssuerOptions.NotBefore,
_jwtIssuerOptions.Expiration,
_jwtIssuerOptions.SigningCredentials);
Thanks in advance.
I came across this question while trying to find the signature for the AudienceValidator delegate. (Thanks for that!)
The audiences enumerable is populated for me when I handle that delegate. One difference I see is that I do not specify the ValidAudiences parameter. You might try excluding that (assuming you haven't already solved this). Here is an example of my working code:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "MyIssuer",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration["SecurityKey"])),
AudienceValidator = validateAudience
};
});
Related
I have an end point which is protected by the [Authorize] attribute. I want to allow access to it, when a token is provided that is signed with the phrase "Super-Secret-Key" For now, I just generate the token with Jwt.io (you can see a screenshot at the end of the post). However, when I visit the end point, just get a 401 Unauthorized, even if I send the encoded token along as a bearer token (using Postman).
What am I missing?
Program.cs
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Super-Secret-Key"))
};
var builder = WebApplication.CreateBuilder(args);
{
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => options.TokenValidationParameters = tokenValidationParameters);
builder.Services.AddAuthorization();
builder.Services.AddControllers();
}
(...)
the issue is that asp.net core tries to validate the token issuer by default that you did not set up on jwt.io. Set ValidateIssuer = false inside TokenValidationParameters:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Super-Secret-Key"))
};
});
If you want to specify an issuer to your JWT token you have to add the iss standard claim (Payload data) as specified inside official docs: https://jwt.io/introduction.
Here's an example:
Validation setup:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
ValidateIssuer = true, //Validate issuer
ValidIssuer = "MyIssuer", //Issuer to validate
//ValidIssuers = new List<string>() { "MyIssuer", "MySecondIssuer" }, //You can specify multiple issuers
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Super-Secret-Key"))
};
});
There also are other "standard" (registered) claims that you can use like:
exp: timestamp that identifies the expiration time of the token.
nbf: timestamp that identifies the time before which the JWT
must not be accepted for processing.
aud: audience identifies recipients that the JWT is
intended for.
that you can validate with standard validation options or with custom-written validation logic.
See: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
What exactly is the call JwtBearerDefaults.AuthenticationScheme and JwtBearerDefaults for?
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = tokenOptions.Issuer,
ValidAudience = tokenOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = SecurityKeyHelper.CreateSecurityKey(tokenOptions.SecurityKey)
};
});
JwtBearerDefaults.AuthenticationScheme is a constant with the value "Bearer". This is just text that is similar in intent to the authentication provider.
When you call AddAuthentication, you need to provide the default authentication provider. Providers are named. By default AddJwtBearer will create an authentication provider with the name, yep you guessed it, JwtBearerDefaults.AuthenticationScheme.
You could ignore the JwtBearerDefaults class altogether if you'd like, this is personal preference. Pass whatever string you want to AddAuthentication, then use the overload of AddJwtBearer that takes a string and an Action to customize the provider's name.
builder.Services.AddAuthentication("MyCoolAuthProvider")
.AddJwtBearer("MyCoolAuthProvider", options => {
//...
});
In the end, why do people use the JwtBearerDefaults class? Same reason you name your for loop variable i. It's what the documentation suggests, and everyone else does it. At least if it's a constant, you can't typo the value.
I am currently using the JwtSecurityToken class in System.IdentityModels.Tokens namespace. I create a token using the following:
DateTime expires = DateTime.UtcNow.AddSeconds(10);
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var genericIdentity = new System.Security.Principal.GenericIdentity(username, "TokenAuth");
ClaimsIdentity identity = new ClaimsIdentity(claims);
string secret = ConfigurationManager.AppSettings["jwtSecret"].ToString();
var securityKey = new InMemorySymmetricSecurityKey(Encoding.Default.GetBytes(secret));
var signingCreds = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.HmacSha256Signature);
var securityToken = handler.CreateToken(
issuer: issuer,
audience: ConfigurationManager.AppSettings["UiUrl"].ToString(),
signingCredentials: signingCreds,
subject: identity,
expires: expires,
notBefore: DateTime.UtcNow
);
return handler.WriteToken(securityToken);
For some reason even though the expires is set to 10 seconds after the current time it doesn't actually throw an exception when the token is being validated until about 5 minutes. After seeing this, I thought maybe there was a minimum expire time of 5 minutes, so I set the expire time to:
DateTime.UtcNow.AddMinutes(5);
Then it expires at 10 minutes, but the exception message says that the expire time is set to what it is supposed to be (5 minutes after the user logs in), and when it shows the current time in the exception it is 5 minutes after the expire time. So, it seems to know when it SHOULD expire, but it doesn't actually throw the exception until 5 minutes after the expire time. Then, since the token seems to be adding 5 minutes to whatever time I set it to expire I set the expire time to:
DateTime.UtcNow.AddMinutes(-5).AddSecond(10);
I tested this and so far it still hasn't expired (After more than ten minutes). Can someone please explain why this is happening and what I am doing wrong? Also, if you see anything else with the code I provided any guidance would be appreciated since I am new to using JWTs and this library.
The problem is related ClockSkew. Normally, the validation libraries (at least the MS one) compensate for clock skew. ClockSkew default value is 5 minutes. See some answer here
You can change ClockSkew in TokenValidationParameters:
var tokenValidationParameters = new TokenValidationParameters
{
//...your setting
// set ClockSkew is zero
ClockSkew = TimeSpan.Zero
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters
});
After reading through #Denis Kucherov's answer, I found out that I could use the same custom validator he posted without using the JwtBearerOptions class which would have required me to add a new library.
Also, Since there are two namespaces which contain a lot of these same classes I will make sure to mention that all of these are using the System.IdentityModels... namespaces. (NOT Microsoft.IdentityModels...)
Below is the code I ended up using:
private bool CustomLifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken tokenToValidate, TokenValidationParameters #param)
{
if (expires != null)
{
return expires > DateTime.UtcNow;
}
return false;
}
private JwtSecurityToken ValidateJwtToken(string tokenString)
{
string secret = ConfigurationManager.AppSettings["jwtSecret"].ToString();
var securityKey = new InMemorySymmetricSecurityKey(Encoding.Default.GetBytes(secret));
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
TokenValidationParameters validation = new TokenValidationParameters()
{
ValidAudience = "MyAudience",
ValidIssuer = "MyIssuer",
ValidateIssuer = true,
ValidateLifetime = true,
LifetimeValidator = CustomLifetimeValidator,
RequireExpirationTime = true,
IssuerSigningKey = securityKey,
ValidateIssuerSigningKey = true,
};
SecurityToken token;
ClaimsPrincipal principal = handler.ValidateToken(tokenString, validation, out token);
return (JwtSecurityToken)token;
}
There are seems to be some issue with LifeTimeValidator. You can just override its logic with a custom delegate. Also, use JwtBearerOptions class to control authentication middleware behavior. For example:
new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidIssuer = _configuration["Tokens:Issuer"],
ValidAudience = _configuration["Tokens:Audience"],
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
LifetimeValidator = LifetimeValidator,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Tokens:Key"]))
}
}
And assign LifetimeValidotor delegate, to provide its own timeout validation logic:
private bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken token, TokenValidationParameters #params)
{
if (expires != null)
{
return expires > DateTime.UtcNow;
}
return false;
}
.NET Core Update
This is handled slightly differently in .NET Core, as the TokenValidationParameters are set in Startup.cs using the ConfigureServices() method and then handled automatically by the middleware.
Also note that the older InMemorySymmetricSecurityKey for signing the secret is now deprecated in favor of SymmetricSecurityKey, which is shown below.
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _config.AuthenticationSettings.TokenAuthority,
ValidAudience = _config.AuthenticationSettings.TokenAuthority,
LifetimeValidator = TokenLifetimeValidator.Validate,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config.AuthenticationSettings.SecurityKey))
};
});
// ...
}
And so I also made my own version of the token validator in #tkd_aj's answer above and threw it in a static class:
public static class TokenLifetimeValidator
{
public static bool Validate(
DateTime? notBefore,
DateTime? expires,
SecurityToken tokenToValidate,
TokenValidationParameters #param
) {
return (expires != null && expires > DateTime.UtcNow);
}
}
I just implemented a JWT token middleware too and although the examples on the internet use UtcNow, I had to use Now or the expire time is off. When I use Now, expiration is spot on.
Below link give you the exact answer, as by default MS have expire time of 5mins.
So either you have to make it custom or time which you will give in
expires: DateTime.Now.AddSeconds(30)
30seconds in above line will be added in expirey time. So total expire time will be 5mins and 30secs
https://github.com/IdentityServer/IdentityServer3/issues/1251
Hope this will help.
I'm working on an ASP.NET Core project that has a website as well as an API. I need the API to be secured by some means, so I implemented JWT authentication. This worked as expected, and the API is only accessible by calls made with a valid token.
The problem is, the built-in login functionality on the website no longer works.
This is the code I added to Startup.cs:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
If I remove this code, the built-in login feature works as usual, but the API no longer works.
Is there a way I can make the JWT authentication only apply to the API controllers? Or is there a way I can make the built-in authentication use JWT?
I had the same problem and resolved it (using Asp.Net Core 3.0).
You can add more than one Authentication schema to your project with code like this.
// Read the token generation data...
var token = Configuration.GetSection("JwtTokenData").Get<JwtTokenData>();
services.AddAuthentication()
.AddCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
})
.AddJwtBearer(options => {
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),
ValidIssuer = token.Issuer,
ValidAudience = token.Audience,
ValidateIssuer = false,
ValidateAudience = false
};
});
Notice how I haven't specified the default authentication schema when I call AddAuthentication. This allows the built-in Authentication to be the default for your pages or controller where you use the [Authorize] attribute.
But when you need to use JWT Authentication then you use this option on the Authorize attribute for your Web API controller or methods
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Using ASP.NET Core 3.1. I want to validate a JWT (and the roles in it).
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(Configuration);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.MetadataAddress = "https://example.com/.well-known/openid-configuration";
});
services.AddScoped<IAuthorizationHandler, MyRoleHandler>();
}
MyRoleHandler
public class MyRoleHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var claims = context.User.Claims;
return Task.CompletedTask;
}
}
https://example.com/.well-known/openid-configuration
{
"issuer": "https://example.com",
"jwks_uri": "https://example.com/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://example.com/connect/authorize",
"token_endpoint": "https://example.com/connect/token",
"end_session_endpoint": "https://example.com/connect/endsession",
"check_session_iframe": "https://example.com/connect/checksession",
"device_authorization_endpoint": "https://example.com/connect/deviceauthorization",
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true,
"response_types_supported": ["code", "token", "id_token", "id_token token", "code id_token", "code token", "code id_token token"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"code_challenge_methods_supported": ["plain", "S256"],
"upgrade_endpoint": "https://example.com/connect/upgrade",
"end_session_accesstoken_endpoint": "https://example.com/connect/endsession/accesstoken",
"active_session_endpoint": "https://example.com/connect/activesession"
}
claims is empty. What am I doing wrong here? I was under the impression the JwtBearer middleware will configure itself when given the openid-configuration?
I figured out now that the error was in fact that the TokenValidationParameters must be set correctly. I don't exactly know why, but as soon as I add them in any capacity, the claims are correctly decoded.
So now I added them as follows:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.Authority = Env.Get("AUTHORITY_BASE_URL");
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidIssuer = Env.Get("ALLOWED_ISSUER"),
ValidateAudience = true,
ValidAudience = Env.Get("ALLOWED_AUDIENCE"),
ClockSkew = TimeSpan.FromSeconds(Env.GetDouble("CLOCK_SKEW")),
RoleClaimType = "authorities"
};
});
And now, in my MyRoleHandler, the claims are correctly decoded an I can check them.