2
votes

I have an Azure Functions API which uses Azure Active Directory authentication. I can test locally and deployed using a browser and curl calls in a process of:

  1. Get a code
  2. Use the code to get a token
  3. Pass the token to authenticate and get the function result.

I now want to call this API from my Blazor WASM app but I'm sure there must be a nice MSAL call to do all the authentication but I cannot find any documentation on what that might be.

Does anyone have a code snippet to illustrate what needs to happen?

Further Information

My Azure Functions App and Blazor WASM client are not part of the same project and are hosted on different sub-domains of Azure hypotheticalapi.azurewebsites.net and hypotheticalweb.azurewebsites.net.

The web client application registration has API Permissions for the API and the API has an application registration which exposes itself with the scope that the client app has permissions for.

Again, the API and Web app work individually. I just don't seem able to get them to talk.

I have been following the "ASP.NET Core Blazor WebAssembly additional security scenarios" documentation but after several attempts I keep coming back to the error:

Microsoft.JSInterop.JSException: invalid_grant: AADSTS65001: 
The user or administrator has not consented to use the application with ID 'e40aabb0-8ed5-4833-b50d-ec7ca4e07996' named 'BallerinaBlazor5Wasm'. 
Send an interactive authorization request for this user and resource.

Even though I have revoked/deleted the client's permissions on the API, it has never repeated asking for consent. Is there a way I should clear the consent I previously gave? No idea how I might do that.

This GitHub Issue appears to be relevant.

2
Hi @phil, I wonder if you managed to solve this issue in the meantime because I an experiencing what seems to be the same problem. - Neits
@Netis I always intended to do a blog post about this but work has not permitted and now I have forgotten most of what I did to get it working. One thing I do recall was that I HAD to use the api://<guid> form for the scope Uri. A host and domain text version wouldn't work. - phil

2 Answers

1
votes

I was stuck for the last two weeks with the same error code in the same setting: Blazor WASM talking to an AAD secured Azure Functions app.

What appeared to be a problem in my case was the scopes that I was listing in the http request when contacting AAD identification provider endpoints. Almost all examples I came across use Microsoft Graph API. There, User.Read is the scope that is given as an example. My first though was that even when I am contacting my own API I have to include the User.Read scope in the request because I was reasoning that this scope is necessary to identify the user. However, this is not the case and the only scope that you have to list when you call the authorize and token endpoints is the one that you exposed under the "Expose an API blade" in your AAD app registration.

I am using the OAuth2 authorization code in my example and not the implicit grant. Make sure that in the manifest of your API registration you have set "accessTokenAcceptedVersion": 2 and not "accessTokenAcceptedVersion": null. The latter implies the use of implicit flow as far as I know.

The scope the I exposed in my API is Api.Read. You can expose more scopes if you need but the point is that you only ask for scopes that you exposed.

I also have both following options unticked (i.e. no implicit flow). However, I tried with selecting "ID token" and it still worked. Note that the "ID token" option is selected by default if you let the Azure Portal create your AAD app registration from your function app Authentication blade.

enter image description here

Blazor code

Program.cs

This code has to be added.

        builder.Services.AddScoped<GraphAPIAuthorizationMessageHandler>();
        
        builder.Services.AddHttpClient("{NAME}", 
                client => client.BaseAddress = new Uri("https://your-azure-functions-url.net"))
            .AddHttpMessageHandler<GraphAPIAuthorizationMessageHandler>();

        builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
            .CreateClient("{NAME}"));

        builder.Services.AddMsalAuthentication(options =>
        {
            builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
            // NOTE: no "api://" when providing the scope
            options.ProviderOptions.DefaultAccessTokenScopes.Add("{you API application id}/{api exposed scope}");
        });

appsetting.json

"AzureAd": {
"Authority": "https://login.microsoftonline.com/{aad tenant id}",
"ClientId": "{application id of your blazor wasm app}",
"ValidateAuthority": true

}

GraphAPIAuthorizationMessageHandler.cs

Note that this class can have a different name. you'll then also reference a different name in Program.cs.

public class GraphAPIAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public GraphAPIAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://your-azure-functions-url.net" },
            // NOTE: here with "api://"
            scopes: new[] { "api://{you API application id}/{api exposed scope}" });
    }
}

I hope this works. If not, let me know.

0
votes

At least you need to get the access token, then use the token to call the function api. In this case, if you want to get the token in only one step, you could use the client credential flow, MSAL sample here, follow every part on the left to complete the prerequisites.

The following are the approximate steps(for more details, you still need to follow the sample above):

1.Create a new App registration and add a client secret.

2.Instantiate the confidential client application with a client secret

app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
           .WithClientSecret(config.ClientSecret)
           .WithAuthority(new Uri(config.Authority))
           .Build();

3.Get the token

string[] scopes = new string[] { "<AppId URI of your function related AD App>/.default" };

result = await app.AcquireTokenForClient(scopes)
                  .ExecuteAsync();

4.Call the function API

httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

// Call the web API.
HttpResponseMessage response = await _httpClient.GetAsync(apiUri);
...
}