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

ASP.NET Core与传统ASP.NET MVC的Azure AD B2C统一登出问题

Azure AD B2C跨ASP.NET与ASP.NET Core应用统一登出问题

我负责维护多个基于不同框架的应用:2个传统.NET的ASP.NET MVC应用,以及1个ASP.NET Core MVC应用,所有应用均采用Azure AD B2C进行身份验证。传统.NET应用通过OpenID Connect中间件对接Azure AD B2C,单点登录(SSO)功能正常,但实现跨应用统一登出时遇到以下问题:

核心问题

  • 单点登出异常:ASP.NET Core应用自身登出功能正常,但从该应用登出后,传统.NET应用仍保持登录状态,破坏了统一的SSO登出体验,即便所有应用使用同一Azure AD B2C租户和策略。
  • 传统.NET框架登出同步问题:在ASP.NET MVC应用中,使用Request.GetOwinContext().Authentication.SignOut()执行登出并跳转至Azure AD B2C登出URL,URL正确且跳转正常,但无法同步登出ASP.NET Core应用。

已尝试的排查动作

  • 确认所有应用的登出URL配置正确
  • 反复检查登出逻辑实现,但SSO体验仍不一致
  • 按照微软文档实现前端通道登出(front-channel logout),但问题未解决

相关代码

ASP.NET Core MVC应用代码

builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration,Microsoft.Identity.Web.Constants.AzureAdB2C);
builder.Services.Configure<CookieAuthenticationOptions>(options =>
{
    options.SlidingExpiration = true;
    options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});

builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
    options.Events.OnRedirectToIdentityProviderForSignOut = context =>
    {
        var id_token_hint = context.Properties.Items.FirstOrDefault(x => x.Key == "id_token_hint").Value;
        if (id_token_hint != null)
        {
            context.ProtocolMessage.SetParameter("id_token_hint", id_token_hint);
        }
        return Task.CompletedTask;
    };
    options.Events.OnAuthenticationFailed = context =>
    {
        context.HandleResponse();
        context.Response.Redirect("/Error/ErrorMessage?message=" + context.Exception.Message);
        return Task.CompletedTask;
    };
});

ASP.NET MVC应用代码

app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

var env = (baseEnv.ToLower() == "live") ? "" : baseEnv;
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = "Cookies",
    CookieSecure = CookieSecureOption.Always,
    CookieHttpOnly = true, 
    CookiePath = "/", 
    ExpireTimeSpan = new TimeSpan(28,0,0,0,0), 
    SlidingExpiration = true
});

var baseUrlCleansed = $"{baseUrl.TrimEnd('/')}";
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    MetadataAddress = "https://" + tenantName + ".b2clogin.com/" + tenantName + ".onmicrosoft.com/" + signinFlow + "/v2.0/.well-known/openid-configuration",
    ClientId = clientId,
    RedirectUri = $"{baseUrlCleansed}/",
    ResponseType = "id_token",
    PostLogoutRedirectUri = $"{baseUrlCleansed}/",
    Scope = "openid",
    SignInAsAuthenticationType = "Cookies",
    UseTokenLifetime = false,

    Notifications = new OpenIdConnectAuthenticationNotifications
    {
        SecurityTokenValidated = async n =>
        {
            var id = n.AuthenticationTicket.Identity;

            id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));

            var claimsProcessor = new ClaimsProcessor();
            await claimsProcessor.ProcessClaims(id);

        },
        RedirectToIdentityProvider = n =>
        {
            if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
            {
                var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
                if (idTokenHint != null)
                {
                    n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                }
            }
            return Task.FromResult(0);
        },
        AuthenticationFailed = n =>
        {
            var exception = n.Exception;
            if (exception is OpenIdConnectProtocolInvalidNonceException && exception.Message.Contains("IDX10316"))
            {
                n.HandleResponse();
                n.Response.Redirect("/");
            }
            if (exception.Message.StartsWith("OICE_20004") || exception.Message.Contains("IDX10311"))
            {
                n.SkipToNextMiddleware();
                n.Response.Redirect("/");
            }
            return Task.FromResult<object>(null);
        },
        MessageReceived = n =>
        {
            return Task.FromResult<object>(null);
        }
    }
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 07:33:11