CountZero
CountZero

Reputation: 6399

Identity Server 4 - Logout - Passing Additional Data

When a user logs out under certain circumstances I want to show them a message on the logged out page. To enable this I want to be able to send an optional parameter from the client to the Identity Server / Authority site on logout.

While I have the standard logout flow working I have hit a brick wall in handling this scenario as information seems thin on the ground and the suggested solutions are not working.

From what I have read the 'state' parameter is the correct way to pass this information but this not coming through currently. AcrValues are only used to send information the other way.

My naive implementation below simply adds a state query string item to the end session endpoint. However, when I check the query string my client uses to go to the identity server instance it is missing.

Redirect(discoveryResponse.EndSessionEndpoint+"&state=foo")

Any help gladly received!

Current flow for MVC client:

Please note; some code has been removed for brevity.

Logout initiated from client controller with state=foo:

public class LogoutController : Controller
{
    public ActionResult Index()
    {
        Request.GetOwinContext().Authentication.SignOut();

        var discoveryClient = new DiscoveryClient(clientConfig.Authority) { Policy = {RequireHttps = false} };

        var discoveryResponse = discoveryClient.GetAsync().Result;

        var tokenClaim = ((ClaimsIdentity)User.Identity).FindFirst("id_token");

        return Redirect(discoveryResponse.EndSessionEndpoint+ "?id_token_hint="+ tokenClaim + "&state=foo");
    }
}

RedirectToIdentityProvider is called for request:

IdTokenHint and PostLogoutRedirectUri are set and passed correctly.

    app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
      {
         Notifications = new OpenIdConnectAuthenticationNotifications
         {
            RedirectToIdentityProvider = n =>
            {
              if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
              return Task.FromResult(0);

              var idTokenHint = n.OwinContext.Authentication.User.FindFirst(OpenIdConnectClaimType.IdToken);

          if (idTokenHint == null) return Task.FromResult(0);

          n.ProtocolMessage.IdTokenHint = idTokenHint.Value;

  n.OwinContext.Response.Cookies.Append("IdentityServerPostLogoutReturnUri", 
  n.ProtocolMessage.PostLogoutRedirectUri);
          n.ProtocolMessage.PostLogoutRedirectUri = 
  n.Options.PostLogoutRedirectUri;


        return Task.FromResult(0);
           }
        }

URL Generated (not the lack of 'state' item):

http://localhost:44362/connect/endsession?post_logout_redirect_uri=http%3a%2f%2flocalhost%3a2577%2fpostloginredirect&id_token_hint=removed&x-client-SKU=ID_NET&x-client-ver=1.0.40306.1554

Logout page on the authority site:

This is where I want to be able to access the state parameter.

public class LogoutController : Controller
{
  public async Task<ViewResult> Index(string logoutId)
  {
    if (logoutId == null) throw new Exception("Missing logoutId");

    var logoutRequest = await interactionService.GetLogoutContextAsync(logoutId);

    var vm = new LoggedOutViewModel(logoutRequest, logoutId);

    if (!string.IsNullOrWhiteSpace(httpContextService.GetCookieValue(PostLogoutReturnUriCookieKey)))
    {
      vm.PostLogoutRedirectUri = httpContextService.GetCookieValue(PostLogoutReturnUriCookieKey);
      httpContextService.ClearCookie(PostLogoutReturnUriCookieKey);
    }

    await httpContextService.SignOutAsync();
    return View("Index", vm);
  }
}

Upvotes: 5

Views: 3704

Answers (1)

CountZero
CountZero

Reputation: 6399

I've dug a little deeper and found what the issue was being caused by the following lines in the Microsoft.Owin.Security.OpenIdConnect middleware.

protected override async Task ApplyResponseGrantAsync()
{
    AuthenticationResponseRevoke signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode);

    if (signout != null)
    {
        // snip
        var notification = new RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
        {
            ProtocolMessage = openIdConnectMessage
        };

        await Options.Notifications.RedirectToIdentityProvider(notification);
        // This was causing the issue
        if (!notification.HandledResponse)
        {
            string redirectUri = notification.ProtocolMessage.CreateLogoutRequestUrl();
            if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
            {
                _logger.WriteWarning("The logout redirect URI is malformed: " + redirectUri);
            }
            Response.Redirect(redirectUri);
        }
    }
}

In order to prevent the middleware from overriding the redirect when it detects a sign out message the following line in the 'HandleResponse' method needs to be called in the RedirectToIdentityProvider event.

This allows the original 'state' query string item to be passed to Identity Server and be pulled out using the interaction service.

app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    // Snip
    Notifications = new OpenIdConnectAuthenticationNotifications
    {
        AuthorizationCodeReceived = async n =>
        {
            // Snip
        },
        RedirectToIdentityProvider = n =>
        {
            // Snip
            n.HandleResponse(); // The magic happens here
        }
}

Upvotes: 6

Related Questions