Authenticating WASM Blazor against Azure Active Directory is covered nicely by Microsoft in their walkthroughs. What they don't cover is the development workflow afterwards. Being a compiled application, every change to the UI is a painful stop-recompile-start process, which is then compounded by an AAD login process. How do we streamline this and set a fake set of credentials during development?
This approach works for me, for now, but I am keen to see what others do. Note this is primarily for development, but I could look to extend this for integration tests (which is next on my list).
In the client, make yourself a fake AuthenticationStateProvider to replace the Remote authentication one you normally use.
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
namespace Blah.Client
{
public class FakeAuthStateProvider : AuthenticationStateProvider, IAccessTokenProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, ">> TEST USER <<"),
new Claim("directoryGroup","abc4567-890-1234-abcd-1234567890abc") //Should match your group you use to determine a policy
}, "Fake authentication type");
var user = new ClaimsPrincipal(identity);
return Task.FromResult(new AuthenticationState(user));
}
public async ValueTask<AccessTokenResult> RequestAccessToken()
{
return new AccessTokenResult(AccessTokenResultStatus.Success, new AccessToken() { Expires = DateTime.Now + new TimeSpan(365,0,0,0) }, "");
}
public async ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options)
{
return new AccessTokenResult(AccessTokenResultStatus.Success, new AccessToken() { Expires = DateTime.Now + new TimeSpan(365, 0, 0, 0) }, "");
}
}
}
In the client program.cs, switch out the auth when in debug:
#if DEBUG
SetupFakeAuth(builder.Services);
#else
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, CustomUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://1234567-890-1234-abcd-1234567890abc/API.Access");
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, CustomUserAccount, CustomAccountFactory>();
#endif
.....
private static void SetupFakeAuth(IServiceCollection services)
{
//https://github.com/dotnet/aspnetcore/blob/c925f99cddac0df90ed0bc4a07ecda6b054a0b02/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs#L28
services.AddOptions();
services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, FakeAuthStateProvider>();
services.TryAddTransient<BaseAddressAuthorizationMessageHandler>();
services.TryAddTransient<AuthorizationMessageHandler>();
services.TryAddScoped(sp =>
{
return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>();
});
services.TryAddScoped<IAccessTokenProviderAccessor, FakeAccessTokenProviderAccessor>();
services.TryAddScoped<SignOutSessionStateManager>();
}
... And define the FakeAuthState provider, which is just a copy of the internal class Microsoft register:
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal
{
internal class FakeAccessTokenProviderAccessor : IAccessTokenProviderAccessor
{
private readonly IServiceProvider _provider;
private IAccessTokenProvider _tokenProvider;
public FakeAccessTokenProviderAccessor(IServiceProvider provider) => _provider = provider;
public IAccessTokenProvider TokenProvider => _tokenProvider ??= _provider.GetRequiredService<IAccessTokenProvider>();
}
}
This should result in a logged in user on the client that has a name and Scopes as usual.
Server side:
in Startup.cs
#if DEBUG
services.AddSingleton<IPolicyEvaluator, FakePolicyEvaluator>();
#else
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
#endif
and a new class:
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
namespace Blah.Server
{
public class FakePolicyEvaluator : IPolicyEvaluator
{
public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
{
const string testScheme = "FakeScheme";
var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(new[] {
new Claim("Permission", "CanViewPage"),
new Claim("Manager", "yes"),
new Claim(ClaimTypes.Role, "Administrator"),
new Claim(ClaimTypes.NameIdentifier, "John")
}, testScheme));
return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal,
new AuthenticationProperties(), testScheme)));
}
public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy,
AuthenticateResult authenticationResult, HttpContext context, object resource)
{
return await Task.FromResult(PolicyAuthorizationResult.Success());
}
}
}
Hope that helps someone. I'll now look to improve this and make it work in testing scenarios.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With