2

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() ) ) );
    }
}
josh2112
  • 829
  • 6
  • 22
  • Just because I want to know. Could you show your AuthenticationStateProvider which validates the users which do not have a company email? Thanks in advance! – Marvin Klein May 05 '23 at 12:28
  • Thanks for adding! This will help me out in my projects a lot. Once I find a solution for your question I will return. Much love – Marvin Klein May 06 '23 at 17:11

0 Answers0