From e5f8f3475571e82f3fe9c5c02ed8dc2e8725df2f Mon Sep 17 00:00:00 2001 From: kjac Date: Wed, 28 Sep 2022 15:30:31 +0200 Subject: [PATCH 01/26] First attempt at OpenIddict --- .../BackOfficeAuthenticationController.cs | 82 ++++++++ .../BackOfficeAuthBuilderExtensions.cs | 190 ++++++++++++++++++ .../ManagementApiComposer.cs | 3 +- .../Umbraco.Cms.ManagementApi.csproj | 4 + 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs new file mode 100644 index 000000000000..4d03f8f1d07f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs @@ -0,0 +1,82 @@ +using System.Globalization; +using System.Security.Claims; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using NSwag.Annotations; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; +using OpenIddict.Validation.AspNetCore; +using Umbraco.Extensions; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.BackOfficeAuthentication; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/back-office-authentication")] +[OpenApiTag("BackOfficeAuthentication")] +public class BackOfficeAuthenticationController : ManagementApiControllerBase +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public BackOfficeAuthenticationController(IHttpContextAccessor httpContextAccessor) + => _httpContextAccessor = httpContextAccessor; + + [HttpPost("authorize")] + [MapToApiVersion("1.0")] + public IActionResult Authorize() + { + HttpContext context = _httpContextAccessor.GetRequiredHttpContext(); + + OpenIddictRequest request = context.GetOpenIddictServerRequest() ?? throw new ApplicationException("TODO: something descriptive"); + int.TryParse(request["hardcoded_identity_id"]?.ToString(), out var identifier); + if (identifier is not (1 or 2)) + { + return new ChallengeResult( + new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, + new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified hardcoded identity is invalid." + })); + } + + // Create a new identity and populate it based on the specified hardcoded identity identifier. + var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType); + identity.AddClaim(new Claim(OpenIddictConstants.Claims.Subject, identifier.ToString(CultureInfo.InvariantCulture))); + identity.AddClaim(new Claim(OpenIddictConstants.Claims.Name, identifier switch + { + 1 => "Alice", + 2 => "Bob", + _ => throw new InvalidOperationException() + }).SetDestinations(OpenIddictConstants.Destinations.AccessToken)); + + var principal = new ClaimsPrincipal(identity); + + principal.SetScopes(identifier switch + { + 1 => request.GetScopes(), + 2 => new[] { "api1" }.Intersect(request.GetScopes()), + _ => throw new InvalidOperationException() + }); + + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal); + } + + [HttpGet("test")] + [MapToApiVersion("1.0")] + public IActionResult Test() => Ok("Hello"); + + [HttpGet("api1")] + [MapToApiVersion("1.0")] + [Authorize("can_use_api_1", AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] + public IActionResult Api1() => Ok($"API1 response: {User.Identity!.Name} has scopes {string.Join(", ", User.GetScopes())}"); + + [HttpGet("api2")] + [MapToApiVersion("1.0")] + [Authorize("can_use_api_2", AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] + public IActionResult Api2() => Ok($"API2 response: {User.Identity!.Name} has scopes {string.Join(", ", User.GetScopes())}"); +} diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs new file mode 100644 index 000000000000..900619f4f011 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -0,0 +1,190 @@ +using System.Collections.Immutable; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenIddict.Abstractions; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.ManagementApi.DependencyInjection; + +public static class BackOfficeAuthBuilderExtensions +{ + public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder) + { + builder + .AddDbContext() + .AddOpenIddict() + .AddAuthorizationPolicies(); + + builder.Services.AddTransient(); + + return builder; + } + + private static IUmbracoBuilder AddDbContext(this IUmbracoBuilder builder) + { + builder.Services.AddDbContext(options => + { + // Configure the DB context + // TODO: use actual Umbraco DbContext once EF is implemented - and remove dependency on Microsoft.EntityFrameworkCore.InMemory + options.UseInMemoryDatabase(nameof(DbContext)); + + // Register the entity sets needed by OpenIddict. + options.UseOpenIddict(); + }); + + return builder; + } + + private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) + { + builder.Services.AddOpenIddict() + + // Register the OpenIddict core components. + .AddCore(options => + { + options + .UseEntityFrameworkCore() + .UseDbContext(); + }) + + // Register the OpenIddict server components. + .AddServer(options => + { + // Enable the authorization and token endpoints. + options + .SetAuthorizationEndpointUris("/umbraco/api/v1.0/back-office-authentication/authorize") + .SetTokenEndpointUris("/umbraco/api/v1.0/back-office-authentication/token"); + + // Enable authorization code flow with PKCE + options + .AllowAuthorizationCodeFlow() + .RequireProofKeyForCodeExchange(); + + // Register the encryption credentials. + options + // TODO: use an actual key, i.e. options.AddEncryptionKey(new SymmetricSecurityKey(..)); + .AddDevelopmentEncryptionCertificate() + .DisableAccessTokenEncryption(); + + // Register the signing credentials. + options + // TODO: use an actual certificate here + .AddDevelopmentSigningCertificate(); + + // Register available scopes + options + // TODO: figure out appropriate scopes (sections? trees?) + .RegisterScopes("api1", "api2"); + + // Register the ASP.NET Core host and configure for custom authentication endpoint. + options + .UseAspNetCore() + .EnableAuthorizationEndpointPassthrough(); + }) + + // Register the OpenIddict validation components. + .AddValidation(options => + { + // Import the configuration from the local OpenIddict server instance. + options.UseLocalServer(); + + // Register the ASP.NET Core host. + options.UseAspNetCore(); + }); + + builder.Services.AddHostedService(); + + return builder; + } + + private static IUmbracoBuilder AddAuthorizationPolicies(this IUmbracoBuilder builder) + { + builder.Services.AddAuthorization(options => + { + // TODO: actual policies for APIs here + options.AddPolicy("can_use_api_1", policy => policy.RequireClaim("scope", "api1")); + options.AddPolicy("can_use_api_2", policy => policy.RequireClaim("scope", "api2")); + }); + + return builder; + } + + // TODO: move this somewhere (find an appropriate namespace for it) + public class ScopeClaimsTransformation : IClaimsTransformation + { + public Task TransformAsync(ClaimsPrincipal principal) + { + if (principal.HasClaim("scope") == false) + { + return Task.FromResult(principal); + } + + ImmutableArray knownScopeClaims = principal.GetClaims("scope"); + var missingScopeClaims = knownScopeClaims.SelectMany(s => s.Split(' ')).Except(knownScopeClaims).ToArray(); + if (missingScopeClaims.Any() == false) + { + return Task.FromResult(principal); + } + + var claimsIdentity = new ClaimsIdentity(); + foreach (var missingScopeClaim in missingScopeClaims) + { + claimsIdentity.AddClaim("scope", missingScopeClaim); + } + + principal.AddIdentity(claimsIdentity); + return Task.FromResult(principal); + } + } + + // TODO: move this somewhere (find an appropriate namespace for it) + public class ClientIdManager : IHostedService + { + private readonly IServiceProvider _serviceProvider; + + public ClientIdManager(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + public async Task StartAsync(CancellationToken cancellationToken) + { + using IServiceScope scope = _serviceProvider.CreateScope(); + + DbContext context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(cancellationToken); + + IOpenIddictApplicationManager manager = scope.ServiceProvider.GetRequiredService(); + + const string backofficeClientId = "umbraco-back-office"; + if (await manager.FindByClientIdAsync(backofficeClientId, cancellationToken) is null) + { + await manager.CreateAsync( + new OpenIddictApplicationDescriptor + { + ClientId = backofficeClientId, + // TODO: fix redirect URI + path + // how do we figure out the current backoffice host? + // - wait for first request? + // - use IServerAddressesFeature? + // - put it in config? + // should we support multiple callback URLS (for external apps)? + RedirectUris = { new Uri("https://localhost:44331/umbraco/login/callback/") }, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.ResponseTypes.Code, + // TODO: figure out appropriate scopes (sections? trees?) + OpenIddictConstants.Permissions.Prefixes.Scope + "api1", + OpenIddictConstants.Permissions.Prefixes.Scope + "api2" + } + }, + cancellationToken); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index de013756aa5e..e5182aae7628 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -36,7 +36,8 @@ public void Compose(IUmbracoBuilder builder) builder .AddNewInstaller() .AddUpgrader() - .AddExamineManagement(); + .AddExamineManagement() + .AddBackOfficeAuthentication(); services.AddApiVersioning(options => { diff --git a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj index 090ae68e25f1..7a215639bd12 100644 --- a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj +++ b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj @@ -19,9 +19,13 @@ + + + + all From e038e1c38427c8afe0fdbd3dca8f653a6a2cf02b Mon Sep 17 00:00:00 2001 From: kjac Date: Wed, 5 Oct 2022 09:02:01 +0200 Subject: [PATCH 02/26] Making headway and more TODOs --- .../BackOfficeAuthenticationController.cs | 41 ++++++++++++++++--- .../BackOfficeAuthBuilderExtensions.cs | 7 ++++ .../UmbracoBuilder.BackOfficeAuth.cs | 9 +++- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs index 4d03f8f1d07f..6d50750744a6 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs @@ -10,6 +10,9 @@ using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using OpenIddict.Validation.AspNetCore; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; using Umbraco.New.Cms.Web.Common.Routing; @@ -21,17 +24,44 @@ namespace Umbraco.Cms.ManagementApi.Controllers.BackOfficeAuthentication; public class BackOfficeAuthenticationController : ManagementApiControllerBase { private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IBackOfficeSignInManager _backOfficeSignInManager; + private readonly IBackOfficeUserManager _backOfficeUserManager; - public BackOfficeAuthenticationController(IHttpContextAccessor httpContextAccessor) - => _httpContextAccessor = httpContextAccessor; + public BackOfficeAuthenticationController(IHttpContextAccessor httpContextAccessor, IBackOfficeSignInManager backOfficeSignInManager, IBackOfficeUserManager backOfficeUserManager) + { + _httpContextAccessor = httpContextAccessor; + _backOfficeSignInManager = backOfficeSignInManager; + _backOfficeUserManager = backOfficeUserManager; + } [HttpPost("authorize")] [MapToApiVersion("1.0")] - public IActionResult Authorize() + public async Task Authorize() { HttpContext context = _httpContextAccessor.GetRequiredHttpContext(); - OpenIddictRequest request = context.GetOpenIddictServerRequest() ?? throw new ApplicationException("TODO: something descriptive"); + + if (request.Username != null && request.Password != null) + { + Microsoft.AspNetCore.Identity.SignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(request.Username, request.Password, true, true); + if (result.Succeeded) + { + // TODO: what does this return if username foes not exist? shouldn't be possible, but hey... + BackOfficeIdentityUser backOfficeUser = await _backOfficeUserManager.FindByNameAsync(request.Username); + ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); + backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Id); + + // TODO: not optimal at all, see if we can find a way that doesn't require us passing every last claim to the token + Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); + foreach (Claim backOfficeClaim in backOfficeClaims) + { + backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); + } + + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); + } + } + int.TryParse(request["hardcoded_identity_id"]?.ToString(), out var identifier); if (identifier is not (1 or 2)) { @@ -77,6 +107,7 @@ public IActionResult Authorize() [HttpGet("api2")] [MapToApiVersion("1.0")] - [Authorize("can_use_api_2", AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] + // [Authorize("can_use_api_2", AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public IActionResult Api2() => Ok($"API2 response: {User.Identity!.Name} has scopes {string.Join(", ", User.GetScopes())}"); } diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 900619f4f011..c7bc722ca46d 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenIddict.Abstractions; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.ManagementApi.DependencyInjection; @@ -93,6 +94,11 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) // Register the ASP.NET Core host. options.UseAspNetCore(); + + options.Configure(validationOptions => + { + validationOptions.TokenValidationParameters.AuthenticationType = Constants.Security.BackOfficeAuthenticationType; + }); }); builder.Services.AddHostedService(); @@ -169,6 +175,7 @@ await manager.CreateAsync( // - use IServerAddressesFeature? // - put it in config? // should we support multiple callback URLS (for external apps)? + // check IHostingEnvironment.EnsureApplicationMainUrl RedirectUris = { new Uri("https://localhost:44331/umbraco/login/callback/") }, Permissions = { diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 97722d830576..1320a18bf15f 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -105,7 +105,14 @@ private static void AddBackOfficeAuthorizationPoliciesInternal(this IUmbracoBuil builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddAuthorization(o => CreatePolicies(o, backOfficeAuthenticationScheme)); + builder.Services.AddAuthorization(o => + { + // TODO: we should not be creating the policies twice :) clean up once we can transition to OpenIddict + CreatePolicies(o, backOfficeAuthenticationScheme); + // TODO: create the correct policies for new backoffice auth + // TODO: use constant OpenIddictServerAspNetCoreDefaults.AuthenticationScheme instead of magic string + CreatePolicies(o, "OpenIddict.Validation.AspNetCore"); + }); } private static void CreatePolicies(AuthorizationOptions options, string backOfficeAuthenticationScheme) From 10077c446dba2ee6bf1d3b3d9cc142bb49d3ac33 Mon Sep 17 00:00:00 2001 From: kjac Date: Wed, 5 Oct 2022 14:12:46 +0200 Subject: [PATCH 03/26] Redo current policies for multiple schemas + clean up auth controller --- .../BackOfficeAuthenticationController.cs | 91 ++-- .../UmbracoBuilder.BackOfficeAuth.cs | 415 ++++++------------ 2 files changed, 168 insertions(+), 338 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs index 6d50750744a6..4deac638c567 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs @@ -1,18 +1,12 @@ -using System.Globalization; -using System.Security.Claims; +using System.Security.Claims; using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.Tokens; using NSwag.Annotations; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; -using OpenIddict.Validation.AspNetCore; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.BackOffice.Security; -using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; using Umbraco.New.Cms.Web.Common.Routing; @@ -39,75 +33,40 @@ public BackOfficeAuthenticationController(IHttpContextAccessor httpContextAccess public async Task Authorize() { HttpContext context = _httpContextAccessor.GetRequiredHttpContext(); - OpenIddictRequest request = context.GetOpenIddictServerRequest() ?? throw new ApplicationException("TODO: something descriptive"); + OpenIddictRequest? request = context.GetOpenIddictServerRequest(); + if (request == null) + { + return BadRequest("Unable to obtain OpenID data from the current request"); + } if (request.Username != null && request.Password != null) { Microsoft.AspNetCore.Identity.SignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(request.Username, request.Password, true, true); - if (result.Succeeded) + if (result.Succeeded) { - // TODO: what does this return if username foes not exist? shouldn't be possible, but hey... BackOfficeIdentityUser backOfficeUser = await _backOfficeUserManager.FindByNameAsync(request.Username); - ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); - backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Id); - - // TODO: not optimal at all, see if we can find a way that doesn't require us passing every last claim to the token - Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); - foreach (Claim backOfficeClaim in backOfficeClaims) + // yes, back office user can be null despite nullable reference types saying otherwise. + // it is highly unlikely though, since we just managed to sign in the user above. + if (backOfficeUser != null) { - backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); - } + ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); + backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Key.ToString()); - return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); - } - } + // TODO: it is not optimal to append all claims to the token. + // the token size grows with each claim, although it is still smaller than the old cookie. + // see if we can find a better way so we do not risk leaking sensitive data in bearer tokens. + // maybe work with scopes instead? + Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); + foreach (Claim backOfficeClaim in backOfficeClaims) + { + backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); + } - int.TryParse(request["hardcoded_identity_id"]?.ToString(), out var identifier); - if (identifier is not (1 or 2)) - { - return new ChallengeResult( - new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, - new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified hardcoded identity is invalid." - })); + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); + } + } } - // Create a new identity and populate it based on the specified hardcoded identity identifier. - var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType); - identity.AddClaim(new Claim(OpenIddictConstants.Claims.Subject, identifier.ToString(CultureInfo.InvariantCulture))); - identity.AddClaim(new Claim(OpenIddictConstants.Claims.Name, identifier switch - { - 1 => "Alice", - 2 => "Bob", - _ => throw new InvalidOperationException() - }).SetDestinations(OpenIddictConstants.Destinations.AccessToken)); - - var principal = new ClaimsPrincipal(identity); - - principal.SetScopes(identifier switch - { - 1 => request.GetScopes(), - 2 => new[] { "api1" }.Intersect(request.GetScopes()), - _ => throw new InvalidOperationException() - }); - - return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal); + return new ChallengeResult(new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }); } - - [HttpGet("test")] - [MapToApiVersion("1.0")] - public IActionResult Test() => Ok("Hello"); - - [HttpGet("api1")] - [MapToApiVersion("1.0")] - [Authorize("can_use_api_1", AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] - public IActionResult Api1() => Ok($"API1 response: {User.Identity!.Name} has scopes {string.Join(", ", User.GetScopes())}"); - - [HttpGet("api2")] - [MapToApiVersion("1.0")] - // [Authorize("can_use_api_2", AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] - [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] - public IActionResult Api2() => Ok($"API2 response: {User.Identity!.Name} has scopes {string.Join(", ", User.GetScopes())}"); } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 1320a18bf15f..2a48b245545a 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -107,333 +107,204 @@ private static void AddBackOfficeAuthorizationPoliciesInternal(this IUmbracoBuil builder.Services.AddAuthorization(o => { - // TODO: we should not be creating the policies twice :) clean up once we can transition to OpenIddict CreatePolicies(o, backOfficeAuthenticationScheme); - // TODO: create the correct policies for new backoffice auth - // TODO: use constant OpenIddictServerAspNetCoreDefaults.AuthenticationScheme instead of magic string - CreatePolicies(o, "OpenIddict.Validation.AspNetCore"); }); } + // TODO: create the correct policies for new backoffice auth private static void CreatePolicies(AuthorizationOptions options, string backOfficeAuthenticationScheme) { - options.AddPolicy(AuthorizationPolicies.MediaPermissionByResource, policy => + void AddPolicy(string policyName, params IAuthorizationRequirement[] requirements) { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new MediaPermissionsResourceRequirement()); - }); + options.AddPolicy(policyName, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + // TODO: use constant OpenIddictServerAspNetCoreDefaults.AuthenticationScheme instead of magic string + policy.AuthenticationSchemes.Add("OpenIddict.Validation.AspNetCore"); + foreach (IAuthorizationRequirement requirement in requirements) + { + policy.Requirements.Add(requirement); + } + }); + } - options.AddPolicy(AuthorizationPolicies.MediaPermissionPathById, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new MediaPermissionsQueryStringRequirement("id")); - }); + AddPolicy(AuthorizationPolicies.MediaPermissionByResource, new MediaPermissionsResourceRequirement()); - options.AddPolicy(AuthorizationPolicies.ContentPermissionByResource, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new ContentPermissionsResourceRequirement()); - }); + AddPolicy(AuthorizationPolicies.MediaPermissionPathById, new MediaPermissionsQueryStringRequirement("id")); - options.AddPolicy(AuthorizationPolicies.ContentPermissionEmptyRecycleBin, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(Constants.System.RecycleBinContent, - ActionDelete.ActionLetter)); - }); + AddPolicy(AuthorizationPolicies.ContentPermissionByResource, new ContentPermissionsResourceRequirement()); - options.AddPolicy(AuthorizationPolicies.ContentPermissionAdministrationById, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter)); - policy.Requirements.Add( - new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter, "contentId")); - }); + AddPolicy( + AuthorizationPolicies.ContentPermissionEmptyRecycleBin, + new ContentPermissionsQueryStringRequirement(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)); - options.AddPolicy(AuthorizationPolicies.ContentPermissionProtectById, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter)); - policy.Requirements.Add( - new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, "contentId")); - }); + AddPolicy( + AuthorizationPolicies.ContentPermissionAdministrationById, + new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter), + new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter, "contentId")); - options.AddPolicy(AuthorizationPolicies.ContentPermissionRollbackById, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter)); - policy.Requirements.Add( - new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter, "contentId")); - }); + AddPolicy( + AuthorizationPolicies.ContentPermissionProtectById, + new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter), + new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, "contentId")); - options.AddPolicy(AuthorizationPolicies.ContentPermissionPublishById, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter)); - }); + AddPolicy( + AuthorizationPolicies.ContentPermissionRollbackById, + new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter), + new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter, "contentId")); - options.AddPolicy(AuthorizationPolicies.ContentPermissionBrowseById, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter)); - policy.Requirements.Add( - new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter, "contentId")); - }); + AddPolicy( + AuthorizationPolicies.ContentPermissionPublishById, + new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter)); - options.AddPolicy(AuthorizationPolicies.ContentPermissionDeleteById, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); - }); + AddPolicy( + AuthorizationPolicies.ContentPermissionBrowseById, + new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter), + new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter, "contentId")); - options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new BackOfficeRequirement()); - }); + AddPolicy( + AuthorizationPolicies.ContentPermissionDeleteById, + new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); - options.AddPolicy(AuthorizationPolicies.BackOfficeAccessWithoutApproval, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new BackOfficeRequirement(false)); - }); + AddPolicy(AuthorizationPolicies.BackOfficeAccess, new BackOfficeRequirement()); - options.AddPolicy(AuthorizationPolicies.AdminUserEditsRequireAdmin, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new AdminUsersRequirement()); - policy.Requirements.Add(new AdminUsersRequirement("userIds")); - }); + AddPolicy(AuthorizationPolicies.BackOfficeAccessWithoutApproval, new BackOfficeRequirement(false)); - options.AddPolicy(AuthorizationPolicies.UserBelongsToUserGroupInRequest, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new UserGroupRequirement()); - policy.Requirements.Add(new UserGroupRequirement("userGroupIds")); - }); + AddPolicy( + AuthorizationPolicies.AdminUserEditsRequireAdmin, + new AdminUsersRequirement(), + new AdminUsersRequirement("userIds")); - options.AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new DenyLocalLoginRequirement()); - }); + AddPolicy( + AuthorizationPolicies.UserBelongsToUserGroupInRequest, + new UserGroupRequirement(), + new UserGroupRequirement("userGroupIds")); - options.AddPolicy(AuthorizationPolicies.SectionAccessContent, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement(Constants.Applications.Content)); - }); + AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, new DenyLocalLoginRequirement()); - options.AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add( - new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media)); - }); + AddPolicy(AuthorizationPolicies.SectionAccessContent, new SectionRequirement(Constants.Applications.Content)); - options.AddPolicy(AuthorizationPolicies.SectionAccessUsers, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement(Constants.Applications.Users)); - }); + AddPolicy( + AuthorizationPolicies.SectionAccessContentOrMedia, + new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media)); - options.AddPolicy(AuthorizationPolicies.SectionAccessForTinyMce, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); - }); + AddPolicy(AuthorizationPolicies.SectionAccessUsers, new SectionRequirement(Constants.Applications.Users)); - options.AddPolicy(AuthorizationPolicies.SectionAccessMedia, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement(Constants.Applications.Media)); - }); + AddPolicy( + AuthorizationPolicies.SectionAccessForTinyMce, + new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); - options.AddPolicy(AuthorizationPolicies.SectionAccessMembers, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement(Constants.Applications.Members)); - }); + AddPolicy(AuthorizationPolicies.SectionAccessMedia, new SectionRequirement(Constants.Applications.Media)); - options.AddPolicy(AuthorizationPolicies.SectionAccessPackages, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement(Constants.Applications.Packages)); - }); + AddPolicy(AuthorizationPolicies.SectionAccessMembers, new SectionRequirement(Constants.Applications.Members)); - options.AddPolicy(AuthorizationPolicies.SectionAccessSettings, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement(Constants.Applications.Settings)); - }); + AddPolicy(AuthorizationPolicies.SectionAccessPackages, new SectionRequirement(Constants.Applications.Packages)); + + AddPolicy(AuthorizationPolicies.SectionAccessSettings, new SectionRequirement(Constants.Applications.Settings)); // We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. - options.AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, - Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); - }); - options.AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, - Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); - }); - options.AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); - }); + AddPolicy( + AuthorizationPolicies.SectionAccessForContentTree, + new SectionRequirement( + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Users, + Constants.Applications.Settings, + Constants.Applications.Packages, + Constants.Applications.Members)); + + AddPolicy( + AuthorizationPolicies.SectionAccessForMediaTree, + new SectionRequirement( + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Users, + Constants.Applications.Settings, + Constants.Applications.Packages, + Constants.Applications.Members)); + + AddPolicy( + AuthorizationPolicies.SectionAccessForMemberTree, + new SectionRequirement( + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Members)); // Permission is granted to this policy if the user has access to any of these sections: Content, media, settings, developer, members - options.AddPolicy(AuthorizationPolicies.SectionAccessForDataTypeReading, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new SectionRequirement( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, - Constants.Applications.Settings, Constants.Applications.Packages)); - }); + AddPolicy( + AuthorizationPolicies.SectionAccessForDataTypeReading, + new SectionRequirement( + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Members, + Constants.Applications.Settings, + Constants.Applications.Packages)); - options.AddPolicy(AuthorizationPolicies.TreeAccessDocuments, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Content)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessDocuments, new TreeRequirement(Constants.Trees.Content)); - options.AddPolicy(AuthorizationPolicies.TreeAccessUsers, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Users)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessUsers, new TreeRequirement(Constants.Trees.Users)); - options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViews)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, new TreeRequirement(Constants.Trees.PartialViews)); - options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViewMacros)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, new TreeRequirement(Constants.Trees.PartialViewMacros)); - options.AddPolicy(AuthorizationPolicies.TreeAccessPackages, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Packages)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessPackages, new TreeRequirement(Constants.Trees.Packages)); - options.AddPolicy(AuthorizationPolicies.TreeAccessLogs, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.LogViewer)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessLogs, new TreeRequirement(Constants.Trees.LogViewer)); - options.AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, new TreeRequirement(Constants.Trees.DataTypes)); - options.AddPolicy(AuthorizationPolicies.TreeAccessTemplates, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Templates)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessTemplates, new TreeRequirement(Constants.Trees.Templates)); - options.AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, new TreeRequirement(Constants.Trees.MemberTypes)); - options.AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.RelationTypes)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, new TreeRequirement(Constants.Trees.RelationTypes)); - options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, new TreeRequirement(Constants.Trees.DocumentTypes)); - options.AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberGroups)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, new TreeRequirement(Constants.Trees.MemberGroups)); - options.AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, new TreeRequirement(Constants.Trees.MediaTypes)); - options.AddPolicy(AuthorizationPolicies.TreeAccessMacros, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Macros)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessMacros, new TreeRequirement(Constants.Trees.Macros)); - options.AddPolicy(AuthorizationPolicies.TreeAccessLanguages, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Languages)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessLanguages, new TreeRequirement(Constants.Trees.Languages)); - options.AddPolicy(AuthorizationPolicies.TreeAccessDictionary, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary)); - }); + AddPolicy(AuthorizationPolicies.TreeAccessDictionary, new TreeRequirement(Constants.Trees.Dictionary)); - options.AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); - }); + AddPolicy( + AuthorizationPolicies.TreeAccessDictionaryOrTemplates, + new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); - options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); - }); + AddPolicy( + AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, + new TreeRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); - options.AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); - }); + AddPolicy( + AuthorizationPolicies.TreeAccessMediaOrMediaTypes, + new TreeRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); - options.AddPolicy(AuthorizationPolicies.TreeAccessMembersOrMemberTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); - }); + AddPolicy( + AuthorizationPolicies.TreeAccessMembersOrMemberTypes, + new TreeRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); - options.AddPolicy(AuthorizationPolicies.TreeAccessAnySchemaTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, - Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)); - }); + AddPolicy( + AuthorizationPolicies.TreeAccessAnySchemaTypes, + new TreeRequirement( + Constants.Trees.DataTypes, + Constants.Trees.DocumentTypes, + Constants.Trees.MediaTypes, + Constants.Trees.MemberTypes)); - options.AddPolicy(AuthorizationPolicies.TreeAccessAnyContentOrTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement( - Constants.Trees.DocumentTypes, Constants.Trees.Content, - Constants.Trees.MediaTypes, Constants.Trees.Media, - Constants.Trees.MemberTypes, Constants.Trees.Members)); - }); + AddPolicy( + AuthorizationPolicies.TreeAccessAnyContentOrTypes, + new TreeRequirement( + Constants.Trees.DocumentTypes, + Constants.Trees.Content, + Constants.Trees.MediaTypes, + Constants.Trees.Media, + Constants.Trees.MemberTypes, + Constants.Trees.Members)); } } From 98d24ce206f0e6115fd82b09d4805a50ff1c4efb Mon Sep 17 00:00:00 2001 From: kjac Date: Wed, 5 Oct 2022 14:39:45 +0200 Subject: [PATCH 04/26] Fix bad merge --- .../Umbraco.Cms.ManagementApi.csproj | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj index 87e67028347d..5d5d8d979669 100644 --- a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj +++ b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj @@ -47,7 +47,7 @@ - + @@ -55,8 +55,5 @@ - - - - + From b1419661b76738321e27401d48420c9e9196ab45 Mon Sep 17 00:00:00 2001 From: kjac Date: Wed, 5 Oct 2022 15:06:34 +0200 Subject: [PATCH 05/26] Clean up some more test code --- .../BackOfficeAuthBuilderExtensions.cs | 91 ++++++++----------- 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index c7bc722ca46d..8872d6fa3658 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -1,7 +1,4 @@ -using System.Collections.Immutable; -using System.Security.Claims; -using Microsoft.AspNetCore.Authentication; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenIddict.Abstractions; @@ -16,10 +13,7 @@ public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder b { builder .AddDbContext() - .AddOpenIddict() - .AddAuthorizationPolicies(); - - builder.Services.AddTransient(); + .AddOpenIddict(); return builder; } @@ -75,10 +69,10 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) // TODO: use an actual certificate here .AddDevelopmentSigningCertificate(); - // Register available scopes - options - // TODO: figure out appropriate scopes (sections? trees?) - .RegisterScopes("api1", "api2"); + // // Register available scopes + // // TODO: if we want to use scopes, we need to setup the available ones here + // options + // .RegisterScopes("some_scope", "another_scope"); // Register the ASP.NET Core host and configure for custom authentication endpoint. options @@ -106,45 +100,34 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) return builder; } - private static IUmbracoBuilder AddAuthorizationPolicies(this IUmbracoBuilder builder) - { - builder.Services.AddAuthorization(options => - { - // TODO: actual policies for APIs here - options.AddPolicy("can_use_api_1", policy => policy.RequireClaim("scope", "api1")); - options.AddPolicy("can_use_api_2", policy => policy.RequireClaim("scope", "api2")); - }); - - return builder; - } - - // TODO: move this somewhere (find an appropriate namespace for it) - public class ScopeClaimsTransformation : IClaimsTransformation - { - public Task TransformAsync(ClaimsPrincipal principal) - { - if (principal.HasClaim("scope") == false) - { - return Task.FromResult(principal); - } - - ImmutableArray knownScopeClaims = principal.GetClaims("scope"); - var missingScopeClaims = knownScopeClaims.SelectMany(s => s.Split(' ')).Except(knownScopeClaims).ToArray(); - if (missingScopeClaims.Any() == false) - { - return Task.FromResult(principal); - } - - var claimsIdentity = new ClaimsIdentity(); - foreach (var missingScopeClaim in missingScopeClaims) - { - claimsIdentity.AddClaim("scope", missingScopeClaim); - } - - principal.AddIdentity(claimsIdentity); - return Task.FromResult(principal); - } - } + // TODO: if we want to use scopes instead of claims, this will come in handy! + // install with builder.Services.AddTransient(); + // public class ScopeClaimsTransformation : IClaimsTransformation + // { + // public Task TransformAsync(ClaimsPrincipal principal) + // { + // if (principal.HasClaim("scope") == false) + // { + // return Task.FromResult(principal); + // } + // + // ImmutableArray knownScopeClaims = principal.GetClaims("scope"); + // var missingScopeClaims = knownScopeClaims.SelectMany(s => s.Split(' ')).Except(knownScopeClaims).ToArray(); + // if (missingScopeClaims.Any() == false) + // { + // return Task.FromResult(principal); + // } + // + // var claimsIdentity = new ClaimsIdentity(); + // foreach (var missingScopeClaim in missingScopeClaims) + // { + // claimsIdentity.AddClaim("scope", missingScopeClaim); + // } + // + // principal.AddIdentity(claimsIdentity); + // return Task.FromResult(principal); + // } + // } // TODO: move this somewhere (find an appropriate namespace for it) public class ClientIdManager : IHostedService @@ -183,9 +166,9 @@ await manager.CreateAsync( OpenIddictConstants.Permissions.Endpoints.Token, OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, OpenIddictConstants.Permissions.ResponseTypes.Code, - // TODO: figure out appropriate scopes (sections? trees?) - OpenIddictConstants.Permissions.Prefixes.Scope + "api1", - OpenIddictConstants.Permissions.Prefixes.Scope + "api2" + // TODO: if we're going with scopes, we may need to add them here + // OpenIddictConstants.Permissions.Prefixes.Scope + "some_scope", + // OpenIddictConstants.Permissions.Prefixes.Scope + "another_scope" } }, cancellationToken); From d8c3547262088c2844a65e04c80e762ba66ed599 Mon Sep 17 00:00:00 2001 From: kjac Date: Wed, 5 Oct 2022 16:18:25 +0200 Subject: [PATCH 06/26] Fix spacing --- .../BackOfficeAuthenticationController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs index 4deac638c567..264c63ce4183 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs @@ -42,7 +42,7 @@ public async Task Authorize() if (request.Username != null && request.Password != null) { Microsoft.AspNetCore.Identity.SignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(request.Username, request.Password, true, true); - if (result.Succeeded) + if (result.Succeeded) { BackOfficeIdentityUser backOfficeUser = await _backOfficeUserManager.FindByNameAsync(request.Username); // yes, back office user can be null despite nullable reference types saying otherwise. From ab702e09a77ed8c5a7f9583b3335a2450a94bf07 Mon Sep 17 00:00:00 2001 From: kjac Date: Thu, 6 Oct 2022 11:41:24 +0200 Subject: [PATCH 07/26] Include AddAuthentication() in OpenIddict addition --- .../DependencyInjection/BackOfficeAuthBuilderExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 8872d6fa3658..b71278206b26 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -35,6 +35,8 @@ private static IUmbracoBuilder AddDbContext(this IUmbracoBuilder builder) private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) { + builder.Services.AddAuthentication(); + builder.Services.AddOpenIddict() // Register the OpenIddict core components. From 1b85e8ee3acdaec5a986c73b41863af310ebb5cf Mon Sep 17 00:00:00 2001 From: kjac Date: Thu, 6 Oct 2022 14:42:37 +0200 Subject: [PATCH 08/26] A little more clean-up --- .../BackOfficeAuthBuilderExtensions.cs | 52 +++---------------- 1 file changed, 7 insertions(+), 45 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index b71278206b26..f8817278f437 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -60,22 +60,14 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) .AllowAuthorizationCodeFlow() .RequireProofKeyForCodeExchange(); - // Register the encryption credentials. + // Register the encryption and signing credentials. + // - see https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html options - // TODO: use an actual key, i.e. options.AddEncryptionKey(new SymmetricSecurityKey(..)); + // TODO: use actual certificates here, see docs above .AddDevelopmentEncryptionCertificate() + .AddDevelopmentSigningCertificate() .DisableAccessTokenEncryption(); - // Register the signing credentials. - options - // TODO: use an actual certificate here - .AddDevelopmentSigningCertificate(); - - // // Register available scopes - // // TODO: if we want to use scopes, we need to setup the available ones here - // options - // .RegisterScopes("some_scope", "another_scope"); - // Register the ASP.NET Core host and configure for custom authentication endpoint. options .UseAspNetCore() @@ -91,6 +83,8 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) // Register the ASP.NET Core host. options.UseAspNetCore(); + // TODO: this is a workaround to make validated principals be perceived as explicit backoffice users by ClaimsPrincipalExtensions.GetUmbracoIdentity + // we may not need it once cookie auth for backoffice is removed - validate and clean up if necessary options.Configure(validationOptions => { validationOptions.TokenValidationParameters.AuthenticationType = Constants.Security.BackOfficeAuthenticationType; @@ -102,35 +96,6 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) return builder; } - // TODO: if we want to use scopes instead of claims, this will come in handy! - // install with builder.Services.AddTransient(); - // public class ScopeClaimsTransformation : IClaimsTransformation - // { - // public Task TransformAsync(ClaimsPrincipal principal) - // { - // if (principal.HasClaim("scope") == false) - // { - // return Task.FromResult(principal); - // } - // - // ImmutableArray knownScopeClaims = principal.GetClaims("scope"); - // var missingScopeClaims = knownScopeClaims.SelectMany(s => s.Split(' ')).Except(knownScopeClaims).ToArray(); - // if (missingScopeClaims.Any() == false) - // { - // return Task.FromResult(principal); - // } - // - // var claimsIdentity = new ClaimsIdentity(); - // foreach (var missingScopeClaim in missingScopeClaims) - // { - // claimsIdentity.AddClaim("scope", missingScopeClaim); - // } - // - // principal.AddIdentity(claimsIdentity); - // return Task.FromResult(principal); - // } - // } - // TODO: move this somewhere (find an appropriate namespace for it) public class ClientIdManager : IHostedService { @@ -167,10 +132,7 @@ await manager.CreateAsync( OpenIddictConstants.Permissions.Endpoints.Authorization, OpenIddictConstants.Permissions.Endpoints.Token, OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, - OpenIddictConstants.Permissions.ResponseTypes.Code, - // TODO: if we're going with scopes, we may need to add them here - // OpenIddictConstants.Permissions.Prefixes.Scope + "some_scope", - // OpenIddictConstants.Permissions.Prefixes.Scope + "another_scope" + OpenIddictConstants.Permissions.ResponseTypes.Code } }, cancellationToken); From 4c61d4e326fc3d6d5ee2f1e3d15eb41972fbac05 Mon Sep 17 00:00:00 2001 From: kjac Date: Sun, 9 Oct 2022 14:35:58 +0200 Subject: [PATCH 09/26] Move application creation to its own implementation + prepare for middleware to handle valid callback URL --- .../BackOfficeApplicationManager.cs | 47 ++++++++++++++ .../IBackOfficeApplicationManager.cs | 6 ++ .../BackOfficeAuthBuilderExtensions.cs | 44 ++++--------- ...ceAuthorizationInitializationMiddleware.cs | 62 +++++++++++++++++++ 4 files changed, 127 insertions(+), 32 deletions(-) create mode 100644 src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs new file mode 100644 index 000000000000..5148c56f655d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs @@ -0,0 +1,47 @@ +using OpenIddict.Abstractions; + +namespace Umbraco.Cms.ManagementApi.Authorization; + +public class BackOfficeApplicationManager : IBackOfficeApplicationManager +{ + private const string BackOfficeClientId = "umbraco-back-office"; + + private readonly IOpenIddictApplicationManager _applicationManager; + + public BackOfficeApplicationManager(IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + public async Task EnsureBackOfficeApplicationAsync(Uri url, CancellationToken cancellationToken = default) + { + if (url.IsAbsoluteUri == false) + { + throw new ArgumentException($"Expected an absolute URL, got: {url}", nameof(url)); + } + + var clientDescriptor = new OpenIddictApplicationDescriptor + { + ClientId = BackOfficeClientId, + RedirectUris = { CallbackUrlFor(url) }, + Type = OpenIddictConstants.ClientTypes.Public, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.ResponseTypes.Code + } + }; + + var client = await _applicationManager.FindByClientIdAsync(BackOfficeClientId, cancellationToken); + if (client == null) + { + await _applicationManager.CreateAsync(clientDescriptor, cancellationToken); + } + else + { + await _applicationManager.UpdateAsync(client, clientDescriptor, cancellationToken); + } + } + + private static Uri CallbackUrlFor(Uri url) => new Uri( $"{url.GetLeftPart(UriPartial.Authority)}/umbraco/login/callback/"); +} diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs new file mode 100644 index 000000000000..697951a40457 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.Authorization; + +public interface IBackOfficeApplicationManager +{ + Task EnsureBackOfficeApplicationAsync(Uri url, CancellationToken cancellationToken = default); +} diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index f8817278f437..0453c4f2154b 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using OpenIddict.Abstractions; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.ManagementApi.Authorization; +using Umbraco.Cms.ManagementApi.Middleware; namespace Umbraco.Cms.ManagementApi.DependencyInjection; @@ -91,17 +92,20 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) }); }); - builder.Services.AddHostedService(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + + builder.Services.AddHostedService(); return builder; } - // TODO: move this somewhere (find an appropriate namespace for it) - public class ClientIdManager : IHostedService + // TODO: remove this once EF is implemented + public class DatabaseManager : IHostedService { private readonly IServiceProvider _serviceProvider; - public ClientIdManager(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + public DatabaseManager(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; public async Task StartAsync(CancellationToken cancellationToken) { @@ -110,33 +114,9 @@ public async Task StartAsync(CancellationToken cancellationToken) DbContext context = scope.ServiceProvider.GetRequiredService(); await context.Database.EnsureCreatedAsync(cancellationToken); - IOpenIddictApplicationManager manager = scope.ServiceProvider.GetRequiredService(); - - const string backofficeClientId = "umbraco-back-office"; - if (await manager.FindByClientIdAsync(backofficeClientId, cancellationToken) is null) - { - await manager.CreateAsync( - new OpenIddictApplicationDescriptor - { - ClientId = backofficeClientId, - // TODO: fix redirect URI + path - // how do we figure out the current backoffice host? - // - wait for first request? - // - use IServerAddressesFeature? - // - put it in config? - // should we support multiple callback URLS (for external apps)? - // check IHostingEnvironment.EnsureApplicationMainUrl - RedirectUris = { new Uri("https://localhost:44331/umbraco/login/callback/") }, - Permissions = - { - OpenIddictConstants.Permissions.Endpoints.Authorization, - OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, - OpenIddictConstants.Permissions.ResponseTypes.Code - } - }, - cancellationToken); - } + // TODO: append BackOfficeAuthorizationInitializationMiddleware to the application and remove this + IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService(); + await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(new Uri("https://localhost:44331/"), cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/src/Umbraco.Cms.ManagementApi/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs b/src/Umbraco.Cms.ManagementApi/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs new file mode 100644 index 000000000000..93ce19906b77 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.ManagementApi.Authorization; + +namespace Umbraco.Cms.ManagementApi.Middleware; + +public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware +{ + private static bool _firstBackOfficeRequest; + private static SemaphoreSlim _firstBackOfficeRequestLocker = new(1); + + private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly IServiceProvider _serviceProvider; + + public BackOfficeAuthorizationInitializationMiddleware(UmbracoRequestPaths umbracoRequestPaths, IServiceProvider serviceProvider) + { + _umbracoRequestPaths = umbracoRequestPaths; + _serviceProvider = serviceProvider; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + await InitializeBackOfficeAuthorizationOnceAsync(context); + await next(context); + } + + private async Task InitializeBackOfficeAuthorizationOnceAsync(HttpContext context) + { + if (_firstBackOfficeRequest) + { + return; + } + + if (_umbracoRequestPaths.IsBackOfficeRequest(context.Request.Path) == false) + { + return; + } + + await _firstBackOfficeRequestLocker.WaitAsync(); + if (_firstBackOfficeRequest == false) + { + using IServiceScope scope = _serviceProvider.CreateScope(); + IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService(); + await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(new Uri(context.Request.GetDisplayUrl())); + _firstBackOfficeRequest = true; + } + + _firstBackOfficeRequestLocker.Release(); + } +} + +// TODO: remove this (used for testing BackOfficeAuthorizationInitializationMiddleware until it can be added to the existing UseBackOffice extension) +// public static class UmbracoApplicationBuilderExtensions +// { +// public static IUmbracoApplicationBuilderContext UseNewBackOffice(this IUmbracoApplicationBuilderContext builder) +// { +// builder.AppBuilder.UseMiddleware(); +// return builder; +// } +// } From c94e55d698b70a5f6df0c7c08eb51ea87b7990a4 Mon Sep 17 00:00:00 2001 From: kjac Date: Sun, 9 Oct 2022 15:26:19 +0200 Subject: [PATCH 10/26] Enable refresh token flow --- .../Authorization/BackOfficeApplicationManager.cs | 1 + .../BackOfficeAuthenticationController.cs | 6 ++++++ .../DependencyInjection/BackOfficeAuthBuilderExtensions.cs | 3 ++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs index 5148c56f655d..ee919a83e05f 100644 --- a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs @@ -28,6 +28,7 @@ public async Task EnsureBackOfficeApplicationAsync(Uri url, CancellationToken ca OpenIddictConstants.Permissions.Endpoints.Authorization, OpenIddictConstants.Permissions.Endpoints.Token, OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, OpenIddictConstants.Permissions.ResponseTypes.Code } }; diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs index 264c63ce4183..7f15d7835dfc 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs @@ -62,6 +62,12 @@ public async Task Authorize() backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); } + if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess)) + { + // "offline_access" scope is required to use refresh tokens + backOfficePrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess); + } + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); } } diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 0453c4f2154b..ad1d5063b848 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -59,7 +59,8 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) // Enable authorization code flow with PKCE options .AllowAuthorizationCodeFlow() - .RequireProofKeyForCodeExchange(); + .RequireProofKeyForCodeExchange() + .AllowRefreshTokenFlow(); // Register the encryption and signing credentials. // - see https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html From 6196260396af5f7b3ca3eae3643103c3b5804cb2 Mon Sep 17 00:00:00 2001 From: kjac Date: Thu, 13 Oct 2022 10:52:24 +0200 Subject: [PATCH 11/26] Fix bad merge from v11/dev --- .../BackOfficeAuthenticationController.cs | 4 +- .../Umbraco.Cms.ManagementApi.csproj | 38 ++----------------- 2 files changed, 4 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs index 7f15d7835dfc..4ed1a12038dc 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs @@ -44,9 +44,7 @@ public async Task Authorize() Microsoft.AspNetCore.Identity.SignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(request.Username, request.Password, true, true); if (result.Succeeded) { - BackOfficeIdentityUser backOfficeUser = await _backOfficeUserManager.FindByNameAsync(request.Username); - // yes, back office user can be null despite nullable reference types saying otherwise. - // it is highly unlikely though, since we just managed to sign in the user above. + BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(request.Username); if (backOfficeUser != null) { ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); diff --git a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj index 35ab6ebfeb96..372aeb1995d7 100644 --- a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj +++ b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj @@ -6,46 +6,14 @@ false - - net6.0 - enable - enable - nullable - Umbraco.Cms.ManagementApi - false - false - - - - - - - - - - - - - - - - - - - - all - - - - - - - + + + From d614562d1def29e0ababa561ae0c1106daa7791c Mon Sep 17 00:00:00 2001 From: kjac Date: Mon, 17 Oct 2022 10:07:34 +0200 Subject: [PATCH 12/26] Support auth for Swagger and Postman in non-production environments + use default login screen for back-office logins --- .../BackOfficeApplicationManager.cs | 125 +++++++++++++++--- .../Authorization/ClientSecretManager.cs | 21 +++ .../Authorization/Constants-OauthClientIds.cs | 23 ++++ .../BackOfficeAuthenticationController.cs | 51 +++---- .../BackOfficeAuthBuilderExtensions.cs | 5 +- .../ManagementApiComposer.cs | 26 ++++ 6 files changed, 203 insertions(+), 48 deletions(-) create mode 100644 src/Umbraco.Cms.ManagementApi/Authorization/ClientSecretManager.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Authorization/Constants-OauthClientIds.cs diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs index ee919a83e05f..c2eda781086a 100644 --- a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs @@ -1,15 +1,24 @@ -using OpenIddict.Abstractions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using OpenIddict.Abstractions; namespace Umbraco.Cms.ManagementApi.Authorization; public class BackOfficeApplicationManager : IBackOfficeApplicationManager { - private const string BackOfficeClientId = "umbraco-back-office"; - private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly IClientSecretManager _clientSecretManager; - public BackOfficeApplicationManager(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager; + public BackOfficeApplicationManager( + IOpenIddictApplicationManager applicationManager, + IWebHostEnvironment webHostEnvironment, + IClientSecretManager clientSecretManager) + { + _applicationManager = applicationManager; + _webHostEnvironment = webHostEnvironment; + _clientSecretManager = clientSecretManager; + } public async Task EnsureBackOfficeApplicationAsync(Uri url, CancellationToken cancellationToken = default) { @@ -18,22 +27,85 @@ public async Task EnsureBackOfficeApplicationAsync(Uri url, CancellationToken ca throw new ArgumentException($"Expected an absolute URL, got: {url}", nameof(url)); } - var clientDescriptor = new OpenIddictApplicationDescriptor - { - ClientId = BackOfficeClientId, - RedirectUris = { CallbackUrlFor(url) }, - Type = OpenIddictConstants.ClientTypes.Public, - Permissions = + await CreateOrUpdate( + new OpenIddictApplicationDescriptor { - OpenIddictConstants.Permissions.Endpoints.Authorization, - OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, - OpenIddictConstants.Permissions.GrantTypes.RefreshToken, - OpenIddictConstants.Permissions.ResponseTypes.Code - } - }; - - var client = await _applicationManager.FindByClientIdAsync(BackOfficeClientId, cancellationToken); + DisplayName = "Umbraco back-office access", + ClientId = Constants.OauthClientIds.BackOffice, + RedirectUris = + { + CallbackUrlFor(url, "/umbraco/login/callback/") + }, + Type = OpenIddictConstants.ClientTypes.Public, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, + OpenIddictConstants.Permissions.ResponseTypes.Code + } + }, + cancellationToken); + + if (_webHostEnvironment.IsProduction()) + { + await Delete(Constants.OauthClientIds.Swagger, cancellationToken); + await Delete(Constants.OauthClientIds.Postman, cancellationToken); + } + else + { + await CreateOrUpdate( + new OpenIddictApplicationDescriptor + { + DisplayName = "Umbraco Swagger access", + ClientId = Constants.OauthClientIds.Swagger, + // TODO: investigate the necessity of client secrets for Swagger + // this is necessary with NSwag - or maybe it's a SwaggerUI3 requirement? investigate if client + // secrets are even necessary if we switch to Swashbuckle + ClientSecret = _clientSecretManager.Get(Constants.OauthClientIds.Swagger), + RedirectUris = + { + CallbackUrlFor(url, "/umbraco/swagger/oauth2-redirect.html") + }, + Type = OpenIddictConstants.ClientTypes.Confidential, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.ResponseTypes.Code + } + }, + cancellationToken); + + await CreateOrUpdate( + new OpenIddictApplicationDescriptor + { + DisplayName = "Umbraco Postman access", + ClientId = Constants.OauthClientIds.Postman, + RedirectUris = + { + new Uri("https://oauth.pstmn.io/v1/callback") + }, + Type = OpenIddictConstants.ClientTypes.Public, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.ResponseTypes.Code + } + }, + cancellationToken); + } + } + + private async Task CreateOrUpdate(OpenIddictApplicationDescriptor clientDescriptor, CancellationToken cancellationToken) + { + var identifier = clientDescriptor.ClientId ?? + throw new ApplicationException($"ClientId is missing for application: {clientDescriptor.DisplayName ?? "(no name)"}"); + var client = await _applicationManager.FindByClientIdAsync(identifier, cancellationToken); if (client == null) { await _applicationManager.CreateAsync(clientDescriptor, cancellationToken); @@ -44,5 +116,16 @@ public async Task EnsureBackOfficeApplicationAsync(Uri url, CancellationToken ca } } - private static Uri CallbackUrlFor(Uri url) => new Uri( $"{url.GetLeftPart(UriPartial.Authority)}/umbraco/login/callback/"); + private async Task Delete(string identifier, CancellationToken cancellationToken) + { + var client = await _applicationManager.FindByClientIdAsync(identifier, cancellationToken); + if (client == null) + { + return; + } + + await _applicationManager.DeleteAsync(client, cancellationToken); + } + + private static Uri CallbackUrlFor(Uri url, string relativePath) => new Uri( $"{url.GetLeftPart(UriPartial.Authority)}/{relativePath.TrimStart(Core.Constants.CharArrays.ForwardSlash)}"); } diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/ClientSecretManager.cs b/src/Umbraco.Cms.ManagementApi/Authorization/ClientSecretManager.cs new file mode 100644 index 000000000000..55a4d0074fb1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Authorization/ClientSecretManager.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Cms.ManagementApi.Authorization; + +public class ClientSecretManager : IClientSecretManager +{ + private Dictionary _secretsByClientId = new(); + + public string Get(string clientId) + { + if (_secretsByClientId.ContainsKey(clientId) == false) + { + _secretsByClientId[clientId] = Guid.NewGuid().ToString("N"); + } + + return _secretsByClientId[clientId]; + } +} + +public interface IClientSecretManager +{ + string Get(string clientId); +} diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/Constants-OauthClientIds.cs b/src/Umbraco.Cms.ManagementApi/Authorization/Constants-OauthClientIds.cs new file mode 100644 index 000000000000..cf40b9476633 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Authorization/Constants-OauthClientIds.cs @@ -0,0 +1,23 @@ +namespace Umbraco.Cms.ManagementApi.Authorization; + +// TODO: move this class to Umbraco.Cms.Core as a partial class +public static class Constants +{ + public static partial class OauthClientIds + { + /// + /// Client ID used for default back-office access + /// + public const string BackOffice = "umbraco-back-office"; + + /// + /// Client ID used for Swagger API access + /// + public const string Swagger = "umbraco-swagger"; + + /// + /// Client ID used for Postman API access + /// + public const string Postman = "umbraco-postman"; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs index 4ed1a12038dc..1add978814b7 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs @@ -1,10 +1,12 @@ using System.Security.Claims; using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Extensions; @@ -28,6 +30,7 @@ public BackOfficeAuthenticationController(IHttpContextAccessor httpContextAccess _backOfficeUserManager = backOfficeUserManager; } + [HttpGet("authorize")] [HttpPost("authorize")] [MapToApiVersion("1.0")] public async Task Authorize() @@ -39,38 +42,36 @@ public async Task Authorize() return BadRequest("Unable to obtain OpenID data from the current request"); } - if (request.Username != null && request.Password != null) + // retrieve the user principal stored in the authentication cookie. + AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); + if (cookieAuthResult.Succeeded && cookieAuthResult.Principal?.Identity?.Name != null) { - Microsoft.AspNetCore.Identity.SignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(request.Username, request.Password, true, true); - if (result.Succeeded) + BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(cookieAuthResult.Principal.Identity.Name); + if (backOfficeUser != null) { - BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(request.Username); - if (backOfficeUser != null) - { - ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); - backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Key.ToString()); - - // TODO: it is not optimal to append all claims to the token. - // the token size grows with each claim, although it is still smaller than the old cookie. - // see if we can find a better way so we do not risk leaking sensitive data in bearer tokens. - // maybe work with scopes instead? - Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); - foreach (Claim backOfficeClaim in backOfficeClaims) - { - backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); - } + ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); + backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Key.ToString()); - if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess)) - { - // "offline_access" scope is required to use refresh tokens - backOfficePrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess); - } + // TODO: it is not optimal to append all claims to the token. + // the token size grows with each claim, although it is still smaller than the old cookie. + // see if we can find a better way so we do not risk leaking sensitive data in bearer tokens. + // maybe work with scopes instead? + Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); + foreach (Claim backOfficeClaim in backOfficeClaims) + { + backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); + } - return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); + if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess)) + { + // "offline_access" scope is required to use refresh tokens + backOfficePrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess); } + + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); } } - return new ChallengeResult(new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }); + return new ChallengeResult(new[] { Constants.Security.BackOfficeAuthenticationType }); } } diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index ad1d5063b848..6ad23591ef67 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -89,11 +89,12 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) // we may not need it once cookie auth for backoffice is removed - validate and clean up if necessary options.Configure(validationOptions => { - validationOptions.TokenValidationParameters.AuthenticationType = Constants.Security.BackOfficeAuthenticationType; + validationOptions.TokenValidationParameters.AuthenticationType = Core.Constants.Security.BackOfficeAuthenticationType; }); }); builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); @@ -115,7 +116,7 @@ public async Task StartAsync(CancellationToken cancellationToken) DbContext context = scope.ServiceProvider.GetRequiredService(); await context.Database.EnsureCreatedAsync(cancellationToken); - // TODO: append BackOfficeAuthorizationInitializationMiddleware to the application and remove this + // 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); } diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index 3a3c4e66a10b..20bb0a778577 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -8,10 +8,13 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using NSwag; using NSwag.AspNetCore; +using NSwag.Generation.Processors.Security; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.ManagementApi.Authorization; using Umbraco.Cms.ManagementApi.Configuration; using Umbraco.Cms.ManagementApi.DependencyInjection; using Umbraco.Cms.Web.Common.ApplicationBuilder; @@ -64,6 +67,20 @@ public void Compose(IUmbracoBuilder builder) { document.Tags = document.Tags.OrderBy(tag => tag.Name).ToList(); }; + + options.AddSecurity("Bearer", Enumerable.Empty(), new OpenApiSecurityScheme + { + Name = "Umbraco", + Type = OpenApiSecuritySchemeType.OAuth2, + Description = "Umbraco Authentication", + Flow = OpenApiOAuth2Flow.AccessCode, + AuthorizationUrl = "/umbraco/api/v1.0/back-office-authentication/authorize", + TokenUrl = "/umbraco/api/v1.0/back-office-authentication/token", + Scopes = new Dictionary(), + }); + // this is documented in OAuth2 setup for swagger, but does not seem to be necessary at the moment. + // it is worth try it if operation authentication starts failing. + // options.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("Bearer")); }); services.AddVersionedApiExplorer(options => @@ -111,6 +128,7 @@ public void Compose(IUmbracoBuilder builder) { GlobalSettings? settings = provider.GetRequiredService>().Value; IHostingEnvironment hostingEnvironment = provider.GetRequiredService(); + IClientSecretManager clientSecretManager = provider.GetRequiredService(); var officePath = settings.GetBackOfficePath(hostingEnvironment); // serve documents (same as app.UseSwagger()) applicationBuilder.UseOpenApi(config => @@ -127,6 +145,14 @@ public void Compose(IUmbracoBuilder builder) config.SwaggerRoutes.Add(new SwaggerUi3Route(ApiAllName, swaggerPath)); config.OperationsSorter = "alpha"; config.TagsSorter = "alpha"; + + config.OAuth2Client = new OAuth2ClientSettings + { + AppName = "Umbraco", + UsePkceWithAuthorizationCodeGrant = true, + ClientId = Constants.OauthClientIds.Swagger, + ClientSecret = clientSecretManager.Get(Constants.OauthClientIds.Swagger) + }; }); } }, From 6c498680f5d28d0e63e87c082954ba95aa7aefa6 Mon Sep 17 00:00:00 2001 From: kjac Date: Wed, 19 Oct 2022 17:28:53 +0200 Subject: [PATCH 13/26] Add workaround to client side login handling so the OAuth return URL is not corrupted before redirection --- .../src/views/common/login.controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js index 5827f7e5306c..89e963b56074 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js @@ -11,7 +11,11 @@ angular.module('umbraco').controller("Umbraco.LoginController", function (events //check if there's a returnPath query string, if so redirect to it var locationObj = $location.search(); if (locationObj.returnPath) { - path = decodeURIComponent(locationObj.returnPath); + // decodeURIComponent(...) does not play nice with OAuth redirect URLs, so until we have a + // dedicated login screen for the new back-office, we need to hardcode this exception + path = locationObj.returnPath.indexOf("/back-office-authentication/authorize") > 0 + ? locationObj.returnPath + : decodeURIComponent(locationObj.returnPath); } // Ensure path is not absolute From cccc8c5d7bbddf6534d761b1a326c51cfd72c5b1 Mon Sep 17 00:00:00 2001 From: kjac Date: Wed, 19 Oct 2022 17:30:26 +0200 Subject: [PATCH 14/26] Add temporary configuration handling for new backoffice --- .../BackOfficeApplicationManager.cs | 17 ++++++++----- .../IBackOfficeApplicationManager.cs | 2 +- .../ManagementApiComposer.cs | 5 +++- .../UmbracoBuilder.Configuration.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 4 +++ .../Configuration/NewBackOfficeSettings.cs | 11 ++++++++ .../NewBackOfficeSettingsValidator.cs | 25 +++++++++++++++++++ 7 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs index c2eda781086a..f958de836834 100644 --- a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +using Umbraco.New.Cms.Core.Models.Configuration; namespace Umbraco.Cms.ManagementApi.Authorization; @@ -9,22 +11,25 @@ public class BackOfficeApplicationManager : IBackOfficeApplicationManager private readonly IOpenIddictApplicationManager _applicationManager; private readonly IWebHostEnvironment _webHostEnvironment; private readonly IClientSecretManager _clientSecretManager; + private readonly Uri? _backOfficeHost; public BackOfficeApplicationManager( IOpenIddictApplicationManager applicationManager, IWebHostEnvironment webHostEnvironment, - IClientSecretManager clientSecretManager) + IClientSecretManager clientSecretManager, + IOptionsMonitor securitySettingsMonitor) { _applicationManager = applicationManager; _webHostEnvironment = webHostEnvironment; _clientSecretManager = clientSecretManager; + _backOfficeHost = securitySettingsMonitor.CurrentValue.BackOfficeHost; } - public async Task EnsureBackOfficeApplicationAsync(Uri url, CancellationToken cancellationToken = default) + public async Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default) { - if (url.IsAbsoluteUri == false) + if (backOfficeUrl.IsAbsoluteUri == false) { - throw new ArgumentException($"Expected an absolute URL, got: {url}", nameof(url)); + throw new ArgumentException($"Expected an absolute URL, got: {backOfficeUrl}", nameof(backOfficeUrl)); } await CreateOrUpdate( @@ -34,7 +39,7 @@ await CreateOrUpdate( ClientId = Constants.OauthClientIds.BackOffice, RedirectUris = { - CallbackUrlFor(url, "/umbraco/login/callback/") + CallbackUrlFor(_backOfficeHost ?? backOfficeUrl, "/umbraco/login/callback/") }, Type = OpenIddictConstants.ClientTypes.Public, Permissions = @@ -66,7 +71,7 @@ await CreateOrUpdate( ClientSecret = _clientSecretManager.Get(Constants.OauthClientIds.Swagger), RedirectUris = { - CallbackUrlFor(url, "/umbraco/swagger/oauth2-redirect.html") + CallbackUrlFor(backOfficeUrl, "/umbraco/swagger/oauth2-redirect.html") }, Type = OpenIddictConstants.ClientTypes.Confidential, Permissions = diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs index 697951a40457..5a0e327950a2 100644 --- a/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs @@ -2,5 +2,5 @@ public interface IBackOfficeApplicationManager { - Task EnsureBackOfficeApplicationAsync(Uri url, CancellationToken cancellationToken = default); + Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default); } diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index 20bb0a778577..51aeefe55c88 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.Options; using NSwag; using NSwag.AspNetCore; -using NSwag.Generation.Processors.Security; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; @@ -19,6 +18,7 @@ using Umbraco.Cms.ManagementApi.DependencyInjection; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Models.Configuration; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.ManagementApi; @@ -94,6 +94,9 @@ public void Compose(IUmbracoBuilder builder) services.AddControllers(); builder.Services.ConfigureOptions(); + builder.AddUmbracoOptions(); + builder.Services.AddSingleton, NewBackOfficeSettingsValidator>(); + builder.Services.Configure(options => { options.AddFilter(new UmbracoPipelineFilter( diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 31ef06c4002f..3be3815afa47 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.DependencyInjection; /// public static partial class UmbracoBuilderExtensions { - private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action>? configure = null) + internal static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action>? configure = null) where TOptions : class { UmbracoOptionsAttribute? umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 03a292c880e6..dc480b160f14 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -47,6 +47,10 @@ <_Parameter1>DynamicProxyGenAssembly2 + + + <_Parameter1>Umbraco.Cms.ManagementApi + diff --git a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs new file mode 100644 index 000000000000..4dcdd7e17e3d --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.New.Cms.Core.Models.Configuration; + +/// TODO: merge this class with relevant existing settings from Core and clean up +[UmbracoOptions($"{Constants.Configuration.ConfigPrefix}NewBackOffice")] +public class NewBackOfficeSettings +{ + public Uri? BackOfficeHost { get; set; } = null; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs new file mode 100644 index 000000000000..840abca30f31 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models.Validation; + +namespace Umbraco.New.Cms.Core.Models.Configuration; + +/// TODO: merge this class with relevant existing settings validators from Core and clean up +public class NewBackOfficeSettingsValidator : ConfigurationValidatorBase, IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, NewBackOfficeSettings options) + { + if (options.BackOfficeHost != null) + { + if (options.BackOfficeHost.IsAbsoluteUri == false) + { + return ValidateOptionsResult.Fail($"{nameof(NewBackOfficeSettings.BackOfficeHost)} must be an absolute URL"); + } + if (options.BackOfficeHost.PathAndQuery != "/") + { + return ValidateOptionsResult.Fail($"{nameof(NewBackOfficeSettings.BackOfficeHost)} must not have any path or query"); + } + } + + return ValidateOptionsResult.Success; + } +} From c142d2234f21d6f1ba37cb7f88fb5e3de489b157 Mon Sep 17 00:00:00 2001 From: kjac Date: Thu, 20 Oct 2022 17:36:19 +0200 Subject: [PATCH 15/26] Restructure the code somewhat, move singular responsibility from management API project --- .../DependencyInjection/BackOfficeAuthBuilderExtensions.cs | 4 ++-- src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs | 3 ++- .../BackOfficeAuthorizationInitializationMiddleware.cs | 2 +- .../BackOfficeApplicationManager.cs | 4 +++- .../{Authorization => Security}/ClientSecretManager.cs | 7 +------ .../Security/IClientSecretManager.cs | 6 ++++++ .../Constants-OauthClientIds.cs | 2 +- .../Models/Configuration/NewBackOfficeSettings.cs | 5 ++--- .../Security}/IBackOfficeApplicationManager.cs | 2 +- .../Umbraco.New.Cms.Infrastructure.csproj | 4 ++++ 10 files changed, 23 insertions(+), 16 deletions(-) rename src/Umbraco.Cms.ManagementApi/{Authorization => Security}/BackOfficeApplicationManager.cs (98%) rename src/Umbraco.Cms.ManagementApi/{Authorization => Security}/ClientSecretManager.cs (74%) create mode 100644 src/Umbraco.Cms.ManagementApi/Security/IClientSecretManager.cs rename src/{Umbraco.Cms.ManagementApi/Authorization => Umbraco.New.Cms.Core}/Constants-OauthClientIds.cs (92%) rename src/{Umbraco.Cms.ManagementApi/Authorization => Umbraco.New.Cms.Infrastructure/Security}/IBackOfficeApplicationManager.cs (74%) diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 6ad23591ef67..461f418a5ca9 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -1,10 +1,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.ManagementApi.Authorization; using Umbraco.Cms.ManagementApi.Middleware; +using Umbraco.Cms.ManagementApi.Security; +using Umbraco.New.Cms.Infrastructure.Security; namespace Umbraco.Cms.ManagementApi.DependencyInjection; diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index 51aeefe55c88..7f6576925f1d 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -13,11 +13,12 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.ManagementApi.Authorization; using Umbraco.Cms.ManagementApi.Configuration; using Umbraco.Cms.ManagementApi.DependencyInjection; +using Umbraco.Cms.ManagementApi.Security; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Extensions; +using Umbraco.New.Cms.Core; using Umbraco.New.Cms.Core.Models.Configuration; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; diff --git a/src/Umbraco.Cms.ManagementApi/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs b/src/Umbraco.Cms.ManagementApi/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs index 93ce19906b77..6ecebb33623c 100644 --- a/src/Umbraco.Cms.ManagementApi/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs +++ b/src/Umbraco.Cms.ManagementApi/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.ManagementApi.Authorization; +using Umbraco.New.Cms.Infrastructure.Security; namespace Umbraco.Cms.ManagementApi.Middleware; diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs similarity index 98% rename from src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs rename to src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs index f958de836834..d3a4acac64eb 100644 --- a/src/Umbraco.Cms.ManagementApi/Authorization/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs @@ -2,9 +2,11 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +using Umbraco.New.Cms.Core; using Umbraco.New.Cms.Core.Models.Configuration; +using Umbraco.New.Cms.Infrastructure.Security; -namespace Umbraco.Cms.ManagementApi.Authorization; +namespace Umbraco.Cms.ManagementApi.Security; public class BackOfficeApplicationManager : IBackOfficeApplicationManager { diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/ClientSecretManager.cs b/src/Umbraco.Cms.ManagementApi/Security/ClientSecretManager.cs similarity index 74% rename from src/Umbraco.Cms.ManagementApi/Authorization/ClientSecretManager.cs rename to src/Umbraco.Cms.ManagementApi/Security/ClientSecretManager.cs index 55a4d0074fb1..cbea254ed4a8 100644 --- a/src/Umbraco.Cms.ManagementApi/Authorization/ClientSecretManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Security/ClientSecretManager.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.ManagementApi.Authorization; +namespace Umbraco.Cms.ManagementApi.Security; public class ClientSecretManager : IClientSecretManager { @@ -14,8 +14,3 @@ public string Get(string clientId) return _secretsByClientId[clientId]; } } - -public interface IClientSecretManager -{ - string Get(string clientId); -} diff --git a/src/Umbraco.Cms.ManagementApi/Security/IClientSecretManager.cs b/src/Umbraco.Cms.ManagementApi/Security/IClientSecretManager.cs new file mode 100644 index 000000000000..7744169b7a83 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Security/IClientSecretManager.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.Security; + +public interface IClientSecretManager +{ + string Get(string clientId); +} diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/Constants-OauthClientIds.cs b/src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs similarity index 92% rename from src/Umbraco.Cms.ManagementApi/Authorization/Constants-OauthClientIds.cs rename to src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs index cf40b9476633..2fdc54e0118a 100644 --- a/src/Umbraco.Cms.ManagementApi/Authorization/Constants-OauthClientIds.cs +++ b/src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.ManagementApi.Authorization; +namespace Umbraco.New.Cms.Core; // TODO: move this class to Umbraco.Cms.Core as a partial class public static class Constants diff --git a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs index 4dcdd7e17e3d..9784afb44114 100644 --- a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs +++ b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs @@ -1,10 +1,9 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.New.Cms.Core.Models.Configuration; /// TODO: merge this class with relevant existing settings from Core and clean up -[UmbracoOptions($"{Constants.Configuration.ConfigPrefix}NewBackOffice")] +[UmbracoOptions($"{Umbraco.Cms.Core.Constants.Configuration.ConfigPrefix}NewBackOffice")] public class NewBackOfficeSettings { public Uri? BackOfficeHost { get; set; } = null; diff --git a/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs b/src/Umbraco.New.Cms.Infrastructure/Security/IBackOfficeApplicationManager.cs similarity index 74% rename from src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs rename to src/Umbraco.New.Cms.Infrastructure/Security/IBackOfficeApplicationManager.cs index 5a0e327950a2..068f5df472f5 100644 --- a/src/Umbraco.Cms.ManagementApi/Authorization/IBackOfficeApplicationManager.cs +++ b/src/Umbraco.New.Cms.Infrastructure/Security/IBackOfficeApplicationManager.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.ManagementApi.Authorization; +namespace Umbraco.New.Cms.Infrastructure.Security; public interface IBackOfficeApplicationManager { diff --git a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj index 8dc5d3cc0034..82159079a4ee 100644 --- a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj +++ b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj @@ -10,4 +10,8 @@ + + + + From 2f57b4bf60f54cb9bc95d44c2f288f9775a8cc5e Mon Sep 17 00:00:00 2001 From: kjac Date: Fri, 21 Oct 2022 07:24:31 +0200 Subject: [PATCH 16/26] Add recurring task for cleaning up old tokens in the DB --- .../BackOfficeAuthBuilderExtensions.cs | 2 + .../HostedServices/OpenIddictCleanup.cs | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/Umbraco.New.Cms.Infrastructure/HostedServices/OpenIddictCleanup.cs diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 461f418a5ca9..92453b2acadd 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.ManagementApi.Middleware; using Umbraco.Cms.ManagementApi.Security; +using Umbraco.New.Cms.Infrastructure.HostedServices; using Umbraco.New.Cms.Infrastructure.Security; namespace Umbraco.Cms.ManagementApi.DependencyInjection; @@ -97,6 +98,7 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddHostedService(); builder.Services.AddHostedService(); return builder; diff --git a/src/Umbraco.New.Cms.Infrastructure/HostedServices/OpenIddictCleanup.cs b/src/Umbraco.New.Cms.Infrastructure/HostedServices/OpenIddictCleanup.cs new file mode 100644 index 000000000000..37a6e4caa633 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/HostedServices/OpenIddictCleanup.cs @@ -0,0 +1,56 @@ +using System.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; +using Umbraco.Cms.Infrastructure.HostedServices; + +namespace Umbraco.New.Cms.Infrastructure.HostedServices; + +// port of the OpenIddict Quartz job for cleaning up - see https://github.com/openiddict/openiddict-core/tree/dev/src/OpenIddict.Quartz +public class OpenIddictCleanup : RecurringHostedServiceBase +{ + // keep tokens and authorizations in the database for 7 days + // - NOTE: this is NOT the same as access token lifetime, which is likely very short + private const int LifespanInSeconds = 7 * 24 * 60 * 60; + + private readonly ILogger _logger; + private readonly IServiceProvider _provider; + + public OpenIddictCleanup( + ILogger logger, IServiceProvider provider) + : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(5)) + { + _logger = logger; + _provider = provider; + } + + public override async Task PerformExecuteAsync(object? state) + { + // hosted services are registered as singletons, but this particular one consumes scoped services... so + // we have to fetch the service dependencies manually using a new scope per invocation. + IServiceScope scope = _provider.CreateScope(); + DateTimeOffset threshold = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(LifespanInSeconds); + + try + { + IOpenIddictTokenManager tokenManager = scope.ServiceProvider.GetService() + ?? throw new ConfigurationErrorsException($"Could not retrieve an {nameof(IOpenIddictTokenManager)} service from the current scope"); + await tokenManager.PruneAsync(threshold); + } + catch (Exception exception) + { + _logger.LogError(exception, "Unable to prune OpenIddict tokens"); + } + + try + { + IOpenIddictAuthorizationManager authorizationManager = scope.ServiceProvider.GetService() + ?? throw new ConfigurationErrorsException($"Could not retrieve an {nameof(IOpenIddictAuthorizationManager)} service from the current scope"); + await authorizationManager.PruneAsync(threshold); + } + catch (Exception exception) + { + _logger.LogError(exception, "Unable to prune OpenIddict authorizations"); + } + } +} From 088448358837c2e8b1f507f190588b24c2a6ea96 Mon Sep 17 00:00:00 2001 From: kjac Date: Fri, 21 Oct 2022 11:02:29 +0200 Subject: [PATCH 17/26] Fix bad merge + make auth controller align with the new management API structure --- .../BackOfficeController.cs} | 10 +++++----- .../Controllers/Security/Paths.cs | 12 ++++++++++++ .../BackOfficeAuthBuilderExtensions.cs | 4 ++-- .../ManagementApiComposer.cs | 5 ++--- .../src/views/common/login.controller.js | 2 +- 5 files changed, 22 insertions(+), 11 deletions(-) rename src/Umbraco.Cms.ManagementApi/Controllers/{BackOfficeAuthentication/BackOfficeAuthenticationController.cs => Security/BackOfficeController.cs} (87%) create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs similarity index 87% rename from src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs rename to src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs index 1add978814b7..efdeede8f52f 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/BackOfficeAuthentication/BackOfficeAuthenticationController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs @@ -12,18 +12,18 @@ using Umbraco.Extensions; using Umbraco.New.Cms.Web.Common.Routing; -namespace Umbraco.Cms.ManagementApi.Controllers.BackOfficeAuthentication; +namespace Umbraco.Cms.ManagementApi.Controllers.Security; [ApiController] -[BackOfficeRoute("api/v{version:apiVersion}/back-office-authentication")] -[OpenApiTag("BackOfficeAuthentication")] -public class BackOfficeAuthenticationController : ManagementApiControllerBase +[VersionedApiBackOfficeRoute(Paths.BackOfficeApiEndpointTemplate)] +[OpenApiTag("Security")] +public class BackOfficeController : ManagementApiControllerBase { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IBackOfficeSignInManager _backOfficeSignInManager; private readonly IBackOfficeUserManager _backOfficeUserManager; - public BackOfficeAuthenticationController(IHttpContextAccessor httpContextAccessor, IBackOfficeSignInManager backOfficeSignInManager, IBackOfficeUserManager backOfficeUserManager) + public BackOfficeController(IHttpContextAccessor httpContextAccessor, IBackOfficeSignInManager backOfficeSignInManager, IBackOfficeUserManager backOfficeUserManager) { _httpContextAccessor = httpContextAccessor; _backOfficeSignInManager = backOfficeSignInManager; diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs new file mode 100644 index 000000000000..3ce1b7c3c6b8 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.ManagementApi.Controllers.Security; + +public static class Paths +{ + public const string BackOfficeApiEndpointTemplate = "security/back-office"; + + public static string BackOfficeApiAuthorizationEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/authorize"); + + public static string BackOfficeApiTokenEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/token"); + + private static string BackOfficeApiEndpointPath(string relativePath) => $"/umbraco/management/api/v1.0/{relativePath}"; +} diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 92453b2acadd..3dee7d40a2b8 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -54,8 +54,8 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) { // Enable the authorization and token endpoints. options - .SetAuthorizationEndpointUris("/umbraco/api/v1.0/back-office-authentication/authorize") - .SetTokenEndpointUris("/umbraco/api/v1.0/back-office-authentication/token"); + .SetAuthorizationEndpointUris(Controllers.Security.Paths.BackOfficeApiAuthorizationEndpoint) + .SetTokenEndpointUris(Controllers.Security.Paths.BackOfficeApiTokenEndpoint); // Enable authorization code flow with PKCE options diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index 2e661ebe257d..b176174adfdf 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -46,7 +46,6 @@ public void Compose(IUmbracoBuilder builder) .AddFactories() .AddServices() .AddMappers() - .AddExamineManagement() .AddBackOfficeAuthentication(); services.AddApiVersioning(options => @@ -75,8 +74,8 @@ public void Compose(IUmbracoBuilder builder) Type = OpenApiSecuritySchemeType.OAuth2, Description = "Umbraco Authentication", Flow = OpenApiOAuth2Flow.AccessCode, - AuthorizationUrl = "/umbraco/api/v1.0/back-office-authentication/authorize", - TokenUrl = "/umbraco/api/v1.0/back-office-authentication/token", + AuthorizationUrl = Controllers.Security.Paths.BackOfficeApiAuthorizationEndpoint, + TokenUrl = Controllers.Security.Paths.BackOfficeApiTokenEndpoint, Scopes = new Dictionary(), }); // this is documented in OAuth2 setup for swagger, but does not seem to be necessary at the moment. diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js index 89e963b56074..13ca4cb1930b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js @@ -13,7 +13,7 @@ angular.module('umbraco').controller("Umbraco.LoginController", function (events if (locationObj.returnPath) { // decodeURIComponent(...) does not play nice with OAuth redirect URLs, so until we have a // dedicated login screen for the new back-office, we need to hardcode this exception - path = locationObj.returnPath.indexOf("/back-office-authentication/authorize") > 0 + path = locationObj.returnPath.indexOf("/security/back-office/authorize") > 0 ? locationObj.returnPath : decodeURIComponent(locationObj.returnPath); } From 9fdfc4d362143130c79048d9245562f0178b0f9c Mon Sep 17 00:00:00 2001 From: kjac Date: Fri, 21 Oct 2022 11:07:13 +0200 Subject: [PATCH 18/26] Explicitly handle the new management API path as a backoffice path (NOTE: this is potentially behaviorally breaking!) --- src/Umbraco.Core/Routing/UmbracoRequestPaths.cs | 4 ++++ .../Umbraco.Core/Routing/UmbracoRequestPathsTests.cs | 1 + 2 files changed, 5 insertions(+) diff --git a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs index fe1e83d2545f..8576def91d7f 100644 --- a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs +++ b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs @@ -19,6 +19,7 @@ public class UmbracoRequestPaths private readonly string _mvcArea; private readonly string _previewMvcPath; private readonly string _surfaceMvcPath; + private readonly string _backOfficeManagementApiPath; /// /// Initializes a new instance of the class. @@ -34,6 +35,7 @@ public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvi _mvcArea = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); _defaultUmbPaths = new List { "/" + _mvcArea, "/" + _mvcArea + "/" }; _backOfficeMvcPath = "/" + _mvcArea + "/BackOffice/"; + _backOfficeManagementApiPath = "/" + _mvcArea + "/management/api/"; _previewMvcPath = "/" + _mvcArea + "/Preview/"; _surfaceMvcPath = "/" + _mvcArea + "/Surface/"; _apiMvcPath = "/" + _mvcArea + "/Api/"; @@ -51,6 +53,7 @@ public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvi /// These are def back office: /// /Umbraco/BackOffice = back office /// /Umbraco/Preview = back office + /// /Umbraco/Management/Api = back office /// /// /// If it's not any of the above then we cannot determine if it's back office or front-end @@ -87,6 +90,7 @@ public bool IsBackOfficeRequest(string absPath) // check for special back office paths if (urlPath.InvariantStartsWith(_backOfficeMvcPath) + || urlPath.InvariantStartsWith(_backOfficeManagementApiPath) || urlPath.InvariantStartsWith(_previewMvcPath)) { return true; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs index 744febae67a5..71553fc9c753 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs @@ -85,6 +85,7 @@ public void Is_Client_Side_Request_InvalidPath_ReturnFalse() [TestCase("http://www.domain.com/myvdir/umbraco/api/blah", "myvdir", false)] [TestCase("http://www.domain.com/MyVdir/umbraco/api/blah", "/myvdir", false)] [TestCase("http://www.domain.com/MyVdir/Umbraco/", "myvdir", true)] + [TestCase("http://www.domain.com/umbraco/management/api/v1.0/my/controller/action/", "", true)] public void Is_Back_Office_Request(string input, string virtualPath, bool expected) { var source = new Uri(input); From c13039ac52be24b69e1ec21385dec1aee6cfb108 Mon Sep 17 00:00:00 2001 From: kjac Date: Mon, 24 Oct 2022 13:55:02 +0200 Subject: [PATCH 19/26] Redo handle the new management API requests as backoffice requests, this time in a non-breaking way --- .../ServicesBuilderExtensions.cs | 8 ++++++++ .../Routing/UmbracoRequestPaths.cs | 20 +++++++++++++++---- .../Routing/UmbracoRequestPathsOptions.cs | 10 ++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Core/Routing/UmbracoRequestPathsOptions.cs diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/ServicesBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/ServicesBuilderExtensions.cs index cb739478c56e..9a05c17bfe22 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/ServicesBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/ServicesBuilderExtensions.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.ManagementApi.Serialization; using Umbraco.Cms.ManagementApi.Services; +using Umbraco.Extensions; using Umbraco.New.Cms.Core.Services.Installer; using Umbraco.New.Cms.Core.Services.Languages; @@ -17,6 +19,12 @@ internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); + // TODO: handle new management API path in core UmbracoRequestPaths (it's a behavioural breaking change so it goes here for now) + builder.Services.Configure(options => + { + options.IsBackOfficeRequest = urlPath => urlPath.InvariantStartsWith($"/umbraco/management/api/"); + }); + return builder; } } diff --git a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs index 8576def91d7f..960be851ff38 100644 --- a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs +++ b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Routing; @@ -19,12 +21,18 @@ public class UmbracoRequestPaths private readonly string _mvcArea; private readonly string _previewMvcPath; private readonly string _surfaceMvcPath; - private readonly string _backOfficeManagementApiPath; + private readonly IOptions _umbracoRequestPathsOptions; + + [Obsolete("Use constructor that takes IOptions - Will be removed in Umbraco 13")] + public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvironment hostingEnvironment) + : this(globalSettings, hostingEnvironment, StaticServiceProvider.Instance.GetRequiredService>()) + { + } /// /// Initializes a new instance of the class. /// - public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvironment hostingEnvironment) + public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvironment hostingEnvironment, IOptions umbracoRequestPathsOptions) { var applicationPath = hostingEnvironment.ApplicationVirtualPath; _appPath = applicationPath.TrimStart(Constants.CharArrays.ForwardSlash); @@ -35,11 +43,11 @@ public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvi _mvcArea = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); _defaultUmbPaths = new List { "/" + _mvcArea, "/" + _mvcArea + "/" }; _backOfficeMvcPath = "/" + _mvcArea + "/BackOffice/"; - _backOfficeManagementApiPath = "/" + _mvcArea + "/management/api/"; _previewMvcPath = "/" + _mvcArea + "/Preview/"; _surfaceMvcPath = "/" + _mvcArea + "/Surface/"; _apiMvcPath = "/" + _mvcArea + "/Api/"; _installPath = hostingEnvironment.ToAbsolute(Constants.SystemDirectories.Install); + _umbracoRequestPathsOptions = umbracoRequestPathsOptions; } /// @@ -90,7 +98,6 @@ public bool IsBackOfficeRequest(string absPath) // check for special back office paths if (urlPath.InvariantStartsWith(_backOfficeMvcPath) - || urlPath.InvariantStartsWith(_backOfficeManagementApiPath) || urlPath.InvariantStartsWith(_previewMvcPath)) { return true; @@ -103,6 +110,11 @@ public bool IsBackOfficeRequest(string absPath) return false; } + if (_umbracoRequestPathsOptions.Value.IsBackOfficeRequest(urlPath)) + { + return true; + } + // if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by // checking how many parts the route has, for example, all PluginController routes will be routed like // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} diff --git a/src/Umbraco.Core/Routing/UmbracoRequestPathsOptions.cs b/src/Umbraco.Core/Routing/UmbracoRequestPathsOptions.cs new file mode 100644 index 000000000000..91f13eab3bc2 --- /dev/null +++ b/src/Umbraco.Core/Routing/UmbracoRequestPathsOptions.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Routing; + +public class UmbracoRequestPathsOptions +{ + /// + /// Gets the delegate that allows us to handle additional URLs as back-office requests. + /// This returns false by default and can be overwritten in Startup.cs. + /// + public Func IsBackOfficeRequest { get; set; } = _ => false; +} From d2041491b06fd7e425bd2990f7489508a52c1fcb Mon Sep 17 00:00:00 2001 From: kjac Date: Tue, 25 Oct 2022 11:31:43 +0200 Subject: [PATCH 20/26] Add/update TODOs --- src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs | 1 + .../Models/Configuration/NewBackOfficeSettings.cs | 2 +- .../Models/Configuration/NewBackOfficeSettingsValidator.cs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index b176174adfdf..536e17a3660a 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -94,6 +94,7 @@ public void Compose(IUmbracoBuilder builder) services.AddControllers(); builder.Services.ConfigureOptions(); + // TODO: when this is moved to core, make the AddUmbracoOptions extension private again and remove core InternalsVisibleTo for Umbraco.Cms.ManagementApi builder.AddUmbracoOptions(); builder.Services.AddSingleton, NewBackOfficeSettingsValidator>(); diff --git a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs index 9784afb44114..cad7b8868de7 100644 --- a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs +++ b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs @@ -2,7 +2,7 @@ namespace Umbraco.New.Cms.Core.Models.Configuration; -/// TODO: merge this class with relevant existing settings from Core and clean up +// TODO: merge this class with relevant existing settings from Core and clean up [UmbracoOptions($"{Umbraco.Cms.Core.Constants.Configuration.ConfigPrefix}NewBackOffice")] public class NewBackOfficeSettings { diff --git a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs index 840abca30f31..bb1a2eda3d69 100644 --- a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs +++ b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs @@ -3,7 +3,7 @@ namespace Umbraco.New.Cms.Core.Models.Configuration; -/// TODO: merge this class with relevant existing settings validators from Core and clean up +// TODO: merge this class with relevant existing settings validators from Core and clean up public class NewBackOfficeSettingsValidator : ConfigurationValidatorBase, IValidateOptions { public ValidateOptionsResult Validate(string? name, NewBackOfficeSettings options) From e8dc663723ae48e80ef3331f9f22c9aae7c054f5 Mon Sep 17 00:00:00 2001 From: kjac Date: Mon, 31 Oct 2022 07:20:05 +0100 Subject: [PATCH 21/26] Revert duplication of current auth policies for OpenIddict (as it breaks everything for V11 without the new management APIs) and introduce a dedicated PoC policy setup for OpenIddict. --- .../BackOfficeAuthBuilderExtensions.cs | 34 +- .../UmbracoBuilder.BackOfficeAuth.cs | 416 +++++++++++------- 2 files changed, 295 insertions(+), 155 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 3dee7d40a2b8..ab132cbef289 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -1,9 +1,13 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using OpenIddict.Validation.AspNetCore; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.ManagementApi.Middleware; using Umbraco.Cms.ManagementApi.Security; +using Umbraco.Cms.Web.Common.Authorization; using Umbraco.New.Cms.Infrastructure.HostedServices; using Umbraco.New.Cms.Infrastructure.Security; @@ -38,6 +42,7 @@ private static IUmbracoBuilder AddDbContext(this IUmbracoBuilder builder) private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) { builder.Services.AddAuthentication(); + builder.Services.AddAuthorization(CreatePolicies); builder.Services.AddOpenIddict() @@ -85,13 +90,6 @@ private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) // Register the ASP.NET Core host. options.UseAspNetCore(); - - // TODO: this is a workaround to make validated principals be perceived as explicit backoffice users by ClaimsPrincipalExtensions.GetUmbracoIdentity - // we may not need it once cookie auth for backoffice is removed - validate and clean up if necessary - options.Configure(validationOptions => - { - validationOptions.TokenValidationParameters.AuthenticationType = Core.Constants.Security.BackOfficeAuthenticationType; - }); }); builder.Services.AddTransient(); @@ -125,4 +123,24 @@ public async Task StartAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } + + // TODO: move this to an appropriate location and implement the policy scheme that should be used for the new management APIs + private static void CreatePolicies(AuthorizationOptions options) + { + void AddPolicy(string policyName, string claimType, params string[] allowedClaimValues) + { + options.AddPolicy($"New{policyName}", policy => + { + policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + policy.RequireClaim(claimType, allowedClaimValues); + }); + } + + // NOTE: these are ONLY sample policies that allow us to test the new management APIs + AddPolicy(AuthorizationPolicies.SectionAccessContent, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content); + AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content); + AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Media); + AddPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Media); + AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content, Constants.Applications.Media); + } } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 2a48b245545a..97722d830576 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -105,206 +105,328 @@ private static void AddBackOfficeAuthorizationPoliciesInternal(this IUmbracoBuil builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddAuthorization(o => - { - CreatePolicies(o, backOfficeAuthenticationScheme); - }); + builder.Services.AddAuthorization(o => CreatePolicies(o, backOfficeAuthenticationScheme)); } - // TODO: create the correct policies for new backoffice auth private static void CreatePolicies(AuthorizationOptions options, string backOfficeAuthenticationScheme) { - void AddPolicy(string policyName, params IAuthorizationRequirement[] requirements) + options.AddPolicy(AuthorizationPolicies.MediaPermissionByResource, policy => { - options.AddPolicy(policyName, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - // TODO: use constant OpenIddictServerAspNetCoreDefaults.AuthenticationScheme instead of magic string - policy.AuthenticationSchemes.Add("OpenIddict.Validation.AspNetCore"); - foreach (IAuthorizationRequirement requirement in requirements) - { - policy.Requirements.Add(requirement); - } - }); - } - - AddPolicy(AuthorizationPolicies.MediaPermissionByResource, new MediaPermissionsResourceRequirement()); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new MediaPermissionsResourceRequirement()); + }); - AddPolicy(AuthorizationPolicies.MediaPermissionPathById, new MediaPermissionsQueryStringRequirement("id")); + options.AddPolicy(AuthorizationPolicies.MediaPermissionPathById, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new MediaPermissionsQueryStringRequirement("id")); + }); - AddPolicy(AuthorizationPolicies.ContentPermissionByResource, new ContentPermissionsResourceRequirement()); + options.AddPolicy(AuthorizationPolicies.ContentPermissionByResource, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsResourceRequirement()); + }); - AddPolicy( - AuthorizationPolicies.ContentPermissionEmptyRecycleBin, - new ContentPermissionsQueryStringRequirement(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)); + options.AddPolicy(AuthorizationPolicies.ContentPermissionEmptyRecycleBin, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(Constants.System.RecycleBinContent, + ActionDelete.ActionLetter)); + }); - AddPolicy( - AuthorizationPolicies.ContentPermissionAdministrationById, - new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter), - new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter, "contentId")); + options.AddPolicy(AuthorizationPolicies.ContentPermissionAdministrationById, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter)); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter, "contentId")); + }); - AddPolicy( - AuthorizationPolicies.ContentPermissionProtectById, - new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter), - new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, "contentId")); + options.AddPolicy(AuthorizationPolicies.ContentPermissionProtectById, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter)); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, "contentId")); + }); - AddPolicy( - AuthorizationPolicies.ContentPermissionRollbackById, - new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter), - new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter, "contentId")); + options.AddPolicy(AuthorizationPolicies.ContentPermissionRollbackById, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter)); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter, "contentId")); + }); - AddPolicy( - AuthorizationPolicies.ContentPermissionPublishById, - new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter)); + options.AddPolicy(AuthorizationPolicies.ContentPermissionPublishById, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter)); + }); - AddPolicy( - AuthorizationPolicies.ContentPermissionBrowseById, - new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter), - new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter, "contentId")); + options.AddPolicy(AuthorizationPolicies.ContentPermissionBrowseById, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter)); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter, "contentId")); + }); - AddPolicy( - AuthorizationPolicies.ContentPermissionDeleteById, - new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); + options.AddPolicy(AuthorizationPolicies.ContentPermissionDeleteById, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); + }); - AddPolicy(AuthorizationPolicies.BackOfficeAccess, new BackOfficeRequirement()); + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new BackOfficeRequirement()); + }); - AddPolicy(AuthorizationPolicies.BackOfficeAccessWithoutApproval, new BackOfficeRequirement(false)); + options.AddPolicy(AuthorizationPolicies.BackOfficeAccessWithoutApproval, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new BackOfficeRequirement(false)); + }); - AddPolicy( - AuthorizationPolicies.AdminUserEditsRequireAdmin, - new AdminUsersRequirement(), - new AdminUsersRequirement("userIds")); + options.AddPolicy(AuthorizationPolicies.AdminUserEditsRequireAdmin, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new AdminUsersRequirement()); + policy.Requirements.Add(new AdminUsersRequirement("userIds")); + }); - AddPolicy( - AuthorizationPolicies.UserBelongsToUserGroupInRequest, - new UserGroupRequirement(), - new UserGroupRequirement("userGroupIds")); + options.AddPolicy(AuthorizationPolicies.UserBelongsToUserGroupInRequest, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new UserGroupRequirement()); + policy.Requirements.Add(new UserGroupRequirement("userGroupIds")); + }); - AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, new DenyLocalLoginRequirement()); + options.AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new DenyLocalLoginRequirement()); + }); - AddPolicy(AuthorizationPolicies.SectionAccessContent, new SectionRequirement(Constants.Applications.Content)); + options.AddPolicy(AuthorizationPolicies.SectionAccessContent, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Content)); + }); - AddPolicy( - AuthorizationPolicies.SectionAccessContentOrMedia, - new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media)); + options.AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add( + new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media)); + }); - AddPolicy(AuthorizationPolicies.SectionAccessUsers, new SectionRequirement(Constants.Applications.Users)); + options.AddPolicy(AuthorizationPolicies.SectionAccessUsers, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Users)); + }); - AddPolicy( - AuthorizationPolicies.SectionAccessForTinyMce, - new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); + options.AddPolicy(AuthorizationPolicies.SectionAccessForTinyMce, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); + }); - AddPolicy(AuthorizationPolicies.SectionAccessMedia, new SectionRequirement(Constants.Applications.Media)); + options.AddPolicy(AuthorizationPolicies.SectionAccessMedia, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Media)); + }); - AddPolicy(AuthorizationPolicies.SectionAccessMembers, new SectionRequirement(Constants.Applications.Members)); + options.AddPolicy(AuthorizationPolicies.SectionAccessMembers, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Members)); + }); - AddPolicy(AuthorizationPolicies.SectionAccessPackages, new SectionRequirement(Constants.Applications.Packages)); + options.AddPolicy(AuthorizationPolicies.SectionAccessPackages, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Packages)); + }); - AddPolicy(AuthorizationPolicies.SectionAccessSettings, new SectionRequirement(Constants.Applications.Settings)); + options.AddPolicy(AuthorizationPolicies.SectionAccessSettings, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Settings)); + }); // We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. - AddPolicy( - AuthorizationPolicies.SectionAccessForContentTree, - new SectionRequirement( - Constants.Applications.Content, - Constants.Applications.Media, - Constants.Applications.Users, - Constants.Applications.Settings, - Constants.Applications.Packages, - Constants.Applications.Members)); - - AddPolicy( - AuthorizationPolicies.SectionAccessForMediaTree, - new SectionRequirement( - Constants.Applications.Content, - Constants.Applications.Media, - Constants.Applications.Users, - Constants.Applications.Settings, - Constants.Applications.Packages, - Constants.Applications.Members)); - - AddPolicy( - AuthorizationPolicies.SectionAccessForMemberTree, - new SectionRequirement( - Constants.Applications.Content, - Constants.Applications.Media, - Constants.Applications.Members)); + options.AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, + Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); + }); + options.AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, + Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); + }); + options.AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); + }); // Permission is granted to this policy if the user has access to any of these sections: Content, media, settings, developer, members - AddPolicy( - AuthorizationPolicies.SectionAccessForDataTypeReading, - new SectionRequirement( - Constants.Applications.Content, - Constants.Applications.Media, - Constants.Applications.Members, - Constants.Applications.Settings, - Constants.Applications.Packages)); + options.AddPolicy(AuthorizationPolicies.SectionAccessForDataTypeReading, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + Constants.Applications.Settings, Constants.Applications.Packages)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessDocuments, new TreeRequirement(Constants.Trees.Content)); + options.AddPolicy(AuthorizationPolicies.TreeAccessDocuments, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Content)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessUsers, new TreeRequirement(Constants.Trees.Users)); + options.AddPolicy(AuthorizationPolicies.TreeAccessUsers, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Users)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, new TreeRequirement(Constants.Trees.PartialViews)); + options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViews)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, new TreeRequirement(Constants.Trees.PartialViewMacros)); + options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViewMacros)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessPackages, new TreeRequirement(Constants.Trees.Packages)); + options.AddPolicy(AuthorizationPolicies.TreeAccessPackages, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Packages)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessLogs, new TreeRequirement(Constants.Trees.LogViewer)); + options.AddPolicy(AuthorizationPolicies.TreeAccessLogs, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.LogViewer)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, new TreeRequirement(Constants.Trees.DataTypes)); + options.AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessTemplates, new TreeRequirement(Constants.Trees.Templates)); + options.AddPolicy(AuthorizationPolicies.TreeAccessTemplates, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Templates)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, new TreeRequirement(Constants.Trees.MemberTypes)); + options.AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, new TreeRequirement(Constants.Trees.RelationTypes)); + options.AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.RelationTypes)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, new TreeRequirement(Constants.Trees.DocumentTypes)); + options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, new TreeRequirement(Constants.Trees.MemberGroups)); + options.AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberGroups)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, new TreeRequirement(Constants.Trees.MediaTypes)); + options.AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessMacros, new TreeRequirement(Constants.Trees.Macros)); + options.AddPolicy(AuthorizationPolicies.TreeAccessMacros, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Macros)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessLanguages, new TreeRequirement(Constants.Trees.Languages)); + options.AddPolicy(AuthorizationPolicies.TreeAccessLanguages, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Languages)); + }); - AddPolicy(AuthorizationPolicies.TreeAccessDictionary, new TreeRequirement(Constants.Trees.Dictionary)); + options.AddPolicy(AuthorizationPolicies.TreeAccessDictionary, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary)); + }); - AddPolicy( - AuthorizationPolicies.TreeAccessDictionaryOrTemplates, - new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); + options.AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); + }); - AddPolicy( - AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, - new TreeRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); + options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); + }); - AddPolicy( - AuthorizationPolicies.TreeAccessMediaOrMediaTypes, - new TreeRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); + options.AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); + }); - AddPolicy( - AuthorizationPolicies.TreeAccessMembersOrMemberTypes, - new TreeRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); + options.AddPolicy(AuthorizationPolicies.TreeAccessMembersOrMemberTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); + }); - AddPolicy( - AuthorizationPolicies.TreeAccessAnySchemaTypes, - new TreeRequirement( - Constants.Trees.DataTypes, - Constants.Trees.DocumentTypes, - Constants.Trees.MediaTypes, - Constants.Trees.MemberTypes)); + options.AddPolicy(AuthorizationPolicies.TreeAccessAnySchemaTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, + Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)); + }); - AddPolicy( - AuthorizationPolicies.TreeAccessAnyContentOrTypes, - new TreeRequirement( - Constants.Trees.DocumentTypes, - Constants.Trees.Content, - Constants.Trees.MediaTypes, - Constants.Trees.Media, - Constants.Trees.MemberTypes, - Constants.Trees.Members)); + options.AddPolicy(AuthorizationPolicies.TreeAccessAnyContentOrTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement( + Constants.Trees.DocumentTypes, Constants.Trees.Content, + Constants.Trees.MediaTypes, Constants.Trees.Media, + Constants.Trees.MemberTypes, Constants.Trees.Members)); + }); } } From 9e1c53cf1d0a583c7886f043d2cf19f413266101 Mon Sep 17 00:00:00 2001 From: kjac Date: Mon, 31 Oct 2022 07:20:19 +0100 Subject: [PATCH 22/26] Fix failing unit tests --- src/Umbraco.Cms.ManagementApi/OpenApi.json | 61 +++++++++++++++++++ .../Objects/TestUmbracoContextFactory.cs | 10 ++- .../Routing/UmbracoRequestPathsTests.cs | 27 ++++++-- .../Security/BackOfficeCookieManagerTests.cs | 15 +++-- 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Cms.ManagementApi/OpenApi.json b/src/Umbraco.Cms.ManagementApi/OpenApi.json index e352c4d7da58..d71ee7346007 100644 --- a/src/Umbraco.Cms.ManagementApi/OpenApi.json +++ b/src/Umbraco.Cms.ManagementApi/OpenApi.json @@ -816,6 +816,46 @@ } } }, + "/umbraco/management/api/v1/security/back-office/authorize": { + "get": { + "tags": [ + "Security" + ], + "operationId": "BackOffice_AuthorizeGET", + "responses": { + "200": { + "description": "", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "post": { + "tags": [ + "Security" + ], + "operationId": "BackOffice_AuthorizePOST", + "responses": { + "200": { + "description": "", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, "/umbraco/management/api/v1/search/index/{indexName}": { "get": { "tags": [ @@ -5246,8 +5286,26 @@ } } } + }, + "securitySchemes": { + "Bearer": { + "type": "oauth2", + "description": "Umbraco Authentication", + "name": "Umbraco", + "flows": { + "authorizationCode": { + "authorizationUrl": "/umbraco/management/api/v1.0/security/back-office/authorize", + "tokenUrl": "/umbraco/management/api/v1.0/security/back-office/token" + } + } + } } }, + "security": [ + { + "Bearer": [] + } + ], "tags": [ { "name": "Culture" @@ -5312,6 +5370,9 @@ { "name": "Search" }, + { + "name": "Security" + }, { "name": "Server" }, diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs index 94a64b31c91c..2099d1d53702 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs @@ -23,7 +23,8 @@ public static IUmbracoContextFactory Create( GlobalSettings globalSettings = null, IUmbracoContextAccessor umbracoContextAccessor = null, IHttpContextAccessor httpContextAccessor = null, - IPublishedUrlProvider publishedUrlProvider = null) + IPublishedUrlProvider publishedUrlProvider = null, + UmbracoRequestPathsOptions umbracoRequestPathsOptions = null) { if (globalSettings == null) { @@ -45,6 +46,11 @@ public static IUmbracoContextFactory Create( publishedUrlProvider = Mock.Of(); } + if (umbracoRequestPathsOptions == null) + { + umbracoRequestPathsOptions = new UmbracoRequestPathsOptions(); + } + var contentCache = new Mock(); var mediaCache = new Mock(); var snapshot = new Mock(); @@ -58,7 +64,7 @@ public static IUmbracoContextFactory Create( var umbracoContextFactory = new UmbracoContextFactory( umbracoContextAccessor, snapshotService.Object, - new UmbracoRequestPaths(Options.Create(globalSettings), hostingEnvironment), + new UmbracoRequestPaths(Options.Create(globalSettings), hostingEnvironment, Options.Create(umbracoRequestPathsOptions)), hostingEnvironment, new UriUtility(hostingEnvironment), new AspNetCoreCookieManager(httpContextAccessor), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs index 71553fc9c753..4ed6ce842ca0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs @@ -18,10 +18,12 @@ public void Setup() { _hostEnvironment = Mock.Of(); _globalSettings = new GlobalSettings(); + _umbracoRequestPathsOptions = new UmbracoRequestPathsOptions(); } private IWebHostEnvironment _hostEnvironment; private GlobalSettings _globalSettings; + private UmbracoRequestPathsOptions _umbracoRequestPathsOptions; private IHostingEnvironment CreateHostingEnvironment(string virtualPath = "") { @@ -49,7 +51,7 @@ private IHostingEnvironment CreateHostingEnvironment(string virtualPath = "") public void Is_Client_Side_Request(string url, bool assert) { var hostingEnvironment = CreateHostingEnvironment(); - var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment); + var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment, Options.Create(_umbracoRequestPathsOptions)); var uri = new Uri("http://test.com" + url); var result = umbracoRequestPaths.IsClientSideRequest(uri.AbsolutePath); @@ -60,7 +62,7 @@ public void Is_Client_Side_Request(string url, bool assert) public void Is_Client_Side_Request_InvalidPath_ReturnFalse() { var hostingEnvironment = CreateHostingEnvironment(); - var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment); + var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment, Options.Create(_umbracoRequestPathsOptions)); // This URL is invalid. Default to false when the extension cannot be determined var uri = new Uri("http://test.com/installing-modules+foobar+\"yipee\""); @@ -85,12 +87,13 @@ public void Is_Client_Side_Request_InvalidPath_ReturnFalse() [TestCase("http://www.domain.com/myvdir/umbraco/api/blah", "myvdir", false)] [TestCase("http://www.domain.com/MyVdir/umbraco/api/blah", "/myvdir", false)] [TestCase("http://www.domain.com/MyVdir/Umbraco/", "myvdir", true)] - [TestCase("http://www.domain.com/umbraco/management/api/v1.0/my/controller/action/", "", true)] + // NOTE: this test case is false for now - will be true once the IsBackOfficeRequest tweak from the new management API is put into UmbracoRequestPaths + [TestCase("http://www.domain.com/umbraco/management/api/v1.0/my/controller/action/", "", false)] public void Is_Back_Office_Request(string input, string virtualPath, bool expected) { var source = new Uri(input); var hostingEnvironment = CreateHostingEnvironment(virtualPath); - var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment); + var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment, Options.Create(_umbracoRequestPathsOptions)); Assert.AreEqual(expected, umbracoRequestPaths.IsBackOfficeRequest(source.AbsolutePath)); } @@ -107,7 +110,21 @@ public void Is_Installer_Request(string input, bool expected) { var source = new Uri(input); var hostingEnvironment = CreateHostingEnvironment(); - var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment); + var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment, Options.Create(_umbracoRequestPathsOptions)); Assert.AreEqual(expected, umbracoRequestPaths.IsInstallerRequest(source.AbsolutePath)); } + + [TestCase("http://www.domain.com/some/path", false)] + [TestCase("http://www.domain.com/umbraco/surface/blah", false)] + [TestCase("http://www.domain.com/umbraco/api/blah", false)] + [TestCase("http://www.domain.com/umbraco/management/api/v1.0/my/controller/action/", true)] + public void Force_Back_Office_Request_With_Request_Paths_Options(string input, bool expected) + { + var source = new Uri(input); + var hostingEnvironment = CreateHostingEnvironment(); + var umbracoRequestPathsOptions = new UmbracoRequestPathsOptions(); + umbracoRequestPathsOptions.IsBackOfficeRequest = _ => true; + var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment, Options.Create(umbracoRequestPathsOptions)); + Assert.AreEqual(expected, umbracoRequestPaths.IsBackOfficeRequest(source.AbsolutePath)); + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs index b1881c132efe..c59f9db68fe0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs @@ -24,12 +24,13 @@ public class BackOfficeCookieManagerTests public void ShouldAuthenticateRequest_When_Not_Configured() { var globalSettings = new GlobalSettings(); + var umbracoRequestPathsOptions = new UmbracoRequestPathsOptions(); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); var mgr = new BackOfficeCookieManager( Mock.Of(), runtime, - new UmbracoRequestPaths(Options.Create(globalSettings), TestHelper.GetHostingEnvironment()), + new UmbracoRequestPaths(Options.Create(globalSettings), TestHelper.GetHostingEnvironment(), Options.Create(umbracoRequestPathsOptions)), Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/umbraco"); @@ -41,6 +42,7 @@ public void ShouldAuthenticateRequest_When_Not_Configured() public void ShouldAuthenticateRequest_When_Configured() { var globalSettings = new GlobalSettings(); + var umbracoRequestPathsOptions = new UmbracoRequestPathsOptions(); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); var mgr = new BackOfficeCookieManager( @@ -49,7 +51,8 @@ public void ShouldAuthenticateRequest_When_Configured() new UmbracoRequestPaths( Options.Create(globalSettings), Mock.Of(x => - x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco")), + x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"), + Options.Create(umbracoRequestPathsOptions)), Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/umbraco"); @@ -61,6 +64,7 @@ public void ShouldAuthenticateRequest_When_Configured() public void ShouldAuthenticateRequest_Is_Back_Office() { var globalSettings = new GlobalSettings(); + var umbracoRequestPathsOptions = new UmbracoRequestPathsOptions(); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); @@ -73,7 +77,8 @@ public void ShouldAuthenticateRequest_Is_Back_Office() Options.Create(globalSettings), Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && - x.ToAbsolute(Constants.SystemDirectories.Install) == "/install")), + x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), + Options.Create(umbracoRequestPathsOptions)), Mock.Of()); var result = mgr.ShouldAuthenticateRequest(remainingTimeoutSecondsPath); @@ -87,6 +92,7 @@ public void ShouldAuthenticateRequest_Is_Back_Office() public void ShouldAuthenticateRequest_Not_Back_Office() { var globalSettings = new GlobalSettings(); + var umbracoRequestPathsOptions = new UmbracoRequestPathsOptions(); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); @@ -97,7 +103,8 @@ public void ShouldAuthenticateRequest_Not_Back_Office() Options.Create(globalSettings), Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && - x.ToAbsolute(Constants.SystemDirectories.Install) == "/install")), + x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), + Options.Create(umbracoRequestPathsOptions)), Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/notbackoffice"); From 8101a7c623504e071111d33d8099a1a0652ec151 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 1 Nov 2022 08:33:31 +0100 Subject: [PATCH 23/26] Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Security/BackOfficeApplicationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs index d3a4acac64eb..cd81f5d374fc 100644 --- a/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs @@ -29,7 +29,7 @@ public BackOfficeApplicationManager( public async Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default) { - if (backOfficeUrl.IsAbsoluteUri == false) + if (backOfficeUrl.IsAbsoluteUri is false) { throw new ArgumentException($"Expected an absolute URL, got: {backOfficeUrl}", nameof(backOfficeUrl)); } From 31286082da72a8db82632563c05b8ed7fb342891 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 1 Nov 2022 08:33:38 +0100 Subject: [PATCH 24/26] Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Security/BackOfficeApplicationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs index cd81f5d374fc..f36aeeb93895 100644 --- a/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs @@ -113,7 +113,7 @@ private async Task CreateOrUpdate(OpenIddictApplicationDescriptor clientDescript var identifier = clientDescriptor.ClientId ?? throw new ApplicationException($"ClientId is missing for application: {clientDescriptor.DisplayName ?? "(no name)"}"); var client = await _applicationManager.FindByClientIdAsync(identifier, cancellationToken); - if (client == null) + if (client is null) { await _applicationManager.CreateAsync(clientDescriptor, cancellationToken); } From 0157dbd3aa68a74397644dbe553c7167623593cf Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 1 Nov 2022 08:33:45 +0100 Subject: [PATCH 25/26] Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Security/BackOfficeApplicationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs index f36aeeb93895..1c0ea342e6b4 100644 --- a/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs @@ -126,7 +126,7 @@ private async Task CreateOrUpdate(OpenIddictApplicationDescriptor clientDescript private async Task Delete(string identifier, CancellationToken cancellationToken) { var client = await _applicationManager.FindByClientIdAsync(identifier, cancellationToken); - if (client == null) + if (client is null) { return; } From 88670a85c444c9048acb2caa54a79a807d5b25d3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 1 Nov 2022 10:23:02 +0100 Subject: [PATCH 26/26] Update src/Umbraco.Core/Routing/UmbracoRequestPaths.cs --- src/Umbraco.Core/Routing/UmbracoRequestPaths.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs index 960be851ff38..02b13cf98633 100644 --- a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs +++ b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs @@ -61,7 +61,6 @@ public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvi /// These are def back office: /// /Umbraco/BackOffice = back office /// /Umbraco/Preview = back office - /// /Umbraco/Management/Api = back office /// /// /// If it's not any of the above then we cannot determine if it's back office or front-end