606 lines
26 KiB
C#
606 lines
26 KiB
C#
using System.Globalization;
|
|
using System.Net;
|
|
using System.Security.Claims;
|
|
using System.Security.Principal;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Umbraco.Cms.Core;
|
|
using Umbraco.Cms.Core.Cache;
|
|
using Umbraco.Cms.Core.Configuration.Grid;
|
|
using Umbraco.Cms.Core.Configuration.Models;
|
|
using Umbraco.Cms.Core.Hosting;
|
|
using Umbraco.Cms.Core.Manifest;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.Security;
|
|
using Umbraco.Cms.Core.Serialization;
|
|
using Umbraco.Cms.Core.Services;
|
|
using Umbraco.Cms.Core.WebAssets;
|
|
using Umbraco.Cms.Infrastructure.WebAssets;
|
|
using Umbraco.Cms.Web.BackOffice.ActionResults;
|
|
using Umbraco.Cms.Web.BackOffice.Filters;
|
|
using Umbraco.Cms.Web.BackOffice.Install;
|
|
using Umbraco.Cms.Web.BackOffice.Security;
|
|
using Umbraco.Cms.Web.Common.ActionsResults;
|
|
using Umbraco.Cms.Web.Common.Attributes;
|
|
using Umbraco.Cms.Web.Common.Authorization;
|
|
using Umbraco.Cms.Web.Common.Controllers;
|
|
using Umbraco.Cms.Web.Common.Filters;
|
|
using Umbraco.Extensions;
|
|
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
|
|
|
namespace Umbraco.Cms.Web.BackOffice.Controllers;
|
|
|
|
[DisableBrowserCache]
|
|
[UmbracoRequireHttps]
|
|
[PluginController(Constants.Web.Mvc.BackOfficeArea)]
|
|
[IsBackOffice]
|
|
public class BackOfficeController : UmbracoController
|
|
{
|
|
private readonly AppCaches _appCaches;
|
|
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
|
|
private readonly BackOfficeServerVariables _backOfficeServerVariables;
|
|
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
|
|
private readonly IBackOfficeExternalLoginProviders _externalLogins;
|
|
private readonly IGridConfig _gridConfig;
|
|
private readonly IHostingEnvironment _hostingEnvironment;
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
private readonly IJsonSerializer _jsonSerializer;
|
|
private readonly ILogger<BackOfficeController> _logger;
|
|
private readonly IManifestParser _manifestParser;
|
|
private readonly IRuntimeMinifier _runtimeMinifier;
|
|
private readonly IRuntimeState _runtimeState;
|
|
private readonly IOptions<SecuritySettings> _securitySettings;
|
|
private readonly ServerVariablesParser _serverVariables;
|
|
private readonly IBackOfficeSignInManager _signInManager;
|
|
|
|
private readonly ILocalizedTextService _textService;
|
|
// See here for examples of what a lot of this is doing: https://github.com/dotnet/aspnetcore/blob/main/src/Identity/samples/IdentitySample.Mvc/Controllers/AccountController.cs
|
|
// along with our AuthenticationController
|
|
|
|
// NOTE: Each action must either be explicitly authorized or explicitly [AllowAnonymous], the latter is optional because
|
|
// this controller itself doesn't require authz but it's more clear what the intention is.
|
|
|
|
private readonly IBackOfficeUserManager _userManager;
|
|
private readonly GlobalSettings _globalSettings;
|
|
|
|
|
|
[ActivatorUtilitiesConstructor]
|
|
public BackOfficeController(
|
|
IBackOfficeUserManager userManager,
|
|
IRuntimeState runtimeState,
|
|
IRuntimeMinifier runtimeMinifier,
|
|
IOptionsSnapshot<GlobalSettings> globalSettings,
|
|
IHostingEnvironment hostingEnvironment,
|
|
ILocalizedTextService textService,
|
|
IGridConfig gridConfig,
|
|
BackOfficeServerVariables backOfficeServerVariables,
|
|
AppCaches appCaches,
|
|
IBackOfficeSignInManager signInManager,
|
|
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
|
ILogger<BackOfficeController> logger,
|
|
IJsonSerializer jsonSerializer,
|
|
IBackOfficeExternalLoginProviders externalLogins,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions,
|
|
IManifestParser manifestParser,
|
|
ServerVariablesParser serverVariables,
|
|
IOptions<SecuritySettings> securitySettings)
|
|
{
|
|
_userManager = userManager;
|
|
_runtimeState = runtimeState;
|
|
_runtimeMinifier = runtimeMinifier;
|
|
_globalSettings = globalSettings.Value;
|
|
_hostingEnvironment = hostingEnvironment;
|
|
_textService = textService;
|
|
_gridConfig = gridConfig ?? throw new ArgumentNullException(nameof(gridConfig));
|
|
_backOfficeServerVariables = backOfficeServerVariables;
|
|
_appCaches = appCaches;
|
|
_signInManager = signInManager;
|
|
_backofficeSecurityAccessor = backofficeSecurityAccessor;
|
|
_logger = logger;
|
|
_jsonSerializer = jsonSerializer;
|
|
_externalLogins = externalLogins;
|
|
_httpContextAccessor = httpContextAccessor;
|
|
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
|
|
_manifestParser = manifestParser;
|
|
_serverVariables = serverVariables;
|
|
_securitySettings = securitySettings;
|
|
}
|
|
|
|
[HttpGet]
|
|
[AllowAnonymous]
|
|
public async Task<IActionResult> Default()
|
|
{
|
|
// Check if we not are in an run state, if so we need to redirect
|
|
if (_runtimeState.Level != RuntimeLevel.Run)
|
|
{
|
|
if (_runtimeState.Level == RuntimeLevel.Upgrade)
|
|
{
|
|
return RedirectToAction(nameof(AuthorizeUpgrade), routeValues: new RouteValueDictionary()
|
|
{
|
|
["redir"] = _globalSettings.GetBackOfficePath(_hostingEnvironment),
|
|
});
|
|
}
|
|
return Redirect("/");
|
|
}
|
|
|
|
// force authentication to occur since this is not an authorized endpoint
|
|
AuthenticateResult result = await this.AuthenticateBackOfficeAsync();
|
|
|
|
var viewPath = Path.Combine(Constants.SystemDirectories.Umbraco, Constants.Web.Mvc.BackOfficeArea, nameof(Default) + ".cshtml")
|
|
.Replace("\\", "/"); // convert to forward slashes since it's a virtual path
|
|
|
|
return await RenderDefaultOrProcessExternalLoginAsync(
|
|
result,
|
|
() => View(viewPath),
|
|
() => View(viewPath));
|
|
}
|
|
|
|
[HttpGet]
|
|
[AllowAnonymous]
|
|
public async Task<IActionResult> VerifyInvite(string invite)
|
|
{
|
|
AuthenticateResult authenticate = await this.AuthenticateBackOfficeAsync();
|
|
|
|
//if you are hitting VerifyInvite, you're already signed in as a different user, and the token is invalid
|
|
//you'll exit on one of the return RedirectToAction(nameof(Default)) but you're still logged in so you just get
|
|
//dumped at the default admin view with no detail
|
|
if (authenticate.Succeeded)
|
|
{
|
|
await _signInManager.SignOutAsync();
|
|
}
|
|
|
|
if (invite == null)
|
|
{
|
|
_logger.LogWarning("VerifyUser endpoint reached with invalid token: NULL");
|
|
return RedirectToAction(nameof(Default));
|
|
}
|
|
|
|
var parts = WebUtility.UrlDecode(invite).Split('|');
|
|
|
|
if (parts.Length != 2)
|
|
{
|
|
_logger.LogWarning("VerifyUser endpoint reached with invalid token: {Invite}", invite);
|
|
return RedirectToAction(nameof(Default));
|
|
}
|
|
|
|
var token = parts[1];
|
|
|
|
var decoded = token.FromUrlBase64();
|
|
if (decoded.IsNullOrWhiteSpace())
|
|
{
|
|
_logger.LogWarning("VerifyUser endpoint reached with invalid token: {Invite}", invite);
|
|
return RedirectToAction(nameof(Default));
|
|
}
|
|
|
|
var id = parts[0];
|
|
|
|
BackOfficeIdentityUser? identityUser = await _userManager.FindByIdAsync(id);
|
|
if (identityUser == null)
|
|
{
|
|
_logger.LogWarning("VerifyUser endpoint reached with non existing user: {UserId}", id);
|
|
return RedirectToAction(nameof(Default));
|
|
}
|
|
|
|
IdentityResult result = await _userManager.ConfirmEmailAsync(identityUser, decoded!);
|
|
|
|
if (result.Succeeded == false)
|
|
{
|
|
_logger.LogWarning("Could not verify email, Error: {Errors}, Token: {Invite}", result.Errors.ToErrorMessage(), invite);
|
|
return new RedirectResult(Url.Action(nameof(Default)) + "#/login/false?invite=3");
|
|
}
|
|
|
|
//sign the user in
|
|
DateTime? previousLastLoginDate = identityUser.LastLoginDateUtc;
|
|
await _signInManager.SignInAsync(identityUser, false);
|
|
//reset the lastlogindate back to previous as the user hasn't actually logged in, to add a flag or similar to BackOfficeSignInManager would be a breaking change
|
|
identityUser.LastLoginDateUtc = previousLastLoginDate;
|
|
await _userManager.UpdateAsync(identityUser);
|
|
|
|
return new RedirectResult(Url.Action(nameof(Default)) + "#/login/false?invite=1");
|
|
}
|
|
|
|
/// <summary>
|
|
/// This Action is used by the installer when an upgrade is detected but the admin user is not logged in. We need to
|
|
/// ensure the user is authenticated before the install takes place so we redirect here to show the standard login
|
|
/// screen.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[StatusCodeResult(HttpStatusCode.ServiceUnavailable)]
|
|
[AllowAnonymous]
|
|
public async Task<IActionResult> AuthorizeUpgrade()
|
|
{
|
|
// force authentication to occur since this is not an authorized endpoint
|
|
AuthenticateResult result = await this.AuthenticateBackOfficeAsync();
|
|
if (result.Succeeded)
|
|
{
|
|
// Redirect to installer if we're already authorized
|
|
var installerUrl = Url.Action(nameof(InstallController.Index), ControllerExtensions.GetControllerName<InstallController>(), new { area = Cms.Core.Constants.Web.Mvc.InstallArea }) ?? "/";
|
|
return new LocalRedirectResult(installerUrl);
|
|
}
|
|
|
|
var viewPath = Path.Combine(Constants.SystemDirectories.Umbraco, Constants.Web.Mvc.BackOfficeArea, nameof(AuthorizeUpgrade) + ".cshtml");
|
|
|
|
return await RenderDefaultOrProcessExternalLoginAsync(
|
|
result,
|
|
//The default view to render when there is no external login info or errors
|
|
() => View(viewPath),
|
|
//The IActionResult to perform if external login is successful
|
|
() => Redirect("/"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the JavaScript main file including all references found in manifests
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
[MinifyJavaScriptResult(Order = 0)]
|
|
[HttpGet]
|
|
[AllowAnonymous]
|
|
public async Task<IActionResult> Application()
|
|
{
|
|
var result = await _runtimeMinifier.GetScriptForLoadingBackOfficeAsync(
|
|
_globalSettings,
|
|
_hostingEnvironment,
|
|
_manifestParser);
|
|
|
|
return new JavaScriptResult(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the json localized text for a given culture or the culture for the current user
|
|
/// </summary>
|
|
/// <param name="culture"></param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[AllowAnonymous]
|
|
public async Task<Dictionary<string, Dictionary<string, string>>> LocalizedText(string? culture = null)
|
|
{
|
|
CultureInfo? cultureInfo = null;
|
|
if (string.IsNullOrWhiteSpace(culture))
|
|
{
|
|
// Force authentication to occur since this is not an authorized endpoint, we need this to get a user.
|
|
AuthenticateResult authenticationResult = await this.AuthenticateBackOfficeAsync();
|
|
// We have to get the culture from the Identity, we can't rely on thread culture
|
|
// It's entirely likely for a user to have a different culture in the backoffice, than their system.
|
|
IIdentity? user = authenticationResult.Principal?.Identity;
|
|
|
|
if (authenticationResult.Succeeded && user is not null)
|
|
{
|
|
cultureInfo = user.GetCulture();
|
|
}
|
|
|
|
cultureInfo ??= CultureInfo.GetCultureInfo(_globalSettings.DefaultUILanguage);
|
|
}
|
|
else
|
|
{
|
|
cultureInfo = CultureInfo.GetCultureInfo(culture);
|
|
}
|
|
|
|
IDictionary<string, string> allValues = _textService.GetAllStoredValues(cultureInfo!);
|
|
var pathedValues = allValues.Select(kv =>
|
|
{
|
|
var slashIndex = kv.Key.IndexOf('/');
|
|
var areaAlias = kv.Key[..slashIndex];
|
|
var valueAlias = kv.Key[(slashIndex + 1)..];
|
|
return new { areaAlias, valueAlias, value = kv.Value };
|
|
});
|
|
|
|
var nestedDictionary = pathedValues
|
|
.GroupBy(pv => pv.areaAlias)
|
|
.ToDictionary(pv => pv.Key, pv =>
|
|
pv.ToDictionary(pve => pve.valueAlias, pve => pve.value));
|
|
|
|
return nestedDictionary;
|
|
}
|
|
|
|
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
|
|
[AngularJsonOnlyConfiguration]
|
|
[HttpGet]
|
|
public IEnumerable<IGridEditorConfig> GetGridConfig() => _gridConfig.EditorsConfig.Editors;
|
|
|
|
/// <summary>
|
|
/// Returns the JavaScript object representing the static server variables javascript object
|
|
/// </summary>
|
|
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
|
|
[MinifyJavaScriptResult(Order = 1)]
|
|
public async Task<JavaScriptResult> ServerVariables()
|
|
{
|
|
// cache the result if debugging is disabled
|
|
var serverVars = await _serverVariables.ParseAsync(await _backOfficeServerVariables.GetServerVariablesAsync());
|
|
var result = _hostingEnvironment.IsDebugMode
|
|
? serverVars
|
|
: _appCaches.RuntimeCache.GetCacheItem(
|
|
typeof(BackOfficeController) + "ServerVariables",
|
|
() => serverVars,
|
|
new TimeSpan(0, 10, 0));
|
|
|
|
return new JavaScriptResult(result);
|
|
}
|
|
|
|
[HttpPost]
|
|
[AllowAnonymous]
|
|
public ActionResult ExternalLogin(string provider, string? redirectUrl = null)
|
|
{
|
|
if (redirectUrl == null || Uri.TryCreate(redirectUrl, UriKind.Absolute, out _))
|
|
{
|
|
redirectUrl = Url.Action(nameof(Default), this.GetControllerName());
|
|
}
|
|
|
|
// Configures the redirect URL and user identifier for the specified external login
|
|
AuthenticationProperties properties =
|
|
_signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
|
|
|
return Challenge(properties, provider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a user links an external login provider in the back office
|
|
/// </summary>
|
|
/// <param name="provider"></param>
|
|
/// <returns></returns>
|
|
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
|
|
[HttpPost]
|
|
public ActionResult LinkLogin(string provider)
|
|
{
|
|
// Request a redirect to the external login provider to link a login for the current user
|
|
var redirectUrl = Url.Action(nameof(ExternalLinkLoginCallback), this.GetControllerName());
|
|
|
|
// Configures the redirect URL and user identifier for the specified external login including xsrf data
|
|
AuthenticationProperties properties =
|
|
_signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
|
|
|
|
return Challenge(properties, provider);
|
|
}
|
|
|
|
[HttpGet]
|
|
[AllowAnonymous]
|
|
public async Task<IActionResult> ValidatePasswordResetCode([Bind(Prefix = "u")] int userId, [Bind(Prefix = "r")] string resetCode)
|
|
{
|
|
BackOfficeIdentityUser? user = await _userManager.FindByIdAsync(userId.ToString(CultureInfo.InvariantCulture));
|
|
if (user != null)
|
|
{
|
|
var result = await _userManager.VerifyUserTokenAsync(user, "Default", "ResetPassword", resetCode);
|
|
if (result)
|
|
{
|
|
//Add a flag and redirect for it to be displayed
|
|
TempData[ViewDataExtensions.TokenPasswordResetCode] =
|
|
_jsonSerializer.Serialize(
|
|
new ValidatePasswordResetCodeModel { UserId = userId, ResetCode = resetCode });
|
|
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
|
|
}
|
|
}
|
|
|
|
//Add error and redirect for it to be displayed
|
|
TempData[ViewDataExtensions.TokenPasswordResetCode] =
|
|
new[] { _textService.Localize("login", "resetCodeExpired") };
|
|
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback path when the user initiates a link login request from the back office to the external provider from the
|
|
/// <see cref="LinkLogin(string)" /> action
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// An example of this is here
|
|
/// https://github.com/dotnet/aspnetcore/blob/main/src/Identity/samples/IdentitySample.Mvc/Controllers/AccountController.cs#L155
|
|
/// which this is based on
|
|
/// </remarks>
|
|
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
|
|
[HttpGet]
|
|
public async Task<IActionResult> ExternalLinkLoginCallback()
|
|
{
|
|
BackOfficeIdentityUser? user = await _userManager.GetUserAsync(User);
|
|
if (user == null)
|
|
{
|
|
// ... this should really not happen
|
|
TempData[ViewDataExtensions.TokenExternalSignInError] = new[] { "Local user does not exist" };
|
|
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
|
|
}
|
|
|
|
ExternalLoginInfo? info =
|
|
await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user));
|
|
|
|
if (info == null)
|
|
{
|
|
//Add error and redirect for it to be displayed
|
|
TempData[ViewDataExtensions.TokenExternalSignInError] =
|
|
new[] { "An error occurred, could not get external login info" };
|
|
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
|
|
}
|
|
|
|
IdentityResult addLoginResult = await _userManager.AddLoginAsync(user, info);
|
|
if (addLoginResult.Succeeded)
|
|
{
|
|
// Update any authentication tokens if succeeded
|
|
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
|
|
|
|
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
|
|
}
|
|
|
|
//Add errors and redirect for it to be displayed
|
|
TempData[ViewDataExtensions.TokenExternalSignInError] = addLoginResult.Errors;
|
|
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info,
|
|
/// otherwise process the external login info.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private async Task<IActionResult> RenderDefaultOrProcessExternalLoginAsync(
|
|
AuthenticateResult authenticateResult,
|
|
Func<IActionResult> defaultResponse,
|
|
Func<IActionResult> externalSignInResponse)
|
|
{
|
|
if (defaultResponse is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(defaultResponse));
|
|
}
|
|
|
|
if (externalSignInResponse is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(externalSignInResponse));
|
|
}
|
|
|
|
ViewData.SetUmbracoPath(_globalSettings.GetUmbracoMvcArea(_hostingEnvironment));
|
|
|
|
//check if there is the TempData or cookies with the any token name specified, if so, assign to view bag and render the view
|
|
if (ViewData.FromBase64CookieData<BackOfficeExternalLoginProviderErrors>(
|
|
_httpContextAccessor.HttpContext,
|
|
ViewDataExtensions.TokenExternalSignInError,
|
|
_jsonSerializer) ||
|
|
ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) || ViewData.FromTempData(TempData, ViewDataExtensions.TokenPasswordResetCode))
|
|
{
|
|
return defaultResponse();
|
|
}
|
|
|
|
//First check if there's external login info, if there's not proceed as normal
|
|
ExternalLoginInfo? loginInfo = await _signInManager.GetExternalLoginInfoAsync();
|
|
|
|
if (loginInfo == null || loginInfo.Principal == null)
|
|
{
|
|
// if the user is not logged in, check if there's any auto login redirects specified
|
|
if (!authenticateResult.Succeeded)
|
|
{
|
|
var oauthRedirectAuthProvider = _externalLogins.GetAutoLoginProvider();
|
|
if (!oauthRedirectAuthProvider.IsNullOrWhiteSpace())
|
|
{
|
|
return ExternalLogin(oauthRedirectAuthProvider!);
|
|
}
|
|
}
|
|
|
|
return defaultResponse();
|
|
}
|
|
|
|
//we're just logging in with an external source, not linking accounts
|
|
return await ExternalSignInAsync(loginInfo, externalSignInResponse);
|
|
}
|
|
|
|
private async Task<IActionResult> ExternalSignInAsync(ExternalLoginInfo loginInfo, Func<IActionResult> response)
|
|
{
|
|
if (loginInfo == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(loginInfo));
|
|
}
|
|
|
|
if (response == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(response));
|
|
}
|
|
|
|
// Sign in the user with this external login provider (which auto links, etc...)
|
|
SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, false, _securitySettings.Value.UserBypassTwoFactorForExternalLogins);
|
|
|
|
var errors = new List<string>();
|
|
|
|
if (result == SignInResult.Success)
|
|
{
|
|
// Update any authentication tokens if succeeded
|
|
await _signInManager.UpdateExternalAuthenticationTokensAsync(loginInfo);
|
|
|
|
// Check if we are in an upgrade state, if so we need to redirect
|
|
if (_runtimeState.Level == RuntimeLevel.Upgrade)
|
|
{
|
|
// redirect to the the installer
|
|
return Redirect("/");
|
|
}
|
|
}
|
|
else if (result == SignInResult.TwoFactorRequired)
|
|
{
|
|
BackOfficeIdentityUser? attemptedUser = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
|
|
if (attemptedUser?.UserName is null)
|
|
{
|
|
return new ValidationErrorResult(
|
|
$"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}");
|
|
}
|
|
|
|
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(attemptedUser.UserName);
|
|
if (twofactorView.IsNullOrWhiteSpace())
|
|
{
|
|
return new ValidationErrorResult(
|
|
$"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
|
|
}
|
|
|
|
// create a with information to display a custom two factor send code view
|
|
var verifyResponse =
|
|
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser.Id })
|
|
{
|
|
StatusCode = StatusCodes.Status402PaymentRequired
|
|
};
|
|
|
|
return verifyResponse;
|
|
}
|
|
else if (result == SignInResult.LockedOut)
|
|
{
|
|
errors.Add(
|
|
$"The local user {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out.");
|
|
}
|
|
else if (result == SignInResult.NotAllowed)
|
|
{
|
|
// This occurs when SignInManager.CanSignInAsync fails which is when RequireConfirmedEmail , RequireConfirmedPhoneNumber or RequireConfirmedAccount fails
|
|
// however since we don't enforce those rules (yet) this shouldn't happen.
|
|
errors.Add(
|
|
$"The user {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} has not confirmed their details and cannot sign in.");
|
|
}
|
|
else if (result == 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 == ExternalLoginSignInResult.NotAllowed)
|
|
{
|
|
// This occurs when the external provider has approved the login but custom logic in OnExternalLogin has denined it.
|
|
errors.Add(
|
|
$"The user {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} has not been accepted and cannot sign in.");
|
|
}
|
|
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);
|
|
}
|
|
else if (!result.Succeeded)
|
|
{
|
|
// this shouldn't occur, the above should catch the correct error but we'll be safe just in case
|
|
errors.Add($"An unknown error with the requested provider ({loginInfo.LoginProvider}) occurred.");
|
|
}
|
|
|
|
if (errors.Count > 0)
|
|
{
|
|
ViewData.SetExternalSignInProviderErrors(
|
|
new BackOfficeExternalLoginProviderErrors(
|
|
loginInfo.LoginProvider,
|
|
errors));
|
|
}
|
|
|
|
return response();
|
|
}
|
|
|
|
private IActionResult RedirectToLocal(string? returnUrl)
|
|
{
|
|
if (Url.IsLocalUrl(returnUrl))
|
|
{
|
|
return Redirect(returnUrl);
|
|
}
|
|
|
|
return Redirect("/");
|
|
}
|
|
}
|