本文介绍了如何使用 Active Directory 存储在 AcquireTokenAsync 中收到的令牌的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用 .NET Core,并且正在尝试使 Web 应用程序与 Web API 通信.两者都需要在其所有类上使用 [Authorize] 属性进行身份验证.为了能够在它们之间进行服务器到服务器通信,我需要检索验证令牌.感谢 Microsoft 教程.

I am using .NET Core, and I'm trying to make a web application talk to a web API. Both require authentication using the [Authorize] attribute on all of their classes. In order to be able to talk between them server-to-server, I need to retrieve the validation token. I've been able to do that thanks to a Microsoft tutorial.

在教程中,他们使用对AcquireTokenByAuthorizationCodeAsync 的调用来将令牌保存在缓存中,以便在其他地方,代码只需执行一个AcquireTokenSilentAsync,这不需要去管理局验证用户.

In the tutorial, they use a call to AcquireTokenByAuthorizationCodeAsync in order to save the token in the cache, so that in other places, the code can just do a AcquireTokenSilentAsync, which doesn't require going to the Authority to validate the user.

此方法不查找令牌缓存,而是将结果存储在其中,因此可以使用其他方法(例如 AcquireTokenSilentAsync)进行查找

当用户已经登录时就会出现问题.存储在 OpenIdConnectEvents.OnAuthorizationCodeReceived 中的方法永远不会被调用,因为没有收到授权.只有在有新登录时才会调用该方法.

The issue comes in when the user is already logged in. The method stored at OpenIdConnectEvents.OnAuthorizationCodeReceived never gets called, since there is no authorization being received. That method only gets called when there's a fresh login.

当用户仅通过 cookie 进行验证时,还有另一个事件称为:CookieAuthenticationEvents.OnValidatePrincipal.这有效,我可以获得令牌,但我必须使用 AcquireTokenAsync,因为那时我没有授权代码.根据文档,它

There is another event called: CookieAuthenticationEvents.OnValidatePrincipal when the user is only being validated via a cookie. This works, and I can get the token, but I have to use AcquireTokenAsync, since I don't have the authorization code at that point. According to the documentation, it

从权威机构获取安全令牌.

这使得调用 AcquireTokenSilentAsync 失败,因为令牌没有被缓存.而且我宁愿不总是使用 AcquireTokenAsync,因为它总是交给管理局.

This makes calling AcquireTokenSilentAsync fail, since the token has not been cached. And I'd rather not always use AcquireTokenAsync, since that always goes to the Authority.

如何告诉 AcquireTokenAsync 获取的令牌被缓存,以便我可以在其他任何地方使用 AcquireTokenSilentAsync?

How can I tell the token gotten by AcquireTokenAsync to be cached so that I can use AcquireTokenSilentAsync everywhere else?

这一切都来自主要的 Web 应用程序项目中的 Startup.cs 文件.

This all comes from the Startup.cs file in the main, Web Application project.

事件处理是这样完成的:

This is how the event handling is done:

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);
}

任何其他代码都可以在链接的教程中找到,或者提问,我会将其添加到问题中.

Any other code can be found in the linked tutorial, or ask and I will add it to the question.

推荐答案

(注意:我已经为这个确切的问题苦苦挣扎了好几天.我遵循了与问题中链接的相同的 Microsoft 教程,并且跟踪了各种问题,如野鹅追逐;事实证明,在使用最新版本的 Microsoft.AspNetCore.Authentication.OpenIdConnect 包时,该示例包含一大堆看似不必要的步骤.).

(Note: I had been struggling with this exact issue for several days. I followed the same Microsoft Tutorial as the one linked in the question, and tracked various problems like a wild goose chase; it turns out the sample contains a whole bunch of seemingly unnecessary steps when using the latest version of the Microsoft.AspNetCore.Authentication.OpenIdConnect package.).

当我读到这个页面时,我终于有了一个突破性的时刻:http://docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html

I eventually had a breakthrough moment when I read this page: http://docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html

解决方案主要是让 OpenID Connect auth 将各种令牌(access_tokenrefresh_token)放入 cookie.

The solution essentially involves letting OpenID Connect auth put the various tokens (access_token, refresh_token) into the cookie.

首先,我使用在 融合应用>https://apps.dev.microsoft.com 和 Azure AD 端点的 v2.0.该应用程序具有应用程序密钥(密码/公钥)并使用 Allow Implicit Flow 用于 Web 平台.

Firstly, I'm using a Converged Application created at https://apps.dev.microsoft.com and v2.0 of the Azure AD endpoint. The App has an Application Secret (password/public key) and uses Allow Implicit Flow for a Web platform.

(出于某种原因,端点的 v2.0 似乎不适用于仅限 Azure AD 的应用程序.我不确定为什么,也不确定它是否真的很重要.)

(For some reason it seems as if v2.0 of the endpoint doesn't work with Azure AD only applications. I'm not sure why, and I'm not sure if it really matters anyway.)

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 回调.不调用 AcquireTokenAsyncAcquireTokenSilentAsync.没有 TokenCache.这些事情似乎都没有必要.

And that's it! No OpenIdConnectOptions.Event callbacks. No calls to AcquireTokenAsync or AcquireTokenSilentAsync. No TokenCache. None of those things seem to be necessary.

魔法似乎是 OpenIdConnectOptions.SaveTokens = true

以下是我使用访问令牌代表使用 Office365 帐户的用户发送电子邮件的示例.

Here's an example where I'm using the access token to send an e-mail on behalf of the user using their Office365 account.

我有一个 WebAPI 控制器操作,它使用 HttpContext.Authentication.GetTokenAsync("access_token") 获取访问令牌:

I have a WebAPI controller action which obtains their access token using 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 过期:


Side Note #1

At some point you might also need to get hold of the refresh_token too, in case the access_token expires:

HttpContext.Authentication.GetTokenAsync("refresh_token")

旁注 #2

我的 OpenIdConnectOptions 实际上还包括一些我在这里省略的东西,例如:


Side Note #2

My OpenIdConnectOptions actually includes a few more things which I've omitted here, for example:

    openIdConnectOptions.Scope.Add("email");
    openIdConnectOptions.Scope.Add("Mail.Send");

我已经使用这些来与 Microsoft.Graph API 一起工作,以代表当前登录的用户发送电子邮件.

I've used these for working with the Microsoft.Graph API to send an e-mail on behalf of the currently logged in user.

(Microsoft Graph 的那些委派权限也在应用上设置).

(Those delegated permissions for Microsoft Graph are set up on the app too).

到目前为止,这个答案解释了如何使用缓存的访问令牌,但没有解释当令牌过期(通常在 1 小时后)时该怎么做.

So far, this answer explains how to use the cached access token but not what to do when the token expires (typically after 1 hour).

选项似乎是:

  1. 强制用户重新登录.(不沉默)
  2. 使用 refresh_token 向 Azure AD 服务发布请求以获取新的 access_token(静默).
  1. Force the user to sign in again. (Not silent)
  2. POST a request to the Azure AD service using the refresh_token to obtain a new access_token (silent).

如何使用端点 v2.0 刷新访问令牌

经过更多的挖掘,我在这个 SO 问题中找到了部分答案:

How to Refresh the Access Token using v2.0 of the Endpoint

After more digging, I found part of the answer in this SO Question:

如何在 asp.net core 中使用刷新令牌和 OpenId Connect 处理过期的访问令牌

似乎 Microsoft OpenIdConnect 库不会为您刷新访问令牌.不幸的是,上述问题的答案缺少关于精确如何刷新令牌的关键细节;大概是因为它取决于 OpenIdConnect 不关心的 Azure AD 的具体细节.

It seems like the Microsoft OpenIdConnect libraries do not refresh the access token for you. Unfortunately the answer in the question above is missing the crucial detail about precisely how to refresh the token; presumably because it depends on specific details about Azure AD which OpenIdConnect doesn't care about.

上述问题的公认答案建议直接向 Azure AD 令牌 REST API 发送请求,而不是使用 Azure AD 库之一.

The accepted answer to the above question suggests sending a request directly to the Azure AD Token REST API instead of using one of the Azure AD libraries.

这是相关文档(注意:这涵盖了 v1.0 和 v2.0 的组合)

Here's the relevant documentation (Note: this covers a mix of v1.0 and v2.0)

这是基于 API 文档的代理:

Here's a proxy based on the API docs:

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 使用的 AzureAdTokenResponseAzureAdErrorResponse 类:

The AzureAdTokenResponse and AzureAdErrorResponse classes used by 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(基于我上面链接的答案)

Finally, my modifications to Startup.cs to refresh the access_token(Based on the answer I linked above)

        // Configure the OWIN pipeline to use cookie auth.
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = OnValidatePrincipal
            },
        });

Startup.cs 中的 OnValidatePrincipal 处理程序(同样,来自上面链接的答案):

The OnValidatePrincipal handler in Startup.cs (Again, from the linked answer above):

    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 解决方案.


Finally, a solution with OpenIdConnect using v2.0 of the Azure AD API.

有趣的是,v2.0 似乎并没有要求在 API 请求中包含 resource;文档表明这是必要的,但 API 本身只是回答不支持 resource.这可能是一件好事 - 大概这意味着访问令牌适用于所有资源(它当然适用于 Microsoft Graph API)

Interestingly, it seems that v2.0 does not ask for a resource to be included in the API request; the documentation suggests it's necessary, but the API itself simply replies that resource is not supported. This is probably a good thing - presumably it means that the access token works for all resources (it certainly works with the Microsoft Graph API)

这篇关于如何使用 Active Directory 存储在 AcquireTokenAsync 中收到的令牌的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

10-25 01:19