最少特权的身份服务器在 Azure 上无法正常工作
Identity Server by leastprivilege doesn't work properly on Azure
我正在尝试实现遵循 OAUTH2/OIDC 协议的体系结构。为了做到这一点,我有 STS(Identity Server v3 by leastprivilege),ASP.NET WebApi 和 ASP.NET MVC 客户端应用程序。我的目标是将 STS 和 REST 服务托管在 Azure 上,以便不同的客户端可以将它们用作 public 服务。到目前为止,一切都很好。在我决定添加一个使用其中一种重定向流程(授权代码流程)的新客户端之前,一切似乎都运行顺利且完美。我想利用它提供的刷新令牌选项。我想为该客户提供短期访问令牌(10 分钟),并让他使用刷新令牌来获取新令牌。这就是它在代码中的样子:
STS:
new Client
{
ClientId = "tripgalleryauthcode",
ClientName = "Trip Gallery (Authorization Code)",
Flow = Flows.AuthorizationCode,
AllowAccessToAllScopes = true,
RequireConsent = false,
RedirectUris = new List<string>
{
Tripgallery.Constants.TripgalleryMvcAuthCodePostLogoutCallback
},
ClientSecrets = new List<Secret>()
{
new Secret(Tripgallery.Constants.TripgalleryClientSecret.Sha256())
},
// refresh token options
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 600,
RefreshTokenUsage = TokenUsage.OneTimeOnly, // Every time generates new refresh token. Not only access token.
RefreshTokenExpiration = TokenExpiration.Sliding,
SlidingRefreshTokenLifetime = 1296000,
PostLogoutRedirectUris = new List<string>()
{
Tripgallery.Constants.TripgalleryPostLogoutCallback
}
}
Mvc 应用程序(客户端):
private ObjectCache _cache;
private readonly string tokensCacheKey = "Tokens";
public HomeController()
{
_cache = MemoryCache.Default;
}
// GET: Home
public ActionResult Index()
{
var authorizeRequest = new AuthorizeRequest(Constants.BoongalooSTSAuthorizationEndpoint);
var state = HttpContext.Request.Url.OriginalString;
var url = authorizeRequest.CreateAuthorizeUrl(
"tripgalleryauthcode",
"code",
"openid profile address tripgallerymanagement offline_access",
Constants.TripgalleryMvcAuthCodePostLogoutCallback,
state);
HttpContext.Response.Redirect(url);
return null;
}
public async Task<ActionResult> StsCallBackForAuthCodeClient()
{
var authCode = Request.QueryString["code"];
var client = new TokenClient(
Constants.TripgallerySTSTokenEndpoint,
"tripgalleryauthcode",
Constants.TripgalleryClientSecret
);
var tokenResponse = await client.RequestAuthorizationCodeAsync(
authCode,
Constants.TripgalleryMvcAuthCodePostLogoutCallback
);
this._cache[this.tokensCacheKey] = new TokenModel()
{
AccessToken = tokenResponse.AccessToken,
IdToken = tokenResponse.IdentityToken,
RefreshToken = tokenResponse.RefreshToken,
AccessTokenExpiresAt = DateTime.Parse(DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToString(CultureInfo.InvariantCulture))
};
return View();
}
public ActionResult StartCallingWebApi()
{
var timer = new Timer(async (e) =>
{
var cachedStuff = this._cache.Get(this.tokensCacheKey) as TokenModel;
await ExecuteWebApiCall(cachedStuff);
}, null, 0, Convert.ToInt32(TimeSpan.FromMinutes(20).TotalMilliseconds));
return null;
}
private async Task ExecuteWebApiCall(TokenModel cachedStuff)
{
// Ensure that access token expires in more than one minute
if (cachedStuff != null && cachedStuff.AccessTokenExpiresAt > DateTime.Now.AddMinutes(1))
{
await MakeValidApiCall(cachedStuff);
}
else
{
// Use the refresh token to get a new access token, id token and refresh token
var client = new TokenClient(
Constants.TripgallerySTSTokenEndpoint,
"tripgalleryauthcode",
Constants.TripgalleryClientSecret
);
if (cachedStuff != null)
{
var newTokens = await client.RequestRefreshTokenAsync(cachedStuff.RefreshToken);
var value = new TokenModel()
{
AccessToken = newTokens.AccessToken,
IdToken = newTokens.IdentityToken,
RefreshToken = newTokens.RefreshToken,
AccessTokenExpiresAt =
DateTime.Parse(
DateTime.Now.AddSeconds(newTokens.ExpiresIn).ToString(CultureInfo.InvariantCulture))
};
this._cache.Set(this.tokensCacheKey, (object)value, new CacheItemPolicy());
await MakeValidApiCall(value);
}
}
}
问题是,如果我将 STS 托管在 Azure 上,出于某种原因,如果我决定在访问令牌过期后 20 分钟或更长时间内使用刷新令牌,我会收到错误消息。不管我的刷新令牌寿命是 15 天。
也就是STS产生的日志:
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.456 +00:00 [Warning] AuthorizationCodeStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] TokenHandleStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] ConsentStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] RefreshTokenStore not configured - falling back to InMemory
w3wp.exe Information: 0 : 2017-04-06 12:01:22.371 +00:00 [Information] Start token request
w3wp.exe Information: 0 : 2017-04-06 12:01:22.418 +00:00 [Information] Client secret id found: "tripgalleryauthcode"
w3wp.exe Information: 0 : 2017-04-06 12:01:22.418 +00:00 [Information] Client validation success
w3wp.exe Information: 0 : 2017-04-06 12:01:22.418 +00:00 [Information] Start token request validation
w3wp.exe Information: 0 : 2017-04-06 12:01:22.433 +00:00 [Information] Start validation of refresh token request
w3wp.exe Warning: 0 : 2017-04-06 12:01:22.574 +00:00 [Warning] "Refresh token is invalid"
"{
\"ClientId\": \"tripgalleryauthcode\",
\"ClientName\": \"Trip Gallery (Authorization Code)\",
\"GrantType\": \"refresh_token\",
\"RefreshToken\": \"140cfb19405a6a4cbace29646751194a\",
\"Raw\": {
\"grant_type\": \"refresh_token\",
\"refresh_token\": \"140cfb19405a6a4cbace29646751194a\"
}
}"
w3wp.exe Information: 0 : 2017-04-06 12:01:22.590 +00:00 [Information] End token request
w3wp.exe Information: 0 : 2017-04-06 12:01:22.590 +00:00 [Information] Returning error: invalid_grant
w3wp.exe Information: 0 : 2017-04-06 12:01:29.465 +00:00 [Information] Start discovery request
w3wp.exe Information: 0 : 2017-04-06 12:01:29.512 +00:00 [Information] Start key discovery request
与我本地机器上的 STS 运行 相同的情况按预期工作。我可以使用刷新令牌获取新令牌。
已爱:
Fred Han - MSFT 确实指出了这个问题。我需要为我的刷新令牌实现持久存储。实现它真的很容易。我是这样做的:
Startup.cs 身份服务器 :
var idServerServiceFactory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
//...
// use custom service for tokens maintainance
var customRefreshTokenStore = new CustomRefreshTokenStore();
idServerServiceFactory.RefreshTokenStore = new Registration<IRefreshTokenStore>(resolver => customRefreshTokenStore);
var options = new IdentityServerOptions
{
Factory = idServerServiceFactory,
// .....
}
idsrvApp.UseIdentityServer(options);
CustomRefreshTokenStore.cs
public class CustomRefreshTokenStore : IRefreshTokenStore
{
public Task StoreAsync(string key, RefreshToken value)
{
// code that uses persitant storage mechanism
}
public Task<RefreshToken> GetAsync(string key)
{
// code that uses persitant storage mechanism
}
public Task RemoveAsync(string key)
{
// code that uses persitant storage mechanism
}
public Task<IEnumerable<ITokenMetadata>> GetAllAsync(string subject)
{
// code that uses persitant storage mechanism
}
public Task RevokeAsync(string subject, string client)
{
// code that uses persitant storage mechanism
}
}
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.456 +00:00 [Warning] AuthorizationCodeStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] TokenHandleStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] ConsentStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] RefreshTokenStore not configured - falling back to InMemory
您似乎 store/maintain 数据在内存中,如果您将其托管在 Azure 网站上并在负载均衡器后面使用多实例,这可能是问题的原因。您可以尝试将数据存储在其他数据存储中,而不是内存存储中。
_cache = MemoryCache.Default;
此外,您通过 Web API 应用程序中的内存存储和检索 tokensCacheKey
,这在 Azure 多实例网络场环境中无法正常工作。请将数据存储在外部存储中,例如 Azure 存储、数据库或 Redis 缓存。
我正在尝试实现遵循 OAUTH2/OIDC 协议的体系结构。为了做到这一点,我有 STS(Identity Server v3 by leastprivilege),ASP.NET WebApi 和 ASP.NET MVC 客户端应用程序。我的目标是将 STS 和 REST 服务托管在 Azure 上,以便不同的客户端可以将它们用作 public 服务。到目前为止,一切都很好。在我决定添加一个使用其中一种重定向流程(授权代码流程)的新客户端之前,一切似乎都运行顺利且完美。我想利用它提供的刷新令牌选项。我想为该客户提供短期访问令牌(10 分钟),并让他使用刷新令牌来获取新令牌。这就是它在代码中的样子:
STS:
new Client
{
ClientId = "tripgalleryauthcode",
ClientName = "Trip Gallery (Authorization Code)",
Flow = Flows.AuthorizationCode,
AllowAccessToAllScopes = true,
RequireConsent = false,
RedirectUris = new List<string>
{
Tripgallery.Constants.TripgalleryMvcAuthCodePostLogoutCallback
},
ClientSecrets = new List<Secret>()
{
new Secret(Tripgallery.Constants.TripgalleryClientSecret.Sha256())
},
// refresh token options
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 600,
RefreshTokenUsage = TokenUsage.OneTimeOnly, // Every time generates new refresh token. Not only access token.
RefreshTokenExpiration = TokenExpiration.Sliding,
SlidingRefreshTokenLifetime = 1296000,
PostLogoutRedirectUris = new List<string>()
{
Tripgallery.Constants.TripgalleryPostLogoutCallback
}
}
Mvc 应用程序(客户端):
private ObjectCache _cache;
private readonly string tokensCacheKey = "Tokens";
public HomeController()
{
_cache = MemoryCache.Default;
}
// GET: Home
public ActionResult Index()
{
var authorizeRequest = new AuthorizeRequest(Constants.BoongalooSTSAuthorizationEndpoint);
var state = HttpContext.Request.Url.OriginalString;
var url = authorizeRequest.CreateAuthorizeUrl(
"tripgalleryauthcode",
"code",
"openid profile address tripgallerymanagement offline_access",
Constants.TripgalleryMvcAuthCodePostLogoutCallback,
state);
HttpContext.Response.Redirect(url);
return null;
}
public async Task<ActionResult> StsCallBackForAuthCodeClient()
{
var authCode = Request.QueryString["code"];
var client = new TokenClient(
Constants.TripgallerySTSTokenEndpoint,
"tripgalleryauthcode",
Constants.TripgalleryClientSecret
);
var tokenResponse = await client.RequestAuthorizationCodeAsync(
authCode,
Constants.TripgalleryMvcAuthCodePostLogoutCallback
);
this._cache[this.tokensCacheKey] = new TokenModel()
{
AccessToken = tokenResponse.AccessToken,
IdToken = tokenResponse.IdentityToken,
RefreshToken = tokenResponse.RefreshToken,
AccessTokenExpiresAt = DateTime.Parse(DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToString(CultureInfo.InvariantCulture))
};
return View();
}
public ActionResult StartCallingWebApi()
{
var timer = new Timer(async (e) =>
{
var cachedStuff = this._cache.Get(this.tokensCacheKey) as TokenModel;
await ExecuteWebApiCall(cachedStuff);
}, null, 0, Convert.ToInt32(TimeSpan.FromMinutes(20).TotalMilliseconds));
return null;
}
private async Task ExecuteWebApiCall(TokenModel cachedStuff)
{
// Ensure that access token expires in more than one minute
if (cachedStuff != null && cachedStuff.AccessTokenExpiresAt > DateTime.Now.AddMinutes(1))
{
await MakeValidApiCall(cachedStuff);
}
else
{
// Use the refresh token to get a new access token, id token and refresh token
var client = new TokenClient(
Constants.TripgallerySTSTokenEndpoint,
"tripgalleryauthcode",
Constants.TripgalleryClientSecret
);
if (cachedStuff != null)
{
var newTokens = await client.RequestRefreshTokenAsync(cachedStuff.RefreshToken);
var value = new TokenModel()
{
AccessToken = newTokens.AccessToken,
IdToken = newTokens.IdentityToken,
RefreshToken = newTokens.RefreshToken,
AccessTokenExpiresAt =
DateTime.Parse(
DateTime.Now.AddSeconds(newTokens.ExpiresIn).ToString(CultureInfo.InvariantCulture))
};
this._cache.Set(this.tokensCacheKey, (object)value, new CacheItemPolicy());
await MakeValidApiCall(value);
}
}
}
问题是,如果我将 STS 托管在 Azure 上,出于某种原因,如果我决定在访问令牌过期后 20 分钟或更长时间内使用刷新令牌,我会收到错误消息。不管我的刷新令牌寿命是 15 天。
也就是STS产生的日志:
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.456 +00:00 [Warning] AuthorizationCodeStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] TokenHandleStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] ConsentStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] RefreshTokenStore not configured - falling back to InMemory
w3wp.exe Information: 0 : 2017-04-06 12:01:22.371 +00:00 [Information] Start token request
w3wp.exe Information: 0 : 2017-04-06 12:01:22.418 +00:00 [Information] Client secret id found: "tripgalleryauthcode"
w3wp.exe Information: 0 : 2017-04-06 12:01:22.418 +00:00 [Information] Client validation success
w3wp.exe Information: 0 : 2017-04-06 12:01:22.418 +00:00 [Information] Start token request validation
w3wp.exe Information: 0 : 2017-04-06 12:01:22.433 +00:00 [Information] Start validation of refresh token request
w3wp.exe Warning: 0 : 2017-04-06 12:01:22.574 +00:00 [Warning] "Refresh token is invalid"
"{
\"ClientId\": \"tripgalleryauthcode\",
\"ClientName\": \"Trip Gallery (Authorization Code)\",
\"GrantType\": \"refresh_token\",
\"RefreshToken\": \"140cfb19405a6a4cbace29646751194a\",
\"Raw\": {
\"grant_type\": \"refresh_token\",
\"refresh_token\": \"140cfb19405a6a4cbace29646751194a\"
}
}"
w3wp.exe Information: 0 : 2017-04-06 12:01:22.590 +00:00 [Information] End token request
w3wp.exe Information: 0 : 2017-04-06 12:01:22.590 +00:00 [Information] Returning error: invalid_grant
w3wp.exe Information: 0 : 2017-04-06 12:01:29.465 +00:00 [Information] Start discovery request
w3wp.exe Information: 0 : 2017-04-06 12:01:29.512 +00:00 [Information] Start key discovery request
与我本地机器上的 STS 运行 相同的情况按预期工作。我可以使用刷新令牌获取新令牌。
已爱: Fred Han - MSFT 确实指出了这个问题。我需要为我的刷新令牌实现持久存储。实现它真的很容易。我是这样做的:
Startup.cs 身份服务器 :
var idServerServiceFactory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
//...
// use custom service for tokens maintainance
var customRefreshTokenStore = new CustomRefreshTokenStore();
idServerServiceFactory.RefreshTokenStore = new Registration<IRefreshTokenStore>(resolver => customRefreshTokenStore);
var options = new IdentityServerOptions
{
Factory = idServerServiceFactory,
// .....
}
idsrvApp.UseIdentityServer(options);
CustomRefreshTokenStore.cs
public class CustomRefreshTokenStore : IRefreshTokenStore
{
public Task StoreAsync(string key, RefreshToken value)
{
// code that uses persitant storage mechanism
}
public Task<RefreshToken> GetAsync(string key)
{
// code that uses persitant storage mechanism
}
public Task RemoveAsync(string key)
{
// code that uses persitant storage mechanism
}
public Task<IEnumerable<ITokenMetadata>> GetAllAsync(string subject)
{
// code that uses persitant storage mechanism
}
public Task RevokeAsync(string subject, string client)
{
// code that uses persitant storage mechanism
}
}
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.456 +00:00 [Warning] AuthorizationCodeStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] TokenHandleStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] ConsentStore not configured - falling back to InMemory
w3wp.exe Warning: 0 : 2017-04-06 12:01:21.512 +00:00 [Warning] RefreshTokenStore not configured - falling back to InMemory
您似乎 store/maintain 数据在内存中,如果您将其托管在 Azure 网站上并在负载均衡器后面使用多实例,这可能是问题的原因。您可以尝试将数据存储在其他数据存储中,而不是内存存储中。
_cache = MemoryCache.Default;
此外,您通过 Web API 应用程序中的内存存储和检索 tokensCacheKey
,这在 Azure 多实例网络场环境中无法正常工作。请将数据存储在外部存储中,例如 Azure 存储、数据库或 Redis 缓存。