使用 OpenIDConnect 时如何注销 ClaimsIdentity 用户

Posted

技术标签:

【中文标题】使用 OpenIDConnect 时如何注销 ClaimsIdentity 用户【英文标题】:How to logout ClaimsIdentity User when using OpenIDConnect 【发布时间】:2020-11-12 09:01:12 【问题描述】:

我有使用 OpenIDConnect 身份验证的 ASP.NET Core 应用程序。在OnTokenValidated 事件中,我检查Authenticated 用户是否存在于我的应用程序的数据库中,如果不存在,那么我将抛出UnauthorizedAccessException

请注意,在OnTokenValidated 事件中,如果用户存在于应用程序的数据库中,我将创建新身份,否则我将保留经过身份验证的用户。

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);
                 
             ;
         );
    
    

问题

1> 在OnRemoteFailure 中,我如何在重定向到 AccessDenied 视图之前自动注销/清除unauthorized 用户? AccessDenied 视图可供匿名用户访问。 我试过了

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);
                     

但是在OnTokenValidated 事件中context.HttpContext.User.Identity.IsAuthenticated 是假的,所以它不起作用。

【问题讨论】:

【参考方案1】:

一个常见的错误是在调用 Signout 方法后执行 Reponse.Redirect,这听起来很明显。但不幸的是,这不起作用。 Signout 方法会创建自己的重定向响应,当您进行重定向时,您会“覆盖”这些重定向

实现这一点的正确方法是不返回任何内容,让 Signout 方法处理重定向:

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

要重定向到备用网址,您可以尝试:

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

【讨论】:

我试过了,但这会将用户重定向到身份服务器的注销页面。我希望用户转到 AccessDenied 页面。我试过public async Task&lt;IActionresult&gt; AccessDenied() await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); return View("AccessDenied"); 没用 它确实被重定向了。可能是我在这里问了错误的问题。我已经开始了一个新线程***.com/questions/63042878/… 澄清你的问题总是好的。但是,为什么不在这两个 Signout 方法上设置 AuthenticationProperties redirectUri 呢?或者尝试改变它们的顺序? 如果你不能让它工作,你总是可以作为一个黑客,只需清除本地会话 cookie 以在本地注销用户。 您对我的回答满意还是缺少什​​么?如果没有,请将我的回答标记为已接受。

以上是关于使用 OpenIDConnect 时如何注销 ClaimsIdentity 用户的主要内容,如果未能解决你的问题,请参考以下文章

Microsoft OpenIdConnect Owin 响应

执行 Auth0/Google 联合 SSO 注销时如何将用户重定向回我的应用程序?

OpenID Connect - 如何处理单次注销

Azure AD B2C OpenID使用WS-Federation和SAML声明提供程序连接单一注销

post_logout_redirect_uri 为空 OpenIdConnect IdentityServer

在结束会话 URI 中使用 IP 地址时,OpenId Connect 用户未注销(发现文档 end_session_endpoint)。使用 Gluu 身份服务器