Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to authenticate a user with Blazor Server

I have a Blazor Server application that uses MongoDB as the database so I'm trying to implement authentication with that. So I can use the <Authenticted>, <AuthorizeView Roles="admin"> and other tags like that in the razor pages.

The built-in authentication template uses SQL Server, which I don't want in this case, and there isn't a clear example of how to do it yourself with another database. Given the example Microsoft provides here

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

namespace BlazorSample.Services
{
    public class CustomAuthStateProvider : AuthenticationStateProvider
    {
        public override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var identity = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Name, "mrfibuli"),
            }, "Fake authentication type");

            var user = new ClaimsPrincipal(identity);

            return Task.FromResult(new AuthenticationState(user));
        }
    }
}

How should one use this in an application? You obviously wouldn't hard code a single value, and value-type, as your only source of authenticating. So how should that be parameterized? With local attributes like:

Username { get; set; }
UserType { get; set; }

In which case where would you set that?

Also how would you then use this to authenticate a user? I have the class added in the startup file under the ConfigurationServices(...) method:

...
services.AddScoped<AuthenticationStateProvider, MongoAuthenticationStateProvider>();
...

I can't figure out how to authenticate anyone. I would imagine you validate the username and password in any number of ways, then when you know it's good you go ahead and update the authentication in .NET. I was following a tutorial where they suggested something like this in the code behind:

using System;
using System.Linq;
using DocsPlatform.Services;
using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;

namespace DocsPlatform.Pages
{
    public class LoginBase : ComponentBase
    {
        [CascadingParameter]
        private Task<AuthenticationState> authStateTask { get; set; }
        protected string username { get; set; }
        protected string password { get; set; }

        protected async Task LoginUser()
        {
            bool isValid = true;

            isValid = dbService.ValidateUser(username, password);

            string email = dbService.GetEmail(username);

            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, username),
                new Claim(ClaimTypes.Email, email),
            };

            var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity));

            return NavigationManager.NavigateTo("/");
        }
    }
}

However, the navigation in the return doesn't work (they didn't explain how their code even compiled) and the SignInAsync() method isn't available in they way they showed. Again, I have no idea how their code compiled. So how would one normally do this?

I can't find any tutorials, examples, etc other than the hundreds of examples that just use the built-in SQL Server template. Where can one find details on how this is used? Anything other than the "use the built-in template" or a link to the documention here would be appreciated as neither explain how to do this.

like image 585
CoderLee Avatar asked Feb 24 '20 20:02

CoderLee


People also ask

How do I authorize a component in Blazor?

[Authorize] attribute Only use [Authorize] on @page components reached via the Blazor Router. Authorization is only performed as an aspect of routing and not for child components rendered within a page. To authorize the display of specific parts within a page, use AuthorizeView instead.


1 Answers

The built-in authentication template uses SQL Server, which I don't want in this case, and there isn't a clear example of how to do it yourself with another database

I guess you're using the ASP.NET Core Identity, right? If you're looking for a way that uses other providers, see official docs

How should one use this in an application? You obviously wouldn't hard code a single value, and value-type, as your only source of authenticating. So how should that be parameterized?

Since you're using the Blazor Server(instead of Blazor Wasm), you don't have to custom the GetAuthenticationStateAsync() method and then create a principal manually. There's already a built-in ServerAuthenticationStateProvider that both inherits from AuthenticationStateProvider and implements the IHostEnvironmentAuthenticationStateProvider interface:

// source code of the built-in ServerAuthenticationStateProvider 
public class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
{
    private Task<AuthenticationState> _authenticationStateTask;

    /// <inheritdoc />
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
        => _authenticationStateTask
        ?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}.");

    /// <inheritdoc />
    public void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask)
    {
        _authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask));
        NotifyAuthenticationStateChanged(_authenticationStateTask);
    }
}

As you see above, the GetAuthenticationStateAsync() will return the auth state set by IHostEnvironmentAuthenticationStateProvider. So what you need is to inject an IHostEnvironmentAuthenticationStateProvider and invoke IHostEnvironmentAuthenticationStateProvider::SetAuthenticationState(...). And finally the authentication state will be sent to Blazor <Authorize/> automatically.

Actually, the above ServerAuthenticationStateProvider have no idea whether the principal is still valid. So there's another built-in concrete class for you : RevalidatingServerAuthenticationStateProvider.

The above code works for every authentication scheme, including ASP.NET Core Identity, JwtBearer, AAD, and so on. It doesn't matter what authentication schemes you're using or which database you're using. Just extends the RevalidatingServerAuthenticationStateProvider class.

For example, if you're using the ASP.NET Core Identity ( you might see an issue related to Cookies(See this thread), it will generate a class of RevalidatingIdentityAuthenticationStateProvider that uses UserManager<TUser> to validate whether the principal is valid.

public class RevalidatingIdentityAuthenticationStateProvider<TUser>
    : RevalidatingServerAuthenticationStateProvider where TUser : class
{
    ...

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        // Get the user manager from a new scope to ensure it fetches fresh data
        // use the UserManager to determine whether the current principal is still valid

Since ASP.NET Core Identity is not limited to SQL Server, the RevalidatingIdentityAuthenticationStateProvider works fine for other databases. If you want to use MongoDB, feel free to create a custom MyMongoDbRevalidatingAuthenticationStateProvider.

Also how would you then use this to authenticate a user

Just declare the component like this:

<AuthorizeView>
    <Authorized>
        ...
    </Authorized>
    <NotAuthorized>
       ...
    </NotAuthorized>
</AuthorizeView>

You will NOT do it manually if you're using the default RevalidatingServerAuthenticationStateProvider. With Blazor Server Side, the authentication is done by the AuthenticationMiddleware, and then the authentication state will be passed to <AuthorizeView/> automatically. And when the authentication state expires, the <AuthorizeView/> will also update automatically.

the navigation in the return doesn't work

Actually, you code should fail before it navigates:

HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity));

Note the SignIn() method will try to send a cookie over HTTP. However, most of the time, there's no HTTP after the connection has been set up. Actually I have answered an exactly same question several month ago.


In short:

  1. It doesn't matter which authentication scheme you're using, implement a RevalidatingServerAuthenticationStateProvider like RevalidatingIdentityAuthenticationStateProviderif you need.
  2. If you're using ASP.NET Core Identity, there's a generated RevalidatingIdentityAuthenticationStateProvider for you.
  3. If you want to use ASP.NET Core Identity + MongoDB, follow official docs to implement such a feature.
  4. If you want to use ASP.NET Core Identity + Blazor Server Side, and use SignIn(...) to send cookies, don't do this directly. See this thread for more details.
like image 124
itminus Avatar answered Oct 19 '22 01:10

itminus