ASP.Net MVC C#: Failed to acquire token silently as no token was found in the cache. Call method AcquireToken

0

I am attempting to retrieve Azure AD user profile data via the Microsoft Graph API. I have set up a small Visual Studio MVC app using code examples from various sources, primarily Microsoft. In my ignorance, I thought this would be a fairly simple process.

I have browsed other similar cases on SO and have attempted to make use of suggestions from others but to no avail. I have been troubleshooting this issue for four days and would greatly appreciate any assistance.

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

As you can see, I have noted in the GetGraphAccessToken() method the error message:

"Failed to acquire token silently as no token was found in the cache. Call method AcquireToken".

I was able to isolate the AcquireTokenSilentAsync() method as the culprit by bracketing it with a pair of Debug.Writeline statements, the first which ran successfully and the second which did not. This was verified by reviewing the contents of the VS Output window, as follows:

Check point #1

Exception thrown: 'Microsoft.IdentityModel.Clients.ActiveDirectory.AdalSilentTokenAcquisitionException' in mscorlib.dll

The program '[13980] iisexpress.exe' has exited with code -1 (0xffffffff).

I really want this thing to work and I would much prefer utilizing the Graph SDK approach as opposed to using a REST API.

Again, I have been banging my head against the wall for four-plus days. My head is okay but the wall is in bad shape.

Thanks in advance.

c#
azure-active-directory
microsoft-graph
azure-ad-b2c
microsoft-graph-sdks
asked on Stack Overflow Aug 21, 2019 by R Powell

2 Answers

1

If AcquireTokenSilent fails, it means that there is no token in the cache so you have to go get one via AcquireToken as in this.

You've tagged the question with "B2C" but is looks like you are using Azure AD?

answered on Stack Overflow Aug 21, 2019 by rbrayb
0

There are a full set of Authentication Providers for the standard set of OAuth flows that are now available so you don't have to use the DelegatedAuthenticationProvider any more. https://github.com/microsoftgraph/msgraph-sdk-dotnet-auth There are docs about how to chose the right Auth provider based on scenario here https://docs.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS

answered on Stack Overflow Aug 28, 2019 by Darrel Miller

User contributions licensed under CC BY-SA 3.0