Calling Graphics API via ViewModel in MVC Web Application

I am trying to use the Graph API to create my own User Profile section in the navigation bar of my web application. For this, I have an AJAX call to the 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 Json, which I use to replace the above items as shown in my AJAX success function.

User Profile 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 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 the access token, however my AJAX call is not returning any data.
Access Token from IIS Log Console Log

I have two questions (maybe 3):

  • What am I doing wrong?
  • Can I use an access token from my Startup.Auth to create an authenticated graphical client? If so how can I do this?

        // 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 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: Fix .. View

Thanks to @Fei Xue I figured out the problem .. kind. This fixes my issue when running locally, but I still can't get the token silently when posting to my stage app. When I first created the application, I enabled Work / School authentication, which was Azure AD. This created a local DB context that it used to cache ADAL tokens. While developing the application, I created a different DB context for the Azure SQL Database I created for the application. I had to update the AdalTokenCache.cs to reflect the application DB context and the new model. I updated the line:

private ApplicationDbContext db = new ApplicationDbContext();

      

with my own context and updated the UserTokenCache model in my new UserTokenCache model. In this case, I changed:

private UserTokenCache Cache;

      

in

private UserTokenCach Cache;

      

Then I updated the rest of the CS to match the UserTokenCach from the application's DB context.

Then I just used the AcquireToken method that came OOB in the UserProfile controller to get the token. This is what it looked like (Note: I also updated the lines in my startup.auth from private to public to use them in my view model):

    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 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 when I play some more.

+3


source to share


1 answer


There are two types of access token issued by Azure Active Directory.

The first is a delegate token , which is used to delegate the user to work with the user's resource.

And the other is an application token that is typically used to perform an operation on an organization-wide resource, and there is no user context in that token. Therefore, we should not use this token to execute a resource like me

that which requires user context.

The code in the message obtains an access token using the client credential stream, which is the application token . So you will get an error when you get a user or image using this kind of token based on custom context.



In this case, you must purchase an access token using the AuthorizationCodeReceived

publish event . This event uses the authorization credential flow thread to obtain a delegate token for the user. Then in the controller, you can get the token using the method AcquireTokenSilentAsync

that will get the access token from catch.

The sample code below is very useful for a scenario that calls Microsoft Graph in a web application to delegate a login to a user:

active-directory-dotnet-graphapi-web

+1


source







All Articles