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

.NET 6 MVC JWT认证SignInAsync报错问题及解决

.NET 6 MVC中JWT认证(结合OIDC)的问题与解决

需求背景

基于.NET 6和ASP.NET MVC实现JWT认证:通过OpenID Connect对接公司身份提供商完成认证,获取JWT访问令牌并自动添加到请求头,用于后续内部服务授权调用。所有控制器均添加[Authorize]特性,不接受基于Cookie的认证方案。

初始实现代码

以下是Program.cs中的初始认证配置代码:

const string AUTH_COOKIE_NAME = "access_token";
const string AUTH_SCHEMA_NAME = "OPENIDC_JWT";
const string BEARER_AUTH_SCHEMA_NAME = "bearer";
const string OIDC_AUTH_SCHEMA_NAME = "oidc";

builder.Services.AddAuthentication(options => {

    options.DefaultScheme = AUTH_SCHEMA_NAME;
    options.DefaultAuthenticateScheme = OIDC_AUTH_SCHEMA_NAME;
    options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME;
})
    .AddOpenIdConnect(OIDC_AUTH_SCHEMA_NAME, o =>
    {
        o.MetadataAddress = builder.Configuration["SingleSignOn:MetaData"];
        o.Authority = builder.Configuration["SingleSignOn:Authority"];
        o.ClientId = builder.Configuration["SingleSignOn:ClientId"];
        o.ClientSecret = builder.Configuration["SingleSignOn:ClientSecret"];
        o.ResponseType = OpenIdConnectResponseType.Code;
        o.SaveTokens = true;
        o.Scope.Clear();
        o.Scope.Add("openid");
        o.Scope.Add("email");
        o.CallbackPath = "/signin-oidc";
        o.AccessDeniedPath = "/Account/AccessDenied";
        o.SignInScheme = BEARER_AUTH_SCHEMA_NAME;
        
        o.Events = new OpenIdConnectEvents
        {
            OnTokenValidated = async context =>
            {
                var accessToken = context.SecurityToken as JwtSecurityToken;
                if (accessToken != null)
                {
                    // 将访问令牌存储到认证属性中
                    context.Properties.StoreTokens(new[]
                    {
                        new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = accessToken.RawData },
                    });
                }
               
                context.Request.Headers.Append("Authorization", $"Bearer {accessToken.RawData}");
                var roleService = context.HttpContext.RequestServices.GetRequiredService<IUserRolesService>();
                roleService.ManageUserRoles(context);
            },
        };
    })
    .AddJwtBearer(BEARER_AUTH_SCHEMA_NAME, options =>
    {
        options.MetadataAddress = "https://access-staging.epam.com/auth/realms/plusx/.well-known/openid-configuration";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true, // 应设置为true并指定应用的ValidAudience
            ValidAudience = builder.Configuration["SingleSignOn:ClientId"],
            ValidateIssuerSigningKey = true,
        };
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];

                // 优先从Authorization头取令牌,没有则从认证Cookie获取
                if (!string.IsNullOrEmpty(authCookie))
                {
                    context.Token = authCookie;
                }
                return Task.CompletedTask;
            }
        };
    })
    .AddPolicyScheme(AUTH_SCHEMA_NAME, AUTH_SCHEMA_NAME, options =>
    {
        options.ForwardDefaultSelector = context =>
        {
            string authHeader = context.Request.Headers[HeaderNames.Authorization];
            string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];

            // 若请求头或Cookie中有令牌,使用bearer方案;否则使用oidc方案
            if (!string.IsNullOrEmpty(authHeader) || !string.IsNullOrEmpty(authCookie))
                return BEARER_AUTH_SCHEMA_NAME;

            return OIDC_AUTH_SCHEMA_NAME;
        };
    });

builder.Services.AddAuthorization();

// 其他服务配置

var app = builder.Build();

AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

// 配置HTTP请求管道
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();// 确保所有内容均通过HTTPS传输
}

app.UseHttpsRedirection();  
app.UseStaticFiles();
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseRouting();
app.UseAuthentication();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Admin}/{action=Index}/{id?}");

app.Run();

角色分配服务示例代码:

public void ManageUserRoles(TokenValidatedContext context)
{
    var user = context.Principal;

    if (user.Identity.IsAuthenticated)
    {
        if (!user.HasClaim(c => c.Type == ClaimTypes.Role))
        {
          if (!string.IsNullOrEmpty(userEmail) && isAdmin)
           {
             _logger.LogInformation($"Set the '{RoleNames.Admin}' role to user with email:  '{userEmail}'");
              identity.AddClaim(new Claim(ClaimTypes.Role, RoleNames.Admin));
            }
    }
}

错误信息

离开OnTokenValidated事件后触发错误:
InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT.

堆栈跟踪:

System.InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT.

   at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)

   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()

   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)

   at Middlewares.SecurityHeadersMiddleware.InvokeAsync(HttpContext context) in Middlewares\SecurityHeadersMiddleware.cs:line 23

   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

尝试过的Scheme配置

  • 仅将Bearer设为默认Scheme:无效果
  • 将AUTH_SCHEMA_NAME设为默认、默认认证、默认挑战Scheme:报错相同
  • 仅设置DefaultScheme为AUTH_SCHEMA_NAME:报错相同
  • 设置DefaultScheme为BEARER_AUTH_SCHEMA_NAME,DefaultChallengeScheme为OIDC_AUTH_SCHEMA_NAME:报错相同

解决方案

通过调整认证Scheme的核心配置解决问题,若需自定义身份声明,需实现IClaimsTransformation接口。以下是可行的完整配置代码:

// 添加服务到容器
builder.Services.AddTransient<IManageUsersRolesJwtContext, UserRolesService>();
builder.Services.AddTransient<IClaimsTransformation, ShipmentClaimsTransformation>();

// 定义认证方案和Cookie名称常量
const string AUTH_COOKIE_NAME = "access_token";
const string CUSTOM_POLICY_SCHEMA_NAME = "OIDC_JWT";
const string BEARER_AUTH_SCHEMA_NAME = "Bearer";
const string OIDC_AUTH_SCHEMA_NAME = "oidc";
const string METADATA_ADDRESS = "https://***/openid-configuration";

// 配置Cookie策略选项
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
});

// 配置认证服务
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CUSTOM_POLICY_SCHEMA_NAME;
    options.DefaultAuthenticateScheme = BEARER_AUTH_SCHEMA_NAME;
    options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME;
})
    // 添加OpenID Connect认证
    .AddOpenIdConnect(OIDC_AUTH_SCHEMA_NAME, o =>
    {
        o.MetadataAddress = builder.Configuration["SingleSignOn:MetaData"];
        o.Authority = builder.Configuration["SingleSignOn:Authority"];
        o.ClientId = builder.Configuration["SingleSignOn:ClientId"];
        o.ClientSecret = builder.Configuration["SingleSignOn:ClientSecret"];
        o.GetClaimsFromUserInfoEndpoint = true;
        o.ResponseType = "code";
        o.SaveTokens = true;
        o.Scope.Clear();
        o.Scope.Add("openid");
        o.Scope.Add("profile");
        o.Scope.Add("email");
        o.CallbackPath = "/signin-oidc";
        o.Events = new OpenIdConnectEvents
        {
            // 令牌验证通过后存储访问令牌
            OnTokenValidated = context =>
            {
                var accessToken = context.SecurityToken as JwtSecurityToken;
                if (accessToken != null)
                {
                    // 将访问令牌存储到认证属性中
                    context.Properties.StoreTokens(new[]
                    {
                        new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = accessToken.RawData },
                    });
                }

                // 设置认证Cookie
                context.HttpContext.Response.Cookies.Append(AUTH_COOKIE_NAME, accessToken.RawData, new CookieOptions
                {
                    HttpOnly = true,
                    Secure = true,
                    SameSite = SameSiteMode.None,
                    Expires = context.SecurityToken.ValidTo.AddMinutes(-1)
                });
                context.Response.Headers.Add("Authorization", $"Bearer {accessToken.RawData}");
                // 重定向到目标地址并结束响应处理
                context.Response.Redirect(context.Properties.RedirectUri);
                context.HandleResponse();
                return Task.CompletedTask;
            }
        };
    })
    // 添加JWT Bearer认证
    .AddJwtBearer(BEARER_AUTH_SCHEMA_NAME, options =>
    {
        // 配置JWT选项
        options.MetadataAddress = METADATA_ADDRESS;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["SingleSignOn:Authority"],
            ValidateAudience = true, // 应设置为true并指定应用的ValidAudience
            ValidAudience = builder.Configuration["SingleSignOn:ClientId"],
            ValidateIssuerSigningKey = true,
        };
        options.Events = new JwtBearerEvents
        {
            // 从Authorization头或认证Cookie中获取令牌
            OnMessageReceived = context =>
            {
                string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];
                string authHeader = context.Request.Headers[HeaderNames.Authorization];

                // 优先从Authorization头取令牌,没有则从认证Cookie获取
                if (!string.IsNullOrEmpty(authCookie))
                {
                     context.Token = authCookie;
                }
                return Task.CompletedTask;
            }
        };
    })
    // 添加自定义策略方案,用于选择认证方式
    .AddPolicyScheme(CUSTOM_POLICY_SCHEMA_NAME, CUSTOM_POLICY_SCHEMA_NAME, options =>
    {
        options.ForwardDefaultSelector = context =>
        {
            var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false;

            string authHeader = context.Request.Headers[HeaderNames.Authorization];
            string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];

            // 若请求头或Cookie中有令牌,使用Bearer方案;否则使用OIDC方案
            if (!string.IsNullOrEmpty(authHeader) || !string.IsNullOrEmpty(authCookie))
                return BEARER_AUTH_SCHEMA_NAME;
            else
                return OIDC_AUTH_SCHEMA_NAME;
        };
    });

builder.Services.AddAuthorization();

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 03:13:12