如何使用 Active Directory 存储在 AcquireTokenAsync 中收到的令牌
Posted
技术标签:
【中文标题】如何使用 Active Directory 存储在 AcquireTokenAsync 中收到的令牌【英文标题】:How to store the token received in AcquireTokenAsync with Active Directory 【发布时间】:2017-05-22 00:13:01 【问题描述】:问题陈述
我正在使用 .NET Core,并且正在尝试使 Web 应用程序与 Web API 对话。两者都需要在其所有类上使用[Authorize]
属性进行身份验证。为了能够在服务器到服务器之间进行通信,我需要检索验证令牌。感谢a Microsoft tutorial,我能够做到这一点。
问题
在教程中,他们使用了对AcquireTokenByAuthorizationCodeAsync
的调用,以便将令牌保存在缓存中,这样在其他地方,代码只需执行AcquireTokenSilentAsync
,这不需要去权威机构验证用户。
此方法不查找令牌缓存,而是将结果存储在其中,因此可以使用AcquireTokenSilentAsync等其他方法进行查找
当用户已经登录时,问题就出现了。存储在OpenIdConnectEvents.OnAuthorizationCodeReceived
的方法永远不会被调用,因为没有收到授权。只有在有新登录时才会调用该方法。
当仅通过 cookie 验证用户时,还有另一个事件称为:CookieAuthenticationEvents.OnValidatePrincipal
。这行得通,我可以获得令牌,但我必须使用AcquireTokenAsync
,因为那时我没有授权码。根据文档,它
从权威机构获取安全令牌。
这使得调用AcquireTokenSilentAsync
失败,因为令牌没有被缓存。而且我宁愿不要总是使用AcquireTokenAsync
,因为那总是交给权威机构。
问题
如何告诉AcquireTokenAsync
获得的令牌被缓存,以便我可以在其他任何地方使用AcquireTokenSilentAsync
?
相关代码
这一切都来自主 Web 应用程序项目中的 Startup.cs 文件。
这是事件处理的完成方式:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
Events = new CookieAuthenticationEvents()
OnValidatePrincipal = OnValidatePrincipal,
);
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
ClientId = ClientId,
Authority = Authority,
PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"],
ResponseType = OpenIdConnectResponseType.CodeIdToken,
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
GetClaimsFromUserInfoEndpoint = false,
Events = new OpenIdConnectEvents()
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
);
这些是背后的事件:
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
AuthenticationResult authResult = await authContext.AcquireTokenAsync(ClientResourceId, clientCred);
// How to store token in authResult?
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
// Acquire a Token for the Graph API and cache it using ADAL. In the TodoListController, we'll use the cache to acquire a token to the Todo List API
string userObjectId = (context.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.ProtocolMessage.Code, new Uri(context.Properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]), clientCred, GraphResourceId);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption();
// Handle sign-in errors differently than generic errors.
private Task OnAuthenticationFailed(FailureContext context)
context.HandleResponse();
context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
return Task.FromResult(0);
任何其他代码都可以在链接的教程中找到,或者问我会添加到问题中。
【问题讨论】:
您找到解决方案了吗? @MichaelFreidgeim,没有。现在,我只是将令牌存储在 Session 数据中。 @MichaelFreidgeim 我相信我在下面的回答中有一个可行的解决方案 @k25,我使用另一个页面将令牌发送给客户端。它只是一个可以轮询的 JSON 响应,并且具有一定的安全性来确保用户是有效的。另请注意,这些令牌的超时速度比一般前端的要快(据我记得大约一个小时),因此请考虑到您必须弄清楚何时重新查询令牌。我个人的方法是跟踪我得到它的时间,如果是一定时间后再试一次。服务器具有相同的信息,如果超过了分配的时间(双方相同),则重新登录。 @David 感谢您抽出宝贵时间!是的,最后我不得不求助于做同样的事情。我非常希望将其作为 AuthenticationProperties 的一部分发送,但就是不知道如何在客户端获取它。 【参考方案1】:(注意:我已经为这个确切的问题苦苦挣扎了好几天。我遵循了与问题中链接的相同的 Microsoft 教程,并跟踪了各种问题,例如大雁追逐;结果是示例使用最新版本的Microsoft.AspNetCore.Authentication.OpenIdConnect
包时包含一大堆看似不必要的步骤。)。
当我阅读此页面时,我终于有了一个突破性的时刻: http://docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html
解决方案主要涉及让 OpenID Connect 身份验证将各种令牌(access_token
、refresh_token
)放入 cookie。
首先,我使用的是在https://apps.dev.microsoft.com 创建的融合应用程序和 Azure AD 端点的 v2.0。该应用程序有一个应用程序密钥(密码/公钥)并使用Allow Implicit Flow
用于 Web 平台。
(由于某种原因,端点的 v2.0 似乎不适用于仅限 Azure AD 的应用程序。我不知道为什么,我也不确定它是否真的很重要。)
Startup.Configure 方法的相关行:
// Configure the OWIN pipeline to use cookie auth.
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Configure the OWIN pipeline to use OpenID Connect auth.
var openIdConnectOptions = new OpenIdConnectOptions
ClientId = "Your-ClientId",
ClientSecret = "Your-ClientSecret",
Authority = "http://login.microsoftonline.com/Your-TenantId/v2.0",
ResponseType = OpenIdConnectResponseType.CodeIdToken,
TokenValidationParameters = new TokenValidationParameters
NameClaimType = "name",
,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
;
openIdConnectOptions.Scope.Add("offline_access");
app.UseOpenIdConnectAuthentication(openIdConnectOptions);
就是这样!没有OpenIdConnectOptions.Event
回调。没有致电 AcquireTokenAsync
或 AcquireTokenSilentAsync
。没有TokenCache
。这些东西似乎都不是必需的。
魔法似乎是OpenIdConnectOptions.SaveTokens = true
的一部分
这是一个示例,我使用访问令牌代表使用 Office365 帐户的用户发送电子邮件。
我有一个 WebAPI 控制器操作,它使用 HttpContext.Authentication.GetTokenAsync("access_token")
获取他们的访问令牌:
[HttpGet]
public async Task<IActionResult> Get()
var graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(async requestMessage =>
var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
));
var message = new Message
Subject = "Hello",
Body = new ItemBody
Content = "World",
ContentType = BodyType.Text,
,
ToRecipients = new[]
new Recipient
EmailAddress = new EmailAddress
Address = "email@address.com",
Name = "Somebody",
,
;
var request = graphClient.Me.SendMail(message, true);
await request.Request().PostAsync();
return Ok();
旁注#1
在某些时候,您可能还需要获取refresh_token
,以防 access_token 过期:
HttpContext.Authentication.GetTokenAsync("refresh_token")
旁注#2
我的OpenIdConnectOptions
实际上还包括一些我在这里省略的内容,例如:
openIdConnectOptions.Scope.Add("email");
openIdConnectOptions.Scope.Add("Mail.Send");
我已将这些用于使用Microsoft.Graph
API 来代表当前登录的用户发送电子邮件。
(Microsoft Graph 的那些委派权限也在应用程序上设置)。
更新 - 如何“静默”刷新 Azure AD 访问令牌
到目前为止,这个答案解释了如何使用缓存的访问令牌,但没有说明令牌过期时(通常在 1 小时后)要做什么。
选项似乎是:
-
强制用户重新登录。 (不沉默)
使用
refresh_token
向 Azure AD 服务发布请求,以获取新的 access_token
(静默)。
如何使用 Endpoint v2.0 刷新访问令牌
经过更多挖掘,我在这个 SO Question 中找到了部分答案:
How to handle expired access token in asp.net core using refresh token with OpenId Connect
Microsoft OpenIdConnect 库似乎不会为您刷新访问令牌。不幸的是,上述问题的答案缺少关于准确如何刷新令牌的关键细节;大概是因为它依赖于 OpenIdConnect 不关心的有关 Azure AD 的特定细节。
上述问题的公认答案建议直接向 Azure AD 令牌 REST API 发送请求,而不是使用 Azure AD 库之一。
这是相关文档(注意:这涵盖了 v1.0 和 v2.0 的混合)
https://developer.microsoft.com/en-us/graph/docs/concepts/rest https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code#refreshing-the-access-tokens这是一个基于 API 文档的代理:
public class AzureAdRefreshTokenProxy
private const string HostUrl = "https://login.microsoftonline.com/";
private const string TokenUrl = $"Your-Tenant-Id/oauth2/v2.0/token";
private const string ContentType = "application/x-www-form-urlencoded";
// "HttpClient is intended to be instantiated once and re-used throughout the life of an application."
// - MSDN Docs:
// https://msdn.microsoft.com/en-us/library/system.net.http.httpclient(v=vs.110).aspx
private static readonly HttpClient Http = new HttpClient BaseAddress = new Uri(HostUrl);
public async Task<AzureAdTokenResponse> RefreshAccessTokenAsync(string refreshToken)
var body = $"client_id=Your-Client-Id" +
$"&refresh_token=refreshToken" +
"&grant_type=refresh_token" +
$"&client_secret=Your-Client-Secret";
var content = new StringContent(body, Encoding.UTF8, ContentType);
using (var response = await Http.PostAsync(TokenUrl, content))
var responseContent = await response.Content.ReadAsStringAsync();
return response.IsSuccessStatusCode
? JsonConvert.DeserializeObject<AzureAdTokenResponse>(responseContent)
: throw new AzureAdTokenApiException(
JsonConvert.DeserializeObject<AzureAdErrorResponse>(responseContent));
JsonConvert
使用的 AzureAdTokenResponse
和 AzureAdErrorResponse
类:
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class AzureAdTokenResponse
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "token_type", Required = Required.Default)]
public string TokenType get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "expires_in", Required = Required.Default)]
public int ExpiresIn get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "expires_on", Required = Required.Default)]
public string ExpiresOn get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "resource", Required = Required.Default)]
public string Resource get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "access_token", Required = Required.Default)]
public string AccessToken get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "refresh_token", Required = Required.Default)]
public string RefreshToken get; set;
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class AzureAdErrorResponse
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error", Required = Required.Default)]
public string Error get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error_description", Required = Required.Default)]
public string ErrorDescription get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error_codes", Required = Required.Default)]
public int[] ErrorCodes get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "timestamp", Required = Required.Default)]
public string Timestamp get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "trace_id", Required = Required.Default)]
public string TraceId get; set;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "correlation_id", Required = Required.Default)]
public string CorrelationId get; set;
public class AzureAdTokenApiException : Exception
public AzureAdErrorResponse Error get;
public AzureAdTokenApiException(AzureAdErrorResponse error) :
base($"error.Error error.ErrorDescription")
Error = error;
最后,我修改了 Startup.cs 以刷新access_token
(根据我上面链接的答案)
// Configure the OWIN pipeline to use cookie auth.
app.UseCookieAuthentication(new CookieAuthenticationOptions
Events = new CookieAuthenticationEvents
OnValidatePrincipal = OnValidatePrincipal
,
);
Startup.cs 中的 OnValidatePrincipal
处理程序(同样,来自上面的链接答案):
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
if (!DateTime.TryParse(context.Properties.Items[".Token.expires_at"], out var expiresAt))
expiresAt = DateTime.Now;
if (expiresAt < DateTime.Now.AddMinutes(-5))
var refreshToken = context.Properties.Items[".Token.refresh_token"];
var refreshTokenService = new AzureAdRefreshTokenService();
var response = await refreshTokenService.RefreshAccessTokenAsync(refreshToken);
context.Properties.Items[".Token.access_token"] = response.AccessToken;
context.Properties.Items[".Token.refresh_token"] = response.RefreshToken;
context.Properties.Items[".Token.expires_at"] = DateTime.Now.AddSeconds(response.ExpiresIn).ToString(CultureInfo.InvariantCulture);
context.ShouldRenew = true;
最后,一个使用 Azure AD API v2.0 的 OpenIdConnect 解决方案。
有趣的是,v2.0 似乎没有要求将resource
包含在 API 请求中;文档表明这是必要的,但 API 本身只是回复 resource
不受支持。这可能是件好事——大概这意味着访问令牌适用于所有资源(它当然适用于 Microsoft Graph API)
【讨论】:
TokenCache 在某些情况下是必要的。如果有刷新令牌,则用于获取新的访问令牌。 这里的文章不错。 dzimchuk.net/adal-distributed-token-cache-in-asp-net-core 此外,asp.net 核心还允许隐式流(id_token 令牌),因此前端通道是一个选项,但除非您每次都可以启动登录流,否则将需要再次刷新令牌。 是的,直接比较新世界使用范围docs.microsoft.com/en-gb/azure/active-directory/develop/… 天哪,我并不孤单!所有关于这些东西的微软示例都被不必要地混淆了,这使得学习如何做一些简单的事情变得非常痛苦。以上是关于如何使用 Active Directory 存储在 AcquireTokenAsync 中收到的令牌的主要内容,如果未能解决你的问题,请参考以下文章
我们可以使用 Azure Active Directory 提供对 blob/容器/存储帐户的访问吗?
我们是不是必须将密码存储在与 Active Directory 链接的系统中?
你能计算出 Active Directory 使用的密码哈希吗?
学习总结-Active Directory 域服务管理03-导入资源