Why my OAuth provider works incorrectly when I add Identity to a project? - asp.net-core

I have code that authenticates an user using OAuth. Here is this code: Github link
I use this code in the ConfigureServices() method of the Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options=>
{
options.LoginPath = new PathString("/Account/Login");
options.LogoutPath = new PathString("/Account/Logout");
options.AccessDeniedPath = new PathString("/Account/Forbidden");
})
.AddVkontakte(options => // here
{
options.ApiVersion = "5.95";
options.ClientId = Configuration["VKontakte:ClientId"];
options.ClientSecret = Configuration["VKontakte:ClientSecret"];
});
services.AddDefaultIdentity<User>(options =>
{
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<ApplicationContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
But when I try to authenticate using it, nothing happens. It works the way I want, only when I remove this strokes
...
services.AddDefaultIdentity<User>(options =>
{
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<ApplicationContext>()
.AddDefaultTokenProviders();
In both cases, the code behind .AddVkontakte(...) works correctly, I checked it in the network inspector of the browser. My code makes requests to the OAuth provider(vk.com) and successfully gets responses. But I don't understand why AddDefaultIdentity<User>(...) doesn't allow .AddVkontakte(...) to authenticate an user.
What do you think about this?

Okay, I looked at this question (Asp Core 2.1 Jwt + Identity. userManager store does not implement IUserRoleStore), and tried to change a little bit options passed to AddAuthentication, and it worked!
Here is the final code:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options=> // defined some options
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ApplicationScheme;
})
.AddCookie(options=>
{
options.LoginPath = new PathString("/Account/Login");
options.LogoutPath = new PathString("/Account/Logout");
options.AccessDeniedPath = new PathString("/Account/Forbidden");
})
.AddVkontakte(options =>
{
options.ApiVersion = "5.95";
options.ClientId = Configuration["VKontakte:ClientId"];
options.ClientSecret = Configuration["VKontakte:ClientSecret"];
});
services.AddDefaultIdentity<User>(options =>
{
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<ApplicationContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
I don't know what does it mean, but it works! Wait and see.

Related

Asp.net core Microsoft OIDC library OnAuthorizedCode recieved event getting fired twice

I have an OIDC application, which was giving me a correlation error for some time. We have managed to resolve it by passing the correlation cookie properly. Our infrastructure has the following network topology.
Openshift Container(OIDC App) -> Reverser Proxy/Api gateway -> Identity Provider.
If we use boilerplate code, the callback path picks up the host of open shift, hence the redirection not working correctly. We are now building the URL on the event onRedirectToIdentitityProvider.
We are also using OnAuthorizationCodeReceived event to make the /Token call as the default configuration was not working out.
We get callback properly with Authorization code but we see onAuthorizationCodeRecieved getting fired twice. This is happening only in the test environment with the reverse proxy setup. In the development box, there is no issue but it does not have the reverse proxy set up(Now, a reverse proxy is just a passthrough so not sure how that could break things).
Code Snippet Below
public void ConfigureServices(IServiceCollection services)
{
string _authorityAPI = Configuration.GetValue<string>("authority");
string _org = Configuration.GetValue<string>("orgDomain");
string clientId = Configuration.GetValue<string>("ClientId");
string clientSecret = Configuration.GetValue<string>("ClientSecret");
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always;
options.HandleSameSiteCookieCompatibility();
services.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Profile/Index/";
options.LogoutPath = "/Profile/Logout/";
})
.AddOpenIdConnect(options =>
{
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.Authority = Configuration.GetValue<string>("Authority");
options.CallbackPath = "/web/auth/callback";
options.ResponseType = OpenIdConnectResponseType.Code;
options.MetadataAddress = string.Format("{0}/.well-known/openid-configuration", _authorityAPI);
options.TokenValidationParameters.ValidateIssuer = false;
//options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = true;
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("email");
options.Scope.Add("profile");
options.NonceCookie.SameSite = SameSiteMode.None;
options.CorrelationCookie.SameSite = SameSiteMode.None;
options.NonceCookie.Path = "/";
options.CorrelationCookie.Path = "/";
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.MetadataAddress, new ApiGatewayRetriever(_authorityAPI, _org));
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = (context) =>
{
if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
context.ProtocolMessage.Parameters.Clear();
context.ProtocolMessage.IssuerAddress = centralLogin + "?RedirectURL=https://" + GetRequestHostName(context, Configuration);
return Task.FromResult(0);
}
return Task.FromResult(0);
}
,
OnAuthorizationCodeReceived = authorizationCtx =>
{
try
{
BellLogger.WriteLog("OnAuthorizationCodeReceived start:" + authorizationCtx.TokenEndpointRequest.Code, Framework.Common.LogType.Info);
HttpClient httpClient = new HttpClient();
TokenClientOptions tokenClientOptions = new TokenClientOptions()
{
ClientId = clientId,
ClientSecret = clientSecret,
Address = string.Format("{0}/v1/token", _authorityAPI)
};
var tokenClient = new TokenClient(httpClient, tokenClientOptions);
var tokenResponse = await tokenClient.RequestAuthorizationCodeTokenAsync(authorizationCtx.TokenEndpointRequest.Code, authorizationCtx.TokenEndpointRequest.RedirectUri).Result;
authorizationCtx.HandleCodeRedemption(tokenResponse.AccessToken, tokenResponse.IdentityToken);
}
catch (Exception ex)
{
Logger.WriteLog(ex, Framework.Common.LogType.Error);
}
return Task.FromResult(0);
}
};
});
services.AddSession(options =>
{
options.Cookie.Name = ".lLogin.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.IsEssential = true;
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
});
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
});
services.AddControllersWithViews();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
if (options.SameSite == SameSiteMode.None)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
// TODO: Use your User Agent library of choice here.
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6") || userAgent.Contains("Chrome/9"))
{
// For .NET Core < 3.1 set SameSite = (SameSiteMode)(-1)
options.SameSite = SameSiteMode.Unspecified;
}
}
}
app.UseHttpsRedirection();
app.UseStaticFiles();
//app.UseAntiXssMiddleware();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseRouting();
app.UseSession();
app.UseAuthorization();
app.UseForwardedHeaders();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
This may not be the answer to the above question, however we solved the problem.
We removed all the custom implementation of events including onAuthorizationCodeReceived.
The issue with the default configuration was, when the reverse proxy routes the call to the OCP environment, the host header was getting added as of the OCP. Reverse proxy added the header of the proper host and then everything started working fine. Host header was the important value.
This might be a redirect issue and it is worth understanding things in terms of URLs rather than C# code. A response to the authorization code flow looks something like this:
https://myapp.com?code=xxx&state=yyy
It is possible that the reverse proxy - or the web server - or the openid provider - expects the response to have (or not have) a trailing backslash. Eg the first URL causes a redirect to the second:
https://myapp.com?code=xxx&state=yyy
https://myapp.com/?code=xxx&state=yyy
I would put some log statements in OnAuthorizationCodeReceived and maybe also trace browser / HTTP traffic to see if this type of thing is happening.

identityserver4 reset password token lifetime

I have an ASP.NET 5.0 application and I am using IdentityServer4 for authentication and authorization.
I am setting the token lifetime for reset password to 20 minutes as below in the startup:
//ResetPasswordTokenLifetime
services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromMinutes(20);
here is how I am validating the token
await this._userManager.VerifyUserTokenAsync(user, this._userManager.Options.Tokens.PasswordResetTokenProvider, "ResetPassword", token)
But when I send the token for resetting password it expires after 8 minutes(I assume this is the default value?) can you please help me with figuring out what am I missing?
this is full configurService in StartUp
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//Connection String
services.AddDbContext<AppIdentityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("PortalConnectionString")));
//Dependency Injection
services.Configure<IdentityApplicationSettings>(Configuration.GetSection("AppSettings"));
services.AddScoped<IEventSink, AppEventSink>();
services.AddTransient<IAppUserValidator<AppUser>, AppUserValidator<AppUser>>();
//AspNetIdentity Configuration
services.AddIdentity<AppUser, IdentityRole>(options =>
{
options.User.RequireUniqueEmail = false;
options.Lockout.MaxFailedAccessAttempts = Configuration.GetSection("AppSettings").GetValue<Int32>("MaxFailedAccessAttempts");
//todo change the timespan
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(360 * 100);
options.Password.RequiredLength = 8;
options.Password.RequireDigit = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
})
.AddUserManager<AppUserManager>()
.AddEntityFrameworkStores<AppIdentityContext>()
.AddDefaultTokenProviders();
//IdentityServer Configuration
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
}).AddAspNetIdentity<AppUser>()
.AddProfileService<ProfileService>()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryClients(Config.GetClients());
//ResetPasswordTokenLifetime
services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromMinutes(Configuration.GetSection("AppSettings").GetValue<Int32>("ResetPasswordTokenLifetime")));
//CORS configuration
services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigins", builder =>
{
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
});
});
//Signing Credentials. Reading from tempkey saved on project for development, and from SSL certificate on Release
if (Environment.IsDevelopment())
{
builder.AddDeveloperSigningCredential();
}
else
{
builder.AddSigningCredential(loadCertificateFromStore());
}
}

IdentityServer 4 WsFederation - How to get access token for calling API

I am using Identity Server 4 with the Ws-Federation plugin. Identity Server is configured to connect to Azure AD for authentication. Here is the relevant code from Identity Server project:
public void ConfigureServices(IServiceCollection services)
{
var rsaCertificate = new X509Certificate2("rsaCert.pfx", "1234");
services.AddRazorPages();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<User, IdentityRole>(options =>
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(2);
options.Lockout.MaxFailedAccessAttempts = 3;
})
.AddDefaultUI()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddUserStore<CustomUserStore>()
.AddUserManager<CustomUserManager>()
.AddDefaultTokenProviders();
services.AddTransient<IUserStore<User>, CustomUserStore>();
services.AddTransient<IEmailSender, EmailSender>();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
})
.AddSigningCredential(rsaCertificate)
.AddInMemoryIdentityResources(IdentityConfig.IdentityResources)
.AddInMemoryApiScopes(IdentityConfig.ApiScopes)
.AddInMemoryClients(IdentityConfig.Clients)
.AddAspNetIdentity<User>()
.AddWsFederationPlugin(options =>
{
options.Licensee = "Licensee";
options.LicenseKey = "LicenseKey";
})
.AddInMemoryRelyingParties(new List<RelyingParty>());
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = "Azure AD App Id";
options.MetadataAddress = "WSFed metadata URL from Azure AD App";
options.Events.OnSecurityTokenValidated = SecurityTokenValidated;
})
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(IdentityConfig.SessionTimeoutInMinutes);
options.SlidingExpiration = true;
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
});
}
I have an API protected with JWT bearer authentication which is connected to the same Identity Server. The relevant code from the API (Please note that https://localhost:5001 is the address which the Identity Server is running in):
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
I have an MVC client as well which is connected to the same Identity Server. I was able to successfully authenticate users from the MVC client. Now, what I would like to do is to call a protected API endpoint in the API project from within the MVC client. I haven't found any way to get the access token necessary for calling the protected API. Relevant code from the MVC client:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Name = "aspnetcorewsfed";
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(Configuration.GetValue<int?>("SessionTimeoutInMinutes") ?? 15);
})
.AddWsFederation(options =>
{
options.MetadataAddress = "https://localhost:5001/wsfed"; // Address of the Identity Server
options.RequireHttpsMetadata = false;
options.Wtrealm = "mvc"; // ClientId registered in Identity Server
options.CallbackPath = "/";
options.SkipUnrecognizedRequests = true;
});
}
There's documentation on the Identity Server website that describes how to access protected APIs as seen here. But this is using OpenIdConnect. Since I am using WsFederation, I have no clue on how to get the access token or refresh token. Is token refresh impossible with WsFed?
Can anyone point me in the right direction on how to go about this?
Use OpenIdConnect in the MVC client instead of WsFed. Change the code in the MVC client's Startup.cs to the following:
services.AddAuthentication(options =>{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
}).AddCookie("Cookies").AddOpenIdConnect("oidc", options =>{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc-openid";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
});
The corresponding client registration in Identity Server should be:
new Client {
ClientId = "mvc-openid",
ClientSecrets = {
new Secret("secret".Sha256())
},
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = {
"https://localhost:6001/signin-oidc"
},
AllowedScopes = new List < string > {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
}
}
https://localhost:5001 is the Identity Server address and https://localhost:6001 is the MVC client address.
The access token for accessing the API can be obtained like so:
var accessToken = await HttpContext.GetTokenAsync("access_token");

IdentityServer4 external authentication fails on localhost only due to invalid Identity.External cookie (cannot be configured)

I am building a React+Redux SPA with dotnetcore 3.1 and Identity Server 4, attempting to implement external authentication (Github) for sign-in. I have largely followed the sample application provided by the Github oauth library. However, I have also tried using Google sign-in (which is not third party) with the same symptoms.
First the user loads /api/auth/github:
public IActionResult Oauth([FromRoute] string provider) {
provider = GetProviderName(provider);
if (string.IsNullOrWhiteSpace(provider)) {
return BadRequest();
}
AuthenticationProperties properties = _signInManager.ConfigureExternalAuthenticationProperties(
provider, "/api/auth/callback");
Log.Information("Properties: {#props}", properties);
return new ChallengeResult(provider, properties);
}
After logging in, the /signin-github endpoint is correctly triggered, which redirects to my /api/auth/callback endpoint:
[HttpGet("callback")]
public async Task<IActionResult> Callback() {
Log.Information("USER: {#user}", User);
ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null) {
return Unauthorized("No login info found");
}
Log.Information("External: {#external}", info);
// Sign in the user with this external login provider if the user already has a login.
SignInResult result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true);
return await HandleSigninResult(result);
}
However, the No login info found is always triggered. With debug logs on, I can see:
AuthenticationScheme: "Identity.External" was not authenticated.
I believe the issue has to do with cookies when using localhost:5000. I had to set cookie.SameSite = SameSiteMode.Lax; on the CorrelationCookie before the hand-off would succeed at all. Looking at the headers from the /signin-github endpoint, it is attempting to set the Identity.External cookie. However, unlike the correlation cookie, this header is unmodified (no samesite=lax policy), and I cannot find any mechanism by which to edit the Identity.External cookie.
I finally ended up deploying the app to production, and indeed the problem went away (because the SameSite modification is no longer necessary).
I have already tried to use ConfigureApplicationCookie, which does affect the other cookies in my app, but not the Identity.External cookie. Here is configuration:
private static void ConfigureCookie(string name, CookieBuilder cookie) {
cookie.Name = name;
cookie.SameSite = SameSiteMode.Lax;
cookie.SecurePolicy = CookieSecurePolicy.None;
}
public static void ConfigureServices(IServiceCollection services, IConfiguration configuration) {
// Identity Server
services.AddDefaultIdentity<UserProfile>(options => {
options.SignIn.RequireConfirmedAccount = true;
options.SignIn.RequireConfirmedEmail = true;
options.Stores.MaxLengthForKeys = 64;
}).AddEntityFrameworkStores<OWSData>().AddDefaultTokenProviders();
services.AddIdentityServer((options) => {
options.PublicOrigin = root;
options.IssuerUri = root;
options.UserInteraction.LoginUrl = $"{root}/account/login";
options.UserInteraction.LogoutUrl = $"{root}/account/logout";
options.UserInteraction.ErrorUrl = $"{root}/account/error";
options.UserInteraction.ConsentUrl = $"{root}/account/terms-of-service";
options.UserInteraction.DeviceVerificationUrl = $"{root}/account/device-verification";
}).AddApiAuthorization<UserProfile, OWSData>(options => {
// Serilog.Log.Information("Clients: {#clients}", options.Clients);
});
services.AddScoped<ICurrentUser, CurrentUser>();
services.AddTransient<IReturnUrlParser, AuthReturnUrl>();
// Auth (JWT)
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie((options) => {
ConfigureCookie(".OWS.Core.", options.Cookie, webHost);
options.LoginPath = "/account/login";
options.AccessDeniedPath = "/account/denied";
options.LogoutPath = "/account/logout";
options.SlidingExpiration = true;
})
.AddIdentityServerJwt()
.AddGoogle(options => {
options.ClientId = configuration["Google:ClientId"];
options.ClientSecret = configuration["Google:ClientSecret"];
ConfigureCookie(".OWS.Correlation.", options.CorrelationCookie);
})
.AddGitHub(options => {
options.Scope.Add("user:email");
options.EnterpriseDomain = configuration["GitHub:EnterpriseDomain"];
options.ClientId = configuration["GitHub:ClientId"];
options.ClientSecret = configuration["GitHub:ClientSecret"];
ConfigureCookie(".OWS.Correlation.", options.CorrelationCookie);
});
// Auth (Password)
services.Configure<IdentityOptions>(options => {
//Password settings
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
//Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
//User settings
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#";
options.User.RequireUniqueEmail = false;
});
// Cookies
services.ConfigureApplicationCookie(options => {
ConfigureCookie(".OWS.Core.", options.Cookie, webHost);
});

How to change ".AspNetCore.Identity.Application" cookie expiration?

I'm using ASP.NET Core with Identity Server and Open Id Connect as described here. I need to change the time of authentication cookie expiration when the Remember Me option is set (14 days by default). I can see that the cookie named ".AspNetCore.Identity.Application" is responsible for that. I'm trying to set the expiration like this:
.AddCookie(options =>
{
options.Cookie.Expiration = TimeSpan.FromDays(1);
options.ExpireTimeSpan = TimeSpan.FromDays(1);
})
But it affects another cookie named ".AspNetCore.Cookies" (containing the same token value), which has Session expiration and doesn't seem to do anything. All the ways to change expiration that I found modify only the ".AspNetCore.Cookies" cookie, I couldn't find any way to modify the ".AspNetCore.Identity.Application" cookie. (By the way, the services.ConfigureApplicationCookie method isn't triggered for me at all for some reason).
Could anyone please explain what is the difference between these two cookies and how can I modify the ".AspNetCore.Identity.Application" expiration?
My code in Startup.ConfigureServices
services.AddMvc(options =>
{
// ...
})
services.AddAuthorization(options =>
{
options.AddPolicy(PolicyNames.UserPolicy, policyBuilder =>
{
// ...
});
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(options =>
{
options.AccessDeniedPath = "/AccessDenied";
options.SlidingExpiration = true;
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "<authority>";
options.RequireHttpsMetadata = false;
options.ClientId = "<id>";
options.ClientSecret = "<secret>";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
// ...
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "MyCookie";
options.Cookie.Expiration = TimeSpan.FromDays(1);
options.ExpireTimeSpan = TimeSpan.FromDays(1);
});
As Kirk Larkin said ".AspNetCore.Identity.Application" cookie is probably set by the Identity Server application that make use of Asp.Net Identity.
So if you want to manage the user session on the IS4 app you need to configure it there.
IS4 application: ".AspNetCore.Identity.Application" cookie.
If you use Identity to configure the cookie as persistent you need to set the expiration when you sign in the user.
var props = new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
await HttpContext.SignInAsync(userId, userName, props);
If you don't set IsPersistent=true then the cookie has session lifetime and you can set the contained authentication ticket expiration like this:
.AddCookie(options => {
options.Cookie.Name = "idsrv_identity";
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = true;
});
Your client application: : ".AspNetCore.Cookies" cookie.
services.ConfigureApplicationCookie isn't called because if you use .AddCookie(...) this takes the precedence. The options are the same.
This set the app cookie as session.
.AddCookie(options => {
options.Cookie.Name = "myappcookie";
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = true;
});
A way to make the app cookie persistent using OIDC is to set the expiration in the OnSigningIn event in AddCookie.
options.Events.OnSigningIn = (context) =>
{
context.CookieOptions.Expires = DateTimeOffset.UtcNow.AddDays(30);
return Task.CompletedTask;
};
A note about user session.
Every situation is different, so there isn't a best solution, but remember that you have to take care of two user session. One on the IS4 app and one on your client app. These can go out of sync. You need to think if a persistent user session on your client app make sense. You don't want that your user remains logged in your client app when the central SSO (single sign-on) session is expired.
After scrambled through the both AspNetCore 3.1 & IdentityServer 4.0.4 repo,
I found the working way to set default authentication cookie option .
TD;LR:
// in Startup.ConfigureService(IServiceCollection services)
services.PostConfigure<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme, option =>
{
option.Cookie.Name = "Hello"; // change cookie name
option.ExpireTimeSpan = TimeSpan.FromSeconds(30); // change cookie expire time span
});
Full Setup:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
// cookie policy to deal with temporary browser incompatibilities
services.AddSameSiteCookiePolicy();
services.AddDefaultAllowAllCors();
// setting up dbcontext for stores;
services.AddDbContext<ApplicationDbContext>(ConfigureDbContext);
services
.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
// read clients from https://stackoverflow.com/a/54892390/4927172
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.UserInteraction.LoginUrl = "/identity/account/login";
options.IssuerUri = _configuration.GetValue<string>("IdentityServer:IssuerUri");
})
.AddAspNetIdentity<ApplicationUser>()
.AddDeveloperSigningCredential()
.AddConfigurationStore<ApplicationConfigurationDbContext>(option => option.ConfigureDbContext = ConfigureDbContext)
.AddOperationalStore<ApplicationPersistedGrantDbContext>(option => { option.ConfigureDbContext = ConfigureDbContext; })
.AddJwtBearerClientAuthentication()
.AddProfileService<ApplicationUserProfileService>();
services.PostConfigure<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme, option =>
{
option.Cookie.Name = "Hello";
option.ExpireTimeSpan = TimeSpan.FromSeconds(30);
});
services.AddScoped<Microsoft.AspNetCore.Identity.UI.Services.IEmailSender, EmailSender>();
services.Configure<SmsOption>(_configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// use this for persisted grants store
InitializeDatabase(app);
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultAllowAllCors();
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseStatusCodePages(async context =>
{
var response = context.HttpContext.Response;
if (response.StatusCode == StatusCodes.Status401Unauthorized ||
response.StatusCode == StatusCodes.Status403Forbidden)
response.Redirect("/identity/account/login");
if (context.HttpContext.Request.Method == "Get" && response.StatusCode == StatusCodes.Status404NotFound)
{
response.Redirect("/index");
}
});
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapRazorPages();
});
}
Adding this line before services.AddAuthentication is what worked for me eventually with IS4, taken from this github issue:
services.ConfigureApplicationCookie(x =>
{
x.ExpireTimeSpan = TimeSpan.FromDays(1);
});
I followed the sample AuthSamples.Cookies of the Github aspnetcore sources.
public void ConfigureServices(IServiceCollection services)
{
...
// Example of how to customize a particular instance of cookie options and
// is able to also use other services.
services.AddSingleton<IConfigureOptions<CookieAuthenticationOptions>, ConfigureMyCookie>();
}
internal class ConfigureMyCookie : IConfigureNamedOptions<CookieAuthenticationOptions>
{
// You can inject services here
public ConfigureMyCookie()
{
}
public void Configure(string name, CookieAuthenticationOptions options)
{
// Identityserver comes with two cookies:
// Identity.Application
// Identity.External
// you can change the options here
{
options.ExpireTimeSpan = TimeSpan.FromHours(8);
}
}
public void Configure(CookieAuthenticationOptions options)
=> Configure(Options.DefaultName, options);
}