DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

Duende Identityserver Logout issue #463

Closed blbaan closed 1 year ago

blbaan commented 1 year ago

Which version of Duende IdentityServer are you using? 6.1.7

Which version of .NET are you using? .Net 6 ánd .NET 4.6.2

Describe the bug I am building an Identity server with the Duende Identity server software package.

The IdentityServer is .Net 6, and the client application is ASP.NET 4.6.2, which could complicate things.

In my Client app I have the following configuration:

` app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions { ClientId = "SomeId", Authority = "https://localhost:5001", RedirectUri = "https://localhost:5002", ClientSecret = "SomeSecret", ResponseType = "Code", Scope = "SomeScopes, PostLogoutRedirectUri = "https://localhost:5002/some-custom-path",

SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,

RedeemCode = true,
SaveTokens = true,
Notifications = new OpenIdConnectAuthenticationNotifications
{
    SecurityTokenValidated = async context =>
    {
        var identity = context.AuthenticationTicket.Identity;
        var claims = identity.Claims;
        await Task.Yield();
    }
}

});` Assuming the OpenID configuration is correct (we can connect both apps together and login and logout through its login and logout pages), we cannot seem to get the PostLogoutRedirectUri in the LogoutContext:

var context = await _interaction.GetLogoutContextAsync(LogoutId); The context contains a couple properties which I expected to be filled, which are:

  1. ClientId
  2. ClientName
  3. PostLogoutRedirectUri

Somehow these values are null in my context. Could anyone explain why this is the case here?

We tried to pass the postLogoutRedirectUri through the RedirectToIdentityProvider in the client application, which also resulted in a null-result. We have searched the internet, but most solutions that we come across are for .NET Core, which does not fit our client application. We also tried the solution in the following post: How to redirect user to client app after logging out from identity server? , which also didn't work on our end.

josephdecock commented 1 year ago

Are you using the end_session endpoint to logout? What parameters are you passing to it? Is there anything logged from IdentityServer during logout?

blbaan commented 1 year ago

The logout method we use from the client is as follows:

`` public ActionResult Logout() { HttpCookie userCookie = new HttpCookie("UserCookie", ""); userCookie.Expires = DateTime.Now.AddYears(-1); Response.Cookies.Add(userCookie);

        HttpContext.GetOwinContext().Authentication.SignOut(
                OpenIdConnectAuthenticationDefaults.AuthenticationType,
                CookieAuthenticationDefaults.AuthenticationType);

        return null;
    }``

Further, the logout methods we use are:

`public async Task OnGet(string logoutId) { LogoutId = logoutId; var showLogoutPrompt = LogoutOptions.ShowLogoutPrompt;

        if (User?.Identity.IsAuthenticated != true)
        {
            // if the user is not authenticated, then just show logged out page
            showLogoutPrompt = false;
        }
        else
        {
            var context = await _interaction.GetLogoutContextAsync(LogoutId);
            if (context?.ShowSignoutPrompt == false)
            {
                // it's safe to automatically sign-out
                showLogoutPrompt = false;
            }
        }

        if (showLogoutPrompt == false)
        {
            // if the request for logout was properly authenticated from IdentityServer, then
            // we don't need to show the prompt and can just log the user out directly.
            return await OnPost();
        }

        return Page();
    }

    public async Task<IActionResult> OnPost()
    {
        if (User?.Identity.IsAuthenticated == true)
        {
            // if there's no current logout context, we need to create one
            // this captures necessary info from the current logged in user
            // this can still return null if there is no context needed
            LogoutId ??= await _interaction.CreateLogoutContextAsync();

            // delete local authentication cookie
            await _signInManager.SignOutAsync();

            // raise the logout event
            await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));

            // see if we need to trigger federated logout
            var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;

            // if it's a local login we can ignore this workflow
            if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider)
            {
                // we need to see if the provider supports external logout
                if (await HttpContext.GetSchemeSupportsSignOutAsync(idp))
                {
                    // build a return URL so the upstream provider will redirect back
                    // to us after the user has logged out. this allows us to then
                    // complete our single sign-out processing.
                    string url = Url.Page("/Account/Logout/Loggedout", new { logoutId = LogoutId });

                    // this triggers a redirect to the external provider for sign-out
                    return SignOut(new AuthenticationProperties { RedirectUri = url }, idp);
                }
            }
        }
        return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId });
    }`

As you might recognise, we took one of the templates as a start and built from there. So in fact, most of the actual logging in and logging out are handled by the Duende NuGet package.

Is this what you requested to see?

josephdecock commented 1 year ago

So far what you've shown looks good. One other thing to check is if the client configuration at IdentityServer includes the post logout redirect uri that you're using. Finally, there should be logs from IdentityServer that tell you more about what is going on with the end session request.

blbaan commented 1 year ago

IdentityServer logs.txt

These are my logs, these are from the console trhoughout the whole logout process.

The problem is that the client configuration contains the PostLogoutRedirectUri (as seen in the DB, in the table ClientPostLogoutRedirectUri), but somehow it doesn't fill the data in the logout context. The list ClientIds does get 1 single value, the clientId we're looking for. But the property ClientId on the LogoutContext doesn't get any value.

josephdecock commented 1 year ago

Thanks for sharing the log. There should also be logs from the call to connect/endsession, which should have occurred immediately before the logout ui request. Can you please share that as well?

blbaan commented 1 year ago

IdentityEndSessionLogs.txt

There we go, I included some extra logs. Sorry for not getting it the first time! :)

josephdecock commented 1 year ago

No problem! The logs show that you are not passing an id_token_hint parameter to the end_session endpoint, which is required for a post logout redirect. The OWIN middleware doesn't pass the id token automatically - you have to customize it with code like this:

app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    // Other config here
    Notifications = new OpenIdConnectAuthenticationNotifications
    {
        RedirectToIdentityProvider = async notification =>
        {
            if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
            {
                var idToken = await notification.OwinContext.Authentication.GetTokenAsync("id_token");
                ctx.ProtocolMessage.IdTokenHint = idToken;
             }
         }
    }
}
blbaan commented 1 year ago

Hello Joseph,

Thank you for looking into this matter. You stated that we are not passing an id_token_hint from the client to the receiver, which might be true.

We added the following code earlier on this week, which I believe would've solved this issue:

`RedirectToIdentityProvider = n =>
                {
                    // if signing out, add the id_token_hint
                    if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                    {
                        var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");

                        if (idTokenHint != null)
                        {
                            n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                        }

                    }

                    return Task.FromResult(0);
                }`

In the logs you received, this code was already added. Your conclusion leads me to believe this doesn't properly work, which is what I'm going to investigate.

Thank you for your effort, i will come back to you if I found a solution! :)

blbaan commented 1 year ago

Hello again.

I have been debugging, and it looks like my Id_token_hint cannot be retrieved as a claim, and the method you suggested to use (GetTokenAsync) on the AuthenticationMiddleware isn't listed. Perhaps we are not getting an id_token from the initial request?

brockallen commented 1 year ago

You will have to capture the id_token at login time via one of the provider events, and then store that in the user's session (typically as a claim).

josephdecock commented 1 year ago

Sorry, I forgot that GetTokenAsync is only in .net core. As Brock says, you can capture the token as a claim. You also showed above that you have SaveTokens on, which should be persisting the tokens into the authentication properties.

blbaan commented 1 year ago

Hello Brock and Joseph,

I have tried adding code from the example linked here: https://github.com/IdentityServer/IdentityServer3.Samples/blob/master/source/Clients/MVC%20OWIN%20Client%20(Hybrid)/Startup.cs#L73 .

It's being called right after the IdentityServer processed the login, before the actual callback method

The AuthenticationTicket is null, which means I cannot fetch an id_token claim through this manner. Do you have any suggestions what I can do next? Or can you explain why the AuthenticationTicket is null?

brockallen commented 1 year ago

There's a ProtocolMessage property or somesuch that has it, IIRC.

Something like this: https://github.com/IdentityServer/IdentityServer3.Samples/blob/master/source/Clients/WebFormsClient/Startup.cs#L54

blbaan commented 1 year ago

Hello Brock,

Thank you for your suggestion. That seems to work! I am now receiving an id_token and storing it in the OwinContext as seen below.

Code snippet

This seems to work, where I see the claims appearing in the callback method, as seen below:

image

However, when I reach the following line of code while logging off, I only see a name-claim containing my email address. Am I missing a small configuration step here?

image

image

brockallen commented 1 year ago

If you look at the rest of the sample, the value is added to the ticket being created:

https://github.com/IdentityServer/IdentityServer3.Samples/blob/master/source/Clients/WebFormsClient/Startup.cs#L73

blbaan commented 1 year ago

Hello Brock,

As you can see in the first image in my previous post, I already added this. See the middle section of the code. Is there anything else I need to watch out for when logging in or logging out? I have scoured the github you sent me and incorporated everything I saw, except the following bit:

image

I see you are just calling the SignOut method, whereas we are adding two parameters. Could that perhaps be why it doesn't send the additional claims, but only the name claim at the log-out ?

brockallen commented 1 year ago

As you can see in the first image in my previous post, I already added this.

No, you added it to the OwinContext user, not the Ticket. In that event the ticket is the object model of what will be issued in the response cookie. The User on the OwinContext is the result of the current incoming cookie.

I'd suggest cloning our sample as it is and get that working, then merge that working code into the real project you're working on.