We have an SPA, written in React together with ASP.net core for hosting.
To authenticate the app, we are using IdentityServer4 and use a cookie. The client is configured according to the sample, described here: https://github.com/IdentityServer/IdentityServer4/tree/main/samples/Quickstarts/4_JavaScriptClient/src
For authenticating a user, everythings works fine. It will be redirected to the login page. After signing in, redirection to the SPA is done. The authentiation cookie is set as expected with:
The cookie is used also in other MVC (.net core and MVC 5) applications for authentication reasons. In the SPA, we are also using SignalR, which needs the cookie for authentication.
Our issue:
After about 30 minutes of idle time in the browser and either doing a refresh or navigating, the authentication cookie (and only that, other remains) is disappearing from the browser automatically. Then the user has to sign in again. Why does this happen together with the SPA?
Code
Complete code can be found in github
Client
Snippets of UserService.ts
const openIdConnectConfig: UserManagerSettings = {
authority: baseUrls.person,
client_id: "js",
redirect_uri: joinUrl(baseUrls.spa, "signincallback"),
response_type: "code",
scope: "openid offline_access profile Person.Api Translation.Api",
post_logout_redirect_uri: baseUrls.spa,
automaticSilentRenew: true
};
export const getUserService = asFactory(() => {
const userManager = new UserManager(openIdConnectConfig);
return createInstance(createStateHandler(defaultUserState), userManager, createSignInProcess(userManager));
}, sameInstancePerSameArguments());
Server
Snipped of Startup.cs
public void ConfigureServices(IServiceCollection services)
{
Log.Information($"Start configuring services. Environment: {_environment.EnvironmentName}");
services.AddControllersWithViews();
services.AddIdentity<LoginInputModel, RoleDto>()
.AddDefaultTokenProviders();
var certificate = LoadSigningCertificate();
var identityServerBuilder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddSigningCredential(certificate)
.AddProfileService<ProfileService>()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));
if (_environment.IsDevelopment())
{
identityServerBuilder.AddDeveloperSigningCredential();
}
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(_sharedAuthTicketKeys))
.SetApplicationName("SharedCookieApp");
services.AddAsposeMailLicense(Configuration);
var optionalStartupSettings = SetupStartupSettings();
if (optionalStartupSettings.IsSome)
{
var settings = optionalStartupSettings.Value;
services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = new PathString("/Account/AccessDenied");
options.Cookie.Name = ".AspNetCore.Auth.Cookie";
options.Cookie.Path = "/";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.LoginPath = new PathString("/account/login");
options.Cookie.SameSite = SameSiteMode.None;
});
var authBuilder = services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "Identity.Application"; });
authBuilder = ConfigureSaml2(authBuilder, settings);
authBuilder = ConfigureGoogle(authBuilder);
authBuilder.AddCookie();
}
else
{
throw new InvalidOperationException($"Startup settings are not configured in appsettings.json.");
}
SetupEntityFramework(services);
}
Snippet of identity server client config from appsettings.json
{
"Enabled": true,
"ClientId": "js",
"ClientName": "JavaScript Client",
"AllowedGrantTypes": [ "authorization_code" ],
"RequirePkce": true,
"RequireClientSecret": false,
"RedirectUris": [ "https://dev.myCompany.ch/i/signincallback", "https://dev.myCompany.com/i/signincallback", "https://dev.myCompany.de/i/signincallback" ],
"PostLogoutRedirectUris": [ "https://dev.myCompany.ch/i/", "https://dev.myCompany.com/i/", "https://dev.myCompany.de/i/" ],
"AllowedCorsOrigins": [],
"AllowedScopes": [ "openid", "offline_access", "profile", "Translation.Api", "Person.Api" ],
"RequireConsent": false,
"AllowOfflineAccess": true
}
Update
In the meantime I discovered that the cookie, while requesting https://ourdomain/.well-known/openid-configuration
after 30 minutes idle time, has lost the values of Domain, Path, Expires/Max-Age, HttpOnly, Secure and SameSite.None. Those values have definitely been set after signing in.
The response cookie has the value of Expires/Max-Age set to a time in the past and therefore the cookie will be dropped by the browser.
Has anyone an idea, why those values got lost after some time?
Finally, I figured out how to tackle this.
It had to do with the configuration of IdentityServer. The missing part was the method AddAspNetIdentity<LoginInputModel>()
.
Before:
var certificate = LoadSigningCertificate();
var identityServerBuilder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddSigningCredential(certificate)
.AddProfileService<ProfileService>()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));
Now:
var certificate = LoadSigningCertificate();
var identityServerBuilder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddSigningCredential(certificate)
.AddAspNetIdentity<LoginInputModel>()
.AddProfileService<ProfileService>()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));
With that additional configuration line, Identity Server is handling the cookie correctly.