I am working on a Blazor WebAssembly project that is setup with AzureAD authentication and makes calls to an API project that's also protected via AzureAD. It's working fine.
My task is to find a solution to make local development less bothersome by (for example) disabling authentication in the local environment.
In the API project, the solution was as simple as changing this block of code in Startup.cs Configure method:
var builder = endpoints.MapControllers();
// env is IHostEnvironment which is an argument passed by the Configure method by default.
if (env.IsEnvironment("Local"))
{
// Add [AllowAnonymous] to all controllers locally, which overrides any authorization requirements.
builder.AllowAnonymous();
}
In the client project, I started by conditionally using the custom authorization message handler in Program.cs Main method:
builder.Services.AddScoped<APIAuthorizationMessageHandler>();
var applicationIdUri = builder.Configuration.GetSection("AzureAd")["ApplicationIdUri"];
var baseUrl = builder.Configuration["APIURL"];
baseUrl = $"{baseUrl}/api/v1/";
var client = builder.Services
.AddHttpClient("API", client => client.BaseAddress = new Uri(baseUrl));
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Local")
{
client.AddHttpMessageHandler<APIAuthorizationMessageHandler>();
}
builder.Services.AddSingleton(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("API"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add($"{applicationIdUri}/full");
});
In case anyone is interested, this is how the authorization message handler looks like:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Extensions.Configuration;
namespace Web.Authorization
{
public class APIAuthorizationMessageHandler : AuthorizationMessageHandler
{
public APIAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigation, IConfiguration configuration) : base(provider, navigation)
{
var applicationIdUri = configuration.GetSection("AzureAd")["ApplicationIdUri"];
ConfigureHandler(
authorizedUrls: new[] { configuration["APIURL"] },
scopes: new[] { $"{applicationIdUri}/full" });
}
}
}
My question is how to best handle the UI files in this scenario, attached below?
App.razor
@using Web.Services
@inject IJSRuntime JSRuntime
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="#">Home</a></li>
</ol>
</div>
</div>
</div><!-- /.container-fluid -->
</section>
<!-- Main content -->
<section class="content">
<div class="error-page">
<h2 class="headline text-warning"> 401</h2>
<div class="error-content">
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Not authorized.</h3>
@if (context.User.Identity?.IsAuthenticated != true)
{
<p>Please login.</p>
}
else
{
<p>You are not authorized to access this resource.</p>
}
</div>
<!-- /.error-content -->
</div>
<!-- /.error-page -->
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="#">Home</a></li>
</ol>
</div>
</div>
</div><!-- /.container-fluid -->
</section>
<!-- Main content -->
<section class="content">
<div class="error-page">
<h2 class="headline text-warning"> 404</h2>
<div class="error-content">
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Oops! Page not found.</h3>
<p>
We could not find the page you were looking for.
Meanwhile, you may <a href="/">return to dashboard</a>.
</p>
</div>
<!-- /.error-content -->
</div>
<!-- /.error-page -->
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
@code
{
protected override async Task OnInitializedAsync()
{
await JSRuntime.InvokeAsync<object>("removeAppLoadingElements");
await JSRuntime.InvokeAsync<object>("initializeMenuTreeview");
}
}
_Imports.razor
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
Shared/LoginDisplay.razor
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.DependencyInjection
@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager
<AuthorizeView>
<Authorized>
<div class="user-panel mt-3 pb-3 mb-3 d-flex">
<div class="info">
<a href="#" class="d-block">@context.User.Identity?.Name</a>
<button class="nav-link btn btn-link" @onclick="BeginLogout">Log out</button>
</div>
</div>
</Authorized>
<NotAuthorized>
<div class="user-panel mt-3 pb-3 mb-3 d-flex">
<div class="info">
<a href="authentication/login" class="d-block">Sign in</a>
</div>
</div>
</NotAuthorized>
</AuthorizeView>
@code {
private async Task BeginLogout(MouseEventArgs args)
{
await SignOutManager.SetSignOutState();
Navigation.NavigateTo("authentication/logout");
}
}
Shared/MainLayout.razor
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.Extensions.Logging
<div class="wrapper">
<!-- Navbar -->
<MainHeader />
<!-- /.navbar -->
<!-- Main Sidebar Container -->
<MainSideBar />
<!-- Content Wrapper. Contains page content -->
<CascadingValue Value="this" Name="MainLayout">
@Body
</CascadingValue>
<!-- /.content-wrapper -->
</div>
Shared/MainSideBar.razor
<aside class="main-sidebar sidebar-dark-primary elevation-4">
<!-- Sidebar -->
<div class="sidebar">
<LoginDisplay />
<!-- Sidebar Menu -->
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar nav-child-indent flex-column" data-widget="treeview" role="menu" data-accordion="true">
<!-- Add icons to the links using the .nav-icon class with font-awesome or any other icon font library -->
<AuthorizeView>
<Authorized>
<li class="nav-item">
<NavLink class="nav-link" href="/" Match="NavLinkMatch.All">
<i class="far fa-circle nav-icon"></i>
<p>Overview</p>
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="/projects">
<i class="far fa-circle nav-icon"></i>
<p>Projects</p>
</NavLink>
</li>
</Authorized>
</AuthorizeView>
</ul>
</nav>
<!-- /.sidebar-menu -->
</div>
<!-- /.sidebar -->
</aside>
Pages/Index.razor
@page "/"
<AuthorizeView>
<Authorized>
<h1>Welcome!</h1>
</Authorized>
<NotAuthorized>
<p>You are not authorized, please <a href="authentication/login">sign in</a>.</p>
</NotAuthorized>
</AuthorizeView>
Best solution that I have come up with so far is to do the environment check everywhere there is an AuthorizeView
, Authorized
, NotAuthorized
, and LoginDisplay
tag and show them only if environment is not local. Does anyone have a more elegant solution?