Search code examples
componentsauthorizationblazorblazor-server-side

Adding extra authorization layer in Blazor


I have an AuthorizeView component based on authentication through an OpenID Connect provider, but I want to add an extra layer based on some informations found about the user in the associated database after OIDC authentication. I am wondering whether the logic illustrated below,

1: Would be the smartest way of implementing this?

2: And if so, how to approach the task of building my own authorization view inside an already existing one?

<AuthorizeView>
   <Authorized>                
     <ExtraLayerAuthorized>                
         <p>Authorized through OIDC and extra layer</p>
     </ExtraLayerAuthorized>
     <NotExtraLayerAuthorized>
        <p>Authorized through OIDC, but NOT extra layer</p>
     </NotExtraLayerAuthorized>
     </Authorized>
    <NotAuthorized>
      <p>No authorization</p>
    </NotAuthorized>
</AuthorizeView>`

Solution

  • Note sure if the is Server or WASM.

    One good way to do this is just to add an additional ClaimsIdentity to the existing ClaimsPrincipal of the standard AuthenticationStateProvider.

    Here's how to do it in Server. See the comments for an explanation of what it does.

    public class MyAuthenticationStxateProvider : ServerAuthenticationStateProvider
    {
        public async override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            // Call the base to get the AuthState and the user provided in the Security Headers by the server
            var authstate = await base.GetAuthenticationStateAsync();
            var user = authstate.User;
    
            if (user?.Identity?.IsAuthenticated ?? false)
            {
                // Do whatever you want here to retrieve the additional user information you want to
                // include in the ClaimsPrincipal - probably some form of Identity Service
                    
                // Construct a ClaimsIdentity instance to attach to the ClaimsPrincipal
                // I just added a role as an example
                var myIdentity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "User") });
                // Add it to the existing ClaimsPrincipal
                user.AddIdentity(myIdentity);
            }
    
            // construct a new state with the updated ClaimsPrincipal
            // - or an empty one of you didn't get a user in the first place
            // All the Authorization components and classes will now use this ClaimsPrincipal 
            return new AuthenticationState(user ?? new ClaimsPrincipal());
        }
    }
    

    This is setup to use Auth0 as the authentication provider.

      "AllowedHosts": "*",
      "Auth0": {
        "Domain": "xxxxx.eu.auth0.com",
        "ClientId": "xxxxxxxxxxxxxxxxxxxxxx",
      }
    

    The service registration looks something like this:

    builder.Services.AddRazorPages();
    builder.Services.AddServerSideBlazor();
    builder.Services.AddSingleton<WeatherForecastService>();
    builder.Services
        .AddAuth0WebAppAuthentication(options => {
            options.Domain = builder.Configuration["Auth0:Domain"];
            options.ClientId = builder.Configuration["Auth0:ClientId"];
        });
    
    builder.Services.AddAuthorization();
    builder.Services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
    

    Login.cshtml.cs

    public class LoginModel : PageModel
    {
        public async Task OnGet(string redirectUri)
        {
            await HttpContext.ChallengeAsync("Auth0", new
                AuthenticationProperties
            { RedirectUri = redirectUri });
        }
    }
    

    Logout.cshtml.cs

    public class LogoutModel : PageModel
    {
        public async Task<IActionResult> OnGetAsync()
        {
            await HttpContext.SignOutAsync();
            return Redirect("/");
        }
    }
    

    And then this page demonstrates logging in and out and adding the second identity when we have a valid user.

    @page "/"
    @inject NavigationManager NavManager
    <PageTitle>Index</PageTitle>
    
    <h1>Hello, world!</h1>
    
    Welcome to your new app.
    
    <SurveyPrompt Title="How is Blazor working for you?" />
    
    <div>
        <button class="btn btn-danger" @onclick=this.LogOut>Log Out</button>
        <button class="btn btn-primary" @onclick=this.LogIn>Log In</button>
    </div>
    <h2>Claims</h2>
    
    <dl>
        @foreach (var claim in user.Claims)
        {
            <dt>@claim.Type</dt>
            <dd>@claim.Value</dd>
        }
    </dl>
    
    @code {
        private ClaimsPrincipal user = new ClaimsPrincipal();
    
        [CascadingParameter] private Task<AuthenticationState> authStateProvider { get; set; } = default!;
    
        protected override async Task OnInitializedAsync()
        {
            var authState = await authStateProvider;
            user = authState.User;
        }
    
        private void LogIn()
        {
            var returnUrl = NavManager.Uri;
            NavManager.NavigateTo($"login?redirectUri={returnUrl}", forceLoad: true);
        }
    
        private void LogOut()
        {
            var returnUrl = NavManager.Uri;
            NavManager.NavigateTo($"logout?redirectUri={returnUrl}", forceLoad: true);
        }
    }