我的任务是做JWT认证。我的应用程序使用 .NET 6 和 ASP NET MVC。 我使用 OpenID Connect 在服务器端身份验证期间获取 JWT 访问令牌,然后将其自动添加到您授权的 API 调用的请求标头中。
我的代码在
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)
{
// Store the access token in the authentication properties
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, // You shoud set ValidateAudience = true and specify ValidAudience for your application.
ValidAudience = builder.Configuration["SingleSignOn:ClientId"],
ValidateIssuerSigningKey = true,
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];
// Token will be taken from Authorization header or if header is not set from authentication 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];
// If Token presents in header or cookie choose "bearer" schema, in other case use "oidc" schema.
if (!string.IsNullOrEmpty(authHeader) || !string.IsNullOrEmpty(authCookie))
return BEARER_AUTH_SCHEMA_NAME;
return OIDC_AUTH_SCHEMA_NAME;
};
});
builder.Services.AddAuthorization();
// more services
var app = builder.Build();
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();//HTTP Strict Transport Security Protocol is used to be sure that none of your content is still server over HTTP
}
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();
正如您从代码中看到的,我通过 OpenIdConnect 向我的公司进行身份验证,接收 JWT 访问令牌并将其插入标头中,以便通过此 JWT 令牌与我公司的其他服务进一步通信。当客户端需要调用受保护的 API 端点时,它必须使用
Authorization
方案在 Bearer
标头中包含 JWT 令牌,如下所示:
Authorization: Bearer your_jwt_token
错误跟踪:上下文进入服务,根据用户的电子邮件,为他们分配声明。服务示例:
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.
。
我正在使用
AddPolicyScheme
之类的处理程序,以防万一我在标头中有 JWT 令牌,我正在使用 JWTBearer
身份验证,验证令牌并进入本地主机上的应用程序;否则,如果标头中的 JWT 令牌为空,我必须通过我公司的身份提供商进行身份验证,接收令牌,将其添加到标头,然后在“AddJwtBearer”中验证它并授权到我的应用程序中。
还要提一下,我的所有控制器上都有属性
[Authorize]
。并且不要让我在身份验证服务中使用 .AddCookie
或使用基于 cookie 的身份验证方案。
我尝试仅使用承载身份验证方案作为默认值:
builder.Services.AddAuthentication(options => {
options.DefaultScheme = BEARER_AUTH_SCHEMA_NAME;
})
-- 之后什么也没有发生。
我尝试在身份验证方案中使用不同的方案变体:
options.DefaultScheme = AUTH_SCHEMA_NAME;
options.DefaultAuthenticateScheme = AUTH_SCHEMA_NAME;
options.DefaultChallengeScheme = AUTH_SCHEMA_NAME;
-- 之后同样的错误:
InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT.
。
尝试仅使用一种默认方案:
options.DefaultScheme = AUTH_SCHEMA_NAME;
之后同样的错误:
InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT.
。
也尝试过这个变体:
builder.Services.AddAuthentication(options => {
options.DefaultScheme = BEARER_AUTH_SCHEMA_NAME;
options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME;
})
之后同样的错误:
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)`
更新: 我的工作示例如下。通过设置
AddAuthentication
选项并指定身份验证方案解决了身份验证方案的问题。如果您需要对您的身份进行一些自定义声明,您需要实现 IClaimsTransformation 接口以在身份验证过程中具有内置声明逻辑。
// Add services to the container.
builder.Services.AddTransient<IManageUsersRolesJwtContext, UserRolesService>();
builder.Services.AddTransient<IClaimsTransformation, ShipmentClaimsTransformation>();
// Define constants for authentication schemes and cookie name const string
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";
// Configure the cookie policy options
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Configure the authentication services
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CUSTOM_POLICY_SCHEMA_NAME;
options.DefaultAuthenticateScheme = BEARER_AUTH_SCHEMA_NAME;
options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME;
})
// Add custom policy scheme for handling bearer and OIDC authentication
.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
{
// Store access token in authentication properties when token is validated
OnTokenValidated = context =>
{
var accessToken = context.SecurityToken as JwtSecurityToken;
if (accessToken != null)
{
// Store the access token in the authentication properties
context.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = accessToken.RawData },
});
}
// Set the authentication 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}");
// Redirect and handle response
context.Response.Redirect(context.Properties.RedirectUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
})
// Add JWT Bearer authentication
.AddJwtBearer(BEARER_AUTH_SCHEMA_NAME, options =>
{
// Configure JWT options
options.MetadataAddress = METADATA_ADDRESS;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["SingleSignOn:Authority"],
ValidateAudience = true, // You shoud set ValidateAudience = true and specify ValidAudience for your application.
ValidAudience = builder.Configuration["SingleSignOn:ClientId"],
ValidateIssuerSigningKey = true,
};
options.Events = new JwtBearerEvents
{
// Get the token from either the authorization header or authentication cookie
OnMessageReceived = context =>
{
string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];
string authHeader = context.Request.Headers[HeaderNames.Authorization];
// Token will be taken from Authorization header or if header is not set from authentication 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];
// If Token presents in the header or cookie choose "bearer" schema, in other case use "oidc" schema.
if (!string.IsNullOrEmpty(authHeader) || !string.IsNullOrEmpty(authCookie))
return BEARER_AUTH_SCHEMA_NAME;
else
return OIDC_AUTH_SCHEMA_NAME;
};
});
builder.Services.AddAuthorization();
```