Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Web.BackOffice.Security;
using Umbraco.Extensions;
using Umbraco.New.Cms.Web.Common.Routing;
using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult;
using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult;

namespace Umbraco.Cms.ManagementApi.Controllers.Security;

Expand All @@ -21,12 +26,18 @@ public class BackOfficeController : ManagementApiControllerBase
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IBackOfficeSignInManager _backOfficeSignInManager;
private readonly IBackOfficeUserManager _backOfficeUserManager;
private readonly IOptions<SecuritySettings> _securitySettings;

public BackOfficeController(IHttpContextAccessor httpContextAccessor, IBackOfficeSignInManager backOfficeSignInManager, IBackOfficeUserManager backOfficeUserManager)
public BackOfficeController(
IHttpContextAccessor httpContextAccessor,
IBackOfficeSignInManager backOfficeSignInManager,
IBackOfficeUserManager backOfficeUserManager,
IOptions<SecuritySettings> securitySettings)
{
_httpContextAccessor = httpContextAccessor;
_backOfficeSignInManager = backOfficeSignInManager;
_backOfficeUserManager = backOfficeUserManager;
_securitySettings = securitySettings;
}

[HttpGet("authorize")]
Expand All @@ -41,36 +52,93 @@ public async Task<IActionResult> Authorize()
return BadRequest("Unable to obtain OpenID data from the current request");
}

return request.IdentityProvider.IsNullOrWhiteSpace()
? await AuthorizeInternal(request)
: await AuthorizeExternal(request);
}

private async Task<IActionResult> AuthorizeInternal(OpenIddictRequest request)
{
// TODO: ensure we handle sign-in notifications for internal logins.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO has been added to the backlog item "New login screen" (23812)

// when the new login screen is implemented for internal logins, make sure it still handles
// user sign-in notifications (calls BackOfficeSignInManager.HandleSignIn) as part of the
// sign-in process
// for future reference, notifications are already handled for the external login flow by
// by calling BackOfficeSignInManager.ExternalLoginSignInAsync

// 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)
var userName = cookieAuthResult.Succeeded
? cookieAuthResult.Principal?.Identity?.Name
: null;

if (userName != null)
{
BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(cookieAuthResult.Principal.Identity.Name);
BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(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);
}
return await SignInBackOfficeUser(backOfficeUser, request);
}
}

return DefaultChallengeResult();
}

private async Task<IActionResult> AuthorizeExternal(OpenIddictRequest request)
{
var provider = request.IdentityProvider ?? throw new ArgumentException("No identity provider found in request", nameof(request));

ExternalLoginInfo? loginInfo = await _backOfficeSignInManager.GetExternalLoginInfoAsync();
if (loginInfo?.Principal != null)
{
IdentitySignInResult result = await _backOfficeSignInManager.ExternalLoginSignInAsync(loginInfo, false, _securitySettings.Value.UserBypassTwoFactorForExternalLogins);

if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess))
if (result.Succeeded)
{
// Update any authentication tokens if succeeded
await _backOfficeSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo);

// sign in the backoffice user associated with the login provider and unique provider key
BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
if (backOfficeUser != null)
{
// "offline_access" scope is required to use refresh tokens
backOfficePrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess);
return await SignInBackOfficeUser(backOfficeUser, request);
}

return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal);
}
else
{
// avoid infinite auth loops when something fails by performing the default challenge (default login screen)
return DefaultChallengeResult();
}
}

return new ChallengeResult(new[] { Constants.Security.BackOfficeAuthenticationType });
AuthenticationProperties properties = _backOfficeSignInManager.ConfigureExternalAuthenticationProperties(provider, null);
return new ChallengeResult(provider, properties);
}

private async Task<IActionResult> SignInBackOfficeUser(BackOfficeIdentityUser backOfficeUser, OpenIddictRequest request)
{
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);
}

private static IActionResult DefaultChallengeResult() => new ChallengeResult(Constants.Security.BackOfficeAuthenticationType);
}