2022-01-19 09:21:50 +01:00
using System.Security.Claims ;
using Microsoft.AspNetCore.Authentication ;
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Http.Extensions ;
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
2022-01-21 13:10:34 +01:00
using Microsoft.Extensions.Logging ;
2022-02-09 10:19:39 +01:00
using Microsoft.Extensions.Options ;
2022-01-19 09:21:50 +01:00
using Umbraco.Cms.Core.Cache ;
2022-02-09 10:19:39 +01:00
using Umbraco.Cms.Core.Configuration.Models ;
2022-01-19 09:21:50 +01:00
using Umbraco.Cms.Core.Logging ;
using Umbraco.Cms.Core.Routing ;
using Umbraco.Cms.Core.Security ;
using Umbraco.Cms.Core.Services ;
using Umbraco.Cms.Core.Web ;
using Umbraco.Cms.Infrastructure.Persistence ;
using Umbraco.Cms.Web.Common.ActionsResults ;
using Umbraco.Cms.Web.Common.Filters ;
using Umbraco.Cms.Web.Common.Security ;
using Umbraco.Extensions ;
using SignInResult = Microsoft . AspNetCore . Identity . SignInResult ;
2022-05-06 15:06:39 +02:00
namespace Umbraco.Cms.Web.Website.Controllers ;
[UmbracoMemberAuthorize]
public class UmbExternalLoginController : SurfaceController
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
private readonly ILogger < UmbExternalLoginController > _logger ;
private readonly IMemberManager _memberManager ;
private readonly IMemberSignInManager _memberSignInManager ;
private readonly IOptions < SecuritySettings > _securitySettings ;
private readonly ITwoFactorLoginService _twoFactorLoginService ;
public UmbExternalLoginController (
ILogger < UmbExternalLoginController > logger ,
IUmbracoContextAccessor umbracoContextAccessor ,
IUmbracoDatabaseFactory databaseFactory ,
ServiceContext services ,
AppCaches appCaches ,
IProfilingLogger profilingLogger ,
IPublishedUrlProvider publishedUrlProvider ,
IMemberSignInManager memberSignInManager ,
IMemberManager memberManager ,
ITwoFactorLoginService twoFactorLoginService ,
IOptions < SecuritySettings > securitySettings )
: base (
umbracoContextAccessor ,
databaseFactory ,
services ,
appCaches ,
profilingLogger ,
publishedUrlProvider )
{
_logger = logger ;
_memberSignInManager = memberSignInManager ;
_memberManager = memberManager ;
_twoFactorLoginService = twoFactorLoginService ;
_securitySettings = securitySettings ;
}
/// <summary>
/// Endpoint used to redirect to a specific login provider. This endpoint is used from the Login Macro snippet.
/// </summary>
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin ( string provider , string? returnUrl = null )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
if ( returnUrl . IsNullOrWhiteSpace ( ) )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
returnUrl = Request . GetEncodedPathAndQuery ( ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
var wrappedReturnUrl =
Url . SurfaceAction ( nameof ( ExternalLoginCallback ) , this . GetControllerName ( ) , new { returnUrl } ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
AuthenticationProperties properties =
_memberSignInManager . ConfigureExternalAuthenticationProperties ( provider , wrappedReturnUrl ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
return Challenge ( properties , provider ) ;
}
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
/// <summary>
/// Endpoint used my the login provider to call back to our solution.
/// </summary>
[HttpGet]
[AllowAnonymous]
public async Task < IActionResult > ExternalLoginCallback ( string returnUrl )
{
var errors = new List < string > ( ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
ExternalLoginInfo ? loginInfo = await _memberSignInManager . GetExternalLoginInfoAsync ( ) ;
if ( loginInfo is null )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
errors . Add ( "Invalid response from the login provider" ) ;
}
else
{
SignInResult result = await _memberSignInManager . ExternalLoginSignInAsync (
loginInfo ,
false ,
_securitySettings . Value . MemberBypassTwoFactorForExternalLogins ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
if ( result = = SignInResult . Success )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
// Update any authentication tokens if succeeded
await _memberSignInManager . UpdateExternalAuthenticationTokensAsync ( loginInfo ) ;
return RedirectToLocal ( returnUrl ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
if ( result = = SignInResult . TwoFactorRequired )
{
MemberIdentityUser attemptedUser =
await _memberManager . FindByLoginAsync ( loginInfo . LoginProvider , loginInfo . ProviderKey ) ;
if ( attemptedUser = = null ! )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
return new ValidationErrorResult (
$"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}" ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
IEnumerable < string > providerNames =
await _twoFactorLoginService . GetEnabledTwoFactorProviderNamesAsync ( attemptedUser . Key ) ;
ViewData . SetTwoFactorProviderNames ( providerNames ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
return CurrentUmbracoPage ( ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
if ( result = = SignInResult . LockedOut )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
errors . Add (
$"The local member {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out." ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
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 member {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 before it can be used." ) ;
}
else if ( result = = MemberSignInManager . 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 = = MemberSignInManager . 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 = = MemberSignInManager . 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 MemberSignInManager . 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." ) ;
}
}
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
if ( errors . Count > 0 )
{
ViewData . SetExternalSignInProviderErrors (
new BackOfficeExternalLoginProviderErrors (
loginInfo ? . LoginProvider ,
errors ) ) ;
return CurrentUmbracoPage ( ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
return RedirectToLocal ( returnUrl ) ;
}
private void AddModelErrors ( IdentityResult result , string prefix = "" )
{
foreach ( IdentityError error in result . Errors )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
ModelState . AddModelError ( prefix , error . Description ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
}
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult LinkLogin ( string provider , string? returnUrl = null )
{
if ( returnUrl . IsNullOrWhiteSpace ( ) )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
returnUrl = Request . GetEncodedPathAndQuery ( ) ;
}
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
var wrappedReturnUrl =
Url . SurfaceAction ( nameof ( ExternalLinkLoginCallback ) , this . GetControllerName ( ) , new { returnUrl } ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
// Configures the redirect URL and user identifier for the specified external login including xsrf data
AuthenticationProperties properties =
_memberSignInManager . ConfigureExternalAuthenticationProperties (
provider ,
wrappedReturnUrl ,
_memberManager . GetUserId ( User ) ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
return Challenge ( properties , provider ) ;
}
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
[HttpGet]
public async Task < IActionResult > ExternalLinkLoginCallback ( string returnUrl )
{
MemberIdentityUser user = await _memberManager . GetUserAsync ( User ) ;
string? loginProvider = null ;
var errors = new List < string > ( ) ;
if ( user = = null ! )
{
// ... this should really not happen
errors . Add ( "Local user does not exist" ) ;
}
else
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
ExternalLoginInfo ? info =
await _memberSignInManager . GetExternalLoginInfoAsync ( await _memberManager . GetUserIdAsync ( user ) ) ;
if ( info = = null )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
// Add error and redirect for it to be displayed
errors . Add ( "An error occurred, could not get external login info" ) ;
2022-01-19 09:21:50 +01:00
}
else
{
2022-05-06 15:06:39 +02:00
loginProvider = info . LoginProvider ;
IdentityResult addLoginResult = await _memberManager . AddLoginAsync ( user , info ) ;
if ( addLoginResult . Succeeded )
2022-01-19 09:21:50 +01:00
{
2022-05-06 15:06:39 +02:00
// Update any authentication tokens if succeeded
await _memberSignInManager . UpdateExternalAuthenticationTokensAsync ( info ) ;
return RedirectToLocal ( returnUrl ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
// Add errors and redirect for it to be displayed
errors . AddRange ( addLoginResult . Errors . Select ( x = > x . Description ) ) ;
}
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
ViewData . SetExternalSignInProviderErrors (
new BackOfficeExternalLoginProviderErrors (
loginProvider ,
errors ) ) ;
return CurrentUmbracoPage ( ) ;
}
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
private IActionResult RedirectToLocal ( string returnUrl ) = >
Url . IsLocalUrl ( returnUrl ) ? Redirect ( returnUrl ) : RedirectToCurrentUmbracoPage ( ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
[HttpPost]
[ValidateAntiForgeryToken]
public async Task < IActionResult > Disassociate ( string provider , string providerKey , string? returnUrl = null )
{
if ( returnUrl . IsNullOrWhiteSpace ( ) )
{
returnUrl = Request . GetEncodedPathAndQuery ( ) ;
}
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
MemberIdentityUser user = await _memberManager . FindByIdAsync ( User . Identity ? . GetUserId ( ) ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
IdentityResult result = await _memberManager . RemoveLoginAsync ( user , provider , providerKey ) ;
2022-01-19 09:21:50 +01:00
2022-05-06 15:06:39 +02:00
if ( result . Succeeded )
{
await _memberSignInManager . SignInAsync ( user , false ) ;
return RedirectToLocal ( returnUrl ! ) ;
2022-01-19 09:21:50 +01:00
}
2022-05-06 15:06:39 +02:00
AddModelErrors ( result ) ;
return CurrentUmbracoPage ( ) ;
2022-01-19 09:21:50 +01:00
}
}