SignOutAsync Doesn't Sign Out - c#

In my ASP.NET Core application I have this route to sign the user out:
[HttpPost("logout")]
public async Task Logout()
{
await _signInManager.SignOutAsync();
}
and I have this route to check the current username:
[HttpGet("getusername")]
public string GetUserName()
{
return _userManager.GetUserName(User);
}
Yet, when the route is called, the second route continues to behave as if the user is logged in. This question claims that this can happen if the HTTP response that SignOutAsync produces doesn't get processed by the client, but it is requested via AJAX with this code:
async function logout(){
let response = await fetch('/api/user/logout', {method: 'post'});
if(response.ok){
Cookies.remove('username');
window.location.reload();
}else{
alert(`Failed to log out due to networking error: ${response.status} ${response.statusText}`)
}
}
so the change is processed. Why does this code not behave as expected, where the sign out AJAX call ends the session? I'm using Firefox 81.0.1 on Linux as my client.

I figured out the issue. It depends on two unexpected facts about Asp.net Core and Firefox respectively:
When a blank string is returned from an Asp.net Core route, it returns with HTTP 204 No Content.
When Firefox receives a response of HTTP 204 No Content, it does not load the new page, but instead just shows the old page again (with a flash that suggests that an actual refresh is taking place!).
Taken together, when I signed out, the username sign-out occurred as it should on the backend, but I didn't realize it because when I refresh the username checking route in another tab, the username re-appeared. I consider this to be an issue with Firefox.

Related

asp.net core 2.2 response cache not work on browser

The response cache works well in Postman But cache does not work in other browsers and "Cache-Control" in browser is " public, max-age=60" .
And every time refresh browser the action method is called .
my Api Code :
[HttpGet]
[ResponseCache(Duration =60)]
public IActionResult GetAllCustomer()
{
Request.HttpContext.Response.Headers.Add("X-Total-Custumer", _h_Plus_SportsContext.Customer.Count().ToString());
return new ObjectResult(_customerService.GetAllCustomer())
{
StatusCode = (int)HttpStatusCode.OK
};
}
You should provide more context, but Chrome and possible other browser are sending by default Cache-control header with a value of 0 (this happens even if Chrome Dev Tools has Disable cache unchecked):
A workaround is to follow a link to your link to be tested or use Back button as indicated here.

Logout IdentityServer4 from .NET Core logs me back in

I am working to convert a .NET MVC 5 application to core. I am using an IdentityServer4 for Authentication/Authorization and I cannot seem to get the logout working.
public async Task Logout()
{
await HttpContext.Authentication.SignOutAsync("Cookies");
await HttpContext.Authentication.SignOutAsync("oidc");
return Redirect("/");
}
I am using this line to logout, but when I do I see it redirect me back to the login url (because you need to be authenticated for "/") and it logs me back in automatically. I have tried to delete the cookies in the logout method and no dice.
There is nothing in the IdentityServer log outside of the user is logged back in, so I don't think it's a configuration issue (worked in MVC5). Has anyone else had this issue? I think it's a small configuration issue that I am missing or logging out incorrectly.
I have tried this (below) but it throws a 404 on the /connect/endsession url. I think the token is too large for the url (2700+ characters).
public async Task Logout()
{
return SignoutAsycn("Cookies", "oidc");
}
My only thought, when working in the MVC5 app I was debugging using the url localhost.xyz.com and the login server was login.xyz.com. Now I am debugging using localhost:44300 and login.xyz.com. The redirect url is setup to be https://localhost:44300.
Any help would be appreciated.
EDIT
So I added fiddler to the mix to watch what is happening between the Core version and the .NET version and when I logout in the .NET version you can see that there are some calls to the IS4 service (though I cannot see details) and everything works.
In the Core version there are no calls to IS4. I can see that the cookies have been removed on the redirect, but the oidc information seems to be intact.
The /.well-know/ information looks correct as well.
EDIT
Log when hitting logout
2017-07-24T13:42:38.4069514-04:00 0HL6INDDIPV32 [INF] Request starting HTTP/1.1 GET http://localhost:44300/Account/Logout (e5be5b71)
2017-07-24T13:42:38.4164473-04:00 0HL6INDDIPV32 [INF] HttpContext.User merged via AutomaticAuthentication from authenticationScheme: "Cookies". (bdba1d38)
2017-07-24T13:42:38.4264444-04:00 0HL6INDDIPV32 [INF] Executing action method "Framework.Controllers.AccountController.Logout (Framework)" with arguments (null) - ModelState is Valid (ba7f4ac2)
2017-07-24T13:42:40.3758113-04:00 0HL6INDDIPV32 [INF] AuthenticationScheme: "Cookies" was successfully authenticated. (1805f3b3)
2017-07-24T13:42:40.3878092-04:00 0HL6INDDIPV32 [INF] AuthenticationScheme: "oidc" signed out. (d3f50c8d)
2017-07-24T13:42:40.4048013-04:00 0HL6INDDIPV32 [INF] AuthenticationScheme: "Cookies" signed out. (d3f50c8d)
2017-07-24T13:42:40.4127980-04:00 0HL6INDDIPV32 [INF] Executing RedirectResult, redirecting to "/". (d98d540e)
2017-07-24T13:42:40.4167958-04:00 0HL6INDDIPV32 [INF] Executed action "Framework.Controllers.AccountController.Logout (Framework)" in 1993.1739ms (afa2e885)
2017-07-24T13:42:40.4508858-04:00 0HL6INDDIPV32 [INF] Request finished in 2047.3797ms 302 (15c52c40)
2017-07-24T13:42:40.4558821-04:00 0HL6INDDIPV33 [INF] Request starting HTTP/1.1 GET http://localhost:44300/ (e5be5b71)
2017-07-24T13:42:40.4668910-04:00 0HL6INDDIPV33 [INF] Authorization failed for user: null. (a4ab1676)
2017-07-24T13:42:40.4738760-04:00 0HL6INDDIPV33 [INF] Authorization failed for the request at filter '"Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter"'. (8b6446cb)
2017-07-24T13:42:40.4818757-04:00 0HL6INDDIPV33 [INF] Executing ChallengeResult with authentication schemes ([]). (f3dca807)
2017-07-24T13:42:40.5168613-04:00 0HL6INDDIPV33 [INF] AuthenticationScheme: "oidc" was challenged. (d45f1f38)
EDIT
I updated the code to do a couple of things. I removed the claims from the token and it will now call the endsession end point and then redirect me logged back in. I updated the code to post to the endsession (below) and samething. Now if I call the /Account/Logout end point on the Identity Server it still will not log me out.
The identity server logs states it received a request to the /endsession endpoint, and no errors.
Thoughts?
private void Signout()
{
var id = httpContext.HttpContext.Authentication.GetTokenAsync("id_token").Result;
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("https://login.xyz.com:8981/");
var actionName = string.Format("/connect/endsession");
var values = new List<KeyValuePair<string, string>>();
values.Add(new KeyValuePair<string, string>("id_token_hint", id));
values.Add(new KeyValuePair<string, string>("post_logout_redirect_uri", "https://localhost:44300/"));
var content = new FormUrlEncodedContent(values);
var response = httpClient.PostAsync(actionName, content);
if (response.Result.IsSuccessStatusCode)
{
var item = response.Result.Content.ReadAsStringAsync();
var t = string.Empty;
}
}
I have tried this (below) but it throws a 404 on the /connect/endsession url. I think the token is too large for the url (2700+ characters).
Certain browsers from north west America (IE) and certain deployments (e.g. IIS on Azure) limit URL length to 2K by default.
For Azure there's a config workaround - IE is just a fact of life.
Two options:
make your identity token smaller (less claims)
the end_session endpoint also support POST - you can post the parameters instead of doing a GET. IIRC the middleware does not support the OOB - so you would need to make your own network request.
Found it!
Multiple issues
A) I found an issue in the BuildLoggedOutViewModelAsync function to get the PostLogoutRedirectUri correctly. I apparently wasn't setting it back to itself.
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri == null
? logout?.Parameters["post_logout_redirect_uri"]
: logout?.PostLogoutRedirectUri,
B) The clients in Identity Server need the PostLogoutRedirectUris to have the ~/signout-callback-oidc and in the .net core client itself you can set it to what ever.
Then use return new SignOutResult(new[] { "oidc", "Cookies" }); to logout and redirect back in.
Hopefully all of this mess will help someone else out!

HTTP Post to Web API 2 - Options request received and handled no further request received

I have a web application using MVC and AngularJS, which connects to a Web API 2 api, that I have set up in a separate project.
Currently I am able to retrieve information from the Api with no problems.
However when I try to do a HTTP Post I am getting no response, originally I was getting a problem with the pre-flight request failing, I have now handled this in my controller, however it does not send the proper request after it has got an OK message back.
I have included my code for the Angular Factory and the C# Controller in the API.
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class RegisterController : ApiController
{
public string Post()
{
return "success";
}
public HttpResponseMessage Options()
{
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
}
}
var RegistrationFactory = function($http, $q, ApiAddress) {
return function(model) {
// $http.post(ApiAddress.getApiAddress() + '/Register/Post', model.ToString());
$http({
method: "POST",
url: ApiAddress.getApiAddress() + '/Register/Post',
data: model,
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}).success(function(data) {
$location.path("/");
});
}
};
RegistrationFactory.$inject = ['$http', '$q', 'ApiAddress'];
Edit:
I am still not having any joy with this, however I tested in Internet Explorer and it works with no problems at all.
I have got it working in chrome by starting with web security disabled, however obviously this is not ideal as it will not work on a user PC with security enabled.
I see that you have done adaptation for CORS on the server side. But I cannot see any client side (javascript) adaptation. May be you should add the code below before calling the service.
$http.defaults.useXDomain = true;
delete $httpProvider.defaults.headers.common['X-Requested-With'];
Let me know if this fixes the issue. Worked for me in all scenarios :)
It's strange that your GETs work, but your POSTs don't.
I would recommend running the code in Google Chrome with web security enabled (so we can watch it go wrong) and with the F12 Developer Options shown.
Select the Network tab, run your code, and watch what happens when the POST is called.
Does your service return a "200 OK" status, or some other value ?
Does any kind of Response get returned ?
It might be worth trying this, and appending a screenshot of the results in your original question. It might help to identify the cause.
I am still not having any joy with this, however I tested in Internet
Explorer and it works with no problems at all.
Btw, you don't have any single sign-on stuff setup in your company, do you ? We've had issues where IE works fine, but other browsers don't allow single sign-on. Just a thought...
CORS requires a OPTIONS-preflight which has HTTP headers in its response that tell the browser whether it is allowed to access the resource.
E.g. HTTP Response Headers:
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Origin: *
Because you have a custom Options handler in your C# controller, it seems those HTTP headers are not returned, stopping the browser to make the call after the preflight.
Avoid the Options method, and you should be good.

MVC4 Forms Authentication + AJAX action

In my ASP.NET MVC4 application, I'm using forms authentication. One view in the app needs to get some string data via a jQuery post() call. I wrote an action that returns string. Everything works fine until the forms authentication ticket expires. After expiration, the AJAX call to the string action results in following:
The code within the action does not get executed
The action returns a HTTP 200 and not a 401
The action returns HTML of the Login view as string
Because 200 status was returned and not 401, the control ends up in jqXHR.done() instead of jqXHR.fail()
Is this expected behavior? Can I do something to make the action return a 401? Or is there something else I should be doing to handle this?
Putting the code in Application_EndRequest() did not work for me. Here is what works in my case:
protected void Application_BeginRequest()
{
HttpRequestBase request = new HttpRequestWrapper(Context.Request);
if (request.IsAjaxRequest())
{
Context.Response.SuppressFormsAuthenticationRedirect = true;
}
}
Yes this is the expected behaviour.
In Asp.Net 4.5 the HttpResponse.SuppressFormsAuthenticationRedirect Property has been added. But the default behaviour is still a redirect to the login page. From MSDN:
By default, forms authentication converts HTTP 401 status codes to 302 in order to redirect to the login page. This isn't appropriate for certain classes of errors, such as when authentication succeeds but authorization fails, or when the current request is an AJAX or web service request. This property provides a way to suppress the redirect behavior and send the original status code to the client.
You could use this property or try the following workaround from this answer:
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == 302 && Context.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
Context.Response.Clear();
Context.Response.StatusCode = 401;
}
}
Or you could take a look at this question and answers: Forms authentication: disable redirect to the login page and pick a method that suits you to return a 401.

How to ensure a url is called from my application and not manually from browser

I have an application that contains a button, on click of this button, it will open a browser window using a URL with querystring parameters (the url of a page that i am coding).
Is there a way to ensure that the URL is coming from my application and only from my application - and not just anyone typing the URL manually in a webbrowser?
If not, what is the best way to ensure that a specific URL is coming from a specific application - and not just manually entered in the address bar or a web browser-
Im using asp.net.
You can check if the request was made from one of the pages of your application using:
Request.UrlReferrer.Contains("mywebsite.com")
That's the simple way.
The secure way is to put a cookie on the client containing a value encrypted using a secure key or hashed using a secure salt. If the cookie is set to expire when the page is closed it should be impossible for someone to forge.
Here's an example:
On the pages that would redirect to the page you are trying to protect:
HttpCookie cookie = new HttpCookie("SecureCheck");
//don't set the cookie's expiration so it's deleted when the browser is closed
cookie.Value = System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(Session.SessionID, "SHA1");
Response.Cookies.Add(cookie);
On the page you are trying to protect:
//check to see if the cookie is there and it has the correct value
if (string.IsNullOrEmpty(Request.Cookies["SecureCheck"]) || System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(Session.SessionID, "SHA1") != Request.Cookies["SecureCheck"])
throw Exception("Invalid request. Please access this page only from the application.");
//if we got this far the exception was not thrown and we are safe to continue
//insert whatever code here
There's no reliable way to do this for a GET request, nor is their any reason to try for a legitimate user. What you should do instead is ensure that regardless of where the request comes from the user has the proper permissions and access rights and that the session is protected appropriately (HTTP only cookies, SSL, etc.) If the request is changing data, then it should be a POST, not a GET, and it should be accompanied by some suitable cross-site request forgery prevention techniques (such as a cookie containing a nonce that is verified against a matching nonce on the form itself).
There is no way, other than rejecting the request if it doesn't contain a previously generated random one-time token in the parameters (that would be stored in the session, for example).
While there is no 100% secure way to do this, what I am suggesting might at least take care of your basic needs.
This is what you can do .
Client: Add a HTTP header with an encoded string that is like hash (sha256) of some word.
Then make your client always do a POST request instead of GET.
Server: Check the HTTP Header for encoded string. Also make sure it is a POST request.
This is not 100% as ofcourse someone smart enough could figure out and still generate a request, but depending on your need you might find this enough or not
You can check the referer, the user agent, add an additional header to the request, always do post requests to that url. However, considering HTTP is transmitted in plain text, somebody is always able to let wireshark or fiddler run, capture the HTTP packets and recreate the requests with your measures in place.
Pass parameters from your application so that you can verify on the server side.
I suggest you use an encryption algorithm and generate random text using a password(key). Then, decrypt the param on the server side and check if it matches your expectation.
I am not very clear though. sorry about that, If had to do something like this, then, I would do something similar to mentioned above.
You can use to check the header on MVC controller like Request.Headers["Accept"]; if it is coming from your code in angularjs or jquery:
sample angularjs like this:
var url = ServiceServerPath + urlSearchService + '/SearchCustomer?input=' + $scope.strInput;
$http({
method: 'GET',
url: url,
headers: {
'Content-Type': 'application/json'
},.....
And on the MVC [HttpGet] Action method
[HttpGet]
[PreventDirectAccess]//It is my custom filters
// ---> /Index/SearchCustomer?input={input}/
public string SearchCustomer(string input)
{
try
{
var isJsonRequestOnMVC = Request.Headers["Accept"];//TODO: This will check if the request comes from MVC else comes from Browser
if (!isJsonRequestOnMVC.Contains("application/json")) return "Error Request on server!";
var serialize = new JavaScriptSerializer();
ISearch customer = new SearchCustomer();
IEnumerable<ContactInfoResult> returnSearch = customer.GetCustomerDynamic(input);
return serialize.Serialize(returnSearch);
}
catch (Exception err)
{
throw;
}
}

Categories

Resources