2020-12-04 02:21:21 +11:00
using System ;
2020-05-25 19:37:16 +10:00
using System.Collections.Generic ;
2020-05-11 17:05:57 +02:00
using System.Globalization ;
2020-07-08 11:18:23 +02:00
using System.IO ;
2020-05-11 17:05:57 +02:00
using System.Linq ;
2020-12-04 02:21:21 +11:00
using System.Security.Claims ;
2020-05-11 17:05:57 +02:00
using System.Threading.Tasks ;
2020-12-04 02:21:21 +11:00
using Microsoft.AspNetCore.Authentication ;
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Identity ;
2020-03-30 21:27:35 +02:00
using Microsoft.AspNetCore.Mvc ;
2020-09-21 09:27:54 +02:00
using Microsoft.Extensions.Logging ;
2020-12-04 02:21:21 +11:00
using Microsoft.Extensions.Options ;
2021-09-09 15:37:13 +02:00
using Umbraco.Cms.Core ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Cache ;
using Umbraco.Cms.Core.Configuration.Grid ;
using Umbraco.Cms.Core.Configuration.Models ;
using Umbraco.Cms.Core.Hosting ;
2021-07-15 13:26:32 -06:00
using Umbraco.Cms.Core.Manifest ;
2021-02-09 10:22:42 +01:00
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 ;
2021-02-15 13:07:12 +01:00
using Umbraco.Cms.Infrastructure.WebAssets ;
2021-02-10 11:11:18 +01:00
using Umbraco.Cms.Web.BackOffice.ActionResults ;
using Umbraco.Cms.Web.BackOffice.Filters ;
using Umbraco.Cms.Web.BackOffice.Security ;
2021-02-10 11:42:04 +01:00
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 ;
2020-05-20 15:25:42 +10:00
using Umbraco.Extensions ;
2021-02-09 10:22:42 +01:00
using Constants = Umbraco . Cms . Core . Constants ;
2021-07-21 14:32:55 -06:00
using SignInResult = Microsoft . AspNetCore . Identity . SignInResult ;
2020-03-30 21:27:35 +02:00
2021-02-10 11:11:18 +01:00
namespace Umbraco.Cms.Web.BackOffice.Controllers
2020-03-30 21:27:35 +02:00
{
2020-11-27 13:33:01 +01:00
[DisableBrowserCache]
2020-12-02 14:28:16 +11:00
[UmbracoRequireHttps]
2020-05-27 18:27:49 +10:00
[PluginController(Constants.Web.Mvc.BackOfficeArea)]
2020-11-27 13:35:22 +01:00
[IsBackOffice]
2020-10-15 11:42:16 +02:00
public class BackOfficeController : UmbracoController
2020-03-30 21:27:35 +02:00
{
2021-03-11 19:35:43 +11:00
// 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
2020-12-02 14:28:16 +11:00
// 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.
2020-09-22 14:44:41 +02:00
private readonly IBackOfficeUserManager _userManager ;
2021-07-26 13:28:18 -06:00
private readonly IRuntimeState _runtimeState ;
2020-03-30 21:27:35 +02:00
private readonly IRuntimeMinifier _runtimeMinifier ;
2020-08-21 14:52:47 +01:00
private readonly GlobalSettings _globalSettings ;
2020-04-03 11:03:06 +11:00
private readonly IHostingEnvironment _hostingEnvironment ;
2020-05-11 17:05:57 +02:00
private readonly ILocalizedTextService _textService ;
private readonly IGridConfig _gridConfig ;
2020-05-20 16:43:06 +10:00
private readonly BackOfficeServerVariables _backOfficeServerVariables ;
private readonly AppCaches _appCaches ;
2020-11-30 22:46:05 +11:00
private readonly IBackOfficeSignInManager _signInManager ;
2020-10-21 16:51:00 +11:00
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor ;
2020-09-21 09:27:54 +02:00
private readonly ILogger < BackOfficeController > _logger ;
2020-08-31 13:39:29 +02:00
private readonly IJsonSerializer _jsonSerializer ;
2020-10-23 14:18:53 +11:00
private readonly IBackOfficeExternalLoginProviders _externalLogins ;
2020-10-23 10:36:47 +02:00
private readonly IHttpContextAccessor _httpContextAccessor ;
2020-12-02 12:22:08 +11:00
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions ;
2021-07-15 13:26:32 -06:00
private readonly IManifestParser _manifestParser ;
2021-01-13 12:48:41 +11:00
private readonly ServerVariablesParser _serverVariables ;
2020-03-30 21:27:35 +02:00
2020-05-20 15:25:42 +10:00
public BackOfficeController (
2020-09-22 14:44:41 +02:00
IBackOfficeUserManager userManager ,
2021-07-26 13:28:18 -06:00
IRuntimeState runtimeState ,
2020-05-20 15:25:42 +10:00
IRuntimeMinifier runtimeMinifier ,
2020-08-23 23:36:48 +02:00
IOptions < GlobalSettings > globalSettings ,
2020-05-20 15:25:42 +10:00
IHostingEnvironment hostingEnvironment ,
ILocalizedTextService textService ,
2020-05-20 16:43:06 +10:00
IGridConfig gridConfig ,
BackOfficeServerVariables backOfficeServerVariables ,
2020-05-21 15:43:33 +10:00
AppCaches appCaches ,
2020-11-30 22:46:05 +11:00
IBackOfficeSignInManager signInManager ,
2020-10-21 16:51:00 +11:00
IBackOfficeSecurityAccessor backofficeSecurityAccessor ,
2020-09-23 13:32:49 +02:00
ILogger < BackOfficeController > logger ,
2020-10-21 16:51:00 +11:00
IJsonSerializer jsonSerializer ,
2020-10-23 10:36:47 +02:00
IBackOfficeExternalLoginProviders externalLogins ,
2020-12-02 12:22:08 +11:00
IHttpContextAccessor httpContextAccessor ,
2021-01-13 12:48:41 +11:00
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions ,
2021-07-15 13:26:32 -06:00
IManifestParser manifestParser ,
2021-01-13 12:48:41 +11:00
ServerVariablesParser serverVariables )
2020-03-30 21:27:35 +02:00
{
2020-05-20 15:25:42 +10:00
_userManager = userManager ;
2021-07-26 13:28:18 -06:00
_runtimeState = runtimeState ;
2020-03-30 21:27:35 +02:00
_runtimeMinifier = runtimeMinifier ;
2020-08-21 14:52:47 +01:00
_globalSettings = globalSettings . Value ;
2020-04-03 11:03:06 +11:00
_hostingEnvironment = hostingEnvironment ;
2020-05-11 17:05:57 +02:00
_textService = textService ;
_gridConfig = gridConfig ? ? throw new ArgumentNullException ( nameof ( gridConfig ) ) ;
2020-05-20 16:43:06 +10:00
_backOfficeServerVariables = backOfficeServerVariables ;
_appCaches = appCaches ;
2020-05-21 15:43:33 +10:00
_signInManager = signInManager ;
2020-09-22 10:01:00 +02:00
_backofficeSecurityAccessor = backofficeSecurityAccessor ;
2020-08-04 14:30:42 +10:00
_logger = logger ;
2020-08-31 13:39:29 +02:00
_jsonSerializer = jsonSerializer ;
2020-10-23 14:18:53 +11:00
_externalLogins = externalLogins ;
2020-10-23 10:36:47 +02:00
_httpContextAccessor = httpContextAccessor ;
2020-12-02 12:22:08 +11:00
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions ;
2021-07-15 13:26:32 -06:00
_manifestParser = manifestParser ;
2021-01-13 12:48:41 +11:00
_serverVariables = serverVariables ;
2020-03-30 21:27:35 +02:00
}
2020-05-13 14:49:00 +10:00
[HttpGet]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-05-21 15:43:33 +10:00
public async Task < IActionResult > Default ( )
2020-03-30 21:27:35 +02:00
{
2021-09-09 15:37:13 +02:00
// Check if we not are in an run state, if so we need to redirect
if ( _runtimeState . Level ! = RuntimeLevel . Run )
{
return Redirect ( "/" ) ;
}
2021-07-26 16:55:41 -06:00
2020-12-02 14:28:16 +11:00
// force authentication to occur since this is not an authorized endpoint
2021-09-10 09:24:11 +02:00
AuthenticateResult result = await this . AuthenticateBackOfficeAsync ( ) ;
2021-09-09 15:37:13 +02:00
2021-09-01 07:50:17 +02:00
var viewPath = Path . Combine ( Constants . SystemDirectories . Umbraco , Constants . Web . Mvc . BackOfficeArea , nameof ( Default ) + ".cshtml" )
2020-08-07 00:48:32 +10:00
. Replace ( "\\" , "/" ) ; // convert to forward slashes since it's a virtual path
2020-09-16 09:58:07 +02:00
2020-05-21 15:43:33 +10:00
return await RenderDefaultOrProcessExternalLoginAsync (
2020-12-02 14:28:16 +11:00
result ,
2020-07-08 11:18:23 +02:00
( ) = > View ( viewPath ) ,
( ) = > View ( viewPath ) ) ;
2020-03-30 21:27:35 +02:00
}
2020-08-04 14:30:42 +10:00
[HttpGet]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-08-04 14:30:42 +10:00
public async Task < IActionResult > VerifyInvite ( string invite )
{
2020-12-02 15:49:28 +11:00
var authenticate = await this . AuthenticateBackOfficeAsync ( ) ;
2020-12-02 14:28:16 +11:00
2020-08-04 14:30:42 +10:00
//if you are hitting VerifyInvite, you're already signed in as a different user, and the token is invalid
2020-08-04 20:29:48 +02:00
//you'll exit on one of the return RedirectToAction(nameof(Default)) but you're still logged in so you just get
2020-08-04 14:30:42 +10:00
//dumped at the default admin view with no detail
2020-12-02 14:28:16 +11:00
if ( authenticate . Succeeded )
2020-08-04 14:30:42 +10:00
{
await _signInManager . SignOutAsync ( ) ;
}
if ( invite = = null )
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "VerifyUser endpoint reached with invalid token: NULL" ) ;
2020-08-04 14:30:42 +10:00
return RedirectToAction ( nameof ( Default ) ) ;
}
var parts = System . Net . WebUtility . UrlDecode ( invite ) . Split ( '|' ) ;
if ( parts . Length ! = 2 )
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "VerifyUser endpoint reached with invalid token: {Invite}" , invite ) ;
2020-08-04 14:30:42 +10:00
return RedirectToAction ( nameof ( Default ) ) ;
}
var token = parts [ 1 ] ;
var decoded = token . FromUrlBase64 ( ) ;
if ( decoded . IsNullOrWhiteSpace ( ) )
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "VerifyUser endpoint reached with invalid token: {Invite}" , invite ) ;
2020-08-04 14:30:42 +10:00
return RedirectToAction ( nameof ( Default ) ) ;
}
var id = parts [ 0 ] ;
var identityUser = await _userManager . FindByIdAsync ( id ) ;
if ( identityUser = = null )
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "VerifyUser endpoint reached with non existing user: {UserId}" , id ) ;
2020-08-04 14:30:42 +10:00
return RedirectToAction ( nameof ( Default ) ) ;
}
var result = await _userManager . ConfirmEmailAsync ( identityUser , decoded ) ;
if ( result . Succeeded = = false )
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "Could not verify email, Error: {Errors}, Token: {Invite}" , result . Errors . ToErrorMessage ( ) , invite ) ;
2020-08-04 14:30:42 +10:00
return new RedirectResult ( Url . Action ( nameof ( Default ) ) + "#/login/false?invite=3" ) ;
}
//sign the user in
DateTime ? previousLastLoginDate = identityUser . LastLoginDateUtc ;
await _signInManager . SignInAsync ( identityUser , false ) ;
2020-08-04 20:29:48 +02:00
//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
2020-08-04 14:30:42 +10:00
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(System.Net.HttpStatusCode.ServiceUnavailable)]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-08-04 14:30:42 +10:00
public async Task < IActionResult > AuthorizeUpgrade ( )
{
2020-12-02 14:28:16 +11:00
// force authentication to occur since this is not an authorized endpoint
2020-12-02 15:49:28 +11:00
var result = await this . AuthenticateBackOfficeAsync ( ) ;
2020-12-02 14:28:16 +11:00
2021-09-01 07:50:17 +02:00
var viewPath = Path . Combine ( Constants . SystemDirectories . Umbraco , Constants . Web . Mvc . BackOfficeArea , nameof ( AuthorizeUpgrade ) + ".cshtml" ) ;
2020-12-02 14:28:16 +11:00
2020-08-04 14:30:42 +10:00
return await RenderDefaultOrProcessExternalLoginAsync (
2020-12-02 14:28:16 +11:00
result ,
2020-08-04 14:30:42 +10:00
//The default view to render when there is no external login info or errors
( ) = > View ( viewPath ) ,
2020-08-04 20:29:48 +02:00
//The IActionResult to perform if external login is successful
2020-08-04 14:30:42 +10:00
( ) = > Redirect ( "/" ) ) ;
}
2020-03-30 21:27:35 +02:00
/// <summary>
/// Returns the JavaScript main file including all references found in manifests
/// </summary>
/// <returns></returns>
[MinifyJavaScriptResult(Order = 0)]
2020-05-13 14:49:00 +10:00
[HttpGet]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-03-30 21:27:35 +02:00
public async Task < IActionResult > Application ( )
{
2021-07-15 13:26:32 -06:00
var result = await _runtimeMinifier . GetScriptForLoadingBackOfficeAsync (
_globalSettings ,
_hostingEnvironment ,
2021-07-15 14:06:33 -06:00
_manifestParser ) ;
2020-03-30 21:27:35 +02:00
return new JavaScriptResult ( result ) ;
}
2020-05-11 17:05:57 +02:00
/// <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]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2021-07-15 14:10:52 +02:00
public async Task < Dictionary < string , Dictionary < string , string > > > LocalizedText ( string culture = null )
2020-05-11 17:05:57 +02:00
{
2021-07-15 14:10:52 +02:00
CultureInfo cultureInfo ;
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.
var user = authenticationResult . Principal ? . Identity ;
2020-05-11 17:05:57 +02:00
2021-07-15 14:10:52 +02:00
cultureInfo = ( authenticationResult . Succeeded & & user is not null )
? user . GetCulture ( )
: CultureInfo . GetCultureInfo ( _globalSettings . DefaultUILanguage ) ;
}
else
{
cultureInfo = CultureInfo . GetCultureInfo ( culture ) ;
}
2020-05-11 17:05:57 +02:00
var allValues = _textService . GetAllStoredValues ( cultureInfo ) ;
var pathedValues = allValues . Select ( kv = >
{
var slashIndex = kv . Key . IndexOf ( '/' ) ;
var areaAlias = kv . Key . Substring ( 0 , slashIndex ) ;
var valueAlias = kv . Key . Substring ( 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 ) ) ;
2020-05-25 19:37:16 +10:00
return nestedDictionary ;
2020-05-11 17:05:57 +02:00
}
2020-11-20 15:32:36 +11:00
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
2021-08-31 14:34:59 +02:00
[AngularJsonOnlyConfiguration]
2020-05-11 17:05:57 +02:00
[HttpGet]
2020-05-25 19:37:16 +10:00
public IEnumerable < IGridEditorConfig > GetGridConfig ( )
2020-05-11 17:05:57 +02:00
{
2020-05-25 19:37:16 +10:00
return _gridConfig . EditorsConfig . Editors ;
2020-05-11 17:05:57 +02:00
}
2020-05-20 15:25:42 +10:00
2020-05-20 16:43:06 +10:00
/// <summary>
/// Returns the JavaScript object representing the static server variables javascript object
/// </summary>
2020-11-20 15:32:36 +11:00
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
2020-05-20 16:43:06 +10:00
[MinifyJavaScriptResult(Order = 1)]
public async Task < JavaScriptResult > ServerVariables ( )
{
2021-01-13 12:48:41 +11:00
// cache the result if debugging is disabled
var serverVars = await _serverVariables . ParseAsync ( await _backOfficeServerVariables . GetServerVariablesAsync ( ) ) ;
2020-05-20 16:43:06 +10:00
var result = _hostingEnvironment . IsDebugMode
? serverVars
: _appCaches . RuntimeCache . GetCacheItem < string > (
typeof ( BackOfficeController ) + "ServerVariables" ,
( ) = > serverVars ,
new TimeSpan ( 0 , 10 , 0 ) ) ;
return new JavaScriptResult ( result ) ;
}
2020-10-21 16:51:00 +11:00
[HttpPost]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-10-21 16:51:00 +11:00
public ActionResult ExternalLogin ( string provider , string redirectUrl = null )
{
if ( redirectUrl = = null )
{
2020-10-22 13:37:47 +02:00
redirectUrl = Url . Action ( nameof ( Default ) , this . GetControllerName ( ) ) ;
2020-10-21 16:51:00 +11:00
}
2020-11-30 22:23:10 +11:00
// Configures the redirect URL and user identifier for the specified external login
2020-10-21 16:51:00 +11:00
var properties = _signInManager . ConfigureExternalAuthenticationProperties ( provider , redirectUrl ) ;
2021-02-09 10:22:42 +01:00
2020-10-21 16:51:00 +11:00
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>
2020-11-20 15:32:36 +11:00
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
2020-10-21 16:51:00 +11:00
[HttpPost]
public ActionResult LinkLogin ( string provider )
{
// Request a redirect to the external login provider to link a login for the current user
2020-10-22 13:37:47 +02:00
var redirectUrl = Url . Action ( nameof ( ExternalLinkLoginCallback ) , this . GetControllerName ( ) ) ;
2020-11-30 22:23:10 +11:00
// Configures the redirect URL and user identifier for the specified external login including xsrf data
var properties = _signInManager . ConfigureExternalAuthenticationProperties ( provider , redirectUrl , _userManager . GetUserId ( User ) ) ;
2021-02-09 10:22:42 +01:00
2020-10-21 16:51:00 +11:00
return Challenge ( properties , provider ) ;
}
2020-05-20 15:25:42 +10:00
[HttpGet]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-05-25 19:37:16 +10:00
public async Task < IActionResult > ValidatePasswordResetCode ( [ Bind ( Prefix = "u" ) ] int userId , [ Bind ( Prefix = "r" ) ] string resetCode )
2020-05-20 15:25:42 +10:00
{
2021-09-20 11:30:09 +02:00
var user = await _userManager . FindByIdAsync ( userId . ToString ( CultureInfo . InvariantCulture ) ) ;
2020-05-20 15:25:42 +10:00
if ( user ! = null )
{
2020-08-31 13:39:29 +02:00
var result = await _userManager . VerifyUserTokenAsync ( user , "Default" , "ResetPassword" , resetCode ) ;
2020-05-20 15:25:42 +10:00
if ( result )
{
//Add a flag and redirect for it to be displayed
2020-08-31 13:39:29 +02:00
TempData [ ViewDataExtensions . TokenPasswordResetCode ] = _jsonSerializer . Serialize ( new ValidatePasswordResetCodeModel { UserId = userId , ResetCode = resetCode } ) ;
2020-10-22 13:37:47 +02:00
return RedirectToLocal ( Url . Action ( nameof ( Default ) , this . GetControllerName ( ) ) ) ;
2020-05-20 15:25:42 +10:00
}
}
//Add error and redirect for it to be displayed
2021-07-05 20:58:04 +02:00
TempData [ ViewDataExtensions . TokenPasswordResetCode ] = new [ ] { _textService . Localize ( "login" , "resetCodeExpired" ) } ;
2020-10-22 13:37:47 +02:00
return RedirectToLocal ( Url . Action ( nameof ( Default ) , this . GetControllerName ( ) ) ) ;
2020-05-20 15:25:42 +10:00
}
2020-10-21 16:51:00 +11:00
/// <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>
2021-03-11 19:35:43 +11:00
/// <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>
2020-11-20 15:32:36 +11:00
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
2020-10-21 16:51:00 +11:00
[HttpGet]
public async Task < IActionResult > ExternalLinkLoginCallback ( )
{
2021-03-11 19:35:43 +11:00
BackOfficeIdentityUser user = await _userManager . GetUserAsync ( User ) ;
2020-11-30 22:23:10 +11:00
if ( user = = null )
2020-10-21 16:51:00 +11:00
{
2020-11-30 22:23:10 +11:00
// ... this should really not happen
TempData [ ViewDataExtensions . TokenExternalSignInError ] = new [ ] { "Local user does not exist" } ;
2020-10-22 13:37:47 +02:00
return RedirectToLocal ( Url . Action ( nameof ( Default ) , this . GetControllerName ( ) ) ) ;
2020-10-21 16:51:00 +11:00
}
2021-03-11 19:35:43 +11:00
ExternalLoginInfo info = await _signInManager . GetExternalLoginInfoAsync ( await _userManager . GetUserIdAsync ( user ) ) ;
2020-11-30 22:23:10 +11:00
2021-03-11 19:35:43 +11:00
if ( info = = null )
2020-10-21 16:51:00 +11:00
{
2020-11-30 22:23:10 +11:00
//Add error and redirect for it to be displayed
TempData [ ViewDataExtensions . TokenExternalSignInError ] = new [ ] { "An error occurred, could not get external login info" } ;
2020-10-22 13:37:47 +02:00
return RedirectToLocal ( Url . Action ( nameof ( Default ) , this . GetControllerName ( ) ) ) ;
2020-10-21 16:51:00 +11:00
}
2021-03-11 19:35:43 +11:00
IdentityResult addLoginResult = await _userManager . AddLoginAsync ( user , info ) ;
2020-10-23 14:18:53 +11:00
if ( addLoginResult . Succeeded )
2020-10-21 16:51:00 +11:00
{
2021-03-11 19:35:43 +11:00
// Update any authentication tokens if succeeded
await _signInManager . UpdateExternalAuthenticationTokensAsync ( info ) ;
2020-10-21 16:51:00 +11:00
2020-10-22 13:37:47 +02:00
return RedirectToLocal ( Url . Action ( nameof ( Default ) , this . GetControllerName ( ) ) ) ;
2020-10-21 16:51:00 +11:00
}
//Add errors and redirect for it to be displayed
2020-10-23 14:18:53 +11:00
TempData [ ViewDataExtensions . TokenExternalSignInError ] = addLoginResult . Errors ;
2020-10-22 13:37:47 +02:00
return RedirectToLocal ( Url . Action ( nameof ( Default ) , this . GetControllerName ( ) ) ) ;
2020-10-21 16:51:00 +11:00
}
2020-05-21 15:43:33 +10:00
/// <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>
2020-10-21 16:51:00 +11:00
private async Task < IActionResult > RenderDefaultOrProcessExternalLoginAsync (
2020-12-02 14:28:16 +11:00
AuthenticateResult authenticateResult ,
2020-05-25 19:37:16 +10:00
Func < IActionResult > defaultResponse ,
Func < IActionResult > externalSignInResponse )
2020-05-21 15:43:33 +10:00
{
if ( defaultResponse is null ) throw new ArgumentNullException ( nameof ( defaultResponse ) ) ;
if ( externalSignInResponse is null ) throw new ArgumentNullException ( nameof ( externalSignInResponse ) ) ;
ViewData . SetUmbracoPath ( _globalSettings . GetUmbracoMvcArea ( _hostingEnvironment ) ) ;
2020-10-23 14:18:53 +11:00
//check if there is the TempData or cookies with the any token name specified, if so, assign to view bag and render the view
2020-10-23 10:36:47 +02:00
if ( ViewData . FromBase64CookieData < BackOfficeExternalLoginProviderErrors > ( _httpContextAccessor . HttpContext , ViewDataExtensions . TokenExternalSignInError , _jsonSerializer ) | |
2020-10-23 14:18:53 +11:00
ViewData . FromTempData ( TempData , ViewDataExtensions . TokenExternalSignInError ) | |
2020-05-21 15:43:33 +10:00
ViewData . FromTempData ( TempData , ViewDataExtensions . TokenPasswordResetCode ) )
2021-07-26 13:12:29 -06:00
{
2020-10-21 16:51:00 +11:00
return defaultResponse ( ) ;
2021-07-26 13:12:29 -06:00
}
2020-05-21 15:43:33 +10:00
//First check if there's external login info, if there's not proceed as normal
2020-10-21 16:51:00 +11:00
var loginInfo = await _signInManager . GetExternalLoginInfoAsync ( ) ;
2020-10-23 14:18:53 +11:00
2020-10-21 16:51:00 +11:00
if ( loginInfo = = null | | loginInfo . Principal = = null )
{
2020-10-23 14:18:53 +11:00
// if the user is not logged in, check if there's any auto login redirects specified
2020-12-02 14:28:16 +11:00
if ( ! authenticateResult . Succeeded )
2020-10-23 14:18:53 +11:00
{
var oauthRedirectAuthProvider = _externalLogins . GetAutoLoginProvider ( ) ;
if ( ! oauthRedirectAuthProvider . IsNullOrWhiteSpace ( ) )
{
return ExternalLogin ( oauthRedirectAuthProvider ) ;
}
}
2020-10-21 16:51:00 +11:00
return defaultResponse ( ) ;
}
//we're just logging in with an external source, not linking accounts
return await ExternalSignInAsync ( loginInfo , externalSignInResponse ) ;
}
2020-05-21 15:43:33 +10:00
2020-10-21 16:51:00 +11:00
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 ) ) ;
2020-05-21 15:43:33 +10:00
2020-11-27 13:34:58 +01:00
// Sign in the user with this external login provider (which auto links, etc...)
2021-07-21 14:32:55 -06:00
SignInResult result = await _signInManager . ExternalLoginSignInAsync ( loginInfo , isPersistent : false ) ;
2021-07-05 20:58:04 +02:00
2020-11-27 13:34:58 +01:00
var errors = new List < string > ( ) ;
2021-07-26 13:12:29 -06:00
if ( result = = SignInResult . Success )
2020-10-21 16:51:00 +11:00
{
2021-03-11 19:35:43 +11:00
// Update any authentication tokens if succeeded
await _signInManager . UpdateExternalAuthenticationTokensAsync ( loginInfo ) ;
2021-07-26 13:28:18 -06:00
// Check if we are in an upgrade state, if so we need to redirect
if ( _runtimeState . Level = = Core . RuntimeLevel . Upgrade )
{
2021-07-26 16:55:41 -06:00
// redirect to the the installer
return Redirect ( "/" ) ;
2021-07-26 13:28:18 -06:00
}
2020-12-04 12:44:27 +11:00
}
2021-07-26 13:12:29 -06:00
else if ( result = = SignInResult . TwoFactorRequired )
2020-10-21 16:51:00 +11:00
{
2020-12-02 12:22:08 +11:00
var attemptedUser = await _userManager . FindByLoginAsync ( loginInfo . LoginProvider , loginInfo . ProviderKey ) ;
if ( attemptedUser = = 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 ;
2020-10-21 16:51:00 +11:00
}
2021-07-21 14:32:55 -06:00
else if ( result = = SignInResult . LockedOut )
2020-10-21 16:51:00 +11:00
{
2020-12-02 12:22:08 +11:00
errors . Add ( $"The local user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out." ) ;
2020-10-21 16:51:00 +11:00
}
2021-07-21 14:32:55 -06:00
else if ( result = = SignInResult . NotAllowed )
2020-10-21 16:51:00 +11:00
{
2020-12-02 12:22:08 +11:00
// 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." ) ;
2020-10-21 16:51:00 +11:00
}
2021-07-21 14:32:55 -06:00
else if ( result = = SignInResult . Failed )
2020-11-27 13:33:01 +01:00
{
2020-11-27 13:34:58 +01:00
// 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." ) ;
2020-10-21 16:51:00 +11:00
}
2021-11-15 11:17:47 +01:00
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." ) ;
}
2020-11-27 13:34:58 +01:00
else if ( result = = AutoLinkSignInResult . FailedNotLinked )
2020-10-21 16:51:00 +11:00
{
2020-11-27 13:34:58 +01:00
errors . Add ( "The requested provider (" + loginInfo . LoginProvider + ") has not been linked to an account, the provider must be linked from the back office." ) ;
2020-10-21 16:51:00 +11:00
}
2020-11-27 13:34:58 +01:00
else if ( result = = AutoLinkSignInResult . FailedNoEmail )
2020-10-23 14:18:53 +11:00
{
2021-02-09 10:22:42 +01:00
errors . Add ( $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked." ) ;
2020-10-23 14:18:53 +11:00
}
2020-11-27 13:34:58 +01:00
else if ( result is AutoLinkSignInResult autoLinkSignInResult & & autoLinkSignInResult . Errors . Count > 0 )
2020-10-23 14:18:53 +11:00
{
2020-11-27 13:34:58 +01:00
errors . AddRange ( autoLinkSignInResult . Errors ) ;
2020-10-23 14:18:53 +11:00
}
2021-07-21 14:32:55 -06:00
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." ) ;
}
2020-10-23 14:18:53 +11:00
2020-11-27 13:34:58 +01:00
if ( errors . Count > 0 )
2020-10-23 14:18:53 +11:00
{
ViewData . SetExternalSignInProviderErrors (
new BackOfficeExternalLoginProviderErrors (
loginInfo . LoginProvider ,
2020-11-27 13:34:58 +01:00
errors ) ) ;
2020-10-23 14:18:53 +11:00
}
2020-11-27 13:34:58 +01:00
return response ( ) ;
2020-10-23 14:18:53 +11:00
}
2020-05-25 19:37:16 +10:00
private IActionResult RedirectToLocal ( string returnUrl )
2020-05-20 15:25:42 +10:00
{
if ( Url . IsLocalUrl ( returnUrl ) )
{
return Redirect ( returnUrl ) ;
}
return Redirect ( "/" ) ;
}
2020-05-21 15:46:43 +10:00
2020-05-19 09:52:58 +02:00
2020-03-30 21:27:35 +02:00
}
}