我根据 Velusia OpenIddict 示例(授权代码流程)进行了强烈建模:
在客户端,授权的第一步是进入登录重定向:
[HttpGet("~/login")]
public ActionResult LogIn(string returnUrl)
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientAspNetCoreConstants.Properties.Issuer] = "https://localhost:44313/"
})
{
// Only allow local return URLs to prevent open redirect attacks.
RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
};
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
}
请注意,它通过
Challenge
重定向到授权服务器上的登录页面:
成功登录后,代码将传输到服务器/授权
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Try to retrieve the user principal stored in the authentication cookie and redirect
// the user agent to the login page (or to an external provider) in the following cases:
//
// - If the user principal can't be extracted or the cookie is too old.
// - If prompt=login was specified by the client application.
// - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result == null || !result.Succeeded || request.HasPrompt(Prompts.Login) ||
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
...
然后,由于我使用的是隐式同意,它会立即将自身传输到 Exchange:
[HttpPost("~/connect/token"), IgnoreAntiforgeryToken, Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/refresh token.
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
然后,神奇地(!),它直接进入 UserInfo (我的实现):
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> Userinfo()
{
var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
var claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
var user = await _userManager.FindByIdAsync(claimsPrincipal?.GetClaim(Claims.Subject) ?? throw new Exception("Principal cannot be found!"));
然后回到重定向LoginCallback指定的客户端
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
public async Task<ActionResult> LogInCallback()
{
// Retrieve the authorization data validated by OpenIddict as part of the callback handling.
var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
// Multiple strategies exist to handle OAuth 2.0/OpenID Connect callbacks, each with their pros and cons:
//
// * Directly using the tokens to perform the necessary action(s) on behalf of the user, which is suitable
// for applications that don't need a long-term access to the user's resources or don't want to store
// access/refresh tokens in a database or in an authentication cookie (which has security implications).
// It is also suitable for applications that don't need to authenticate users but only need to perform
...
return SignIn(new ClaimsPrincipal(identity), properties, CookieAuthenticationDefaults.AuthenticationScheme);
所有声明均被收集并存储在 cookie 中。
结果是,当我转到受保护的控制器时,所有用
Destinations.IdentityToken
目的地指定的声明都会出现!
这太完美了,正是我想要的!但该示例使用 cookie 身份验证。我需要使用 JWT 身份验证。
我可以让 JWT 身份验证正常工作,除非我无法将我的声明加载到受保护的控制器中。
有几个问题:
Challenge
(第一个代码块)调用登录页面时,我无法让 UserInfo 执行。我已经匹配了所有看起来相同的查询参数。在我的program.cs (.net 6) 中的客户端应用程序中
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore().UseDbContext<OpenIddictContext>();
})
.AddClient(options =>
{
options.AllowAuthorizationCodeFlow();
options.AddDevelopmentEncryptionCertificate().AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableRedirectionEndpointPassthrough()
.EnablePostLogoutRedirectionEndpointPassthrough();
options.UseSystemNetHttp();
options.AddRegistration(new OpenIddict.Client.OpenIddictClientRegistration
{
Issuer = new Uri(configuration?["OpenIddict:Issuer"] ?? throw new Exception("Configuration.Issuer is null for AddOpenIddict")),
ClientId = configuration["OpenIddict:ClientId"],
ClientSecret = configuration["OpenIddict:ClientSecret"],
Scopes = { Scopes.OpenId, Scopes.OfflineAccess, "api" },
RedirectUri = new Uri("callback/login/local", UriKind.Relative), //Use this when going directly to the login
//RedirectUri=new Uri("swagger/oauth2-redirect.html", UriKind.Relative), //Use this when using Swagger to JWT authenticate
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative)
});
})
.AddValidation(option =>
{
option.SetIssuer(configuration?["OpenIddict:Issuer"] ?? throw new Exception("Configuration.Issuer is null for AddOpenIddict"));
option.AddAudiences(configuration?["OpenIddict:Audience"] ?? throw new Exception("Configuration is missing!"));
option.UseSystemNetHttp();
option.UseAspNetCore();
});
我改变了这个(cookie身份验证)
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.ExpireTimeSpan = TimeSpan.FromMinutes(50);
options.SlidingExpiration = false;
});
对此:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
//.AddCookie(p =>
//{
// p.SlidingExpiration = true;
// p.Events.OnSigningIn = (context) =>
// {
// context.CookieOptions.Expires = DateTimeOffset.UtcNow.AddHours(14);
// return Task.CompletedTask;
// };
//})
//.AddOpenIdConnect(options =>
//{
// options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// options.RequireHttpsMetadata = true;
// options.Authority = configuration?["OpenIddict:Issuer"];
// options.ClientId = configuration?["OpenIddict:ClientId"];
// options.ClientSecret = configuration?["OpenIddict:ClientSecret"];
// options.ResponseType = OpenIdConnectResponseType.Code;
// options.Scope.Add("openid");
// options.Scope.Add("profile");
// options.Scope.Add("offline_access");
// options.Scope.Add("api");
// options.GetClaimsFromUserInfoEndpoint = true;
// options.SaveTokens = true;
// //options.TokenValidationParameters = new TokenValidationParameters
// //{
// // NameClaimType = "name",
// // RoleClaimType = "role"
// //};
//});
.AddJwtBearer(options =>
{
options.Authority = configuration?["OpenIddict:Issuer"];
options.Audience = configuration?["OpenIddict:Audience"];
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = configuration?["OpenIddict:Issuer"],
ValidAudience = configuration?["OpenIddict:Audience"],
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.Zero
};
});
请注意,我尝试了基于.NET OpenIdConnect 的多种配置,但均无济于事。
我当前的配置基于这样的想法:我需要 JWTBearer 进行身份验证,并需要 AddOpenIdConnect 进行附加声明。此设置可以很好地进行身份验证,但不会添加额外的声明:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Policy1", policy => policy.RequireClaim("Claim1", "0"));
options.AddPolicy("Policy2", policy => policy.RequireClaim("Claim2", "1"));
options.AddPolicy("Policy3", policy => policy.RequireClaim("Claim3", "1"));
options.AddPolicy("DefaultPolicy", policy => policy.RequireAuthenticatedUser().RequireClaim("HasAccess"));
//var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
// JwtBearerDefaults.AuthenticationScheme);
//defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
//options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(p =>
{
p.SlidingExpiration = true;
p.Events.OnSigningIn = (context) =>
{
context.CookieOptions.Expires = DateTimeOffset.UtcNow.AddHours(14);
return Task.CompletedTask;
};
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.RequireHttpsMetadata = true;
options.Authority = configuration?["OpenIddict:Issuer"];
options.ClientId = configuration?["OpenIddict:ClientId"];
options.ClientSecret = configuration?["OpenIddict:ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("offline_access");
options.Scope.Add("api");
//options.Scope.Add("openid");
//options.Scope.Add("offline_access");
//options.Scope.Add("api");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
//options.TokenValidationParameters = new TokenValidationParameters
//{
// NameClaimType = "name",
// RoleClaimType = "role"
//};
})
.AddJwtBearer(options =>
{
options.Authority = configuration?["OpenIddict:Issuer"];
options.Audience = configuration?["OpenIddict:Audience"];
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = configuration?["OpenIddict:Issuer"],
ValidAudience = configuration?["OpenIddict:Audience"],
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.Zero
};
});
您使用此设置告诉 OpenIDConnect 处理程序,它还应该对 UserInfo 端点进行单独的调用,以获取有关用户的其他声明。
options.GetClaimsFromUserInfoEndpoint = true;
AddJwtBearer 永远不会调用 UserInfo 端点。
额外打电话的原因有很多;原因之一是您可以减小 ID 令牌的大小。
您通常总是在登录后发出 cookie,并且将令牌存储在 cookie 中是安全的,因为 cookie 是由 ASP.NET Core 中的数据保护 API 加密的。然而,cookie 可能会变得相当大,如果这是一个问题,那么有一些方法可以使用自定义 SessionStore 来减小 cookie 大小。
第一个示例中什么触发了 UserInfo 的执行?奇怪的是,当我不通过挑战(第一个代码块)调用登录页面时,我无法让 UserInfo 执行。我已经匹配了所有看起来相同的查询参数。
OpenIddict 客户端堆栈在处理授权响应(即回调请求)时自动为您执行用户信息请求。您不必做任何事情来实现这一点:OpenIddict 从 userinfo 响应中提取所有声明并通过返回的
ClaimsPrincipal
公开:然后由您决定最终存储什么(例如,在身份验证 cookie 中或在例如数据库)。
id_token(我得到的)不应该包含所有相关信息,这样就不需要 UserInfo 端点了吗?
OpenIddict 在这方面相当没有主见,由您决定是否应将声明存储在身份令牌中(即通过在调用
Destinations.IdentityToken
时指定 SetDestinations
)以及是否应由 userinfo 端点返回它们。
仅通过 userinfo 端点返回声明对于大型声明才有意义,因为如果身份令牌最终太大,当用作
id_token_hint
参数时会超出查询字符串限制等内容,这是不切实际的。