You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

IdentityServer4登录回调遇UserNotFoundException时,如何代码触发外部IDP登出?

IdentityServer4外部登录回调中处理用户不存在时的外部IDP登出实现

问题场景

我正在修改基于IdentityServer4的代码,遇到这样的需求:在登录流程的回调Action中,当查询不到本地用户(触发UserNotFoundException)时,需要将用户从外部身份提供商(External Identity Provider)登出,之后强制用户重新登录。

尝试过的思路都依赖IdentityServer生成的logoutId,但直接调用CreateLogoutContextAsync()获取到的logoutId总是为null,因为此时并没有主动触发登出流程。

原代码(带待实现标记)

/// <summary>
/// Post processing of external authentication
/// </summary>
[HttpGet]
public async Task<IActionResult> Callback()
{
    try
    {
        // Read external identity from the temporary cookie
        AuthenticateResult result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
        if (!result.Succeeded)// if external authentication failed, just produce a login here URL, no need to logout from any (internal/external) provider.
        {
            if (result.Failure != null)
            {
                _logger.Error(result.Failure);
            }
            
            // todo: url for login
            // login url for should be passed via model
            return RedirectToAction("AccessDenied", "Account");
        }

        // Lookup our user and external provider info
        (User user, string provider, string providerUserId, IEnumerable<Claim> claims) = await FindUserFromExternalProviderAsync(result);

        if (user == null)
        {
            // todo: aws or webex, LogoutId here
            // todo: delete local authentication cookies
            //var logoutId = await _interaction.CreateLogoutContextAsync();//it always comes as null, probably because the logout process is not initiated

            //var model = new LoggedOutViewModel
            //{
            //    LogoutId = logoutId,
            //    Reason = LogoutReasons.ACCESS_DENIED,

            //};
            // build a model so the logged out page knows what to display
            //var vm = await BuildLoggedOutViewModelAsync(model.LogoutId, model.Reason);

            // todo: url for login
            throw new UserNotFoundException(providerUserId, null, provider);// pass LogoutId url here

            //// Redirect to the external identity provider for logout
            //string url = Url.Action("Logout", new { logoutId = vm.LogoutId, reason = vm.Reason });

            //// this triggers a redirect to the external provider for sign-out
            //return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
        }

        // This allows us to collect any additional claims or properties for the specific protocols used and store them in the local auth cookie
        // This is typically used to store data needed for sign out from those protocols
        var additionalLocalClaims = new List<Claim>();
        var localSignInProps = new AuthenticationProperties();
        ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
        ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps);

        // Issue authentication cookie for user
        var identityServerUser = new IdentityServerUser(user.Subject)
        {
            DisplayName = user.Login,
            IdentityProvider = provider,
            AdditionalClaims = additionalLocalClaims
        };

        await HttpContext.SignInAsync(identityServerUser, localSignInProps);

        // Delete temporary cookie used during external authentication
        await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);

        // Retrieve return URL
        string returnUrl = result.Properties.Items["returnUrl"] ?? "~/";

        // Check if external login is in the context of an OIDC request
        AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(returnUrl);
        await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Subject, user.Login, true, context?.Client.ClientId));

        if (context != null && context.IsNativeClient())
        {
            // The client is native, so this change in how to
            // return the response is for better UX for the end user.
            return this.LoadingPage("Redirect", returnUrl);
        }

        return Redirect(returnUrl);
    }
    catch (UserNotFoundException ex)
    {
        //_logger.Error() here
        return RedirectToAction("AccessDenied", "Account");
    }
}

解决方案实现

不需要依赖logoutId,直接利用外部认证会话的现有信息触发外部IDP登出,步骤如下:

  1. 清理IdentityServer存储外部认证信息的临时Cookie
  2. 获取外部身份提供商的认证Scheme
  3. 构造登出后要跳转的登录页URL
  4. 调用SignOut方法触发外部IDP的登出流程,并重定向到登录页

修改后的代码片段(替换原user == null分支的逻辑)

if (user == null)
{
    // 1. 清理IdentityServer的外部临时Cookie
    await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);

    // 2. 获取外部身份提供商的认证Scheme(从外部认证结果中提取)
    var externalAuthScheme = result.Principal.Identity.AuthenticationType;

    // 3. 构造登出后跳转的登录页URL(可携带原请求路径)
    var postLogoutRedirectUrl = Url.Action("Login", "Account", new { returnUrl = result.Properties.Items["returnUrl"] });

    // 4. 触发外部IDP登出,完成后重定向到登录页
    return SignOut(
        new AuthenticationProperties { RedirectUri = postLogoutRedirectUrl },
        externalAuthScheme
    );
}

补充说明

  • 外部认证的Scheme可以从result.Principal.Identity.AuthenticationType直接获取,也可以从result.Properties.Items["scheme"]读取(取决于IdentityServer版本和配置)
  • 登出后重定向的URL携带原returnUrl,可让用户重新登录后回到原本的请求路径
  • 此方法适用于OIDC、SAML2等主流外部认证协议,ASP.NET Core会自动根据Scheme对应的认证处理器发起对应登出请求(比如OIDC的end_session_endpoint)

内容的提问来源于stack exchange,提问作者DayTimeCoder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 00:29:56