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!
Related
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.
My app communicates with an internal web API that requires authentication.
When I send the request I get the 401 challenge as expected, the handshake occurs, the authenticated request is re-sent and everything continues fine.
However, I know that the auth is required. Why do I have to wait for the challenge? Can I force the request to send the credentials in the first request?
My request generation is like this:
private static HttpWebRequest BuildRequest(string url, string methodType)
{
var request = HttpWebRequest.CreateHttp(url);
request.PreAuthenticate = true;
request.AuthenticationLevel = AuthenticationLevel.MutualAuthRequested;
request.Credentials = CredentialCache.DefaultNetworkCredentials;
request.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials;
request.ContentType = CONTENT_TYPE;
request.Method = methodType;
request.UserAgent = BuildUserAgent();
return request;
}
Even with this code, the auth header isn't included in the initial request.
I know how to include the auth info with basic.... what I want to do is to use Windows Auth of the user executing the app (so I can't store the password in a config file).
UPDATE I also tried using a HttpClient and its own .Credentials property with the same result: no auth header is added to the initial request.
The only way I could get the auth header in was to hack it in manually using basic authentication (which won't fly for this use-case)
Ntlm is a challenge/response based authentication protocol. You need to make the first request so that the server can issue the challenge then in the subsequent request the client sends the response to the challenge. The server then verifies this response with the domain controller by giving it the challenge and the response that the client sent.
Without knowing the challenge you can't send the response which is why 2 requests are needed.
Basic authentication is password based so you can short circuit this by sending the credentials with the first request but in my experience this can be a problem for some servers to handle.
More details available here:
http://msdn.microsoft.com/en-us/library/windows/desktop/aa378749(v=vs.85).aspx
I'm not 100% sure, but I suspect that there is no way around this; it's simply the way HttpWebRequest works.
In the online .NET source, function DoSubmitRequestProcessing which is here, you can see this comment just after the start of the function, line 1731:
// We have a response of some sort, see if we need to resubmit
// it do to authentication, redirection or something
// else, then handle clearing out state and draining out old response.
A little further down (line 1795) (some lines removed for brevity)
if (resubmit)
{
if (CacheProtocol != null && _HttpResponse != null) CacheProtocol.Reset();
ClearRequestForResubmit(ntlmFollowupRequest);
...
}
And in ClearRequestForResubmit line 5891:
// We're uploading and need to resubmit for Authentication or Redirect.
and then (Line 5923):
// The second NTLM request is required to use the same connection, don't close it
if (ntlmFollowupRequest) {....}
To my (admittedly n00bish) eyes these comments seem to indicate that the developers decided to follow the "standard" challenge-response protocol for NTML/Kerberos and not include any way of sending authentication headers up-front.
Setting PreAuthenticate is what you want, which you are doing. The very first request will still do the handshake but for subsequent requests it will automatically send the credentials (based on the URL being used). You can read up on it here: http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.preauthenticate(v=vs.110).aspx.
I've ASP.NET MVC5 project with standard auth process.
After calling /Account/Login I've got response with new cookie .AspNet.ApplicationCookie with auth token. The question is how to get this token in Login action, right after it's generated?
My only idea is to try obtain it from response's cookie but it doesn't work:
// ... somwhere in login action
AuthenticationManager.SignIn(identity);
// <-- How to obtain auth token here? Code below don't work
var token = HttpContext.Current.GetOwinContext().Response.Cookies[".AspNet.ApplicationCookie"];
.. but this is not even compiling.
Request.Cookies[".AspNet.ApplicationCookie"]
Not sure if there is a leading period "." though, also try:
Request.Cookies["AspNet.ApplicationCookie"]
On callback action from OAuth you could use Request.Cookies[".AspNet.ExternalCookie"]
I believe you may be confusing Request and Response. Response is what you send TO the client - so you never want to read a cookie from there. Instead, read the cookie from the Request object.
If you "read" from the Response object and the cookie doesn't' already exist in it, it will simply create a NEW cookie of that name - with no value!
I am trying to return a ActionResult from an MVC api, and if you give it a redirect uri in the post body it should redirect that uri. Right now if you do not give it a uri it performs fine. But if you give it a uri where it should redirect to that uri it just returns a 302 and does not navigate anywhere.
Now I think one issue I am having is that the returned location in the response header is the base URI for the MVC and then the input URI. One problem I have is that all documentation on this I have found, well sucks.
So my question is how do I use the RedirectResult action to actually redirect to a different URI? Also can I redirect to a URI that is outside the current domain? I am worried I cannot since it is appending the base URI to the location in the response. It does not force the clients browser to navigate to another URI.
Here is my action result.
[AllowAnonymous]
public ActionResult Dispatch(Dto.GoogleAnalyticsEvent evt)
{
log.DebugFormat("Dispatch()");
if (evt.Redirect != null)
{
return new RedirectResult(evt.Redirect, false);
}
else
{
return Dispatch<Dto.GoogleAnalyticsEvent>(evt, _api.DispatchAnalyticsEvent, true);
}
}
Here is the returned response if you give it a uri in the post body. The response code is actually a 302 found.
Access-Control-Allow-Head... Content-Type
Access-Control-Allow-Meth... GET,POST
Cache-Control private
Content-Length 139
Content-Type text/html; charset=utf-8
Date Wed, 22 May 2013 22:42:25 GMT
Location /events/www.google.com
Server Microsoft-IIS/7.5
access-control-allow-orig... *
Thanks in advance for the help.
Just to clarify, I was looking for the response from the API to force the browser to navigate to a URI upon response. Though now that I type that out loud it seems that browsers probably would not allow that. How is a 302 found supposed to affect the browser?
If you are calling this action via AJAX (or some other form of direct call to server) the browser will have no way to know that server returned 302 (or 40x, 50x, 200 or any other kind of response).
The behavior is exactly the same as if you get 200+data response for AJAX request - browser will not magically navigate to that response page.
Solution: if you need to redirect as result of AJAX request you need to redesign your protocol to return some 200 response that says "please redirect to this Url" and set window.location.href accordingly on client side when you get such response.
Note that AJAX call will follow 302 redirect and result of the call will be page you are redirecting to (if on the same domain) or "access denied" of some sort if it is cross domain and CORS not turned on destination. See How to prevent ajax requests to follow redirects using jQuery for more background on it.
I have a office plugin that connect a service using HttpWebRequest.
Inside a domain I pass CredentialCache.DefaultNetworkCredentials so all is fine.
Outside a domain a user need to provide username, domain and password.
This don't work atm.
Some part of the code out of it:
CookieContainer cookies = new CookieContainer();
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = WebRequestMethods.Http.Post;
request.AllowAutoRedirect = true;
request.CookieContainer = cookies; // provide session cookie to handle redirects of login controller of the webservice
if (isWindowAuthentication) // isWindowAuthentication is set earlier by config
{
if (Common.UserName.Length > 0)
{
string[] domainuser;
string username;
string domain;
if (Common.UserName.Contains("#"))
{
domainuser = Common.UserName.Split('#');
username = domainuser[0];
domain = domainuser[1];
}
else
{
domainuser = Common.UserName.Split('\\');
username = domainuser[1];
domain = domainuser[0];
}
NetworkCredential nc = new NetworkCredential(username, Common.Password, domain);
CredentialCache cache = new CredentialCache();
cache.Add(request.RequestUri, "NTLM", nc);
request.Credentials = cache;
}
else
{
request.Credentials = CredentialCache.DefaultNetworkCredentials;
}
}
Later on I do the request request.GetResponse();.
If I use CredentialCache.DefaultNetworkCredentials then everything works fine.
The moment I switch to my own new NetworkCredential() part the authentication fails.
I checked the logs of the Apache (it is Apache 2.2 using SSPI mod).
When it succeed the first request redirect to the login controller, then the login controller request credentials. Passed and works (redirect to the target site).
Log 1 (works):
192.168.14.9 - - [25/Oct/2012:11:35:35 +0200] "POST /ror/ioi/start?document%5Bguid%5D=%7Be3d8f1de-10f2-4493-a0c0-97c2acb034e6%7D HTTP/1.1" 302 202
192.168.14.9 - - [25/Oct/2012:11:35:35 +0200] "GET /ror_auth/login?ror_referer=%2Fror%2Fioi%2Fstart%3Fdocument%255Bguid%255D%3D%257Be3d8f1de-10f2-4493-a0c0-97c2acb034e6%257D HTTP/1.1" 401 401
192.168.14.9 - - [25/Oct/2012:11:35:35 +0200] "GET /ror_auth/login?ror_referer=%2Fror%2Fioi%2Fstart%3Fdocument%255Bguid%255D%3D%257Be3d8f1de-10f2-4493-a0c0-97c2acb034e6%257D HTTP/1.1" 401 401
192.168.14.9 - rausch [25/Oct/2012:11:35:35 +0200] "GET /ror_auth/login?ror_referer=%2Fror%2Fioi%2Fstart%3Fdocument%255Bguid%255D%3D%257Be3d8f1de-10f2-4493-a0c0-97c2acb034e6%257D HTTP/1.1" 302 156
The own credential results here Log 2 (do not work):
192.168.14.9 - - [25/Oct/2012:12:05:23 +0200] "POST /ror/ioi/start?document%5Bguid%5D=%7B6ac54e8a-19f1-4ccd-9684-8d864dd9ccf7%7D HTTP/1.1" 302 202
192.168.14.9 - - [25/Oct/2012:12:05:23 +0200] "GET /ror_auth/login?ror_referer=%2Fror%2Fioi%2Fstart%3Fdocument%255Bguid%255D%3D%257B6ac54e8a-19f1-4ccd-9684-8d864dd9ccf7%257D HTTP/1.1" 401 401
What I don't understand is when I inspect e.g. CredentialCache.DefaultNetworkCredentials.UserName then is is empty.
Anyone know what to do and how I have to set my own credentials correct that the authentication works as expected?
Finally after a lot of testing and investigation and many resources on stack overflow I found out what is going on.
The problem seems to be that the httpwebrequest don't handle the authentication when parts of the webseite requests credentials and some don't.
Background:
Our Site has its own session management and redirect to a login controller when no valid session is available. Only this login controller is set to NTLM authentication.
This we made because we have a web site without NTLM auth at all (no 401, 302 request loops in IE!) and only validate once (and we handle authentication on different url to prevent the problem that IE stop posting data at non-authenticated sites => see http://support.microsoft.com/?id=251404).
Solution:
I normally sent a request on my target page and the webserver redirect, authenticate and redirect back to the target. As the httpwebrequest don't handle this for any reason if I have my own credentials set (see above code of my question) I changed to code to authenticate once to my login controller and store the session in a cookie container.
For all following request I don't autenticate at all anymore. I add the cookie container and my server gets a valid session. So I don't have to authenticate anymore. Sideeffect is better performance this way.
Another tricky thing was that I not only use httpwebrequest, I also use a webform control.
Therefor I found the solution to add my own cookie session here: Use cookies from CookieContainer in WebBrowser (Thanks to Aaron who saved me a lot of trouble as well).