如何将AcquireTokenAsync中收到的令牌与Active Directory一起存储
问题陈述
我正在使用.NET Core,我正在尝试将Web应用程序与Web API进行对话。 两者都要求使用所有类的[Authorize]
属性进行身份validation。 为了能够在服务器到服务器之间进行通信,我需要检索validation令牌。 由于Microsoft教程 ,我已经能够做到这一点。
问题
在本教程中,他们使用对AcquireTokenByAuthorizationCodeAsync
的调用来将令牌保存在缓存中,以便在其他地方,代码可以只执行AcquireTokenSilentAsync
,这不需要去管理局validation用户。
此方法不查找令牌缓存,而是将结果存储在其中,因此可以使用其他方法(如AcquireTokenSilentAsync)查找它
当用户已经登录时会出现问题。由于没有收到授权, OpenIdConnectEvents.OnAuthorizationCodeReceived
永远不会调用存储在OpenIdConnectEvents.OnAuthorizationCodeReceived
的方法。 只有在重新登录时才会调用该方法。
当用户仅通过cookievalidation时,还有另一个事件叫做: CookieAuthenticationEvents.OnValidatePrincipal
。 这是有效的,我可以获得令牌,但我必须使用AcquireTokenAsync
,因为那时我没有授权码。 根据文件,它
从权限获取安全令牌。
这使得调用AcquireTokenSilentAsync
失败,因为令牌尚未缓存。 而且我宁愿不总是使用AcquireTokenAsync
,因为这总是发给管理局。
题
如何判断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); }
可以在链接的教程中找到任何其他代码,或者询问,我会将其添加到问题中。
(注意:我几天来一直在努力解决这个问题。我跟着问题中链接的微软教程一样,跟踪了各种问题,比如一个疯狂的追逐;事实certificate这个样本包含了一大堆看似使用最新版本的Microsoft.AspNetCore.Authentication.OpenIdConnect
包时不必要的步骤。) 。
当我读到这个页面时,我终于有了一个突破性的时刻: http : //docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html
该解决方案主要涉及让OpenID Connect auth将各种令牌( access_token
, refresh_token
)放入cookie中。
首先,我使用的是在https://apps.dev.microsoft.com创建的融合应用程序和Azure AD端点的v2.0。 该应用程序具有应用程序密钥(密码/公钥),并为Web平台使用Allow Implicit Flow
。
(由于某种原因,似乎端点的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 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
在某些时候,如果access_token过期,您可能还需要获取refresh_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
(静默)。
如何使用端点的v2.0刷新访问令牌
经过更多的挖掘,我在这个SO问题中找到了部分答案:
如何使用OpenId Connect的刷新令牌在asp.net核心中处理过期的访问令牌
看起来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 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 (responseContent) : throw new AzureAdTokenApiException( JsonConvert.DeserializeObject(responseContent)); } } }
AzureAdTokenResponse
使用的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; } } }
最后,OpenIdConnect使用Azure AD API v2.0的解决方案。
有趣的是,似乎v2.0并未要求将resource
包含在API请求中; 文档表明这是必要的,但API本身只是回复说不支持该resource
。 这可能是一件好事 - 可能这意味着访问令牌适用于所有资源(它肯定适用于Microsoft Graph API)