0

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?

BigThinker
  • 81
  • 1
  • 11

1 Answers1

0

Rather than trying to disable everything, I use a simple AuthenicationProvider linked to an Identity provider and UserBar to change between identities. This lets me setup the default identity and switch between identities quickly to test code.

Here's the code:

A static class to hold the test identities

public static class TestIdentities
{
    public const string Provider = "Dumb Provider";

    public static ClaimsIdentity GetIdentity(string userName)
    {
        var identity = identities.FirstOrDefault(item => item.Name.Equals(userName, StringComparison.OrdinalIgnoreCase));
        if (identity == null)
            return new ClaimsIdentity();

        return new ClaimsIdentity(identity.Claims, Provider);
    }

    private static List<TestIdentity> identities = new List<TestIdentity>()
        {
            Visitor1Identity,
            User1Identity,
            User2Identity,
            Admin1Identity,
        };

    public static List<string> GetTestIdentities()
    {
        var list = new List<string> { "None" };
        list.AddRange(identities.Select(identity => identity.Name!).ToList());
        return list;
    }

    public static Dictionary<Guid, string> TestIdentitiesDictionary()
    {
        var list = new Dictionary<Guid, string>();
        identities.ForEach(identity => list.Add(identity.Id, identity.Name));
        return list;
    }

    public static TestIdentity Visitor1Identity
        => new TestIdentity
        {
            Id = new Guid("10000000-0000-0000-0000-000000000001"),
            Name = "Visitor-1",
            Role = "VisitorRole"
        };

    public static TestIdentity User1Identity
        => new TestIdentity
        {
            Id = new Guid("20000000-0000-0000-0000-000000000001"),
            Name = "User-1",
            Role = "UserRole"
        };

    public static TestIdentity User2Identity
        => new TestIdentity
        {
            Id = new Guid("20000000-0000-0000-0000-000000000002"),
            Name = "User-2",
            Role = "UserRole"
        };

    public static TestIdentity Admin1Identity
        => new TestIdentity
        {
            Id = new Guid("30000000-0000-0000-0000-000000000001"),
            Name = "Admin-1",
            Role = "AdminRole"
        };
}

public record TestIdentity
{
    public string Name { get; set; } = string.Empty;

    public Guid Id { get; set; } = Guid.Empty;

    public string Role { get; set; } = string.Empty;

    public Claim[] Claims
        => new[]{
            new Claim(ClaimTypes.Sid, Id.ToString()),
            new Claim(ClaimTypes.Name, Name),
            new Claim(ClaimTypes.Role, Role)
    };

}

The authentication provider

public class VerySimpleAuthenticationStateProvider : AuthenticationStateProvider
{
    ClaimsPrincipal? _user;

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
        => Task.FromResult(new AuthenticationState(_user ?? new ClaimsPrincipal()));

    public Task<AuthenticationState> ChangeIdentityAsync(string username)
    {
        _user = new ClaimsPrincipal(TestIdentities.GetIdentity(username));
        var task = GetAuthenticationStateAsync();
        NotifyAuthenticationStateChanged(task);
        return task;
    }
}

And the Nav Bar component to switch between identities:

@implements IDisposable
@namespace Blazr.App.UI
@using System.Security.Claims

<span class="me-2">Change User:</span>
<div class="w-25">
    <select id="userselect" class="form-control" @onchange="ChangeUser">
        @foreach (var value in TestIdentities.GetTestIdentities())
        {
            @if (value == _currentUserName)
            {
                <option value="@value" selected>@value</option>
            }
            else
            {
                <option value="@value">@value</option>
            }
        }
    </select>
</div>
<span class="text-nowrap ms-3">
    <AuthorizeView>
        <Authorized>
            Hello, @(this.user.Identity?.Name ?? string.Empty)
        </Authorized>
        <NotAuthorized>
            Not Logged In
        </NotAuthorized>
    </AuthorizeView>
</span>

@code {

    [CascadingParameter] private Task<AuthenticationState>? authTask { get; set; }
    private Task<AuthenticationState> AuthTask => authTask!;

    [Inject] private AuthenticationStateProvider? authState { get; set; }
    private VerySimpleAuthenticationStateProvider AuthState => (VerySimpleAuthenticationStateProvider)authState!;

    private ClaimsPrincipal user = new ClaimsPrincipal();
    private string _currentUserName = "None";

    protected async override Task OnInitializedAsync()
    {
        var authState = await AuthTask;
        this.user = authState.User;
        AuthState.AuthenticationStateChanged += this.OnUserChanged;
    }

    private async Task ChangeUser(ChangeEventArgs e)
        => await AuthState.ChangeIdentityAsync(e.Value?.ToString() ?? string.Empty);

    private async void OnUserChanged(Task<AuthenticationState> state)
        => await this.GetUser(state);

    private async Task GetUser(Task<AuthenticationState> state)
    {
        var authState = await state;
        this.user = authState.User;
    }

    public void Dispose()
        => AuthState.AuthenticationStateChanged -= this.OnUserChanged;
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • @Marvin and Shaun, I tried to follow your steps but I got a runtime exception when testing your authentication state provider. After searching for long, I found the solution at this [answer](https://stackoverflow.com/a/66063573/1873477) which I adapted to better fit my projects. – BigThinker Feb 27 '23 at 10:04