2
votes

I'm trying to build a feature where a client application retrieves the graph resources via WebAPI layer. The scenario has following applications:

  1. Angular5 Client application
  2. ASP.Net Core Web API

The Angular5 client application uses MSAL to authenticate against application (resisted as Converged application via apps.dev.microsoft.com registration application; AAD v2 endpoint). The authentication flow defines the Web API as scope while login or getting access token

constructor() {
   var logger = new Msal.Logger((logLevel, message, piiEnabled) =>
   {
     console.log(message);
   },
   { level: Msal.LogLevel.Verbose, correlationId: '12345' });
   this.app = new Msal.UserAgentApplication(
      CONFIGSETTINGS.clientId,
      null,
      this.authCallback,
      {
        redirectUri: window.location.origin,
        cacheLocation: 'localStorage', 
        logger: logger 
      }
   );
}

public getAPIAccessToken() {
return this.app.acquireTokenSilent(CONFIGSETTINGS.scopes).then(
    accessToken => {
      return accessToken;
    },
    error => {
      return this.app.acquireTokenSilent(CONFIGSETTINGS.scopes).then(
        accessToken => {
          return accessToken;
        },
        err => {
          console.error(err);
        }
      );
    }
  );

}

Here scope is defined as scopes: ['api://<<guid of application>>/readAccess']. This is the exact value which was generated when I've registered the Web API in registeration portal. Also, the client application id is added as Pre-authorized applications enter image description here.

The Web API layer (built in dotnet core -- and uses JwtBearer to validate the authentication), defines the API which internally fetches the graph resources (using HttpClient). To get the access token, I've used following code

        public async Task<string> GetAccesToken(string resourceName)
        {
            var userAssertion = this.GetUserAssertion();
            string upn = GetLoggedInUpn();
            var userTokenCache = new SessionTokenCache(upn, new Microsoft.Extensions.Caching.Memory.MemoryCache(new MemoryCacheOptions())).GetCacheInstance();
            string msGraphScope = "https://graph.microsoft.com/User.Read";
            string authority = string.Format("https://login.microsoftonline.com/{0}/v2.0", this.authConfig.TenantId);
            ConfidentialClientApplication clientApplication = new ConfidentialClientApplication(this.authConfig.ClientId, authority, new ClientCredential(this.authConfig.AppKey), userTokenCache, null);

            var result = await clientApplication.AcquireTokenOnBehalfOfAsync(new string[] { msGraphScope }, userAssertion);
            return result != null ? result.AccessToken : null;
        }



        private UserAssertion GetUserAssertion()
        {
             string token = this.httpContextAccessor.HttpContext.Request.Headers["Authorization"];
            string upn = GetLoggedInUpn();
            if (token.StartsWith("Bearer", true, CultureInfo.InvariantCulture))
            {
                 token = token.Trim().Substring("Bearer".Length).Trim();
                return new UserAssertion(token, "urn:ietf:params:oauth:grant-type:jwt-bearer");
            }
            else
            {
               throw new Exception($"ApiAuthService.GetUserAssertion() failed: Invalid Authorization token");
             }
         }

Note here, The method AcquireTokenOnBehalfOfAsync is used to get the access token using graph scope. However it throws the following exception:

AADSTS65001: The user or administrator has not consented to use the application with ID '<>' named '<>'. Send an interactive authorization request for this user and resource.

I'm not sure why the of-behalf flow for AAD v2 is not working even when client application uses the Web API as scope while fetching access token and Web API registers the client application as the pre-authorized application. Note - I've tried using the other methods of ConfidentialClientApplication but even those did not work.

Can someone please point out how the above flow can work without providing the admin consent on Web API?

1

1 Answers

0
votes

I've been trying to figure this out for weeks! My solution isn't great (it requires the user to go through the consent process again for the Web API), but I'm not sure that's entirely unexpected. After all, either the Admin has to give consent for the Web API to access the graph for the user, or the user has to give consent.

Anyway, the key was getting consent from the user, which of course the Web API can't do since it has no UI. However, ConfidentialClientApplication will tell you the URL that the user has to visit with GetAuthorizationRequestUrlAsync.

Here's a snippet of the code that I used to get it working (I'm leaving out all the details of propagating the url back to the webapp, but you can check out https://github.com/rlittletht/msal-s2s-ref for a working example.)

async Task<string> GetAuthenticationUrlForConsent(ConfidentialClientApplication cca, string []graphScopes)
{
    // if this throws, just let it throw
    Uri uri = await cca.GetAuthorizationRequestUrlAsync(graphScopes, "", null);
    return uri.AbsoluteUri;
}

async Task<string> GetAccessTokenForGraph()
{
    // (be sure to use the redirectUri here that matches the Web platform
    // that you added to your WebApi
    ConfidentialClientApplication cca =
        new ConfidentialClientApplication(Startup.clientId,
            "http://localhost/webapisvc/auth.aspx",
            new ClientCredential(Startup.appKey), null, null);

    string[] graphScopes = {"https://graph.microsoft.com/.default"};

    UserAssertion userAssertion = GetUserAssertion();

    AuthenticationResult authResult = null;
    try
    {
        authResult = await cca.AcquireTokenOnBehalfOfAsync(graphScopes, userAssertion);
    }
    catch (Exception exc)
    {
        if (exc is Microsoft.Identity.Client.MsalUiRequiredException
            || exc.InnerException is Microsoft.Identity.Client.MsalUiRequiredException)
        {
            // We failed because we don't have consent from the user -- even
            // though they consented for the WebApp application to access
            // the graph, they also need to consent to this WebApi to grant permission
            string sUrl = await GetAuthenticationUrlForConsent(cca, graphScopes);

            // you will need to implement this exception and handle it in the callers
            throw new WebApiExceptionNeedConsent(sUrl, "WebApi does not have consent from the user to access the graph on behalf of the user", exc);
        }
        // otherwise, just rethrow
        throw;
    }
    return authResult.AccessToken;
}

One of the things that I don't like about my solution is that it requires that I add a "Web" platform to my WebApi for the sole purpose of being able to give it a redirectUri when I create the ConfidentialClientApplication. I wish there was some way to just launch the consent workflow, get the user consent, and then just terminate the flow (since I don't need a token to be returned to me -- all I want is consent to be granted).

But, I'm willing to live with the extra clunky step since it actually gets consent granted and now the API can call the graph on behalf of the user.

If someone has a better, cleaner, solution, PLEASE let us know! This was incredibly frustrating to research.