如何将AcquireTokenAsync中收到的令牌与Active Directory一起存储

Dav*_*vid 9 c# active-directory asp.net-web-api .net-core

问题陈述

我正在使用.NET Core,我正在尝试将Web应用程序与Web API进行对话.两者都要求使用[Authorize]所有类的属性进行身份验证.为了能够在服务器到服务器之间进行通信,我需要检索验证令牌.由于Microsoft教程,我已经能够做到这一点.

问题

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

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

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

还有另一个事件叫做:CookieAuthenticationEvents.OnValidatePrincipal当用户仅通过cookie验证时.这是有效的,我可以获得令牌,但我必须使用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,
    }
});
Run Code Online (Sandbox Code Playgroud)

这些是背后的事件:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

Ben*_*ell 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中.

首先,我使用的是在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);
Run Code Online (Sandbox Code Playgroud)

就是这样!没有OpenIdConnectOptions.Event回调.没有电话 AcquireTokenAsyncAcquireTokenSilentAsync.不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();
    }
Run Code Online (Sandbox Code Playgroud)

附注#1

在某些时候refresh_token,如果access_token过期,您可能还需要抓住它:

HttpContext.Authentication.GetTokenAsync("refresh_token")
Run Code Online (Sandbox Code Playgroud)

附注#2

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

    openIdConnectOptions.Scope.Add("email");
    openIdConnectOptions.Scope.Add("Mail.Send");
Run Code Online (Sandbox Code Playgroud)

我已经使用它们来处理Microsoft.GraphAPI,代表当前登录的用户发送电子邮件.

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


更新 - 如何"静默"刷新Azure AD访问令牌

到目前为止,这个答案解释了如何使用缓存的访问令牌,但不解释令牌过期时的操作(通常在1小时后).

选项似乎是:

  1. 强制用户再次登录.(不沉默)
  2. 使用refresh_token获取新的access_token(静默)向Azure AD服务发出请求.

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

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

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

看起来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));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用的AzureAdTokenResponseAzureAdErrorResponseJsonConvert:

[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;
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,我对Startup.cs进行了修改以刷新access_token (基于我上面链接的答案)

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

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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

最后,OpenIdConnect使用Azure AD API v2.0的解决方案.

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

  • 天啊,我并不孤单!所有关于这些东西的微软样本都被不必要地混淆了,这使得学习如何做一些简单难以置信的事情. (3认同)