Improve functionality for external member logins (#11855)
* Bugfix - Take ufprt from form data if the request has form content type, otherwise fallback to use the query * External linking for members * Changed migration to reuse old table * removed unnecessary web.config files * Cleanup * Extracted class to own file * Clean up * Rollback changes to Umbraco.Web.UI.csproj * Fixed migration for SqlCE * Change notification handler to be on deleted * Update src/Umbraco.Infrastructure/Security/MemberUserStore.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Fixed issue with errors not shown on member linking * fixed issue with errors * clean up * Fix issue where external logins could not be used to upgrade Umbraco, because the externalLogin table was expected to look different. (Like after the migration) * Fixed issue in Ignore legacy column now using result column. Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
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;
|
||||
|
||||
namespace Umbraco.Cms.Web.Website.Controllers
|
||||
{
|
||||
[UmbracoMemberAuthorize]
|
||||
public class UmbExternalLoginController : SurfaceController
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly IMemberSignInManagerExternalLogins _memberSignInManager;
|
||||
|
||||
public UmbExternalLoginController(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManagerExternalLogins memberSignInManager,
|
||||
IMemberManager memberManager)
|
||||
: base(
|
||||
umbracoContextAccessor,
|
||||
databaseFactory,
|
||||
services,
|
||||
appCaches,
|
||||
profilingLogger,
|
||||
publishedUrlProvider)
|
||||
{
|
||||
_memberSignInManager = memberSignInManager;
|
||||
_memberManager = memberManager;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (returnUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
returnUrl = Request.GetEncodedPathAndQuery();
|
||||
}
|
||||
|
||||
var wrappedReturnUrl =
|
||||
Url.SurfaceAction(nameof(ExternalLoginCallback), this.GetControllerName(), new { returnUrl });
|
||||
|
||||
AuthenticationProperties properties =
|
||||
_memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl);
|
||||
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
|
||||
ExternalLoginInfo loginInfo = await _memberSignInManager.GetExternalLoginInfoAsync();
|
||||
if (loginInfo is null)
|
||||
{
|
||||
errors.Add("Invalid response from the login provider");
|
||||
}
|
||||
else
|
||||
{
|
||||
SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false);
|
||||
|
||||
if (result == SignInResult.Success)
|
||||
{
|
||||
// Update any authentication tokens if succeeded
|
||||
await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo);
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
if (result == SignInResult.TwoFactorRequired)
|
||||
{
|
||||
MemberIdentityUser attemptedUser =
|
||||
await _memberManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
|
||||
if (attemptedUser == null)
|
||||
{
|
||||
return new ValidationErrorResult(
|
||||
$"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}");
|
||||
}
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse =
|
||||
new ObjectResult(new { userId = attemptedUser.Id })
|
||||
{
|
||||
StatusCode = StatusCodes.Status402PaymentRequired
|
||||
};
|
||||
|
||||
return verifyResponse;
|
||||
}
|
||||
|
||||
if (result == SignInResult.LockedOut)
|
||||
{
|
||||
errors.Add(
|
||||
$"The local member {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 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.");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo?.LoginProvider,
|
||||
errors));
|
||||
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
private void AddModelErrors(IdentityResult result, string prefix = "")
|
||||
{
|
||||
foreach (IdentityError error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(prefix, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public IActionResult LinkLogin(string provider, string returnUrl = null)
|
||||
{
|
||||
if (returnUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
returnUrl = Request.GetEncodedPathAndQuery();
|
||||
}
|
||||
|
||||
var wrappedReturnUrl =
|
||||
Url.SurfaceAction(nameof(ExternalLinkLoginCallback), this.GetControllerName(), new { returnUrl });
|
||||
|
||||
// Configures the redirect URL and user identifier for the specified external login including xsrf data
|
||||
AuthenticationProperties properties =
|
||||
_memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl,
|
||||
_memberManager.GetUserId(User));
|
||||
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
ExternalLoginInfo info =
|
||||
await _memberSignInManager.GetExternalLoginInfoAsync(await _memberManager.GetUserIdAsync(user));
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
//Add error and redirect for it to be displayed
|
||||
errors.Add( "An error occurred, could not get external login info");
|
||||
}
|
||||
else
|
||||
{
|
||||
loginProvider = info.LoginProvider;
|
||||
IdentityResult addLoginResult = await _memberManager.AddLoginAsync(user, info);
|
||||
if (addLoginResult.Succeeded)
|
||||
{
|
||||
// Update any authentication tokens if succeeded
|
||||
await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(info);
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
//Add errors and redirect for it to be displayed
|
||||
errors.AddRange(addLoginResult.Errors.Select(x => x.Description));
|
||||
}
|
||||
}
|
||||
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginProvider,
|
||||
errors));
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
private IActionResult RedirectToLocal(string returnUrl) =>
|
||||
Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage();
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Disassociate(string provider, string providerKey, string returnUrl = null)
|
||||
{
|
||||
if (returnUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
returnUrl = Request.GetEncodedPathAndQuery();
|
||||
}
|
||||
|
||||
MemberIdentityUser user = await _memberManager.FindByIdAsync(User.Identity.GetUserId());
|
||||
|
||||
IdentityResult result = await _memberManager.RemoveLoginAsync(user, provider, providerKey);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await _memberSignInManager.SignInAsync(user, false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
AddModelErrors(result);
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user