我正在使用.NET Core,而我正试图让Web应用程序与Web API进行通信。两者都需要在所有类上使用[Authorize]
属性进行身份验证。为了能够在服务器到服务器之间进行通信,我需要检索验证令牌。由于a Microsoft tutorial,我能够做到这一点。
在本教程中,他们使用对AcquireTokenByAuthorizationCodeAsync
的调用来将令牌保存在缓存中,这样在其他地方,代码就可以执行AcquireTokenSilentAsync
,而不是OpenIdConnectEvents.OnAuthorizationCodeReceived
。需要前往管理局验证用户。
此方法不查找令牌缓存,而是将结果存储在其中,因此可以使用其他方法查找它,例如AcquireTokenSilentAsync
当用户已经登录时会出现问题。存储在CookieAuthenticationEvents.OnValidatePrincipal
的方法永远不会被调用,因为没有收到授权。只有在重新登录时才会调用该方法。
当用户仅通过cookie验证时,还有另一个名为AcquireTokenAsync
的事件。这有效,我可以获得令牌,但我必须使用AcquireTokenSilentAsync
,因为此时我没有授权码。根据文件,它
从当局获取安全令牌。
这使得调用AcquireTokenAsync
失败,因为令牌尚未缓存。而且我并不总是使用AcquireTokenAsync
,因为那总是发给管理局。
如何判断AcquireTokenSilentAsync
获取的令牌是否已被缓存,以便我可以在其他地方使用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,
}
});
?
这一切都来自主Web应用程序项目中的Startup.cs文件。
这是事件处理的完成方式:
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);
}
这些是背后的事件:
return item.indexOf("j") == 0;
可以在链接教程中找到任何其他代码,或者询问,我会将其添加到问题中。
答案 0 :(得分:12)
(注意:几天来我一直在努力解决这个问题。我跟着问题中链接的微软教程一样,跟踪了各种各样的问题,比如一个疯狂的追逐;结果是示例在使用最新版本的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中。
首先,我使用在Azure AD端点的https://apps.dev.microsoft.com和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();
}
在某些时候,如果access_token过期,您可能还需要抓住refresh_token
:
HttpContext.Authentication.GetTokenAsync("refresh_token")
我的OpenIdConnectOptions
实际上还包含了一些我在这里省略的内容,例如:
openIdConnectOptions.Scope.Add("email");
openIdConnectOptions.Scope.Add("Mail.Send");
我已使用这些来处理Microsoft.Graph
API,代表当前登录的用户发送电子邮件。
(Microsoft Graph的那些委派权限也在应用程序上设置)。
到目前为止,这个答案解释了如何使用缓存的访问令牌,但不解释令牌过期时的操作(通常在1小时后)。
选项似乎是:
refresh_token
向Azure AD服务发布请求以获取新的access_token
(无声)。经过多次挖掘,我在这个SO问题中找到了部分答案:
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的混合)
以下是基于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));
}
}
}
AzureAdTokenResponse
使用的AzureAdErrorResponse
和JsonConvert
类:
[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)