Skip to content

Basic Authentication: Standalone login page for frontend-only deployments (closes #22144)#22168

Merged
AndyButland merged 14 commits intomainfrom
v17/improvement/22144-login-endpoints-front-end-only
Apr 11, 2026
Merged

Basic Authentication: Standalone login page for frontend-only deployments (closes #22144)#22168
AndyButland merged 14 commits intomainfrom
v17/improvement/22144-login-endpoints-front-end-only

Conversation

@AndyButland
Copy link
Copy Markdown
Contributor

@AndyButland AndyButland commented Mar 18, 2026

Summary

Addresses #22144 which reports that Umbraco configurations that a) disable the backoffice and b) want to use basic authentication for the front-end of the website and c) have 2FA or external login providers, are unable to do so. Basic authentication uses a backoffice account, and, for anything on than basic user name and password, the built-in browser login won't suffice. There needs to be HTML forms, and without the backoffice available, these don't exist.

This PR adds a standalone, server-rendered login page for basic authentication that works independently of the backoffice SPA.

I've reworked the registration extension methods by extracting AddBackOfficeSignIn() from AddBackOffice() so backoffice cookie authentication can be registered without the full backoffice (OpenIddict, Management API, SPA).

The server-rendered pages supports username/password login, two-factor authentication, and external login providers.

Behaviour change: standalone login for all basic auth

This PR changes the login redirect behaviour for all RedirectToLoginPage: true configurations, not just frontend-only deployments. Previously, basic auth redirected to the backoffice SPA login (/umbraco/). Now it always redirects to a lightweight server-rendered login page at /umbraco/basic-auth/login.

Rationale:

  • The standalone page is purpose-built for "authenticate and return to the frontend" — no SPA bundle, no OpenIddict token flow.
  • The backoffice SPA login is designed for entering the backoffice, which is a different intent.
  • Faster, lighter, and more predictable for end users visiting the frontend.

We don't have to make this change of course, but now we have this dedicated login flow, it seems to make more sense to use it than the backoffice.

New public API: AddBackOfficeSignIn()

For frontend-only deployments that need basic auth with backoffice credentials but no backoffice UI:

builder.CreateUmbracoBuilder()
    .AddCore()
    .AddBackOfficeSignIn()   // Identity + cookie auth, no OpenIddict/SPA
    .AddWebsite()
    .AddComposers()
    .Build();

Three deployment tiers are now supported:

Setup Basic Auth Behavior
.AddCore().AddWebsite() No backoffice auth — middleware returns 401 gracefully
.AddCore().AddBackOfficeSignIn().AddWebsite() Standalone login page with cookie auth
.AddBackOffice().AddWebsite() Full backoffice + standalone login page for basic auth

All also support and/or for .AddDeliveryApi(). For full details, see umbraco/UmbracoDocs#7861 and #21630.

Login flow

Username/password

  1. User visits a protected page
  2. Middleware redirects to /umbraco/basic-auth/login?returnPath=...
  3. User enters backoffice credentials
  4. On success, redirected back to the original page
image

Two-factor authentication

  1. After password verification, if 2FA is required, user is redirected to /umbraco/basic-auth/2fa
  2. User enters verification code from their authenticator app
  3. On success, redirected back to the original page
image

External login providers

External providers (Google, Microsoft, etc.) appear as buttons below the username/password form when configured. The OAuth flow uses IBackOfficeSignInManager directly — no OpenIddict dependency.

Security and accessibility checklist

  • All POST actions protected with [ValidateAntiForgeryToken]
  • returnPath validated via Url.IsLocalUrl() to prevent open redirects.
  • Controller returns 404 when basic auth is disabled (prevents backdoor sign-in).
  • lockoutOnFailure: true on password sign-in.
  • Same "Invalid username or password" message for all credential failures (no username enumeration).
  • Views use Razor HTML encoding (XSS safe).
  • Accessible: role="alert" on errors, aria-describedby/aria-invalid on inputs, <main> landmark, proper focus outlines, WCAG AA contrast.

Testing

These scenarios verify the standalone login page for basic authentication across different deployment configurations.

Prerequisites

Umbraco site with published content

You need an Umbraco site with at least one published content node so there is a frontend page to protect.

Basic authentication configuration

All scenarios require basic authentication to be enabled in appsettings.json. Adjust RedirectToLoginPage per scenario.

{
  "Umbraco": {
    "CMS": {
      "BasicAuth": {
        "Enabled": true,
        "RedirectToLoginPage": true
      }
    }
  }
}

See Basic Auth Settings documentation for all available options.

Program.cs configurations

Two configurations are used across these scenarios:

Normal Umbraco setup (with backoffice):

builder.CreateUmbracoBuilder()
    .AddBackOffice()
    .AddWebsite()
    .AddComposers()
    .Build();

// ...

app.UseUmbraco()
    .WithMiddleware(u =>
    {
        u.UseBackOffice();
        u.UseWebsite();
    })
    .WithEndpoints(u =>
    {
        u.UseBackOfficeEndpoints();
        u.UseWebsiteEndpoints();
    });

Without backoffice (frontend-only):

builder.CreateUmbracoBuilder()
    .AddCore()
    .AddBackOfficeSignIn()
    .AddWebsite()
    .AddComposers()
    .Build();

// ...

app.UseUmbraco()
    .WithMiddleware(u =>
    {
        u.UseWebsite();
    })
    .WithEndpoints(u =>
    {
        u.UseWebsiteEndpoints();
    });

Scenario 1: Login page redirect — normal Umbraco setup

Config: BasicAuth.Enabled: true, RedirectToLoginPage: true, normal Umbraco setup

Steps

  1. Configure appsettings.json with Enabled: true and RedirectToLoginPage: true
  2. Use the normal Umbraco Program.cs setup (with .AddBackOffice())
  3. Build and run
  4. Open a browser and navigate to the home page
  5. You should be redirected to /umbraco/basic-auth/login?returnPath=%2F
  6. The standalone login form should be displayed
  7. Enter valid backoffice credentials and submit
  8. You should be redirected back to the home page, which now displays

Expected result

  • Standalone server-rendered login form (not the backoffice SPA login)
  • Successful login redirects to the original page

Scenario 2: Browser popup — normal Umbraco setup

Config: BasicAuth.Enabled: true, RedirectToLoginPage: false, normal Umbraco setup

Steps

  1. Configure appsettings.json with Enabled: true and RedirectToLoginPage: false
  2. Use the normal Umbraco Program.cs setup (with .AddBackOffice())
  3. Build and run
  4. Open a browser and navigate to the home page
  5. The browser's native Basic authentication popup should appear
  6. Enter valid backoffice credentials
  7. The home page should display

Expected result

  • Browser's native authentication popup (no redirect to any login page)
  • 401 response with WWW-Authenticate: Basic realm="Umbraco login" header

Scenario 3: Login page redirect — without backoffice

Config: BasicAuth.Enabled: true, RedirectToLoginPage: true, frontend-only setup

Steps

  1. Configure appsettings.json with Enabled: true and RedirectToLoginPage: true
  2. Use the frontend-only Program.cs setup (with .AddCore() + .AddBackOfficeSignIn() + .AddWebsite())
  3. Build and run
  4. Open a browser and navigate to the home page
  5. You should be redirected to /umbraco/basic-auth/login?returnPath=%2F
  6. The standalone login form should be displayed
  7. Enter valid backoffice credentials and submit
  8. You should be redirected back to the home page

Expected result

  • Identical behavior to Scenario 1
  • No backoffice routes are registered — navigating to /umbraco/ would return 404
  • The standalone login page works independently of the backoffice SPA

Scenario 4: Browser popup — without backoffice

Config: BasicAuth.Enabled: true, RedirectToLoginPage: false, frontend-only setup

Steps

  1. Configure appsettings.json with Enabled: true and RedirectToLoginPage: false
  2. Use the frontend-only Program.cs setup (with .AddCore() + .AddBackOfficeSignIn() + .AddWebsite())
  3. Build and run
  4. Open a browser and navigate to the home page
  5. The browser's native Basic authentication popup should appear
  6. Enter valid backoffice credentials
  7. The home page should display

Expected result

  • Identical behavior to Scenario 2
  • No startup errors despite the backoffice not being registered

Scenario 5: Two-factor authentication — without backoffice

Config: BasicAuth.Enabled: true, RedirectToLoginPage: true, frontend-only setup, 2FA enabled

Setup: Configure 2FA

2FA must be configured and enabled on the test user's account before switching to the frontend-only setup.

Install and configure 2FA
  1. Install the Umbraco.Community.User2FA NuGet package into your project:

    dotnet add package Umbraco.Community.User2FA
    
  2. Add the configuration to appsettings.json:

    {
      "User2FA": {
        "AuthenticatorIssuerName": "My Umbraco Site"
      }
    }
  3. Run the site with the normal Umbraco setup (.AddBackOffice()) so the backoffice is available

  4. Log into the backoffice

  5. Click your user avatar in the top right corner

  6. Select "Configure Two Factor" and scan the QR code with an authenticator app (e.g. Microsoft Authenticator, Google Authenticator)

  7. Verify the code to complete setup

Steps

  1. Ensure 2FA is configured on your test account (see setup above)
  2. Configure appsettings.json with Enabled: true and RedirectToLoginPage: true
  3. Switch to the frontend-only Program.cs setup (.AddCore() + .AddBackOfficeSignIn() + .AddWebsite())
  4. Build and run
  5. Navigate to the home page — you should be redirected to the login page
  6. Enter your backoffice credentials and submit
  7. You should be redirected to /umbraco/basic-auth/2fa
  8. The 2FA code entry form should be displayed
  9. Enter the code from your authenticator app and submit
  10. You should be redirected back to the home page

Additional test: Browser popup with 2FA

  1. Change RedirectToLoginPage to false
  2. Navigate to the home page — the browser Basic popup appears
  3. Enter credentials — since 2FA is required, you should be redirected to the 2FA page (the browser popup cannot complete 2FA)
  4. Enter the code and submit
  5. You should be redirected back to the home page

Expected result

  • Login form accepts credentials, then redirects to 2FA page
  • 2FA page shows code input (and provider selector if multiple providers are configured)
  • Successful code entry redirects to the original page
  • When using the browser popup (RedirectToLoginPage: false), the middleware still redirects to the 2FA page

Scenario 6: External login provider — without backoffice

Config: BasicAuth.Enabled: true, RedirectToLoginPage: true, frontend-only setup, Google external login configured

Setup: Configure Google external login

The external login provider must be configured before testing. This requires a Google OAuth application.

Create Google OAuth credentials
  1. Go to Google Cloud Console
  2. Create a new project (or use an existing one)
  3. Navigate to APIs & Services > Credentials
  4. Click Create Credentials > OAuth client ID
  5. Set application type to Web application
  6. Add https://localhost:44339/umbraco-google-signin as an Authorized redirect URI (adjust port to match your setup)
  7. Note the Client ID and Client Secret
Install NuGet package
dotnet add package Microsoft.AspNetCore.Authentication.Google
Create GoogleBackOfficeExternalLoginProviderOptions.cs
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Core;

public class GoogleBackOfficeExternalLoginProviderOptions
    : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
    public const string SchemeName = "Google";

    public void Configure(string? name, BackOfficeExternalLoginProviderOptions options)
    {
        if (name != Constants.Security.BackOfficeExternalAuthenticationTypePrefix + SchemeName)
        {
            return;
        }

        Configure(options);
    }

    public void Configure(BackOfficeExternalLoginProviderOptions options)
    {
        options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
            autoLinkExternalAccount: true,
            defaultUserGroups: ["admin"],
            defaultCulture: null,
            allowManualLinking: true);

        options.DenyLocalLogin = false;
    }
}
Create ConfigureGoogleAuthenticationOptions.cs
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Web.Common.Helpers;

public class ConfigureGoogleAuthenticationOptions : IConfigureNamedOptions<GoogleOptions>
{
    private readonly OAuthOptionsHelper _helper;

    public ConfigureGoogleAuthenticationOptions(OAuthOptionsHelper helper)
    {
        _helper = helper;
    }

    public void Configure(string? name, GoogleOptions options)
    {
        if (name == BackOfficeAuthenticationBuilder.SchemeForBackOffice(
                GoogleBackOfficeExternalLoginProviderOptions.SchemeName))
        {
            Configure(options);
        }
    }

    public void Configure(GoogleOptions options)
    {
        options.CallbackPath = "/umbraco-google-signin";
        options.ClientId = "YOUR_GOOGLE_CLIENT_ID";
        options.ClientSecret = "YOUR_GOOGLE_CLIENT_SECRET";

        _helper.SetDefaultErrorEventHandling(
            options, GoogleBackOfficeExternalLoginProviderOptions.SchemeName);
    }
}
Create GoogleBackOfficeExternalLoginComposer.cs
using Microsoft.AspNetCore.Authentication.Google;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Core.Composing;

public class GoogleBackOfficeExternalLoginComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.Services.ConfigureOptions<GoogleBackOfficeExternalLoginProviderOptions>();
        builder.Services.ConfigureOptions<ConfigureGoogleAuthenticationOptions>();

        builder.AddBackOfficeExternalLogins(logins =>
        {
            logins.AddBackOfficeLogin(backOfficeAuthenticationBuilder =>
            {
                backOfficeAuthenticationBuilder.AddGoogle(
                    BackOfficeAuthenticationBuilder.SchemeForBackOffice(
                        GoogleBackOfficeExternalLoginProviderOptions.SchemeName)!,
                    options => { });
            });
        });
    }
}

See the External Login Providers documentation for full details.

Steps

  1. Ensure Google external login is configured (see setup above)
  2. Configure appsettings.json with Enabled: true and RedirectToLoginPage: true
  3. Use the frontend-only Program.cs setup (.AddCore() + .AddBackOfficeSignIn() + .AddWebsite())
  4. Build and run
  5. Navigate to the home page — you should be redirected to the login page
  6. The login form should show a "Sign in with Google" button below the username/password fields
  7. Click the Google button
  8. Complete the Google OAuth flow (select account, grant permissions)
  9. You should be redirected back to the home page

Expected result

  • External provider buttons appear on the login form when providers are configured
  • OAuth challenge redirects to Google
  • Successful authentication redirects back to the protected page
  • If the user has 2FA enabled, the 2FA page is shown after external login (unless UserBypassTwoFactorForExternalLogins is configured)

Copilot AI review requested due to automatic review settings March 18, 2026 11:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a lightweight, server-rendered Basic Auth login + 2FA flow (and optional external providers) intended to work even when the backoffice SPA/OpenIddict aren’t present, and splits backoffice auth registration so cookie sign-in can be enabled independently via AddBackOfficeSignIn().

Changes:

  • Introduces /umbraco/basic-auth/login and /umbraco/basic-auth/2fa endpoints (controller, routes, models, Razor views).
  • Hardens BasicAuthenticationMiddleware behavior (route exclusions, 2FA redirect behavior, auth-scheme guard).
  • Refactors backoffice DI to separate cookie auth from OpenIddict services; updates integration tests accordingly.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddlewareTests.cs Adds middleware unit tests for redirects/401/2FA and missing services scenarios.
tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/BasicAuthLoginControllerTests.cs Adds controller unit tests for login, 2FA, returnPath validation, and configuration error paths.
tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs Updates integration wiring to new backoffice auth registration split.
tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs Updates integration wiring to new backoffice auth registration split.
src/Umbraco.Web.Website/Routing/BasicAuthLoginRoutes.cs Adds endpoint routing for the standalone basic-auth login/2FA pages.
src/Umbraco.Web.Website/Models/BasicAuthTwoFactorModel.cs Adds 2FA view model.
src/Umbraco.Web.Website/Models/BasicAuthLoginModel.cs Adds login view model (incl. external providers list).
src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs Adds standalone login redirect, 2FA redirect behavior, and backoffice-scheme guard.
src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilderExtensions.cs Registers the new basic-auth routes alongside existing website routes.
src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs Registers BasicAuthLoginRoutes in DI.
src/Umbraco.Web.Website/Controllers/BasicAuthLoginController.cs Adds server-rendered login/2FA/external-login endpoints for basic-auth flows.
src/Umbraco.Cms.StaticAssets/umbraco/BasicAuthLogin/TwoFactor.cshtml Adds standalone 2FA page markup/styles.
src/Umbraco.Cms.StaticAssets/umbraco/BasicAuthLogin/Login.cshtml Adds standalone login page markup/styles (incl. external provider buttons).
src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs Moves OpenIddict-dependent registration out of backoffice identity.
src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs Introduces AddBackOfficeSignIn() and reworks AddBackOffice() composition.
src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs Splits cookie auth vs OpenIddict registrations; obsoletes AddBackOfficeAuthentication().
global.json Allows prerelease SDK selection.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs Outdated
Comment thread src/Umbraco.Web.Website/Routing/BasicAuthLoginRoutes.cs
Comment thread global.json Outdated
@AndyButland AndyButland changed the title Basic Auth: Standalone login page for frontend-only deployments (closes #22144) Basic Authentication: Standalone login page for frontend-only deployments (closes #22144) Mar 18, 2026
Copy link
Copy Markdown
Contributor

@Migaroez Migaroez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some cde comments, nothing blocking. Testing functionality next

Comment thread src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs Outdated
Comment thread src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs Outdated
Copy link
Copy Markdown
Contributor

@Migaroez Migaroez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overal happy with the functionality, the only 2 minor points are

Happy to let it be merged in without the improvements.

@AndyButland AndyButland added release/17.4.0 category/notable status/needs-docs Requires new or updated documentation labels Apr 11, 2026
@AndyButland
Copy link
Copy Markdown
Contributor Author

Views aren't customizable which might be an issue for some implementors.

Yes, I was thinking this could come later if requested, but your solution works just fine, so let's include it from the start. I've cherry-picked it in here.

There is no clear indication the form was submitted and it can be slow on first use

I've added a small amount of JavaScript to disable the button/update the text, so you have a bit more than the standard browser navigation to indicate that a form submission is pending.

@AndyButland AndyButland enabled auto-merge (squash) April 11, 2026 08:36
@AndyButland AndyButland merged commit 0f4d0a6 into main Apr 11, 2026
26 of 27 checks passed
@AndyButland AndyButland deleted the v17/improvement/22144-login-endpoints-front-end-only branch April 11, 2026 08:55
@AndyButland
Copy link
Copy Markdown
Contributor Author

Docs PR is here: umbraco/UmbracoDocs#7954

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category/notable release/17.4.0 status/needs-docs Requires new or updated documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants