ASP.NET Core: OpenIDConnect: Keycloak: Logout: not redirecting to keycloak login page - asp.net-core

I am implementing openidconnect authentication using keycloak server in my asp.net core application. The login works ok, but I'm not able to implement logout. The behavior is that it takes me to the home page of my web app; instead it should take me to the keycloak login page. Below is my code:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
// Store the session to cookies
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// OpenId authentication
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(cookie =>
{
cookie.Cookie.Name = "keycloak.cookie";
cookie.Cookie.MaxAge = TimeSpan.FromMinutes(60);
cookie.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
cookie.SlidingExpiration = true;
})
.AddOpenIdConnect(options =>
{
options.Authority = Configuration.GetSection("Keycloak")["Authority"];
//Keycloak client ID
options.ClientId = Configuration.GetSection("Keycloak")["ClientId"];
//Keycloak client secret
options.ClientSecret = Configuration.GetSection("Keycloak")["ClientSecret"];
// For testing we disable https (should be true for production)
options.RequireHttpsMetadata = false;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
// OpenID flow to use
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.Events.OnSignedOutCallbackRedirect += context =>
{
context.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
}
Index.shtml:
<a class="nav-link text-light" asp-page-handler="Logout" asp-page="/Pages/Index">Sign out</a>
Index.cshtml.cs:
public IActionResult Logout()
{
return new SignOutResult(
new[] {
OpenIdConnectDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme
});
}

You are not supposed to return anything from the Logout method, because SignOutAsync genereates its own response.
So, this is how I do logouts in my applications:
/// <summary>
/// Do the logout
/// </summary>
/// <returns></returns>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
//Important, this method should never return anything.
}

Related

openid connect: asp.net core: logging back in after a successful logout

I have an asp.net core web application that uses keycloak openidconnect for authentication. I have configured a client in keycloak for standard and implicit flow and have specified valid redirect uris. When the app is tun, it prompts me with a keycloak login page which is correct and then redirects me to my application page. Logout button logs the user out(I can see the cookie being cleared). But when I click on the login button again, instead of prompting me with a keycloak login page, it directly takes me back to my application's home page. In fiddler, I can see it hitting the keycloak server; a new token is issued. I think I'm missing some configuration in keycloak server. Any help is appreciated.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
// Store the session to cookies
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// OpenId authentication
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(cookie =>
{
cookie.Cookie.Name = "keycloak.cookie";
cookie.Cookie.MaxAge = TimeSpan.FromMinutes(60);
cookie.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
cookie.SlidingExpiration = true;
})
.AddOpenIdConnect(options =>
{
options.Authority = Configuration.GetSection("Keycloak")["Authority"];
//Keycloak client ID
options.ClientId = Configuration.GetSection("Keycloak")["ClientId"];
//Keycloak client secret
options.ClientSecret = Configuration.GetSection("Keycloak")["ClientSecret"];
// For testing we disable https (should be true for production)
options.RequireHttpsMetadata = false;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
// OpenID flow to use
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
});
}
index.cshtml:
#if(User.Identity.IsAuthenticated)
{
<form class="form-inline" asp-page="/Index" asp-page-handler="Logout">
<button type="submit" class="nav-link btn btn-link text-light">Sign out</button>
</form>
} else{
<a class="nav-link text-light" asp-page="/Index">Sign in</a>
}
Index.cshtml.cs:
public void OnGet(){...}
public async Task<IActionResult> OpPostLogout(){
Console.WriteLine("Logging out...");
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
Console.WriteLine("Signed out of Cookie Authentication!");
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
Console.WriteLine("Signed out of OpenIDConnect!");
return RedirectToPage("./Logout");
}
I modified my Logout method to the following:
public IActionResult OnPostLogout()
{
return new SignOutResult(
new[] {
OpenIdConnectDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme
});
}
In my keycloak server, I modified the redirecturi and postlogout redirect uri to my application uri(for example: https://localhost:5001/*)
My StartUp.cs:
services.AddAuthentication(options =>
{
// Store the session to cookies
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// OpenId authentication
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration.GetSection("Keycloak")["Authority"];
//Keycloak client ID
options.ClientId = Configuration.GetSection("Keycloak")["ClientId"];
//Keycloak client secret
//options.ClientSecret = Configuration.GetSection("Keycloak")["ClientSecret"];
// For testing we disable https (should be true for production)
options.RequireHttpsMetadata = false;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
// OpenID flow to use
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
});

User.Identity.IsAuthenticated returns false after login while using OpenId Connect with Auth0

I am trying to implement user authentication in an ASP.Net Core (v2.1) MVC application using OpenId Connect and Auth0. I have the required configurations stored in the AppSettings files and application runs well till the Auth0 login page comes. Post login it hits the Callback URL which basically invokes a method (method name is Callback) in my Account Controller. In the callback method I am trying to get the access token if the user is authenticated. However, the User.Identity.IsAuthenticated returns false. Here is my code in the Startup.cs file--
public void ConfigureServices(IServiceCollection services)
{
//Set Cookie Policy
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Add authentication services
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options => {
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
options.ResponseType = "code";
options.Scope.Clear();
options.Scope.Add("openid");
options.CallbackPath = new PathString("/oauth/callback");
options.ClaimsIssuer = "Auth0";
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
And here is my code in the Account Controller
public class AccountController : Controller
{
public async Task Login(string returnUrl = "/")
{
await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { RedirectUri = returnUrl });
}
[Authorize]
public async Task Logout()
{
await HttpContext.SignOutAsync("Auth0", new AuthenticationProperties
{
RedirectUri = Url.Action("Index", "Home")
});
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
public IActionResult AccessDenied()
{
return View();
}
[Authorize]
public IActionResult Claims()
{
return View();
}
[Route("/oauth/callback")]
public async Task<ActionResult> CallbackAsync()
{
if (User.Identity.IsAuthenticated)
{
string accessToken = await HttpContext.GetTokenAsync("access_token");
}
return RedirectToAction("Claims", "Account");
}
}
Please help. Any help will be appreciated.
Thanks,
Amit Anand
In fact i'm not sure why your custom CallbackAsync method fires during OIDC login . The callback url of OIDC middleware will handle token valiation ,token decode,exchange token and finally fill the user principle . You shouldn't handle the process and let OIDC middlware handle it , so change the route of the CallbackAsync method(or change the CallbackPath in OIDC middleware , but of course the url should match the url config in Auth0's portal ) , for example : [Route("/oauth/callbackAfterLogin")] .
After change that , the process will be : user will be redirect to Auth0 for login -->Auth0 validate the user's credential and redirect user back to url https://localhost:xxx/oauth/callback-->OIDC middlware handle token --> authentication success . If you want to redirect to CallbackAsync(route is /oauth/callbackAfterLogin) and get tokens there , you can directly pass the url in ChallengeAsync method when login :
await HttpContext.ChallengeAsync("Auth0",
new AuthenticationProperties() { RedirectUri = "/oauth/callbackAfterLogin"});

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);
}

Asp.net Core 2 with IdentityServer4 - Redirect to Login after cookie expiration

I have an Asp.net Core 2.2 MVC application that authenticates using an IdentityServer4 server.
It is configured as you can see on the bottom, with really short times for quick testing.
The desired behavior is:
Login (suppose without the "remember me checked")
Do things...
Wait until the session expires
On the next navigation click redirect on the login page for a new interactive sign-in
I supposed I must work on cookies and session server side, but my first doubt is that I have to work more with id_token.
Anyway the current behavior is:
Login without the "remember me checked"
Wait until the session expires
Click on a dummy page and I see that the session is empty (as expected) -> The login is available on the top menu
So I click on login -> No login page showed -> a new session server side is available and in the browser there is a new value of ".AspNetCore.Cookies" but the same for ".AspNetCore.Identity.Application" and "idsrv.session".
If I logout, the cookie client side is correctly removed, so at the next login shows the expected credential form.
What I'm doing wrong?
Is it correct to try to get a new interactive sign-in checking the cookie expiration?
Do I have to follow another way working on the ids (id_token) objects?
CODE
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfiguration>(Configuration);
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromSeconds(30);
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = Configuration.GetValue<string>("IdentitySettings:Authority");
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.SaveTokens = true;
options.Events.OnTicketReceived = async (context) =>
{
context.Properties.ExpiresUtc = DateTime.UtcNow.AddSeconds(30);
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment() || env.IsStaging())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseAuthentication();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
EDIT
The logout is done as following
public async void OnPost()
{
await HttpContext.SignOutAsync("Cookies");
await HttpContext.SignOutAsync("oidc",
new AuthenticationProperties
{
RedirectUri = "http://localhost:5002"
});
}

Using [Authorize] with OpenIdConnect in MVC 6 results in immediate empty 401 response

I'm trying to add Azure AD authentication to my ASP.NET 5 MVC 6 application and have followed this example on GitHub. Everything works fine if I put the recommended code in an action method:
Context.Response.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
However, if I try using the [Authorize] attribute instead, I get an immediate empty 401 response.
How can I make [Authorize] redirect properly to Azure AD?
My configuration is as follows:
public void ConfigureServices(IServiceCollection services) {
...
services.Configure<ExternalAuthenticationOptions>(options => {
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
...
app.UseCookieAuthentication(options => {
options.AutomaticAuthentication = true;
});
app.UseOpenIdConnectAuthentication(options => {
options.ClientId = Configuration.Get("AzureAd:ClientId");
options.Authority = String.Format(Configuration.Get("AzureAd:AadInstance"), Configuration.Get("AzureAd:Tenant"));
options.RedirectUri = "https://localhost:44300";
options.PostLogoutRedirectUri = Configuration.Get("AzureAd:PostLogoutRedirectUri");
options.Notifications = new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = OnAuthenticationFailed
};
});
...
}
To automatically redirect your users to AAD when hitting a protected resource (i.e when catching a 401 response), the best option is to enable the automatic mode:
app.UseOpenIdConnectAuthentication(options => {
options.AutomaticAuthentication = true;
options.ClientId = Configuration.Get("AzureAd:ClientId");
options.Authority = String.Format(Configuration.Get("AzureAd:AadInstance"), Configuration.Get("AzureAd:Tenant"));
options.RedirectUri = "https://localhost:44300";
options.PostLogoutRedirectUri = Configuration.Get("AzureAd:PostLogoutRedirectUri");
options.Notifications = new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = OnAuthenticationFailed
};
});