I'm trying to use Graph API to create my own "User Profile" section of the navbar of my web app. To do this I have an AJAX call to a GetUser Action of my UserProfile Controller:
$.ajax({ type: "GET", url: "@Url.Action("GetUser", "UserProfile", null)", dataType: "json", success: function (data, status, xhr) { console.log("in AJAX"); $(".img-circle, .user-image").attr("src", data.Picture); $("#user-menu-expanded").text(data.User.DisplayName + " - " + data.User.JobTitle); $("#user-menu-spinner").remove(); console.log(data); }, error: function (ex) { console.log(ex); } }); The controller returns my UserProfileViewModel as a Json which I use to replace the above elements as shown in my AJAX success function.
UserProfile Controller:
public JsonResult GetUser() { var model = new UserProfileViewModel(); return Json(model, JsonRequestBehavior.AllowGet); } My UserProfileViewModel looks like this:
public UserProfileViewModel() { var graphClient = GetAuthGraphClient(); GetPicture(graphClient); GetUserProfile(graphClient); } public GraphServiceClient GetAuthGraphClient() { string graphResourceID = "https://graph.microsoft.com/"; return new GraphServiceClient( new DelegateAuthenticationProvider((requestMessage) => { string accessToken = GetTokenForApplication(graphResourceID); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken); return Task.FromResult(0); } )); } public string GetTokenForApplication(string graphResourceID) { 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; string authority = "https://login.microsoftonline.com/" + tenantID; try { ClientCredential clientcred = new ClientCredential(clientId, appKey); // initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database AuthenticationContext authenticationContext = new AuthenticationContext(authority); var token = authenticationContext.AcquireTokenAsync(graphResourceID, clientcred).Result.AccessToken; return token; } catch (Exception e) { // Capture error for handling outside of catch block ErrorMessage = e.Message; return null; } } public void GetPicture(GraphServiceClient graphClient) { Stream photo = Task.Run(async () => { return await graphClient.Me.Photo.Content.Request().GetAsync(); }).Result; using (var memoryStream = new MemoryStream()) { photo.CopyTo(memoryStream); var base64pic = Convert.ToBase64String(memoryStream.ToArray()); this.Picture = "data:image;base64," + base64pic; HttpContext.Current.Cache.Add("Pic", this.Picture, null, DateTime.Now.AddHours(5), Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, null); } } public void GetUserProfile(GraphServiceClient graphClient) { this.User = Task.Run(async () => { return await graphClient.Me.Request().GetAsync(); }).Result; } I am successfully getting an access token, however my AJAX call is not returning any data.
Access Token from IIS Log Console Log
I have two questions (possibly 3):
- What am I doing wrong?
Is it possible to use the access token from my Startup.Auth to create an authenticated Graph Client? If so, how would I go about doing that?
// This is the resource ID of the AAD Graph API. We'll need this to request a token to call the Graph API. string graphResourceId = "https://graph.microsoft.com"; //https://graph.windows.net public void ConfigureAuth(IAppBuilder app) { ApplicationDbContext db = new ApplicationDbContext(); app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); app.UseKentorOwinCookieSaver(); app.UseCookieAuthentication(new CookieAuthenticationOptions()); app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions { ClientId = clientId, Authority = Authority, PostLogoutRedirectUri = postLogoutRedirectUri, Notifications = new OpenIdConnectAuthenticationNotifications() { // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. AuthorizationCodeReceived = (context) => { var code = context.Code; ClientCredential credential = new ClientCredential(clientId, appKey); string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value; AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID)); AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId); HttpContext.Current.Cache.Add("Token", result.AccessToken, null, DateTime.Now.AddHours(5), Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, null); return Task.FromResult(0); } } }); } }
Updated Code per Comment Below
public string GetTokenForApplication(string graphResourceID) { 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; string authority = "https://login.microsoftonline.com/" + tenantID; try { // 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 database AuthenticationContext authenticationContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(userObjectID)); var result = authenticationContext.AcquireTokenSilent(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId)); return result.AccessToken; } catch (Exception e) { // Capture error for handling outside of catch block ErrorMessage = e.Message; return null; } } Update 2: The Fix.. Kind of
Thanks to @Fei Xue I figured out the problem.. kind of. This fixes my problem when running locally, but I still fail to acquire the token silently when publishing to my stage application.. When I first created the application, I included Work/School authentication that was Azure AD. This created a local DB Context that it used for the ADAL token cache. While developing the application, I created another DB Context for the Azure SQL DB I created for the app. I had to update my AdalTokenCache.cs to reflect my app's DB context and the new model. I updated the line:
private ApplicationDbContext db = new ApplicationDbContext(); with my own context and updated the UserTokenCache model to my new context's UserTokenCache model. In this case I changed:
private UserTokenCache Cache; to:
private UserTokenCach Cache; I then updated the rest of the CS to match the UserTokenCach from the app's DB context.
I then just used the AcquireToken method that came OOB in the UserProfile controller to get a token. This is what it wound up looking like (Note: I also updated the strings in my startup.auth from private to public so I could use them in my viewmodel):
public string GetTokenForApplication(string graphResourceID) { 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; string authority = "https://login.microsoftonline.com/" + tenantID; try { // get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc) ClientCredential clientcred = new ClientCredential(Startup.clientId, Startup.appKey); // initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database AuthenticationContext authenticationContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(signedInUserID)); var result = authenticationContext.AcquireTokenSilent(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId)); return result.AccessToken; } catch (Exception e) { // Capture error for handling outside of catch block ErrorMessage = e.Message; return null; } } I'll update as I play around some more.