Using multiple APIs in Blazor with Azure AD authentication
source link: https://damienbod.com/2020/12/14/using-multiple-apis-in-blazor-with-azure-ad-authentication/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Using multiple APIs in Blazor with Azure AD authentication
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
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.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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK