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!