Search code examples
single-sign-onopenid-connect.net-6.0

OpenIdConnect Authentication Cookie not being deleted on signing out


I'm trying to implement OpenIdConnect as my authentication provider, using .NET6. I have the sign-in part working properly, however, on sign-out, the authentication cookie is not being deleted.

I've seen plenty of other threads referencing similar problems (this, for example provides a pretty good summary of several of these), but none of the approaches seem to work.

Broadly speaking, my authentication code is configured as follows:

.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = AuthenticateScheme;
    options.DefaultSignOutScheme = AuthenticateScheme;
    options.DefaultChallengeScheme = ChallengeScheme;
})
.AddCookie(AuthenticateScheme, options =>
{
    //trying to avoid having to add this!
    //options.Events.OnSigningOut = async ctx =>
    //{
    //    await Task.Run(() => ctx.HttpContext.Response.Cookies.Delete(CookieName));
    //};
})
.AddOpenIdConnect(ChallengeScheme, options =>
{
    options.SignInScheme = AuthenticateScheme;
    options.SignOutScheme = AuthenticateScheme;
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.CallbackPath = CallbackPath;
    options.UsePkce = true;
    options.Authority = Authority;
    options.ClientId = ClientId;
    options.ClientSecret = ClientSecret;

    options.Scope.Clear();
    options.Scope.Add(OpenIdConnectScope.OpenId);
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
    options.Scope.Add(OpenIdConnectScope.Email);
    options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
    options.MapInboundClaims = false;

    options.TokenValidationParameters = new TokenValidationParameters
    {
        RoleClaimType = "roles",
        NameClaimType = "preferred_username",
        ValidateIssuer = false
    };

    options.Events.OnRedirectToIdentityProvider = ctx =>
    {
        // Prevent redirect loop
        if (ctx.Response.StatusCode == 401)
        {
            ctx.HandleResponse();
        }

        return Task.CompletedTask;
    };

    options.Events.OnAuthenticationFailed = context =>
    {
        context.HandleResponse();
        context.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(context.Exception.Message));
        return Task.CompletedTask;
    };
});

And over on my Startup.cs, I have the following code within the Configure method:

app.Map("/Logout", map =>
{
    map.Run(async appBuilder =>
    {
        // tried this, didn't work
        //await appBuilder.SignOutAsync("MyAuthCookie", new AuthenticationProperties { RedirectUri = "/" });
        // tried this, didn't work
        //await appBuilder.SignOutAsync();
    });
});
app.UseNotFoundHandler();
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseGetaCategories();
app.UseGetaCategoriesFind();

app.UseAnonymousId();
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "Default", pattern: "{controller}/{action}/{id?}");
    endpoints.MapControllers();
    endpoints.MapRazorPages();
    endpoints.MapContent();
});

Any suggestions would be greatly appreciated. For now, the only way I've been able to get this to work is to explicitly delete the cookie within the OnSigningOut event, but this doesn't feel like the right approach.


Solution

  • A more complete logout should look something like this:

    /// <summary>
    /// Do the logout
    /// </summary>
    /// <returns></returns>
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task Logout()
    {
        await HttpContext.SignOutAsync(AuthenticateScheme);
        await HttpContext.SignOutAsync(ChallengeScheme);
    
        //Important, this method should never return anything.
    }
    

    You might need to tweak the schema names if you don't use the default ones.

    In the AddAuthentication method, I would se these settings, you want cookie for everything except the challenge.

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = AuthenticateScheme;
        options.DefaultChallengeScheme = ChallengeScheme;
    })