diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs index d9d31fa422..89ee1fd625 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs @@ -40,8 +40,33 @@ public class BackOfficeController : ManagementApiControllerBase _securitySettings = securitySettings; } + // FIXME: this is a temporary solution to get the new backoffice auth rolling. + // once the old backoffice auth is no longer necessary, clean this up and merge with 2FA handling etc. + [HttpPost("login")] + [MapToApiVersion("1.0")] + public async Task Login(LoginRequestModel model) + { + var validated = await _backOfficeUserManager.ValidateCredentialsAsync(model.Username, model.Password); + if (validated is false) + { + return Unauthorized(); + } + + var claims = new List { new(ClaimTypes.Name, model.Username) }; + var claimsIdentity = new ClaimsIdentity(claims, Constants.Security.NewBackOfficeAuthenticationType); + await HttpContext.SignInAsync(Constants.Security.NewBackOfficeAuthenticationType, new ClaimsPrincipal(claimsIdentity)); + + return Ok(); + } + + public class LoginRequestModel + { + public required string Username { get; init; } + + public required string Password { get; init; } + } + [HttpGet("authorize")] - [HttpPost("authorize")] [MapToApiVersion("1.0")] public async Task Authorize() { @@ -67,7 +92,7 @@ public class BackOfficeController : ManagementApiControllerBase // by calling BackOfficeSignInManager.ExternalLoginSignInAsync // retrieve the user principal stored in the authentication cookie. - AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); + AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.NewBackOfficeAuthenticationType); var userName = cookieAuthResult.Succeeded ? cookieAuthResult.Principal?.Identity?.Name : null; @@ -140,5 +165,5 @@ public class BackOfficeController : ManagementApiControllerBase return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); } - private static IActionResult DefaultChallengeResult() => new ChallengeResult(Constants.Security.BackOfficeAuthenticationType); + private static IActionResult DefaultChallengeResult() => new ChallengeResult(Constants.Security.NewBackOfficeAuthenticationType); } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 412886aa71..c292704b17 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -19,7 +19,8 @@ public static class BackOfficeAuthBuilderExtensions { builder .AddDbContext() - .AddOpenIddict(); + .AddOpenIddict() + .AddBackOfficeLogin(); return builder; } @@ -124,6 +125,19 @@ public static class BackOfficeAuthBuilderExtensions return builder; } + private static IUmbracoBuilder AddBackOfficeLogin(this IUmbracoBuilder builder) + { + builder.Services + .AddAuthentication() + .AddCookie(Constants.Security.NewBackOfficeAuthenticationType, options => + { + options.LoginPath = "/umbraco/login"; + options.Cookie.Name = Constants.Security.NewBackOfficeAuthenticationType; + }); + + return builder; + } + // TODO: remove this once EF is implemented public class DatabaseManager : IHostedService { @@ -140,7 +154,7 @@ public static class BackOfficeAuthBuilderExtensions // TODO: add BackOfficeAuthorizationInitializationMiddleware before UseAuthorization (to make it run for unauthorized API requests) and remove this IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService(); - await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(new Uri("https://localhost:44331/"), cancellationToken); + await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(new Uri("https://localhost:44339/"), cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 483c09a53f..af546f191a 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -5803,12 +5803,27 @@ "description": "Success" } } - }, + } + }, + "/umbraco/management/api/v1/security/back-office/login": { "post": { "tags": [ "Security" ], - "operationId": "PostSecurityBackOfficeAuthorize", + "operationId": "PostSecurityBackOfficeLogin", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/LoginRequestModel" + } + ] + } + } + } + }, "responses": { "200": { "description": "Success" @@ -9863,6 +9878,18 @@ }, "additionalProperties": false }, + "LoginRequestModel": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false + }, "MediaItemResponseModel": { "type": "object", "allOf": [ diff --git a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs index 42d37816c0..8677f57922 100644 --- a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs @@ -13,15 +13,17 @@ public class BackOfficeApplicationManager : IBackOfficeApplicationManager private readonly IOpenIddictApplicationManager _applicationManager; private readonly IWebHostEnvironment _webHostEnvironment; private readonly Uri? _backOfficeHost; + private readonly string? _authorizeCallbackPathName; public BackOfficeApplicationManager( IOpenIddictApplicationManager applicationManager, IWebHostEnvironment webHostEnvironment, - IOptionsMonitor securitySettingsMonitor) + IOptions securitySettings) { _applicationManager = applicationManager; _webHostEnvironment = webHostEnvironment; - _backOfficeHost = securitySettingsMonitor.CurrentValue.BackOfficeHost; + _backOfficeHost = securitySettings.Value.BackOfficeHost; + _authorizeCallbackPathName = securitySettings.Value.AuthorizeCallbackPathName; } public async Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default) @@ -38,7 +40,7 @@ public class BackOfficeApplicationManager : IBackOfficeApplicationManager ClientId = Constants.OauthClientIds.BackOffice, RedirectUris = { - CallbackUrlFor(_backOfficeHost ?? backOfficeUrl, "/umbraco") + CallbackUrlFor(_backOfficeHost ?? backOfficeUrl, _authorizeCallbackPathName ?? "/umbraco") }, Type = OpenIddictConstants.ClientTypes.Public, Permissions = diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 97976231b2..bdc7eb40b2 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -78,6 +78,8 @@ public static partial class Constants public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; public const string BackOfficeTwoFactorRememberMeAuthenticationType = "UmbracoTwoFactorRememberMeCookie"; + // FIXME: remove this in favor of BackOfficeAuthenticationType when the old backoffice auth is no longer necessary + public const string NewBackOfficeAuthenticationType = "NewUmbracoBackOffice"; public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__"; diff --git a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs index 97d21a1fc1..0926908ceb 100644 --- a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs +++ b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs @@ -7,4 +7,6 @@ namespace Umbraco.New.Cms.Core.Models.Configuration; public class NewBackOfficeSettings { public Uri? BackOfficeHost { get; set; } = null; + + public string? AuthorizeCallbackPathName { get; set; } = null; } diff --git a/src/Umbraco.Web.UI.New/BackOfficeLoginController.cs b/src/Umbraco.Web.UI.New/BackOfficeLoginController.cs index 8f34e1b645..e20728c1a5 100644 --- a/src/Umbraco.Web.UI.New/BackOfficeLoginController.cs +++ b/src/Umbraco.Web.UI.New/BackOfficeLoginController.cs @@ -27,10 +27,7 @@ public class BackOfficeLoginController : Controller // GET public IActionResult Index(BackOfficeLoginModel model) { - var authUrl = _linkGenerator.GetUmbracoApiServiceBaseUrl( - controller => controller.PostLogin(new LoginModel())); - - model.AuthUrl = authUrl ?? string.Empty; + model.AuthUrl = "/umbraco/management/api/v1.0/security/back-office"; return View("/umbraco/UmbracoLogin/Index.cshtml", model); } diff --git a/src/Umbraco.Web.UI.New/umbraco/UmbracoLogin/Index.cshtml b/src/Umbraco.Web.UI.New/umbraco/UmbracoLogin/Index.cshtml index 37e476d8f0..ea31c09e5d 100644 --- a/src/Umbraco.Web.UI.New/umbraco/UmbracoLogin/Index.cshtml +++ b/src/Umbraco.Web.UI.New/umbraco/UmbracoLogin/Index.cshtml @@ -1,7 +1,4 @@ @model Umbraco.Cms.Web.UI.BackOfficeLoginModel -@{ - var authUrlLogin = Model.AuthUrl + "PostLogin"; -} @@ -43,7 +40,7 @@