In ASP Net Core 3.1 Expiration cookie is not redirecting to login page when using ajax - asp.net-core

In my app, when my cookie expire, I'm redirect to my Account/Login page. But When I call ajax method and cookie is expired , the action return 401 and I'm not redirecting to my Account/login page...
I add [Authorize] attribute on my controller.
The xhr.status parameter return 401.
Example ajax method :
$(document).on('click', '.ajax-modal', function (event) {
var url = $(this).data('url');
var id = $(this).attr('data-content');
if (id != null)
url = url + '/' + id;
$.get(url)
.done(
function (data) {
placeholderElement.html(data);
placeholderElement.find('.modal').modal('show');
}
)
.fail(
function (xhr, httpStatusMessage, customErrorMessage) {
selectErrorPage(xhr.status);
}
);
});
My ConfigureServices method :
public void ConfigureServices(IServiceCollection services)
{
#region Session
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(1000);
options.Cookie.HttpOnly = true; // permet d'empecher à du code JS d'accèder aux cookies
// Make the session cookie essential
options.Cookie.IsEssential = true;
});
#endregion
#region Cookie
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "TestCookie";
options.ExpireTimeSpan = TimeSpan.FromSeconds(10);
options.LoginPath = "/Account/login";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.Cookie.SameSite = SameSiteMode.Strict;
});
#endregion
Thanks for your help

I came across the issue where I am using cookie authentication in .NET Core 5, yet once the user is authenticated, everything BUT any initial AJAX request in the application works.
Every AJAX request would result in a 401. Even using the jQuery load feature would result in a 401, which was just a GET request to a controller with the [Authorize(Role = "My Role")]
However, I found that I could retrieve the data if I grabbed the URL directly and pasted it in the browser. Then suddenly, all my AJAX worked for the life of the cookie. I noticed the difference in some of the AJAX posts. The ones that didn't work used AspNetCore.AntiForgery in the headers, whereas the ones that did use AspNetCore.Cookies that authenticated.
My fix was to add a redirect in the OnRedirectToLogin event under cookie authentication. It works for all synchronous and asynchronous calls ensuring that AJAX redirects to the login page and authenticates as the current user. I don't know if this is the proper way to handle my issue, but here is the code.
EDIT: I should mention that all of the AJAX code worked perfectly in my .NET 4 web application. When I changed to 5, I experienced new issues.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => {
o.LoginPath = "/Account/Login";
o.LogoutPath = "/Account/Logout";
o.AccessDeniedPath = "/Error/AccessDenied";
o.SlidingExpiration = true;
//add this to force and request to redirect (my purpose AJAX not going to login page on request and authenticating)
o.Events.OnRedirectToLogin = (context) => {
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});

Related

.net 5 Authentication Cookie not set

I have a Umbraco 9 .Net 5 AspNet Core project.
I'm trying to set an auth cookie. I've followed microsofts guide and got it working in a seperate project but when trying to implement it in my Umbraco project it fails. I'm not sure why but I guess the Umbraco 9 Configuration has a part in it.
I've got as far as getting User.Identity.IsAuthenticated = true in the same controller as I sign in but as soon as I redirect to another controller the Authentication status is false.
I also try to set the LoginPath option when configure the cookie but it still redirect to the default path (/Account/Login) so something here is no working either
My StartUp.cs looks like following
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbraco(mEnvironment, mConfig)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.Build();
services.AddDistributedMemoryCache();
//services.AddSession(options =>
//{
// options.IdleTimeout = TimeSpan.FromSeconds(10);
// options.Cookie.HttpOnly = true;
// options.Cookie.IsEssential = true;
//});
services.AddControllersWithViews();
services.AddRazorPages();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.LoginPath = "/portal/"; //not working, still redirects to default
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseAuthorization();
//umbraco setup
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
//app.UseSession();
}
My Login controller action looks like follows:
public async Task<ActionResult> Login()
{
var claimsIdentity = new ClaimsIdentity(new List<Claim>
{
new Claim(UserClaimProperties.UserRole, MemberRole, ClaimValueTypes.String)
}, CookieAuthenticationDefaults.AuthenticationScheme);
var authProps = new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
IsPersistent = true,
AllowRefresh = true,
RedirectUri = "/"
};
await HttpContext.SignInAsync(
//CookieAuthenticationDefaults.AuthenticationScheme, //from MS-example but isAuth will be false using this
new ClaimsPrincipal(claimsIdentity),
authProps);
var isAuthenticated = User.Identity.IsAuthenticated;
return Redirect("/myview/");
}
If I set the Auth Scheme to "Cookies" in SignInAsync like it is in the microsoft example isAuthenticated will be false but without this I'll at least get it true here.
When redirected to the next action the User.Identity.IsAuthenticated is false.
Any suggestions why that is or why my LoginPath configuration wont work?
Edit: I don't want to create Umbraco members for each user that logs in. I just want to sign in a user to the context and be able to validate that the user is signed in by myself in my controllers.
Edit 2: I've try to catch the sign in event and got a breakpoint in that even. In my demo app(without umbraco) I'll get to the breakpoint in the one with Umbraco this breakpoint is never hit so. Is this because Umbraco probably override this or hijack the event?
Not sure why but after testing different Authentication schemes I got an error that the schemes I tested was not registered and I got a list of already registered schemes.
I thought that by doing this
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.LoginPath = "/portal/"; //not working, still redirects to default
});
I've registered the "Cookies" scheme.
One of the schemes listed as registered was "Identity.Application" and by using that one I could get the User identity from the context in my redirect controller.

How to invalidate the session after log out in IdentityServer4

We have website with SSO using IdentitySrver4. We recently tested our site for security and we found one vulnerability which is as follow,
A session token for the application remained valid (and could be used
to authenticate requests to the application) even after the logout
function had been invoked in the associated session. This indicated
that the session termination mechanism was not fully effective and
increased the possibility of unauthorised access to the application.
It should be noted that the tokens did have an effective time out
after a period of time. The logout function terminated the associated
session client-side (by removing the session cookie from the user’s
browser) but the session remained valid server-side. Requests which
were made after the logout function had been used, but which provided
the original session cookie, continued to be successful.
Following are code snippets,
IdentityServer
StartUp ConfigureServices:-
services.AddIdentityServer(.......
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opt=> {
opt.ExpireTimeSpan = TimeSpan.FromMinutes(Convert.ToInt32(Configuration["CookieTimeOut"]));
//This has time limit of 30 minutes
})
.AddOpenIdConnect("oidc", opts =>
{.......
The Login code is as follow,
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, cp);
LogOut method in IdentityServer:-
[HttpGet]
public async Task<IActionResult> Logout(string clientId, string returnUrl, string culture)
{
var clientsList = new List<Client>();
// delete authentication cookie
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var cookiesToBeDeleted = Request.Cookies.Keys;
foreach (string cookie in cookiesToBeDeleted)
{
Response.Cookies.Delete(cookie);
}
var logoutURL = _configuration["DefaultLogoutRedirectUrl"];
if (Uri.IsWellFormedUriString(returnUrl, UriKind.Absolute))
{
//TODO: validate that the return url belongs to the client who has initiated the logout request
//if the url validation fails then we should return to a pre-determined url that is mentioned in the config
logoutURL = returnUrl;
}
var vm = new LoggedOutViewModel()
{
PostLogoutRedirectUri = logoutURL,
SignOutUrls = _clients.Value
.Where(client => !string.IsNullOrWhiteSpace(client.FrontChannelLogoutUri))
.Select(client => client.FrontChannelLogoutUri),
ClientName = clientId,
AutomaticRedirectAfterSignOut = true
};
//If there is no return url then display a local logged out page
return View("LoggedOut", vm);
}
[HttpGet]
public IActionResult LoggedOut(string returnUrl)
{
var vm = new LoggedOutViewModel()
{
PostLogoutRedirectUri = returnUrl,
SignOutUrls = null,
AutomaticRedirectAfterSignOut = true
};
return View(vm);
}
We have used FrontChannel logouts,Here all the client's "FrontChannelLogoutUri" are rendered in "IFrame" in logout page of the Identity Server.
Client Code (MVC App):-
StartUp ConfigureServices:-
services.AddAuthentication(options =>
{
options.DefaultScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, ck =>
{
ck.Cookie.Name = "ClientCookie";
ck.ExpireTimeSpan = TimeSpan.FromMinutes(Convert.ToInt32(Configuration["CookieTimeOut"]));
//This also has value of 30 minutes.
})
.AddOpenIdConnect("oidc", opts =>
{.......
LogOut function in Client App :-
public async Task<IActionResult> Logout(string path)
{
var logoutUrl = //This is IdentityServer LogOut Method URL
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var prop = new Microsoft.AspNetCore.Authentication.AuthenticationProperties()
{
RedirectUri = logoutUrl
};
await HttpContext.SignOutAsync("oidc", prop);
return Redirect(logoutUrl);
}
The FrontChannel method for Client App:-
[AllowAnonymous]
public async Task<IActionResult> ForcedSignout()
{
var cookiesToBeDeleted = Request.Cookies.Keys;
foreach (string cookie in cookiesToBeDeleted)
{
Response.Cookies.Delete(cookie);
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return View();
}
The logout workflow is as follow:-
When logout is called from Client MVC App, we have called both
HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme) & HttpContext.SignOutAsync("oidc", prop).
Then user is redirected to the IdentityServer Logout method, which again calls the HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme).
When Identity's logout page is rendered we generate the "IFrames" with Client's "FrontChannelLogoutUri" (in this case "ForcedSignout()" of Client App).
"ForcedSignout" method again deletes the cookies and call HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme).
Steps to replicate the issue:-
We captured the Client App's edit method in postman.
The postman request with changed data was run and it worked.
After that user was logged out of the Client App and the postman request was again run which ran successfully even though we have logged out.
Then we waited for 30 minutes (Inactivity) and tried the postman request but this time it did not work as the sessions had timed out.
We need to know, how can we remove/invalidate the session from server when the user logs out of the application? Thank you.

AuthenticateResult.Succeeded is false with Okta and Sustainsys.SAML2

I have a .Net Core 2 application which leverages Sustainsys.Saml2.AspNetCor2 (2.7.0). The front end is an Angular application. The SAML approach I'm taking is based on, and very similar to, the approach taken in this reference implementation: https://github.com/hmacat/Saml2WebAPIAndAngularSpaExample
*Everything works fine with the test IDP (https://stubidp.sustainsys.com).
But when we try to integrate with Okta, the AuthenticateResult.Succeeded property in the callback method (see below) is always false, even though the SAML posted to the ASC endpoint appears to indicate a successful authentication. We are not seeing any errors at all. It's just not succeeding.
(Note that my company does not have access to Okta - that is maintained by a partner company.)
Here is the server code in the controller:
[AllowAnonymous]
[HttpPost, HttpGet]
[Route("api/Security/InitiateSamlSingleSignOn")]
public IActionResult InitiateSamlSingleSignOn(string returnUrl)
{
return new ChallengeResult(
Saml2Defaults.Scheme,
new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(SamlLoginCallback), new { returnUrl })
});
}
[AllowAnonymous]
[HttpPost, HttpGet]
[Route("api/Security/SamlLoginCallback")]
public async Task<IActionResult> SamlLoginCallback(string returnUrl)
{
var authenticateResult = await HttpContext.AuthenticateAsync(ApplicationSamlConstants.External);
if (!authenticateResult.Succeeded)
{
return Unauthorized();
}
// more code below, never reached
}
Here is a screenshot of some of the SAML sent by Okta, captured using the Chrome extension, SAML-tracer:
I don't know how to investigate this further.
Any help would be most appreciated!
In the ConfigureServices method, in case it's useful, I have the following (in relevant part):
public void ConfigureServices(IServiceCollection services)
{
// [snip]
if (usingSAML)
{
services.Configure<CookiePolicyOptions>(options =>
{
// SameSiteMode.None is required to support SAML SSO.
options.MinimumSameSitePolicy = SameSiteMode.None;
options.CheckConsentNeeded = context => false;
// Some older browsers don't support SameSiteMode.None.
options.OnAppendCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});
authBuilder = services.AddAuthentication(o =>
{
o.DefaultScheme = ApplicationSamlConstants.Application;
o.DefaultSignInScheme = ApplicationSamlConstants.External;
o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
authBuilder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
// see https://stackoverflow.com/questions/46243697/asp-net-core-persistent-authentication-custom-cookie-authentication
options.ExpireTimeSpan = new System.TimeSpan(365, 0, 0, 0, 0);
options.AccessDeniedPath = new PathString("/login");
options.LoginPath = new PathString("/login");
})
.AddCookie(ApplicationSamlConstants.Application)
.AddCookie(ApplicationSamlConstants.External)
.AddSaml2(options =>
{
options.SPOptions.EntityId = new EntityId(this.Configuration["Saml:SPEntityId"]);
options.IdentityProviders.Add(
new IdentityProvider(
new EntityId(this.Configuration["Saml:IDPEntityId"]), options.SPOptions)
{
MetadataLocation = this.Configuration["Saml:IDPMetaDataBaseUrl"],
LoadMetadata = true,
});
options.SPOptions.ServiceCertificates.Add(new X509Certificate2(this.Configuration["Saml:CertificateFileName"]));
});
}
// [snip]
}
UPDATE: I modified the code to capture more logging information, and what I have found is that, at the Saml2/Acs endpoint, the user is being authenticated.
In the log files, I see this:
2020-09-14 09:28:09.307 -05:00 [DBG] Signature validation passed for Saml Response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id
2020-09-14 09:28:09.369 -05:00 [DBG] Extracted SAML assertion id1622894416505593469999142
2020-09-14 09:28:09.385 -05:00 [INF] Successfully processed SAML response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id and authenticated bankoetest#sfi.cloud
However, when I get to the SamlLoginCallback method, this authentication information is not present in the AuthenticateResult obtained by this call:
var authenticateResult = await HttpContext.AuthenticateAsync(ApplicationSamlConstants.External);
My custom logging information for the authentication result object looks like this:
2020-09-14 09:28:09.432 -05:00 [ERR] SAML Authentication Failure: authenticateResult.Failure (Exception object) is null;
No information was returned for the authentication scheme;
authenticateResult.Principal is null;
authenticateResult.Properties is null.
authenticateResult.Ticket is null.
What could be going wrong?
The root cause here was ultimately the result of differences in the case of the Url used by Okta vs our code in redirect logic. The URLs matched, but the case did not. This caused cookies to be unreadable by later-invoked methods which were being sent to a URL which was different, even though the difference was only in the casing of the path. Once we made sure that all paths matched exactly, down to the casing, it started working.

Unable to expire a .NetCore cookie

I'm using .NetCore and Cookie authentication. Account management (login, logout, etc) is all managed through an API as opposed to using the Identity UI. My understanding is that the server can't actually send anything to the client to actually remove the cookie, rather, I would need to "expire" the cookie on the server and return it to the client.
So here is my setup
Startup.cs
services.AddAuthentication("mycookie")
.AddCookie("mycookie", options => {
options.Cookie.HttpOnly = true;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.Cookie.Expiration = TimeSpan.FromMinutes(30);
});
In my ApiController I have the following:
AccountController.cs
[HttpGet("signout")]
public async Task<IActionResult> SignOut()
{
var temp = new AuthenticationProperties()
{
ExpiresUtc = DateTime.Now.AddDays(-1)
}
await HttpContext.SignOutAsync("mycookie",temp);
return Ok();
}
However, my cookie never seems to expire when I call signout. I noticed in the browser dev console, the cookie says Expires on: Session. I would've expected to see a date/time there.
How do I get rid of the cookie or expire it when signout is called?
use the Clear-Site-Data response header. Among other things, it allows you to clear cookies on the client's browser.
In my example below, I'm clearing the user's cache and cookies for my domain.
public IActionResult Logout()
{
this.HttpContext.Response.Headers.Add("Clear-Site-Data", new StringValues(new string[] { "\"cache\"", "\"cookies\"" }));
return Ok();
}
First be sure to check browser support for this header.
If your browser does not support Clear-Site-Data, then you should be able to expire them like so:
this.Response.Cookies.Append("cookieName", "deleted", new CookieOptions()
{
Expires = DateTimeOffset.MinValue
});
When this logout response returns to the browser, I'm able to see the cookies disappear (using Chrome 73.0.3683.86, Developer Tools/Storage/Cookies/{myDomain} )

How to redirect to log in page on 401 using JWT authorization in ASP.NET Core

I have this JWT authorization configuration in my Startup.cs:
services.AddAuthentication(opts =>
{
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opts =>
{
opts.RequireHttpsMetadata = false;
opts.SaveToken = true;
opts.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my_secret_key")),
ValidIssuer = "iss",
ValidAudience = "aud",
ValidateIssuerSigningKey = true,
ValidateLifetime = true
};
});
My HomeController has [Authorize] attribute. So upon access to Home/Index, I get a 401 response and I am presented with a blank page. I want to redirect to my Account/LogIn page but I am not sure how to do it.
I read that this shouldn't automatically redirect because it won't make sense to API calls if they are not authorized and then you redirect them, so what is the proper way on how I would get them to the login page on 401.
Please bear in mind that in this project, I have both Web API and Action methods with [Authorize] attributes so I need to redirect only when it is an action method.
You may use StatusCodePages middleware. Add the following inot your Configure method:
app.UseStatusCodePages(async context => {
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
// you may also check requests path to do this only for specific methods
// && request.Path.Value.StartsWith("/specificPath")
{
response.Redirect("/account/login")
}
});
I read that this shouldn't automatically redirect because it won't make sense to API calls
this relates to API calls, that returns data other than pages. Let's say your app do call to API in the background. Redirect action to login page doesn't help, as app doesn't know how to authenticate itself in background without user involving.
Thanks for your suggestion... after spending a good time on google i could find your post and that worked for me. You raised a very good point because it does not make sense for app API calls.
However, I have a situation where the Actions called from the app has a specific notation route (/api/[Controller]/[Action]) which makes me possible to distinguish if my controller has been called by Browser or App.
app.UseStatusCodePages(async context =>
{
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
var path = request.Path.Value ?? "";
if (response.StatusCode == (int)HttpStatusCode.Unauthorized && path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase))
{
response.Redirect("~/Account/Login");
}
});
This works for both Razor Pages and MVC Views as follows: response.Redirect("/Login"); for Razor Pages. response.Redirect("/Home/Login"); for MVC Views. In order for this to work, the Authorize filter has to be added to the Controller. The code block also has to be added between app.UseAuthentication(); and app.UseAuthorization(); methods.
app.UseStatusCodePages(async context =>
{
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized ||
response.StatusCode == (int)HttpStatusCode.Forbidden)
response.Redirect("/Home/Login");
});