Search code examples
c#.netkeycloakopenid-connectdotnet-aspire

.NET OpenID Connect SignOut with keycloak Missing parameters: id_token_hint


I recently found a .NET Aspire example from Microsoft, where OpenID Connect with keycloak is implemented.

I got the solution working and Login with keycloak to retrieve my auth tokens, aswell as the authentication against the backend API works.

The solution however does not completely implement the logout mechanism, so I decided to try implementing it myself.

The issue I'm having is, that when I navigate to the logout endpoint with a NavLink, I will be redirected to keycloak and the session in keycloak will be removed, however the redirect back to my SPA does not work, as I get the error Missing parameters: id_token_hint from keycloak.

I've found another StackOverflow post, where the solution was to set options.SaveTokens = true in the OpenIdConnect configuration, however this is already set in my configuration and it still doesn't work. Is there anything else I need to configure for it to properly work?

OpenIdConnect configuration in Program.cs

var oidcScheme = OpenIdConnectDefaults.AuthenticationScheme;
builder.Services.AddAuthentication(oidcScheme)
                .AddKeycloakOpenIdConnect("keycloak", realm: "WeatherShop", oidcScheme, options =>
                {
                    options.ClientId = "WeatherWeb";
                    options.ResponseType = OpenIdConnectResponseType.Code;
                    options.Scope.Add("weather:all");
                    options.RequireHttpsMetadata = false;
                    options.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
                    options.SaveTokens = true;
                    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                })
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);

OpenID Connect enpoint configuration

internal static IEndpointConventionBuilder MapLoginAndLogout(this IEndpointRouteBuilder endpoints)
{
    var group = endpoints.MapGroup("authentication");

    // This redirects the user to the Keycloak login page and, after successful login, redirects them to the home page.
    group.MapGet("/login", () => TypedResults.Challenge(new AuthenticationProperties { RedirectUri = "/" }))
        .AllowAnonymous();

    // This logs the user out of the application and redirects them to the home page.
    group.MapGet("/logout", () => TypedResults.SignOut(new AuthenticationProperties { RedirectUri = "/" },
        [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]));

    return group;
}

Keycloak Client config

{
    "id" : "016c17d1-8e0f-4a67-9116-86b4691ba99c",
    "clientId" : "WeatherWeb",
    "name" : "",
    "description" : "",
    "rootUrl" : "",
    "adminUrl" : "",
    "baseUrl" : "",
    "surrogateAuthRequired" : false,
    "enabled" : true,
    "alwaysDisplayInConsole" : false,
    "clientAuthenticatorType" : "client-secret",
    "redirectUris" : [ "https://localhost:7058/signin-oidc" ],
    "webOrigins" : [ "https://localhost:7058" ],
    "notBefore" : 0,
    "bearerOnly" : false,
    "consentRequired" : false,
    "standardFlowEnabled" : true,
    "implicitFlowEnabled" : false,
    "directAccessGrantsEnabled" : false,
    "serviceAccountsEnabled" : false,
    "publicClient" : true,
    "frontchannelLogout" : true,
    "protocol" : "openid-connect",
    "attributes" : {
      "oidc.ciba.grant.enabled" : "false",
      "post.logout.redirect.uris" : "https://localhost:7058/signout-callback-oidc",
      "oauth2.device.authorization.grant.enabled" : "false",
      "backchannel.logout.session.required" : "true",
      "backchannel.logout.revoke.offline.tokens" : "false"
    },
    "authenticationFlowBindingOverrides" : { },
    "fullScopeAllowed" : true,
    "nodeReRegistrationTimeout" : -1,
    "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ],
    "optionalClientScopes" : [ "address", "phone", "offline_access", "weather:all", "microprofile-jwt" ]
  }

Solution

  • So after more digging, I figured out, that for some reason, the logout endpoint is called twice by my application and after some research it seems, that this is a common problem when using Blazor Server mode. So basically my app called the SignOut Method twice, where the first time, the id token was set correctly, but the second time, since logout was already executed, there was no token.

    So in my case, the solution was toimplement a service to handle navigating to logout instead of using a NavLink.

    public class LogoutService(IHttpContextAccessor httpContextAccessor, NavigationManager navigationManager)
    {
        // Be careful in a mulit user seting, as these static variables would be shared across scopes
        private static readonly Lock _lock = new();
        private static bool _isLoggingOut; 
    
        public void Logout()
        {
            lock (_lock)
            {
                if (_isLoggingOut) return;
                _isLoggingOut = true;
            }
    
            var context = httpContextAccessor.HttpContext;
            if (context != null)
            {
                // Navigate to the logout endpoint
      
    
          navigationManager.NavigateTo("/authentication/logout", true);
        }
        _isLoggingOut = false;
    }
    

    }

    This ensures, that navigation to "authentication/logout" only occurs once and then everything works as expected.

    If anyone is interested in the full, working code example you can find it here: https://github.com/cmos12345/keycloak-dotnet-example