首页 > 解决方案 > 使用 Azure AD 登录后使用 WebApp 静默获取令牌失败?

问题描述

我将 WebApp 和 WebAPI 注册到同一个 Azure AD 中。

我正在尝试从 WebApp 调用 WebAPI。

我已在 azure AD Applcaition 中将服务 WebAPI 添加到我的 WebApp 中。像下面 - 在此处输入图像描述

当我运行 WebAPI 时,它会在登录成功后给我登录屏幕,我可以访问 WebAPI 方法。这是正常行为。

当我运行 WebApp 时,它会显示相同的登录屏幕,登录成功后我可以看到 WebApp。

现在我想从 WebApp 调用 WebAPI 方法,但我不想要 WebAPI 的登录屏幕,因为当我运行 WebApp 时,我将获得登录屏幕,登录后我希望通过使用相同的用户,我应该能够访问 WebAPI 而无需再次登录,因为我有适用于 WebApp 和 WebAPI 的令牌。

WebAPI 代码 -

启动.Auth.cs

public partial class Startup
    {
        private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
        private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
        private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
        private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];

        public static readonly string Authority = aadInstance + tenantId;

        // This is the resource ID of the AAD Graph API.  We'll need this to request a token to call the Graph API.
        string graphResourceId = "https://graph.windows.net";

        public void ConfigureAuth(IAppBuilder app)
        {
            ApplicationDbContext db = new ApplicationDbContext();

            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = clientId,
                    Authority = Authority,
                    PostLogoutRedirectUri = postLogoutRedirectUri,

                    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 signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
                           AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
                           AuthenticationResult result = authContext.AcquireTokenByAuthorizationCodeAsync(
                               code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId).Result;

                           return Task.FromResult(0);
                       }
                    }
                });
        }

        private static string EnsureTrailingSlash(string value)
        {
            if (value == null)
            {
                value = string.Empty;
            }

            if (!value.EndsWith("/", StringComparison.Ordinal))
            {
                return value + "/";
            }

            return value;
        }
    }

测试控制器.cs

[Authorize]
    public class TestController : ApiController
    {
        [HttpGet]
        [Route("api/getdata")]
        public IEnumerable<string> GetData()
        {
            return new string[] { "value1", "value2" };
        }
    }

网络应用程序代码 -

Startup.Auth.cs

 public partial class Startup
    {
        private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
        private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
        private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
        private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];

        public static readonly string Authority = aadInstance + tenantId;

        // This is the resource ID of the AAD Graph API.  We'll need this to request a token to call the Graph API.
        string graphResourceId = "https://graph.windows.net";

        public void ConfigureAuth(IAppBuilder app)
        {
            ApplicationDbContext db = new ApplicationDbContext();

            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = clientId,
                    Authority = Authority,
                    PostLogoutRedirectUri = postLogoutRedirectUri,

                    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 signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
                           AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
                           AuthenticationResult result = authContext.AcquireTokenByAuthorizationCodeAsync(
                               code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId).Result;

                           return Task.FromResult(0);
                       }
                    }
                });
        }

        private static string EnsureTrailingSlash(string value)
        {
            if (value == null)
            {
                value = string.Empty;
            }

            if (!value.EndsWith("/", StringComparison.Ordinal))
            {
                return value + "/";
            }

            return value;
        }
    }

家庭控制器.cs

[Authorize]
    public class HomeController : Controller
    {
        private static string clientIdWebApp = ConfigurationManager.AppSettings["ida:clientIdWebApp"];
        private static string clientIdWebApi = ConfigurationManager.AppSettings["ida:clientIdWebApi"];
        private static string clientSecretWebApp = ConfigurationManager.AppSettings["ida:clientSecretWebApp"];
        private static string aadInstance = (ConfigurationManager.AppSettings["ida:AADInstance"]);
        private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
        private static string PostLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
        Uri redirectUri = new Uri(PostLogoutRedirectUri);
        public static readonly string Authority = aadInstance + tenantId;

        public ActionResult Index()
        {          
                return View();
        }

        public async System.Threading.Tasks.Task<ActionResult> About()
        {
            ViewBag.Message = "Your application description page.";
            try
            {
                AuthenticationResult result = null;

                string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
                AuthenticationContext authContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(userObjectID));
                ClientCredential credential = new ClientCredential(clientIdWebApp, clientSecretWebApp);
                //AcquireTokenSilentAsync should have to work as i'm accessing WebAPI using same user I logged in to WebApp 
                result = authContext.AcquireTokenSilentAsync(clientIdWebApi,credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId)).Result;
                // gettign exception {"Failed to acquire token silently as no token was found in the cache. Call method AcquireToken"} but I got match id into cache. 
        // and if use AcquireToken instead then it works but api response is login html //page instead of api output 
                HttpClient client = new HttpClient();
                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://MYWEBAPI/api/getdata");
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
                HttpResponseMessage response = await client.SendAsync(request);

                // Return the user's profile in the view.
                if (response.IsSuccessStatusCode)
                {
                    string responseString = await response.Content.ReadAsStringAsync();
                }
            }
            catch (AdalException ex)
            {
            }
            return View();
        }
    }

AdalTokenCache.cs - WebApp 和 WebAPI 相同

 public class ADALTokenCache : TokenCache
    {
        private ApplicationDbContext db = new ApplicationDbContext();
        private string userId;
        private UserTokenCache Cache;

        public ADALTokenCache(string signedInUserId)
        {
            // associate the cache to the current user of the web app
            userId = signedInUserId;
            this.AfterAccess = AfterAccessNotification;
            this.BeforeAccess = BeforeAccessNotification;
            this.BeforeWrite = BeforeWriteNotification;
            // look up the entry in the database
            Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
            // place the entry in memory
            this.Deserialize((Cache == null) ? null : MachineKey.Unprotect(Cache.cacheBits,"ADALCache"));
        }

        // clean up the database
        public override void Clear()
        {
            base.Clear();
            var cacheEntry = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
            db.UserTokenCacheList.Remove(cacheEntry);
            db.SaveChanges();
        }

        // Notification raised before ADAL accesses the cache.
        // This is your chance to update the in-memory copy from the DB, if the in-memory version is stale
        void BeforeAccessNotification(TokenCacheNotificationArgs args)
        {
            if (Cache == null)
            {
                // first time access
                Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
            }
            else
            { 
                // retrieve last write from the DB
                var status = from e in db.UserTokenCacheList
                             where (e.webUserUniqueId == userId)
                select new
                {
                    LastWrite = e.LastWrite
                };

                // if the in-memory copy is older than the persistent copy
                if (status.First().LastWrite > Cache.LastWrite)
                {
                    // read from from storage, update in-memory copy
                    Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
                }
            }
            this.Deserialize((Cache == null) ? null : MachineKey.Unprotect(Cache.cacheBits, "ADALCache"));
        }

        // Notification raised after ADAL accessed the cache.
        // If the HasStateChanged flag is set, ADAL changed the content of the cache
        void AfterAccessNotification(TokenCacheNotificationArgs args)
        {
            // if state changed
            if (this.HasStateChanged)
            {
                if (Cache == null)
                {
                    Cache = new UserTokenCache
                    {
                        webUserUniqueId = userId
                    };
                }

                Cache.cacheBits = MachineKey.Protect(this.Serialize(), "ADALCache");
                Cache.LastWrite = DateTime.Now;

                // update the DB and the lastwrite 
                db.Entry(Cache).State = Cache.UserTokenCacheId == 0 ? EntityState.Added : EntityState.Modified;
                db.SaveChanges();
                this.HasStateChanged = false;
            }
        }

        void BeforeWriteNotification(TokenCacheNotificationArgs args)
        {
            // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
        }

        public override void DeleteItem(TokenCacheItem item)
        {
            base.DeleteItem(item);
        }
    }   

最重要的是,我发现 webapi 也具有AccountController以下相同的登录代码,就像 webapp 一样。在这种情况下应该怎么做?

 public class AccountController : BaseMvcController
    {
        public void SignIn()
        {
            // Send an OpenID Connect sign-in request.
            if (!Request.IsAuthenticated)
            {
                HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" },
                    OpenIdConnectAuthenticationDefaults.AuthenticationType);
            }
        }

        public void SignOut()
        {
            string callbackUrl = Url.Action("SignOutCallback", "Account", routeValues: null, protocol: Request.Url.Scheme);

            HttpContext.GetOwinContext().Authentication.SignOut(
                new AuthenticationProperties { RedirectUri = callbackUrl },
                OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
        }

        public ActionResult SignOutCallback()
        {
            if (Request.IsAuthenticated)
            {
                // Redirect to home page if the user is authenticated.
                return RedirectToAction("Index", "Home");
            }

            return View();
        }
    }

标签: c#.netazureazure-active-directory

解决方案


AcquireTokenSilentAsync仅当您的应用程序至少之前已经为目标资源(在您的情况下为后端 Web API)获取了一个有效令牌并且将该令牌缓存以供后续使用时,该方法才能为您提供帮助。

您可能会收到此错误,因为您甚至没有真正通过 Web API 的身份验证一次(即为 Web API 获取了一个有效的令牌并传递了一次),因此缓存中没有可用的内容。

简而言之,您将无法在第一次使用 AcquireTokenSilentAsync 进行身份验证。

为了进一步理解,请查看您在问题本身中共享的 GitHub 示例。保护后端 Web API

  • 示例代码首先使用授权代码流获取 Web API 的有效令牌。

  • 只有当第一个有效令牌存在时,它才会被缓存,随后的调用可以由authContext.AcquireTokenSilentAsync. 这也作为示例文档的一部分明确说明。

  • 资源ID。在 Azure AD 中注册 Web API 时创建的 Web API 的 App ID URI

  • 令牌缓存。缓存访问令牌的对象。请参阅令牌缓存。

如果 AcquireTokenByAuthorizationCodeAsync 成功,ADAL 会缓存令牌。稍后,您可以通过调用 AcquireTokenSilentAsync 从缓存中获取令牌

示例代码

  1. 首次使用授权码流获取有效令牌

    // OpenID Connect 中间件在获得授权码时发送此事件。

    public override async Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
    {
        string authorizationCode = context.ProtocolMessage.Code;
        string authority = "https://login.microsoftonline.com/" + tenantID
        string resourceID = "https://tailspin.onmicrosoft.com/surveys.webapi" // App ID URI
        ClientCredential credential = new ClientCredential(clientId, clientSecret);
    
        AuthenticationContext authContext = new AuthenticationContext(authority, tokenCache);
        AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
        authorizationCode, new Uri(redirectUri), credential, resourceID);
    
        // If successful, the token is in authResult.AccessToken
    }
    
  2. 稍后,您可以通过调用 AcquireTokenSilentAsync 从缓存中获取令牌:

    AuthenticationContext authContext = new AuthenticationContext(authority, tokenCache);
    var result = await authContext.AcquireTokenSilentAsync(resourceID, credential, new UserIdentifier(userId, UserIdentifierType.UniqueId));
    

推荐阅读