bdnts / BlazorServerIdentityInterop

Blazor Server Identity via Interop
9 stars 3 forks source link

BlazorServerIdentityInterop

Blazor Server Identity

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

Use Cases

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.

Sign Up, Sign In, Sign Out (SUSISO)

SUSISO are the three core functions that any Identity solution must perform to be successful.

Constraints

There are several mitigating factors that can hinder an implementation's choice. Some of these are:

Technical Constraints

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.

Commentary

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.

Implementations

Some of the possible implementations for BSI are:

BlazorServerIdentityInterop 1.0

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.

BlazorServerIdentityInterop 2.0

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:

Replicate

Of course you can download the Zip file. Here are the steps to replicate:

Create a blank project with Identity.

Source Code Modifications

Startup.cs

The Anti-Forgery system needs to added defining the name of the Form Token and Header Token.

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
            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>();
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
            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();

interop.js

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=/";
    },
};

Interop.cs

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;
            }
        }
    }
}

_Pages/Host.cshtml

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

RestSharp package

I use the RestSharp package for simplicity and clarity of code.

SignIn.razor

Retrieve this module from the GitHub repository

Compile and run the app

Hello World shouldn't be any different this time. Add SignIn to the Url: https://localhost:44339/SignIn

Enhance

Going to add some frills to the UI, like menu items, some test authentication, and SignUp and SignOut

LoginDisplay.razor

Index.razor

SignUp.razor & SignOut.razor

Fetch from the GitHub repository and put in Areas/Identity/Pages/Account

Compile and Test