Serve auth-protected SPA on "/" using BFF with a custom login screen before using azure auth

Viewed 296

I realize the question sounds rather long and complicated! I'll try my best to explain. I have a ASP.NET Core 5 application which is a BFF (backend for frontend) for my Angular application. You MUST log in using Azure AD before you can access the SPA. The result of that is saved in a cookie, so we have 2 auth schemes (Azure AD and cookie). Resources like images are accessible without being authenticated.

The problem is that I have a HomeController that has an Index() action which has an [AuthorizeForScopes] and [Authorize] attribute that check if the user is logged in and authorized the scopes. If not, the AuthorizeForScopes will bring them to the Microsoft Login Page. If they are logged in, the method does a redirect to /home where the SPA runs.

This setup has some issues:

  • I want the SPA to run on /
    • This can't be done because the HomeController.Index() method runs there.
    • Using something like return PhysicalFile(..) is not a great solution; during development that path is different because of the webpack proxy etc... and on production the SPA isn't loaded correctly for some reason.
  • If the user is not logged in, I want to show a server side rendered login screen which acts as the "introduction page" of the app. It shows a "Log in with MS" button which should then trigger the MS login.
    • This doesn't work because the [Authorize] and [AuthorizeForScopes] tags immediately bring the user to the MS login screen.

I can't really accomplish both nicely. Here is some code to explain why:

Homecontroller

[Route("/")]
public class HomeController : Controller
{
    [HttpGet]
    [Authorize]
    [AuthorizeForScopes(ScopeKeySection = "Scopes")]
    public IActionResult Index()
    {
        // We can't run the SPA on "/" because we need to intercept that here to require that the user is authorized before accessing the SPA.
        return Redirect("/home");
    }
}

important middleware

// This is done AFTER useStaticFiles(), useEndpoints() and useSpaStaticFiles()

// Redirect the user to authenticate if the user isnt at this moment
// All static frontend related files are served using the UseSpaStaticFiles middleware
// What's left is the index.html
app.Use(async (context, next) =>
{
    if (context.User?.Identity?.IsAuthenticated != true)
    {
        await context.ChallengeAsync(WellKnownAuthenticationSchemes.OpenIdConnect);
    }
    else
    {
        await next();
    }
});


Auth

// Some other auth config is left out; it's not important.

services.Configure<CookieAuthenticationOptions>("Cookie", opt =>
{
    opt.Cookie.IsEssential = true;
    opt.Cookie.Name = "auth";
    opt.Cookie.SameSite = SameSiteMode.Lax; // Required due to cross origin nature of signin-oidc callback
    opt.Cookie.SecurePolicy = Environment.IsDevelopment()
        ? CookieSecurePolicy.SameAsRequest
        : CookieSecurePolicy.Always;
    opt.Cookie.HttpOnly = true;
    opt.SlidingExpiration = true;
    opt.ExpireTimeSpan = TimeSpan.FromDays(90);
});


services.AddAuthentication("Cookie");

services
    .AddMicrosoftIdentityWebAppAuthentication(Configuration,
        cookieScheme: "Cookie")
    .EnableTokenAcquisitionToCallDownstreamApi(Configuration.GetValue<string>("Scopes").Split(" "))
    .AddDistributedTokenCaches();


So, to summarize:

  • I want to show a custom login screen if the user is not logged in/has not authorized scopes which would probably run on /login and then pressing the button on that page runs that [Authorize] and [AuthorizeForScopes] logic that is currently on Index()
  • I want the SPA to run on / and only be accessible if the user is logged in and has authorized the scopes.

I would very much appreciate the help!

0 Answers
Related