IdentityServer4 访问令牌更新
IdentityServer4 Access token updating
上周我正在尝试配置 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 到 get/post/put/delete 数据。我将访问令牌添加到请求并获得结果。
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 问题,但我仍然感到困惑。你能帮我弄清楚吗?
有两种方法可以做到这一点:
客户端 - 使用 oidc-client-js 等库在客户端处理身份验证和令牌获取。这具有允许通过在幕后调用 authorize
端点的 prompt=none
自动更新令牌的功能。
刷新令牌 - 将其存储在您现有的 cookie 中,然后根据需要使用它来请求新的访问令牌。在这种模式下,执行 AJAX 调用的客户端代码需要知道令牌错误并自动从服务器请求新令牌,由此 GetAccessTokenAsync()
可以使用刷新令牌来获取新的访问令牌。
经过调查和评论交流,我找到了答案。在每次 API 调用之前,我都会得到超时时间,并根据结果更新 access_token 或 return existing:
[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 调用 headers:
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 存入静态变量或缓存,由您决定。
如果您有任何建议或替代方案,讨论起来会很有趣。希望它能帮助别人。
上周我正在尝试配置 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 到 get/post/put/delete 数据。我将访问令牌添加到请求并获得结果。
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 问题,但我仍然感到困惑。你能帮我弄清楚吗?
有两种方法可以做到这一点:
客户端 - 使用 oidc-client-js 等库在客户端处理身份验证和令牌获取。这具有允许通过在幕后调用 authorize
端点的 prompt=none
自动更新令牌的功能。
刷新令牌 - 将其存储在您现有的 cookie 中,然后根据需要使用它来请求新的访问令牌。在这种模式下,执行 AJAX 调用的客户端代码需要知道令牌错误并自动从服务器请求新令牌,由此 GetAccessTokenAsync()
可以使用刷新令牌来获取新的访问令牌。
经过调查和评论交流,我找到了答案。在每次 API 调用之前,我都会得到超时时间,并根据结果更新 access_token 或 return existing:
[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 调用 headers:
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 存入静态变量或缓存,由您决定。
如果您有任何建议或替代方案,讨论起来会很有趣。希望它能帮助别人。