Reputation: 99
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.
Upvotes: 0
Views: 1448
Reputation: 142222
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://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS
Upvotes: 0