Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e5f8f34
First attempt at OpenIddict
kjac Sep 28, 2022
e038e1c
Making headway and more TODOs
kjac Oct 5, 2022
10077c4
Redo current policies for multiple schemas + clean up auth controller
kjac Oct 5, 2022
df7fab2
Merge branch 'v10/dev' into v10/feature/new-backoffice-auth
kjac Oct 5, 2022
98d24ce
Fix bad merge
kjac Oct 5, 2022
b141966
Clean up some more test code
kjac Oct 5, 2022
d8c3547
Fix spacing
kjac Oct 5, 2022
ab702e0
Include AddAuthentication() in OpenIddict addition
kjac Oct 6, 2022
1b85e8e
A little more clean-up
kjac Oct 6, 2022
4c61d4e
Move application creation to its own implementation + prepare for mid…
kjac Oct 9, 2022
c94e55d
Enable refresh token flow
kjac Oct 9, 2022
16366e3
Merge branch 'v11/dev' into v11/new-backoffice/openiddict
kjac Oct 13, 2022
6196260
Fix bad merge from v11/dev
kjac Oct 13, 2022
c30cecb
Merge branch 'v11/dev' into v11/new-backoffice/openiddict
kjac Oct 14, 2022
d614562
Support auth for Swagger and Postman in non-production environments +…
kjac Oct 17, 2022
4114678
Merge branch 'v11/dev' into v11/new-backoffice/openiddict
kjac Oct 17, 2022
6c49868
Add workaround to client side login handling so the OAuth return URL …
kjac Oct 19, 2022
cccc8c5
Add temporary configuration handling for new backoffice
kjac Oct 19, 2022
c142d22
Restructure the code somewhat, move singular responsibility from mana…
kjac Oct 20, 2022
2f57b4b
Add recurring task for cleaning up old tokens in the DB
kjac Oct 21, 2022
7932973
Merge branch 'v11/dev' into v11/new-backoffice/openiddict
kjac Oct 21, 2022
0884483
Fix bad merge + make auth controller align with the new management AP…
kjac Oct 21, 2022
9fdfc4d
Explicitly handle the new management API path as a backoffice path (N…
kjac Oct 21, 2022
c13039a
Redo handle the new management API requests as backoffice requests, t…
kjac Oct 24, 2022
21effe7
Merge branch 'v11/dev' into v11/new-backoffice/openiddict
kjac Oct 25, 2022
d204149
Add/update TODOs
kjac Oct 25, 2022
b7ec6a1
Merge branch 'v11/dev' into v11/new-backoffice/openiddict
kjac Oct 27, 2022
e8dc663
Revert duplication of current auth policies for OpenIddict (as it bre…
kjac Oct 31, 2022
9e1c53c
Fix failing unit tests
kjac Oct 31, 2022
8101a7c
Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationMa…
kjac Nov 1, 2022
3128608
Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationMa…
kjac Nov 1, 2022
0157dbd
Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationMa…
kjac Nov 1, 2022
88670a8
Update src/Umbraco.Core/Routing/UmbracoRequestPaths.cs
kjac Nov 1, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs
Original file line number Diff line number Diff line change
@@ -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}";
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -17,6 +19,12 @@ internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder)
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;
}
}
37 changes: 33 additions & 4 deletions src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@
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;
Expand All @@ -44,7 +45,8 @@ public void Compose(IUmbracoBuilder builder)
.AddTrees()
.AddFactories()
.AddServices()
.AddMappers();
.AddMappers()
.AddBackOfficeAuthentication();

services.AddApiVersioning(options =>
{
Expand All @@ -65,6 +67,20 @@ public void Compose(IUmbracoBuilder builder)
{
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 =>
Expand All @@ -78,6 +94,10 @@ public void Compose(IUmbracoBuilder builder)
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(
Expand Down Expand Up @@ -125,6 +145,7 @@ public void Compose(IUmbracoBuilder builder)
{
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 =>
Expand All @@ -141,6 +162,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)
};
});
}
},
Expand Down
Loading