Search code examples
asp.netcookiesidentityserver4

HttpContext.SignOutAsync() not deleting local cookie


I am following along the IdentityServer4 Quickstart project to get up to speed on how it is implemented. My Startup.cs is close to identical, my implementation of the AccountController.cs class is more or less the same, and so on.

However, there seems to be some sort of low-level conflict with how local authentication cookies are handled.

  1. In the startup, I do not call app.UseAuthentication() as IdentityServer sets that up later on. I have gone so far as to copy the services.AddAuthentication() from the quickstart project verbatim, but am still having issues.

When logging in, the Quickstart project produces two cookies: one Antiforgery Validation cookie, and one cookie called idsrv. Nowhere in the project is this explicitly defined to do so.

When I run my implementation of it, however, I get three cookies: An Antiforgery Validation cookie, an idsrv.session cookie, and an .AspNetCore.Identity.Application cookie. I can force ASP.NET to name the cookie 'idsrv', but the existence of a .session cookie leads me to believe it isn't using the right scheme.

  1. When I try to log out by calling HttpContext.SignOutAsync(), the cookie is not deleted and nothing happens: it stays signed in. I have found questions with similar problems, but those seem to be a) implementing an external auth and b) implementing redirects that supposedly overwrite the signout url. I am doing neither.

My login implementation:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login( LoginInputModel model, string action )
{
   var context = await _interactionService.GetAuthorizationContextAsync( model.ReturnUrl );

   if( action != "Login" )
   {
      if( context != null )
      {
         // Handle a cancelled login
         await _interactionService.GrantConsentAsync( context, ConsentResponse.Denied );
      }
      else
         return Redirect( "~/" );
   }

   var errors = ModelState.Values.SelectMany( v => v.Errors );
   if( ModelState.IsValid )
   {
      var user = await _userManager.FindByNameAsync( model.Username );
      if( user != null && await _userManager.CheckPasswordAsync( user, model.Password ) )
      {
         await _eventService.RaiseAsync( new UserLoginSuccessEvent( user.UserName, user.Id, user.UserName ) );

         //Handle RememberMe Checkbox
         AuthenticationProperties props = null;
         if( model.RememberMe )
         {
            props = new AuthenticationProperties
            {
               IsPersistent = true,
               ExpiresUtc = DateTimeOffset.UtcNow.Add( Config.AccountRememberMeDuration ),
            };
         }

         //Issue Auth Cookie
         await HttpContext.SignInAsync( user.Id, user.UserName, props );

         if( _interactionService.IsValidReturnUrl( model.ReturnUrl ) || Url.IsLocalUrl( model.ReturnUrl ) )
            return Redirect( model.ReturnUrl );
         return Redirect( "~/" );
      }

      //If we made it this far, the authentication failed
      await _eventService.RaiseAsync( new UserLoginFailureEvent( model.Username, "Invalid Credentials" ) );
      ModelState.AddModelError( "", "Invalid Credentials" );
   }

   //Display form with error message
   model.Password = string.Empty;
   return View( model );
}

My Logout Implementation:

[HttpGet]
public async Task Logout( LogoutInputModel model )
{
   if( User?.Identity.IsAuthenticated == true )
   {
      await HttpContext.SignOutAsync();
      await _eventService.RaiseAsync( new UserLogoutSuccessEvent( User.GetSubjectId(), User.GetDisplayName() ) );
   }
}

My Startup.cs:

public void ConfigureServices( IServiceCollection services )
{
   services.AddMvc();
   services.AddIdentityServer()
      .AddOperationalStore( options =>
      {
         options.ConfigureDbContext = builder =>
             builder.UseSqlServer( Config.ConnectionString, sqlOptions => sqlOptions.MigrationsAssembly( Config.MigrationsAssembly ) );

         options.EnableTokenCleanup = true;
         options.TokenCleanupInterval = 30; //Every 30 seconds
      } )
      .AddConfigurationStore( options =>
      {
         options.ConfigureDbContext = builder =>
             builder.UseSqlServer( Config.ConnectionString, sqlOptions => sqlOptions.MigrationsAssembly( Config.MigrationsAssembly ) );
      } )
      .AddDeveloperSigningCredential();

   services.AddDbContext<CustomUserContext>( builder =>
      builder.UseSqlServer( Config.ConnectionString, sqlOptions => sqlOptions.MigrationsAssembly( Config.MigrationsAssembly ) )
   );
   services.AddIdentity<CustomUser, CustomRole>()
      .AddEntityFrameworkStores<CustomUserContext>();

   services.AddAuthentication();
}

public void Configure( IApplicationBuilder app, IHostingEnvironment env )
{
   if( env.IsDevelopment() )
      app.UseDeveloperExceptionPage();

      
   app.UseIdentityServer();
   app.UseStaticFiles();
   app.UseMvcWithDefaultRoute(); //TODO: Routes

}

How I'm calling the logout function:

<div class="account-actions">
    @using( Html.BeginForm( "Logout", "Account", FormMethod.Get ) )
    {
        <input type="submit"
               class="account-action-button account-action-logout"
               value="Logout" />
    }
</div>

Solution

  • I had the same issue. I thought I might try to call SignOutAsync( schema ) instead. By trying to sign out of a schema that didn't exist I got an error message with the list of schemas supported. One of them was "Identity.Application" and doing a sign out on that worked. e.g. await this.HttpContext.SignOutAsync( "Identity.Application" );