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

Blazor应用跨浏览器Azure B2C令牌异常:Chrome转Edge后认证失败

问题描述

我有一个基于.NET Blazor的应用,使用Azure B2C进行用户身份认证,遇到以下跨浏览器认证异常:

  • 在Chrome浏览器打开https://mywebsite.com
  • 受网络策略强制切换至Edge浏览器,显示Azure B2C登录界面,用户完成登录
  • Azure日志显示认证成功,但应用未收到令牌,跳转至错误页面https://mywebsite.com/MicrosoftIdentity/Account/Error
  • 错误截图:认证错误页面

核心问题:Windows强制跨浏览器(Chrome→Edge)时,Azure B2C认证流程中断,应用无法接收并验证令牌。


当前配置代码

Program.cs

builder.Services.AddScoped<HttpContextAccessor>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)                
    .AddMicrosoftIdentityWebApp(options => {
        builder.Configuration.Bind("AzureAdB2C", options);
        options.Events = new OpenIdConnectEvents
        {
            OnRedirectToIdentityProvider = async ctxt => { await Task.Yield(); },
            OnAuthenticationFailed = async ctxt => { await Task.Yield(); },
            OnSignedOutCallbackRedirect = async ctxt => { 
                ctxt.HttpContext.Response.Redirect(ctxt.Options.SignedOutRedirectUri); 
                ctxt.HandleResponse(); 
                await Task.Yield(); 
            },
            OnTicketReceived = async ctxt =>
            {
                if (ctxt.Principal != null)
                {
                    if (ctxt.Principal.Identity is ClaimsIdentity identity)
                    {
                        var colClaims = await ctxt.Principal.Claims.ToDynamicListAsync();
                        var IdentityProvider = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.microsoft.com/identity/claims/identityprovider")?.Value;
                        var Objectidentifier = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value;
                        var EmailAddress = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value;
                        var FirstName = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname")?.Value;
                        var LastName = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname")?.Value;
                        var AzureB2CFlow = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.microsoft.com/claims/authnclassreference")?.Value;
                        var auth_time = colClaims.FirstOrDefault(
                            c => c.Type == "auth_time")?.Value;
                        var DisplayName = colClaims.FirstOrDefault(
                            c => c.Type == "name")?.Value;
                        var idp_access_token = colClaims.FirstOrDefault(
                            c => c.Type == "idp_access_token")?.Value;
                    }
                }
                await Task.Yield();
            },
        };
    });

Appsettings.json

"AzureAdB2C": 
{
 "Instance": "https://{ Azure B2C Domain Name}.b2clogin.com/tfp/", 
 "ClientId": "<Client-Id>", 
 "Domain": "{ Azure B2C Domain Name}.onmicrosoft.com", 
 "SignUpSignInPolicyId": "B2C_1_Blazorsigninsingup", 
 "ResetPasswordPolicyId": "",
 "EditProfilePolicyId": "" 
}, 
"Logging": 
{ 
 "LogLevel": { 
 "Default": "Information",
 "Microsoft.AspNetCore": "Warning" 
 } 
},
 "AllowedHosts": "*" 
}

原因分析

跨浏览器跳转时,Chrome和Edge属于独立的浏览器进程,默认情况下:

  1. 认证流程中生成的state参数(用于防CSRF攻击)存储在浏览器会话Cookie中,无法跨浏览器共享
  2. 应用使用内存缓存存储认证会话状态,切换浏览器后,新进程无法访问原会话的状态数据,导致应用无法验证Azure B2C回调请求的合法性,进而拒绝接收令牌
  3. 空的OnAuthenticationFailed事件无法捕获具体错误,排查难度大

解决方案

1. 使用分布式缓存存储认证会话状态

将认证会话状态(如state、nonce)存储到分布式缓存(如Redis、SQL Server),确保跨浏览器/进程的请求能访问到相同的状态数据。

代码调整示例(Redis分布式缓存)

首先安装NuGet包:Microsoft.Extensions.Caching.StackExchangeRedis

在Program.cs中添加缓存配置并修改认证逻辑:

// 配置Redis分布式缓存(替换为你的Redis连接字符串)
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "AuthState-";
});

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)                
    .AddMicrosoftIdentityWebApp(options => {
        builder.Configuration.Bind("AzureAdB2C", options);
        
        // 配置使用分布式缓存存储认证状态
        var serviceProvider = builder.Services.BuildServiceProvider();
        var dataProtection = serviceProvider.GetRequiredService<IDataProtectionProvider>();
        var distributedCache = serviceProvider.GetRequiredService<IDistributedCache>();
        
        options.StateDataFormat = new DistributedCacheDataFormat(
            dataProtection.CreateProtector("Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectMiddleware", OpenIdConnectDefaults.AuthenticationScheme, "v1"),
            distributedCache,
            new OptionsMonitor<DistributedCacheDataFormatOptions>(new DistributedCacheDataFormatOptions(), null, null));

        // 完善错误捕获逻辑
        options.Events = new OpenIdConnectEvents
        {
            OnAuthenticationFailed = async ctxt =>
            {
                // 记录错误日志
                var logger = ctxt.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
                logger.LogError(ctxt.Exception, "Azure B2C认证失败");
                
                // 自定义错误跳转
                ctxt.Response.Redirect("/AuthError?message=" + Uri.EscapeDataString(ctxt.Exception.Message));
                ctxt.HandleResponse();
                await Task.Yield();
            },
            // 保留原有其他事件逻辑
            OnRedirectToIdentityProvider = async ctxt => { await Task.Yield(); },
            OnSignedOutCallbackRedirect = async ctxt => { 
                ctxt.HttpContext.Response.Redirect(ctxt.Options.SignedOutRedirectUri); 
                ctxt.HandleResponse(); 
                await Task.Yield(); 
            },
            OnTicketReceived = async ctxt =>
            {
                if (ctxt.Principal != null)
                {
                    if (ctxt.Principal.Identity is ClaimsIdentity identity)
                    {
                        var colClaims = await ctxt.Principal.Claims.ToDynamicListAsync();
                        var IdentityProvider = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.microsoft.com/identity/claims/identityprovider")?.Value;
                        var Objectidentifier = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value;
                        var EmailAddress = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value;
                        var FirstName = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname")?.Value;
                        var LastName = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname")?.Value;
                        var AzureB2CFlow = colClaims.FirstOrDefault(
                            c => c.Type == "http://schemas.microsoft.com/claims/authnclassreference")?.Value;
                        var auth_time = colClaims.FirstOrDefault(
                            c => c.Type == "auth_time")?.Value;
                        var DisplayName = colClaims.FirstOrDefault(
                            c => c.Type == "name")?.Value;
                        var idp_access_token = colClaims.FirstOrDefault(
                            c => c.Type == "idp_access_token")?.Value;
                    }
                }
                await Task.Yield();
            },
        };
    });

2. 优化Azure B2C应用配置

  • 在Azure B2C门户中,确保**回复URL(Redirect URI)**配置正确,包含https://mywebsite.com/signin-oidc(OpenID Connect默认回调路径)
  • 检查令牌配置,确保颁发的JWT格式兼容Blazor应用的解析逻辑

3. 提前引导浏览器选择(可选)

如果网络策略允许,可在应用中提前检测浏览器类型,引导用户直接使用Edge打开,避免强制切换:

// 在Blazor组件的OnInitializedAsync中添加检测逻辑
protected override async Task OnInitializedAsync()
{
    var httpContext = await HttpContextAccessor.HttpContext;
    var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
    
    if (userAgent.Contains("Chrome") && !userAgent.Contains("Edg"))
    {
        NavigationManager.NavigateTo("/UseEdge");
    }
}

验证步骤
  1. 部署修改后的应用
  2. 在Chrome中打开应用,触发强制切换到Edge
  3. 完成登录后,检查是否成功跳转回应用且用户身份正常加载
  4. 查看应用日志,确认无认证失败记录

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 04:23:12