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:
Bjarke Berg
2020-11-27 13:49:42 +01:00
30 changed files with 319 additions and 581 deletions

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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))

View File

@@ -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]

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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

View File

@@ -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();

View File

@@ -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 =>
{

View File

@@ -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
{

View File

@@ -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>();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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();
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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; }
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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)))
{

View File

@@ -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;

View File

@@ -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

View File

@@ -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; }
}
}

View File

@@ -50,7 +50,7 @@ function externalLoginInfoService(externalLoginInfo, umbRequestHelper) {
return true;
}
else {
return x.properties.AutoLinkOptions.AllowManualLinking;
return x.properties.ExternalSignInAutoLinkOptions.AllowManualLinking;
}
});
return providers;

View File

@@ -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}}">

View File

@@ -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>

View File

@@ -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

View File

@@ -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