【问题标题】:OpenIdDict (Code Flow) - Handling Access Token ExpirationOpenIdDict(代码流) - 处理访问令牌过期
【发布时间】:2020-01-24 21:03:51
【问题描述】:

我正在 ASP.Net Core 2.1 应用程序中进行重构,以从使用 SPA 的隐式流交换到使用 MVC 客户端应用程序的授权代码流。由于我们使用的是 OpenIDDict 库,因此我遵循了记录在案的 Code Flow Example,它在启动和运行方面非常出色,但我很快发现我的访问令牌即将到期,并且(如预期的那样)资源服务器开始拒绝请求。

我的问题是:如何最好地刷新访问令牌?

总的来说,我是 OpenID Connect 的新手,但我从大量可用资源中理解了理论上的模式。措辞对我来说仍然有点不透明(授予、委托人、范围等),但举一个很好的例子,我相信我可以做到这一点。

提前致谢!

我尝试过的:

基于类似的问题,我尝试使用来自上述同一来源的Refresh Flow 示例来实现刷新令牌流。尽管我相信我正确设置了身份验证服务器管道设置,但我无法找到任何使用 C# 客户端的示例(上面的示例使用 Angular 应用程序)。

编辑:当我使用 refresh_token 授权向我的令牌端点发送帖子时,我正确地取回了一个新的访问令牌。我的问题是我不确定如何最好地从那里处理它。 GetTokenAsync 继续返回陈旧的令牌。

客户端启动:

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})                
.AddCookie(options =>
{
    options.LoginPath = new PathString("/signin");     
})
.AddOpenIdConnect(options =>
{                    
    // Note: these settings must match the application details
    // inserted in the database at the server level.
    options.ClientId = "Portal"; //TODO replace via configuration   
    options.ClientSecret = "---";                                             

    options.RequireHttpsMetadata = false;
    options.GetClaimsFromUserInfoEndpoint = true;
    options.SaveTokens = true;

    options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;                    

    // Note: setting the Authority allows the OIDC client middleware to automatically
    // retrieve the identity provider's configuration and spare you from setting
    // the different endpoints URIs or the token validation parameters explicitly.
    options.Authority = "https://localhost:57851"; //TODO replace via configuration

    options.Scope.Add("email");
    options.Scope.Add("roles");                     
    options.Scope.Add("offline_access");

    options.SecurityTokenValidator = new JwtSecurityTokenHandler
    {
        // Disable the built-in JWT claims mapping feature.
        InboundClaimTypeMap = new Dictionary<string, string>()
    };

    options.TokenValidationParameters.NameClaimType = "name";
    options.TokenValidationParameters.RoleClaimType = "role";
});

验证启动:

.AddServer(options =>
            {
                // Register the ASP.NET Core MVC services used by OpenIddict.
                // Note: if you don't call this method, you won't be able to
                // bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
                options.UseMvc();

                // Enable the authorization, logout, token and userinfo endpoints.
                options.EnableAuthorizationEndpoint("/connect/authorize")
                    .EnableLogoutEndpoint("/connect/logout")
                    .EnableTokenEndpoint("/connect/token")
                    .EnableUserinfoEndpoint("/api/userinfo");

                options
                    .AllowAuthorizationCodeFlow()
                    .AllowRefreshTokenFlow();

                // Mark the "email", "profile" and "roles" scopes as supported scopes.
                options.RegisterScopes(
                    OpenIdConnectConstants.Scopes.Email,
                    OpenIdConnectConstants.Scopes.Profile,
                    OpenIddictConstants.Scopes.Roles,
                    OpenIddictConstants.Scopes.OfflineAccess);

                // When request caching is enabled, authorization and logout requests
                // are stored in the distributed cache by OpenIddict and the user agent
                // is redirected to the same page with a single parameter (request_id).
                // This allows flowing large OpenID Connect requests even when using
                // an external authentication provider like Google, Facebook or Twitter.
                options.EnableRequestCaching();

                // During development, you can disable the HTTPS requirement.
                if (env.IsDevelopment())
                {
                    options.DisableHttpsRequirement();
                    options.AddEphemeralSigningKey(); // TODO: In production, use a X.509 certificate ?
                }

                options.SetAccessTokenLifetime(TimeSpan.FromMinutes(openIdConnectConfig.AccessTokenLifetimeInMinutes));
                options.SetRefreshTokenLifetime(TimeSpan.FromHours(12));                    
            })
            .AddValidation();

描述符:

var descriptor = new OpenIddictApplicationDescriptor{
ClientId = config.Id,
ClientSecret = config.Secret,
DisplayName = config.DisplayName,                    
PostLogoutRedirectUris = { new Uri($"{config.ClientOrigin}/signout-callback-oidc") },
RedirectUris = { new Uri($"{config.ClientOrigin}/signin-oidc") },
Permissions =
{
    OpenIddictConstants.Permissions.Endpoints.Authorization,
    OpenIddictConstants.Permissions.Endpoints.Logout,
    OpenIddictConstants.Permissions.Endpoints.Token,
    OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,                        
    OpenIddictConstants.Permissions.GrantTypes.RefreshToken,                        
    OpenIddictConstants.Permissions.Scopes.Email,
    OpenIddictConstants.Permissions.Scopes.Profile,
    OpenIddictConstants.Permissions.Scopes.Roles
}};

令牌端点:

if (request.IsRefreshTokenGrantType()){
// Retrieve the claims principal stored in the refresh token.
var info = await HttpContext.AuthenticateAsync(OpenIdConnectServerDefaults.AuthenticationScheme);

// Retrieve the user profile corresponding to the refresh token.
// Note: if you want to automatically invalidate the refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null)
{
    return BadRequest(new OpenIdConnectResponse
    {
        Error = OpenIdConnectConstants.Errors.InvalidGrant,
        ErrorDescription = "The refresh token is no longer valid."
    });
}

// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
    return BadRequest(new OpenIdConnectResponse
    {
        Error = OpenIdConnectConstants.Errors.InvalidGrant,
        ErrorDescription = "The user is no longer allowed to sign in."
    });
}

// Create a new authentication ticket, but reuse the properties stored
// in the refresh token, including the scopes originally granted.
var ticket = await CreateTicketAsync(request, user, info.Properties);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OfflineAccess);      

return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);}

【问题讨论】:

  • 你找到答案了吗@playtoh?
  • 简短的回答是否定的,没有什么明确的。更长的答案是我最终做了类似以下的事情:发出请求时,我会检查当前令牌上的“expires_at”,如果它已过期或接近过期,则使用我的刷新令牌去获取新的访问令牌。然后在收到新的访问令牌后,我覆盖了现有的(cookie 存储)并用它发出了原始请求。

标签: openid-connect openiddict


【解决方案1】:

尝试使用您的访问令牌调用受保护资源是可以接受的,即使它可能已过期、无效等。如果受保护资源拒绝该令牌,您可以尝试通过发送 POST 到带有刷新令牌的 /token 端点。这是一些 JS,但这个概念仍然适用。

var refreshAccessToken = function(req, res) {
    var form_data = qs.stringify(
    {
        grant_type: 'refresh_token',
        refresh_token: refresh_token
    });
    var headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': 'Basic ' + encodeClientCredentials(client.client_id, 
                                                            client.client_secret)
    };
    console.log('Refreshing token %s', refresh_token);
    var tokRes = request('POST', authServer.tokenEndpoint, {    
            body: form_data,
            headers: headers
    });
    if (tokRes.statusCode >= 200 && tokRes.statusCode < 300) {
        var body = JSON.parse(tokRes.getBody());

        access_token = body.access_token;
        console.log('Got access token: %s', access_token);
        if (body.refresh_token) {
            refresh_token = body.refresh_token;
            console.log('Got refresh token: %s', refresh_token);
        }
        scope = body.scope;
        console.log('Got scope: %s', scope);

        // try again
        res.redirect('/fetch_resource');
        return;
    } else {
        console.log('No refresh token, asking the user to get a new access token');
        // tell the user to get a new access token
        refresh_token = null;
        res.render('error', {error: 'Unable to refresh token.'});
        return;
    }
};

【讨论】:

  • 那么如果是密码流,那么您会不断地向用户询问用户名和密码,或者冒着在某处存储凭据的风险?作者在问 openiddict 哪个是 NetCore 中的 openid 库,您的回答也不相关。
猜你喜欢
  • 2018-02-11
  • 1970-01-01
  • 1970-01-01
  • 2016-01-14
  • 2017-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多