Blazor Server Identity (BSI) is about utilizing MS Identity in Blazor applications that are hosted on the Server.
Not a combination of Server and Web Assembly (WASM).
There are many different ways of implementing Identity, depending on the Use Case:
These are just some of the possible use cases that influence the choice of implmentation.
SUSISO are the three core functions that any Identity solution must perform to be successful.
There are several mitigating factors that can hinder an implementation's choice. Some of these are:
Blazor is different from other Web technologies, because of its reliance on Signal/R.
An analogy I like to use is the difference between snail mail and the telephone: Both do their jobs exceedingly well, but their inherent differences means a solution for one doesn't carry over to the other.
Snail mail has postmarks, the to and from address. The telephone has numbers and caller id. Some of these differences can become obstacles to any BSI implementation.
SignInManager.Context
which would contain HttpContext. This is especially true when the browser is not running on the server, Context=null.This is not an easy problem to solve, on the low end. On the high end, there are ootb solutions, and a company can throw lots of resources to deploy an implemtation. But at the low end, the individual or small shop building web apps for small businesses, and such, this is a pretty daunting challenge. As Developers, the desire is to have a similar ootb experience as one gets with Asp.Net Core Identity. But it is not there. Because the technologies are fundamentally different. This is the situation I found myself in. That's why I went looking for guidance and solutions, and found myself slashing through the jungle. Not to worry, it is a jungle I've slashed through many times before.
Some of the possible implementations for BSI are:
ClaimsPrincipal
can be directly injected into Identity.
There is no UI/UX issue, because it is 100% Blazor.
Code is only added to the project, no changes. The additions are just to 3 files, so it is not complex.
If templates were available, a developer would be up and running in minutes. It scales, it is secure, easily customized.OnPostAsync()
method using Javascript.
OnPostAsync()
is in Razor space, so no different than calling MVC/Razor pages, just bypassing the markup. This meant the UI/UX was all Blazor.
No API to build. No 3rd party solution to integrate with.It works, and very satisfactorily. However, several reviewers did not like the idea of inserting an AF token at the beginning of a circuit, and the reliance
on a dynamically generated form to be posted to Login.OnPostAsync()
. But it works.
MVC/Razor pages are the approved guidance for Idenity with Blazor. (https://docs.microsoft.com/en-us/aspnet/core/security/blazor/?view=aspnetcore-3.1) Using Login Razor page, it first makes a call to OnGet, which returns the Login form, with the AF token in a hidden field. With some small changes to Startup.cs, the token will be added to a header by the Middleware. So I took a new approach:
OnValidSubmit()
after checking validity of credentialsLogin.OnGet()
and get the form, with the AF token attached.Login.OnPostAsync()
User credentials are validated.NavigationManager.NavigateTo("/", true)
true
setting If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.
Of course you can download the Zip file. Here are the steps to replicate:
andNext
and
Create
Blazor Server App
Change
Change Authentication
Dialog, select
OK
Create
button.Hello World
page should be displayedRegister
and a Log In
Menu items.
test01@email
Apply Migrations
button will apear. Click on it.
*
Continue of the
Confirm Form Resubmisson` dialogRegister confirmation
screen should appear.
*
- Where it says
Click hee to confirm your account
go ahead and do so.
Confirm Email
page will appear
Click on
Login
Login
to the application.Log out
Log out
and stop the applicationThe Anti-Forgery system needs to added defining the name of the Form Token and Header Token.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
ConfigureServices()
add services.AddAntiforgery(options => { options.HeaderName = "X-CSRF-TOKEN-HEADER"; options.FormFieldName = "X-CSRF-TOKEN-FORM"; });
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAntiforgery(options => { options.HeaderName = "X-CSRF-TOKEN-HEADER"; options.FormFieldName = "X-CSRF-TOKEN-FORM"; });**
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
services.AddSingleton<WeatherForecastService>();
}
Configure()
to the following public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
app.User()
block of code above app.UseHttpsRedirection()
app.Use(next => context =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
return next(context);
});
app.UseHttpsRedirection();
Borrowing 1 method from Oqtane.
window.interop = {
setCookie: function (name, value, days) {
var d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
},
};
Borrowing the matching wrapper
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.JSInterop;
//https://www.oqtane.org/Resources/Blog/PostId/527/exploring-authentication-in-blazor
namespace BlazorServerIdentityInterop.Shared
{
public class Interop
{
private readonly IJSRuntime _jsRuntime;
public Interop(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public Task SetCookie(string name, string value, int days)
{
try
{
_jsRuntime.InvokeAsync<string>(
"interop.setCookie",
name, value, days);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
}
}
blazor.server.js
<script src="https://github.com/bdnts/BlazorServerIdentityInterop/raw/master/_framework/blazor.server.js"></script>
<script src="https://github.com/bdnts/BlazorServerIdentityInterop/raw/master/~/js/Interop.js"></script>
I use the RestSharp package for simplicity and clarity of code.
Retrieve this module from the GitHub repository
Hello World
shouldn't be any different this time.
Add SignIn
to the Url:
https://localhost:44339/SignIn
SignIn
page
SignIn
page to sign in the previously created user.
Hello World
should redisplay, but the user name should appear at the top, the same as when using LoginGoing to add some frills to the UI, like menu items, some test authentication, and SignUp and SignOut
Replace the existing code with the following:
<AuthorizeView>
<NotAuthorized>
<NavLink class="nav-link" href="https://github.com/bdnts/BlazorServerIdentityInterop/blob/master/SignUp" Match="NavLinkMatch.All">
<span class="oi oi-person" aria-hidden="true"></span> Sign Up
</NavLink>
<NavLink class="nav-link" href="https://github.com/bdnts/BlazorServerIdentityInterop/blob/master/SignIn" Match="NavLinkMatch.All">
<span class="oi oi-account-login" aria-hidden="true"></span> Sign In
</NavLink>
<a style="color:red" href="https://github.com/bdnts/BlazorServerIdentityInterop/blob/master/Identity/Account/Register">Register</a>
<a style="color:red" href="https://github.com/bdnts/BlazorServerIdentityInterop/blob/master/Identity/Account/Login">Log in</a>
</NotAuthorized>
<Authorized>
<a href="https://github.com/bdnts/BlazorServerIdentityInterop/blob/master/Identity/Account/Manage">Hello, @context.User.Identity.Name!</a>
<NavLink class="nav-link" href="https://github.com/bdnts/BlazorServerIdentityInterop/blob/master/SignOut">
<span class="oi oi-account-logout" aria-hidden="true"></span> Sign Out
</NavLink>
<form method="post" action="Identity/Account/LogOut">
<button type="submit" style="color:red" class="nav-link btn btn-link ">Log out</button>
</form>
</Authorized>
</AuthorizeView>
<AuthorizeView>
<Authorized>
<h2>User @context.User.Identity.Name is authenticated</h2>
</Authorized>
<NotAuthorized>
<h5>No one authorized</h5>
</NotAuthorized>
</AuthorizeView>
Fetch from the GitHub repository and put in Areas/Identity/Pages/Account