The post shows how to create a Blazor application which is hosted in an ASP.NET Core application and provides a public API which uses multiple downstream APIs. Both the Blazor client and the Blazor API are protected by Azure AD authentication. The Blazor UI Client is protected like any single page application. This is a public client which cannot keep a secret.
Each downstream API uses a different type of access token in this demo. One API delegates to a second API using the on behalf of flow. The second API uses a client credentials flow for APP to APP access and the third API uses a delegated Graph API. Only the API created for the Blazor WASM application is public. All other APIs require a secret to access the API. A certificate could also be used instead of a secret.
Code: https://github.com/damienbod/AzureADAuthRazorUiServiceApiCertificate
Posts in this series
- Implement Azure AD Client credentials flow using Client Certificates for service APIs
- Using Key Vault certificates with Microsoft.Identity.Web and ASP.NET Core applications
- Using encrypted access tokens in Azure with Microsoft.Identity.Web and Azure App registrations
- Implement a Web APP and an ASP.NET Core Secure API using Azure AD which delegates to a second API
- Using multiple APIs in Angular and ASP.NET Core with Azure AD authentication
- Using multiple APIs in Blazor with Azure AD authentication
- Azure AD Access Token Lifetime Policy Management in ASP.NET Core
- Implement OAUTH Device Code Flow with Azure AD and ASP.NET Core
- Implement app roles authorization with Azure AD and ASP.NET Core
Setup
The applications are setup very similar to the previous post in this series. Four Azure App Registrations are setup for the different applications. The Blazor WASM client uses a public SPA Azure App registration. This has one API exposed here, the access_as_user scope from the Blazor server Azure App registration. The WASM SPA has no access to the further downstream APIs. We want to have as few as possible access tokens in the browser. The Blazor Server application uses a secret to access the downstream APIs which are exposed in the API Azure App registration.
Blazor Server
The Blazor server (API) and client (UI) applications were setup using the Visual Studio templates. The Client application is hosted as part of the server and so deployed together. The Blazor server application is otherwise a simple API project. The API uses Microsoft.Identity.Web as the Azure AD client. The application requires user secrets for the protected downstream APIs.
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <UserSecretsId>BlazorAzureADWithApis.Server-B86B9EF3-5CCE-46B7-A115-E5D3ACB43477</UserSecretsId> <WebProject_DirectoryAccessLevelKey>1</WebProject_DirectoryAccessLevelKey> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\Client\BlazorAzureADWithApis.Client.csproj" /> <ProjectReference Include="..\Shared\BlazorAzureADWithApis.Shared.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.1" /> <PackageReference Include="Microsoft.Identity.Web.MicrosoftGraphBeta" Version="1.4.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" NoWarn="NU1605" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1" NoWarn="NU1605" /> <PackageReference Include="Microsoft.Identity.Web" Version="1.4.0" /> <PackageReference Include="Microsoft.Identity.Web.UI" Version="1.4.0" /> </ItemGroup> </Project>
The ConfigureServices method adds the required services for the Azure AD API authorization. The access to the downstream APIs are implemented as scoped services. The ValidateAccessTokenPolicy policy is used to validate the access token used for the public API in this project. This is the API which the Blazor WASM client uses.
public void ConfigureServices(IServiceCollection services) { services.AddScoped<GraphApiClientService>(); services.AddScoped<ServiceApiClientService>(); services.AddScoped<UserApiClientService>(); services.AddMicrosoftIdentityWebApiAuthentication(Configuration) .EnableTokenAcquisitionToCallDownstreamApi() .AddInMemoryTokenCaches(); services.AddControllers(options => { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); options.Filters.Add(new AuthorizeFilter(policy)); }); services.AddAuthorization(options => { options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy => { // Validate ClientId from token // only accept tokens issued .... validateAccessTokenPolicy.RequireClaim("azp", "ad6b0351-92b4-4ee9-ac8d-3e76e5fd1c67"); }); }); services.AddControllersWithViews(); services.AddRazorPages(); }
The Configure method adds the middleware for the APIs like any ASP.NET Core API. It also adds the middleware for the Blazor UI.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseHttpsRedirection(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapControllers(); endpoints.MapFallbackToFile("index.html"); }); }
A user secret is required for the protected downstream APIs. These APIs cannot be accessed from the public Blazor UI. The less access tokens you use in the public zone, the better.
{ "AzureAd": { "ClientSecret": "your client secret from the API App registration" } }
The DelegatedUserApiCallsController is the API which can be used to access the downstream API. This API accepts access tokens which the Blazor UI requested. The controller calls the API services for further API calls, in this case a delegated user API request.
using System.Collections.Generic; using System.Threading.Tasks; using BlazorAzureADWithApis.Server.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Identity.Web.Resource; namespace BlazorAzureADWithApis.Server.Controllers { [Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [ApiController] [Route("[controller]")] public class DelegatedUserApiCallsController : ControllerBase { private UserApiClientService _userApiClientService; static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" }; public DelegatedUserApiCallsController(UserApiClientService userApiClientService) { _userApiClientService = userApiClientService; } [HttpGet] public async Task<IEnumerable<string>> Get() { HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); return await _userApiClientService.GetApiDataAsync(); } } }
The service uses the IHTTPClientFactory to manage the HttpClient connections. The ITokenAcquisition interface is used to get the access tokens for the downstream API using the correct scope. The downstream APIs are implemented to require a secret.
using Microsoft.Identity.Web; using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; using System.Threading.Tasks; namespace BlazorAzureADWithApis.Server.Services { public class UserApiClientService { private readonly IHttpClientFactory _clientFactory; private readonly ITokenAcquisition _tokenAcquisition; public UserApiClientService( ITokenAcquisition tokenAcquisition, IHttpClientFactory clientFactory) { _clientFactory = clientFactory; _tokenAcquisition = tokenAcquisition; } public async Task<IEnumerable<string>> GetApiDataAsync() { var client = _clientFactory.CreateClient(); var scopes = new List<string> { "api://b2a09168-54e2-4bc4-af92-a710a64ef1fa/access_as_user" }; var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes); client.BaseAddress = new Uri("https://localhost:44395"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await client.GetAsync("ApiForUserData"); if (response.IsSuccessStatusCode) { var data = await JsonSerializer.DeserializeAsync<List<string>>( await response.Content.ReadAsStreamAsync()); return data; } throw new Exception("oh no..."); } } }
Blazor Client
The Blazor Client project implements the WASM UI. This project uses the Microsoft.Authentication.WebAssembly.Msal to authenticate against Azure AD.
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.1" PrivateAssets="all" /> <PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="5.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="5.0.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Shared\BlazorAzureADWithApis.Shared.csproj" /> </ItemGroup> <ItemGroup> <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" /> </ItemGroup> </Project>
The Program class builds the services for the WASM application in the static Main method. The scope which will be used to access the API for this WASM client is defined here. The IHttpClientFactory is used to create HttpClient instances for the API calls. The app.settings.json configuration is saved in the wwwroot folder. The BlazorAzureADWithApis.ServerAPI HttpClient uses the BaseAddressAuthorizationMessageHandler to add the access tokens in the Http Header for the API calls.
using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace BlazorAzureADWithApis.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.AddHttpClient("BlazorAzureADWithApis.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>(); // Supply HttpClient instances that include access tokens when making requests to the server project builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("BlazorAzureADWithApis.ServerAPI")); builder.Services.AddMsalAuthentication(options => { builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); options.ProviderOptions.DefaultAccessTokenScopes.Add("api://2b50a014-f353-4c10-aace-024f19a55569/access_as_user"); }); await builder.Build().RunAsync(); } } }
The App.razor component defines how the application should login and the component to use for this.
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> @if (!context.User.Identity.IsAuthenticated) { <RedirectToLogin /> } else { <p>You are not authorized to access this resource.</p> } </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
The RedirectToLogin component is used to redirect to the authentication provider, in our case Azure AD.
@inject NavigationManager Navigation @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @code { protected override void OnInitialized() { Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}"); } }
A razor component can then be used to call the API client which uses the access token acquired from Azure AD. This uses the HttpClient which was defined in the Main method and uses the BaseAddressAuthorizationMessageHandler.
@page "/delegateduserapicall" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @attribute [Authorize] @inject HttpClient Http <h1>Data from Delegated User API</h1> @if (apiData == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Data</th> </tr> </thead> <tbody> @foreach (var data in apiData) { <tr> <td>@data</td> </tr> } </tbody> </table> } @code { private string[] apiData; protected override async Task OnInitializedAsync() { try { apiData = await Http.GetFromJsonAsync<string[]>("DelegatedUserApiCalls"); } catch (AccessTokenNotAvailableException exception) { exception.Redirect(); } } }
Running the code
Start the three applications from Visual Studio and click the login link. A popup will open and you can login to Azure AD and give your consent for this client. Before this will work, you will need to setup your own Azure App registrations and set the configurations in the projects. Also add your user secret to the Blazor Server, User and Service API projects.
The API can be called using the acces token for this API. The delegated access token then calls the downstream API using the delegated acces token and the data is returned.
Links
https://docs.microsoft.com/en-us/aspnet/core/blazor/security
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki
[…] Using multiple APIs in Blazor with Azure AD authentication (Damien Bowden) […]
[…] Using multiple APIs in Blazor with Azure AD authentication – Damien Bowden […]
Hi Damien,
thank you so much for the series, it is very documented and helpful !
Are you planning to make one around the usage of Azure API management as well ?
Hi Dimitri
thanks,
maybe, I was thinking about this, wondering about the use case of API management, where it would add value, reduce the overall costs or increase the quality of the solution.
Greetings Damien
Hi Damien,
nice post! It realy helped me.
Where is claim azp configured? Is it part of the client app registration or the server API registration? I think some screenshots of the azure AD configuration pages would help
Mathias
Hi Damien, great post ! Same here i don’t get how to get the azp claim in Azure ad o_0
i replace it with “appid” for the check, but i must admit that i’m not sure if it’s equal for the security…
Hi Jean.R, thanks
I guess you use the version 1 API, open the manifest file of the Azure App registration and check th<t version 2 is set
Greetings Damien
Hi Damien, Great post. I’m trying something along these lines where I have a Blazor WASM with Server API (accessing SQL Azure using EF) and I also need to Call the GraphAPI. I’ve set up the Client (SPA) and Server API (Web) App registrations and can authenticate successfully with our AAD but keep getting 403 Forbidden when I try to call the Server API (similar to your Direct API call) and also when trying to call Graph. API. Am I missing something in terms of configurations on the App Reg’s?
Hi Liam you need to use V2 access tokens and give consent in the App registration. Depending on the app type, you use a Spa type (no secret) or Web (with secret/certificate) and consent needs to be granted. See if the Microsoft.Identity.Web WIKI docs in github help, this describes setting up the App registration. Only delegated scopes can be used. Let me know if this works, otherwise maybe we could share manifests and I can compare.
Greetings Damien
Thanks for the quick reply Damien. Appreciated.
In my Server API App Reg. (which is of type Web Platform) I have delegated access to User.Read and User.Read.All and given admin consent to both. I have enabled both access tokens and id tokens. Relatively new to all this so not sure how to go about using V2 access tokens. Can’t find much online about it. I have no problem sharing app reg manifests if you want to take a look . Just let me know how you would like to go about it .
Thanks again!
Hi Damien, Tried a few more options over the weekend with different combinations of settings on the app registrations but still getting 403 when trying to access anything that requires a token. I can see the bearer token being assigned in my dev tools once I’m authenticated with AAD but even when I try to use it in Swagger UI with the API I still get 403. Any ideas ? Really need to get this working for a project I’m working so any help you can provide would be appreciated !