Backstory: I'm trying to link up a new client application using .NET 5 (MVC) with an existing IdentityServer4. IdentityServer4 (IS4 in short) is used both to authenticate the client, as well as providing claims & roles and the access_token that the API (separate webapp) depends on to request data on the backend systems. On the new client I'm using the IdentityModel package to handle the authentication and authorization. So far I've managed have both authentication and authorization working, but I'm running into issues regarding the expiration date of the access_token.
I've currently configured the client in IS4 to have the following. If the line says '(default)', it means that it's the defaults as suggested or set by IdentityModel/IS4:
- IdentityTokenLifetime: 300s / 5m (default)
- IdentityAccessToken: 300s / 5m (shortened to allow me to test)
- AuthorizationCodeLifetime: 300s / 5m (default)
Flow First example:
- User browses to webpage, and get's redirected to IS4 to log in.
- User fills in user/pass and successfully authenticates, and get's redirected back to the secure section of the web app.
- When user hits the secure webpage, an api request is done to an external api using the users' access_token to grab the users' data.
- Request comes back with users' data, and webpage with that data.
- Working perfectly.
Second example:
- User browses to webpage, and get's redirected to IS4 to log in.
- User is already authenticated with IS4 using cookie, thus successfully authenticates, and get's redirected back to the secure section of the web app.
- When user hits the secure webpage, an api request is done to an external api using the users' access_token to grab the users' data.
- Request comes back with users' data, and webpage with that data.
- Working perfectly.
Third example:
- User waits 15 minutes on the webpage, and then refreshes the page.
- User is already logged into the website, so no redirect to IS4 is happening.
- Since user refreshes, the user hits the secure webpage, an api request is done to an external api using the users' access_token to grab the users' data.
- Request comes back empty, as the access_token has expired (10 minutes prior)
- Sad smiley :'(
Fourth example:
- Following example three: User sees error and restarts browser.
- User browses to webpage, and get's redirected to IS4 to log in.
- User is already authenticated with IS4 using cookie, thus successfully authenticates, and get's redirected back to the secure section of the web app.
- When user hits the secure webpage, an api request is done to an external api using the users' access_token to grab the users' data.
- Request comes back with users' data, and webpage with that data. (because access_token is newly generated with future expiry date due to new IS4 'login')
- Working perfectly.
Example three is the problem I'm having. What I expect to happen is that the [Authorization]-check doesn't allow expired sessions (access_tokens) to pass through, and instead redirect the user to IS4 to re-authenticate automatically based on the valid cookie the user still has (like example four).
What I've tried to fix:
- Lengthening the IdentityAccessToken lifetime: Doesn't fix the problem, instead just moves the problem to the new expire_date.
- Using the IdentityModel client "Web5" example on our existing IS4 implementation, which is showing the same behavior.
--
The requirements of the application are to have a short access_token lifetime to allow quick updates of users' right and access based on changed claims/roles in the backend, meanwhile allowing for 'persistent' logins to reduce the amount of time users' spend having to fill their accoutn details in.
It is entirely possible that instead of a technical problem my thoughtprocess or understanding of these things is wrong. If so, please enlighten me as to what the flow should be, preferably with a working example.
--
IdentityModel in the client is configured as following:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services
.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Events.OnSigningOut = async e =>
{
// revoke refresh token on sign-out
await e.HttpContext.RevokeUserRefreshTokenAsync();
};
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => {
options.GetClaimsFromUserInfoEndpoint = true;
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Configuration.GetValue<string>("IdentityServer:Authority");
options.ClientId = Configuration.GetValue<string>("IdentityServer:ClientId");
options.ClientSecret = Configuration.GetValue<string>("IdentityServer:ClientSecret");
options.RequireHttpsMetadata = Configuration.GetValue<bool>("IdentityServer:RequireHttpsMetadata");
options.UsePkce = true;
options.ResponseType = OidcConstants.ResponseTypes.CodeIdToken;
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
// Scopes
options.Scope.Add("openid");
options.Scope.Add("offline_access");
})
.AddOpenIdConnect("persistent", options => {
options.CallbackPath = "/signin-persistent";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.Prompt = OidcConstants.PromptModes.None;
return Task.FromResult<object>(null);
},
OnMessageReceived = context => {
if (string.Equals(context.ProtocolMessage.Error, "login_required", StringComparison.Ordinal))
{
context.HandleResponse();
context.Response.Redirect("/");
}
return Task.FromResult<object>(null);
}
};
...
// Rest of 'persistent' is similar as the non-persistent one
...
});
// Examples of IdentityModel suggest that calling this function make the boilerplate tasks of refreshing tokens and alike automatically work
services.AddAccessTokenManagement();