Merge remote-tracking branch 'origin/netcore/netcore' into netcore/feature/linux-paths
# Conflicts: # src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs
This commit is contained in:
@@ -17,8 +17,6 @@ namespace Umbraco.Extensions
|
||||
/// <returns></returns>
|
||||
public static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user)
|
||||
{
|
||||
// TODO: It would be nice to get rid of this and only rely on Claims, not a strongly typed identity instance
|
||||
|
||||
//If it's already a UmbracoBackOfficeIdentity
|
||||
if (user.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) return backOfficeIdentity;
|
||||
|
||||
@@ -55,10 +53,10 @@ namespace Umbraco.Extensions
|
||||
/// <returns></returns>
|
||||
public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now)
|
||||
{
|
||||
var claimsPrincipal = user as ClaimsPrincipal;
|
||||
if (claimsPrincipal == null) return 0;
|
||||
var umbIdentity = user.GetUmbracoIdentity();
|
||||
if (umbIdentity == null) return 0;
|
||||
|
||||
var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value;
|
||||
var ticketExpires = umbIdentity.FindFirstValue(Constants.Security.TicketExpiresClaimType);
|
||||
if (ticketExpires.IsNullOrWhiteSpace()) return 0;
|
||||
|
||||
var utcExpired = DateTimeOffset.Parse(ticketExpires, null, DateTimeStyles.RoundtripKind);
|
||||
|
||||
@@ -32,6 +32,13 @@ namespace Umbraco.Core.Security
|
||||
/// <returns></returns>
|
||||
ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true);
|
||||
|
||||
/// <summary>
|
||||
/// Authorizes the full request, checks for SSL and validates the current user
|
||||
/// </summary>
|
||||
/// <param name="throwExceptions">set to true if you want exceptions to be thrown if failed</param>
|
||||
/// <returns></returns>
|
||||
ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specified user as access to the app
|
||||
/// </summary>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@@ -22,7 +21,6 @@ using Umbraco.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.BackOffice.Filters;
|
||||
using Umbraco.Web.BackOffice.Security;
|
||||
using Umbraco.Web.Common.ActionsResults;
|
||||
using Umbraco.Web.Common.Attributes;
|
||||
using Umbraco.Web.Common.Controllers;
|
||||
@@ -46,7 +44,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)] // TODO: Maybe this could be applied with our Application Model conventions
|
||||
//[ValidationFilter] // TODO: I don't actually think this is required with our custom Application Model conventions applied
|
||||
[AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions
|
||||
[IsBackOffice]
|
||||
[IsBackOffice] // TODO: This could be applied with our Application Model conventions
|
||||
public class AuthenticationController : UmbracoApiControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
|
||||
@@ -165,6 +163,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
var user = await _userManager.FindByIdAsync(User.Identity.GetUserId());
|
||||
if (user == null) throw new InvalidOperationException("Could not find user");
|
||||
|
||||
ExternalSignInAutoLinkOptions autoLinkOptions = null;
|
||||
var authType = (await _signInManager.GetExternalAuthenticationSchemesAsync())
|
||||
.FirstOrDefault(x => x.Name == unlinkLoginModel.LoginProvider);
|
||||
|
||||
@@ -174,18 +173,11 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var opt = _externalAuthenticationOptions.Get(authType.Name);
|
||||
if (opt == null)
|
||||
autoLinkOptions = _externalAuthenticationOptions.Get(authType.Name);
|
||||
if (!autoLinkOptions.AllowManualLinking)
|
||||
{
|
||||
return BadRequest($"Could not find external authentication options registered for provider {unlinkLoginModel.LoginProvider}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!opt.Options.AutoLinkOptions.AllowManualLinking)
|
||||
{
|
||||
// If AllowManualLinking is disabled for this provider we cannot unlink
|
||||
return BadRequest();
|
||||
}
|
||||
// If AllowManualLinking is disabled for this provider we cannot unlink
|
||||
return BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,26 +199,18 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<double> GetRemainingTimeoutSeconds()
|
||||
public double GetRemainingTimeoutSeconds()
|
||||
{
|
||||
// force authentication to occur since this is not an authorized endpoint
|
||||
var result = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
|
||||
if (!result.Succeeded)
|
||||
var backOfficeIdentity = HttpContext.User.GetUmbracoIdentity();
|
||||
var remainingSeconds = HttpContext.User.GetRemainingAuthSeconds();
|
||||
if (remainingSeconds <= 30 && backOfficeIdentity != null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var remainingSeconds = result.Principal.GetRemainingAuthSeconds();
|
||||
if (remainingSeconds <= 30)
|
||||
{
|
||||
var username = result.Principal.FindFirst(ClaimTypes.Name)?.Value;
|
||||
|
||||
//NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in
|
||||
// the timeout process.
|
||||
|
||||
_logger.LogInformation(
|
||||
"User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}",
|
||||
username ?? "unknown",
|
||||
backOfficeIdentity.Name,
|
||||
_ipResolver.GetCurrentRequestIpAddress());
|
||||
}
|
||||
|
||||
@@ -238,11 +222,14 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<bool> IsAuthenticated()
|
||||
public bool IsAuthenticated()
|
||||
{
|
||||
// force authentication to occur since this is not an authorized endpoint
|
||||
var result = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
|
||||
return result.Succeeded;
|
||||
var attempt = _backofficeSecurityAccessor.BackOfficeSecurity.AuthorizeRequest();
|
||||
if (attempt == ValidateRequestAttempt.Success)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -256,7 +243,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </remarks>
|
||||
[UmbracoBackOfficeAuthorize]
|
||||
[SetAngularAntiForgeryTokens]
|
||||
[CheckIfUserTicketDataIsStale]
|
||||
//[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level
|
||||
public UserDetail GetCurrentUser()
|
||||
{
|
||||
var user = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
|
||||
@@ -572,17 +559,13 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ValidateAngularAntiForgeryToken]
|
||||
public async Task<IActionResult> PostLogout()
|
||||
public IActionResult PostLogout()
|
||||
{
|
||||
// force authentication to occur since this is not an authorized endpoint
|
||||
var result = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
|
||||
if (!result.Succeeded) return Ok();
|
||||
|
||||
await _signInManager.SignOutAsync();
|
||||
HttpContext.SignOutAsync(Constants.Security.BackOfficeAuthenticationType);
|
||||
|
||||
_logger.LogInformation("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, HttpContext.Connection.RemoteIpAddress);
|
||||
|
||||
var userId = int.Parse(result.Principal.Identity.GetUserId());
|
||||
var userId = int.Parse(User.Identity.GetUserId());
|
||||
var args = _userManager.RaiseLogoutSuccessEvent(User, userId);
|
||||
if (!args.SignOutRedirectUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
@@ -596,6 +579,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return the <see cref="UserDetail"/> for the given <see cref="IUser"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -34,14 +34,12 @@ using Microsoft.AspNetCore.Identity;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Web.Security;
|
||||
using Umbraco.Web.BackOffice.Security;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Controllers
|
||||
{
|
||||
[DisableBrowserCache]
|
||||
[DisableBrowserCache] //TODO Reintroduce
|
||||
//[UmbracoRequireHttps] //TODO Reintroduce
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeArea)]
|
||||
[IsBackOffice]
|
||||
public class BackOfficeController : UmbracoController
|
||||
{
|
||||
private readonly IBackOfficeUserManager _userManager;
|
||||
@@ -404,57 +402,186 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
{
|
||||
if (loginInfo == null) throw new ArgumentNullException(nameof(loginInfo));
|
||||
if (response == null) throw new ArgumentNullException(nameof(response));
|
||||
ExternalSignInAutoLinkOptions autoLinkOptions = null;
|
||||
|
||||
// Sign in the user with this external login provider (which auto links, etc...)
|
||||
var result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false);
|
||||
var authType = (await _signInManager.GetExternalAuthenticationSchemesAsync())
|
||||
.FirstOrDefault(x => x.Name == loginInfo.LoginProvider);
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
if (result == Microsoft.AspNetCore.Identity.SignInResult.Success)
|
||||
if (authType == null)
|
||||
{
|
||||
|
||||
_logger.LogWarning("Could not find external authentication provider registered: {LoginProvider}", loginInfo.LoginProvider);
|
||||
}
|
||||
else if (result == Microsoft.AspNetCore.Identity.SignInResult.LockedOut)
|
||||
else
|
||||
{
|
||||
// TODO: We've never actually dealt with this before
|
||||
}
|
||||
else if (result == Microsoft.AspNetCore.Identity.SignInResult.TwoFactorRequired)
|
||||
{
|
||||
// TODO: We've never actually dealt with this before
|
||||
}
|
||||
else if (result == Microsoft.AspNetCore.Identity.SignInResult.NotAllowed)
|
||||
{
|
||||
// TODO: We've never actually dealt with this before
|
||||
}
|
||||
else if (result == Microsoft.AspNetCore.Identity.SignInResult.Failed)
|
||||
{
|
||||
// Failed only occurs when the user does not exist
|
||||
errors.Add("The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account, the provider must be linked from the back office.");
|
||||
}
|
||||
else if (result == AutoLinkSignInResult.FailedNotLinked)
|
||||
{
|
||||
errors.Add("The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account, the provider must be linked from the back office.");
|
||||
}
|
||||
else if (result == AutoLinkSignInResult.FailedNoEmail)
|
||||
{
|
||||
errors.Add($"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked.");
|
||||
}
|
||||
else if (result is AutoLinkSignInResult autoLinkSignInResult && autoLinkSignInResult.Errors.Count > 0)
|
||||
{
|
||||
errors.AddRange(autoLinkSignInResult.Errors);
|
||||
autoLinkOptions = _externalLogins.Get(authType.Name);
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
// Sign in the user with this external login provider if the user already has a login
|
||||
|
||||
var user = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
|
||||
if (user != null)
|
||||
{
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo.LoginProvider,
|
||||
errors));
|
||||
var shouldSignIn = true;
|
||||
if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null)
|
||||
{
|
||||
shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo);
|
||||
if (shouldSignIn == false)
|
||||
{
|
||||
_logger.LogWarning("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.LoginProvider, user.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSignIn)
|
||||
{
|
||||
//sign in
|
||||
await _signInManager.SignInAsync(user, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions) == false)
|
||||
{
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo.LoginProvider,
|
||||
new[] { "The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account" }));
|
||||
}
|
||||
|
||||
//Remove the cookie otherwise this message will keep appearing
|
||||
Response.Cookies.Delete(Constants.Security.BackOfficeExternalCookieName);
|
||||
}
|
||||
|
||||
return response();
|
||||
}
|
||||
|
||||
private async Task<bool> AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions)
|
||||
{
|
||||
if (autoLinkOptions == null)
|
||||
return false;
|
||||
|
||||
if (autoLinkOptions.AutoLinkExternalAccount == false)
|
||||
return true; // TODO: This seems weird to return true, but it was like that before so must be a reason?
|
||||
|
||||
var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
|
||||
//we are allowing auto-linking/creating of local accounts
|
||||
if (email.IsNullOrWhiteSpace())
|
||||
{
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo.LoginProvider,
|
||||
new[] { $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked." }));
|
||||
}
|
||||
else
|
||||
{
|
||||
//Now we need to perform the auto-link, so first we need to lookup/create a user with the email address
|
||||
var autoLinkUser = await _userManager.FindByEmailAsync(email);
|
||||
if (autoLinkUser != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
//call the callback if one is assigned
|
||||
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo.LoginProvider,
|
||||
new[] { "Could not link login provider " + loginInfo.LoginProvider + ". " + ex.Message }));
|
||||
return true;
|
||||
}
|
||||
|
||||
await LinkUser(autoLinkUser, loginInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
var name = loginInfo.Principal?.Identity?.Name;
|
||||
if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null");
|
||||
|
||||
autoLinkUser = BackOfficeIdentityUser.CreateNew(_globalSettings, email, email, autoLinkOptions.GetUserAutoLinkCulture(_globalSettings), name);
|
||||
|
||||
foreach (var userGroup in autoLinkOptions.DefaultUserGroups)
|
||||
{
|
||||
autoLinkUser.AddRole(userGroup);
|
||||
}
|
||||
|
||||
//call the callback if one is assigned
|
||||
try
|
||||
{
|
||||
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo.LoginProvider,
|
||||
new[] { "Could not link login provider " + loginInfo.LoginProvider + ". " + ex.Message }));
|
||||
return true;
|
||||
}
|
||||
|
||||
var userCreationResult = await _userManager.CreateAsync(autoLinkUser);
|
||||
|
||||
if (userCreationResult.Succeeded == false)
|
||||
{
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo.LoginProvider,
|
||||
userCreationResult.Errors.Select(x => x.Description).ToList()));
|
||||
}
|
||||
else
|
||||
{
|
||||
await LinkUser(autoLinkUser, loginInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
|
||||
{
|
||||
var existingLogins = await _userManager.GetLoginsAsync(autoLinkUser);
|
||||
var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey);
|
||||
|
||||
// if it already exists (perhaps it was added in the AutoLink callbak) then we just continue
|
||||
if (exists != null)
|
||||
{
|
||||
//sign in
|
||||
await _signInManager.SignInAsync(autoLinkUser, isPersistent: false);
|
||||
return;
|
||||
}
|
||||
|
||||
var linkResult = await _userManager.AddLoginAsync(autoLinkUser, loginInfo);
|
||||
if (linkResult.Succeeded)
|
||||
{
|
||||
//we're good! sign in
|
||||
await _signInManager.SignInAsync(autoLinkUser, isPersistent: false);
|
||||
return;
|
||||
}
|
||||
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo.LoginProvider,
|
||||
linkResult.Errors.Select(x => x.Description).ToList()));
|
||||
|
||||
//If this fails, we should really delete the user since it will be in an inconsistent state!
|
||||
var deleteResult = await _userManager.DeleteAsync(autoLinkUser);
|
||||
if (!deleteResult.Succeeded)
|
||||
{
|
||||
//DOH! ... this isn't good, combine all errors to be shown
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo.LoginProvider,
|
||||
linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList()));
|
||||
}
|
||||
}
|
||||
|
||||
// Used for XSRF protection when adding external logins
|
||||
// TODO: This is duplicated in BackOfficeSignInManager
|
||||
private const string XsrfKey = "XsrfId";
|
||||
|
||||
private IActionResult RedirectToLocal(string returnUrl)
|
||||
{
|
||||
if (Url.IsLocalUrl(returnUrl))
|
||||
|
||||
@@ -17,9 +17,9 @@ using Umbraco.Web.BackOffice.HealthCheck;
|
||||
using Umbraco.Web.BackOffice.Profiling;
|
||||
using Umbraco.Web.BackOffice.PropertyEditors;
|
||||
using Umbraco.Web.BackOffice.Routing;
|
||||
using Umbraco.Web.BackOffice.Security;
|
||||
using Umbraco.Web.BackOffice.Trees;
|
||||
using Umbraco.Web.Common.Attributes;
|
||||
using Umbraco.Web.Common.Security;
|
||||
using Umbraco.Web.Features;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
using Umbraco.Web.Trees;
|
||||
@@ -135,7 +135,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// Returns the server variables for authenticated users
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal Task<Dictionary<string, object>> GetServerVariablesAsync()
|
||||
internal async Task<Dictionary<string, object>> GetServerVariablesAsync()
|
||||
{
|
||||
var globalSettings = _globalSettings;
|
||||
var backOfficeControllerName = ControllerExtensions.GetControllerName<BackOfficeController>();
|
||||
@@ -149,8 +149,8 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
// having each url defined here explicitly - we can do that in v8! for now
|
||||
// for umbraco services we'll stick to explicitly defining the endpoints.
|
||||
|
||||
{"externalLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.ExternalLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
|
||||
{"externalLinkLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.LinkLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
|
||||
// {"externalLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.ExternalLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
|
||||
// {"externalLinkLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.LinkLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
|
||||
{"gridConfig", _linkGenerator.GetPathByAction(nameof(BackOfficeController.GetGridConfig), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
|
||||
// TODO: This is ultra confusing! this same key is used for different things, when returning the full app when authenticated it is this URL but when not auth'd it's actually the ServerVariables address
|
||||
{"serverVarsJs", _linkGenerator.GetPathByAction(nameof(BackOfficeController.Application), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
|
||||
@@ -418,14 +418,11 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
"externalLogins", new Dictionary<string, object>
|
||||
{
|
||||
{
|
||||
// TODO: It would be nicer to not have to manually translate these properties
|
||||
// but then needs to be changed in quite a few places in angular
|
||||
"providers", _externalLogins.GetBackOfficeProviders()
|
||||
.Select(p => new
|
||||
{
|
||||
authType = p.AuthenticationType,
|
||||
caption = p.Name,
|
||||
properties = p.Options
|
||||
authType = p.AuthenticationType, caption = p.Name,
|
||||
properties = p.Properties
|
||||
})
|
||||
.ToArray()
|
||||
}
|
||||
@@ -444,7 +441,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
}
|
||||
};
|
||||
return Task.FromResult(defaultVals);
|
||||
return defaultVals;
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
|
||||
@@ -8,7 +8,8 @@ using Umbraco.Web.PublishedCache;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Controllers
|
||||
{
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
|
||||
[IsBackOffice]
|
||||
public class PublishedSnapshotCacheStatusController : UmbracoAuthorizedApiController
|
||||
{
|
||||
private readonly IPublishedSnapshotService _publishedSnapshotService;
|
||||
|
||||
@@ -41,6 +41,7 @@ using IUser = Umbraco.Core.Models.Membership.IUser;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Common.ActionsResults;
|
||||
using Umbraco.Web.Common.Security;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Controllers
|
||||
{
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Umbraco.Extensions
|
||||
services.TryAddScoped<BackOfficeLookupNormalizer>();
|
||||
services.TryAddScoped<BackOfficeIdentityErrorDescriber>();
|
||||
services.TryAddScoped<IIpResolver, AspNetCoreIpResolver>();
|
||||
services.TryAddSingleton<IBackOfficeExternalLoginProviders, BackOfficeExternalLoginProviders>();
|
||||
services.TryAddSingleton<IBackOfficeExternalLoginProviders, NopBackOfficeExternalLoginProviders>();
|
||||
|
||||
/*
|
||||
* IdentityBuilderExtensions.AddUserManager adds UserManager<BackOfficeIdentityUser> to service collection
|
||||
|
||||
@@ -13,13 +13,13 @@ using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.WebAssets;
|
||||
using Umbraco.Web.BackOffice.Controllers;
|
||||
using Umbraco.Web.Common.Security;
|
||||
using Umbraco.Web.Features;
|
||||
using Umbraco.Web.Models;
|
||||
using Umbraco.Web.WebApi;
|
||||
using Umbraco.Web.WebAssets;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.BackOffice.Security;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
{
|
||||
@@ -75,7 +75,7 @@ namespace Umbraco.Extensions
|
||||
{
|
||||
authType = p.AuthenticationType,
|
||||
caption = p.Name,
|
||||
properties = p.Options
|
||||
properties = p.Properties
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
|
||||
@@ -31,10 +31,8 @@ namespace Umbraco.Extensions
|
||||
{
|
||||
builder.Services.AddAntiforgery();
|
||||
builder.Services.AddSingleton<IFilterProvider, OverrideAuthorizationFilterProvider>();
|
||||
|
||||
builder.Services
|
||||
.AddAuthentication() // This just creates a builder, nothing more
|
||||
// Add our custom schemes which are cookie handlers
|
||||
.AddAuthentication(Core.Constants.Security.BackOfficeAuthenticationType)
|
||||
.AddCookie(Core.Constants.Security.BackOfficeAuthenticationType)
|
||||
.AddCookie(Core.Constants.Security.BackOfficeExternalAuthenticationType, o =>
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using System;
|
||||
using Umbraco.Web.BackOffice.Security;
|
||||
using Umbraco.Web.Common.Security;
|
||||
|
||||
namespace Umbraco.Web.Editors.Filters
|
||||
{
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
public class AutoLinkSignInResult : SignInResult
|
||||
{
|
||||
public static AutoLinkSignInResult FailedNotLinked = new AutoLinkSignInResult()
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedNoEmail = new AutoLinkSignInResult()
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedException(string error) => new AutoLinkSignInResult(new[] { error })
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedCreatingUser(IReadOnlyCollection<string> errors) => new AutoLinkSignInResult(errors)
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedLinkingUser(IReadOnlyCollection<string> errors) => new AutoLinkSignInResult(errors)
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public AutoLinkSignInResult(IReadOnlyCollection<string> errors)
|
||||
{
|
||||
Errors = errors ?? throw new ArgumentNullException(nameof(errors));
|
||||
}
|
||||
|
||||
public AutoLinkSignInResult()
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> Errors { get; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Builder;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom <see cref="AuthenticationBuilder"/> used to associate external logins with umbraco external login options
|
||||
/// </summary>
|
||||
public class BackOfficeAuthenticationBuilder : AuthenticationBuilder
|
||||
{
|
||||
private readonly BackOfficeExternalLoginProviderOptions _loginProviderOptions;
|
||||
|
||||
public BackOfficeAuthenticationBuilder(IServiceCollection services, BackOfficeExternalLoginProviderOptions loginProviderOptions)
|
||||
: base(services)
|
||||
{
|
||||
_loginProviderOptions = loginProviderOptions;
|
||||
}
|
||||
|
||||
public string SchemeForBackOffice(string scheme)
|
||||
{
|
||||
return Constants.Security.BackOfficeExternalAuthenticationTypePrefix + scheme;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to track the final authenticationScheme being registered for the external login
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions"></typeparam>
|
||||
/// <typeparam name="THandler"></typeparam>
|
||||
/// <param name="authenticationScheme"></param>
|
||||
/// <param name="displayName"></param>
|
||||
/// <param name="configureOptions"></param>
|
||||
/// <returns></returns>
|
||||
public override AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
|
||||
{
|
||||
// Validate that the prefix is set
|
||||
if (!authenticationScheme.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix))
|
||||
{
|
||||
throw new InvalidOperationException($"The {nameof(authenticationScheme)} is not prefixed with {Constants.Security.BackOfficeExternalAuthenticationTypePrefix}. The scheme must be created with a call to the method {nameof(SchemeForBackOffice)}");
|
||||
}
|
||||
|
||||
// add our login provider to the container along with a custom options configuration
|
||||
Services.AddSingleton(x => new BackOfficeExternalLoginProvider(displayName, authenticationScheme, _loginProviderOptions));
|
||||
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureBackOfficeScheme<TOptions>>());
|
||||
|
||||
return base.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
|
||||
// TODO: We could override and throw NotImplementedException for other methods?
|
||||
|
||||
// Ensures that the sign in scheme is always the Umbraco back office external type
|
||||
private class EnsureBackOfficeScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
|
||||
{
|
||||
public void PostConfigure(string name, TOptions options)
|
||||
{
|
||||
options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to add back office login providers
|
||||
/// </summary>
|
||||
public class BackOfficeExternalLoginsBuilder
|
||||
{
|
||||
public BackOfficeExternalLoginsBuilder(IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
private readonly IServiceCollection _services;
|
||||
|
||||
/// <summary>
|
||||
/// Add a back office login provider with options
|
||||
/// </summary>
|
||||
/// <param name="loginProviderOptions"></param>
|
||||
/// <param name="build"></param>
|
||||
/// <returns></returns>
|
||||
public BackOfficeExternalLoginsBuilder AddBackOfficeLogin(
|
||||
BackOfficeExternalLoginProviderOptions loginProviderOptions,
|
||||
Action<BackOfficeAuthenticationBuilder> build)
|
||||
{
|
||||
build(new BackOfficeAuthenticationBuilder(_services, loginProviderOptions));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AuthenticationBuilderExtensions
|
||||
{
|
||||
public static IUmbracoBuilder AddBackOfficeExternalLogins(this IUmbracoBuilder umbracoBuilder, Action<BackOfficeExternalLoginsBuilder> builder)
|
||||
{
|
||||
builder(new BackOfficeExternalLoginsBuilder(umbracoBuilder.Services));
|
||||
return umbracoBuilder;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We need to implement this and extend it to support the back office external login options
|
||||
// basically migrate things from AuthenticationManagerExtensions & AuthenticationOptionsExtensions
|
||||
// and use this to get the back office external login infos
|
||||
public interface IBackOfficeExternalLoginProviders
|
||||
{
|
||||
BackOfficeExternalLoginProvider Get(string authenticationType);
|
||||
|
||||
IEnumerable<BackOfficeExternalLoginProvider> GetBackOfficeProviders();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option
|
||||
/// </summary>
|
||||
/// <param name="manager"></param>
|
||||
/// <returns></returns>
|
||||
string GetAutoLoginProvider();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if there is any external provider that has the Deny Local Login option configured
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool HasDenyLocalLogin();
|
||||
}
|
||||
|
||||
public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProviders
|
||||
{
|
||||
public BackOfficeExternalLoginProviders(IEnumerable<BackOfficeExternalLoginProvider> externalLogins)
|
||||
{
|
||||
_externalLogins = externalLogins;
|
||||
}
|
||||
|
||||
private readonly IEnumerable<BackOfficeExternalLoginProvider> _externalLogins;
|
||||
|
||||
public BackOfficeExternalLoginProvider Get(string authenticationType)
|
||||
{
|
||||
return _externalLogins.FirstOrDefault(x => x.AuthenticationType == authenticationType);
|
||||
}
|
||||
|
||||
public string GetAutoLoginProvider()
|
||||
{
|
||||
var found = _externalLogins.Where(x => x.Options.AutoRedirectLoginToExternalProvider).ToList();
|
||||
return found.Count > 0 ? found[0].AuthenticationType : null;
|
||||
}
|
||||
|
||||
public IEnumerable<BackOfficeExternalLoginProvider> GetBackOfficeProviders()
|
||||
{
|
||||
return _externalLogins;
|
||||
}
|
||||
|
||||
public bool HasDenyLocalLogin()
|
||||
{
|
||||
var found = _externalLogins.Where(x => x.Options.DenyLocalLogin).ToList();
|
||||
return found.Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class BackOfficeExternalLoginProvider : IEquatable<BackOfficeExternalLoginProvider>
|
||||
{
|
||||
public BackOfficeExternalLoginProvider(string name, string authenticationType, BackOfficeExternalLoginProviderOptions properties)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
AuthenticationType = authenticationType ?? throw new ArgumentNullException(nameof(authenticationType));
|
||||
Options = properties ?? throw new ArgumentNullException(nameof(properties));
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string AuthenticationType { get; }
|
||||
public BackOfficeExternalLoginProviderOptions Options { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return Equals(obj as BackOfficeExternalLoginProvider);
|
||||
}
|
||||
|
||||
public bool Equals(BackOfficeExternalLoginProvider other)
|
||||
{
|
||||
return other != null &&
|
||||
Name == other.Name &&
|
||||
AuthenticationType == other.AuthenticationType;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Name, AuthenticationType);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,8 +6,6 @@ using Umbraco.Web.Common.Attributes;
|
||||
|
||||
namespace Umbraco.Web.Common.ApplicationModels
|
||||
{
|
||||
// TODO: This should just exist in the back office project
|
||||
|
||||
/// <summary>
|
||||
/// An application model provider for all Umbraco Back Office controllers
|
||||
/// </summary>
|
||||
@@ -51,7 +49,12 @@ namespace Umbraco.Web.Common.ApplicationModels
|
||||
}
|
||||
|
||||
private bool IsBackOfficeController(ControllerModel controller)
|
||||
=> controller.Attributes.OfType<IsBackOfficeAttribute>().Any();
|
||||
|
||||
{
|
||||
var pluginControllerAttribute = controller.Attributes.OfType<PluginControllerAttribute>().FirstOrDefault();
|
||||
return pluginControllerAttribute != null
|
||||
&& (pluginControllerAttribute.AreaName == Core.Constants.Web.Mvc.BackOfficeArea
|
||||
|| pluginControllerAttribute.AreaName == Core.Constants.Web.Mvc.BackOfficeApiArea
|
||||
|| pluginControllerAttribute.AreaName == Core.Constants.Web.Mvc.BackOfficeTreeArea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ using Umbraco.Web.Common.Filters;
|
||||
|
||||
namespace Umbraco.Web.Common.ApplicationModels
|
||||
{
|
||||
|
||||
// TODO: This should just exist in the back office project
|
||||
|
||||
public class BackOfficeIdentityCultureConvention : IActionModelConvention
|
||||
{
|
||||
public void Apply(ActionModel action)
|
||||
|
||||
@@ -81,7 +81,6 @@ namespace Umbraco.Web.Common.ApplicationModels
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsUmbracoApiController(ControllerModel controller)
|
||||
=> controller.Attributes.OfType<UmbracoApiControllerAttribute>().Any();
|
||||
private bool IsUmbracoApiController(ControllerModel controller) => controller.Attributes.OfType<UmbracoApiControllerAttribute>().Any();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Umbraco.Web.Common.ModelBinding;
|
||||
using System.Linq;
|
||||
using Umbraco.Web.Common.Attributes;
|
||||
using Umbraco.Web.Actions;
|
||||
using Umbraco.Web.Common.Filters;
|
||||
|
||||
namespace Umbraco.Web.Common.ApplicationModels
|
||||
{
|
||||
@@ -24,6 +21,4 @@ namespace Umbraco.Web.Common.ApplicationModels
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Umbraco.Web.Common.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures authorization is successful for a back office user.
|
||||
/// </summary>
|
||||
public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute, IAuthorizeData
|
||||
public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute
|
||||
{
|
||||
// Implements IAuthorizeData to return the back office scheme so that all requests with this attributes
|
||||
// get authenticated with this scheme.
|
||||
// TODO: We'll have to refactor this as part of the authz policy changes.
|
||||
public string AuthenticationSchemes { get; set; } = Umbraco.Core.Constants.Security.BackOfficeAuthenticationType;
|
||||
public string Policy { get; set; }
|
||||
public string Roles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using System;
|
||||
@@ -12,8 +11,6 @@ using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment;
|
||||
namespace Umbraco.Web.Common.Filters
|
||||
{
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Ensures authorization is successful for a back office user.
|
||||
/// </summary>
|
||||
@@ -31,7 +28,6 @@ namespace Umbraco.Web.Common.Filters
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly bool _redirectToUmbracoLogin;
|
||||
private string _redirectUrl;
|
||||
|
||||
|
||||
private UmbracoBackOfficeAuthorizeFilter(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
|
||||
@@ -1,42 +1,23 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Options used to configure back office external login providers
|
||||
/// </summary>
|
||||
public class BackOfficeExternalLoginProviderOptions
|
||||
{
|
||||
public BackOfficeExternalLoginProviderOptions(
|
||||
string buttonStyle, string icon,
|
||||
ExternalSignInAutoLinkOptions autoLinkOptions = null,
|
||||
bool denyLocalLogin = false,
|
||||
bool autoRedirectLoginToExternalProvider = false,
|
||||
string customBackOfficeView = null)
|
||||
{
|
||||
ButtonStyle = buttonStyle;
|
||||
Icon = icon;
|
||||
AutoLinkOptions = autoLinkOptions ?? new ExternalSignInAutoLinkOptions();
|
||||
DenyLocalLogin = denyLocalLogin;
|
||||
AutoRedirectLoginToExternalProvider = autoRedirectLoginToExternalProvider;
|
||||
CustomBackOfficeView = customBackOfficeView;
|
||||
}
|
||||
|
||||
public string ButtonStyle { get; }
|
||||
public string Icon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Options used to control how users can be auto-linked/created/updated based on the external login provider
|
||||
/// </summary>
|
||||
public ExternalSignInAutoLinkOptions AutoLinkOptions { get; }
|
||||
public ExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new ExternalSignInAutoLinkOptions();
|
||||
|
||||
/// <summary>
|
||||
/// When set to true will disable all local user login functionality
|
||||
/// </summary>
|
||||
public bool DenyLocalLogin { get; }
|
||||
public bool DenyLocalLogin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When specified this will automatically redirect to the OAuth login provider instead of prompting the user to click on the OAuth button first.
|
||||
@@ -45,7 +26,7 @@ namespace Umbraco.Web.BackOffice.Security
|
||||
/// This is generally used in conjunction with <see cref="DenyLocalLogin"/>. If more than one OAuth provider specifies this, the last registered
|
||||
/// provider's redirect settings will win.
|
||||
/// </remarks>
|
||||
public bool AutoRedirectLoginToExternalProvider { get; }
|
||||
public bool AutoRedirectLoginToExternalProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A virtual path to a custom angular view that is used to replace the entire UI that renders the external login button that the user interacts with
|
||||
@@ -54,6 +35,6 @@ namespace Umbraco.Web.BackOffice.Security
|
||||
/// If this view is specified it is 100% up to the user to render the html responsible for rendering the link/un-link buttons along with showing any errors
|
||||
/// that occur. This overrides what Umbraco normally does by default.
|
||||
/// </remarks>
|
||||
public string CustomBackOfficeView { get; }
|
||||
public string CustomBackOfficeView { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,10 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.BackOffice;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Web.BackOffice.Security;
|
||||
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
|
||||
// TODO: There's potential to extract an interface for this for only what we use and put that in Core without aspnetcore refs, but we need to wait till were done with it since there's a bit to implement
|
||||
@@ -26,28 +23,21 @@ namespace Umbraco.Web.Common.Security
|
||||
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
|
||||
private const string LoginProviderKey = "LoginProvider";
|
||||
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
|
||||
private const string XsrfKey = "XsrfId";
|
||||
private const string XsrfKey = "XsrfId"; // TODO: See BackOfficeController.XsrfKey
|
||||
|
||||
private BackOfficeUserManager _userManager;
|
||||
private readonly IBackOfficeExternalLoginProviders _externalLogins;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
|
||||
public BackOfficeSignInManager(
|
||||
BackOfficeUserManager userManager,
|
||||
IHttpContextAccessor contextAccessor,
|
||||
IBackOfficeExternalLoginProviders externalLogins,
|
||||
IUserClaimsPrincipalFactory<BackOfficeIdentityUser> claimsFactory,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
ILogger<SignInManager<BackOfficeIdentityUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes,
|
||||
IUserConfirmation<BackOfficeIdentityUser> confirmation)
|
||||
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_externalLogins = externalLogins;
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
// TODO: Need to migrate more from Umbraco.Web.Security.BackOfficeSignInManager
|
||||
@@ -200,8 +190,7 @@ namespace Umbraco.Web.Common.Security
|
||||
|
||||
await Context.SignOutAsync(Constants.Security.BackOfficeAuthenticationType);
|
||||
await Context.SignOutAsync(Constants.Security.BackOfficeExternalAuthenticationType);
|
||||
// TODO: Put this back in when we implement it
|
||||
//await Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
await Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
}
|
||||
|
||||
|
||||
@@ -310,51 +299,7 @@ namespace Umbraco.Web.Common.Security
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking
|
||||
/// </summary>
|
||||
/// <param name="loginProvider"></param>
|
||||
/// <param name="providerKey"></param>
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <param name="bypassTwoFactor"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<SignInResult> ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false)
|
||||
{
|
||||
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
|
||||
// to be able to deal with auto-linking and reduce duplicate lookups
|
||||
|
||||
var autoLinkOptions = _externalLogins.Get(loginInfo.LoginProvider)?.Options?.AutoLinkOptions;
|
||||
var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
|
||||
if (user == null)
|
||||
{
|
||||
// user doesn't exist so see if we can auto link
|
||||
return await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions);
|
||||
}
|
||||
|
||||
if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null)
|
||||
{
|
||||
var shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo);
|
||||
if (shouldSignIn == false)
|
||||
{
|
||||
Logger.LogWarning("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.LoginProvider, user.Id);
|
||||
}
|
||||
}
|
||||
|
||||
var error = await PreSignInCheck(user);
|
||||
if (error != null)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
return await SignInOrTwoFactorAsync(user, isPersistent, loginInfo.LoginProvider, bypassTwoFactor);
|
||||
}
|
||||
|
||||
public override Task<IEnumerable<AuthenticationScheme>> GetExternalAuthenticationSchemesAsync()
|
||||
{
|
||||
// TODO: We can filter these so that they only include the back office ones.
|
||||
// That can be done by either checking the scheme (maybe) or comparing it to what we have registered in the collection of BackOfficeExternalLoginProvider
|
||||
return base.GetExternalAuthenticationSchemesAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<SignInResult> SignInOrTwoFactorAsync(BackOfficeIdentityUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false)
|
||||
{
|
||||
@@ -525,117 +470,5 @@ namespace Umbraco.Web.Common.Security
|
||||
public string UserId { get; set; }
|
||||
public string LoginProvider { get; set; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used for auto linking/creating user accounts for external logins
|
||||
/// </summary>
|
||||
/// <param name="loginInfo"></param>
|
||||
/// <param name="autoLinkOptions"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<SignInResult> AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions)
|
||||
{
|
||||
// If there are no autolink options then the attempt is failed (user does not exist)
|
||||
if (autoLinkOptions == null || !autoLinkOptions.AutoLinkExternalAccount)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
|
||||
//we are allowing auto-linking/creating of local accounts
|
||||
if (email.IsNullOrWhiteSpace())
|
||||
{
|
||||
return AutoLinkSignInResult.FailedNoEmail;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Now we need to perform the auto-link, so first we need to lookup/create a user with the email address
|
||||
var autoLinkUser = await UserManager.FindByEmailAsync(email);
|
||||
if (autoLinkUser != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
//call the callback if one is assigned
|
||||
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
|
||||
return AutoLinkSignInResult.FailedException(ex.Message);
|
||||
}
|
||||
|
||||
return await LinkUser(autoLinkUser, loginInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
var name = loginInfo.Principal?.Identity?.Name;
|
||||
if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null");
|
||||
|
||||
autoLinkUser = BackOfficeIdentityUser.CreateNew(_globalSettings, email, email, autoLinkOptions.GetUserAutoLinkCulture(_globalSettings), name);
|
||||
|
||||
foreach (var userGroup in autoLinkOptions.DefaultUserGroups)
|
||||
{
|
||||
autoLinkUser.AddRole(userGroup);
|
||||
}
|
||||
|
||||
//call the callback if one is assigned
|
||||
try
|
||||
{
|
||||
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
|
||||
return AutoLinkSignInResult.FailedException(ex.Message);
|
||||
}
|
||||
|
||||
var userCreationResult = await _userManager.CreateAsync(autoLinkUser);
|
||||
|
||||
if (!userCreationResult.Succeeded)
|
||||
{
|
||||
return AutoLinkSignInResult.FailedCreatingUser(userCreationResult.Errors.Select(x => x.Description).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
return await LinkUser(autoLinkUser, loginInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SignInResult> LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
|
||||
{
|
||||
var existingLogins = await _userManager.GetLoginsAsync(autoLinkUser);
|
||||
var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey);
|
||||
|
||||
// if it already exists (perhaps it was added in the AutoLink callbak) then we just continue
|
||||
if (exists != null)
|
||||
{
|
||||
//sign in
|
||||
return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider);
|
||||
}
|
||||
|
||||
var linkResult = await _userManager.AddLoginAsync(autoLinkUser, loginInfo);
|
||||
if (linkResult.Succeeded)
|
||||
{
|
||||
//we're good! sign in
|
||||
return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider);
|
||||
}
|
||||
|
||||
//If this fails, we should really delete the user since it will be in an inconsistent state!
|
||||
var deleteResult = await _userManager.DeleteAsync(autoLinkUser);
|
||||
if (deleteResult.Succeeded)
|
||||
{
|
||||
var errors = linkResult.Errors.Select(x => x.Description).ToList();
|
||||
return AutoLinkSignInResult.FailedLinkingUser(errors);
|
||||
}
|
||||
else
|
||||
{
|
||||
//DOH! ... this isn't good, combine all errors to be shown
|
||||
var errors = linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList();
|
||||
return AutoLinkSignInResult.FailedLinkingUser(errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
// TODO: This is only for the back office, does it need to be in common?
|
||||
|
||||
public class BackOfficeSecurity : IBackOfficeSecurity
|
||||
{
|
||||
@@ -52,6 +51,18 @@ namespace Umbraco.Web.Common.Security
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false)
|
||||
{
|
||||
// check for secure connection
|
||||
if (_globalSettings.UseHttps && !_httpContextAccessor.GetRequiredHttpContext().Request.IsHttps)
|
||||
{
|
||||
if (throwExceptions) throw new SecurityException("This installation requires a secure connection (via SSL). Please update the URL to include https://");
|
||||
return ValidateRequestAttempt.FailedNoSsl;
|
||||
}
|
||||
return ValidateCurrentUser(throwExceptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Attempt<int> GetUserId()
|
||||
{
|
||||
@@ -94,7 +105,6 @@ namespace Umbraco.Web.Common.Security
|
||||
|
||||
var user = CurrentUser;
|
||||
|
||||
// TODO: All of this is done as part of identity/backofficesigninmanager
|
||||
// Check for console access
|
||||
if (user == null || (requiresApproval && user.IsApproved == false) || (user.IsLockedOut && RequestIsInUmbracoApplication(_httpContextAccessor, _globalSettings, _hostingEnvironment)))
|
||||
{
|
||||
|
||||
@@ -9,8 +9,6 @@ using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
// TODO: This is only for the back office, does it need to be in common?
|
||||
|
||||
public class BackOfficeSecurityFactory: IBackOfficeSecurityFactory
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
@@ -5,8 +5,9 @@ using Umbraco.Core.BackOffice;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using SecurityConstants = Umbraco.Core.Constants.Security;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Options used to configure auto-linking external OAuth providers
|
||||
/// </summary>
|
||||
@@ -21,12 +22,10 @@ namespace Umbraco.Web.BackOffice.Security
|
||||
public ExternalSignInAutoLinkOptions(
|
||||
bool autoLinkExternalAccount = false,
|
||||
string[] defaultUserGroups = null,
|
||||
string defaultCulture = null,
|
||||
bool allowManualLinking = true)
|
||||
string defaultCulture = null)
|
||||
{
|
||||
DefaultUserGroups = defaultUserGroups ?? new[] { SecurityConstants.EditorGroupAlias };
|
||||
AutoLinkExternalAccount = autoLinkExternalAccount;
|
||||
AllowManualLinking = allowManualLinking;
|
||||
_defaultCulture = defaultCulture;
|
||||
}
|
||||
|
||||
@@ -34,7 +33,7 @@ namespace Umbraco.Web.BackOffice.Security
|
||||
/// By default this is true which allows the user to manually link and unlink the external provider, if set to false the back office user
|
||||
/// will not see and cannot perform manual linking or unlinking of the external provider.
|
||||
/// </summary>
|
||||
public bool AllowManualLinking { get; }
|
||||
public bool AllowManualLinking { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// A callback executed during account auto-linking and before the user is persisted
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
// TODO: We need to implement this and extend it to support the back office external login options
|
||||
// basically migrate things from AuthenticationManagerExtensions & AuthenticationOptionsExtensions
|
||||
// and use this to get the back office external login infos
|
||||
public interface IBackOfficeExternalLoginProviders
|
||||
{
|
||||
ExternalSignInAutoLinkOptions Get(string authenticationType);
|
||||
|
||||
IEnumerable<BackOfficeExternalLoginProvider> GetBackOfficeProviders();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option
|
||||
/// </summary>
|
||||
/// <param name="manager"></param>
|
||||
/// <returns></returns>
|
||||
string GetAutoLoginProvider();
|
||||
|
||||
bool HasDenyLocalLogin();
|
||||
}
|
||||
|
||||
// TODO: This class is just a placeholder for later
|
||||
public class NopBackOfficeExternalLoginProviders : IBackOfficeExternalLoginProviders
|
||||
{
|
||||
public ExternalSignInAutoLinkOptions Get(string authenticationType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetAutoLoginProvider()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<BackOfficeExternalLoginProvider> GetBackOfficeProviders()
|
||||
{
|
||||
return Enumerable.Empty<BackOfficeExternalLoginProvider>();
|
||||
}
|
||||
|
||||
public bool HasDenyLocalLogin()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we'll need to register these somehow
|
||||
public class BackOfficeExternalLoginProvider
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string AuthenticationType { get; set; }
|
||||
|
||||
// TODO: I believe this should be replaced with just a reference to BackOfficeExternalLoginProviderOptions
|
||||
public IReadOnlyDictionary<string, object> Properties { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,7 +50,7 @@ function externalLoginInfoService(externalLoginInfo, umbRequestHelper) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return x.properties.AutoLinkOptions.AllowManualLinking;
|
||||
return x.properties.ExternalSignInAutoLinkOptions.AllowManualLinking;
|
||||
}
|
||||
});
|
||||
return providers;
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
|
||||
<div ng-if="login.customView" ng-include="login.customView"></div>
|
||||
|
||||
<div ng-if="!login.customView && login.properties.AutoLinkOptions.AllowManualLinking">
|
||||
<div ng-if="!login.customView && login.properties.ExternalSignInAutoLinkOptions.AllowManualLinking">
|
||||
<form ng-submit="linkProvider($event)" ng-if="login.linkedProviderKey == undefined" method="POST" action="{{externalLinkLoginFormAction}}" name="oauthloginform" id="oauthloginform-{{login.authType}}">
|
||||
<input type="hidden" name="provider" value="{{login.authType}}" />
|
||||
<button class="btn btn-block btn-social"
|
||||
ng-class="login.properties.ButtonStyle"
|
||||
ng-class="login.properties.SocialStyle"
|
||||
id="{{login.authType}}">
|
||||
|
||||
<i class="fa" ng-class="login.properties.SocialIcon"></i>
|
||||
@@ -67,7 +67,7 @@
|
||||
<button ng-if="login.linkedProviderKey != undefined"
|
||||
ng-click="unlink($event, login.authType, login.linkedProviderKey)"
|
||||
class="btn btn-block btn-social"
|
||||
ng-class="login.properties.ButtonStyle"
|
||||
ng-class="login.properties.SocialStyle"
|
||||
id="{{login.authType}}"
|
||||
name="provider"
|
||||
value="{{login.authType}}">
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<form method="POST" action="{{vm.externalLoginFormAction}}">
|
||||
<button type="submit"
|
||||
class="btn btn-block btn-social"
|
||||
ng-class="login.properties.ButtonStyle"
|
||||
ng-class="login.properties.SocialStyle"
|
||||
id="{{login.authType}}" name="provider" value="{{login.authType}}"
|
||||
title="Log in using your {{login.caption}} account">
|
||||
<i class="fa" ng-class="login.properties.SocialIcon"></i>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@using Microsoft.Extensions.Options;
|
||||
@using Umbraco.Core
|
||||
@using Umbraco.Web.WebAssets
|
||||
@using Umbraco.Web.BackOffice.Security
|
||||
@using Umbraco.Web.Common.Security
|
||||
@using Umbraco.Core.WebAssets
|
||||
@using Umbraco.Core.Configuration
|
||||
@using Umbraco.Core.Configuration.Models
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@using Microsoft.Extensions.Options;
|
||||
@using Umbraco.Core
|
||||
@using Umbraco.Web.WebAssets
|
||||
@using Umbraco.Web.BackOffice.Security
|
||||
@using Umbraco.Web.Common.Security
|
||||
@using Umbraco.Core.WebAssets
|
||||
@using Umbraco.Core.Configuration
|
||||
@using Umbraco.Core.Configuration.Models
|
||||
|
||||
Reference in New Issue
Block a user