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;
}
///
/// Endpoint used to redirect to a specific login provider. This endpoint is used from the Login Macro snippet.
///
[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);
}
///
/// Endpoint used my the login provider to call back to our solution.
///
[HttpGet]
[AllowAnonymous]
public async Task ExternalLoginCallback(string returnUrl)
{
var errors = new List();
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 ExternalLinkLoginCallback(string returnUrl)
{
MemberIdentityUser user = await _memberManager.GetUserAsync(User);
string loginProvider = null;
var errors = new List();
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 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();
}
}
}