Mathias F
Mathias F

Reputation: 15931

Call Api from blazor and pass Auth token

I have a blazor webassembly project that gets created from https://github.com/Azure-Samples/ms-identity-blazor-wasm/tree/main/WebApp-graph-user/Call-MSGraph.

Its basically the project that gets created when you use the .net core template for a Blazor application that uses authentication via AD B2B

dotnet new blazorwasm -au SingleOrg --client-id "{CLIENT ID}" -o {APP NAME} --tenant-id "{TENANT ID}"

I was then able to call graph.api when the user logged in. I then tried to call my own Api with that authentication as described in https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-3.1.

I used

   builder.Services.AddHttpClient<ITestDataService, TestDataService>(
      client => client.BaseAddress = new Uri("https://localhost:44342/"))
        .AddHttpMessageHandler(x =>
        {
            var handler = x.GetRequiredService<AuthorizationMessageHandler>()
                .ConfigureHandler(new[] { "https://localhost:44342/" },
                    scopes: new[] { "https://graph.microsoft.com/User.Read" });

            return handler;
        });

I can see that a token is attached when calling the Api but authentication fails (401). The api is generated from Visual Studio templates for B2B AD and uses the configuration that is also used for the Blazor application.

This is its Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
        .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
    services.AddControllers();
}


        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseCors(policy =>
            policy.WithOrigins("http://localhost:5000", 
                "https://localhost:5001")
            .AllowAnyMethod()
            .WithHeaders(HeaderNames.ContentType,
                HeaderNames.Authorization,
                "x-custom-header")
            .AllowCredentials());

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

Do you have any idea what is missing?

The complete source is available at github https://github.com/mathiasfritsch/blazor-calls-api

Upvotes: 0

Views: 4288

Answers (1)

Jim Xu
Jim Xu

Reputation: 23161

If you want to call Microsoft graph and your custom API in one blazor webassembly project, we can implement it by creating different HTTP client to call different API

For example

  • Register a server API app

    1. Register an AAD app for the Server API app
    2. Expose an API
  • Register a client app

    1. Register a client app
    2. Enable Implicit grant flow
    3. Add API permissions. (Graph API permissions and API app permissions)
  • Configure API app

Please add the following code in Startup.cs

 public void ConfigureServices(IServiceCollection services)
 {
        JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
        services.AddCors(options =>
            {
                options.AddDefaultPolicy(
                    builder => builder.AllowAnyOrigin()
                        .AllowAnyHeader()
                        .AllowAnyMethod());
            });
               services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
                .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));

            services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
            {
                options.Authority += "/v2.0";


                options.TokenValidationParameters = new TokenValidationParameters
                {
                    
                    ValidIssuers = new[] {
                      $"https://sts.windows.net/{Configuration["AzureAD:TenantId"]}/",
                      $"https://login.microsoftonline.com/{Configuration["AzureAD:TenantId"]}/v2.0"

                    },
                    RoleClaimType = "roles",
                    // The web API accepts as audiences both the Client ID (options.Audience) and api://{ClientID}.
                    ValidAudiences = new[]
                    {
                           options.Audience,
                           $"api://{options.Audience}"
                    }

                };

            });
....
}
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.OAuthClientId(Configuration["Swagger:ClientId"]);
                c.OAuthScopeSeparator(" ");
                c.OAuthAppName("Protected Api");

                c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
            });
            app.UseHttpsRedirection();

            app.UseRouting();
            app.UseCors();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
  • Configure Client APP
  1. Create custom AuthorizationMessageHandler for Graph API and custom API
// custom API
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://localhost:44300/" },
            scopes: new[] { "the API app scope" });
    }
}
//Graph API
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class GraphAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public GraphAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://graph.microsoft.com/" },
            scopes: new[] { "https://graph.microsoft.com/User.Read" });
    }
}
  1. Add the following code to the program.cs
public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
            builder.Services.AddScoped<GraphAuthorizationMessageHandler>();
            // register HTTP client to call our own api
            builder.Services.AddHttpClient("MyAPI", client => client.BaseAddress = new Uri("https://localhost:44300/"))
              .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
            // register HTTP client to call graph api
            builder.Services.AddHttpClient("GraphAPI", client => client.BaseAddress = new Uri("https://graph.microsoft.com/"))
              .AddHttpMessageHandler<GraphAuthorizationMessageHandler>();
  
            builder.Services.AddMsalAuthentication(options =>
            {
                builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
                options.ProviderOptions.DefaultAccessTokenScopes.Add("<the API app scope>");
                options.ProviderOptions.AdditionalScopesToConsent.Add("https://graph.microsoft.com/User.Read");
            });

            await builder.Build().RunAsync();
        }
    }
  1. Call the api
@inject IHttpClientFactory _clientFactory

var httpClient = _clientFactory.CreateClient("<the client name you register>");
await apiClient.GetStringAsync("path");

enter image description here

Upvotes: 3

Related Questions