Home > OS >  Custom AuthenticationStateProvider in blazor project doesn't work on server side
Custom AuthenticationStateProvider in blazor project doesn't work on server side

Time:10-12

Hi all! I'm trying to make my custom auth mode in Blazor WebAssembly App (this is where studio creates 3 projects - client, server, shared). Idea is avoid IS4 auth and make my oun "internal" user for test purposes and understand the work of auth mech as well. I'm doing it by creating my custom AuthenticationStateProvider? like it shown in official docs. This is my AuthenticationStateProvider class:

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private bool _isLoggedIn = true;

    //this is a parameter defininng whether user logged in or not
    //changed by reflection
    public bool IsLoggedIn
    {
        get
        {
            return _isLoggedIn;
        }
        set
        {
            _isLoggedIn = value;
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }
    }
    private static CustomAuthStateProvider _myInstance = null;



    public Serilog.ILogger _logger { get; set; }

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        ClaimsIdentity identity;

        Task<AuthenticationState> rez;

     if (IsLoggedIn)
        {
            identity = new ClaimsIdentity(new[]
            {
            new Claim(ClaimTypes.Name, "User01"),
            }, "Fake authentication type");
        }
        else
        {
            identity = new ClaimsIdentity();
        }
        var user = new ClaimsPrincipal(identity);

        rez = Task.FromResult(new AuthenticationState(user));

        return rez;

    }

    public static CustomAuthStateProvider GetMyInstance(Serilog.ILogger logger = null, string mySide = "")
    {
        //implementing singleton
        if (_myInstance == null)
        {
            _myInstance = new CustomAuthStateProvider();
            _myInstance._logger = logger;
        }
        return _myInstance;
    }
}

This is how i plug it on client side (program.cs)

builder.Services.AddSingleton<AuthenticationStateProvider>(x => CustomAuthStateProvider.GetMyInstance(Log.Logger, "Client"));

This is how i plug it on server side (startup.cs)

services.AddSingleton<AuthenticationStateProvider, CustomAuthStateProvider>();

Problem: it works fine on client side, that means i can login, logout and use AutorizeView and similar tags. But It doesnt' t work on server side, that means HttpContext.User doesn't see any user authenticated and i cant use [Authorize] and similar attributes. What i'm doing wrong? How HttpContext.User is connected to AuthenticationStateProvider in asp.net core project? Thanks ;-)

CodePudding user response:

Here's a very simple Test AuthenticationStateProvider I hashed up recently for a Server Side project.

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Blazor.Auth.Test
{
    public class TestAuthenticationStateProvider : AuthenticationStateProvider
    {

        public TestUserType UserType { get; private set; } = TestUserType.None;

        private ClaimsPrincipal Admin
        {
            get
            {
                var identity = new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.Sid, "985fdabb-5e4e-4637-b53a-d331a3158680"),
                    new Claim(ClaimTypes.Name, "Administrator"),
                    new Claim(ClaimTypes.Role, "Admin")
                }, "Test authentication type");
                return new ClaimsPrincipal(identity);
            }
        }

        private ClaimsPrincipal User
        {
            get
            {
                var identity = new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.Sid, "024672e0-250a-46fc-bd35-1902974cf9e1"),
                    new Claim(ClaimTypes.Name, "Normal User"),
                    new Claim(ClaimTypes.Role, "User")
                }, "Test authentication type");
                return new ClaimsPrincipal(identity);
            }
        }

        private ClaimsPrincipal Visitor
        {
            get
            {
                var identity = new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.Sid, "3ef75379-69d6-4f8b-ab5f-857c32775571"),
                    new Claim(ClaimTypes.Name, "Visitor"),
                    new Claim(ClaimTypes.Role, "Visitor")
                }, "Test authentication type");
                return new ClaimsPrincipal(identity);
            }
        }

        private ClaimsPrincipal Anonymous
        {
            get
            {
                var identity = new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.Sid, "0ade1e94-b50e-46cc-b5f1-319a96a6d92f"),
                    new Claim(ClaimTypes.Name, "Anonymous"),
                    new Claim(ClaimTypes.Role, "Anonymous")
                }, null);
                return new ClaimsPrincipal(identity);
            }
        }

        public override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var task = this.UserType switch
            {
                TestUserType.Admin => Task.FromResult(new AuthenticationState(this.Admin)),
                TestUserType.User => Task.FromResult(new AuthenticationState(this.User)),
                TestUserType.None => Task.FromResult(new AuthenticationState(this.Anonymous)),
                _ => Task.FromResult(new AuthenticationState(this.Visitor))
            };
            return task;
        }

        public Task<AuthenticationState> ChangeUser(TestUserType userType)
        {
            this.UserType = userType;
            var task = this.GetAuthenticationStateAsync();
            this.NotifyAuthenticationStateChanged(task);
            return task;
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Blazor.Auth.Test
{
    public enum TestUserType
    {
        None,
        Visitor,
        User,
        Admin
    }
}

Startup regstration:

services.AddScoped<AuthenticationStateProvider, TestAuthenticationStateProvider>();

Simple Component I add to NavMenu to switch users.

<AuthorizeView>
    <Authorized>
        <div class="m-1 p-1 text-white">
            @user.Identity.Name
        </div>
    </Authorized>
    <NotAuthorized>
        <div class="m-1 p-1 text-white">
            Not Logged In
        </div>
    </NotAuthorized>
</AuthorizeView>
<div class="m-1 p-3">
    <select class="form-control" @onchange="ChangeUser">
        @foreach (var value in Enum.GetNames(typeof(TestUserType)))
        {
            <option value="@value">@value</option>
        }
    </select>
</div>

@code {

    [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;
    }

    private async Task ChangeUser(ChangeEventArgs e)
    {
        var en = Enum.Parse<TestUserType>(e.Value.ToString());
        var authState = await ((TestAuthenticationStateProvider)AuthState).ChangeUser(en);
        this.user = authState.User;
    }
}

CodePudding user response:

AuthenticationStateProvider is an object that is populated by Blazor with a ClaimPrincipal object. It is done automatically, both in Blazor Server App and Blazor WebAssembly App, provided that you've chosen Individual Accounts when you created your app. The ClaimPrincipal data is retrieved from the HttpContext object, when you access the App for the first time, or when you navigate out of the SPA space of your app, as for instance when you are redirected to an Identity Login Page (This is a Razor Page, and it is not part of Blazor) (this means that you've checked up the Individual Accounts when you created your Blazor WebAssembly App). After logging in, you are being redirected back to your Blazor App. This is when the framework capture the ClaimPrincipal object in order to refresh the AuthenticationStateProvider object.

But, if you don't choose Individual Accounts your AuthenticationStateProvider is useless, unless you introduce a custom authentication method or whatever. You can, as for instance, use Jwt token authentication, authenticate a user's credentials viaa Web Api's methods, and return to the client a Jwt Token that you should store somewhere, say in the local storage, access it when you have to retrieve data from a Web Api method, add the Jwt Token to the Authorization Headers of the HttpClient service, etc.

What you did in your app is to create a custom AuthenticationStateProvider, create a ClaimPrincipal object with some claims, and it works fine on the front-end (browser). But you don't have any authentication system on the Server (Web Api with methods that do what I've described above: Authenticate the user, create a Jwt Token, returns it the the front-end, etc. Customizing the AuthenticationStateProvider object is easy-peasy. What you want to do is hard and complicated and dangerous. Anyhow, below is a link to an app created by the creator of Blazor, Steve Anderson. It was created before Blazor offered you ready made system of authentication such as Identity, IdentityServer4, and more. This can be the best source for you to learn what is going on, what is the role of the AuthenticationStateProvider, and how it works with authentication mechanism, and much more.

  • Related