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 · 24 comments

Comments

@danroth27
Copy link
Member

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 this to the 3.0.0-preview2 milestone Nov 16, 2018

@danroth27 danroth27 added this to To do in Blazor via automation Nov 28, 2018

@danroth27 danroth27 moved this from To do to Design in Blazor Nov 28, 2018

@danroth27 danroth27 assigned javiercn and unassigned rynowak Jan 25, 2019

@danroth27

This comment has been minimized.

Copy link
Member Author

commented Jan 25, 2019

@josephayoung

This comment has been minimized.

Copy link

commented Feb 24, 2019

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

This comment has been minimized.

Copy link

commented Feb 24, 2019

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

This comment has been minimized.

Copy link

commented Feb 25, 2019

@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

This comment has been minimized.

Copy link

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.

@robertmclaws

This comment has been minimized.

Copy link

commented Mar 16, 2019

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!

@rynowak rynowak moved this from Design to In progress in Blazor Mar 16, 2019

@conficient

This comment has been minimized.

Copy link
Contributor

commented May 3, 2019

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

@danroth27 danroth27 moved this from To do to In progress in Blazor May 3, 2019

@danroth27 danroth27 changed the title Implement: Authentication and authorization for Razor Components Implement: Authentication and authorization for Blazor May 3, 2019

@conficient

This comment has been minimized.

Copy link
Contributor

commented May 6, 2019

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

This comment has been minimized.

Copy link
Member

commented May 6, 2019

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

This comment has been minimized.

Copy link
Contributor

commented May 6, 2019

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

@jrobertshawe

This comment has been minimized.

Copy link

commented May 23, 2019

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

This comment has been minimized.

Copy link
Contributor

commented May 23, 2019

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

This comment has been minimized.

Copy link

commented May 23, 2019

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

This comment has been minimized.

Copy link
Member

commented May 31, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.