Aspnetcore: Implement: Authentication and authorization for Blazor

Created on 16 Nov 2018  路  26Comments  路  Source: dotnet/aspnetcore

Scope

This will be something similar to what the SPA templates have in terms of functionality.

We will try to simplify the amount of code and put it in libraries so it can be serviced.

Things to consider:

  • Login/registration/etc systems, 2FA, etc. - probably delegate to existing identity UI if at all
  • Simpler cookie-based auth system
  • Authorization on a per-component basis

    • Ability to control the auth logic

    • Ability to tie in with server-side (MVC) auth policies

Design notes: https://gist.github.com/SteveSandersonMS/60ca3a5f70a7f42fba14981add7e7f79

Components Big Rock Done Design area-blazor enhancement

Most helpful comment

And for authorization, what about an Authorize Razor Component that does the same kind of work as [Authorize]? Authentication certainly feels like a global injectable to me, but authorization feels like it should cascade.

Something like this:

@inject ClaimsPrincipal CurrentUser

@if (IsAuthorized())
{
    @ChildContent
}

@functions {
    [Parameter] string Roles { get; set; }
    [Parameter] RenderFragment ChildContent { get; set; }

    bool IsAuthorized()
    {
        if (!string.IsNullOrWhiteSpace(Roles))
        {
            var roles = Roles.Split(',').Select(x => x.Trim());
            foreach (var role in roles)
            {
                if (CurrentUser.IsInRole(role)) return true;
            }
            return false;
        }

        return CurrentUser.Identity.IsAuthenticated;
    }
}

And used like this:

            <Authorize Roles="Photographer, Customer">
                <div class="nav--center">
                    <a href="/Photographer" class="menu-item">Dashboard</a>
                    <a href="/Photographer/Cards" class="menu-item">Cards</a>
                </div>
            </Authorize>    
            <Authorize Roles="Customer">
                <div class="nav--right">
                    <cart-mini></cart-mini>
                </div>
            </Authorize>

All 26 comments

While we await official guidance, I wanted to share a decent authentication pattern I've been using:

  • In Startup.cs, configure authentication using the normal AuthenticationBuilder as though you're configuring a normal MVC app, with OpenIdConnect, Cookies, etc. (Here I'm using an Azure AD B2C package, but behind the scenes it's doing all the normal stuff):
public void ConfigureServices(IServiceCollection services)
{
            services.AddRazorComponents<App.Startup>();

            services.AddAuthentication(o => o.DefaultAuthenticateScheme = AzureADB2CDefaults.CookieScheme)
                .AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
}
  • Configure DI to inject both the HttpContextAccessor and the ClaimsPrincipal using that accessor (the latter for easier access in the components):
    services.AddHttpContextAccessor();
    services.AddScoped<ClaimsPrincipal>(context => context.GetRequiredService<IHttpContextAccessor>()?.HttpContext?.User);
  • In the server project, add a normal MVC controller that handles all of your sign-in/sign-out/etc. like you normally would, with methods like this:
public IActionResult SignIn([FromRoute] string scheme)
{
            scheme = scheme ?? AzureADB2CDefaults.AuthenticationScheme;
            var redirectUrl = Url.Content("~/");
            return Challenge(
                new AuthenticationProperties { RedirectUri = redirectUrl },
                scheme);
}
  • In Startup.Configure(), make sure you call UseAuthentication and UseMvcWithDefaultRoute at the minimum:
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
  • And then, anywhere you need the ClaimsPrincipal, you can just ask for it with a @inject ClaimsPrincipal CurrentUser. Here's an example using it in a nav bar:
@inject ClaimsPrincipal CurrentUser

    <nav class="nav">
        <a href="/" class="logo">
            <img src="/images/logo.png" />
        </a>

        @if (CurrentUser.Identity.IsAuthenticated == false)
        {
            <div class="nav--right">
                <button onclick=@SignIn style="padding-right: 20px;">Log In</button>
                <!--If user is customer-->
                <cart-mini>
            </div>
        }
        else if (CurrentUser.IsInRole("Photographer"))
        {
            <div class="nav--center">
                <a href="/Photographer" class="menu-item">Dashboard</a>
                <a href="/Photographer/Cards" class="menu-item">Cards</a>
            </div>
            <div class="nav--right">
                <a href="/Photographer/SalesHistory"><img src="~/images/icons/alert.png" alt="notifications" /></a>
                <img src="@(CurrentUser.Avatar() ?? "~/images/avatar_512.png")" onclick=@ToggleDropdown class="profileImg" />
            </div>
      }

This pattern ended up feeling cleaner to me than the cascading parameter that the Blazor Pizza workshop uses (https://github.com/dotnet-presentations/blazor-workshop/tree/master/src/BlazingPizza.ComponentsLibrary/Authentication), mainly because I don't like the look of using [CascadingParameter] public User User { get; set; } everywhere. I'd rather be able to inject the principal (or a derivative of it) where I need it.

I also didn't like injecting IHttpContextAccessor directly like some people have done, because that feels too ASP.NET-y, whereas ClaimsPrincipal is available in the client app without having to reference Microsoft.AspNetCore.Http.

And for authorization, what about an Authorize Razor Component that does the same kind of work as [Authorize]? Authentication certainly feels like a global injectable to me, but authorization feels like it should cascade.

Something like this:

@inject ClaimsPrincipal CurrentUser

@if (IsAuthorized())
{
    @ChildContent
}

@functions {
    [Parameter] string Roles { get; set; }
    [Parameter] RenderFragment ChildContent { get; set; }

    bool IsAuthorized()
    {
        if (!string.IsNullOrWhiteSpace(Roles))
        {
            var roles = Roles.Split(',').Select(x => x.Trim());
            foreach (var role in roles)
            {
                if (CurrentUser.IsInRole(role)) return true;
            }
            return false;
        }

        return CurrentUser.Identity.IsAuthenticated;
    }
}

And used like this:

            <Authorize Roles="Photographer, Customer">
                <div class="nav--center">
                    <a href="/Photographer" class="menu-item">Dashboard</a>
                    <a href="/Photographer/Cards" class="menu-item">Cards</a>
                </div>
            </Authorize>    
            <Authorize Roles="Customer">
                <div class="nav--right">
                    <cart-mini></cart-mini>
                </div>
            </Authorize>

@josephayoung regarding Authorize component - it's a great idea. I had implemented similar concept as a tag helper, and it worked perfectly for my needs (though it used policies instead of roles).

@josephayoung +1 for the ease of use we can get at the roles. Many SPA's these days have multi page scenarios like that these days, and being able to perform a simple role check in an if like that is an absolute bonus.

Having the identity available globally too is a boon too, that way all we need is a simple inject.

One thing that's also worth considering is attachment of roles to pages. It's all very well saying if(.. InRole("..") , but an interesting idea I borrowed from the AureliaJS frame work in all of my apps is the ability to attach the roles a page is allowed to have access it, in the router.

In aurelia for example, when you create an entry in the routing data structure in your app base, you generally have something like the following:

{ route: 'suppliers', moduleId: './pages/suppliers', nav: true, name: 'suppliers', auth: true, roles: ['admin', 'projectmanager'], title: 'Suppliers', icon: '', settings: "fa-star" },

Some stuff is mandatory, such as the route, the URL fragment, name etc but other stuff like "nav:true" and "auth:true", roles array etc are optional.

If the optional stuff exists, then the router actually does the auth check on behalf of the user, before the page route is even routed to, and in aurelias case, if a route doesn't match, an event is raised giving the app developer the chance to catch the role fail, and take remedial action.

I wanted to point out that I built a framework for authentication in BlazorEssentials. I have an AppStateBase that is injected into the app and handles dealing with the authentication cycle. Your app has a page that processes login hashes if you have to redirect elsewhere (like in Auth0), and you handle the authentication lifecycle events in AppStartup. I have this functioning properly (using Auth0) in a closed source app.

I'm going to be adjusting the app registration to use the IOptions API + Microsoft DI to make it a little more fluent. But I think this, combined with either the Razor Component, or an [Authorize] attribute on the ViewModel (which I also demonstrate in BlazorEssentials) would work well too.

Hope that helps!

@SteveSandersonMS should this be closed?

Yes, it's done.

Will it work in Blazor Client (WebAssembly)?

Yes, but we will only provide a built-in auth state provider for the hosted-on-ASP.NET Core webassembly case. For standalone you will need to implement your own auth state provider, eg by integrating with 3rd-party login.

@SteveSandersonMS - Steve, do you have a link to the Doc's yet?

Cheers
Shawty

If this is going to be closed, can you please create a new update to track the implemntation work (ideally with the design notes) and update - https://github.com/aspnet/AspNetCore/issues/8177 it has a pointer to this item.

Documentation?

Is there any opinion on how this would (or would not) apply to the router?

In MVC we can apply [Authorize] attributes to controllers to secure only specific routes, or make everything secure with a global filter then open up specific methods/controllers with an [AllowAnonymous] attribute - is anything like this planned?

Not sure if this is the best approach... I made my app secure by amending the MainLayout.razor file to inject the user and only execute @Body if the user is authenticated. If not, it shows a <Login /> component instead with the login form. This totally disables routing of course, but it works :)

That's pretty much how everything of mine works too.

A nice client-side Blazor auth example using Identity: https://github.com/stavroskasidis/BlazorWithIdentity

I'd be interested to know if there is a way to obtain the HttpContext or the ClaimsPrincipal in the client app for an _ASP.NET Core Hosted_ Blazor application. I've looked at @josephayoung code fragments but these won't work in Startup.cs on the client app, since it has no access to the Identity - only the .Server will have this.

The only way I can see at present is to implement a Web API on to return the user details to the client if it requests them.

There is no HttpContext in client-side Blazor. However as part of the auth work we are planning to make a ClaimsPrincipal available on the client.

Wow thanks for the fast response @SteveSandersonMS - is that in Preview6 ?

I am porting a React frontend application to Blazor and after reading this thread I will pause on the security side until it is supported. My current implementation in React is obtaining a JWT token and passing to Ocelot->API services using the ADAL library. Will Blazor support retreiving JWT tokens for the purpose of attaching to API calls?

Will Blazor support retreiving JWT tokens for the purpose of attaching to API calls?

No @jrobertshawe I don't think it will - Steve's notes on Auth state

if you want to get a JWT from your own server or some external server, we leave it up to you to do that and to implement a suitable IAuthenticationStateProvider.

You could look at other examples such as this or this.

I feel it's important for client side applications to be able to easily send API calls with bearer token for authentication. Azure AD is commonly used to secure ASP.NET Core APIs.

Have people tried using Azure AD (ADAL.JS library?) with Blazor to authenticate the API calls?

The remaining work is going to be handled as part of https://github.com/aspnet/AspNetCore/issues/10698

using System;
using System.IO.Compression;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json.Serialization;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using MedicalDivision.Server.Security;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace test.Server
{
    public class Startup
    {
        private X509Certificate2 Cert { get; }
        private IConfiguration Configuration { get; }
        private IWebHostEnvironment Env { get; }
        private ITokenProvider TokenProvider { get; }
        private PasswordHelper PasswordHelper { get; }
        private IHttpContextAccessor httpContextAccessor { get; }
        private IServiceProvider ServiceProvider { get; }

        public readonly string _myAllowSpecificOrigins = "_myAllowSpecificOrigins";

        public Startup(IConfiguration configuration,IWebHostEnvironment env,IServiceProvider serviceProvider)
        {
            ServiceProvider = serviceProvider;
            Configuration = configuration;
            Env = env;
            Cert = new X509Certificate2(Convert.FromBase64String(Configuration["Auth:Cert:Data"]),  Configuration["Auth:Cert:Password"], X509KeyStorageFlags.MachineKeySet);
            TokenProvider =new JwtTokenProvider(Cert, Configuration,env);
            PasswordHelper = new PasswordHelper();
            httpContextAccessor = ServiceProvider.GetService<IHttpContextAccessor>();
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton(serviceProvider => new AuthManager(Configuration,TokenProvider));


            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o =>
            {
                o.TokenValidationParameters = TokenProvider.GetValidationParameters();
                o.SecurityTokenValidators.Clear();
                o.SecurityTokenValidators.Add(new CustomTokenValidator(ServiceProvider));
            });
            services.AddAuthorization();
            services.AddResponseCompression(opts =>
            {
                opts.EnableForHttps = true;
                opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream", "image/png", "font/otf", "image/gif", "image/x-icon", "image/jpeg", "application/pdf", "image/svg+xml", "font/ttf", "font/woff", "font/woff2", "application/xml", "text/csv" });
            });
            services.Configure<GzipCompressionProviderOptions>(o =>
            {
                o.Level = CompressionLevel.Optimal;
            });
            services.AddCors(options =>
            {
                options.AddPolicy(_myAllowSpecificOrigins,
                    builder =>
                    {
                        builder
                            .AllowAnyOrigin() 
                            .AllowAnyMethod()
                            .AllowAnyHeader();
                            //.AllowCredentials();
                    });
            });
            services.AddMvc().AddNewtonsoftJson();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
             app.UseAuthentication();
            app.UseAuthorization(); 
            app.UseResponseCompression();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBlazorDebugging();
            }

            app.UseCors(_myAllowSpecificOrigins);
            app.UseStaticFiles();
            app.UseClientSideBlazorFiles<Client.Startup>();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
                endpoints.MapFallbackToClientSideBlazor<Client.Startup>("index.html");
            });
        }
    }
}

and the custom validation class

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace test.Server.Security
{
    public class CustomTokenValidator : ISecurityTokenValidator
    {
        private readonly JwtSecurityTokenHandler _tokenHandler;
        //private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly HttpContextAccessor _httpContextAccessor = new HttpContextAccessor();

        public CustomTokenValidator(IServiceProvider serviceProvider)
        {
            _tokenHandler = new JwtSecurityTokenHandler();
           // _httpContextAccessor = httpContextAccessor;
           // _httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();

        }

        public bool CanValidateToken => true;

        public int MaximumTokenSizeInBytes { get; set; } = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;

        public bool CanReadToken(string securityToken)
        {
            return _tokenHandler.CanReadToken(securityToken);
        }

        public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters,
            out SecurityToken validatedToken)
        {
            //How to access HttpContext/IP address from here?
            var httpContext = _httpContextAccessor.HttpContext;

            var xx = httpContext.Connection.RemoteIpAddress;
//simple condition for testing
            var yy = xx.ToString()=="::1";
            if (yy)
            { 
                validatedToken=null;
                return  null;
            }
            var principal = _tokenHandler.ValidateToken(securityToken, validationParameters, out validatedToken);

            return principal;
        }
    }
}

i use [Authorize] attribute on a controller function for api and it always display the data , these attribute not working, when i debug the identity user i found it not authenticated but it always send the json data while it should sent not authenticated response, any help to know why the authorize attribute not working ??

Hi.

It looks like you are posting on a closed issue!
We're very likely to lose track of your bug/feedback/question unless you:

  1. Open a new issue
  2. Explain very clearly what you need help with
  3. If you think you have found a bug, include detailed repro steps so that we can investigate the problem
Was this page helpful?
0 / 5 - 0 ratings