IdentityServer4访问令牌更新

时间:2018-04-13 12:16:41

标签: access-token identityserver4 refresh-token

上周我正在尝试配置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问题,但我仍然感到困惑。你能帮我解决一下吗?

2 个答案:

答案 0 :(得分:2)

经过调查和评论后,我找到了答案。在每次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存储到静态变量或缓存中,由您决定。

如果您有任何建议或替代方案,那么讨论是有趣的。希望它可以帮助某人。

答案 1 :(得分:0)

有两种方法可以做到这一点:

客户端 - 使用像oidc-client-js这样的lib在客户端处理身份验证和获取令牌。此功能允许通过prompt=none调用后续authorize端点自动续订令牌。

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