Search code examples
asp.net-coreblazorauthorizationblazor-webassembly

Authorize attribute works in client but not in controller


I have a Blazor web app (.NET 8) with interactive render mode WebAssembly, and I use https://github.com/IUCrimson/AspNet.Security.CAS for authentication. It's mostly working great, and I can see my claims on the client and server. However, authorization only works for Razor pages. The authorize attribute on my controller is ignored. Is there some step I am missing in my setup?

Program.cs

using AspNetCore.Security.CAS;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.FluentUI.AspNetCore.Components;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

ConfigurationManager configuration = builder.Configuration;
IWebHostEnvironment environment = builder.Environment;

builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents();

builder.Services.AddScoped<AuthenticationStateProvider, PersistingServerAuthenticationStateProvider>();

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options => {
        options.AccessDeniedPath = "/AccessDenied";
        options.Cookie.SameSite = SameSiteMode.Strict;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.ExpireTimeSpan = TimeSpan.FromHours(1); // set to match CAS timeout
        options.LoginPath = new PathString("/api/login");
        options.SlidingExpiration = false;
        options.Events = new CookieAuthenticationEvents {
            OnSigningIn = context => {
                var _dal = new DatabaseService(configuration);
                var principal = context.Principal;
                ClaimsIdentity identity = (ClaimsIdentity)context.Principal.Identity;
                var claims = new List<Claim> {
                    new(ClaimTypes.Role, "ADMIN")
                };

                identity.AddClaims(claims);
                return Task.FromResult(0);
            }
        };
    })
    .AddCAS(options => {
        options.CasServerUrlBase = "myCasServer";
        options.ServiceForceHTTPS = true;
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    });

builder.Services.AddHttpClient();
builder.Services.AddSingleton<DatabaseService, DatabaseService>();
builder.Services.AddControllers();

builder.Services.AddFluentUIComponents();

var app = builder.Build();

if (app.Environment.IsDevelopment()) {
    app.UseWebAssemblyDebugging();
} else {
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(OnlinePab.Client._Imports).Assembly);

app.MapControllers();

app.Run();

Controller

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Boo.Controllers {

    [Authorize(Roles = "NOBODY")]
    [Route("api")]
    [ApiController]
    public class MyController() : ControllerBase {

        [AllowAnonymous]
        [Route("login")]
        public async Task Login(string returnUrl) {
            var props = new AuthenticationProperties { RedirectUri = returnUrl };
            await HttpContext.ChallengeAsync("CAS", props);
        }

        [Route("boo")]
        public ContentResult NotAllowed() {
            return base.Content("<div>Boo!</div>", "text/html");
        }
    }
}

Solution

  • Trial and error led me to add app.UseRouting() in Program.cs, which has fixed the issue. I'd love to hear a technical reason as to why this works, given that routing was working fine without it, but authorization was not.

    var app = builder.Build();
    
    app.UseRouting(); // Fix authorization for controller
    
    if (app.Environment.IsDevelopment()) {
        app.UseWebAssemblyDebugging();
    } else {
        app.UseExceptionHandler("/Error", createScopeForErrors: true);
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }