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:
Design notes: https://gist.github.com/SteveSandersonMS/60ca3a5f70a7f42fba14981add7e7f79
Some thoughts from the community here: https://brockallen.com/2019/01/11/using-oauth-and-oidc-with-blazor/
While we await official guidance, I wanted to share a decent authentication pattern I've been using:
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));
}
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);
public IActionResult SignIn([FromRoute] string scheme)
{
scheme = scheme ?? AzureADB2CDefaults.AuthenticationScheme;
var redirectUrl = Url.Content("~/");
return Challenge(
new AuthenticationProperties { RedirectUri = redirectUrl },
scheme);
}
Startup.Configure()
, make sure you call UseAuthentication
and UseMvcWithDefaultRoute
at the minimum:app.UseAuthentication();
app.UseMvcWithDefaultRoute();
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
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.
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:
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:
And used like this: