0

I'm executing a GET to

GET https://localhost:44301/connect/endsession?id_token_hint=eyJhbGciO...GzHCPw

as suggested in the docs for EndSession endpoint.

It seems to work (in a way) because I get a hit on my breakpoint in the method redirected to.

[HttpGet("logout")]
public async Task<IActionResult> LogOut(
    [FromQuery] string id_token_hint,
    [FromQuery] string post_logout_redirect_uri,
    [FromQuery] string session,
    [FromQuery] string logoutId)
{ 
  LogoutRequest context = await InteractionService
    .GetLogoutContextAsync(logoutId);
  ...
}

Here, I'm getting a value in logoutId (unless I skip passing the identity token, resulting i null), while the other variables are not set, staying as null. At first, I was happy to see that context wasn't null. However, I soon learned that it's set poorly, despite following stuff that work.

I can see the client's name and ID (which seems to be correct). However, everything else is null except for the array Parameters, which contains zero elements.

I've made sure to pass in the identity token, not access token. I've also tried the full version with all the parameters described in the docs (trying various redirect URLs both mentioned in my configuration and others). The same (mis)behavior followed, though.

GET https://localhost:44301/connect/endsession
?id_token_hint=eyJhbGciO...GzHCPw
&post_logout_redirect_uri=https://get_the_duck.off
&session=1337

Since I'm getting the breaky hit and recieve some value as logoutId parsable by the interaction service, I feel that it's wired up correctly (which is expected since the security as such works as expected). However, my application seem to be a stalker and just won't let them go, so to speak. I suspect, there's some tiny detail that the docs don't mention (or obscures in a formulation I don't comprehend). (Googling gave nothing I recognized as relevant.)

Proof of effort (along a bunch of blogs on security, not dedicated to the signout specifically).

Konrad Viltersten
  • 36,151
  • 76
  • 250
  • 438
  • Are you redirecting the user's browser to that URL or invoking it some other way? – mackie Aug 24 '21 at 10:09
  • @mackie The user's browser performs a GET request to the endpoint in the question, providing ID token. Then, IDS4 routes that call through back-channel to a method of my security controller (still withing the IDP) passing me `logoutId` which I'm using for `GetLogoutContextAsync(logoutId)` in the interaction service. That way, I obtain a logout context. However, that context seems to be missing data (except for the ID of the client, which is correct). – Konrad Viltersten Sep 24 '21 at 14:40
  • @mackie I tried to `SingOutAsync(scheme)` where scheme was a bunch of different options: `Identity.Application`, `idsrv`, `idsrv.exernal` and a few others. However, the token can be then reused for access and it seems that the user isn't signed out. Which kind of makes sense if there's nothing in the logout context that refers the user's GUID. – Konrad Viltersten Sep 24 '21 at 14:42
  • When you say "the token" do you mean the access token issued to a client application? Signing out will not revoke any access tokens since by their nature they cannot be revoked. Refresh tokens/reference tokens could be cleaned up if you create logic to do so or explicitly use the revocation endpoint from your client application. If you're refering to the auth cookie then removing a cookie that's not backed by something in the backend will not make its value invalid so it could still be reused if someone manages to copy it. – mackie Sep 24 '21 at 15:48
  • @mackie Sorry for being imprecise. What I meant is that **after** the execution of `SignOutAsync(...)`, the two cookies (named *idsrv.session* and *.AspNetCore.Identity:application*) are still there. Those appear when I invoke `SignInAsync(...)` and I expect them to be gone when I sign out the user. The token in the browser gets deleted by the Angular application but when it then calls to */connect/authorize*, a new token is being issued without passing any credentials. If I manually remove the cookies, the credentials are requested prior to issuing any token. I conclude user isn't logged out. – Konrad Viltersten Sep 25 '21 at 11:40
  • That does sound odd. What do the response headers look like for the request? I’d expect to see Set-Cookie headers setting the expiry to be some time in the past. – mackie Sep 26 '21 at 16:53
  • @mackie There's several headers like `set-cookie: whatever=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=none; httponly` where *whatever* contains the two discussed tokens. I also noticed that the one called *idsrv.session* can be manually changed in the browser using JS but the other just won't budge. I guess it **has** to be deleted/reset from the backend application (however insanely it may sound). – Konrad Viltersten Sep 27 '21 at 08:08
  • That sounds like what I'd expect to see. If you're using Chrome do those headers have any warning triangles next to them? Normally if there's a problem it will indicate as such or at least log something to the console. FYI idsrv.session is not set to httponly by design - it needs to be readable by client side script because it enables the session monitoring feature. – mackie Sep 27 '21 at 08:17
  • @mackie Yes, *idsrv*-cookie isn't secure. I just pasted one of the `set-cookie` headers for example. As far I can see, there's no triangles or other warnings. I'm running Edge and Chrome with the same behavior, both incognito and... whatever the opposite of incognito may be. Regular, I guess. I can't shake of the sensation that it's all about the logout context not being recognized from the passed ID token when signing out. – Konrad Viltersten Sep 27 '21 at 09:20

2 Answers2

0

Why do you need to call that endpoint? There are many cookies/sessions involved and the easiest is to do it in ASP.NET Core using:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task Logout()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);

    //Important, this method should never return anything.
}

This usually works for me to do a complete signout from my ASP.NET Core client.

Also, it is a best practice to only accept logout using HTTP POST, not GET.

Tore Nestenius
  • 16,431
  • 5
  • 30
  • 40
  • I'm not entirely certain if I got it right. I'll answer the question but it seems to me like it makes no sense. Why I call */connect/endsession*? Because the docs say so. Why GET? The same answer. Why I have GET on my `LogOut(...)` method? Because that's the one that Identity Server calls, they won't request a POST. Why go through it at all? Because the cookie remains unless I sign out manually in the method. Why not just remove the cookie? I need to log info on the action **and** it crashes right now giving me 500 when I specify the schema anyway. The cookie isn't removed. – Konrad Viltersten Aug 23 '21 at 10:24
  • the OpenIDConnect and Cookie authentication handler in your client usually handles all of this for you. Do look in a tool like fiddler and see what request creates the 500 error and see in the log what the error contains. Have you set the LogoutPath and SignedOutRedirectUri parameters? What does your startup class look like? – Tore Nestenius Aug 23 '21 at 10:30
  • Well, the logout path is set to the endpoint that I show in my question. That's how Identity Server back-channels the request made to */connect/endsession* by the client application. I sense that we're talking about different setups here. I'm trying to follow the one I linked to in the question. Perhaps it's different for us, as we do have a SPA making the calls? – Konrad Viltersten Aug 23 '21 at 13:06
  • Are you aware of the real purpose of LogOutPath? Its not what you think it is. See this question https://stackoverflow.com/questions/52709492/what-does-the-cookieauthenticationoptions-logoutpath-property-do-in-asp-net-core – Tore Nestenius Aug 23 '21 at 17:23
  • I'm looking at that answer (and a number of others). What hits me is that I have `AddAuthentication()` followed by `.AddIdentityServerAuthentication("default",...)` while the other examples have `AddCookie(...)` and/or `AddOpenIdConnect(...)`. I've tried your suggested approach (including a bunch of others schemes that were available). Nothing removes the set session cookie. – Konrad Viltersten Sep 24 '21 at 15:14
0

If you look into the official quickstart, you can see they suggest a combination of initial Get, followed by the Post with an optional prompt in between. And the final Post method implementation performs the actual sign-out by calling the

await HttpContext.SignOutAsync();
d_f
  • 4,599
  • 2
  • 23
  • 34
  • In the example linked, there's a part of creating the model based on the context in method `BuildLoggedOutViewModelAsync(string logoutId)`. In there, there's the passage `await _interaction.GetLogoutContextAsync(logoutId)`. The problem is that when I access it, I see the ID and name of the client properly **but** the rest of the fields is `null`. So I can't log out because the application thinks that my user isn't authenticated. But the session cookie is there and I can access APIs using the access token. Makes no sense to me... Invoking `HttpContext.SignOutAsync()` can't log out then. – Konrad Viltersten Sep 23 '21 at 18:16
  • please read and check carefully what mackie asked. if you don't have access to the user, that means your ASP.NET app hosting the Identityserver can't treat the call as authenticated. One possible reason for that is calling signout in iframe or any similar cross-domain call when the cookie wouldn't be sent. You have to ensure (with browser console) that the call to identityserver contains the cookie. – d_f Sep 23 '21 at 22:51
  • Perhaps I'm misunderstanding the naming here. The *cookie sent* is what exactly here? According to the docs, the logout is supposed to be GET with identity token, return URL and state. How would we pass the cookie based on that? In headers as a the bearer? I think I'm missing something. We are talking an SPA, not MVC application here, right? – Konrad Viltersten Sep 24 '21 at 14:35
  • That GET is just an http request from the browser, and the logout page in Identityserver is just a page in an MVC app, so in addition to any specific data such as the token and state, it assumes it's own cookies embedded into the request by the browser. That cookie is the main stuff the app needs to perform the logout while the token is mostly for validation purposes and might be easily omitted. – d_f Oct 07 '21 at 09:27
  • I believe that the main cause of confusion is the fact that we're doing SPA and not MVC-application. It complicates things considerably. I thought it was apparent in my question and/or interchangeably applicable in both cases. But I'm realizing that it is not so. :) – Konrad Viltersten Oct 07 '21 at 18:26
  • Actually you can't make it a pure SPA (you mean a client-side js-driven SPA, I guess). Login/Logout pages must be server side to make the things work. Now I start thinking you've totally removed the logout page coming with the templates and that is the cause of your issue. – d_f Oct 07 '21 at 18:35
  • Correction: you **can** create a pure (client side, js-driven) SPA authenticating toward an Identityserver (MVC) instance hosted outside. That is actually the best practice and the only approach acceptable for enterprises. What MS offers with their templates is a kind of hybrid for quick start, and there we face a "kinda" SPA in one box with a "kinda" MVC. – d_f Oct 07 '21 at 18:48
  • That's an accurate description, in my opinion. And the problem becomes apparent when the out-of-the-box sort-of-SPA is an actual, real SPA with full autonomy from the backend. There are very few examples out there touching it and I haven't found any showing the full proper flow. This has been a very educative (although frustrating as duck) journey for me. I've done several security projects but the current one was something else. – Konrad Viltersten Oct 08 '21 at 06:54