Warning: file_get_contents(/data/phpspider/zhask/data//catemap/0/assembly/6.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 如何使用Active Directory存储AcquireTokenAsync中接收的令牌 问题陈述_C#_Asp.net Web Api_Active Directory_.net Core - Fatal编程技术网

C# 如何使用Active Directory存储AcquireTokenAsync中接收的令牌 问题陈述

C# 如何使用Active Directory存储AcquireTokenAsync中接收的令牌 问题陈述,c#,asp.net-web-api,active-directory,.net-core,C#,Asp.net Web Api,Active Directory,.net Core,我使用的是.NETCore,我试图让一个web应用程序与一个web API对话。两者都需要在其所有类上使用[Authorize]属性进行身份验证。为了能够在它们之间进行服务器到服务器的通信,我需要检索验证令牌。多亏了你,我才能做到这一点 问题 在本教程中,他们使用对AcquireTokenByAuthorizationCodeAsync的调用将令牌保存在缓存中,以便在其他地方,代码只需执行AcquireTokenSilentAsync,而无需到授权机构验证用户 此方法不查找令牌缓存,但将结果存储

我使用的是.NETCore,我试图让一个web应用程序与一个web API对话。两者都需要在其所有类上使用
[Authorize]
属性进行身份验证。为了能够在它们之间进行服务器到服务器的通信,我需要检索验证令牌。多亏了你,我才能做到这一点

问题 在本教程中,他们使用对
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);
}
任何其他代码都可以在链接教程中找到,或者询问,我会将其添加到问题中。

(注意:我已经为这个问题挣扎了好几天。我遵循了与问题中链接的相同的Microsoft教程,并像白鹅追逐一样跟踪各种问题;结果表明,在使用最新版本的
Microsoft.AspNetCore.Authentic时,示例包含了一大堆看似不必要的步骤。)OpenIdConnect
package。)

当我读到这一页时,我终于有了一个突破性的时刻:

该解决方案本质上涉及让OpenID Connect auth将各种令牌(
access\u token
refresh\u token
)放入cookie

首先,我使用的是在Azure AD endpoint和v2.0上创建的聚合应用程序。该应用程序具有应用程序机密(密码/公钥),并使用
允许隐式流
作为Web平台

(出于某种原因,端点的v2.0似乎不适用于Azure纯广告应用程序。我不确定原因,也不确定这是否真的很重要。)

启动的相关行。配置方法:

    // 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
callbacks。没有调用
AcquireTokenAsync
AcquireTokenSilentAsync
。没有
TokenCache
。这些似乎都不是必需的

这种神奇似乎是OpenIdConnectOptions.SaveTokens=true的一部分

下面是一个示例,我使用访问令牌代表使用其Office365帐户的用户发送电子邮件

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


旁注#2 我的
OpenIdConnectOptions
实际上还包括一些我在这里省略的内容,例如:

    openIdConnectOptions.Scope.Add("email");
    openIdConnectOptions.Scope.Add("Mail.Send");
我使用这些函数与Microsoft.Graph
API一起工作,代表当前登录的用户发送电子邮件

(Microsoft Graph的授权权限也在应用程序上设置)


更新-如何“静默”刷新Azure AD访问令牌 到目前为止,这个答案解释了如何使用缓存的访问令牌,而不是在令牌过期时(通常在1小时后)应该做什么

选择似乎是:

  • 强制用户重新登录。(非静默)
  • 使用
    刷新\u令牌
    向Azure广告服务发布请求,以获取新的
    访问\u令牌
    (无提示)
  • 如何使用端点的v2.0刷新访问令牌 经过进一步挖掘,我在这个问题中找到了部分答案:

    Microsoft OpenIdConnect库似乎没有为您刷新访问令牌。不幸的是,上述问题中的答案缺少有关如何刷新令牌的关键细节;可能是因为它取决于OpenIdConnect不关心的Azure AD的特定细节

    上述问题的公认答案建议直接向Azure AD Token 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));
            }
        }
    }
    
    最后,我修改了Startup.cs以刷新
    access\u令牌
    (基于我上面链接的答案)

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

    ValidatePrincipal上的私有异步任务(CookieValidatePrincipalContext) { if(context.Properties.Items.ContainsKey(“.Token.expires\u at”)) { 如果(!DateTime.TryParse)(续
        openIdConnectOptions.Scope.Add("email");
        openIdConnectOptions.Scope.Add("Mail.Send");
    
    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));
            }
        }
    }
    
    [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;
        }
    }
    
            // Configure the OWIN pipeline to use cookie auth.
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                Events = new CookieAuthenticationEvents
                {
                    OnValidatePrincipal = 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;
                }
            }
        }