Search code examples
c#.netauthenticationblazorclaims

Blazor server authentication


i want add authentication to my blazor server side application. I have this code to login.

var claims = new List<Claim>
{
    new Claim(type: ClaimTypes.NameIdentifier, user.Username),
    new Claim(type: ClaimTypes.Name, user.Name ?? user.Username),
    new Claim(type: ClaimTypes.Sid, user.ID.ToString())
};
if (user.Email != null)
    claims.Add(new Claim(type: ClaimTypes.Email, user.Email));

if (user.UserRoles != null)
{
    foreach (var userRole in user.UserRoles)
    {
        claims.Add(new Claim(type: ClaimTypes.Role, userRole.ID_Role));
    }
}

var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

var authProperties = new AuthenticationProperties
{
    AllowRefresh = _authModel.Value.AllowRefresh,
    ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(_authModel.Value.LoginExpirationMinutes),
    IsPersistent = input.IsPersistent
};

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

Everything looks good. The code normaly runs. But when i watch to the HttpContext.User or AuthenticationStateProvider it's empty. In explorer i don't see cookie. What i do bad? Thank you very much


Solution

  • Is the authentication internal to your Blazor app or are you doing any API access?

    Normally you hand off to an external to the SPA authentication provider who then calls back to your SPA with the HttpContext.User set. The default AuthenticationStateProvider then reads the user data from HttpContext.User.

    You are authenticating internally within the SPA. The startup page is loaded and HttpContext.User already set. I'm not sure that you can reset it. You can write a custom AuthenticationStateProvider. Take a look at my answer to question Custom AuthenticationStateProvider in blazor project doesn't work on server side which shows how to build a test AuthenticationStateProvider. You should be able to plug this together with your code.

    Update

    Here's a very dumb AuthenticationStateProvider that provides the authentication for two fixed users. Call ChangeIdentity from a button to switch. You don't need to try and do anything with the HttpContext.

    using Microsoft.AspNetCore.Components.Authorization;
    using System.Security.Claims;
    using System.Threading.Tasks;
    
    namespace Blazor.Auth.Test
    {
        public class DumbAuthenticationStateProvider : AuthenticationStateProvider
        {
            public static ClaimsPrincipal User
                => new ClaimsPrincipal(new ClaimsIdentity(UserClaims, "Dumb Auth Type"));
    
            public static Claim[] UserClaims
                => new[]{
                        new Claim(ClaimTypes.Sid, "024672e0-250a-46fc-bd35-1902974cf9e1"),
                        new Claim(ClaimTypes.Name, "Normal User"),
                        new Claim(ClaimTypes.NameIdentifier, "Normal User"),
                        new Claim(ClaimTypes.Email, "user@user,com"),
                        new Claim(ClaimTypes.Role, "User")
                };
            public static ClaimsPrincipal Visitor
                => new ClaimsPrincipal(new ClaimsIdentity(VisitorClaims, "Dumb Auth Type"));
    
            public static Claim[] VisitorClaims
                => new[]{
                        new Claim(ClaimTypes.Sid, "324672e0-250a-46fc-bd35-1902974cf9e1"),
                        new Claim(ClaimTypes.Name, "Visitor"),
                        new Claim(ClaimTypes.NameIdentifier, "Normal Visitor"),
                        new Claim(ClaimTypes.Email, "visitor@user,com"),
                        new Claim(ClaimTypes.Role, "Visitor")
                };
    
            bool _switch;
    
            public override Task<AuthenticationState> GetAuthenticationStateAsync()
                => Task.FromResult(_switch 
                    ? new AuthenticationState(User)
                    : new AuthenticationState(Visitor)
                    );
    
            public Task<AuthenticationState> ChangeIdentity()
            {
                _switch = !_switch;
                var task = this.GetAuthenticationStateAsync();
                this.NotifyAuthenticationStateChanged(task);
                return task;
            }
    
        }
    }
    

    And the services setup in StartUp:

    services.AddScoped<AuthenticationStateProvider, DumbAuthenticationStateProvider>();
    services.AddAuthorizationCore();
    
       <button class="btn btn-dark" @onclick="ChangeID">Switch</button>
    
    @code {
    
       [Inject] private AuthenticationStateProvider authenticationStateProvider {get; set;}
    
        private DumbAuthenticationStateProvider myAuthenticationStateProvider  => authenticationStateProvider as DumbAuthenticationStateProvider;
    
        private async Task ChangeID()
        {
            await myAuthenticationStateProvider.ChangeIdentity();
        }
    }
     
    

    Second Update based on GitHub copy of app.

    First you need to ensure the new AuthenticationStateProvider is loaded after ServerSideBlazor (it loads ServerAuthenticationSateProvider which overloads whatever has been loaded before) and clean up StartUp:

            {
                //services.AddAuthentication(
                //    CookieAuthenticationDefaults.AuthenticationScheme)
                //    .AddCookie();
                // services.AddAuthorization();
    
                //services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<DumbAuthenticationStateProvider>());
    
                services.AddRazorPages();
                services.AddServerSideBlazor();
                services.AddScoped<AuthenticationStateProvider, DumbAuthenticationStateProvider>();
                services.AddAuthorizationCore();
                services.AddSingleton<WeatherForecastService>();
            }
    
    

    Update App.razor - change out RouteView for AuthorizeRouteView.

    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
    

    My Test Page

    @page "/fetchdata"
    @using Microsoft.AspNetCore.Components.Authorization
    
    @using WebApplication6.Data
    <AuthorizeView>
        <Authorized>
            <div class="m-2 p-2">
                <a href="Identity/Account/Manage">Hello, @context.User.Identity.Name!</a>
            </div>
        </Authorized>
    </AuthorizeView>
    
    
    <button class="btn btn-dark" @onclick="ChangeID">Switch</button>
    
    @code {
    
        [Inject] private AuthenticationStateProvider authenticationStateProvider { get; set; }
    
        private DumbAuthenticationStateProvider myAuthenticationStateProvider => authenticationStateProvider as DumbAuthenticationStateProvider;
    
        private async Task ChangeID()
        {
            await myAuthenticationStateProvider.ChangeIdentity();
        }
    }