1

I have an application which needs access to a custom API as well as the ability to send emails via the Graphs API. When I get my access token I seem to only be able to pass graphs OR my api scopes but not a combination of both.

I create my app as follows

_clientApp = PublicClientApplicationBuilder.Create(ClientId)
            .WithAuthority(AzureCloudInstance.AzurePublic, Tenant)
            .Build();

I then authenticate as follows

private async Task<AuthenticationResult> AuthenticateAsync(string[] scopes)
    {
        string error = String.Empty;
        AuthenticationResult authResult = null;

        var accounts = await _clientApp.GetAccountsAsync();
        var firstAccount = accounts.FirstOrDefault();

        try
        {
            authResult = await _clientApp.AcquireTokenSilent(scopes, firstAccount).ExecuteAsync();
        }
        catch (MsalUiRequiredException ex)
        {
            // A MsalUiRequiredException happened on AcquireTokenSilent. 
            // This indicates you need to call AcquireTokenInteractive to acquire a token
            System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");

            try
            {
                authResult = await _clientApp.AcquireTokenInteractive(scopes)
                    .WithAccount(accounts.FirstOrDefault())
                    .WithPrompt(Prompt.SelectAccount)
                    .ExecuteAsync();
            }
            catch (MsalException msalex)
            {
                error += $"Error Acquiring Token:{System.Environment.NewLine}{msalex}" + Environment.NewLine;
            }
        }
        catch (Exception ex)
        {
            error += $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}" + Environment.NewLine;
        }

        if (string.IsNullOrEmpty(error))
            return authResult;
        else
            throw new AuthenticationException(error);
    }

If I pass the scope

string[] graphsScopes = new string[] { "user.read" };

I can send the email successfully but I cannot make calls to my customAPI and if I pass the scope

string[] customScopes = new string[] { "api://<MyApiId>/<MyScopeName>" };

I can make calls to my custom API but cannot send emails.

If I try to add both I only get scopes in the AuthenticationResult for whichever one came first in the scopes array.

If I make two calls to AuthenticateAsync, one for each scope, and use the two separate AccessTokens retrieved for their respective calls to my api and to send an email it all works but I am not sure why I should need to manage two different access tokens in my application to one registered application in the azure portal.

I found this Multiple resources in a single authorization request question which mentions not being able to mix scopes from microsoft graph and outlook.office365.com but no indication as to why or if the same applies to mixing customAPI scopes and graph scopes

russelrillema
  • 452
  • 7
  • 14

1 Answers1

1

You need to call AcquireToken twice when acquiring tokens silently at least. This is because an access token is only valid for one API, named by the audience (aud) claim in the token. So to call two APIs, you need two different access tokens.

There used to be issues with specifying scopes for two different APIs when authenticating interactively, but IIRC it should work now. You should be able to use the refresh token you get to get tokens for different APIs. Or to be more precise, you should be able to get a token for the other API silently after the interactive call.

juunas
  • 54,244
  • 13
  • 113
  • 149
  • Yeah, it does get the second token silently but I don't understand why I need two tokens when both API permissions I require are listed in the same application. Do you by any chance understand what the reason is for this, what I see as a, limitation? – russelrillema Dec 10 '19 at 10:09
  • They are required by one app, but you are calling _two APIs_, and thus need two access tokens. Like I said, an access token is only valid for _one API_, and you have two APIs that you are calling. – juunas Dec 10 '19 at 10:15
  • 1
    Hmmm, interesting. I think there is an aspect of this that I am not grasping 100% but at least I know this is the necessary way of doing it and I am not unnecessarily complicating matters. Thanks a lot for the help – russelrillema Dec 10 '19 at 10:34
  • 2
    @russelrillema Consider an app calling multiple APIs. The app may be authorized to call all APIs on behalf of the user (e.g. the user has consented), but each API has not been authorized to call the other APIs. Separate access tokens, each one identifying the API it is intended for, ensures that an access token used for one API cannot be re-used by that API to call another API. (continued...) – Philippe Signoret Dec 10 '19 at 17:27
  • 2
    (...) If (and only if) an API _is_ authorized to call another API on behalf of the user, [a specific flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) can be used to exchange the access token the first API received for a new access token, now intended for the second API. – Philippe Signoret Dec 10 '19 at 17:28