OpenIdDict Velusia 示例:什么触发 UserInfo 被命中?

问题描述 投票:0回答:2

我根据 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 身份验证正常工作,除非我无法将我的声明加载到受保护的控制器中。

有几个问题:

  1. 第一个示例中什么触发了 UserInfo 的执行?奇怪的是,当我不通过
    Challenge
    (第一个代码块)调用登录页面时,我无法让 UserInfo 执行。我已经匹配了所有看起来相同的查询参数。
  2. id_token(我得到的)不应该包含所有相关信息,这样就不需要 UserInfo 端点了吗?
  3. 在这种情况下,将用户声明信息存储在 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 的多种配置,但均无济于事。

我当前的配置基于这样的想法:我需要 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
    };
});
jwt openid openiddict
2个回答
1
投票

您使用此设置告诉 OpenIDConnect 处理程序,它还应该对 UserInfo 端点进行单独的调用,以获取有关用户的其他声明。

options.GetClaimsFromUserInfoEndpoint = true;

AddJwtBearer 永远不会调用 UserInfo 端点。

额外打电话的原因有很多;原因之一是您可以减小 ID 令牌的大小。

您通常总是在登录后发出 cookie,并且将令牌存储在 cookie 中是安全的,因为 cookie 是由 ASP.NET Core 中的数据保护 API 加密的。然而,cookie 可能会变得相当大,如果这是一个问题,那么有一些方法可以使用自定义 SessionStore 来减小 cookie 大小。


0
投票

第一个示例中什么触发了 UserInfo 的执行?奇怪的是,当我不通过挑战(第一个代码块)调用登录页面时,我无法让 UserInfo 执行。我已经匹配了所有看起来相同的查询参数。

OpenIddict 客户端堆栈在处理授权响应(即回调请求)时自动为您执行用户信息请求。您不必做任何事情来实现这一点:OpenIddict 从 userinfo 响应中提取所有声明并通过返回的

ClaimsPrincipal
公开:然后由您决定最终存储什么(例如,在身份验证 cookie 中或在例如数据库)。

id_token(我得到的)不应该包含所有相关信息,这样就不需要 UserInfo 端点了吗?

OpenIddict 在这方面相当没有主见,由您决定是否应将声明存储在身份令牌中(即通过在调用

Destinations.IdentityToken
时指定
SetDestinations
)以及是否应由 userinfo 端点返回它们。

仅通过 userinfo 端点返回声明对于大型声明才有意义,因为如果身份令牌最终太大,当用作

id_token_hint
参数时会超出查询字符串限制等内容,这是不切实际的。

© www.soinside.com 2019 - 2024. All rights reserved.