1
votes

Development Information: Azure Registered Mobile Application, Azure hosted API, Visual Studio 2019, mobile application is built in Xamarin Forms targeting .Net-5 and API is built in Asp.Net targeting .Net-5.

Desired Workflow: User does Authorization code flow authentication interactive if AcquireTokenSilently fails, utilizes MS Authenticator. Once authenticated, the user gets an access token for Graph and for the custom hosted API.

What is working: I can do the MFA authentication piece using the MSAL library and an access token is received in the mobile application, but the token is only good for making calls to Graph. I need to be able to call the protected resource (hosted API) as well. My thoughts originally was that the JWT access token would work with the hosted API as well as Graph, but after reading more, I have read that you have to acquire two separate tokens as to prevent issues where two resources may have the same scope(s).

The problem I am facing is that I thought upon Authenticating there would be an easy way to get another resources access token without having to authenticate a second time. Isn't that the point of the OpenID authentication?? My last thought was to do an implicit authentication from the mobile app to the protected API using a client id, client secret, but I don't want to store a client secret in the mobile app for fear it may expose something sensitive to a user. I tried following the documentation of Microsoft and set up specific scopes for the hosted API and registered it in the Azure portal, but this didn't seem to have any affect on the authentication piece. I still have to authenticate against the API which isn't the desired result.

Is it possible to authenticate once for Graph and then knowing that the user authenticated correctly as they now have a valid token for Graph then somehow call this protected API without having to force them to do authentication again for the protected API and if so, how to do this without exposing any sensitive information (client secret) in the mobile application?

CODE

  // Microsoft Authentication client for native/mobile apps
  public static IPublicClientApplication PCA;
  
  //Setting up the PublicClientApplicationBuilder
  //OAuthSettings is a static class with required values (app id, tenant id, etc)
  var builder = PublicClientApplicationBuilder
                            .Create(OAuthSettings.ApplicationId)
                            .WithTenantId(OAuthSettings.TenantId)
                            .WithBroker()
                            .WithRedirectUri(OAuthSettings.RedirectUri);

  PCA = builder.Build();

  //Scopes being used in initial authentication
  //I tried adding to this with a custom scope, I added on the 
  //protected api and it caused an exception, so I didn't think
  //I could use those together with the scopes for Graph
  //Custom scope for the protected API is as follows:
  //XXX.User.Common (where XXX is our company name)
  public const string Scopes = "User.Read MailboxSettings.Read Calendars.ReadWrite";
  
  try
  {
     var accounts = await PCA.GetAccountsAsync();

     var silentAuthResult = await PCA
                    .AcquireTokenSilent(Scopes.Split(' '), accounts.FirstOrDefault())
                    .ExecuteAsync();

                
   }
   catch (MsalUiRequiredException msalEx)
   {
               
       var windowLocatorService = DependencyService.Get<IParentWindowLocatorService>();

       // Prompt the user to sign-in
       var interactiveRequest = PCA.AcquireTokenInteractive(Scopes);
                

       AuthUIParent = windowLocatorService?.GetCurrentParentWindow();

       if (AuthUIParent != null)
       {
          interactiveRequest = interactiveRequest
                        .WithParentActivityOrWindow(AuthUIParent);
       }

       var interactiveAuthResult = await interactiveRequest.ExecuteAsync();
       var accounts = await PCA.GetAccountsAsync();
       
       //at this point, I have a valid Graph token, but I need to
       //get a valid token for the protected API, 
       //unsure of how to do this last piece 
    }
           
1
Have you tried acquiring token for your protected API in your mobile app after the user is authenticated? You should be able to do so without prompting the user to authenticate again.Gaurav Mantri
@GauravMantri I tried calling AcquireTokenSilently from the MSAL library passing in parameters (scopes, IAccount) and it fails to find a token in the token cache as it hasn't been authenticated aganst the protected API. Can you give more direction as to how to acquire the second token without prompting the user to authenticate again? It looks like using the MSAL library is the suggested way, but the only other option I see is calling AcquireTokenInteractively per the suggested flow, won't this cause the user to Authenticate again?Ryan Wilson
If you're using authorization code flow via IdentitySever, then you should get an id_token and an access_token. The id_token determines what scope you have. If you specify the scope for the client to access that API, then the users logged in will automatically have access to that API via their access_token. To sum, just passing in the access_token in the auth header should give you access to that API.GH DevOps
Sure. Can you edit your question and include the code for token acquisition in your mobile app?Gaurav Mantri
@GauravMantri Sure thing. Thank you. Please hang on for a few minutes while I get the update in place.Ryan Wilson

1 Answers

1
votes

Much of this answer is thanks to Gaurav Mantri but he was too modest to accept credit, and he asked me to post an answer.

First thing I needed to change was the flow of the authentication.

Authenticate the user against the protected API, then get the Graph token, as the Graph authentication works well with the AcquireTokenSilent(IEnumerable<string> scopes, IAccount account) method of MSAL library. I was doing this backwards by authenticating against Graph then trying to get the protected API token.

So, the code for authenticating then looks like the following:

 public async Task SignIn()
 {
     try
     {
         //check for any accounts which are authenticated
         var accounts = await PCA.GetAccountsAsync();

         //try to Authenticate against the protected API silently     
         var silentAuthResult = await PCA
                    .AcquireTokenSilent(new string[] { "api://{your api client id}/.default" }, accounts.FirstOrDefault())
                    .ExecuteAsync();

                
     }
     catch (MsalUiRequiredException msalEx)
     {
        // This exception is thrown when an interactive sign-in is required.
        var windowLocatorService = DependencyService.Get<IParentWindowLocatorService>();

        // Prompt the user to sign-in
        var interactiveRequest = PCA.AcquireTokenInteractive(new string[] { "api://{your api client id}/.default" });


        AuthUIParent = windowLocatorService?.GetCurrentParentWindow();

        if (AuthUIParent != null)
        {
             interactiveRequest = interactiveRequest
                        .WithParentActivityOrWindow(AuthUIParent);
        }

        var interactiveAuthResult = await interactiveRequest.ExecuteAsync();
        var accounts = await PCA.GetAccountsAsync();
        
        //Now we can get the Graph token silently
        //We now have valid tokens for both Graph and our protected API        
        var graphtokenresult = await PCA.AcquireTokenSilent(Scopes, accounts.First()).ExecuteAsync();
                
      }
      catch (Exception ex)
      {
                
      }               
  }
 

In the Azure portal there were a few things I needed to ensure were set in the configurations.

  1. Make sure you set some custom scopes for the protected API - quickstart-configure-app-expose-web-apis

  2. (This one isn't documented in the MSDN documentation)

    On the same page you can add scopes for the protected API, there is a section called Add a client application, you would think by clicking add and then selecting an application which you want to grant permission to access your API would suffice, but it is not.

    enter image description here

You also need to go into the protected API's Manifest and add the client id of the application you want to grant access manually, as clicking the add button and selecting the client application does not modify the manifest. So, open the manifest of your protected API and add the client application id to the section of the manifect labeled knownClientApplications:

enter image description here

Once all of this has been done, you can now receive an access token which will authorize against the protected API as well as a Graph token for getting user information. I hope this is helpful and thanks again to Gaurav Mantri. If anyone has more questions about this, please contact me and I'll do my best to pass on what I have learned.