Check Authorization Rules Programatically in ASP.NET Blazor

_Imports.razor

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization

Pages/ProceduralLogic.razor:

@page "/procedural-logic"
@inject IAuthorizationService AuthorizationService

<h1>Procedural Logic Example</h1>

<button @onclick="@DoSomething">Do something important</button>

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    private async Task DoSomething()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user is not null)
            {
                if (user.Identity is not null && user.Identity.IsAuthenticated)
                {
                    // ...
                }

                if (user.IsInRole("Admin"))
                {
                    // ...
                }

                if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))
                    .Succeeded)
                {
                    // ...
                }
            }
        }
    }
}

References
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-6.0#procedural-logic

Create Admin Account on ASP.NET Blazor Startup

public class StartupWorker: BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private static readonly string[] Roles = { "Admin", "Manager", "Member" };
    
    public StartupWorker(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await CreateRoles();
        await CreateAdmin();
    }

    private async Task CreateRoles()
    {
        using var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
        var roleManager = serviceScope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
        foreach (var role in Roles)
        {
            if (!await roleManager.RoleExistsAsync(role))
            {
                await roleManager.CreateAsync(new IdentityRole(role));
            }
        }
    }

    private async Task CreateAdmin()
    {
        using var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
        var userManager = serviceScope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
        var user = await userManager.FindByNameAsync("admin");

        if (user == null)
        {
            var identity = new ApplicationUser("admin") { FirstName = "Admin", LastName = "" };
            var password = "12345";
            await userManager.CreateAsync(identity, password);
            await userManager.AddToRoleAsync(identity, "Admin");
        }
    }
}

References
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/secure-data?view=aspnetcore-7.0

Add Role services to Identity in ASP.NET Blazor

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

References
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/secure-data?view=aspnetcore-7.0
https://stackoverflow.com/questions/52522248/store-does-not-implement-iuserrolestoretuser-asp-net-core-identity

Configure Send Endpoint in MassTransit

public record SubmitOrder
{
    public string OrderId { get; init; }
}

public async Task SendOrder(ISendEndpointProvider sendEndpointProvider)
{
    var endpoint = await sendEndpointProvider.GetSendEndpoint(_serviceAddress);

    await endpoint.Send(new SubmitOrder { OrderId = "123" });
}

Endpoint Address

rabbitmq://localhost/input-queue
rabbitmq://localhost/input-queue?durable=false

Short Addresses

GetSendEndpoint(new Uri("queue:input-queue"))

Address Conventions

Using send endpoints might seem too verbose, because before sending any message, you need to get the send endpoint and to do that you need to have an endpoint address. Usually, addresses are kept in the configuration and accessing the configuration from all over the application is not a good practice.

Endpoint conventions solve this issue by allowing you to configure the mapping between message types and endpoint addresses. A potential downside here that you will not be able to send messages of the same type to different endpoints by using conventions. If you need to do this, keep using the GetSendEndpoint method.

EndpointConvention.Map<SubmitOrder>(new Uri("rabbitmq://mq.acme.com/order/order_processing"));
public async Task Post(SubmitOrderRequest request)
{
    if (AllGoodWith(request))
        await _bus.Send(ConvertToCommand(request));
}

Also, from inside the consumer, you can do the same using the ConsumeContext.Send overload:

EndpointConvention.Map<StartDelivery>(new Uri(ConfigurationManager.AppSettings["deliveryServiceQueue"]));

The EndpointConvention.Map<T> method is static, so it can be called from everywhere. It is important to remember that you cannot configure conventions for the same message twice. If you try to do this – the Map method will throw an exception. This is also important when writing tests, so you need to configure the conventions at the same time as you configure your test bus (harness).

It is better to configure send conventions before you start the bus.

References
https://masstransit.io/documentation/concepts/producers#send-endpoint
https://stackoverflow.com/questions/62713786/masstransit-endpointconvention-azure-service-bus/

Configure Receive Endpoints in MassTransit

Explicitly Configure Endpoints

services.AddMassTransit(x =>
{
    x.AddConsumer<SubmitOrderConsumer>();
    
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.ReceiveEndpoint("order-service", e =>
        {
            e.ConfigureConsumer<SubmitOrderConsumer>(context);
        });
    });
});

Temporary Endpoints

Some consumers only need to receive messages while connected, and any messages published while disconnected should be discarded. This can be achieved by using a TemporaryEndpointDefinition to configure the receive endpoint.

services.AddMassTransit(x =>
{
    x.AddConsumer<SubmitOrderConsumer>();

    x.UsingInMemory((context, cfg) =>
    {
        cfg.ReceiveEndpoint(new TemporaryEndpointDefinition(), e =>
        {
            e.ConfigureConsumer<SubmitOrderConsumer>(context);
        });

        cfg.ConfigureEndpoints(context);
    });
});

Endpoint Configuration

services.AddMassTransit(x =>
{
    x.AddConsumer<SubmitOrderConsumer>(typeof(SubmitOrderConsumerDefinition))
        .Endpoint(e =>
        {
            // override the default endpoint name
            e.Name = "order-service-extreme";

            // specify the endpoint as temporary (may be non-durable, auto-delete, etc.)
            e.Temporary = false;

            // specify an optional concurrent message limit for the consumer
            e.ConcurrentMessageLimit = 8;

            // only use if needed, a sensible default is provided, and a reasonable
            // value is automatically calculated based upon ConcurrentMessageLimit if
            // the transport supports it.
            e.PrefetchCount = 16;

            // set if each service instance should have its own endpoint for the consumer
            // so that messages fan out to each instance.
            e.InstanceId = "something-unique";
        });

    x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context));
});

References
https://masstransit.io/documentation/configuration#receive-endpoints

Detect Online Users using Circuit Handler in ASP.NET Blazor Server

public class UserCircuitHandler : CircuitHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserCircuitHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public override async Task OnConnectionUpAsync(Circuit circuit,
        CancellationToken cancellationToken)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        var userAgent = httpContext.Request.Headers["User-Agent"];
        string uaString = userAgent.ToString();
        var uaParser = Parser.GetDefault();
        ClientInfo c = uaParser.Parse(uaString);
        Console.WriteLine(httpContext.User.Identity.Name);
    }

    public override async Task OnConnectionDownAsync(Circuit circuit,
        CancellationToken cancellationToken)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        var userAgent = httpContext.Request.Headers["User-Agent"];
        string uaString = userAgent.ToString();
        var uaParser = Parser.GetDefault();
        ClientInfo c = uaParser.Parse(uaString);
        Console.WriteLine(httpContext.User.Identity.Name);
    }
}

References
https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/signalr?view=aspnetcore-7.0#blazor-server-circuit-handler
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional-scenarios?view=aspnetcore-7.0#circuit-handler-to-capture-users-for-custom-services

Apply Authorization to All Pages in Blazor

Program.cs

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

builder.Services.ConfigureApplicationCookie(options =>
{
    options.ExpireTimeSpan=TimeSpan.FromDays(14);
    options.SlidingExpiration = true;
    options.Cookie.Name = "BI";
    options.Cookie.HttpOnly = true;
    options.LoginPath = "/Login";
    options.LogoutPath="/Logout";
});

To apply authorization to all pages in Blazor, you have to add:

@attribute [Microsoft.AspNetCore.Authorization.Authorize]

…to your _Imports.razor file.

@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]

…on pages that don’t require authorization.

References
https://stackoverflow.com/questions/60840986/blazor-redirect-to-login-if-user-is-not-authenticated
https://stackoverflow.com/questions/71434131/blazor-allow-anonymous-for-razor-page
https://stackoverflow.com/questions/50633896/asp-net-core-identity-change-login-url
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/secure-data?view=aspnetcore-7.0