Browser back button does not execute the controller method - caching

I am working in asp.net core. I am facing an issue which is when I am returning to last visited web page through the browser back button, my controller action method is not being executed.
When we press the back button, the browser fetches data from the cache. So, if we want to execute the controller action method, we need to prevent the browser from caching that page.
I googled a lot about this. Through this, I found a lot of solution based on the cache in ASP.NET MVC. Like, disabling cache.
I checked this site and tried also. https://learn.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-2.2
. It's not working.
We are performing some actions based on the cookies. So disabling cache, should not clear this also.
Is there any another way in ASP.NET Core to execute the controller action method when press browser back button?
Thanks in advance.

You should be careful while using no-cache. For Caching, it plays import role in performance.
If you want to set specific controller action with no-cache, you could follow:
configure CacheProfiles in Startup.cs
services.AddMvc(options =>
{
options.CacheProfiles.Add("Never",
new CacheProfile()
{
Location = ResponseCacheLocation.None,
NoStore = true
});
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
Useage
[ResponseCache(CacheProfileName = "Never")]
public IActionResult Index()
{
return View();
}
If you insist on no cache for all requests, try middleware.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
if (context.Response.Headers.ContainsKey("Cache-Control"))
{
context.Response.Headers["Cache-Control"] = "no-cache,no-store";
}
else
{
context.Response.Headers.Add("Cache-Control", "no-cache,no-store");
}
if (context.Response.Headers.ContainsKey("Pragma"))
{
context.Response.Headers["Pragma"] = "no-cache";
}
else
{
context.Response.Headers.Add("Pragma", "no-cache");
}
return Task.FromResult(0);
});
await next.Invoke();
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

Related

AJAX call to an Action with Authorize attribute in .NET Core 3.1

On my pet project (a lyrics website), I wish to add "like" functionality, like this:
Code is open source (here's my current branch). A click on the heart icon should add a like to the databse for the logged in user, and if the user isn't logged in, it should redirect to the login page (IdentityServer 4, separate project and domain).
Controller Action:
[Authorize]
[Route("lyrics/like/{lyricId}")]
public async Task<IActionResult> Like(
int lyricId)
{
try
{
string userId = User.GetUserId().ToString();
await _lyricsService.LikeLyricAsync(userId, lyricId);
return RedirectToAction("Index", "Home");
}
catch
{
return RedirectToAction("Index", "Home");
}
}
JavaScript on the View:
<script>
docReady(function () {
let likeBtn = document.getElementById('like-btn');
let likeLyric = (event) => {
event.preventDefault();
console.log('attemping to like a lyric!');
// 1. create a new XMLHttpRequest object
let request = new XMLHttpRequest();
// 2. configure the request
request.open('GET', 'https://localhost:5001/lyrics/like/#Model.Id');
// 3. send the request over the network
request.send();
// 4. this will be called after the response is received
request.onload = function () {
if (request.status != 200) {
// analyse http status of the response
alert(`Error ${request.status}: ${request.statusText}`);
} else {
// show the result
alert(`Done, got ${request.response.length} bytes`); // response is the server response
}
};
request.onprogress = function (event) {
if (event.lengthComputable) {
alert(`Received ${event.loaded} of ${event.total} bytes`);
} else {
alert(`Received ${event.loaded} bytes`); // no Content-Length
}
};
request.onerror = function () {
alert("Request failed");
};
}
likeBtn.addEventListener('click', likeLyric);
});
</script>
I tried to expand on the request.onload function by adding an:
else if (request.status === 302) {
window.location = request.response;
}
But it doesn't seem to get to that, the .send() fails. What am I doing wrong here?
Here's a screen grab of what is happening:
The error is:
attemping to like a lyric!
govenda-sera:1 Access to XMLHttpRequest at 'https://localhost:5006/connect/authorize?client_id=bejebeje-mvc-local&redirect_uri=https%3A%2F%2Flocalhost%3A5001%2Fsignin-oidc&response_type=code&scope=openid&code_challenge=2mUDM3-gR1jhn7E2EY7T17FkPTHikE8v-KQOBMskazM&code_challenge_method=S256&response_mode=form_post&nonce=637437449511000684.OWQ3MTM4MjItOTJhOS00YjgzLTk1OTYtYWE2ZGUyMzRlYzUyOWE1MTkwNjgtNzI2YS00OWJjLTgzYjAtOTY1MDQ1ZDU3YzE1&state=CfDJ8DxKnFiqfK1HscY3j3s4hc-YvLoUa_X_46X1CclU7U-RahgrNQULQOLJu6943zTWCYa5Q5acO7g7vx03ddXSOOKkUtxZQAMHSgnQHFzBvhXnoC2i6yS0PpGxns7oA7tuvcgnp-jxub7RePZl5QAe5BwfXWkyHtMkFAmTkuultwz5w-Duenyb4KNrZRk1RLn6TLL93BS6YfIfoozorOnvKel4cFFjxIc7F_QXgVFKZm6ud5lN2nItw5WhkDfU6qMHhUUSQXQRJqWSit4CW_1hPpbHZhJmatXWxD8mLVFcSEKMNQz2UIU00RDxBCQW09Skuy3Uoz50Vwp4dEYPtNIcolIKrLn1pJguNsYRWBw391uWO7rMy9W5DPJV44fMVe8UR5xKNUarkelFX4CzHidF-rE&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0' (redirected from 'https://localhost:5001/lyrics/like/938') from origin 'https://localhost:5001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
govenda-sera:131 GET https://localhost:5006/connect/authorize?client_id=bejebeje-mvc-local&redirect_uri=https%3A%2F%2Flocalhost%3A5001%2Fsignin-oidc&response_type=code&scope=openid&code_challenge=2mUDM3-gR1jhn7E2EY7T17FkPTHikE8v-KQOBMskazM&code_challenge_method=S256&response_mode=form_post&nonce=637437449511000684.OWQ3MTM4MjItOTJhOS00YjgzLTk1OTYtYWE2ZGUyMzRlYzUyOWE1MTkwNjgtNzI2YS00OWJjLTgzYjAtOTY1MDQ1ZDU3YzE1&state=CfDJ8DxKnFiqfK1HscY3j3s4hc-YvLoUa_X_46X1CclU7U-RahgrNQULQOLJu6943zTWCYa5Q5acO7g7vx03ddXSOOKkUtxZQAMHSgnQHFzBvhXnoC2i6yS0PpGxns7oA7tuvcgnp-jxub7RePZl5QAe5BwfXWkyHtMkFAmTkuultwz5w-Duenyb4KNrZRk1RLn6TLL93BS6YfIfoozorOnvKel4cFFjxIc7F_QXgVFKZm6ud5lN2nItw5WhkDfU6qMHhUUSQXQRJqWSit4CW_1hPpbHZhJmatXWxD8mLVFcSEKMNQz2UIU00RDxBCQW09Skuy3Uoz50Vwp4dEYPtNIcolIKrLn1pJguNsYRWBw391uWO7rMy9W5DPJV44fMVe8UR5xKNUarkelFX4CzHidF-rE&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0 net::ERR_FAILED
likeLyric
You can't do an AJAX call to this URL to login the user:
https://localhost:5006/connect/authorize?....
If you want the user to login/authenticate, then you need to redirect the browser to that page.
Or better, don't show the heart icon if the user is not logged in, better to have a login to like button? The user might otherwise be surprised why he needs to login.
It is caused by cors, you need to enable cors in backend.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(name: "AllowOrigins",
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...
app.UseRouting();
app.UseCors("AllowOrigins");
//...
}
}
In addition, can you switch to another browser to access correctly?

SignOut does not redirect to site home page

I'm trying to setup an ASP.net Core 3 MVC app that uses OIDC to connect to my company's SSO portal (OpenAM).
I used Visual Studio 2019 project generator to create a basic app with no authentication and then I added the OIDC client capabilities following the steps at http://docs.identityserver.io/en/latest/quickstarts/2_interactive_aspnetcore.html#creating-an-mvc-client .
Logging in works great with minimal changes to the Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// Setup Identity Server client
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://mycompany.com/ssoservice/oauth2";
options.RequireHttpsMetadata = false;
options.ClientId = "openIdClient";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.ProtocolValidator.RequireNonce = false;
options.SaveTokens = true;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true;
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
// endpoints.MapDefaultControllerRoute();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
I also set up a Logout controller action:
[Authorize]
public IActionResult Logout()
{
return SignOut("Cookies", "oidc");
}
The action actually works, i.e. when activated the cookie is deleted and the user is logged out from the SSO portal, but when the browser redirects to the /signout-callback-oidc endpoint it receives an HTTP 200 response without any content. I would have expected to have it automatically redirect to the site home page "/", which is the default value of the OpenIdConnectOptions.SignedOutRedirectUri property.
What am I missing?
Ok, after fiddling some more time, I found out this is the result of a missing draft implementation in the latest community OpenAM release (and also in the current paid ForgeRock AM, but they are working on it: https://bugster.forgerock.org/jira/browse/OPENAM-13831). Basically, the .net core handler for /signout-callback-oidc relies on having the state parameter available in order to redirect, like Ruard van Elburg mentioned in the comments:
https://github.com/aspnet/AspNetCore/blob/4fa5a228cfeb52926b30a2741b99112a64454b36/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L312-L315
OpenAM does not send back the state parameter, as reported in my logs. Therefore, we need to perform the redirect ourselves - the most straightforward way seems to be using the OnSignedOutCallbackRedirect event:
Startup.cs
services.AddAuthentication(...)
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
...
options.Events.OnSignedOutCallbackRedirect += context =>
{
context.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
...
});
Thanks to all the users that replied to the discussion, your contributions allowed me to find the clues to the correct solution.
you return SignOut,
instead, SignOut user and return RedirectToAction("Home","Index")

How to handle an unauthorized ajax call

I am trying to figure out how to prevent a cors error from showing up in developer tools. The way I get the cors error is when I am using an application but in another tab/window I log out of that application but then go back to the other tab and try to do work. Below is my ajax call.
function RemoveScholarshipRequest(id, name) {
if (confirm("Are you sure you want to delete the scholarship request for " + name + "?")) {
var dataSource = $('#Pending').data('kendoGrid').dataSource;
$.ajax({
type: "POST",
url: '#Url.Action("RemoveRequest", "Admin")',
data: {id: id}
}).done(function (response, data, xhr) {
if (response.success) {
dataSource.read();
alert(response.responseText);
}
else if (!response.success) {
if (response.responseText === "Not Authenticated")
alert(response.responseText);
console.log("error", data.status);
//This shows status message eg. Forbidden
console.log("STATUS: "+JSON.stringify(xhr.status));
}
}).fail(function (response) {
console.log(response);
console.log(JSON.stringify(response));
//window.location.href = "/forms/ScholarshipDisbursement/Admin/PendingRequests";
});
}
}
The controller action that the above ajax method calls is below:
[AllowAnonymous]
[HttpPost]
public ActionResult RemoveRequest(string id)
{
if (!User.Identity.IsAuthenticated)
{
return Json(new { success = false, responseText = "Not Authenticated" }, JsonRequestBehavior.AllowGet);
}
if (User.IsInRole("Developer") || User.IsInRole("BannerAdmin"))
{
new ScholarshipRequestStore().DeleteScholarshipRequest(id);
return Json(new { success = true, responseText = "Successfully deleted" }, JsonRequestBehavior.AllowGet);
}
else
{
return Json(new { success = false, responseText = "You are not an authorized user" }, JsonRequestBehavior.AllowGet);
}
}
One way I get around the cors error is by putting AllowAnonymous on the method and then checking for authentication in the method itself but I don't really like that idea. Is there another way of resolving this issue?
Allow anonymous will not solve this, instead you need to send the allow origin header in your api. You can do this by enabling CORs in the startup class as follows
public void ConfigureServices(IServiceCollection services)
{
// Add Cors
services.AddCors(o => o.AddPolicy("MyPolicy", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
// Add framework services.
services.AddMvc();
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new CorsAuthorizationFilterFactory("MyPolicy"));
});
...
...
...
}
// This method gets called by the runtime. Use this method to configure
//the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// Enable Cors
app.UseCors("MyPolicy");
//app.UseMvcWithDefaultRoute();
app.UseMvc();
...
...
...
}
and then using the "Enable cors" attribute on your controller
[EnableCors("MyPolicy")]
[AllowAnonymous]
[HttpPost]
public ActionResult RemoveRequest(string id)
read this for better idea https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-2.2
Note: I have allowed any origin to talk to the API, you can specify whatever origin you want like "https://example.com"
AllowAnonymous won't resolve a "cross-origin" request. The issue you are getting is due to tabbed browsing within your browser having a shared store of authenticated sessions. When you log out in tab 1, the session cookie is removed and then tab 2 is no longer authenticated. This is why AllowAnonymous "works" because without a current authenticated session, you're an anonymous user.
CORS, on the other hand, is when you allow calls to http://myservice.com to come from a different host like http://myclient.com. Anonymous access won't have any impact on that.

Session cookie not being set on Edge (dot net core)

Session cookies are being set on Chrome, FireFox and even IE but not on Edge
The browser version is Microsoft Edge 42.17134.1.0
DotNet core version is 2.1
and the following information is used in my startup.cs file
public void ConfigureServices(IServiceCollection services) {
services.Configure < CookiePolicyOptions > (options => {
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1).AddJsonOptions(options => {
options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver();
}).AddSessionStateTempDataProvider();
services.AddDistributedMemoryCache();
services.AddSession(o => {
o.IdleTimeout = TimeSpan.FromMinutes(80);
o.Cookie.HttpOnly = true;
o.Cookie.Name = "my-session-cookie";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSession();
app.UseSpaStaticFiles();
app.UseMvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa => {
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment()) {
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
Here are some of the things I've tried out so far:
Adding the IsEssential condition to session options
Removing CookiePolicyOptions and UseCookiePolicy
Attempting to add an expiration date to the session cookie (didn't even start the solution)
Using fetch on Edge is causing the set-cookie header to not set a cookie on the browser
The solution was to add credentials: "same-origin" to the fetch options object
DOT NOT ADD IT TO THE HEADER
Quotes from HERE
By default, fetch won't send or receive any cookies
That means your have add the credentials object to it so it can set those cookies
Since Aug 25, 2017. The spec changed the default credentials policy to
same-origin.
I guess Edge have not implemented that default yet
Here's an example of a working fetch
fetch(link, {
body: JSON.stringify(myDataObject),
method: "POST",
credentials: "same-origin",
headers: {
"content-type": "application/json"
}
});
Open the Edge setting and click the "Advanced settings", under Cookies section, select "Under Cookies section" option, then re-test your application.
If still not working, try to reset your browser configuration to default and test your website again.

When using an API route, return Http Response 401 instead of redirect to login page when not authorised

I'm building an ASP.NET Core 2.0 website using MVC and WebAPI to provide access to a series of microservices. Where a WebAPI controller requires a user to be authenticated and authorised (using the Authorize attribute), any unauthorised or not-logged in user gets the response back as the entire HTML for the MVC login page.
When unauthorised users access the API, I would like to return the HTTP status code 401 and its associated error message in the response, instead of an entire HTML page.
I've looked at a few existing questions and noticed that they either refer to ASP.NET MVC (such as SuppressDefaultHostAuthentication in WebApi.Owin also suppressing authentication outside webapi) which is no good for ASP.NET Core 2.0. Or they are using a hackaround for Core 1.x, which just doesn't seem right (ASP.Net core MVC6 Redirect to Login when not authorised).
Has a proper solution been implemented in Core 2.0 that anyone is aware of? If not, any ideas how it could be implemented properly?
For reference, there's part of a controller as an example:
[Authorize]
[ApiVersion("1.0")]
[Produces("application/json")]
[Route("api/V{ver:apiVersion}/Organisation")]
public class OrganisationController : Controller
{
...
[HttpGet]
public async Task<IEnumerable<string>> Get()
{
return await _organisationService.GetAllSubdomains();
}
...
}
And the configurations within Statup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
// Add API version control
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ErrorResponses = new DefaultErrorResponseProvider();
});
// Add and configure MVC services.
services.AddMvc()
.AddJsonOptions(setupAction =>
{
// Configure the contract resolver that is used when serializing .NET objects to JSON and vice versa.
setupAction.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseStatusCodePagesWithRedirects("/error/index?errorCode={0}");
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
...
}
There is an easy way to suppress redirect to Login page for unathorized requests. Just add following call of ConfigureApplicationCookie extension method in your ConfigureServices:
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
};
});
Or if you need custom error message in response body:
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Some custom error message if required");
};
});
As far as you're using redirects to custom error pages for error codes (UseStatusCodePagesWithRedirects() call in Configure method), you should add filter for 401 error. To achieve this, remove call to UseStatusCodePagesWithRedirects and use UseStatusCodePages extension method with skip of redirect for Unauthorized code:
//app.UseStatusCodePagesWithRedirects("/error/index?errorCode={0}");
app.UseStatusCodePages(context =>
{
if (context.HttpContext.Response.StatusCode != (int)HttpStatusCode.Unauthorized)
{
var location = string.Format(CultureInfo.InvariantCulture, "/error/index?errorCode={0}", context.HttpContext.Response.StatusCode);
context.HttpContext.Response.Redirect(location);
}
return Task.CompletedTask;
});
If you're using JWT for authentication with an ASP.NET Core 2 API; you can configure the unauthorized response when you're configuring the services for Authentication & JWT:
services.AddAuthentication( JwtBearerDefaults.AuthenticationScheme )
.AddJwtBearer(options => options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.NoResult();
c.Response.StatusCode = 401;
c.Response.ContentType = "text/plain";
return c.Response.WriteAsync("There was an issue authorizing you.");
}
});

Resources