Greg Gum
Greg Gum

Reputation: 37909

How to implement Custom Authorization in Blazor Server

I would like to bypass AspnetCore identity completely, and instead use a custom method of Authenticating the current user.

However, I would still like the existing Authorization framework to work. In other words, I want to be able to use the AuthorizeView and @attribute [Authorize] to maintain security.

I have searched and searched but am not finding any details on how to implement this.

Upvotes: 16

Views: 20377

Answers (2)

Greg Gum
Greg Gum

Reputation: 37909

Ok, so you want to implement Custom Authentication in your Blazor Server app. In other words, you want to use a different method than ASP.net Identity to register and authenticate users. But you still want to use the built in Authorization goodies such as AuthorizedView and the [Authorize] attribute on your pages.

Note that what I say here is only applicable to a Blazor Server App. If you are using a Blazor Webassembly, you need a different solution which is not covered here, as it is a completely different security model.

Ok, to get started: To implement Custom Authentication, you need to implement a class called AuthenticationStateProvider.

Here is the link to the docs for creating a Custom AuthenticationStateProvider. However, the docs don't give a complete solution, so here is the rest of it based on the docs:

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    public CustomAuthenticationStateProvider()
    {
        this.CurrentUser = this.GetAnonymous();
    }

    private ClaimsPrincipal CurrentUser { get; set; }

    private ClaimsPrincipal GetUser(string userName, string id, string role)
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes. Sid, id),
            new Claim(ClaimTypes.Name, userName),
            new Claim(ClaimTypes.Role, role)
        }, "Authentication type");
        return new ClaimsPrincipal(identity);
    }

    private ClaimsPrincipal GetAnonymous()
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Sid, "0"),
            new Claim(ClaimTypes.Name, "Anonymous"),
            new Claim(ClaimTypes.Role, "Anonymous")
        }, null);
        return new ClaimsPrincipal(identity);
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var task = Task.FromResult(new AuthenticationState(this.CurrentUser));
        return task;
    }

    public Task<AuthenticationState> ChangeUser(string username, string id, string role)
    {
        this.CurrentUser = this.GetUser(username, id, role);
        var task = this.GetAuthenticationStateAsync();
        this.NotifyAuthenticationStateChanged(task);
        return task;
    }

    public Task<AuthenticationState> Logout()
    {
        this.CurrentUser = this.GetAnonymous();
        var task = this.GetAuthenticationStateAsync();
        this.NotifyAuthenticationStateChanged(task);
        return task;
    }
}

After this is in place, register it in program.cs, and comment out the existing one:

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
//builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();

Now you are ready to use it to log your user in:

<AuthorizeView>
    <Authorized>
        <div class="m-1 p-1 text-white">
            Welcome,  @user.Identity.Name
        </div>
    </Authorized>
    <NotAuthorized>
        <div>
            <div>
                Your email<input type="text" @bind-value="@Email" />
            </div>
            <div>
                Your password<input type="text" @bind-value="@Password" />
            </div>
            <div>
                <button style="width:70px;"  @onclick="Login">Submit</button>
            </div>
        </div>
    </NotAuthorized>
</AuthorizeView>

@code{
    public string Email { get; set; }

    public string Password { get; set; }

    public string Id { get; set; } = "12345";

    [CascadingParameter] public Task<AuthenticationState> AuthTask { get; set; }

    [Inject] private AuthenticationStateProvider AuthState { get; set; }

    private System.Security.Claims.ClaimsPrincipal user;

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

    public async Task Login()
    {
        //Todo:  Validate against actual database.
        var authState = await ((CustomAuthenticationStateProvider)AuthState).ChangeUser(this.Email, this.Id, "Associate");
        this.user = authState.User;
    }
}

In your implementation, of course you need to authorize your user as you see fit.

Disclaimer: I am not a security expert in any way. The above has not been tested other than a brief smoke test while I had lunch.

Note that this does not keep the user closes the page, or refreshes the page.

However, with the above in place, the built in Authorization framework works, including roles.

Upvotes: 15

Remi THOMAS
Remi THOMAS

Reputation: 910

Indeed, all the information is quite fragmented in the documentation. That's why I created this example.

https://github.com/iso8859/AspNetCoreAuthMultiLang

At the end of file myServerAuthenticationStateProvider.cs you can play with roles.

result = new AuthenticationState(
                new ClaimsPrincipal(
                    new ClaimsIdentity(new[] { 
                        new Claim(ClaimTypes.Name, m_login),
                        new Claim(ClaimTypes.Role, "demo_role"),
                        new Claim(ClaimTypes.Role, "Admin"),
                    }, "PutAppNameHere"
            )));

Upvotes: 5

Related Questions