OpenId Connect authentication for new management API (#13318)
* First attempt at OpenIddict * Making headway and more TODOs * Redo current policies for multiple schemas + clean up auth controller * Fix bad merge * Clean up some more test code * Fix spacing * Include AddAuthentication() in OpenIddict addition * A little more clean-up * Move application creation to its own implementation + prepare for middleware to handle valid callback URL * Enable refresh token flow * Fix bad merge from v11/dev * Support auth for Swagger and Postman in non-production environments + use default login screen for back-office logins * Add workaround to client side login handling so the OAuth return URL is not corrupted before redirection * Add temporary configuration handling for new backoffice * Restructure the code somewhat, move singular responsibility from management API project * Add recurring task for cleaning up old tokens in the DB * Fix bad merge + make auth controller align with the new management API structure * Explicitly handle the new management API path as a backoffice path (NOTE: this is potentially behaviorally breaking!) * Redo handle the new management API requests as backoffice requests, this time in a non-breaking way * Add/update TODOs * 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. * Fix failing unit tests * Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Update src/Umbraco.Core/Routing/UmbracoRequestPaths.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
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;
|
||||
using Umbraco.New.Cms.Web.Common.Routing;
|
||||
|
||||
namespace Umbraco.Cms.ManagementApi.Controllers.Security;
|
||||
|
||||
[ApiController]
|
||||
[VersionedApiBackOfficeRoute(Paths.BackOfficeApiEndpointTemplate)]
|
||||
[OpenApiTag("Security")]
|
||||
public class BackOfficeController : ManagementApiControllerBase
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IBackOfficeSignInManager _backOfficeSignInManager;
|
||||
private readonly IBackOfficeUserManager _backOfficeUserManager;
|
||||
|
||||
public BackOfficeController(IHttpContextAccessor httpContextAccessor, IBackOfficeSignInManager backOfficeSignInManager, IBackOfficeUserManager backOfficeUserManager)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_backOfficeSignInManager = backOfficeSignInManager;
|
||||
_backOfficeUserManager = backOfficeUserManager;
|
||||
}
|
||||
|
||||
[HttpGet("authorize")]
|
||||
[HttpPost("authorize")]
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> Authorize()
|
||||
{
|
||||
HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
|
||||
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
|
||||
if (request == null)
|
||||
{
|
||||
return BadRequest("Unable to obtain OpenID data from the current request");
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(cookieAuthResult.Principal.Identity.Name);
|
||||
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);
|
||||
}
|
||||
|
||||
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[] { Constants.Security.BackOfficeAuthenticationType });
|
||||
}
|
||||
}
|
||||
12
src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs
Normal file
12
src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs
Normal file
@@ -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}";
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
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;
|
||||
|
||||
namespace Umbraco.Cms.ManagementApi.DependencyInjection;
|
||||
|
||||
public static class BackOfficeAuthBuilderExtensions
|
||||
{
|
||||
public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder
|
||||
.AddDbContext()
|
||||
.AddOpenIddict();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static IUmbracoBuilder AddDbContext(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddDbContext<DbContext>(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.AddAuthentication();
|
||||
builder.Services.AddAuthorization(CreatePolicies);
|
||||
|
||||
builder.Services.AddOpenIddict()
|
||||
|
||||
// Register the OpenIddict core components.
|
||||
.AddCore(options =>
|
||||
{
|
||||
options
|
||||
.UseEntityFrameworkCore()
|
||||
.UseDbContext<DbContext>();
|
||||
})
|
||||
|
||||
// Register the OpenIddict server components.
|
||||
.AddServer(options =>
|
||||
{
|
||||
// Enable the authorization and token endpoints.
|
||||
options
|
||||
.SetAuthorizationEndpointUris(Controllers.Security.Paths.BackOfficeApiAuthorizationEndpoint)
|
||||
.SetTokenEndpointUris(Controllers.Security.Paths.BackOfficeApiTokenEndpoint);
|
||||
|
||||
// Enable authorization code flow with PKCE
|
||||
options
|
||||
.AllowAuthorizationCodeFlow()
|
||||
.RequireProofKeyForCodeExchange()
|
||||
.AllowRefreshTokenFlow();
|
||||
|
||||
// Register the encryption and signing credentials.
|
||||
// - see https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
|
||||
options
|
||||
// TODO: use actual certificates here, see docs above
|
||||
.AddDevelopmentEncryptionCertificate()
|
||||
.AddDevelopmentSigningCertificate()
|
||||
.DisableAccessTokenEncryption();
|
||||
|
||||
// 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.AddTransient<IBackOfficeApplicationManager, BackOfficeApplicationManager>();
|
||||
builder.Services.AddSingleton<IClientSecretManager, ClientSecretManager>();
|
||||
builder.Services.AddSingleton<BackOfficeAuthorizationInitializationMiddleware>();
|
||||
|
||||
builder.Services.AddHostedService<OpenIddictCleanup>();
|
||||
builder.Services.AddHostedService<DatabaseManager>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
// TODO: remove this once EF is implemented
|
||||
public class DatabaseManager : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DatabaseManager(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceProvider.CreateScope();
|
||||
|
||||
DbContext context = scope.ServiceProvider.GetRequiredService<DbContext>();
|
||||
await context.Database.EnsureCreatedAsync(cancellationToken);
|
||||
|
||||
// TODO: add BackOfficeAuthorizationInitializationMiddleware before UseAuthorization (to make it run for unauthorized API requests) and remove this
|
||||
IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService<IBackOfficeApplicationManager>();
|
||||
await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(new Uri("https://localhost:44331/"), 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);
|
||||
}
|
||||
}
|
||||
@@ -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 @@ public static class ServicesBuilderExtensions
|
||||
builder.Services.AddTransient<ISystemTextJsonSerializer, SystemTextJsonSerializer>();
|
||||
builder.Services.AddTransient<IUploadFileService, UploadFileService>();
|
||||
|
||||
// TODO: handle new management API path in core UmbracoRequestPaths (it's a behavioural breaking change so it goes here for now)
|
||||
builder.Services.Configure<UmbracoRequestPathsOptions>(options =>
|
||||
{
|
||||
options.IsBackOfficeRequest = urlPath => urlPath.InvariantStartsWith($"/umbraco/management/api/");
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,18 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using NSwag;
|
||||
using NSwag.AspNetCore;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
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;
|
||||
|
||||
namespace Umbraco.Cms.ManagementApi;
|
||||
@@ -44,7 +45,8 @@ public class ManagementApiComposer : IComposer
|
||||
.AddTrees()
|
||||
.AddFactories()
|
||||
.AddServices()
|
||||
.AddMappers();
|
||||
.AddMappers()
|
||||
.AddBackOfficeAuthentication();
|
||||
|
||||
services.AddApiVersioning(options =>
|
||||
{
|
||||
@@ -65,6 +67,20 @@ public class ManagementApiComposer : IComposer
|
||||
{
|
||||
document.Tags = document.Tags.OrderBy(tag => tag.Name).ToList();
|
||||
};
|
||||
|
||||
options.AddSecurity("Bearer", Enumerable.Empty<string>(), new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Umbraco",
|
||||
Type = OpenApiSecuritySchemeType.OAuth2,
|
||||
Description = "Umbraco Authentication",
|
||||
Flow = OpenApiOAuth2Flow.AccessCode,
|
||||
AuthorizationUrl = Controllers.Security.Paths.BackOfficeApiAuthorizationEndpoint,
|
||||
TokenUrl = Controllers.Security.Paths.BackOfficeApiTokenEndpoint,
|
||||
Scopes = new Dictionary<string, string>(),
|
||||
});
|
||||
// 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 =>
|
||||
@@ -78,6 +94,10 @@ public class ManagementApiComposer : IComposer
|
||||
services.AddControllers();
|
||||
builder.Services.ConfigureOptions<ConfigureMvcOptions>();
|
||||
|
||||
// TODO: when this is moved to core, make the AddUmbracoOptions extension private again and remove core InternalsVisibleTo for Umbraco.Cms.ManagementApi
|
||||
builder.AddUmbracoOptions<NewBackOfficeSettings>();
|
||||
builder.Services.AddSingleton<IValidateOptions<NewBackOfficeSettings>, NewBackOfficeSettingsValidator>();
|
||||
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options =>
|
||||
{
|
||||
options.AddFilter(new UmbracoPipelineFilter(
|
||||
@@ -125,6 +145,7 @@ public class ManagementApiComposer : IComposer
|
||||
{
|
||||
GlobalSettings? settings = provider.GetRequiredService<IOptions<GlobalSettings>>().Value;
|
||||
IHostingEnvironment hostingEnvironment = provider.GetRequiredService<IHostingEnvironment>();
|
||||
IClientSecretManager clientSecretManager = provider.GetRequiredService<IClientSecretManager>();
|
||||
var officePath = settings.GetBackOfficePath(hostingEnvironment);
|
||||
// serve documents (same as app.UseSwagger())
|
||||
applicationBuilder.UseOpenApi(config =>
|
||||
@@ -141,6 +162,14 @@ public class ManagementApiComposer : IComposer
|
||||
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)
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.New.Cms.Infrastructure.Security;
|
||||
|
||||
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<IBackOfficeApplicationManager>();
|
||||
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<BackOfficeAuthorizationInitializationMiddleware>();
|
||||
// return builder;
|
||||
// }
|
||||
// }
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
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.Security;
|
||||
|
||||
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,
|
||||
IOptionsMonitor<NewBackOfficeSettings> securitySettingsMonitor)
|
||||
{
|
||||
_applicationManager = applicationManager;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
_clientSecretManager = clientSecretManager;
|
||||
_backOfficeHost = securitySettingsMonitor.CurrentValue.BackOfficeHost;
|
||||
}
|
||||
|
||||
public async Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (backOfficeUrl.IsAbsoluteUri is false)
|
||||
{
|
||||
throw new ArgumentException($"Expected an absolute URL, got: {backOfficeUrl}", nameof(backOfficeUrl));
|
||||
}
|
||||
|
||||
await CreateOrUpdate(
|
||||
new OpenIddictApplicationDescriptor
|
||||
{
|
||||
DisplayName = "Umbraco back-office access",
|
||||
ClientId = Constants.OauthClientIds.BackOffice,
|
||||
RedirectUris =
|
||||
{
|
||||
CallbackUrlFor(_backOfficeHost ?? backOfficeUrl, "/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(backOfficeUrl, "/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 is null)
|
||||
{
|
||||
await _applicationManager.CreateAsync(clientDescriptor, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _applicationManager.UpdateAsync(client, clientDescriptor, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Delete(string identifier, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = await _applicationManager.FindByClientIdAsync(identifier, cancellationToken);
|
||||
if (client is 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)}");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Umbraco.Cms.ManagementApi.Security;
|
||||
|
||||
public class ClientSecretManager : IClientSecretManager
|
||||
{
|
||||
private Dictionary<string, string> _secretsByClientId = new();
|
||||
|
||||
public string Get(string clientId)
|
||||
{
|
||||
if (_secretsByClientId.ContainsKey(clientId) == false)
|
||||
{
|
||||
_secretsByClientId[clientId] = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
return _secretsByClientId[clientId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.ManagementApi.Security;
|
||||
|
||||
public interface IClientSecretManager
|
||||
{
|
||||
string Get(string clientId);
|
||||
}
|
||||
@@ -10,7 +10,10 @@
|
||||
<PackageReference Include="JsonPatch.Net" Version="2.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22472.11" />
|
||||
<PackageReference Include="NSwag.AspNetCore" Version="13.17.0" />
|
||||
<PackageReference Include="OpenIddict.AspNetCore" Version="3.1.1" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.DependencyInjection;
|
||||
/// </summary>
|
||||
public static partial class UmbracoBuilderExtensions
|
||||
{
|
||||
private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder, Action<OptionsBuilder<TOptions>>? configure = null)
|
||||
internal static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder, Action<OptionsBuilder<TOptions>>? configure = null)
|
||||
where TOptions : class
|
||||
{
|
||||
UmbracoOptionsAttribute? umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute<UmbracoOptionsAttribute>();
|
||||
|
||||
@@ -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,11 +21,18 @@ public class UmbracoRequestPaths
|
||||
private readonly string _mvcArea;
|
||||
private readonly string _previewMvcPath;
|
||||
private readonly string _surfaceMvcPath;
|
||||
private readonly IOptions<UmbracoRequestPathsOptions> _umbracoRequestPathsOptions;
|
||||
|
||||
[Obsolete("Use constructor that takes IOptions<UmbracoRequestPathsOptions> - Will be removed in Umbraco 13")]
|
||||
public UmbracoRequestPaths(IOptions<GlobalSettings> globalSettings, IHostingEnvironment hostingEnvironment)
|
||||
: this(globalSettings, hostingEnvironment, StaticServiceProvider.Instance.GetRequiredService<IOptions<UmbracoRequestPathsOptions>>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoRequestPaths" /> class.
|
||||
/// </summary>
|
||||
public UmbracoRequestPaths(IOptions<GlobalSettings> globalSettings, IHostingEnvironment hostingEnvironment)
|
||||
public UmbracoRequestPaths(IOptions<GlobalSettings> globalSettings, IHostingEnvironment hostingEnvironment, IOptions<UmbracoRequestPathsOptions> umbracoRequestPathsOptions)
|
||||
{
|
||||
var applicationPath = hostingEnvironment.ApplicationVirtualPath;
|
||||
_appPath = applicationPath.TrimStart(Constants.CharArrays.ForwardSlash);
|
||||
@@ -38,6 +47,7 @@ public class UmbracoRequestPaths
|
||||
_surfaceMvcPath = "/" + _mvcArea + "/Surface/";
|
||||
_apiMvcPath = "/" + _mvcArea + "/Api/";
|
||||
_installPath = hostingEnvironment.ToAbsolute(Constants.SystemDirectories.Install);
|
||||
_umbracoRequestPathsOptions = umbracoRequestPathsOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -99,6 +109,11 @@ public class UmbracoRequestPaths
|
||||
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}
|
||||
|
||||
10
src/Umbraco.Core/Routing/UmbracoRequestPathsOptions.cs
Normal file
10
src/Umbraco.Core/Routing/UmbracoRequestPathsOptions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
public class UmbracoRequestPathsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Func<string, bool> IsBackOfficeRequest { get; set; } = _ => false;
|
||||
}
|
||||
@@ -47,6 +47,10 @@
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<!-- TODO: remove this when new backoffice config etc. can be moved to core -->
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Cms.ManagementApi</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
23
src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs
Normal file
23
src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Umbraco.New.Cms.Core;
|
||||
|
||||
// TODO: move this class to Umbraco.Cms.Core as a partial class
|
||||
public static class Constants
|
||||
{
|
||||
public static partial class OauthClientIds
|
||||
{
|
||||
/// <summary>
|
||||
/// Client ID used for default back-office access
|
||||
/// </summary>
|
||||
public const string BackOffice = "umbraco-back-office";
|
||||
|
||||
/// <summary>
|
||||
/// Client ID used for Swagger API access
|
||||
/// </summary>
|
||||
public const string Swagger = "umbraco-swagger";
|
||||
|
||||
/// <summary>
|
||||
/// Client ID used for Postman API access
|
||||
/// </summary>
|
||||
public const string Postman = "umbraco-postman";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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($"{Umbraco.Cms.Core.Constants.Configuration.ConfigPrefix}NewBackOffice")]
|
||||
public class NewBackOfficeSettings
|
||||
{
|
||||
public Uri? BackOfficeHost { get; set; } = null;
|
||||
}
|
||||
@@ -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<NewBackOfficeSettings>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<OpenIddictCleanup> _logger;
|
||||
private readonly IServiceProvider _provider;
|
||||
|
||||
public OpenIddictCleanup(
|
||||
ILogger<OpenIddictCleanup> 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<IOpenIddictTokenManager>()
|
||||
?? 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<IOpenIddictAuthorizationManager>()
|
||||
?? 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.New.Cms.Infrastructure.Security;
|
||||
|
||||
public interface IBackOfficeApplicationManager
|
||||
{
|
||||
Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -10,4 +10,8 @@
|
||||
<ProjectReference Include="..\Umbraco.New.Cms.Core\Umbraco.New.Cms.Core.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenIddict.Abstractions" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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("/security/back-office/authorize") > 0
|
||||
? locationObj.returnPath
|
||||
: decodeURIComponent(locationObj.returnPath);
|
||||
}
|
||||
|
||||
// Ensure path is not absolute
|
||||
|
||||
@@ -23,7 +23,8 @@ public class TestUmbracoContextFactory
|
||||
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 class TestUmbracoContextFactory
|
||||
publishedUrlProvider = Mock.Of<IPublishedUrlProvider>();
|
||||
}
|
||||
|
||||
if (umbracoRequestPathsOptions == null)
|
||||
{
|
||||
umbracoRequestPathsOptions = new UmbracoRequestPathsOptions();
|
||||
}
|
||||
|
||||
var contentCache = new Mock<IPublishedContentCache>();
|
||||
var mediaCache = new Mock<IPublishedMediaCache>();
|
||||
var snapshot = new Mock<IPublishedSnapshot>();
|
||||
@@ -58,7 +64,7 @@ public class TestUmbracoContextFactory
|
||||
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),
|
||||
|
||||
@@ -18,10 +18,12 @@ public class UmbracoRequestPathsTests
|
||||
{
|
||||
_hostEnvironment = Mock.Of<IWebHostEnvironment>();
|
||||
_globalSettings = new GlobalSettings();
|
||||
_umbracoRequestPathsOptions = new UmbracoRequestPathsOptions();
|
||||
}
|
||||
|
||||
private IWebHostEnvironment _hostEnvironment;
|
||||
private GlobalSettings _globalSettings;
|
||||
private UmbracoRequestPathsOptions _umbracoRequestPathsOptions;
|
||||
|
||||
private IHostingEnvironment CreateHostingEnvironment(string virtualPath = "")
|
||||
{
|
||||
@@ -49,7 +51,7 @@ public class UmbracoRequestPathsTests
|
||||
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 class UmbracoRequestPathsTests
|
||||
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,11 +87,13 @@ public class UmbracoRequestPathsTests
|
||||
[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)]
|
||||
// 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));
|
||||
}
|
||||
|
||||
@@ -106,7 +110,21 @@ public class UmbracoRequestPathsTests
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IRuntimeState>(x => x.Level == RuntimeLevel.Install);
|
||||
var mgr = new BackOfficeCookieManager(
|
||||
Mock.Of<IUmbracoContextAccessor>(),
|
||||
runtime,
|
||||
new UmbracoRequestPaths(Options.Create(globalSettings), TestHelper.GetHostingEnvironment()),
|
||||
new UmbracoRequestPaths(Options.Create(globalSettings), TestHelper.GetHostingEnvironment(), Options.Create(umbracoRequestPathsOptions)),
|
||||
Mock.Of<IBasicAuthService>());
|
||||
|
||||
var result = mgr.ShouldAuthenticateRequest("/umbraco");
|
||||
@@ -41,6 +42,7 @@ public class BackOfficeCookieManagerTests
|
||||
public void ShouldAuthenticateRequest_When_Configured()
|
||||
{
|
||||
var globalSettings = new GlobalSettings();
|
||||
var umbracoRequestPathsOptions = new UmbracoRequestPathsOptions();
|
||||
|
||||
var runtime = Mock.Of<IRuntimeState>(x => x.Level == RuntimeLevel.Run);
|
||||
var mgr = new BackOfficeCookieManager(
|
||||
@@ -49,7 +51,8 @@ public class BackOfficeCookieManagerTests
|
||||
new UmbracoRequestPaths(
|
||||
Options.Create(globalSettings),
|
||||
Mock.Of<IHostingEnvironment>(x =>
|
||||
x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco")),
|
||||
x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"),
|
||||
Options.Create(umbracoRequestPathsOptions)),
|
||||
Mock.Of<IBasicAuthService>());
|
||||
|
||||
var result = mgr.ShouldAuthenticateRequest("/umbraco");
|
||||
@@ -61,6 +64,7 @@ public class BackOfficeCookieManagerTests
|
||||
public void ShouldAuthenticateRequest_Is_Back_Office()
|
||||
{
|
||||
var globalSettings = new GlobalSettings();
|
||||
var umbracoRequestPathsOptions = new UmbracoRequestPathsOptions();
|
||||
|
||||
var runtime = Mock.Of<IRuntimeState>(x => x.Level == RuntimeLevel.Run);
|
||||
|
||||
@@ -73,7 +77,8 @@ public class BackOfficeCookieManagerTests
|
||||
Options.Create(globalSettings),
|
||||
Mock.Of<IHostingEnvironment>(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<IBasicAuthService>());
|
||||
|
||||
var result = mgr.ShouldAuthenticateRequest(remainingTimeoutSecondsPath);
|
||||
@@ -87,6 +92,7 @@ public class BackOfficeCookieManagerTests
|
||||
public void ShouldAuthenticateRequest_Not_Back_Office()
|
||||
{
|
||||
var globalSettings = new GlobalSettings();
|
||||
var umbracoRequestPathsOptions = new UmbracoRequestPathsOptions();
|
||||
|
||||
var runtime = Mock.Of<IRuntimeState>(x => x.Level == RuntimeLevel.Run);
|
||||
|
||||
@@ -97,7 +103,8 @@ public class BackOfficeCookieManagerTests
|
||||
Options.Create(globalSettings),
|
||||
Mock.Of<IHostingEnvironment>(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<IBasicAuthService>());
|
||||
|
||||
var result = mgr.ShouldAuthenticateRequest("/notbackoffice");
|
||||
|
||||
Reference in New Issue
Block a user