c# - 使用 Azure AD 登录后无法使用 WebApp 静默获取 token ?

标签 c# .net azure azure-active-directory

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

我正在尝试从 WebApp 调用 WebAPI。

我已将服务 WebAPI 添加到我的 Azure AD Applcaition 中的 WebApp 中。像下面这样- enter image description here

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

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

现在我想从 WebApp 调用 WebAPI 方法,但我不需要 WebAPI 的登录屏幕,因为当我运行 WebApp 时,我将获得登录屏幕 登录后,我希望通过使用相同的用户,我应该能够访问 WebAPI,而无需再次执行登录操作,因为我拥有适用于两者的 token 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;
        }
    }

TestController.cs

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

Web 应用程序代码 -

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

HomeController.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();
        }
    }

最佳答案

AcquireTokenSilentAsync 方法可以为您提供帮助,前提是您的应用程序之前已获取目标资源(在您的情况下是后端 Web API)的有效 token 至少一次,并且已缓存该 token 以供后续使用使用。

您可能会收到此错误,因为您甚至没有真正向 Web API 进行过身份验证(即获取了 Web API 的有效 token 并传递了一次),因此缓存中没有任何内容可供使用。

简单地说,您将无法使用 AcquireTokenSilentAsync 进行首次身份验证。

为了进一步理解,请查看您在问题本身中共享的 GitHub 示例。 Secure a backend web API

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

  • 仅当第一个有效 token 存在时,它才会被缓存,并且后续调用可以通过 authContext.AcquireTokenSilentAsync 来处理。这也作为示例文档的一部分明确说明。

  • resourceID. The App ID URI of the web API, which you created when you registered the web API in Azure AD

  • tokenCache. An object that caches the access tokens. See Token caching.

If AcquireTokenByAuthorizationCodeAsync succeeds, ADAL caches the token. Later, you can get the token from the cache by calling AcquireTokenSilentAsync

示例代码

  1. 首次使用授权代码流程获取有效 token

    //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 从缓存中获取 token :

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

关于c# - 使用 Azure AD 登录后无法使用 WebApp 静默获取 token ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52745656/

相关文章:

c# - 如何将此字符串转换为整数数组?

c# - 使用 LINQ 合并列中的非分组元素

c# - 以编程方式更改 IE 代理后如何单击“确定”按钮

c# - 根据字符串选择文件

c# - 在 if 语句中比较字符串

.net - (DevForce、OpenAccess、LLBLGen)与 Entity Framework 4 和 CTP4

c# - YouTube Api Xamarin Android 应用程序 PCL HttpClient 问题

azure - 如何使用同一根模块中的输出值来创建另一个资源

mysql - 有关 mysqldump 和 Microsoft Azure 的问题

azure - 从 Azure 函数/逻辑应用访问本地 Active Directory