2

I have an ASP.NET application that is registered on Azure. It is uses Azure Active directory to authenticate users to the application. There is a user that his last name had to change for some reason(John Smith -> John Dough), his account is also connected to an E1 licence and therefore his email address has changed (smithj@ddd.com -> smithd@ddd.com).

This user tried to authenticate to my application and it failed with a message that says:

multiple_matching_tokens_detected: The cache contains multiple tokens satisfying the requirements. Call AcquireToken again providing more requirements (e.g. UserId)

You should know that the application is hosted locally on one of the Azure virtual machines but it is registered on Azure so users can login with the same credentials

My stack trace is like the following:

[AdalException: multiple_matching_tokens_detected: The cache contains multiple tokens satisfying the requirements. Call AcquireToken again providing more requirements (e.g. UserId)]
   Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache.LoadSingleItemFromCache(String authority, String resource, String clientId, TokenSubjectType subjectType, String uniqueId, String displayableId, CallState callState) +724
   Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache.LoadFromCache(String authority, String resource, String clientId, TokenSubjectType subjectType, String uniqueId, String displayableId, CallState callState) +110
   Microsoft.IdentityModel.Clients.ActiveDirectory.<RunAsync>d__0.MoveNext() +1796
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62
   Microsoft.IdentityModel.Clients.ActiveDirectory.<AcquireTokenSilentCommonAsync>d__10.MoveNext() +317
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62
   Microsoft.IdentityModel.Clients.ActiveDirectory.<AcquireTokenSilentAsync>d__5c.MoveNext() +268
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62
   MRTWebApplication.<GetTokenForApplication>d__6.MoveNext() +559
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62
   MRTWebApplication.<<GetUserData>b__5_0>d.MoveNext() +194
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62
   Microsoft.Azure.ActiveDirectory.GraphClient.Extensions.<SetToken>d__1.MoveNext() +207
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62
   Microsoft.Azure.ActiveDirectory.GraphClient.Extensions.<ExecuteAsync>d__4d`2.MoveNext() +993
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62
   Microsoft.Azure.ActiveDirectory.GraphClient.Extensions.<<ExecuteAsync>b__0>d__2.MoveNext() +263

[AggregateException: One or more errors occurred.]
   System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification) +4719080
   MRTWebApplication._Default.GetUserData() +773
   MRTWebApplication._Default.Page_Load(Object sender, EventArgs e) +43
   System.Web.UI.Control.OnLoad(EventArgs e) +103
   System.Web.UI.Control.LoadRecursive() +68
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3811 

I was looking into way to clear this cache so this user can re-authenticate again and get a token. I couldn't find a way! I also looked into changing permissions of the registered app and re-grant them again to users but that did not work. My code is like the following:

private static string clientId = ConfigurationManager.AppSettings["ida:ClientID"];
private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string graphResourceId = "https://graph.windows.net";

protected void Page_Load(object sender, EventArgs e)
{
      if (Request.IsAuthenticated)
      {
          IList<string> groups = GetUserData();
      }
}

Notice that I am calling the function 'GetUserData()' that actually brings all the groups that a user belongs to. The code of this function is like the following:

  public IList<string> GetUserData()
        {
            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;

            Uri servicePointUri = new Uri(graphResourceId);
            Uri serviceRoot = new Uri(servicePointUri, tenantID);
            ActiveDirectoryClient activeDirectoryClient = new ActiveDirectoryClient(serviceRoot,
                    async () => await GetTokenForApplication());

            IList<string> groupMembership = new List<string>();
            // use the token for querying the graph to get the user details
            IUser user = activeDirectoryClient.Users
                .Where(u => u.ObjectId.Equals(userObjectID))
                .ExecuteAsync().Result.CurrentPage.ToList().First();
            var userFetcher = (IUserFetcher)user;
            requestor = user.DisplayName;
            IPagedCollection<IDirectoryObject> pagedCollection = userFetcher.MemberOf.ExecuteAsync().Result;
            do
            {
                List<IDirectoryObject> directoryObjects = pagedCollection.CurrentPage.ToList();
                foreach (IDirectoryObject directoryObject in directoryObjects)
                {
                    if (directoryObject is Group)
                    {
                        var group = directoryObject as Group;
                        groupMembership.Add(group.DisplayName);
                    }
                }
                pagedCollection = pagedCollection.GetNextPageAsync().Result;
            } while (pagedCollection != null);

            return groupMembership;
        }

The 'GetUserData()' function is calling another function called 'GetTokenForApplication()' which is responsible of getting a token form Azure. The source code of the previous function is like the following:

  protected async Task<string> 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 EF DB
        AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID, new ADALTokenCache(signedInUserID));
        AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenSilentAsync(graphResourceId, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
        return authenticationResult.AccessToken;


    } 

I strongly believe that I can avoid that from happening by changing MyADALTokenCache function that calls the 2 main functions AfterAccessNotification(), BeforeAccessNotification() function like the following:

  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"));
            this.Deserialize((Cache == null) ? null : Cache.cacheBits);
        }

AfterAccessNotification function is like the following:

void AfterAccessNotification(TokenCacheNotificationArgs args)
        {
            // if state changed
            if (this.HasStateChanged)
            {
                if(Cache == null)
                {
                    Cache = new UserTokenCache
                    {
                        webUserUniqueId = userId,
                        //cacheBits = MachineKey.Protect(this.Serialize(), "ADALCache"),
                        //LastWrite = DateTime.Now
                    };
                }
                Cache.cacheBits = this.Serialize();
                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;
            }
        }

BeforeAccessNotification function is like the following:

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"));
            this.Deserialize((Cache == null) ? null : Cache.cacheBits);
        }

The Clear function is just cleaning up the database

public override void Clear()
        {
            base.Clear();
            foreach (var cacheEntry in db.UserTokenCacheList)
                db.UserTokenCacheList.Remove(cacheEntry);
            db.SaveChanges();
        }

Any Ideal solution for this problem? Thanks!

Nan Yu
  • 26,101
  • 9
  • 68
  • 148
Ray
  • 781
  • 2
  • 17
  • 42
  • What happened when you restart the app and clear the cache ? – Nan Yu Oct 10 '17 at 02:52
  • My app is not an 'App Service' Therefore, you can't restart the app. My application is only registered (under app registration) so it can use AAD Auth. The application is hosted locally on one of our Azure VMs – Ray Oct 10 '17 at 13:29
  • Does this answer your question? [multiple\_matching\_tokens\_detected with ADAL](https://stackoverflow.com/questions/32000185/multiple-matching-tokens-detected-with-adal) – lesyk Sep 02 '20 at 07:24

1 Answers1

3

You have two options:

Option 1: Use upn when acquiring the token silent with a UserIdentifier object:

AuthenticationResult authenticationResult = await 
authenticationContext.AcquireTokenSilentAsync(graphResourceId, clientcred, new UserIdentifier(userUpn, UserIdentifierType.RequiredDisplayableId));

Note: More info about UserIdentifierType here.

Option 2: Clean up the cache

Please also see this question that describes a similar problem.

Andre Teixeira
  • 783
  • 3
  • 11