IdentityServer4 访问令牌更新

Posted

技术标签:

【中文标题】IdentityServer4 访问令牌更新【英文标题】:IdentityServer4 Access token updating 【发布时间】:2018-09-23 19:14:33 【问题描述】:

上周我正在尝试配置 IdentityServer4 以获取自动更新的访问令牌。

我有一个 API:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            
                options.Authority = "http://localhost:5100";
                options.RequireHttpsMetadata = false;
                options.ApiName = "api1";  
            );

我的 MVC 客户端配置:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        services.AddAuthentication(options =>
            
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            )
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            
                options.SignInScheme = "Cookies";

                options.Authority = "http://localhost:5100";
                options.RequireHttpsMetadata = false;

                options.ClientId = "mvc";
                options.ClientSecret = "secret";
                options.ResponseType = "code id_token";

                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;

                options.Scope.Add("api1");
                options.Scope.Add("offline_access");
            );

以及 IdentityServer 的客户端配置:

return new List<Client>
        
            new Client
            
                ClientId = "mvc",
                ClientName = "My mvc",
                AllowedGrantTypes = GrantTypes.Hybrid,

                RequireConsent = false,
                AccessTokenLifetime = 10,

                ClientSecrets =
                
                    new Secret("secret".Sha256())
                ,

                RedirectUris           =  "http://localhost:5102/signin-oidc" ,
                PostLogoutRedirectUris =  "http://localhost:5102/signout-callback-oidc" ,

                AllowedScopes =
                
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "api1"
                ,
                AllowOfflineAccess = true
            

        ;

在客户端,我使用 AJAX 查询调用 API 来获取/发布/放置/删除数据。我将访问令牌添加到请求中并获得结果。

private async getAuthenticationHeader(): Promise<any> 
    return axios.get('/token').then((response: any) => 
        return  headers:  Authorization: `Bearer $response.data`  ;
    );


async getAsync<T>(url: string): Promise<T> 
    return this.httpClient
        .get(url, await this.getAuthenticationHeader())
        .then((response: any) => response.data as T)
        .catch((err: Error) => 
            console.error(err);
            throw err;
        );

访问令牌由MVC客户端方法提供:

[HttpGet("token")]
public async Task<string> GetAccessTokenAsync()

    //todo DoronkinDY: Cache and clear when token expired
    return await HttpContext.GetTokenAsync("access_token");

它工作正常。访问令牌过期后(在我的情况下,由于倾斜,它发生在 10 秒和 5 分钟后)我在客户端收到 401,所以如果有机会在访问令牌过期时自动更新访问令牌,那就太好了。

根据我认为的文档,可以通过将 AllowOfflineAccess 设置为 true 并添加合适的范围“offline_access”来实现。

也许我不了解访问和刷新令牌使用的正确流程。我可以自动完成还是不可能?我想,我们可以在查询中使用刷新令牌,但我不明白如何。

我已经阅读了很多 SO 答案和 github 问题,但我仍然感到困惑。你能帮我弄清楚吗?

【问题讨论】:

“我已经阅读了很多这样的答案” 也许你可以链接到其中的一些,以避免任何人将其标记为重复,如果他们没有帮不了你。 ;) 感谢您对 SO 问题的通知,我找到了合适的:***.com/questions/44175115/… 我已经实现了第二种方法。我的解决方案包括检查访问令牌的过期时间,如果不到 1 分钟 - 更新访问令牌。 【参考方案1】:

在 cmets 进行调查和沟通后,我找到了答案。在每次 API 调用之前,我都会获得过期时间并根据结果更新 access_token 或返回现有:

[HttpGet("config/accesstoken")]
public async Task<string> GetOrUpdateAccessTokenAsync()

    var accessToken = await HttpContext.GetTokenAsync("access_token");
    var expiredDate = DateTime.Parse(await HttpContext.GetTokenAsync("expires_at"), null, DateTimeStyles.RoundtripKind);

    if (!((expiredDate - DateTime.Now).TotalMinutes < 1))
    
        return accessToken;
    

    lock (LockObject)
    
        if (_expiredAt.HasValue && !((_expiredAt.Value - DateTime.Now).TotalMinutes < 1))
        
            return accessToken;
        

        var response = DiscoveryClient.GetAsync(_identitySettings.Authority).Result;
        if (response.IsError)
        
            throw new Exception(response.Error);
        

        var tokenClient = new TokenClient(response.TokenEndpoint, _identitySettings.Id, _identitySettings.Secret);
        var refreshToken = HttpContext.GetTokenAsync("refresh_token").Result;

        var tokenResult = tokenClient.RequestRefreshTokenAsync(refreshToken).Result;
        if (tokenResult.IsError)
        
            throw new Exception();
        

        accessToken = tokenResult.AccessToken;
        var idToken = HttpContext.GetTokenAsync("id_token").Result;

        var tokens = new List<AuthenticationToken>
        
            new AuthenticationToken
            
                Name = OpenIdConnectParameterNames.IdToken,
                Value = idToken
            ,
            new AuthenticationToken
            
                Name = OpenIdConnectParameterNames.AccessToken,
                Value = accessToken
            ,
            new AuthenticationToken
            
                Name = OpenIdConnectParameterNames.RefreshToken,
                Value = tokenResult.RefreshToken
            
        ;

        var expiredAt = DateTime.UtcNow.AddSeconds(tokenResult.ExpiresIn);
        tokens.Add(new AuthenticationToken
        
            Name = "expires_at",
            Value = expiredAt.ToString("o", CultureInfo.InvariantCulture)
        );

        var info = HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme).Result;
        info.Properties.StoreTokens(tokens);
            HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, info.Principal, info.Properties).Wait();

            _expiredAt = expiredAt.ToLocalTime();
        

        return accessToken;                                                 
    

我调用这个方法来获取 access_token 并将 int 添加到 API 调用头中:

private async getAuthenticationHeader(): Promise<any> 
    return axios.get('config/accesstoken').then((response: any) => 
        return  headers:  Authorization: `Bearer $response.data`  ;
    );


async getAsync<T>(url: string): Promise<T> 
    return this.axios
        .get(url, await this.getAuthenticationHeader())
        .then((response: any) => response.data as T)
        .catch((err: Error) => 
            console.error(err);
            throw err;
        );

实施了双重检查锁定以防止同时异步 API 调用尝试同时更改 access_token。您可以选择将您的 access_token 存入静态变量或缓存中,这取决于您。

如果您有任何建议或替代方案,欢迎讨论。希望它可以帮助某人。

【讨论】:

【参考方案2】:

有两种方法:

客户端 - 使用 oidc-client-js 之类的库在客户端处理令牌的身份验证和获取。它有一个功能,允许通过prompt=none 在后台调用authorize 端点自动更新令牌。

刷新令牌 - 将其存储在您现有的 cookie 中,然后根据需要使用它来请求新的访问令牌。在这种模式下,执行 AJAX 调用的客户端代码需要注意令牌错误并自动从服务器请求新令牌,GetAccessTokenAsync() 可以使用刷新令牌来获取新的访问令牌。

【讨论】:

我已经尝试过oidc-client通过mgr.getUser方法请求用户。它不起作用,可能是因为我没有调用 mgr.signIn 方法。我只是将 [Authorize] 属性添加到返回我的视图的操作中。我会用你建议的设置再调查一次。 如果我使用刷新令牌,我需要知道访问令牌的过期时间,以避免对 API 的所有调用向 IS4 发出不必要的请求。我的意思是您应该知道何时需要更新访问令牌。您能建议实施这种方法的好方法吗? 要使用 oidc-client-js 客户端库,您需要配置 IDP 以使用该客户端并通过该库执行登录过程。这是应用程序架构的重大转变,因此目前可能不是最适合您的方法。关于刷新令牌方法 - 您可以在 API 调用第一次失败时更新令牌(即查找401 状态代码和任何其他标头,如WWW-Authenticate)。或者,您可以从访问令牌中的 exp 声明中获取令牌的到期时间,并在每次 API 调用之前检查这一点。 我在每次 API 调用之前获得过期时间,并根据结果更新访问令牌或只返回现有的令牌。对于我的架构来说,这似乎是更好的解决方案。我将添加这个问题的答案。谢谢! 别担心,请随意给这个答案一个赞成票;)

以上是关于IdentityServer4 访问令牌更新的主要内容,如果未能解决你的问题,请参考以下文章

IdentityServer4 如何在授权代码流中存储和更新令牌

IdentityServer4源码解析_4_令牌发放接口

IdentityServer4 参考访问令牌有时作为 JWT 令牌传输

如何通过方法(不是api)通过identityserver4获取访问令牌?

刷新 IdentityServer4 客户端中的访问令牌

如何通过自省验证 IdentityServer4 访问令牌