【发布时间】:2023-02-25 16:28:37
【问题描述】:
我强烈模仿 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 重定向到授权服务器上的登录页面:
成功登录后,代码传输到服务器 /Authorize
[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 身份验证正常工作,除非我无法将我的声明加载到我的受保护控制器中。
所以有几个问题:
- 是什么触发了第一个示例中要执行的 UserInfo?奇怪的是,当我不通过
Challenge(第一个代码块)调用登录页面时,我无法让 UserInfo 执行。我匹配了所有看起来相同的查询参数。 - id_token(我得到的)不应该包含所有相关信息,以便不需要 UserInfo 端点吗?
- 在这种情况下,将用户声明信息存储在 cookie 中是否合适?我看不到任何其他保存此信息的好方法。在这种情况下执行此操作的最佳方法是什么,以便我的声明委托人在我进入受保护的控制器后自动加载所有声明?
在我的 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 的配置都无济于事。
【问题讨论】:
标签: jwt openid openiddict