【问题标题】:ASP.Net MVC C#: Failed to acquire token silently as no token was found in the cache. Call method AcquireTokenASP.Net MVC C#:无法静默获取令牌,因为在缓存中找不到令牌。调用方法 AcquireToken
【发布时间】:2019-08-21 15:01:26
【问题描述】:

我正在尝试通过 Microsoft Graph API 检索 Azure AD 用户配置文件数据。我使用来自各种来源(主要是 Microsoft)的代码示例设置了一个小型 Visual Studio MVC 应用程序。在我的无知中,我认为这将是一个相当简单的过程。

我在 SO 上浏览过其他类似的案例,并试图利用其他人的建议,但无济于事。我已经解决了这个问题四天,非常感谢任何帮助。

//  UserProfileController.cs
-- contains the calling method: var graphToken = await AuthenticationHelper.GetGraphAccessToken();
//  
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using SSO_Test.Models;
using SSO_Test.Utils;
using System.Net.Http.Headers;

namespace SSO_Test.Controllers
{
    [Authorize]
    public class UserProfileController : Controller
    {
        private ApplicationDbContext db = new ApplicationDbContext();
        private string clientId = ConfigurationManager.AppSettings["ClientId"];
        private string appKey = ConfigurationManager.AppSettings["ClientSecret"];
        private string aadInstance = ConfigurationManager.AppSettings["AADInstance"];
        private string graphResourceID = "https://graph.microsoft.com";

        // GET: UserProfile
        public async Task<ActionResult> Index()
        {
            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;
            try
            {
                var graphToken = await AuthenticationHelper.GetGraphAccessToken();
                var authenticationProvider = new DelegateAuthenticationProvider(
                (requestMessage) =>
                {
                    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", graphToken);
                    return Task.FromResult(0);
                });
                var graphClient = new GraphServiceClient(authenticationProvider);
                var user = await graphClient.Me.Request().GetAsync();
                return View(user);
            }
            catch (AdalException ex)
            {
                // Return to error page.
                ViewBag.Message = ex.Message;
                return View("Error");
            }
            // if the above failed, the user needs to explicitly re-authenticate for the app to obtain the required token
            catch (Exception)
            {
                return View("Relogin");
            }
        }

        public void RefreshSession()
        {
            HttpContext.GetOwinContext().Authentication.Challenge(
                new AuthenticationProperties { RedirectUri = "/Home" },
                OpenIdConnectAuthenticationDefaults.AuthenticationType);
        }
    }
}
//AuthenticationHelper.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using SSO_Test.Models;

namespace SSO_Test.Utils
{
    public static class AuthenticationHelper
    {
        public static async Task<string> GetGraphAccessToken()
        {
            var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
            var userObjectId = ClaimsPrincipal.Current.FindFirst(SettingsHelper.ClaimTypeObjectIdentifier).Value;
            var clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret);
            var userIdentifier = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId);
            // create auth context
            AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.AzureAdAuthority, new ADALTokenCache(signInUserId));
            //added check point for verification purposes
            System.Diagnostics.Debug.WriteLine("Check point #1");
            //GOOD TO THIS POINT
            var result = await authContext.AcquireTokenSilentAsync(SettingsHelper.AzureAdGraphResourceURL, clientCredential, userIdentifier);
            //ERROR MESSAGE:  "Failed to acquire token silently as no token was found in the cache. Call method AcquireToken"
            System.Diagnostics.Debug.WriteLine("Check point #2");
            //app never reaches the second check point

            return result.AccessToken;
        }
    }

}
//ADALTokenCache.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Security;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace SSO_Test.Models
{
    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.BeforeAccess = BeforeAccessNotification;
            this.AfterAccess = AfterAccessNotification;
            this.BeforeWrite = BeforeWriteNotification;
            // look up the entry in the database
            Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
            // place the entry in memory
            this.DeserializeAdalV3((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.DeserializeAdalV3((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)
            {
                Cache = new UserTokenCache
                {
                    webUserUniqueId = userId,
                    //cacheBits = MachineKey.Protect(this.Serialize(), "ADALCache"),
                    cacheBits = MachineKey.Protect(this.SerializeAdalV3(), "ADALCache"),
                    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);
        }
    }
}
//ApplicationDbContext.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace SSO_Test.Models
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
        }

        public DbSet<UserTokenCache> UserTokenCacheList { get; set; }
    }

    public class UserTokenCache
    {
        [Key]
        public int UserTokenCacheId { get; set; }
        public string webUserUniqueId { get; set; }
        public byte[] cacheBits { get; set; }
        public DateTime LastWrite { get; set; }
    }
}

如您所见,我在 GetGraphAccessToken() 方法中记录了错误消息:

“由于在缓存中未找到令牌,因此无法静默获取令牌。 调用方法 AcquireToken"。

我能够将 AcquireTokenSilentAsync() 方法隔离为罪魁祸首,方法是用一对 Debug.Writeline 语句括起来,第一个语句运行成功,第二个语句没有运行。这已通过查看 VS 输出窗口的内容得到验证,如下所示:

检查点 #1

抛出异常: 'Microsoft.IdentityModel.Clients.ActiveDirectory.AdalSilentTokenAcquisitionException' 在 mscorlib.dll 中

程序“[13980] iisexpress.exe”已退出,代码为 -1 (0xffffffff)。

我真的希望这个东西能够工作,我更喜欢使用 Graph SDK 方法而不是使用 REST API。

再一次,我已经用头撞墙了四天多。我的头还好,但墙的形状不好。

提前致谢。

【问题讨论】:

  • 我注意到您正在使用 ADAL。您会考虑使用 MSAL 吗?如果可以,我建议您查看this MSAL sample
  • 我下载并运行了示例,没有任何问题。问题是,我还没有为 ASP.Net Core 做好准备。但是,我能够从示例中收集到足够的信息来改进我的 .Net 4.6 应用程序以利用基于 MSAL 的缓存。

标签: c# azure-active-directory microsoft-graph-api azure-ad-b2c microsoft-graph-sdks


【解决方案1】:

如果 AcquireTokenSilent 失败,则表示缓存中没有令牌,因此您必须通过 AcquireToken 去获取一个,如 this

您已将问题标记为“B2C”,但看起来您正在使用 Azure AD?

【讨论】:

    【解决方案2】:

    现在有一套完整的身份验证提供程序可用于标准的 OAuth 流程集,因此您不必再使用 DelegatedAuthenticationProvider。 https://github.com/microsoftgraph/msgraph-sdk-dotnet-auth这里有关于如何根据场景选择正确的身份验证提供程序的文档https://docs.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS

    【讨论】:

    • 感谢您的反馈。我已从 ADAL 迁移到 MSAL 缓存,这为我解决了这个问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-23
    • 2015-07-10
    • 1970-01-01
    相关资源
    最近更新 更多