How to use a blazor server app with a web api for authentication?

António Albuquerque 25 Reputation points
2023-11-30T19:45:34.3966667+00:00

I've search the web for a simple tutorial for this but I can't seem to find this exact scenario.

I have the following projects:

WebApi This is a standard .NET 8.0 web api with individual accounts authentication. I've created everything using the microsoft defaults and I'm using the new endpoints created automatically by app.MapGroup("/account").MapIdentityApi(). This works fine and I'm able to login and register using postman / swagger

ApiClient

This is a class library that handles all the calls to the WebApi including the login, register, etc. This ApiClient library has a method to set the access token before calling the actual method if it required authentication. This is also working fine

Now, what I want to do now is to create a Blazor Server app (I'm new at Blazor) that will provide me an UI for Login (for example) and when the user puts in the username/password it will call the apiClient.Login(...) method with the username and password. What I need to do then is check if I get the correct token and if so store this somewhere on blazor side and tell blazor that I'm authenticated. So when I go to an [Authorized] page in blazor and it requires me to call an api, it will pass the access token stored on login and call the apiClient method I want.

All examples online either use a different authentication JWT token, AAD, etc. or they simple show creating the database and the auth layer all in the Blazor server side. This is not what I need. I need a standalone API using the new IdentityUI endpoints and a way to connect from blazor to that API and keep me logged in, etc.

I think I need to create a custom AuthenticationStateProvider but I still don't understand how can I handle this. Do I use cookie based auth in blazor? Do I rely solely on the api to know I'm logged in? How will the AuthorizedViews in blazor know that I'm authenticated. I assume I have to manually set the user in the HttpContext.User with the claims I get from the API, etc.

Another thing is the 2FA authentication. The generated identity UI endpoint for the login works as follows: You login with your username / password, you get an 401 error back with the detail "TwoFactorRequired" and you then need to call the same login endpoint with the user / pass and 2fa code. Which means that in the blazor UI I would need to catch that 401, keep the user/pass in a hidden or variable, show the 2FA code page and then call login again with the 3 values. This doesn't seem too secure if I'm storing the username/password in a hidden field just to get the 2FA. Or maybe there is another way to login with 2fa without using the standard UI endpoint.

Can someone point me to a tutorial or give me some lights on how to implement this?

My reason to want to use a independent API is because I want to be able to expose the said API to other devices, etc.

The reason I want to use the default identity UI endpoints is for the management of all the 2FA, recovery codes, refresh tokens, etc. I had a custom logic using JWT tokens in the past and it was a pain to maintain.

Any help is appreciated.

Thanks

Blazor
Blazor
A free and open-source web framework that enables developers to create web apps using C# and HTML being developed by Microsoft.
1,665 questions
{count} vote

Accepted answer
  1. Ruikai Feng - MSFT 2,756 Reputation points Microsoft Vendor
    2023-12-05T09:15:47.2066667+00:00

    Hi,@António Albuquerque,For your questions:

    How to I say to the blazor app that we are now authenticated

    --You could try NotifyAuthenticationStateChanged method

    and then using HttpContext.SignInAsync to sign in that user (using cookie based auth here). Is this correct

    --You should avoid it ,you would interact with server via signalr instead of http in Blazor,For detailed reason,please check this document

    The other question is where do I store the access tokens I receive after login

    --Create a scoped service to hold the tokens,follow this document(Notice the authentication service is for MVC part as documented)

    If you just want authenticate in a razor component,a minimal example:

    public class AuthenticationService
    {
        public event Action<ClaimsPrincipal>? UserChanged;
        private ClaimsPrincipal? currentUser;
        public ClaimsPrincipal CurrentUser
        {
            get { return currentUser ?? new(); }
            set
            {
                currentUser = value;
    
                if (UserChanged is not null)
                {
                    UserChanged(currentUser);
                }
            }
        }
    }
    
    
    public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private AuthenticationState authenticationState;
    
        public CustomAuthenticationStateProvider(AuthenticationService service)
        {
            authenticationState = new AuthenticationState(service.CurrentUser);
    
            service.UserChanged += (newUser) =>
            {
                authenticationState = new AuthenticationState(newUser);
    
                NotifyAuthenticationStateChanged(
                    Task.FromResult(new AuthenticationState(newUser)));
            };
        }
    
        public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
            Task.FromResult(authenticationState);
    }
    
    
    public class TokenProvider
    {
        public string? accesstoken { get; set; }
    
        public string? refreshtoken { get; set; }
    }
    
    public class UserProfile
    {
        public string? UserName { get; set; }
        //other claims....
    }
    
    
    

    register the services:

    builder.Services.AddScoped<AuthenticationService>();
    builder.Services.AddScoped<TokenProvider>();
    builder.Services.AddScoped<AuthenticationStateProvider,
        CustomAuthenticationStateProvider>();
    
    builder.Services.AddHttpClient("Auth", op =>
    {
        op.BaseAddress = new Uri("target uri");
        
    });
    
    
    

    razor component:

    
    <AuthorizeView>
        <Authorized>
            <p>Hello, @context.User.Identity?.Name!</p>
        </Authorized>
        <NotAuthorized>
            <div>
                Email:<input @bind="email" />
            </div>
            <div>
                Password:   <input type="password" @bind="password" />
            </div>
            <div>
                <button @onclick="SignIn">Sign in</button>
            </div>
            <p>You're not authorized.</p>
        </NotAuthorized>
    </AuthorizeView>
    
    @code {
        public string email = string.Empty;
        public string password = string.Empty;
        
    
        private async Task SignIn()
        {
            var httpclient = httpclientfactory.CreateClient("Auth");
            var response = await httpclient.PostAsJsonAsync("/account/login", new { email = email, password = password });
            if (response.IsSuccessStatusCode)
            {
                tokenprovider = await response.Content.ReadFromJsonAsync<TokenProvider>();
                httpclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenprovider.accesstoken);
                var userprofile = await httpclient.GetFromJsonAsync<UserProfile>("/profile");
                //add some check here
                var identity = new ClaimsIdentity(
                 new[]
                  {
                    new Claim(ClaimTypes.Name,  userprofile.UserName),
                  },
               "Custom Authentication");
    
                var newUser = new ClaimsPrincipal(identity);
                AuthenticationService.CurrentUser = newUser;            
            } 
        }
    }
    
    
    

    Result:

    12.5

    If you want to keep your authentication state for a long time,you may check this document related and this part of document


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    Best regards,

    Ruikai Feng

    1 person found this answer helpful.

0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.