ASP.NET Core 6 app.
I have JWT authentication:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateActor = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
// ...
};
})
;
I want to do the next. I want to make tokens alive for 2 hours if user doesn't make any requests. If user sends a request, token's lifetime should be DateTime.UtcNow + 2hours. I think, I should store tokens in DB with their expire time and every request I have to to check this time from db and update it if it isn't expired.
I've investigated how to do that. I found:
One. TokenValidationParameters has LifetimeValidator property, which is a delegate. The problem is I can't use there any application services (e.g. DbContext). Also I thinks it isn't a correct place where I should update anithing (token time in my case)
Two. I can declare my class which implements ISecurityTokenValidator and use it like this:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new MyJwtSecurityTokenHandler());
...
})
There is ValidateLifetime method, but again I can't use any services there. I don't know how to inject there anything.
How can I do custom token lifetime validation and lifetime update?
See below pseudo code where stored the token to the handler and return the token as a string.
expiryMin is a variable that holds the minutes in number. in your case, the value should be 120.
double expiryMin = 120D;
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])); //Jwt:Key - reading from config
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(configuration["Jwt:Issuer"], configuration["Jwt:Issuer"],
null, expires: DateTime.Now.AddMinutes(expiryMin),
signingCredentials: credentials); // Jwt:Issuer - reading from config
return new JwtSecurityTokenHandler().WriteToken(token);
Related
Okay, so I am working on creating an OIDC client that will also handle refresh tokens. I have made some progress, but have some questions.
Here is my ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Login/Index";
options.Events.OnValidatePrincipal = async context => await OnValidatePrincipalAsync(context);
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Configuration["auth:oidc:authority"];
options.ClientId = Configuration["auth:oidc:clientid"];
options.ClientSecret = Configuration["auth:oidc:clientsecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.UseTokenLifetime = true;
options.SignedOutRedirectUri = "https://contoso.com";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = Configuration["auth:oidc:authority"],
ValidAudience = Configuration["auth:oidc:clientid"],
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromSeconds(3)
};
});
services.AddAccessTokenManagement();
services.Configure<OidcOptions>(Configuration.GetSection("oidc"));
}
Here is my OnValidatePrincipalAsync(context)
private async Task OnValidatePrincipalAsync(CookieValidatePrincipalContext context)
{
const string AccessTokenName = "access_token";
const string RefreshTokenName = "refresh_token";
const string ExpirationTokenName = "expires_at";
if (context.Principal.Identity.IsAuthenticated)
{
var exp = context.Properties.GetTokenValue(ExpirationTokenName);
var expires = DateTime.Parse(exp, CultureInfo.InvariantCulture).ToUniversalTime();
if (expires < DateTime.UtcNow)
{
// If we don't have the refresh token, then check if this client has set the
// "AllowOfflineAccess" property set in Identity Server and if we have requested
// the "OpenIdConnectScope.OfflineAccess" scope when requesting an access token.
var refreshToken = context.Properties.GetTokenValue(RefreshTokenName);
if (refreshToken == null)
{
context.RejectPrincipal();
return;
}
var cancellationToken = context.HttpContext.RequestAborted;
// Obtain the OpenIdConnect options that have been registered with the
// "AddOpenIdConnect" call. Make sure we get the same scheme that has
// been passed to the "AddOpenIdConnect" call.
//
// TODO: Cache the token client options
// The OpenId Connect configuration will not change, unless there has
// been a change to the client's settings. In that case, it is a good
// idea not to refresh and make sure the user does re-authenticate.
var serviceProvider = context.HttpContext.RequestServices;
var openIdConnectOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>().Get("OpenIdConnect");
openIdConnectOptions.Scope.Clear();
openIdConnectOptions.Scope.Add("email");
openIdConnectOptions.Scope.Add("profile");
openIdConnectOptions.Scope.Add("offline_access");
var configuration = openIdConnectOptions.Configuration ?? await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
// Set the proper token client options
var tokenClientOptions = new TokenClientOptions
{
Address = configuration.TokenEndpoint,
ClientId = openIdConnectOptions.ClientId,
ClientSecret = openIdConnectOptions.ClientSecret,
};
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
using var httpClient = httpClientFactory.CreateClient();
var tokenClient = new TokenClient(httpClient, tokenClientOptions);
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken, cancellationToken: cancellationToken).ConfigureAwait(false);
if (tokenResponse.IsError)
{
context.RejectPrincipal();
return;
}
// Update the tokens
var expirationValue = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString("o", CultureInfo.InvariantCulture);
context.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = RefreshTokenName, Value = tokenResponse.RefreshToken },
new AuthenticationToken { Name = AccessTokenName, Value = tokenResponse.AccessToken },
new AuthenticationToken { Name = ExpirationTokenName, Value = expirationValue }
});
// Update the cookie with the new tokens
context.ShouldRenew = true;
}
}
}
I've done some experimenting which includes not using the Configuration to get the OpenIdConnectOptions in my OnValidatePrincipal and just create a new OpenIdConnectOptions object , and I still have not been able to understand my issue.
Here are my Current Issues
First Issue
I seem to be able to successfully send a request to the token endpoint after my desired period of time (every 2 minutes and five seconds). I notice that my client application is making a request to the ?authorize endpoint of my authorization server, even though I don't believe I have it configured to do so in my OnValidatePrincipalContext fucntion. I created an all new OpenIdConnectOptions object because I thought the current configuration was triggering it.
First Question
When is this signin-oidc request triggered? I think that's what's triggering the request to my authN server's authorize endpoint. I should not have to query this endpoint if I'm doing silent refresh?
Second Issue
My authorization server is picking up the openid scope when my client makes this request:
POST https://<authorization-server>/oauth/oidc/token HTTP/1.1
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<refresh-token>&client_id=<client-id>&client_secret=<client-secret>
But, in my OnValidatePrincipalContext function I explicitly remove the openid scope by calling
openIdConnectOptions.Scope.Clear();
openIdConnectOptions.Scope.Add("email");
openIdConnectOptions.Scope.Add("profile");
openIdConnectOptions.Scope.Add("offline_access");
Second Question
How do I properly handle the Oidc configuration middleware so that when I go to request a new refresh token the correct request is built and sent to my authN server? Am I doint the wrong kind of authentication scheme (i.e cookie vs bearer)? If I am, how can I tell?
Thank you.
When is this signin-oidc request triggered?
Its triggered by the authorization server when the user have successfully authenticated and given consent to the requested scopes. It will ask the browser to post the authorization code to this endpoint. Its typically performed done by using a self-submitting HTML form that will create a post request to this endpoint.
You should always ask for the openid scope, otherwise it won't work.
A picture showing the flow for the endpoint is:
For the second question one alternative is to take a look at the IdentityModel.AspNetCore library. This library can automatically handle the automatic renewal of the access token using the refresh token.
See also this blog post
I want to Expire Existing JWT Access Token and Refresh Token while i call login api and generate new access and refresh token.
i am use JwtSecurityTokenHandler for generating access token. i also put Expires for expiring token. But I want To Expire Forcefully Existing access and refresh token .
Expire Forcefully is called Revoke in term of JWT. However, in the natural JWT token doesn't want to be revoked because it defines a compact and self-contained way for securely transmitting information between parties here. So you don't want to call revoke, it will make your system is overhead.
However, many enterprise systems require a mechanism to stop JWT immediately. So you should build a cache solution to store all provided tokens. Then each time user call your API, your API server must be validated once again with your Identity Server instead of itself, that is invalid with the term of self-contained. But that depends on your choice, I recommend you to use Redis Distributed Cache to force expire your provided token, at least this solution can be scale.
In my scenario, in case of user is deactivated or removed from the system need to expire the tokens generated against the user. For this, I made a validation in the JWT bearer middleware.
services.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = "localhost",
ValidAudience = "localhost"
};
x.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var UserGUID = context.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Sid).Value;
var user = context.HttpContext.RequestServices.GetRequiredService<IUserManager>().GetUserAsync(UserGUID, string.Empty, false).Result;
var failed = user == null || !user.IsActive;
if (failed)
context.Fail(new Exception("Token not valid anymore"));
return Task.CompletedTask;
}
};
})
I have a Google Pub/Sub push subscription that sends a JWT token to the endpoint. The endpoint needs to validate this token. From Google documentation, I need to check the issuer, the audience and the signature. This works fine, except for whatever I add to IssuerSigningKey(s), the token is valid. I expected this to break whenever I e.g. remove a part of the key.
I tried all kinds of different values for IssuerSigningKey and IssuerSigningKeys. No matter what, I get a valid response. Changing e.g. the domain or audience parameters does return a 401 Unauthorized.
public void ConfigureServices(IServiceCollection services)
{
string domain = "https://accounts.google.com";
string audience = "theaudience";
// Just to debug/test
string signingKey = "---- - BEGIN PRIVATE KEY-----\nMIIfujHGitJ\n---- - END PRIVATE KEY-----\n";
var certificates =
this.FetchGoogleCertificates().GetAwaiter().GetResult();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = domain;
options.Audience = audience;
options.TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.FromHours(48), // This is just for debugging. Maybe we do need a little clock skew if the clock in Google is not aligned with the VD system
ValidateAudience = true, // Validate the audience, this will change in production to the endpoint URL
ValidateIssuer = true, // Validate the issuer (Google). If this is wrong, we get a 500 error instead of 40x
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
/* Stuff I also tried:
IssuerSigningKey = new RsaSecurityKey(new RSACryptoServiceProvider(2048))
IssuerSigningKeys = certificates.Values.Select(x => new X509SecurityKey(x)),
IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
{
return certificates
.Where(x => x.Key.ToUpper() == kid.ToUpper())
.Select(x => new X509SecurityKey(x.Value));
}
*/
};
});
services.AddAuthorization();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
What is happening here?
From the ASP.NET blog post on JWT Validation:
First, the Authority property should not be set on the
JwtBearerOptions. If it’s set, the middleware assumes that it can go
to that URI to get token validation information.
You are leaving the Authority property set, in your code example, which causes the verification library to make a network request to accounts.google.com to get token validation information. If you leave the Authority property unset, it will be forced to use your TokenValidationParameters.
Created an Authentication Api to handle the auth for several apps. This is a basic auth. username and pw. No OAuth with Google etc. The api gets called with the credentials and it responds with an AthenticationResult. It works correctly except on AuthenticationResult.Success. As I learned I cannot serialize the ClaimsPrincipal. As I am reading it seems the answer it to convert to a token. Is this correct? The AuthenticationResult.Failed serializes w/o issue. What is the best solution here. I will continue to look.
thx for reading
General Steps
That's correct, you'll need to complete the following steps:
Return a token from your authentication API.
Configure your application for JWT Bearer authentication.
Include that token as part of an authorize header on every request to the server.
Require authentication/authorization in your controllers.
There is an excellent ASP.NET Core 2.2 JWT Authentication Tutorial you should check out.
There's too much code involved to post all of it in it's entirety, but here are some key snippets (some code slightly modified for greater clarity out of context from the tutorial):
Some Key Code Snippets
Creating the token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
// 'user' is the model for the authenticated user
// also note that you can include many claims here
// but keep in mind that if the token causes the
// request headers to be too large, some servers
// such as IIS may reject the request.
new Claim(ClaimTypes.Name, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
Configuring JWT Authentication (in Startup.cs ConfigureServices method)
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.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
};
});
Don't forget to configure the app to actually use authentication in Startup.cs Configure method:
app.UseAuthentication();
I just wonder, is it possible to configure JWT tokens for 2 different audiences ? First would have a token expiry date set and the second one not.
I have the following code for JWT configuration, but it works for a single audience only.
private void ConfigureSecurity(IServiceCollection services)
{
services.AddAuthentication()
.AddCookie(cfg => cfg.SlidingExpiration = true)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
RequireExpirationTime = false,
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"])),
};
});
services.AddAuthorization();
}
There are a couple of things you can try. First of the AddJwtBearer function accepts a scheme name too. You can try adding two JwtBearers with different scheme names. Not entirely sure if it will allow multiple jwts though
Another way is make to try configuring your own JwtEventBearer and set it to cfg.Events.
If all else fails, you can always manually create and validate the jwt.
You will first need to make the two token validation parameters objects. You can create your token like this:
var handler = new JwtSecurityTokenHandler();
var jwt = handler.CreateJwtSecurityToken(new SecurityTokenDescriptor
{
Audience = myAudience,
Expires = DateTime.UtcNow.Add(Expiary),
Subject = myPrincipal,
SigningCredentials = Signing
});
return handler.WriteToken(jwt);
For validation, you can first check the audience:
var _token = handle.ReadJwtToken(token);
if (_token.Audiences == ...)
Once you find out what the audience is and which token validation parameters to use, you can validate with this:
SecurityToken sToken = handle.CreateJwtSecurityToken();
var myPrincipal = handle.ValidateToken(token, TokenValidationParameters, out sToken);