Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement: Authentication and authorization for Blazor #4048

Closed
danroth27 opened this issue Nov 16, 2018 · 26 comments
Closed

Implement: Authentication and authorization for Blazor #4048

danroth27 opened this issue Nov 16, 2018 · 26 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components Components Big Rock This issue tracks a big effort which can span multiple issues Done This issue has been fixed enhancement This issue represents an ask for new feature or an enhancement to an existing one Needs: Design This issue requires design work before implementating.

Comments

@danroth27
Copy link
Member

danroth27 commented Nov 16, 2018

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

@danroth27 danroth27 added enhancement This issue represents an ask for new feature or an enhancement to an existing one area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels Nov 16, 2018
@danroth27 danroth27 added this to the 3.0.0-preview2 milestone Nov 16, 2018
@mkArtakMSFT mkArtakMSFT added the Needs: Design This issue requires design work before implementating. label Nov 28, 2018
@danroth27 danroth27 assigned javiercn and unassigned rynowak Jan 25, 2019
@danroth27
Copy link
Member Author

Some thoughts from the community here: https://brockallen.com/2019/01/11/using-oauth-and-oidc-with-blazor/

@josephayoung
Copy link

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.

@josephayoung
Copy link

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>

@andriysavin
Copy link

@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).

@shawty
Copy link

shawty commented Feb 28, 2019

@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.

@rynowak rynowak added the Components Big Rock This issue tracks a big effort which can span multiple issues label Mar 4, 2019
@rynowak rynowak mentioned this issue Mar 4, 2019
56 tasks
@robertmclaws
Copy link

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!

@conficient
Copy link
Contributor

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.

@SteveSandersonMS
Copy link
Member

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.

@conficient
Copy link
Contributor

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

@jrobertshawe
Copy link

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?

@conficient
Copy link
Contributor

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.

@juho-hanhimaki
Copy link
Contributor

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?

@mkArtakMSFT
Copy link
Member

The remaining work is going to be handled as part of #10698

@AlaaNassar
Copy link

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 ??

@mkArtakMSFT
Copy link
Member

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

@dotnet dotnet locked as resolved and limited conversation to collaborators Aug 14, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components Components Big Rock This issue tracks a big effort which can span multiple issues Done This issue has been fixed enhancement This issue represents an ask for new feature or an enhancement to an existing one Needs: Design This issue requires design work before implementating.
Projects
None yet
Development

No branches or pull requests