Please forgive me as I'm sure some of my terminology is incorrect here.
I have a Blazor server app which uses Microsoft Identity / Azure AD (via AddAuthentication( ... ).AddMicrosoftIdentityWebApp()
) to authenticate my company's users via their Office365 email address.
We now have a situation where we want to allow outsiders (those without a company email) into the app via a standard username/password. I have a custom AuthenticationStateProvider
that's able to validate the user's credentials, create tokens, and verify them. It's added via:
builder.Services.AddScoped<DealerAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider, DealerAuthenticationStateProvider>(
p => p.GetRequiredService<DealerAuthenticationStateProvider>() );
Each of these solutions works fine by themselves. Is there a way to combine them? I understand I'm replacing the default AuthenticationStateProvider
, which blows away the Microsoft Identity provider. Is there a way to allow my AuthenticationStateProvider
to detect that Microsoft Identity is in use and fall back to that provider so that these solutions might coexist peacefully?
Or am I going about this all wrong?
EDIT: Added my custom AuthenticationStateProvider
by request. It's pretty simple with SignIn
, SignOut
, and overridden GetAuthenticationStateAsync
methods. JWT bits have been taken out for brevity.
public class DealerAuthenticationStateProvider : AuthenticationStateProvider
{
private const string LOCALSTORAGE_IDENTITY = "identity";
private readonly ProtectedLocalStorage localStorage;
public DealerAuthenticationStateProvider( ProtectedLocalStorage localStorage ) =>
this.localStorage = localStorage;
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
ProtectedBrowserStorageResult<string> storedPrincipal = new();
// Apparently this is expected to fail during rendering, but is called again
// https://stackoverflow.com/a/75510739/1110311
try { storedPrincipal = await localStorage.GetAsync<string>( LOCALSTORAGE_IDENTITY ); }
catch( InvalidOperationException ) {}
var principal = new ClaimsPrincipal();
try
{
if( storedPrincipal.Success )
principal = ... // Decrypt and validate the JWT from storedPrincipal.Value
}
catch
{
// Token validation failed? Delete it
await localStorage.DeleteAsync( LOCALSTORAGE_ACCESS_TOKEN );
}
return new AuthenticationState( principal );
}
public async Task<bool> SignInAsync( DealerAuthFormModel form )
{
var principal = new ClaimsPrincipal();
if( ... ) // The dealer number from the form is valid
{
var identity = new ClaimsIdentity( new Claim[]
{
new (ClaimTypes.Name, dealer.Name ),
new (ClaimTypes.NameIdentifier, dealer.AccountNumber )
}, TokenParams.AuthenticationType );
var token = ... // Encode and encrypt a JWT
await localStorage.SetAsync( LOCALSTORAGE_ACCESS_TOKEN, token );
principal = new ClaimsPrincipal( identity );
}
NotifyAuthenticationStateChanged( Task.FromResult( new AuthenticationState( principal ) ) );
return principal.Identity != null;
}
public async Task SignOutAsync()
{
await localStorage.DeleteAsync( LOCALSTORAGE_ACCESS_TOKEN );
NotifyAuthenticationStateChanged( Task.FromResult( new AuthenticationState( new ClaimsPrincipal() ) ) );
}
}