【问题标题】:Azure AD ADAL in MVC Application - Token ExpirationMVC 应用程序中的 Azure AD ADAL - 令牌过期
【发布时间】:2017-05-07 00:08:09
【问题描述】:

我创建了一个使用 Azure AD 进行身份验证的 Web 应用程序。 访问令牌在一小时后过期(默认情况下)。 由于我的应用程序主要使用 Telerik Controls,因此它不会对服务器进行任何整页往返。 为了刷新令牌,我实现了 JavaScript 倒计时。 我尝试了不同的方法

  1. 在令牌过期之前插入隐藏的 IFrame 并使用我的应用内的页面刷新 IFrame 的内容
  2. 插入隐藏的 IFrame 并使用 Microsoft 登录链接刷新 IFrame 的内容,如 Microsoft 文章中所述:https://docs.microsoft.com/en-us/azure/active-directory/active-directory-v2-protocols-implicit
  3. 使用 Microsoft 登录链接打开一个临时窗口
  4. 我还在这里查看了 ADAL JavaScript 库:https://github.com/AzureAD/azure-activedirectory-library-for-js/tree/dev 但这意味着我必须重建整个应用程序

所有方法都不起作用。更糟糕的是:如果我在浏览器中按 F5 进行整页刷新,大约在令牌过期前 5 分钟,页面将不再加载,并且浏览器中的“加载”圈会无休止地旋转。

有没有人有一个可行的方法来处理 MVC 中的 ADAL 访问令牌?

这是配置我的身份验证的代码

 public void ConfigureAuth(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions() { CookieSecure = CookieSecureOption.Always });

            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = clientId,
                    Authority = "https://login.microsoftonline.com/common/",

                    TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
                    {
                        // instead of using the default validation (validating against a single issuer value, as we do in line of business apps (single tenant apps)), 
                        // we turn off validation
                        //
                        // NOTE:
                        // * In a multitenant scenario you can never validate against a fixed issuer string, as every tenant will send a different one.
                        // * If you don’t care about validating tenants, as is the case for apps giving access to 1st party resources, you just turn off validation.
                        // * If you do care about validating tenants, think of the case in which your app sells access to premium content and you want to limit access only to the tenant that paid a fee, 
                        //       you still need to turn off the default validation but you do need to add logic that compares the incoming issuer to a list of tenants that paid you, 
                        //       and block access if that’s not the case.
                        // * Refer to the following sample for a custom validation logic: https://github.com/AzureADSamples/WebApp-WebAPI-MultiTenant-OpenIdConnect-DotNet

                        ValidateIssuer = false
                    },

                    Notifications = new OpenIdConnectAuthenticationNotifications()
                    {
                        // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. 
                        AuthorizationCodeReceived = (context) =>
                        {
                            var code = context.Code;

                            ClientCredential credential = new ClientCredential(clientId, appKey);
                            string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
                            string signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

                            AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", "https://login.microsoftonline.com", tenantID), new ADALTokenCache(signInUserId));

                            // Get the access token for AAD Graph. Doing this will also initialize the token cache associated with the authentication context
                            // In theory, you could acquire token for any service your application has access to here so that you can initialize the token cache
                            AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, "https://graph.windows.net");

                            return Task.FromResult(0);
                        },

                        RedirectToIdentityProvider = (context) =>
                        {
                            // This ensures that the address used for sign in and sign out is picked up dynamically from the request
                            // this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings
                            // Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.
                            string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
                            context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
                            context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;

                            return Task.FromResult(0);
                        },

                        AuthenticationFailed = (context) =>
                        {
                            // Suppress the exception if you don't want to see the error
                            context.HandleResponse();
                            return Task.FromResult(0);
                        }
                    }

                });
        }

这是我的 BaseController 中用于在令牌过期之前刷新令牌的代码。我监控 Token Expiration 客户端,在 Token 过期前调用这个方法:

[HttpGet]
public ActionResult RefreshAuthenticationToken()
{
    var refreshToken = DirectoryUserRepository.GetTokenForApplication().Result.RefreshToken;
    var newToken = DirectoryUserRepository.RefreshToken(refreshToken);
    return Json(new { TokenExpirationTimestampUTC = newToken.ExpiresOn.GetUnixTimestamp() }, JsonRequestBehavior.AllowGet);
}

以下是其他缺失的方法

public async Task<AuthenticationToken> GetTokenForApplication()
        {
            string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
            string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
            string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

            // get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
            ClientCredential clientcred = new ClientCredential(clientId, appKey);
            // initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
            AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID, new ADALTokenCache(signedInUserID));
            try
            {

                var authenticationResult = await authenticationContext
                    .AcquireTokenSilentAsync(
                  graphResourceID,
                  new ClientCredential(clientId, appKey),
                  new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
                return new AuthenticationToken(authenticationResult.AccessToken) { ExpiresOn = authenticationResult.ExpiresOn, RefreshToken = authenticationResult.RefreshToken };

                //return authenticationResult.AccessToken;

            }
            catch (AggregateException e)
            {
                foreach (Exception inner in e.InnerExceptions)
                {
                    if (!(inner is AdalException)) continue;
                    if (((AdalException)inner).ErrorCode == AdalError.FailedToAcquireTokenSilently)
                    {
                        authenticationContext.TokenCache.Clear();
                    }
                }
                throw e.InnerException;
            }
            catch (AdalException exception)
            {
                if (exception.ErrorCode == AdalError.FailedToAcquireTokenSilently)
                {
                    authenticationContext.TokenCache.Clear();
                    throw;
                }
                return null;
            }
        }



  public AuthenticationToken RefreshToken(string refreshToken)
        {
            string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
            string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;

            // get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
            ClientCredential clientcred = new ClientCredential(clientId, appKey);
            // initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
            AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID, new ADALTokenCache(signedInUserID));
            try
            {

                var authenticationResult = authenticationContext
                    .AcquireTokenByRefreshToken(
                    refreshToken,
                    clientcred,
                    graphResourceID);
                return new AuthenticationToken(authenticationResult.AccessToken) { ExpiresOn = authenticationResult.ExpiresOn, RefreshToken = authenticationResult.RefreshToken };
            }
            catch (AggregateException e)
            {
                foreach (Exception inner in e.InnerExceptions)
                {
                    if (!(inner is AdalException)) continue;
                    if (((AdalException)inner).ErrorCode == AdalError.FailedToAcquireTokenSilently)
                    {
                        authenticationContext.TokenCache.Clear();
                    }
                }
                throw e.InnerException;
            }
            catch (AdalException exception)
            {
                if (exception.ErrorCode == AdalError.FailedToAcquireTokenSilently)
                {
                    authenticationContext.TokenCache.Clear();
                    throw;
                }
                return null;
            }
        }

【问题讨论】:

    标签: azure authentication model-view-controller access-token adal


    【解决方案1】:

    我们可以从 Web 应用程序中使用两种访问令牌来调用 Web API。

    第一个是使用委托用户身份和 OAuth 2.0 授权代码授予流程。第二个是将应用程序身份与 OAuth 2.0 Client Credentials Grant 流程结合使用。

    当我们使用委托的用户token时,我们可以通过web应用服务器端刷新token,然后token就过期了。

    如果您使用应用程序身份获取令牌,我们可以使用应用程序的凭据再次获取访问令牌。

    由于我的应用程序主要使用 Telerik Controls,它不会对服务器进行任何整页往返。

    你的意思是控件使用AJAX接收数据吗?如果我理解正确,我们可以在 MVC 应用程序中开发一个代理来获取数据。而在代理服务中,我们可以在令牌过期时更新令牌。

    下图是关于 web 应用调用 web API 的流程:

    更新(通过 HTTP 请求刷新访问令牌)

     public static void RefreshToken(string refreshToken)
     {
            HttpClient client = new HttpClient();
            string clientId = "{clientId}";
            string secret = "{secret}";
            string resource = "https://graph.windows.net";
            StringBuilder sb = new StringBuilder();
            sb.Append($"client_id={clientId}");
            sb.Append($"&grant_type=refresh_token");
            sb.Append($"&client_secret={secret}");
            sb.Append($"&resource={resource}");
            sb.Append($"&refresh_token={refreshToken}");
    
            HttpContent bodyContent = new StringContent(sb.ToString(), Encoding.UTF8, "application/x-www-form-urlencoded");
            var tokenResponse = client.PostAsync("https://login.microsoftonline.com/common/oauth2/token", bodyContent).Result;
            var stringResponse = tokenResponse.Content.ReadAsStringAsync().Result;
            JObject jObject = JObject.Parse(stringResponse);
            Console.WriteLine(jObject["access_token"].Value<string>());
     }
    

    【讨论】:

    • 是的,控件使用 AJAX 从控制器获取数据
    • 在这种情况下,您可以构建一个端点来交换 Web 应用程序的令牌。或者您可以创建一个Web API代理,如果令牌过期,您可以自动处理。
    • 我已经编辑了我的初始帖子并现在添加了代码......我执行以下操作,我监控令牌有效性客户端。如果令牌即将过期,我会执行 Ajax 请求以在服务器端刷新令牌。但是即使在令牌刷新之后,客户端也不会获得新的令牌。保存令牌信息的 cookie 不会刷新。即使我在我的页面上放置了一个隐藏的 IFrame 并请求刷新令牌......它仍然会过期。
    • 上面的代码会从缓存中获取令牌,并在令牌过期时尝试刷新令牌。要强制刷新令牌,我们可以直接发出请求。我已经在帖子中附加了代码。
    • '正在使用与整个解决方案中相同的数据(我从 web.config 中读取它们)。在响应中,我收到错误消息:{"error":"invalid_client","error_description":"AADSTS70002:请求正文必须包含以下参数:'client_secret or client_assertion'。\r\n跟踪 ID:5f78ed97-542b- 4f10-a7ea-bc6f0daef1f5\r\n相关 ID: 890f78b9-4992-4218-bc75-da260a6b50c1\r\n时间戳: 2017-01-04 12:55:30Z","error_codes":[70002],"timestamp":" 2017-01-04 12:55:30Z","trace_id":"5f78ed97-542b-4f10-a7ea-bc6f0daef1f5","correlation_id":"890f78b9-4992-4218-bc75-da260a6b50c1"}。我错过了什么
    猜你喜欢
    • 1970-01-01
    • 2018-04-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-04
    • 1970-01-01
    • 2018-12-20
    • 2017-03-25
    • 1970-01-01
    相关资源
    最近更新 更多