Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Server;
using Umbraco.Cms.Api.Common.DependencyInjection;
Expand All @@ -9,9 +7,9 @@
using Umbraco.Cms.Api.Management.Middleware;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Web.Common.ApplicationBuilder;

Expand All @@ -27,12 +25,12 @@ public static class BackOfficeAuthBuilderExtensions
/// </summary>
/// <param name="builder">The <see cref="IUmbracoBuilder"/> to which back office authentication services will be added.</param>
/// <returns>The same <see cref="IUmbracoBuilder"/> instance with back office authentication configured.</returns>
[Obsolete("Use AddBackOffice() or AddBackOfficeSignIn() instead. Scheduled for removal in Umbraco 19.")]
public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder)
{
builder
.AddAuthentication()
.AddUmbracoOpenIddict()
.AddBackOfficeLogin();
.AddBackOfficeCookieAuthentication()
.AddBackOfficeOpenIddictServices();

return builder;
}
Expand All @@ -52,22 +50,13 @@ public static IUmbracoBuilder AddTokenRevocation(this IUmbracoBuilder builder)
return builder;
}

private static IUmbracoBuilder AddAuthentication(this IUmbracoBuilder builder)
{
builder.Services.AddAuthentication();
builder.AddAuthorizationPolicies();

builder.Services.AddTransient<IBackOfficeApplicationManager, BackOfficeApplicationManager>();
builder.Services.AddSingleton<BackOfficeAuthorizationInitializationMiddleware>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new BackofficePipelineFilter("Backoffice")));

return builder;
}

private static IUmbracoBuilder AddBackOfficeLogin(this IUmbracoBuilder builder)
/// <summary>
/// Registers backoffice cookie authentication schemes, cookie configuration, and authorization policies.
/// Does NOT register OpenIddict or the backoffice SPA infrastructure.
/// </summary>
internal static IUmbracoBuilder AddBackOfficeCookieAuthentication(this IUmbracoBuilder builder)
{
builder.Services
.AddAuthentication()
builder.Services.AddAuthentication()

// Add our custom schemes which are cookie handlers
.AddCookie(Constants.Security.BackOfficeAuthenticationType)
Expand All @@ -93,7 +82,30 @@ private static IUmbracoBuilder AddBackOfficeLogin(this IUmbracoBuilder builder)
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});

// Add OpnIddict server event handler to refresh the cookie that exposes the backoffice authentication outside the scope of the backoffice.
builder.Services.AddScoped<BackOfficeSecurityStampValidator>();
builder.Services.ConfigureOptions<ConfigureBackOfficeCookieOptions>();
builder.Services.ConfigureOptions<ConfigureBackOfficeExposedCookieOptions>();
builder.Services.ConfigureOptions<ConfigureBackOfficeSecurityStampValidatorOptions>();

builder.AddAuthorizationPolicies();

return builder;
}

/// <summary>
/// Registers OpenIddict services, the backoffice application manager, authorization initialization middleware,
/// and OpenIddict event handlers. These are only needed for the full backoffice SPA flow.
/// </summary>
internal static IUmbracoBuilder AddBackOfficeOpenIddictServices(this IUmbracoBuilder builder)
{
builder.AddUmbracoOpenIddict();

builder.Services.AddTransient<IBackOfficeApplicationManager, BackOfficeApplicationManager>();
builder.Services.AddScoped<IBackOfficeUserClientCredentialsManager, BackOfficeUserClientCredentialsManager>();
builder.Services.AddSingleton<BackOfficeAuthorizationInitializationMiddleware>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new BackofficePipelineFilter("Backoffice")));

// Add OpenIddict server event handler to refresh the cookie that exposes the backoffice authentication outside the scope of the backoffice.
builder.Services.AddSingleton<ExposeBackOfficeAuthenticationOpenIddictServerEventsHandler>();
builder.Services.Configure<OpenIddictServerOptions>(options =>
{
Expand All @@ -109,11 +121,6 @@ private static IUmbracoBuilder AddBackOfficeLogin(this IUmbracoBuilder builder)
.Build());
});

builder.Services.AddScoped<BackOfficeSecurityStampValidator>();
builder.Services.ConfigureOptions<ConfigureBackOfficeCookieOptions>();
builder.Services.ConfigureOptions<ConfigureBackOfficeExposedCookieOptions>();
builder.Services.ConfigureOptions<ConfigureBackOfficeSecurityStampValidatorOptions>();

return builder;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,45 @@ public static partial class UmbracoBuilderExtensions
/// Adds all required components to run the Umbraco back office.
/// </summary>
/// <remarks>
/// This method calls <c>AddCore()</c> internally to register all core services,
/// then adds backoffice-specific services on top.
/// This method calls <c>AddCore()</c> and <see cref="AddBackOfficeSignIn"/> internally
/// to register core services, identity, and cookie authentication, then adds backoffice-specific
/// services on top (OpenIddict, backoffice SPA infrastructure, token management).
/// <para>
/// For frontend-only deployments that only need basic authentication with backoffice credentials
/// (no backoffice UI), use <see cref="AddBackOfficeSignIn"/> instead.
/// </para>
/// </remarks>
/// <param name="builder">The Umbraco builder.</param>
/// <param name="configureMvc">Optional action to configure the MVC builder.</param>
/// <returns>The Umbraco builder.</returns>
public static IUmbracoBuilder AddBackOffice(this IUmbracoBuilder builder, Action<IMvcBuilder>? configureMvc = null) =>
builder
.AddCore(configureMvc) // All core services
.AddBackOfficeCore() // Backoffice-specific: IBackOfficePathGenerator
.AddBackOfficeIdentity() // Backoffice user identity
.AddBackOfficeAuthentication() // OpenIddict, authorization policies
.AddTokenRevocation() // Token cleanup handlers
.AddMembersIdentity(); // Member identity (also needed for backoffice admin)
.AddCore(configureMvc) // All core services
.AddBackOfficeSignIn() // Identity + Cookie authentication
.AddBackOfficeCore() // IBackOfficePathGenerator, IBackOfficeEnabledMarker
.AddBackOfficeOpenIddictServices() // OpenIddict, application manager, middleware
.AddTokenRevocation() // Token cleanup handlers
.AddMembersIdentity(); // Member identity (also needed for backoffice admin)

/// <summary>
/// Adds backoffice identity and cookie authentication without the full backoffice UI or OpenIddict.
/// Use this for frontend-only deployments that need basic authentication with backoffice credentials.
/// </summary>
/// <remarks>
/// This registers the backoffice identity system (user manager, sign-in manager) and cookie authentication
/// schemes, but does NOT register OpenIddict, the Management API, or the backoffice SPA. It enables
/// <c>BasicAuthenticationMiddleware</c> to authenticate users via a standalone server-rendered login page.
/// <para>
/// Requires <c>AddCore()</c> to have been called first.
/// For full backoffice support, use <see cref="AddBackOffice"/> instead.
/// </para>
/// </remarks>
/// <param name="builder">The <see cref="IUmbracoBuilder"/> to configure.</param>
/// <returns>The same <see cref="IUmbracoBuilder"/> instance.</returns>
public static IUmbracoBuilder AddBackOfficeSignIn(this IUmbracoBuilder builder) =>
Comment thread
Migaroez marked this conversation as resolved.
builder
.AddBackOfficeIdentity()
.AddBackOfficeCookieAuthentication();

/// <summary>
/// Registers the essential services required for the Umbraco back office, including the back office path generator and the physical file system implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public static IUmbracoBuilder AddBackOfficeIdentity(this IUmbracoBuilder builder
services.AddScoped<IInviteUriProvider, InviteUriProvider>();
services.AddScoped<IForgotPasswordUriProvider, ForgotPasswordUriProvider>();
services.AddScoped<IBackOfficePasswordChanger, BackOfficePasswordChanger>();
services.AddScoped<IBackOfficeUserClientCredentialsManager, BackOfficeUserClientCredentialsManager>();

services.AddSingleton<IBackOfficeUserPasswordChecker, NoopBackOfficeUserPasswordChecker>();

Expand Down
169 changes: 169 additions & 0 deletions src/Umbraco.Cms.StaticAssets/umbraco/BasicAuthLogin/Login.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
@model Umbraco.Cms.Web.Website.Models.BasicAuthLoginModel
@{
Layout = null;
var hasError = !string.IsNullOrEmpty(Model?.ErrorMessage);
var externalProviders = Model?.ExternalLoginProviders?.ToList() ?? [];
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Umbraco - Sign In</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f4f4f4;
color: #1b264f;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.login-container {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 16px rgba(0,0,0,0.08);
padding: 2.5rem;
width: 100%;
max-width: 400px;
}
h1 {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: #555d67;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid #d8d7d9;
border-radius: 4px;
font-size: 0.9375rem;
transition: border-color 0.15s, outline-color 0.15s;
}
input:focus {
border-color: #2152a3;
outline: 2px solid #2152a3;
outline-offset: 1px;
}
.btn {
display: block;
width: 100%;
padding: 0.75rem;
background: #1b264f;
color: #fff;
border: none;
border-radius: 4px;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
margin-top: 0.5rem;
}
.btn:hover { background: #2152a3; }
.btn:focus {
outline: 2px solid #2152a3;
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-external {
background: #fff;
color: #1b264f;
border: 1px solid #d8d7d9;
}
.btn-external:hover {
background: #f4f4f4;
}
.error-message {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
border-radius: 4px;
padding: 0.75rem;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.divider {
display: flex;
align-items: center;
margin: 1.25rem 0;
color: #555d67;
font-size: 0.8125rem;
}
.divider::before,
.divider::after {
content: "";
flex: 1;
border-bottom: 1px solid #d8d7d9;
}
.divider::before { margin-right: 0.75rem; }
.divider::after { margin-left: 0.75rem; }
</style>
</head>
<body>
<main class="login-container">
<h1>Umbraco</h1>
<p class="subtitle">Sign in with your backoffice account</p>

@if (hasError)
{
<div id="login-error" class="error-message" role="alert">@Model!.ErrorMessage</div>
}

<form method="post" action="@Url.Action("Login", "BasicAuthLogin")">
@Html.AntiForgeryToken()
<input type="hidden" name="returnPath" value="@Model?.ReturnPath" />

<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" autocomplete="username"
required autofocus
@(hasError ? "aria-describedby=login-error aria-invalid=true" : "") />
</div>

<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="current-password"
required
@(hasError ? "aria-describedby=login-error aria-invalid=true" : "") />
</div>

<button type="submit" class="btn" data-submitting-text="Signing in&#x2026;">Sign in</button>
</form>

@if (externalProviders.Count > 0)
{
<div class="divider">or</div>

@foreach (var provider in externalProviders)
{
<form method="post" action="@Url.Action("ExternalLogin", "BasicAuthLogin")">
@Html.AntiForgeryToken()
<input type="hidden" name="provider" value="@provider.Name" />
<input type="hidden" name="returnPath" value="@Model?.ReturnPath" />
<button type="submit" class="btn btn-external" data-submitting-text="Redirecting&#x2026;">Sign in with @(provider.DisplayName ?? provider.Name)</button>
</form>
}
}
</main>
<script src="@Url.Content("~/umbraco/basic-auth/login.js")"></script>
</body>
</html>
Loading
Loading