LP13
LP13

Reputation: 34149

How to logout ClaimsIdentity User when using OpenIDConnect

I have ASP.NET Core application that is using OpenIDConnect authentication. In OnTokenValidated event i check if Authenticated user exists in the my application's database, and if not then i am throwing UnauthorizedAccessException

Note that in OnTokenValidated event i am creating new identity if user exists in the application's DB, else i leave authenticated user as it is.

public class Startup
{
    private readonly ILogger<Startup> _logger;
    public IConfiguration Configuration { get; }
    private IHostingEnvironment _environment { get; }

    public Startup(IConfiguration configuration, IHostingEnvironment env, ILogger<Startup> logger)
    {
        Configuration = configuration;
        _environment = env;
        _logger = logger;
    }

    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
         .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
         {
             options.LoginPath = "/Home";
             options.AccessDeniedPath = "/Account/Forbidden";
             options.Cookie = new CookieBuilder()
             {
                 Name = "myCookie",
                 HttpOnly = true,
             };
             options.SlidingExpiration = true;
         })
         .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
         {
             //options.ForwardChallenge                
             options.Authority = configuration["IdentityOptions:Authority"];
             options.ClientId = configuration["IdentityOptions:ClientID"];
             options.ResponseType = "id_token";
             options.CallbackPath = "/Home";
             options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; //tells cookies scheme to persist user's identity in cookie.
             options.SignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;//tells cookies scheme to remove persisted cookie
             options.Scope.Add(OpenIdConnectScope.Email);
             
             options.Events = new OpenIdConnectEvents()
             {
                 OnTokenValidated = async context =>
                 {
                     var emailClaim = context.Principal.Claims.SingleOrDefault(x => x.Type == ClaimTypes.Email);
                     
                    // check if user exists in client applications db
                     CompanyUser cu = null;
                     using (var serviceProvider = services.BuildServiceProvider())
                     {
                         using (var serviceScope = serviceProvider.CreateScope())
                         {
                             using (var accountService = serviceScope.ServiceProvider.GetService<IAccountService>())
                             {
                                 cu = await accountService.Authorize(emailClaim.Value);
                             }
                         }
                     }

                     if (cu == null)
                     {
                         // context.Principal.Identity.IsAuthenticated is true here
                         
                         throw new UnauthorizedAccessException(string.Format("Could not find user for login '{0}' ", emailClaim.Value));
                     }

                     //We will create new identity to store only required claims.
                     var newIdentity = new ClaimsIdentity(context.Principal.Identity.AuthenticationType);

                     // keep the id_token for logout 
                     newIdentity.AddClaim(new Claim(IdentityClaimTypes.IdToken, context.ProtocolMessage.IdToken));

                     // add email claim
                     newIdentity.AddClaim(emailClaim);                         
                     
                     context.Properties.IsPersistent = true;
                     context.Properties.ExpiresUtc = DateTime.UtcNow.AddHours(3);

                     // overwrite existing authentication ticket
                     context.Principal = new ClaimsPrincipal(newIdentity);
                 },                     
                 OnRedirectToIdentityProviderForSignOut = async context =>
                 {
                     var idTokenHint = context.HttpContext?.User?.FindFirst("id_token");
                     if (idTokenHint != null)
                         context.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                     await Task.FromResult(0);
                 },
                 OnRemoteFailure = async context =>
                 {  
                     //WHY context.HttpContext.User.Identity.IsAuthenticated is false here??
                     if (context.Failure is UnauthorizedAccessException)
                     {
                         context.Response.Redirect("/Account/AccessDenied");
                     }
                     else
                     {
                         context.Response.Redirect("/Account/Error");
                     }
                     context.HandleResponse();
                     await Task.FromResult(0);
                 }
             };
         });
    }
    

}

Questions

1> In OnRemoteFailure How do i auto logout/clear unauthorized user BEFORE redirecting to AccessDenied view? AccessDenied view is accessible to anonymous user. I have tried

OnRemoteFailure = async context =>
                     {
                         if (context.Failure is UnauthorizedAccessException)
                         {
                             await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.SignOutAsync(context.HttpContext, CookieAuthenticationDefaults.AuthenticationScheme);
                             await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.SignOutAsync(context.HttpContext, OpenIdConnectDefaults.AuthenticationScheme);

                             context.Response.Redirect("/Account/AccessDenied");
                         }
                         else 
                         {
                             context.Response.Redirect("/Account/Error");
                         }
                         context.HandleResponse();
                         await Task.FromResult(0);
                     }

However in OnTokenValidated event context.HttpContext.User.Identity.IsAuthenticated is false so it didn't work.

Upvotes: 1

Views: 3852

Answers (1)

Tore Nestenius
Tore Nestenius

Reputation: 19971

A common mistake is to do a Reponse.Redirect after the Signout method calls, that sounds obvious. But that does unfortunately not work. The Signout methods creates their own redirect responses and when you do your redirect you "override" these redirects

The proper way to do implement this is to not return anything and let the Signout methods handle the redirect:

    public async Task DoLogout()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
    }

To redirect to an alternative Url, you can try:

        await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties( )
        {
            RedirectUri = "alternativeUrl"
        });

Upvotes: 3

Related Questions