From 0c24e35ba2fc64970079de1ec80c8285d418b73a Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 13 Sep 2024 14:56:58 -0700 Subject: [PATCH 01/29] External User Authentication Feature --- .../OrchardCore.OpenId/Manifest.cs | 2 + .../OrchardCore.OpenId.csproj | 1 + .../Controllers/AccountBaseController.cs | 22 + .../Controllers/AccountController.cs | 647 +--------------- .../ExternalAuthenticationController.cs | 690 ++++++++++++++++++ .../Controllers/RegistrationController.cs | 4 +- .../DataMigrations/ExternalUserMigrations.cs | 64 ++ ...rnalAuthenticationSettingsDisplayDriver.cs | 60 ++ .../ExternalUserLoginSettingsDisplayDriver.cs | 50 ++ ...ernalUserRoleLoginSettingsDisplayDriver.cs | 49 ++ .../Drivers/LoginSettingsDisplayDriver.cs | 3 - .../RegisterUserLoginFormDisplayDriver.cs | 2 +- .../RegistrationSettingsDisplayDriver.cs | 6 - .../Extensions/ControllerExtensions.cs | 2 +- .../ScriptExternalLoginEventHandler.cs | 6 +- .../OrchardCore.Users/Manifest.cs | 11 + .../Models/ExternalAuthenticationSettings.cs | 14 + .../Models/RegistrationSettings.cs | 16 + .../Models/UserRegistrationType.cs | 1 + .../OrchardCore.Users/Startup.cs | 54 +- ...ExternalAuthenticationSettings.Edit.cshtml | 149 ++++ .../ExternalUserLoginSettings.Edit.cshtml | 12 + .../ExternalUserRoleLoginSettings.Edit.cshtml | 156 ++++ .../Views/LoginSettings.Edit.cshtml | 164 ----- .../Views/RegistrationSettings.Edit.cshtml | 174 ----- .../Models/ExternalUserLoginSettings.cs | 6 + .../Models/ExternalUserRoleLoginSettings.cs | 8 + .../Models/LoginSettings.cs | 3 + .../OrchardCore.Users.Core/UserConstants.cs | 2 + src/docs/releases/2.1.0.md | 21 + .../AccountControllerTests.cs | 20 +- 31 files changed, 1406 insertions(+), 1013 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalUserMigrations.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalAuthenticationSettings.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthenticationSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserRoleLoginSettings.Edit.cshtml create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginSettings.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserRoleLoginSettings.cs diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Manifest.cs index d62397ecb78..a044a2ea97a 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Manifest.cs @@ -1,5 +1,6 @@ using OrchardCore.Modules.Manifest; using OrchardCore.OpenId; +using OrchardCore.Users; [assembly: Module( Name = "OpenID Connect", @@ -24,6 +25,7 @@ Dependencies = [ OpenIdConstants.Features.Core, + UserConstants.Features.ExternalAuthentication, ] )] diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj b/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj index cb676217dd9..9b9c681a2ae 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj @@ -26,6 +26,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountBaseController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountBaseController.cs index a6dffc4612f..d6feab23742 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountBaseController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountBaseController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; using OrchardCore.Users.Models; using OrchardCore.Workflows.Services; @@ -36,4 +37,25 @@ protected IActionResult RedirectToLocal(string returnUrl) return Redirect("~/"); } + + protected void CopyTempDataErrorsToModelState() + { + foreach (var errorMessage in TempData.Where(x => x.Key.StartsWith("error")).Select(x => x.Value.ToString())) + { + ModelState.AddModelError(string.Empty, errorMessage); + } + } + + + protected bool AddUserEnabledError(IUser user, IStringLocalizer S) + { + if (user is not User localUser || !localUser.IsEnabled) + { + ModelState.AddModelError(string.Empty, S["The specified user is not allowed to sign in."]); + + return true; + } + + return false; + } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index d37e09f1cc7..0063bbe30ed 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -1,4 +1,3 @@ -using System.Security.Claims; using System.Text.Json.Nodes; using System.Text.Json.Settings; using Microsoft.AspNetCore.Authentication; @@ -10,8 +9,6 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; @@ -24,15 +21,16 @@ using OrchardCore.Users.Models; using OrchardCore.Users.Services; using OrchardCore.Users.ViewModels; +using OrchardCore.Workflows.Helpers; using YesSql.Services; -using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; namespace OrchardCore.Users.Controllers; [Authorize] public sealed class AccountController : AccountBaseController { - public const string DefaultExternalLoginProtector = "DefaultExternalLogin"; + [Obsolete("This property will be removed in v3. Instead use ExternalAuthenticationController.DefaultExternalLoginProtector")] + public const string DefaultExternalLoginProtector = ExternalAuthenticationController.DefaultExternalLoginProtector; private readonly IUserService _userService; private readonly SignInManager _signInManager; @@ -47,8 +45,6 @@ public sealed class AccountController : AccountBaseController private readonly INotifier _notifier; private readonly IClock _clock; private readonly IDistributedCache _distributedCache; - private readonly IEnumerable _externalLoginHandlers; - private readonly IdentityOptions _identityOptions; private static readonly JsonMergeSettings _jsonMergeSettings = new() { @@ -74,9 +70,7 @@ public AccountController( IDataProtectionProvider dataProtectionProvider, IShellFeaturesManager shellFeaturesManager, IDisplayManager loginFormDisplayManager, - IUpdateModelAccessor updateModelAccessor, - IEnumerable externalLoginHandlers, - IOptions identityOptions) + IUpdateModelAccessor updateModelAccessor) { _signInManager = signInManager; _userManager = userManager; @@ -91,8 +85,6 @@ public AccountController( _shellFeaturesManager = shellFeaturesManager; _loginFormDisplayManager = loginFormDisplayManager; _updateModelAccessor = updateModelAccessor; - _externalLoginHandlers = externalLoginHandlers; - _identityOptions = identityOptions.Value; H = htmlLocalizer; S = stringLocalizer; @@ -110,21 +102,21 @@ public async Task Login(string returnUrl = null) // Clear the existing external cookie to ensure a clean login process. await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - var loginSettings = await _siteService.GetSettingsAsync(); + var loginSettings = await _siteService.GetSettingsAsync(); if (loginSettings.UseExternalProviderIfOnlyOneDefined) { var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); if (schemes.Count() == 1) { var dataProtector = _dataProtectionProvider.CreateProtector(DefaultExternalLoginProtector) - .ToTimeLimitedDataProtector(); + .ToTimeLimitedDataProtector(); var token = Guid.NewGuid(); var expiration = new TimeSpan(0, 0, 5); var protectedToken = dataProtector.Protect(token.ToString(), _clock.UtcNow.Add(expiration)); await _distributedCache.SetAsync(token.ToString(), token.ToByteArray(), new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = expiration }); - return RedirectToAction(nameof(DefaultExternalLogin), new { protectedToken, returnUrl }); + return RedirectToAction(nameof(ExternalAuthenticationController.DefaultExternalLogin), typeof(ExternalAuthenticationController).ControllerName(), new { protectedToken, returnUrl }); } } @@ -137,41 +129,6 @@ public async Task Login(string returnUrl = null) return View(formShape); } - [HttpGet] - [AllowAnonymous] - public async Task DefaultExternalLogin(string protectedToken, string returnUrl = null) - { - var loginSettings = await _siteService.GetSettingsAsync(); - if (loginSettings.UseExternalProviderIfOnlyOneDefined) - { - var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); - if (schemes.Count() == 1) - { - var dataProtector = _dataProtectionProvider.CreateProtector(DefaultExternalLoginProtector) - .ToTimeLimitedDataProtector(); - - try - { - if (Guid.TryParse(dataProtector.Unprotect(protectedToken), out var token)) - { - var tokenBytes = await _distributedCache.GetAsync(token.ToString()); - var cacheToken = new Guid(tokenBytes); - if (token.Equals(cacheToken)) - { - return ExternalLogin(schemes.First().Name, returnUrl); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while validating {defaultExternalLogin} token", DefaultExternalLoginProtector); - } - } - } - - return RedirectToAction(nameof(Login)); - } - [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] @@ -203,7 +160,7 @@ public async Task LoginPOST(string returnUrl = null) var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, lockoutOnFailure: true); if (result.Succeeded) { - if (!await AddConfirmEmailErrorAsync(user) && !AddUserEnabledError(user)) + if (!await AddConfirmEmailErrorAsync(user) && !AddUserEnabledError(user, S)) { result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: true); @@ -281,7 +238,7 @@ public async Task ChangePassword(ChangePasswordViewModel model, s if (TryValidateModel(model) && ModelState.IsValid) { var user = await _userService.GetAuthenticatedUserAsync(User); - if (await _userService.ChangePasswordAsync(user, model.CurrentPassword, model.Password, (key, message) => ModelState.AddModelError(key, message))) + if (await _userService.ChangePasswordAsync(user, model.CurrentPassword, model.Password, ModelState.AddModelError)) { if (Url.IsLocalUrl(returnUrl)) { @@ -301,504 +258,6 @@ public async Task ChangePassword(ChangePasswordViewModel model, s public IActionResult ChangePasswordConfirmation() => View(); - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public IActionResult ExternalLogin(string provider, string returnUrl = null) - { - // Request a redirect to the external login provider. - var redirectUrl = Url.Action(nameof(ExternalLoginCallback), new { returnUrl }); - var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); - - return Challenge(properties, provider); - } - - private async Task ExternalLoginSignInAsync(IUser user, ExternalLoginInfo info) - { - var externalClaims = info.Principal.GetSerializableClaims(); - var userRoles = await _userManager.GetRolesAsync(user); - var userInfo = user as User; - - var context = new UpdateUserContext(user, info.LoginProvider, externalClaims, userInfo.Properties) - { - UserClaims = userInfo.UserClaims, - UserRoles = userRoles, - }; - foreach (var item in _externalLoginHandlers) - { - try - { - await item.UpdateUserAsync(context); - } - catch (Exception ex) - { - _logger.LogError(ex, "{ExternalLoginHandler}.UpdateUserAsync threw an exception", item.GetType()); - } - } - - if (await UpdateUserPropertiesAsync(_userManager, userInfo, context)) - { - await _userManager.UpdateAsync(user); - } - - var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); - - if (result.Succeeded) - { - await _accountEvents.InvokeAsync((e, user) => e.LoggedInAsync(user), user, _logger); - - var identityResult = await _signInManager.UpdateExternalAuthenticationTokensAsync(info); - if (!identityResult.Succeeded) - { - _logger.LogError("Error updating the external authentication tokens."); - } - } - else - { - await _accountEvents.InvokeAsync((e, user) => e.LoggingInFailedAsync(user), user, _logger); - } - - return result; - } - - [HttpGet] - [AllowAnonymous] - public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) - { - if (remoteError != null) - { - _logger.LogError("Error from external provider: {Error}", remoteError); - ModelState.AddModelError(string.Empty, S["An error occurred in external provider."]); - - return RedirectToLogin(returnUrl); - } - - var info = await _signInManager.GetExternalLoginInfoAsync(); - if (info == null) - { - _logger.LogError("Could not get external login info."); - ModelState.AddModelError(string.Empty, S["An error occurred in external provider."]); - - return RedirectToLogin(returnUrl); - } - - var registrationSettings = await _siteService.GetSettingsAsync(); - var iUser = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); - - CopyTempDataErrorsToModelState(); - - if (iUser != null) - { - if (!await AddConfirmEmailErrorAsync(iUser) && !AddUserEnabledError(iUser)) - { - await _accountEvents.InvokeAsync((e, user, modelState) => e.LoggingInAsync(user.UserName, (key, message) => modelState.AddModelError(key, message)), iUser, ModelState, _logger); - - var signInResult = await ExternalLoginSignInAsync(iUser, info); - if (signInResult.Succeeded) - { - return await LoggedInActionResultAsync(iUser, returnUrl, info); - } - else - { - ModelState.AddModelError(string.Empty, S["Invalid login attempt."]); - } - } - } - else - { - var email = info.GetEmail(); - - if (_identityOptions.User.RequireUniqueEmail && !string.IsNullOrWhiteSpace(email)) - { - iUser = await _userManager.FindByEmailAsync(email); - } - - ViewData["ReturnUrl"] = returnUrl; - ViewData["LoginProvider"] = info.LoginProvider; - - if (iUser != null) - { - if (iUser is User userToLink && registrationSettings.UsersMustValidateEmail && !userToLink.EmailConfirmed) - { - return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), - new - { - Area = UserConstants.Features.Users, - Controller = typeof(EmailConfirmationController).ControllerName(), - ReturnUrl = returnUrl, - }); - } - - // Link external login to an existing user - ViewData["UserName"] = iUser.UserName; - - return View(nameof(LinkExternalLogin)); - } - - // No user could be matched, check if a new user can register. - if (registrationSettings.UsersCanRegister == UserRegistrationType.NoRegistration) - { - var message = S["Site does not allow user registration."]; - _logger.LogWarning("Site does not allow user registration."); - ModelState.AddModelError(string.Empty, message); - } - else - { - var externalLoginViewModel = new RegisterExternalLoginViewModel - { - NoPassword = registrationSettings.NoPasswordForExternalUsers, - NoEmail = registrationSettings.NoEmailForExternalUsers, - NoUsername = registrationSettings.NoUsernameForExternalUsers, - - // If registrationSettings.NoUsernameForExternalUsers is true, this username will not be used - UserName = await GenerateUsernameAsync(info), - Email = info.GetEmail(), - }; - - // The user doesn't exist and no information required, we can create the account locally - // instead of redirecting to the ExternalLogin. - var noInformationRequired = externalLoginViewModel.NoPassword - && externalLoginViewModel.NoEmail - && externalLoginViewModel.NoUsername; - - if (noInformationRequired) - { - iUser = await this.RegisterUser(new RegisterUserForm() - { - UserName = externalLoginViewModel.UserName, - Email = externalLoginViewModel.Email, - Password = null, - }, S["Confirm your account"], _logger); - - // If the registration was successful we can link the external provider and redirect the user. - if (iUser != null) - { - var identityResult = await _signInManager.UserManager.AddLoginAsync(iUser, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName)); - if (identityResult.Succeeded) - { - _logger.LogInformation(3, "User account linked to {LoginProvider} provider.", info.LoginProvider); - - // The login info must be linked before we consider a redirect, or the login info is lost. - if (iUser is User user) - { - if (registrationSettings.UsersMustValidateEmail && !user.EmailConfirmed) - { - return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), - new - { - Area = UserConstants.Features.Users, - Controller = typeof(EmailConfirmationController).ControllerName(), - ReturnUrl = returnUrl, - }); - } - - if (registrationSettings.UsersAreModerated && !user.IsEnabled) - { - return RedirectToAction(nameof(RegistrationController.RegistrationPending), - new - { - Area = UserConstants.Features.Users, - Controller = typeof(RegistrationController).ControllerName(), - ReturnUrl = returnUrl, - }); - } - } - - // We have created/linked to the local user, so we must verify the login. - // If it does not succeed, the user is not allowed to login - var signInResult = await ExternalLoginSignInAsync(iUser, info); - if (signInResult.Succeeded) - { - return await LoggedInActionResultAsync(iUser, returnUrl, info); - } - - ModelState.AddModelError(string.Empty, S["Invalid login attempt."]); - - return RedirectToLogin(returnUrl); - } - - AddIdentityErrors(identityResult); - } - } - - return View(nameof(RegisterExternalLogin), externalLoginViewModel); - } - } - - return RedirectToLogin(returnUrl); - } - - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task RegisterExternalLogin(RegisterExternalLoginViewModel model, string returnUrl = null) - { - var info = await _signInManager.GetExternalLoginInfoAsync(); - - if (info == null) - { - _logger.LogWarning("Error loading external login info."); - - return NotFound(); - } - - var settings = await _siteService.GetSettingsAsync(); - - if (settings.UsersCanRegister == UserRegistrationType.NoRegistration) - { - _logger.LogWarning("Site does not allow user registration."); - - return NotFound(); - } - - ViewData["ReturnUrl"] = returnUrl; - ViewData["LoginProvider"] = info.LoginProvider; - - model.NoPassword = settings.NoPasswordForExternalUsers; - model.NoEmail = settings.NoEmailForExternalUsers; - model.NoUsername = settings.NoUsernameForExternalUsers; - - ModelState.Clear(); - - if (model.NoEmail && string.IsNullOrWhiteSpace(model.Email)) - { - model.Email = info.Principal.FindFirstValue(ClaimTypes.Email) ?? info.Principal.FindFirstValue("email"); - } - - if (model.NoUsername && string.IsNullOrWhiteSpace(model.UserName)) - { - model.UserName = await GenerateUsernameAsync(info); - } - - if (model.NoPassword) - { - model.Password = null; - model.ConfirmPassword = null; - } - - if (TryValidateModel(model) && ModelState.IsValid) - { - var iUser = await this.RegisterUser( - new RegisterUserForm() - { - UserName = model.UserName, - Email = model.Email, - Password = model.Password, - }, S["Confirm your account"], _logger); - - if (iUser is null) - { - ModelState.AddModelError(string.Empty, "Registration Failed."); - } - else - { - var identityResult = await _signInManager.UserManager.AddLoginAsync(iUser, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName)); - if (identityResult.Succeeded) - { - _logger.LogInformation(3, "User account linked to {provider} provider.", info.LoginProvider); - - // The login info must be linked before we consider a redirect, or the login info is lost. - if (iUser is User user) - { - if (settings.UsersMustValidateEmail && !user.EmailConfirmed) - { - return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), - new - { - Area = UserConstants.Features.Users, - Controller = typeof(EmailConfirmationController).ControllerName(), - ReturnUrl = returnUrl, - }); - } - - if (settings.UsersAreModerated && !user.IsEnabled) - { - return RedirectToAction(nameof(RegistrationController.RegistrationPending), - new - { - Area = UserConstants.Features.Users, - Controller = typeof(RegistrationController).ControllerName(), - ReturnUrl = returnUrl, - }); - } - } - - // we have created/linked to the local user, so we must verify the login. If it does not succeed, - // the user is not allowed to login - var signInResult = await ExternalLoginSignInAsync(iUser, info); - if (signInResult.Succeeded) - { - return await LoggedInActionResultAsync(iUser, returnUrl, info); - } - } - AddIdentityErrors(identityResult); - } - } - - return View(nameof(RegisterExternalLogin), model); - } - - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task LinkExternalLogin(LinkExternalLoginViewModel model, string returnUrl = null) - { - var info = await _signInManager.GetExternalLoginInfoAsync(); - - if (info == null) - { - _logger.LogWarning("Error loading external login info."); - - return NotFound(); - } - - var user = await _userManager.FindByEmailAsync(info.GetEmail()); - - if (user == null) - { - _logger.LogWarning("Suspicious login detected from external provider. {provider} with key [{providerKey}] for {identity}", - info.LoginProvider, info.ProviderKey, info.Principal?.Identity?.Name); - - return RedirectToAction(nameof(Login)); - } - - if (ModelState.IsValid) - { - await _accountEvents.InvokeAsync((e, model, modelState) => e.LoggingInAsync(user.UserName, (key, message) => modelState.AddModelError(key, message)), model, ModelState, _logger); - - var signInResult = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false); - if (!signInResult.Succeeded) - { - user = null; - ModelState.AddModelError(string.Empty, S["Invalid login attempt."]); - } - else - { - var identityResult = await _signInManager.UserManager.AddLoginAsync(user, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName)); - if (identityResult.Succeeded) - { - _logger.LogInformation(3, "User account linked to {provider} provider.", info.LoginProvider); - // we have created/linked to the local user, so we must verify the login. If it does not succeed, - // the user is not allowed to login - if ((await ExternalLoginSignInAsync(user, info)).Succeeded) - { - return await LoggedInActionResultAsync(user, returnUrl, info); - } - } - AddIdentityErrors(identityResult); - } - } - - CopyModelStateErrorsToTempData(null); - - return RedirectToAction(nameof(Login)); - } - - [HttpGet] - public async Task ExternalLogins() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - return Forbid(); - } - - var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) }; - model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) - .Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) - .ToList(); - model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1; - // model.StatusMessage = StatusMessage; - - CopyTempDataErrorsToModelState(); - - return View(model); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task LinkLogin(string provider) - { - // Clear the existing external cookie to ensure a clean login process. - await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - - // Request a redirect to the external login provider to link a login for the current user. - var redirectUrl = Url.Action(nameof(LinkLoginCallback)); - var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); - - return new ChallengeResult(provider, properties); - } - - [HttpGet] - public async Task LinkLoginCallback() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - _logger.LogError("Unable to load user with ID '{UserId}'.", _userManager.GetUserId(User)); - - return RedirectToAction(nameof(Login)); - } - - var info = await _signInManager.GetExternalLoginInfoAsync(); - if (info == null) - { - _logger.LogError("Unexpected error occurred loading external login info for user '{UserName}'.", user.UserName); - - return RedirectToAction(nameof(Login)); - } - - var result = await _userManager.AddLoginAsync(user, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName)); - if (!result.Succeeded) - { - _logger.LogError("Unexpected error occurred adding external login info for user '{UserName}'.", user.UserName); - - return RedirectToAction(nameof(Login)); - } - - // Clear the existing external cookie to ensure a clean login process. - await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - // Perform External Login SignIn. - await ExternalLoginSignInAsync(user, info); - // StatusMessage = "The external login was added."; - - return RedirectToAction(nameof(ExternalLogins)); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task RemoveLogin(RemoveLoginViewModel model) - { - var user = await _userManager.GetUserAsync(User); - if (user == null || user is not User u) - { - _logger.LogError("Unable to load user with ID '{UserId}'.", _userManager.GetUserId(User)); - - return RedirectToAction(nameof(Login)); - } - - var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey); - if (!result.Succeeded) - { - _logger.LogError("Unexpected error occurred removing external login info for user '{UserName}'.", user.UserName); - - return RedirectToAction(nameof(Login)); - } - - // Remove External Authentication Tokens. - foreach (var item in u.UserTokens.Where(c => c.LoginProvider == model.LoginProvider).ToList()) - { - if (!(await _userManager.RemoveAuthenticationTokenAsync(user, model.LoginProvider, item.Name)).Succeeded) - { - _logger.LogError("Could not remove '{TokenName}' token while unlinking '{LoginProvider}' provider from user '{UserName}'.", item.Name, model.LoginProvider, user.UserName); - } - } - - await _signInManager.SignInAsync(user, isPersistent: false); - // StatusMessage = "The external login was removed."; - return RedirectToAction(nameof(ExternalLogins)); - } - public static async Task UpdateUserPropertiesAsync(UserManager userManager, User user, UpdateUserContext context) { await userManager.AddToRolesAsync(user, context.RolesToAdd.Distinct()); @@ -854,86 +313,6 @@ public static async Task UpdateUserPropertiesAsync(UserManager user return userNeedUpdate; } - private async Task GenerateUsernameAsync(ExternalLoginInfo info) - { - var ret = string.Concat("u", IdGenerator.GenerateId()); - var externalClaims = info?.Principal.GetSerializableClaims(); - var userNames = new Dictionary(); - - foreach (var item in _externalLoginHandlers) - { - try - { - var userName = await item.GenerateUserName(info.LoginProvider, externalClaims.ToArray()); - if (!string.IsNullOrWhiteSpace(userName)) - { - // Set the return value to the username generated by the first IExternalLoginHandler. - if (userNames.Count == 0) - { - ret = userName; - } - userNames.Add(item.GetType(), userName); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "{externalLoginHandler} - IExternalLoginHandler.GenerateUserName threw an exception", item.GetType()); - } - } - - if (userNames.Count > 1) - { - _logger.LogWarning("More than one IExternalLoginHandler generated username. Used first one registered, {externalLoginHandler}", userNames.FirstOrDefault().Key); - } - - return ret; - } - - private RedirectToActionResult RedirectToLogin(string returnUrl) - { - CopyModelStateErrorsToTempData(); - - return RedirectToAction(nameof(Login), new { returnUrl }); - } - - private void CopyModelStateErrorsToTempData(string key = "") - { - var iix = 0; - - foreach (var state in ModelState) - { - if (key != null && state.Key != key) - { - continue; - } - - foreach (var item in state.Value.Errors) - { - TempData[$"error_{iix++}"] = item.ErrorMessage; - } - } - } - - private void CopyTempDataErrorsToModelState() - { - foreach (var errorMessage in TempData.Where(x => x.Key.StartsWith("error")).Select(x => x.Value.ToString())) - { - ModelState.AddModelError(string.Empty, errorMessage); - } - } - - private bool AddUserEnabledError(IUser user) - { - if (user is not User localUser || !localUser.IsEnabled) - { - ModelState.AddModelError(string.Empty, S["The specified user is not allowed to sign in."]); - - return true; - } - - return false; - } - private async Task AddConfirmEmailErrorAsync(IUser user) { var registrationFeatureIsAvailable = (await _shellFeaturesManager.GetAvailableFeaturesAsync()) @@ -957,12 +336,4 @@ private async Task AddConfirmEmailErrorAsync(IUser user) return false; } - - private void AddIdentityErrors(IdentityResult result) - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs new file mode 100644 index 00000000000..896cb70fc96 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs @@ -0,0 +1,690 @@ +using System.Security.Claims; +using System.Text.Json.Settings; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Environment.Shell; +using OrchardCore.Modules; +using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Settings; +using OrchardCore.Users.Events; +using OrchardCore.Users.Handlers; +using OrchardCore.Users.Models; +using OrchardCore.Users.ViewModels; + +namespace OrchardCore.Users.Controllers; + +[Feature(UserConstants.Features.ExternalAuthentication)] +public sealed class ExternalAuthenticationController : AccountBaseController +{ + public const string DefaultExternalLoginProtector = "DefaultExternalLogin"; + + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly IDistributedCache _distributedCache; + private readonly ISiteService _siteService; + private readonly IEnumerable _accountEvents; + private readonly IShellFeaturesManager _shellFeaturesManager; + private readonly IEnumerable _externalLoginHandlers; + private readonly IdentityOptions _identityOptions; + + private static readonly JsonMergeSettings _jsonMergeSettings = new() + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge + }; + + internal readonly IHtmlLocalizer H; + internal readonly IStringLocalizer S; + + public ExternalAuthenticationController( + SignInManager signInManager, + UserManager userManager, + ILogger logger, + IDataProtectionProvider dataProtectionProvider, + IDistributedCache distributedCache, + ISiteService siteService, + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer, + IEnumerable accountEvents, + IShellFeaturesManager shellFeaturesManager, + IEnumerable externalLoginHandlers, + IOptions identityOptions) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + _dataProtectionProvider = dataProtectionProvider; + _distributedCache = distributedCache; + _siteService = siteService; + _accountEvents = accountEvents; + _shellFeaturesManager = shellFeaturesManager; + _externalLoginHandlers = externalLoginHandlers; + _identityOptions = identityOptions.Value; + + H = htmlLocalizer; + S = stringLocalizer; + } + + [HttpGet] + [AllowAnonymous] + public async Task DefaultExternalLogin(string protectedToken, string returnUrl = null) + { + var loginSettings = await _siteService.GetSettingsAsync(); + if (loginSettings.UseExternalProviderIfOnlyOneDefined) + { + var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); + if (schemes.Count() == 1) + { + var dataProtector = _dataProtectionProvider.CreateProtector(DefaultExternalLoginProtector) + .ToTimeLimitedDataProtector(); + + try + { + if (Guid.TryParse(dataProtector.Unprotect(protectedToken), out var token)) + { + var tokenBytes = await _distributedCache.GetAsync(token.ToString()); + var cacheToken = new Guid(tokenBytes); + if (token.Equals(cacheToken)) + { + return ExternalLogin(schemes.First().Name, returnUrl); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while validating {defaultExternalLogin} token", DefaultExternalLoginProtector); + } + } + } + + return RedirectToLogin(); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public IActionResult ExternalLogin(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider. + var redirectUrl = Url.Action(nameof(ExternalLoginCallback), new { returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + + return Challenge(properties, provider); + } + + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) + { + if (remoteError != null) + { + _logger.LogError("Error from external provider: {Error}", remoteError); + ModelState.AddModelError(string.Empty, S["An error occurred in external provider."]); + + return RedirectToLogin(returnUrl); + } + + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + _logger.LogError("Could not get external login info."); + ModelState.AddModelError(string.Empty, S["An error occurred in external provider."]); + + return RedirectToLogin(returnUrl); + } + + var iUser = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); + + CopyTempDataErrorsToModelState(); + + if (iUser != null) + { + if (!await AddConfirmEmailErrorAsync(iUser) && !AddUserEnabledError(iUser, S)) + { + await _accountEvents.InvokeAsync((e, user, modelState) => e.LoggingInAsync(user.UserName, (key, message) => modelState.AddModelError(key, message)), iUser, ModelState, _logger); + + var signInResult = await ExternalLoginSignInAsync(iUser, info); + if (signInResult.Succeeded) + { + return await LoggedInActionResultAsync(iUser, returnUrl, info); + } + else + { + ModelState.AddModelError(string.Empty, S["Invalid login attempt."]); + } + } + } + else + { + var email = info.GetEmail(); + + if (_identityOptions.User.RequireUniqueEmail && !string.IsNullOrWhiteSpace(email)) + { + iUser = await _userManager.FindByEmailAsync(email); + } + + ViewData["ReturnUrl"] = returnUrl; + ViewData["LoginProvider"] = info.LoginProvider; + + var registrationSettings = await _siteService.GetSettingsAsync(); + + if (iUser != null) + { + if (iUser is User userToLink && registrationSettings.UsersMustValidateEmail && !userToLink.EmailConfirmed) + { + return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), + new + { + Area = UserConstants.Features.Users, + Controller = typeof(EmailConfirmationController).ControllerName(), + ReturnUrl = returnUrl, + }); + } + + // Link external login to an existing user. + ViewData["UserName"] = iUser.UserName; + + return View(nameof(LinkExternalLogin)); + } + + var settings = await _siteService.GetSettingsAsync(); + + var externalLoginViewModel = new RegisterExternalLoginViewModel + { + NoPassword = settings.NoPassword, + NoEmail = settings.NoEmail, + NoUsername = settings.NoUsername, + + UserName = await GenerateUsernameAsync(info), + Email = info.GetEmail(), + }; + + // The user doesn't exist and no information required, we can create the account locally + // instead of redirecting to the ExternalLogin. + var noInformationRequired = externalLoginViewModel.NoPassword + && externalLoginViewModel.NoEmail + && externalLoginViewModel.NoUsername; + + if (noInformationRequired) + { + iUser = await this.RegisterUser(new RegisterUserForm() + { + UserName = externalLoginViewModel.UserName, + Email = externalLoginViewModel.Email, + Password = null, + }, S["Confirm your account"], _logger); + + // If the registration was successful we can link the external provider and redirect the user. + if (iUser != null) + { + var identityResult = await _signInManager.UserManager.AddLoginAsync(iUser, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName)); + if (identityResult.Succeeded) + { + _logger.LogInformation(3, "User account linked to {LoginProvider} provider.", info.LoginProvider); + + // The login info must be linked before we consider a redirect, or the login info is lost. + if (iUser is User user) + { + if (registrationSettings.UsersMustValidateEmail && !user.EmailConfirmed) + { + return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), + new + { + Area = UserConstants.Features.Users, + Controller = typeof(EmailConfirmationController).ControllerName(), + ReturnUrl = returnUrl, + }); + } + + if (registrationSettings.UsersAreModerated && !user.IsEnabled) + { + return RedirectToAction(nameof(RegistrationController.RegistrationPending), + new + { + Area = UserConstants.Features.Users, + Controller = typeof(RegistrationController).ControllerName(), + ReturnUrl = returnUrl, + }); + } + } + + // We have created/linked to the local user, so we must verify the login. + // If it does not succeed, the user is not allowed to login + var signInResult = await ExternalLoginSignInAsync(iUser, info); + if (signInResult.Succeeded) + { + return await LoggedInActionResultAsync(iUser, returnUrl, info); + } + + ModelState.AddModelError(string.Empty, S["Invalid login attempt."]); + + return RedirectToLogin(returnUrl); + } + + AddIdentityErrors(identityResult); + } + } + + return View(nameof(RegisterExternalLogin), externalLoginViewModel); + } + + return RedirectToLogin(returnUrl); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task RegisterExternalLogin(RegisterExternalLoginViewModel model, string returnUrl = null) + { + var info = await _signInManager.GetExternalLoginInfoAsync(); + + if (info == null) + { + _logger.LogWarning("Error loading external login info."); + + return NotFound(); + } + + ViewData["ReturnUrl"] = returnUrl; + ViewData["LoginProvider"] = info.LoginProvider; + + var settings = await _siteService.GetSettingsAsync(); + + model.NoPassword = settings.NoPassword; + model.NoEmail = settings.NoEmail; + model.NoUsername = settings.NoUsername; + + ModelState.Clear(); + + if (model.NoEmail && string.IsNullOrWhiteSpace(model.Email)) + { + model.Email = info.Principal.FindFirstValue(ClaimTypes.Email) ?? info.Principal.FindFirstValue("email"); + } + + if (model.NoUsername && string.IsNullOrWhiteSpace(model.UserName)) + { + model.UserName = await GenerateUsernameAsync(info); + } + + if (model.NoPassword) + { + model.Password = null; + model.ConfirmPassword = null; + } + + if (TryValidateModel(model) && ModelState.IsValid) + { + var iUser = await this.RegisterUser( + new RegisterUserForm() + { + UserName = model.UserName, + Email = model.Email, + Password = model.Password, + }, S["Confirm your account"], _logger); + + if (iUser is null) + { + ModelState.AddModelError(string.Empty, "Registration Failed."); + } + else + { + var identityResult = await _signInManager.UserManager.AddLoginAsync(iUser, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName)); + if (identityResult.Succeeded) + { + _logger.LogInformation(3, "User account linked to {provider} provider.", info.LoginProvider); + + // The login info must be linked before we consider a redirect, or the login info is lost. + if (iUser is User user) + { + var registrationSettings = await _siteService.GetSettingsAsync(); + + if (registrationSettings.UsersMustValidateEmail && !user.EmailConfirmed) + { + return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), + new + { + Area = UserConstants.Features.Users, + Controller = typeof(EmailConfirmationController).ControllerName(), + ReturnUrl = returnUrl, + }); + } + + if (registrationSettings.UsersAreModerated && !user.IsEnabled) + { + return RedirectToAction(nameof(RegistrationController.RegistrationPending), + new + { + Area = UserConstants.Features.Users, + Controller = typeof(RegistrationController).ControllerName(), + ReturnUrl = returnUrl, + }); + } + } + + // we have created/linked to the local user, so we must verify the login. If it does not succeed, + // the user is not allowed to login + var signInResult = await ExternalLoginSignInAsync(iUser, info); + if (signInResult.Succeeded) + { + return await LoggedInActionResultAsync(iUser, returnUrl, info); + } + } + AddIdentityErrors(identityResult); + } + } + + return View(nameof(RegisterExternalLogin), model); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task LinkExternalLogin(LinkExternalLoginViewModel model, string returnUrl = null) + { + var info = await _signInManager.GetExternalLoginInfoAsync(); + + if (info == null) + { + _logger.LogWarning("Error loading external login info."); + + return NotFound(); + } + + var user = await _userManager.FindByEmailAsync(info.GetEmail()); + + if (user == null) + { + _logger.LogWarning("Suspicious login detected from external provider. {provider} with key [{providerKey}] for {identity}", + info.LoginProvider, info.ProviderKey, info.Principal?.Identity?.Name); + + return RedirectToLogin(); + } + + if (ModelState.IsValid) + { + await _accountEvents.InvokeAsync((e, model, modelState) => e.LoggingInAsync(user.UserName, (key, message) => modelState.AddModelError(key, message)), model, ModelState, _logger); + + var signInResult = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false); + if (!signInResult.Succeeded) + { + user = null; + ModelState.AddModelError(string.Empty, S["Invalid login attempt."]); + } + else + { + var identityResult = await _signInManager.UserManager.AddLoginAsync(user, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName)); + if (identityResult.Succeeded) + { + _logger.LogInformation(3, "User account linked to {provider} provider.", info.LoginProvider); + // we have created/linked to the local user, so we must verify the login. If it does not succeed, + // the user is not allowed to login. + if ((await ExternalLoginSignInAsync(user, info)).Succeeded) + { + return await LoggedInActionResultAsync(user, returnUrl, info); + } + } + AddIdentityErrors(identityResult); + } + } + + CopyModelStateErrorsToTempData(null); + + return RedirectToLogin(); + } + + [HttpGet] + public async Task ExternalLogins() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return Forbid(); + } + + var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) }; + model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1; + + CopyTempDataErrorsToModelState(); + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task LinkLogin(string provider) + { + // Clear the existing external cookie to ensure a clean login process. + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + // Request a redirect to the external login provider to link a login for the current user. + var redirectUrl = Url.Action(nameof(LinkLoginCallback)); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + + return new ChallengeResult(provider, properties); + } + + [HttpGet] + public async Task LinkLoginCallback() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + _logger.LogError("Unable to load user with ID '{UserId}'.", _userManager.GetUserId(User)); + + return RedirectToLogin(); + } + + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + _logger.LogError("Unexpected error occurred loading external login info for user '{UserName}'.", user.UserName); + + return RedirectToLogin(); + } + + var result = await _userManager.AddLoginAsync(user, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName)); + if (!result.Succeeded) + { + _logger.LogError("Unexpected error occurred adding external login info for user '{UserName}'.", user.UserName); + + return RedirectToLogin(); + } + + // Clear the existing external cookie to ensure a clean login process. + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + // Perform External Login SignIn. + await ExternalLoginSignInAsync(user, info); + + return RedirectToAction(nameof(ExternalLogins)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RemoveLogin(RemoveLoginViewModel model) + { + var user = await _userManager.GetUserAsync(User); + if (user == null || user is not User u) + { + _logger.LogError("Unable to load user with ID '{UserId}'.", _userManager.GetUserId(User)); + + return RedirectToLogin(); + } + + var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey); + if (!result.Succeeded) + { + _logger.LogError("Unexpected error occurred removing external login info for user '{UserName}'.", user.UserName); + + return RedirectToLogin(); + } + + // Remove External Authentication Tokens. + foreach (var item in u.UserTokens.Where(c => c.LoginProvider == model.LoginProvider).ToList()) + { + if (!(await _userManager.RemoveAuthenticationTokenAsync(user, model.LoginProvider, item.Name)).Succeeded) + { + _logger.LogError("Could not remove '{TokenName}' token while unlinking '{LoginProvider}' provider from user '{UserName}'.", item.Name, model.LoginProvider, user.UserName); + } + } + + await _signInManager.SignInAsync(user, isPersistent: false); + + return RedirectToAction(nameof(ExternalLogins)); + } + + private async Task ExternalLoginSignInAsync(IUser user, ExternalLoginInfo info) + { + var externalClaims = info.Principal.GetSerializableClaims(); + var userRoles = await _userManager.GetRolesAsync(user); + var userInfo = user as User; + + var context = new UpdateUserContext(user, info.LoginProvider, externalClaims, userInfo.Properties) + { + UserClaims = userInfo.UserClaims, + UserRoles = userRoles, + }; + foreach (var item in _externalLoginHandlers) + { + try + { + await item.UpdateUserAsync(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "{ExternalLoginHandler}.UpdateUserAsync threw an exception", item.GetType()); + } + } + + if (await AccountController.UpdateUserPropertiesAsync(_userManager, userInfo, context)) + { + await _userManager.UpdateAsync(user); + } + + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); + + if (result.Succeeded) + { + await _accountEvents.InvokeAsync((e, user) => e.LoggedInAsync(user), user, _logger); + + var identityResult = await _signInManager.UpdateExternalAuthenticationTokensAsync(info); + if (!identityResult.Succeeded) + { + _logger.LogError("Error updating the external authentication tokens."); + } + } + else + { + await _accountEvents.InvokeAsync((e, user) => e.LoggingInFailedAsync(user), user, _logger); + } + + return result; + } + + private void AddIdentityErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + private RedirectToActionResult RedirectToLogin(string returnUrl) + { + CopyModelStateErrorsToTempData(); + + return RedirectToAction(nameof(AccountController.Login), typeof(AccountController).ControllerName(), new { returnUrl }); + } + + private RedirectToActionResult RedirectToLogin() + => RedirectToAction(nameof(AccountController.Login), typeof(AccountController).ControllerName()); + + private void CopyModelStateErrorsToTempData(string key = "") + { + var iix = 0; + + foreach (var state in ModelState) + { + if (key != null && state.Key != key) + { + continue; + } + + foreach (var item in state.Value.Errors) + { + TempData[$"error_{iix++}"] = item.ErrorMessage; + } + } + } + + private async Task AddConfirmEmailErrorAsync(IUser user) + { + var registrationFeatureIsAvailable = (await _shellFeaturesManager.GetAvailableFeaturesAsync()) + .Any(feature => feature.Id == UserConstants.Features.UserRegistration); + + if (!registrationFeatureIsAvailable) + { + return false; + } + + var registrationSettings = await _siteService.GetSettingsAsync(); + if (registrationSettings.UsersMustValidateEmail) + { + // Require that the users have a confirmed email before they can log on. + if (!await _userManager.IsEmailConfirmedAsync(user)) + { + ModelState.AddModelError(string.Empty, S["You must confirm your email."]); + return true; + } + } + + return false; + } + + private async Task GenerateUsernameAsync(ExternalLoginInfo info) + { + var ret = string.Concat("u", IdGenerator.GenerateId()); + var externalClaims = info?.Principal.GetSerializableClaims(); + var userNames = new Dictionary(); + + foreach (var item in _externalLoginHandlers) + { + try + { + var userName = await item.GenerateUserName(info.LoginProvider, externalClaims.ToArray()); + if (!string.IsNullOrWhiteSpace(userName)) + { + // Set the return value to the username generated by the first IExternalLoginHandler. + if (userNames.Count == 0) + { + ret = userName; + } + userNames.Add(item.GetType(), userName); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "{externalLoginHandler} - IExternalLoginHandler.GenerateUserName threw an exception", item.GetType()); + } + } + + if (userNames.Count > 1) + { + _logger.LogWarning("More than one IExternalLoginHandler generated username. Used first one registered, {externalLoginHandler}", userNames.FirstOrDefault().Key); + } + + return ret; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs index 15fbeec174a..dd327250e25 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs @@ -54,7 +54,7 @@ public RegistrationController( public async Task Register(string returnUrl = null) { var settings = await _siteService.GetSettingsAsync(); - if (settings.UsersCanRegister != UserRegistrationType.AllowRegistration) + if (!settings.AllowSiteRegistration) { return NotFound(); } @@ -74,7 +74,7 @@ public async Task RegisterPOST(string returnUrl = null) { var settings = await _siteService.GetSettingsAsync(); - if (settings.UsersCanRegister != UserRegistrationType.AllowRegistration) + if (!settings.AllowSiteRegistration) { return NotFound(); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalUserMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalUserMigrations.cs new file mode 100644 index 00000000000..5dd72289952 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalUserMigrations.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Data.Migration; +using OrchardCore.Entities; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Scope; +using OrchardCore.Settings; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.DataMigrations; + +public sealed class ExternalUserMigrations : DataMigration +{ +#pragma warning disable CA1822 // Mark members as static + public int Create() +#pragma warning restore CA1822 // Mark members as static +#pragma warning disable CS0618 // Type or member is obsolete + { + ShellScope.AddDeferredTask(async scope => + { + var siteService = scope.ServiceProvider.GetRequiredService(); + + var site = await siteService.LoadSiteSettingsAsync(); + + var registrationSettings = site.As(); + + registrationSettings.AllowSiteRegistration = registrationSettings.UsersCanRegister == UserRegistrationType.AllowRegistration; + + site.Put(registrationSettings); + + site.Put(new ExternalAuthenticationSettings + { + NoUsername = registrationSettings.NoUsernameForExternalUsers, + NoEmail = registrationSettings.NoEmailForExternalUsers, + NoPassword = registrationSettings.NoPasswordForExternalUsers, + GenerateUsernameScript = registrationSettings.GenerateUsernameScript, + UseScriptToGenerateUsername = registrationSettings.UseScriptToGenerateUsername + }); + + var loginSettings = site.As(); + + site.Put(new ExternalUserLoginSettings + { + UseExternalProviderIfOnlyOneDefined = loginSettings.UseExternalProviderIfOnlyOneDefined, + }); + + await siteService.UpdateSiteSettingsAsync(site); + + if (registrationSettings.UsersCanRegister == UserRegistrationType.AllowOnlyExternalUsers) + { + var featuresManager = scope.ServiceProvider.GetRequiredService(); + + if (await featuresManager.IsFeatureEnabledAsync(UserConstants.Features.ExternalAuthentication)) + { + return; + } + + await featuresManager.EnableFeaturesAsync(UserConstants.Features.ExternalAuthentication); + } + }); + + return 1; + } +#pragma warning restore CS0618 // Type or member is obsolete +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationSettingsDisplayDriver.cs new file mode 100644 index 00000000000..93008277702 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationSettingsDisplayDriver.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Settings; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Drivers; + +public sealed class ExternalAuthenticationSettingsDisplayDriver : SiteDisplayDriver +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + + public ExternalAuthenticationSettingsDisplayDriver( + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService) + { + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + } + + protected override string SettingsGroupId + => RegistrationSettingsDisplayDriver.GroupId; + + public override async Task EditAsync(ISite site, ExternalAuthenticationSettings settings, BuildEditorContext context) + { + var user = _httpContextAccessor.HttpContext?.User; + + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) + { + return null; + } + + return Initialize("ExternalAuthenticationSettings_Edit", model => + { + model.NoPassword = settings.NoPassword; + model.NoUsername = settings.NoUsername; + model.NoEmail = settings.NoEmail; + model.UseScriptToGenerateUsername = settings.UseScriptToGenerateUsername; + model.GenerateUsernameScript = settings.GenerateUsernameScript; + }).Location("Content:10") + .OnGroup(SettingsGroupId); + } + + public override async Task UpdateAsync(ISite site, ExternalAuthenticationSettings settings, UpdateEditorContext context) + { + var user = _httpContextAccessor.HttpContext?.User; + + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) + { + return null; + } + + await context.Updater.TryUpdateModelAsync(settings, Prefix); + + return await EditAsync(site, settings, context); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs new file mode 100644 index 00000000000..2c4a623d321 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Settings; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Drivers; + +public sealed class ExternalUserLoginSettingsDisplayDriver : SiteDisplayDriver +{ + public const string GroupId = "userLogin"; + + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + + public ExternalUserLoginSettingsDisplayDriver( + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService) + { + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + } + + protected override string SettingsGroupId + => GroupId; + + public override IDisplayResult Edit(ISite site, ExternalUserLoginSettings settings, BuildEditorContext context) + { + return Initialize("ExternalUserLoginSettings_Edit", model => + { + model.UseExternalProviderIfOnlyOneDefined = settings.UseExternalProviderIfOnlyOneDefined; + }).Location("Content:5#General") + .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.ManageUsers)) + .OnGroup(SettingsGroupId); + } + + public override async Task UpdateAsync(ISite site, ExternalUserLoginSettings section, UpdateEditorContext context) + { + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, CommonPermissions.ManageUsers)) + { + return null; + } + + await context.Updater.TryUpdateModelAsync(section, Prefix); + + return await EditAsync(site, section, context); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs new file mode 100644 index 00000000000..6b725a71b98 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Settings; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Drivers; + +public sealed class ExternalUserRoleLoginSettingsDisplayDriver : SiteDisplayDriver +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + + public ExternalUserRoleLoginSettingsDisplayDriver( + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService) + { + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + } + + protected override string SettingsGroupId + => ExternalUserLoginSettingsDisplayDriver.GroupId; + + public override IDisplayResult Edit(ISite site, ExternalUserRoleLoginSettings settings, BuildEditorContext context) + { + return Initialize("ExternalUserRoleLoginSettings_Edit", model => + { + model.UseScriptToSyncRoles = settings.UseScriptToSyncRoles; + model.SyncRolesScript = settings.SyncRolesScript; + }).Location("Content:10#General") + .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.ManageUsers)) + .OnGroup(SettingsGroupId); + } + + public override async Task UpdateAsync(ISite site, ExternalUserRoleLoginSettings section, UpdateEditorContext context) + { + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, CommonPermissions.ManageUsers)) + { + return null; + } + + await context.Updater.TryUpdateModelAsync(section, Prefix); + + return await EditAsync(site, section, context); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginSettingsDisplayDriver.cs index f4fa63a747c..9decddac8ed 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginSettingsDisplayDriver.cs @@ -31,10 +31,7 @@ public override IDisplayResult Edit(ISite site, LoginSettings settings, BuildEdi return Initialize("LoginSettings_Edit", model => { model.UseSiteTheme = settings.UseSiteTheme; - model.UseExternalProviderIfOnlyOneDefined = settings.UseExternalProviderIfOnlyOneDefined; model.DisableLocalLogin = settings.DisableLocalLogin; - model.UseScriptToSyncRoles = settings.UseScriptToSyncRoles; - model.SyncRolesScript = settings.SyncRolesScript; model.AllowChangingEmail = settings.AllowChangingEmail; model.AllowChangingUsername = settings.AllowChangingUsername; model.AllowChangingPhoneNumber = settings.AllowChangingPhoneNumber; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs index f3cbd90c902..9d693dc3794 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs @@ -18,7 +18,7 @@ public override async Task EditAsync(LoginForm model, BuildEdito { var settings = await _siteService.GetSettingsAsync(); - if (settings.UsersCanRegister != UserRegistrationType.AllowRegistration) + if (!settings.AllowSiteRegistration) { return null; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs index cc411c313b1..4c3917882b3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs @@ -37,15 +37,9 @@ public override async Task EditAsync(ISite site, RegistrationSet return Initialize("RegistrationSettings_Edit", model => { - model.UsersCanRegister = settings.UsersCanRegister; model.UsersMustValidateEmail = settings.UsersMustValidateEmail; model.UsersAreModerated = settings.UsersAreModerated; model.UseSiteTheme = settings.UseSiteTheme; - model.NoPasswordForExternalUsers = settings.NoPasswordForExternalUsers; - model.NoUsernameForExternalUsers = settings.NoUsernameForExternalUsers; - model.NoEmailForExternalUsers = settings.NoEmailForExternalUsers; - model.UseScriptToGenerateUsername = settings.UseScriptToGenerateUsername; - model.GenerateUsernameScript = settings.GenerateUsernameScript; }).Location("Content:5") .OnGroup(SettingsGroupId); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs index 68b99f3828b..7d094121093 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs @@ -60,7 +60,7 @@ internal static async Task RegisterUser(this Controller controller, Regis var settings = await controller.ControllerContext.HttpContext.RequestServices.GetRequiredService().GetSettingsAsync(); - if (settings.UsersCanRegister != UserRegistrationType.NoRegistration) + if (!settings.AllowSiteRegistration) { var registrationEvents = controller.ControllerContext.HttpContext.RequestServices.GetServices(); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs b/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs index 6cf8858698f..5cc941bc5a8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs @@ -32,7 +32,7 @@ ILogger logger public async Task GenerateUserName(string provider, IEnumerable claims) { - var registrationSettings = await _siteService.GetSettingsAsync(); + var registrationSettings = await _siteService.GetSettingsAsync(); if (registrationSettings.UseScriptToGenerateUsername) { @@ -58,12 +58,12 @@ public async Task GenerateUserName(string provider, IEnumerable(); + var loginSettings = await _siteService.GetSettingsAsync(); UpdateUserInternal(context, loginSettings); } - public void UpdateUserInternal(UpdateUserContext context, LoginSettings loginSettings) + public void UpdateUserInternal(UpdateUserContext context, ExternalUserRoleLoginSettings loginSettings) { if (!loginSettings.UseScriptToSyncRoles) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs index ffc43e988bc..395e11e3802 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs @@ -19,6 +19,17 @@ Category = "Security" )] +[assembly: Feature( + Id = UserConstants.Features.ExternalAuthentication, + Name = "External User Authentication", + Description = "Provides a way to allow authentication using identity provider.", + Dependencies = + [ + UserConstants.Features.Users, + ], + Category = "Security" +)] + [assembly: Feature( Id = "OrchardCore.Users.ChangeEmail", Name = "Users Change Email", diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalAuthenticationSettings.cs b/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalAuthenticationSettings.cs new file mode 100644 index 00000000000..a9a6ee3f38f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalAuthenticationSettings.cs @@ -0,0 +1,14 @@ +namespace OrchardCore.Users.Models; + +public class ExternalAuthenticationSettings +{ + public bool NoPassword { get; set; } + + public bool NoUsername { get; set; } + + public bool NoEmail { get; set; } + + public bool UseScriptToGenerateUsername { get; set; } + + public string GenerateUsernameScript { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Models/RegistrationSettings.cs b/src/OrchardCore.Modules/OrchardCore.Users/Models/RegistrationSettings.cs index 38a1e843147..7635b3e003f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Models/RegistrationSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Models/RegistrationSettings.cs @@ -2,13 +2,29 @@ namespace OrchardCore.Users.Models; public class RegistrationSettings { + [Obsolete("This property is no longer used and will be removed in the next major release.")] public UserRegistrationType UsersCanRegister { get; set; } + + public bool AllowSiteRegistration { get; set; } + public bool UsersMustValidateEmail { get; set; } + public bool UsersAreModerated { get; set; } + public bool UseSiteTheme { get; set; } + + [Obsolete("This property is no longer used and will be removed in the next major release. Instead use ExternalAuthenticationSettings.NoPassword")] public bool NoPasswordForExternalUsers { get; set; } + + [Obsolete("This property is no longer used and will be removed in the next major release. Instead use ExternalAuthenticationSettings.NoUsername")] public bool NoUsernameForExternalUsers { get; set; } + + [Obsolete("This property is no longer used and will be removed in the next major release. Instead use ExternalAuthenticationSettings.NoEmail")] public bool NoEmailForExternalUsers { get; set; } + + [Obsolete("This property is no longer used and will be removed in the next major release. Instead use ExternalAuthenticationSettings.UseScriptToGenerateUsername")] public bool UseScriptToGenerateUsername { get; set; } + + [Obsolete("This property is no longer used and will be removed in the next major release. Instead use ExternalAuthenticationSettings.GenerateUsernameScript")] public string GenerateUsernameScript { get; set; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Models/UserRegistrationType.cs b/src/OrchardCore.Modules/OrchardCore.Users/Models/UserRegistrationType.cs index 5e0596069a9..8788aed6842 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Models/UserRegistrationType.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Models/UserRegistrationType.cs @@ -1,5 +1,6 @@ namespace OrchardCore.Users.Models; +[Obsolete("This type is no longer used and will be removed in the next major release.")] public enum UserRegistrationType { NoRegistration = 0, diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 4ee688f27fa..f21bf42c87b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -38,6 +38,7 @@ using OrchardCore.Sms; using OrchardCore.Users.Commands; using OrchardCore.Users.Controllers; +using OrchardCore.Users.DataMigrations; using OrchardCore.Users.Deployment; using OrchardCore.Users.Drivers; using OrchardCore.Users.Handlers; @@ -113,17 +114,6 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde } ); - routes.MapAreaControllerRoute( - name: "ExternalLogins", - areaName: UserConstants.Features.Users, - pattern: _userOptions.ExternalLoginsUrl, - defaults: new - { - controller = _accountControllerName, - action = nameof(AccountController.ExternalLogins), - } - ); - routes.MapAreaControllerRoute( name: "ConfirmEmail", areaName: UserConstants.Features.Users, @@ -151,6 +141,8 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde public override void ConfigureServices(IServiceCollection services) { + services.AddDataMigration(); + services.Configure(userOptions => { var configuration = ShellScope.Services.GetRequiredService(); @@ -247,6 +239,46 @@ public override void ConfigureServices(IServiceCollection services) } } +[Feature(UserConstants.Features.ExternalAuthentication)] +public sealed class ExternalAuthenticationStartup : StartupBase +{ + private static readonly string _accountControllerName = typeof(AccountController).ControllerName(); + + private UserOptions _userOptions; + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSiteDisplayDriver(); + services.AddSiteDisplayDriver(); + } + + public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + _userOptions ??= serviceProvider.GetRequiredService>().Value; + + routes.MapAreaControllerRoute( + name: "ExternalLogins", + areaName: UserConstants.Features.Users, + pattern: _userOptions.ExternalLoginsUrl, + defaults: new + { + controller = _accountControllerName, + action = nameof(ExternalAuthenticationController.ExternalLogins), + } + ); + } +} + +[Feature(UserConstants.Features.ExternalAuthentication)] +[RequireFeatures("OrchardCore.Roles")] +public sealed class RoleExternalAuthenticationStartup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddSiteDisplayDriver(); + } +} + [RequireFeatures("OrchardCore.Roles")] public sealed class RolesStartup : StartupBase { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthenticationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthenticationSettings.Edit.cshtml new file mode 100644 index 00000000000..64cd9ab541e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthenticationSettings.Edit.cshtml @@ -0,0 +1,149 @@ +@using OrchardCore.Users.Models +@model ExternalAuthenticationSettings + + + + + + + + + +
@T["External Authentication"] @T["Settings when registering with external authentication providers"]
+ +
+
+ + + + @T["When a new user logs in with an external provider, they are not required to provide a local username. You can customize how it works by providing an IExternalLoginEventHandler or writing a script."] +
+
+
+
+ + + + @T["When a new user logs in with an external provider and the email claim is defined, they are not required to provide a local email address."] +
+
+
+
+ + + + @T["When a new user logs in with an external provider, they are not required to provide a local password."] +
+
+
+
+ + + + @T["If selected, any IExternalLoginEventHandlers defined in modules are not triggered"] +
+
+ +
+
+********************************************************************************************
+* context          : {userName,loginProvider,externalClaims[]}                             *
+* ======================================================================================== *
+* -userName        : String                                                                *
+* -loginProvider   : String                                                                *
+* -externalClaims  : [{subject,issuer,originalIssuer,properties[],type,value,valueType}]   *
+*  -subject        : String                                                                *
+*  -issuer         : String                                                                *
+*  -originalIssuer : String                                                                *
+*  -properties     : [{key,value}]                                                         *
+*   -key           : String                                                                *
+*   -value         : String                                                                *
+*  -type           : String                                                                *
+*  -value          : String                                                                *
+*  -valueType      : String                                                                *
+* ======================================================================================== *
+*    Description                                                                           *
+* ---------------------------------------------------------------------------------------- *
+*    Use the loginProvider and externalClaims properties of the context variable to        *
+*    inspect who authenticated the user and with what claims. If you do not set the        *
+*    context.userName property, a username will be generated.                              *
+*                                                                                          *
+********************************************************************************************
+
+ +
+ +
+ +
+ +
+ +
+ + diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml new file mode 100644 index 00000000000..7841ccd2332 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml @@ -0,0 +1,12 @@ +@using OrchardCore.Users.Models + +@model ExternalUserLoginSettings + +
+
+ + + + @T["If only one external provider is defined, auto challenge the provider to login. You should also configure registration options"] +
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserRoleLoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserRoleLoginSettings.Edit.cshtml new file mode 100644 index 00000000000..71d634522ce --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserRoleLoginSettings.Edit.cshtml @@ -0,0 +1,156 @@ +@model OrchardCore.Users.Models.ExternalUserRoleLoginSettings + +
+
+ + + + @T["If selected, any IExternalLoginEventHandlers defined in modules are not triggered"] +
+
+
+ +
+ +
+ +
+ + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml index 8b274cfbc9e..912b09c0c18 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml @@ -38,15 +38,6 @@ -
-
- - - - @T["If only one external provider is defined, auto challenge the provider to login. You should also configure registration options"] -
-
-
@@ -55,158 +46,3 @@ @T["When selected, users are not allowed to login with username/password. Make sure there is at least one admin with linked account"]
- -
-
- - - - @T["If selected, any IExternalLoginEventHandlers defined in modules are not triggered"] -
-
-
- -
- -
- -
- - - - - diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml index 3b4e81786f8..d4a06ff957e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml @@ -9,24 +9,6 @@ -
- - - - -
-
    -
  • @T["when 'No Registration' is selected, no new users can register"]
  • -
  • @T["when 'Allow Registration' is selected, a link is displayed to allow the users to register on the site. If there are external providers, once authenticated the user can register"]
  • -
  • @T["when 'Allow Only External Users' is selected, only users authenticated with external providers can register on the site"]
  • -
-
-
-
@@ -53,159 +35,3 @@ @T["Requires an active site theme."]
- -
@T["External Authentication"] @T["Settings when registering with external authentication providers"]
- -
-
- - - - @T["When a new user logs in with an external provider, they are not required to provide a local username. You can customize how it works by providing an IExternalLoginEventHandler or writing a script."] -
-
-
-
- - - - @T["When a new user logs in with an external provider and the email claim is defined, they are not required to provide a local email address."] -
-
-
-
- - - - @T["When a new user logs in with an external provider, they are not required to provide a local password."] -
-
-
-
- - - - @T["If selected, any IExternalLoginEventHandlers defined in modules are not triggered"] -
-
- -
-
-********************************************************************************************
-* context          : {userName,loginProvider,externalClaims[]}                             *
-* ======================================================================================== *
-* -userName        : String                                                                *
-* -loginProvider   : String                                                                *
-* -externalClaims  : [{subject,issuer,originalIssuer,properties[],type,value,valueType}]   *
-*  -subject        : String                                                                *
-*  -issuer         : String                                                                *
-*  -originalIssuer : String                                                                *
-*  -properties     : [{key,value}]                                                         *
-*   -key           : String                                                                *
-*   -value         : String                                                                *
-*  -type           : String                                                                *
-*  -value          : String                                                                *
-*  -valueType      : String                                                                *
-* ======================================================================================== *
-*    Description                                                                           *
-* ---------------------------------------------------------------------------------------- *
-*    Use the loginProvider and externalClaims properties of the context variable to        *
-*    inspect who authenticated the user and with what claims. If you do not set the        *
-*    context.userName property, a username will be generated.                              *
-*                                                                                          *
-********************************************************************************************
-
- -
- -
- -
- -
- -
- - diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginSettings.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginSettings.cs new file mode 100644 index 00000000000..5d37aed7c96 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginSettings.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Users.Models; + +public class ExternalUserLoginSettings +{ + public bool UseExternalProviderIfOnlyOneDefined { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserRoleLoginSettings.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserRoleLoginSettings.cs new file mode 100644 index 00000000000..2d054508dc2 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserRoleLoginSettings.cs @@ -0,0 +1,8 @@ +namespace OrchardCore.Users.Models; + +public class ExternalUserRoleLoginSettings +{ + public bool UseScriptToSyncRoles { get; set; } + + public string SyncRolesScript { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/LoginSettings.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/LoginSettings.cs index 9228fba989f..365f19dbea8 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/LoginSettings.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/LoginSettings.cs @@ -6,12 +6,15 @@ public class LoginSettings { public bool UseSiteTheme { get; set; } + [Obsolete("This property is no longer used and will be removed in the next major release. Instead use ExternalUserLoginSettings.NoPassword")] public bool UseExternalProviderIfOnlyOneDefined { get; set; } public bool DisableLocalLogin { get; set; } + [Obsolete("This property is no longer used and will be removed in the next major release. Instead use ExternalUserRoleLoginSettings.SyncRolesScript")] public bool UseScriptToSyncRoles { get; set; } + [Obsolete("This property is no longer used and will be removed in the next major release. Instead use ExternalUserRoleLoginSettings.SyncRolesScript")] public string SyncRolesScript { get; set; } public bool AllowChangingUsername { get; set; } diff --git a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs index 596ee932221..1b774450120 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs @@ -18,6 +18,8 @@ public static class Features public const string UserRegistration = "OrchardCore.Users.Registration"; + public const string ExternalAuthentication = "OrchardCore.Users.ExternalAuthentication"; + public const string ResetPassword = "OrchardCore.Users.ResetPassword"; } } diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 96d4d7e3a43..34b4c51ed9f 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -1,3 +1,24 @@ # Orchard Core 2.1.0 Release date: Not yet released + +## Change Logs + +### External User Authentication Feature + +A new feature, **External User Authentication**, has been added. This feature was separated from the existing **Users** feature to provide better dependency management and offer an option to disable external authentication by default. + +After upgrading the site, a fast-forward option allows you to enable this feature automatically when needed. As a result, the following settings have been relocated to new classes: + +- The properties `UseScriptToSyncRoles` and `SyncRolesScript` have been moved from `LoginSettings` to a new class, `ExternalUserRoleLoginSettings`. +- The property `UseExternalProviderIfOnlyOneDefined` has been moved from `LoginSettings` to `ExternalUserLoginSettings`. + +Additionally, the following properties have been moved from `RegistrationSettings` to `ExternalAuthenticationSettings`: + +- `NoPasswordForExternalUsers` is now `ExternalAuthenticationSettings.NoPassword`. +- `NoUsernameForExternalUsers` is now `ExternalAuthenticationSettings.NoUsername`. +- `NoEmailForExternalUsers` is now `ExternalAuthenticationSettings.NoEmail`. +- `UseScriptToGenerateUsername` is now `ExternalAuthenticationSettings.UseScriptToGenerateUsername`. +- `GenerateUsernameScript` is now `ExternalAuthenticationSettings.GenerateUsernameScript`. + +The `UsersCanRegister` property in the `RegistrationSettings` class has been marked obsolete, and a new property, `AllowSiteRegistration`, has been introduced. To enable site registration, set `AllowSiteRegistration` to `true`. For external registration, ensure the **External User Authentication** feature is enabled. diff --git a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs index a61ca667837..01c1c16e7a2 100644 --- a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs +++ b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs @@ -23,7 +23,7 @@ public async Task ExternalLoginSignIn_Test() // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - UsersCanRegister = UserRegistrationType.AllowRegistration, + AllowSiteRegistration = true, }); // Act @@ -63,7 +63,7 @@ await context.UsingTenantScopeAsync(async scope => var scriptExternalLoginEventHandler = scope.ServiceProvider.GetServices() .FirstOrDefault(x => x.GetType() == typeof(ScriptExternalLoginEventHandler)) as ScriptExternalLoginEventHandler; - var loginSettings = new LoginSettings + var loginSettings = new ExternalUserRoleLoginSettings { UseScriptToSyncRoles = true, SyncRolesScript = """ @@ -130,7 +130,7 @@ await context.UsingTenantScopeAsync(async scope => var scriptExternalLoginEventHandler = scope.ServiceProvider.GetServices() .FirstOrDefault(x => x.GetType() == typeof(ScriptExternalLoginEventHandler)) as ScriptExternalLoginEventHandler; - var loginSettings = new LoginSettings + var loginSettings = new ExternalUserRoleLoginSettings { UseScriptToSyncRoles = true, SyncRolesScript = """ @@ -174,7 +174,7 @@ public async Task Register_WhenAllowed_RegisterUser() // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - UsersCanRegister = UserRegistrationType.AllowRegistration, + AllowSiteRegistration = true, }); var responseFromGet = await context.Client.GetAsync("Register"); @@ -213,7 +213,7 @@ public async Task Register_WhenNotAllowed_ReturnNotFound() // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - UsersCanRegister = UserRegistrationType.NoRegistration, + AllowSiteRegistration = false, }); // Act @@ -229,7 +229,7 @@ public async Task Register_WhenFeatureIsNotEnable_ReturnNotFound() // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - UsersCanRegister = UserRegistrationType.AllowRegistration, + AllowSiteRegistration = true, }, enableRegistrationFeature: false); // Act @@ -245,7 +245,7 @@ public async Task Register_WhenRequireUniqueEmailIsTrue_PreventRegisteringMultip // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - UsersCanRegister = UserRegistrationType.AllowRegistration, + AllowSiteRegistration = true, }); var responseFromGet = await context.Client.GetAsync("Register"); @@ -292,7 +292,7 @@ public async Task Register_WhenRequireUniqueEmailIsFalse_AllowRegisteringMultipl // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - UsersCanRegister = UserRegistrationType.AllowRegistration, + AllowSiteRegistration = true, }, enableRegistrationFeature: true, requireUniqueEmail: false); // Register First User @@ -341,7 +341,7 @@ public async Task Register_WhenModeration_RedirectToRegistrationPending() // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - UsersCanRegister = UserRegistrationType.AllowRegistration, + AllowSiteRegistration = true, UsersAreModerated = true, }); @@ -382,7 +382,7 @@ public async Task Register_WhenRequireEmailConfirmation_RedirectToConfirmEmailSe // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - UsersCanRegister = UserRegistrationType.AllowRegistration, + AllowSiteRegistration = true, UsersMustValidateEmail = true, }); From cd96228a80d2b5b5c0b458a8f5ba4d3c1ac0ad86 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 13 Sep 2024 17:26:26 -0700 Subject: [PATCH 02/29] the feature is not discoverble --- .../OrchardCore.Users/Startup.cs | 3 +-- .../AccountControllerTests.cs | 23 +++++++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index f21bf42c87b..25c210c82bb 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -269,8 +269,7 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde } } -[Feature(UserConstants.Features.ExternalAuthentication)] -[RequireFeatures("OrchardCore.Roles")] +[RequireFeatures(UserConstants.Features.ExternalAuthentication, "OrchardCore.Roles")] public sealed class RoleExternalAuthenticationStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) diff --git a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs index 01c1c16e7a2..5d227df5b0e 100644 --- a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs +++ b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs @@ -24,7 +24,7 @@ public async Task ExternalLoginSignIn_Test() var context = await GetSiteContextAsync(new RegistrationSettings() { AllowSiteRegistration = true, - }); + }, true, true, true); // Act var model = new RegisterViewModel() @@ -433,7 +433,7 @@ private static async Task CreateRequestMessageAsync(Register return PostRequestHelper.CreateMessageWithCookies("Register", data, response); } - private static async Task GetSiteContextAsync(RegistrationSettings settings, bool enableRegistrationFeature = true, bool requireUniqueEmail = true) + private static async Task GetSiteContextAsync(RegistrationSettings settings, bool enableRegistrationFeature = true, bool requireUniqueEmail = true, bool enableExternalAuthentication = false) { var context = new SiteContext(); @@ -460,14 +460,27 @@ await recipeExecutor.ExecuteAsync( CancellationToken.None); } - if (enableRegistrationFeature) + if (enableRegistrationFeature || enableExternalAuthentication) { var shellFeatureManager = scope.ServiceProvider.GetRequiredService(); var extensionManager = scope.ServiceProvider.GetRequiredService(); - var extensionInfo = extensionManager.GetExtension(UserConstants.Features.UserRegistration); + var features = new List(); + + if (enableRegistrationFeature) + { + var extensionInfo = extensionManager.GetExtension(UserConstants.Features.UserRegistration); + + features.Add(new FeatureInfo(UserConstants.Features.UserRegistration, extensionInfo)); + } + + if (enableExternalAuthentication) + { + var extensionInfo = extensionManager.GetExtension(UserConstants.Features.ExternalAuthentication); + features.Add(new FeatureInfo(UserConstants.Features.ExternalAuthentication, extensionInfo)); + } - await shellFeatureManager.EnableFeaturesAsync([new FeatureInfo(UserConstants.Features.UserRegistration, extensionInfo)], true); + await shellFeatureManager.EnableFeaturesAsync(features, true); } var siteService = scope.ServiceProvider.GetRequiredService(); From 0448e58391d8610b0b2fa512823664a2b7aee17a Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 13 Sep 2024 21:31:51 -0700 Subject: [PATCH 03/29] Update dependencies --- .../OrchardCore.Facebook/Manifest.cs | 3 ++- .../OrchardCore.GitHub/Manifest.cs | 6 +++++- .../OrchardCore.Google/Manifest.cs | 6 +++++- .../OrchardCore.Microsoft.Authentication/Manifest.cs | 12 ++++++++++-- .../OrchardCore.OpenId/Manifest.cs | 3 +-- .../OrchardCore.OpenId/OrchardCore.OpenId.csproj | 1 - .../OrchardCore.Twitter/Manifest.cs | 6 +++++- 7 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/Manifest.cs index 2374c361c7a..659243405a1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Manifest.cs @@ -24,7 +24,8 @@ Description = "Authenticates users from Meta.", Dependencies = [ - FacebookConstants.Features.Core + FacebookConstants.Features.Core, + "OrchardCore.Users.ExternalAuthentication", ] )] diff --git a/src/OrchardCore.Modules/OrchardCore.GitHub/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.GitHub/Manifest.cs index 50934ea7ff0..3b719d76abf 100644 --- a/src/OrchardCore.Modules/OrchardCore.GitHub/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.GitHub/Manifest.cs @@ -13,5 +13,9 @@ Id = GitHubConstants.Features.GitHubAuthentication, Name = "GitHub Authentication", Category = "GitHub", - Description = "Authenticates users with their GitHub Account." + Description = "Authenticates users with their GitHub Account.", + Dependencies = + [ + "OrchardCore.Users.ExternalAuthentication", + ] )] diff --git a/src/OrchardCore.Modules/OrchardCore.Google/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Google/Manifest.cs index 5ca5c03429d..731c3576ec8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Google/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Google/Manifest.cs @@ -13,7 +13,11 @@ Id = GoogleConstants.Features.GoogleAuthentication, Name = "Google Authentication", Category = "Google", - Description = "Authenticates users with their Google Account." + Description = "Authenticates users with their Google Account.", + Dependencies = + [ + "OrchardCore.Users.ExternalAuthentication", + ] )] [assembly: Feature( diff --git a/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Manifest.cs index 0567a313124..cb8444401fd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Manifest.cs @@ -13,12 +13,20 @@ Id = MicrosoftAuthenticationConstants.Features.MicrosoftAccount, Name = "Microsoft Account Authentication", Category = "Microsoft Authentication", - Description = "Authenticates users with their Microsoft Account." + Description = "Authenticates users with their Microsoft Account.", + Dependencies = + [ + "OrchardCore.Users.ExternalAuthentication", + ] )] [assembly: Feature( Id = MicrosoftAuthenticationConstants.Features.AAD, Name = "Microsoft Entra ID (Azure Active Directory) Authentication", Category = "Microsoft Authentication", - Description = "Authenticates users with their Microsoft Entra ID Account." + Description = "Authenticates users with their Microsoft Entra ID Account.", + Dependencies = + [ + "OrchardCore.Users.ExternalAuthentication", + ] )] diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Manifest.cs index a044a2ea97a..9984c59fa46 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Manifest.cs @@ -1,6 +1,5 @@ using OrchardCore.Modules.Manifest; using OrchardCore.OpenId; -using OrchardCore.Users; [assembly: Module( Name = "OpenID Connect", @@ -25,7 +24,7 @@ Dependencies = [ OpenIdConstants.Features.Core, - UserConstants.Features.ExternalAuthentication, + "OrchardCore.Users.ExternalAuthentication", ] )] diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj b/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj index 9b9c681a2ae..cb676217dd9 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj @@ -26,7 +26,6 @@ -
diff --git a/src/OrchardCore.Modules/OrchardCore.Twitter/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Twitter/Manifest.cs index 338b41b6184..5f51c5f9707 100644 --- a/src/OrchardCore.Modules/OrchardCore.Twitter/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Twitter/Manifest.cs @@ -21,5 +21,9 @@ Name = "Sign in with X (Twitter)", Category = "X (Twitter)", Description = "Authenticates users with their X (Twitter) Account.", - Dependencies = [TwitterConstants.Features.Twitter] + Dependencies = + [ + TwitterConstants.Features.Twitter, + "OrchardCore.Users.ExternalAuthentication", + ] )] From 02d73cb5f270fb814ff08ef29d951c6a39a43d40 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sun, 15 Sep 2024 12:47:14 -0700 Subject: [PATCH 04/29] Fix tests --- .../Extensions/ControllerExtensions.cs | 2 +- .../AccountControllerTests.cs | 77 ++++++++++++++----- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs index 7d094121093..ff6a21e2dca 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs @@ -60,7 +60,7 @@ internal static async Task RegisterUser(this Controller controller, Regis var settings = await controller.ControllerContext.HttpContext.RequestServices.GetRequiredService().GetSettingsAsync(); - if (!settings.AllowSiteRegistration) + if (settings.AllowSiteRegistration) { var registrationEvents = controller.ControllerContext.HttpContext.RequestServices.GetServices(); diff --git a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs index 5d227df5b0e..01353e526ee 100644 --- a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs +++ b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs @@ -1,8 +1,6 @@ using System.Text.Json.Nodes; +using OrchardCore.Deployment.Services; using OrchardCore.Entities; -using OrchardCore.Environment.Extensions; -using OrchardCore.Environment.Extensions.Features; -using OrchardCore.Environment.Shell; using OrchardCore.Recipes.Services; using OrchardCore.Settings; using OrchardCore.Tests.Apis.Context; @@ -460,37 +458,76 @@ await recipeExecutor.ExecuteAsync( CancellationToken.None); } - if (enableRegistrationFeature || enableExternalAuthentication) - { - var shellFeatureManager = scope.ServiceProvider.GetRequiredService(); - var extensionManager = scope.ServiceProvider.GetRequiredService(); + var siteService = scope.ServiceProvider.GetRequiredService(); - var features = new List(); + var site = await siteService.LoadSiteSettingsAsync(); + + site.Put(settings); + + await siteService.UpdateSiteSettingsAsync(site); + }); + + if (enableRegistrationFeature || enableExternalAuthentication) + { + await context.UsingTenantScopeAsync(async scope => + { + var featureIds = new JsonArray(); if (enableRegistrationFeature) { - var extensionInfo = extensionManager.GetExtension(UserConstants.Features.UserRegistration); - - features.Add(new FeatureInfo(UserConstants.Features.UserRegistration, extensionInfo)); + featureIds.Add(UserConstants.Features.UserRegistration); } if (enableExternalAuthentication) { - var extensionInfo = extensionManager.GetExtension(UserConstants.Features.ExternalAuthentication); - features.Add(new FeatureInfo(UserConstants.Features.ExternalAuthentication, extensionInfo)); + featureIds.Add(UserConstants.Features.ExternalAuthentication); } - await shellFeatureManager.EnableFeaturesAsync(features, true); - } + var tempArchiveName = Path.GetTempFileName() + ".json"; + var tempArchiveFolder = PathExtensions.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - var siteService = scope.ServiceProvider.GetRequiredService(); + var data = new JsonObject + { + ["steps"] = new JsonArray + { + new JsonObject + { + { "name", "feature" }, + { "enable", featureIds }, + } + }, + }; - var site = await siteService.LoadSiteSettingsAsync(); + try + { + using (var stream = new FileStream(tempArchiveName, FileMode.Create)) + { + var bytes = Encoding.UTF8.GetBytes(data.ToString()); - site.Put(settings); + await stream.WriteAsync(bytes); + } - await siteService.UpdateSiteSettingsAsync(site); - }); + Directory.CreateDirectory(tempArchiveFolder); + File.Move(tempArchiveName, Path.Combine(tempArchiveFolder, "Recipe.json")); + + var deploymentManager = scope.ServiceProvider.GetRequiredService(); + + await deploymentManager.ImportDeploymentPackageAsync(new PhysicalFileProvider(tempArchiveFolder)); + } + finally + { + if (File.Exists(tempArchiveName)) + { + File.Delete(tempArchiveName); + } + + if (Directory.Exists(tempArchiveFolder)) + { + Directory.Delete(tempArchiveFolder, true); + } + } + }); + } return context; } From 2d9c8b2e879e18dff4adc95753bced49e4db45a0 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sun, 15 Sep 2024 16:53:48 -0700 Subject: [PATCH 05/29] fix build --- .../OrchardCore.Users/Controllers/AccountController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index 0063bbe30ed..1df3c1e668d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -108,7 +108,7 @@ public async Task Login(string returnUrl = null) var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); if (schemes.Count() == 1) { - var dataProtector = _dataProtectionProvider.CreateProtector(DefaultExternalLoginProtector) + var dataProtector = _dataProtectionProvider.CreateProtector(ExternalAuthenticationController.DefaultExternalLoginProtector) .ToTimeLimitedDataProtector(); var token = Guid.NewGuid(); From a3488143c035e92144e94612183fcf2c593af77e Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sun, 15 Sep 2024 17:08:11 -0700 Subject: [PATCH 06/29] Fix the invalid link in docs --- src/docs/releases/2.1.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 321c715edb4..694d563bf2c 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -25,4 +25,4 @@ The `UsersCanRegister` property in the `RegistrationSettings` class has been mar ### New 'Azure Communication SMS' feature -A new feature was added to allow you to send SMS messages using Azure Communication Services (ACS). Simply enable it then navigate to the admin dashboard > `Configurations` >> `Settings` >> `SMS` to configure the provider. For more information you can refer to the [docs](../reference/modules/Sms.Azure/README.md). +A new feature was added to allow you to send SMS messages using Azure Communication Services (ACS). Simply enable it then navigate to the admin dashboard > `Configurations` >> `Settings` >> `SMS` to configure the provider. For more information you can refer to the [docs](reference/modules/Sms.Azure/README.md). From 5b2969ff28d2bb33bb268a835de2dea5f6269c18 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sun, 15 Sep 2024 17:10:06 -0700 Subject: [PATCH 07/29] update startup --- src/OrchardCore.Modules/OrchardCore.Users/Startup.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 25c210c82bb..b9931c37013 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -250,6 +250,7 @@ public override void ConfigureServices(IServiceCollection services) { services.AddSiteDisplayDriver(); services.AddSiteDisplayDriver(); + services.AddSiteDisplayDriver(); } public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) @@ -269,15 +270,6 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde } } -[RequireFeatures(UserConstants.Features.ExternalAuthentication, "OrchardCore.Roles")] -public sealed class RoleExternalAuthenticationStartup : StartupBase -{ - public override void ConfigureServices(IServiceCollection services) - { - services.AddSiteDisplayDriver(); - } -} - [RequireFeatures("OrchardCore.Roles")] public sealed class RolesStartup : StartupBase { From 5036ebdf2aa14e81e5eb7ccab5343498b6711281 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sun, 15 Sep 2024 17:17:06 -0700 Subject: [PATCH 08/29] fix docs --- src/docs/reference/modules/Sms/README.md | 2 +- src/docs/releases/2.1.0.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/reference/modules/Sms/README.md b/src/docs/reference/modules/Sms/README.md index dae4526b949..fb90d681a67 100644 --- a/src/docs/reference/modules/Sms/README.md +++ b/src/docs/reference/modules/Sms/README.md @@ -20,7 +20,7 @@ To enable the [Twilio](https://www.twilio.com) provider, navigate to `Configurat ## Additional Available Providers -- [Azure Communication](../reference/modules/Sms.Azure/README.md) service provider. +- [Azure Communication](../Sms.Azure/README.md) service provider. ## Adding Custom Providers diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 694d563bf2c..321c715edb4 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -25,4 +25,4 @@ The `UsersCanRegister` property in the `RegistrationSettings` class has been mar ### New 'Azure Communication SMS' feature -A new feature was added to allow you to send SMS messages using Azure Communication Services (ACS). Simply enable it then navigate to the admin dashboard > `Configurations` >> `Settings` >> `SMS` to configure the provider. For more information you can refer to the [docs](reference/modules/Sms.Azure/README.md). +A new feature was added to allow you to send SMS messages using Azure Communication Services (ACS). Simply enable it then navigate to the admin dashboard > `Configurations` >> `Settings` >> `SMS` to configure the provider. For more information you can refer to the [docs](../reference/modules/Sms.Azure/README.md). From cb358cb15b65fed92bc4bfa8abb42b0878865ba3 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 09:39:45 -0700 Subject: [PATCH 09/29] use options to cleanup code --- .../Controllers/AccountBaseController.cs | 1 - .../Controllers/AccountController.cs | 188 +++++---------- .../ExternalAuthenticationController.cs | 9 +- .../Controllers/RegistrationController.cs | 17 +- .../ExternalUserLoginSettingsDisplayDriver.cs | 25 +- ...ernalUserRoleLoginSettingsDisplayDriver.cs | 2 +- .../RegisterUserLoginFormDisplayDriver.cs | 14 +- .../RegistrationSettingsDisplayDriver.cs | 28 ++- .../Extensions/ControllerExtensions.cs | 68 +++--- .../RegistrationOptionsConfigurations.cs | 27 +++ .../OrchardCore.Users/Startup.cs | 221 +++++++++--------- .../Models/ExternalUserLoginOptions.cs | 6 + .../Models/RegistrationOptions.cs | 12 + .../ExternalUserLoginOptionsConfigurations.cs | 24 ++ .../UserManagerHelper.cs | 71 ++++++ .../AccountControllerTests.cs | 4 +- 16 files changed, 408 insertions(+), 309 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/RegistrationOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/ExternalUserLoginOptionsConfigurations.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/UserManagerHelper.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountBaseController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountBaseController.cs index d6feab23742..333ca658405 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountBaseController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountBaseController.cs @@ -46,7 +46,6 @@ protected void CopyTempDataErrorsToModelState() } } - protected bool AddUserEnabledError(IUser user, IStringLocalizer S) { if (user is not User localUser || !localUser.IsEnabled) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index 1df3c1e668d..ce7227f8a37 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Nodes; -using System.Text.Json.Settings; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; @@ -9,10 +7,10 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; -using OrchardCore.Environment.Shell; using OrchardCore.Modules; using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Settings; @@ -21,7 +19,6 @@ using OrchardCore.Users.Models; using OrchardCore.Users.Services; using OrchardCore.Users.ViewModels; -using OrchardCore.Workflows.Helpers; using YesSql.Services; namespace OrchardCore.Users.Controllers; @@ -38,20 +35,15 @@ public sealed class AccountController : AccountBaseController private readonly ILogger _logger; private readonly ISiteService _siteService; private readonly IEnumerable _accountEvents; + private readonly ExternalUserLoginSettings _externalUserLoginSettings; + private readonly RegistrationOptions _registrationOptions; private readonly IDataProtectionProvider _dataProtectionProvider; - private readonly IShellFeaturesManager _shellFeaturesManager; private readonly IDisplayManager _loginFormDisplayManager; private readonly IUpdateModelAccessor _updateModelAccessor; private readonly INotifier _notifier; private readonly IClock _clock; private readonly IDistributedCache _distributedCache; - private static readonly JsonMergeSettings _jsonMergeSettings = new() - { - MergeArrayHandling = MergeArrayHandling.Replace, - MergeNullValueHandling = MergeNullValueHandling.Merge - }; - internal readonly IHtmlLocalizer H; internal readonly IStringLocalizer S; @@ -64,11 +56,12 @@ public AccountController( IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer, IEnumerable accountEvents, + IOptions registrationOptions, + IOptions externalUserLoginSettings, INotifier notifier, IClock clock, IDistributedCache distributedCache, IDataProtectionProvider dataProtectionProvider, - IShellFeaturesManager shellFeaturesManager, IDisplayManager loginFormDisplayManager, IUpdateModelAccessor updateModelAccessor) { @@ -78,11 +71,12 @@ public AccountController( _logger = logger; _siteService = siteService; _accountEvents = accountEvents; + _externalUserLoginSettings = externalUserLoginSettings.Value; + _registrationOptions = registrationOptions.Value; _notifier = notifier; _clock = clock; _distributedCache = distributedCache; _dataProtectionProvider = dataProtectionProvider; - _shellFeaturesManager = shellFeaturesManager; _loginFormDisplayManager = loginFormDisplayManager; _updateModelAccessor = updateModelAccessor; @@ -102,8 +96,7 @@ public async Task Login(string returnUrl = null) // Clear the existing external cookie to ensure a clean login process. await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - var loginSettings = await _siteService.GetSettingsAsync(); - if (loginSettings.UseExternalProviderIfOnlyOneDefined) + if (_externalUserLoginSettings.UseExternalProviderIfOnlyOneDefined) { var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); if (schemes.Count() == 1) @@ -141,72 +134,74 @@ public async Task LoginPOST(string returnUrl = null) var formShape = await _loginFormDisplayManager.UpdateEditorAsync(model, _updateModelAccessor.ModelUpdater, false, string.Empty, string.Empty); - var disableLocalLogin = (await _siteService.GetSettingsAsync()).DisableLocalLogin; + var loginSettings = await _siteService.GetSettingsAsync(); - if (disableLocalLogin) + if (loginSettings.DisableLocalLogin) { ModelState.AddModelError(string.Empty, S["Local login is disabled."]); + + return View(formShape); } - else + + await _accountEvents.InvokeAsync((e, model, modelState) => e.LoggingInAsync(model.UserName, (key, message) => modelState.AddModelError(key, message)), model, ModelState, _logger); + + IUser user = null; + + if (ModelState.IsValid) { - await _accountEvents.InvokeAsync((e, model, modelState) => e.LoggingInAsync(model.UserName, (key, message) => modelState.AddModelError(key, message)), model, ModelState, _logger); + user = await _userService.GetUserAsync(model.UserName); - IUser user = null; - if (ModelState.IsValid) + if (user != null) { - user = await _userService.GetUserAsync(model.UserName); - if (user != null) + var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, lockoutOnFailure: true); + if (result.Succeeded) { - var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, lockoutOnFailure: true); - if (result.Succeeded) + if (!await AddConfirmEmailErrorAsync(user) && !AddUserEnabledError(user, S)) { - if (!await AddConfirmEmailErrorAsync(user) && !AddUserEnabledError(user, S)) - { - result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: true); + result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: true); - if (result.Succeeded) - { - _logger.LogInformation(1, "User logged in."); - await _accountEvents.InvokeAsync((e, user) => e.LoggedInAsync(user), user, _logger); + if (result.Succeeded) + { + _logger.LogInformation(1, "User logged in."); + await _accountEvents.InvokeAsync((e, user) => e.LoggedInAsync(user), user, _logger); - return await LoggedInActionResultAsync(user, returnUrl); - } + return await LoggedInActionResultAsync(user, returnUrl); } } + } - if (result.RequiresTwoFactor) - { - return RedirectToAction(nameof(TwoFactorAuthenticationController.LoginWithTwoFactorAuthentication), - typeof(TwoFactorAuthenticationController).ControllerName(), - new - { - returnUrl, - model.RememberMe - }); - } + if (result.RequiresTwoFactor) + { + return RedirectToAction(nameof(TwoFactorAuthenticationController.LoginWithTwoFactorAuthentication), + typeof(TwoFactorAuthenticationController).ControllerName(), + new + { + returnUrl, + model.RememberMe + }); + } - if (result.IsLockedOut) - { - ModelState.AddModelError(string.Empty, S["The account is locked out"]); - await _accountEvents.InvokeAsync((e, user) => e.IsLockedOutAsync(user), user, _logger); + if (result.IsLockedOut) + { + ModelState.AddModelError(string.Empty, S["The account is locked out"]); + await _accountEvents.InvokeAsync((e, user) => e.IsLockedOutAsync(user), user, _logger); - return View(); - } + return View(); } - - ModelState.AddModelError(string.Empty, S["Invalid login attempt."]); } - if (user == null) - { - // Login failed unknown user. - await _accountEvents.InvokeAsync((e, model) => e.LoggingInFailedAsync(model.UserName), model, _logger); - } - else - { - // Login failed with a known user. - await _accountEvents.InvokeAsync((e, user) => e.LoggingInFailedAsync(user), user, _logger); - } + ModelState.AddModelError(string.Empty, S["Invalid login attempt."]); + } + + if (user == null) + { + // Login failed unknown user. + await _accountEvents.InvokeAsync((e, model) => e.LoggingInFailedAsync(model.UserName), model, _logger); + } + else + { + // Login failed with a known user. + await _accountEvents.InvokeAsync((e, user) => e.LoggingInFailedAsync(user), user, _logger); } // If we got this far, something failed, redisplay form. @@ -258,78 +253,19 @@ public async Task ChangePassword(ChangePasswordViewModel model, s public IActionResult ChangePasswordConfirmation() => View(); - public static async Task UpdateUserPropertiesAsync(UserManager userManager, User user, UpdateUserContext context) - { - await userManager.AddToRolesAsync(user, context.RolesToAdd.Distinct()); - await userManager.RemoveFromRolesAsync(user, context.RolesToRemove.Distinct()); - - var userNeedUpdate = false; - if (context.PropertiesToUpdate != null) - { - var currentProperties = user.Properties.DeepClone(); - user.Properties.Merge(context.PropertiesToUpdate, _jsonMergeSettings); - userNeedUpdate = !JsonNode.DeepEquals(currentProperties, user.Properties); - } - - var currentClaims = user.UserClaims - .Where(x => !string.IsNullOrEmpty(x.ClaimType)) - .DistinctBy(x => new { x.ClaimType, x.ClaimValue }) - .ToList(); - - var claimsChanged = false; - if (context.ClaimsToRemove?.Count > 0) - { - var claimsToRemove = context.ClaimsToRemove.ToHashSet(); - foreach (var item in claimsToRemove) - { - var exists = currentClaims.FirstOrDefault(claim => claim.ClaimType == item.ClaimType && claim.ClaimValue == item.ClaimValue); - if (exists is not null) - { - currentClaims.Remove(exists); - claimsChanged = true; - } - } - } - - if (context.ClaimsToUpdate?.Count > 0) - { - foreach (var item in context.ClaimsToUpdate) - { - var existing = currentClaims.FirstOrDefault(claim => claim.ClaimType == item.ClaimType && claim.ClaimValue == item.ClaimValue); - if (existing is null) - { - currentClaims.Add(item); - claimsChanged = true; - } - } - } - - if (claimsChanged) - { - user.UserClaims = currentClaims; - userNeedUpdate = true; - } - - return userNeedUpdate; - } + [Obsolete("This method will be removed in version 3. Instead please use UserManagerHelper.UpdateUserPropertiesAsync(userManager, user, context).")] + public static Task UpdateUserPropertiesAsync(UserManager userManager, User user, UpdateUserContext context) + => UserManagerHelper.UpdateUserPropertiesAsync(userManager, user, context); private async Task AddConfirmEmailErrorAsync(IUser user) { - var registrationFeatureIsAvailable = (await _shellFeaturesManager.GetAvailableFeaturesAsync()) - .Any(feature => feature.Id == UserConstants.Features.UserRegistration); - - if (!registrationFeatureIsAvailable) - { - return false; - } - - var registrationSettings = await _siteService.GetSettingsAsync(); - if (registrationSettings.UsersMustValidateEmail) + if (_registrationOptions.UsersMustValidateEmail) { // Require that the users have a confirmed email before they can log on. if (!await _userManager.IsEmailConfirmedAsync(user)) { ModelState.AddModelError(string.Empty, S["You must confirm your email."]); + return true; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs index 896cb70fc96..525900d2b3d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs @@ -1,5 +1,4 @@ using System.Security.Claims; -using System.Text.Json.Settings; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; @@ -37,12 +36,6 @@ public sealed class ExternalAuthenticationController : AccountBaseController private readonly IEnumerable _externalLoginHandlers; private readonly IdentityOptions _identityOptions; - private static readonly JsonMergeSettings _jsonMergeSettings = new() - { - MergeArrayHandling = MergeArrayHandling.Replace, - MergeNullValueHandling = MergeNullValueHandling.Merge - }; - internal readonly IHtmlLocalizer H; internal readonly IStringLocalizer S; @@ -568,7 +561,7 @@ public async Task RemoveLogin(RemoveLoginViewModel model) } } - if (await AccountController.UpdateUserPropertiesAsync(_userManager, userInfo, context)) + if (await UserManagerHelper.UpdateUserPropertiesAsync(_userManager, userInfo, context)) { await _userManager.UpdateAsync(user); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs index dd327250e25..1671d15f056 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; @@ -23,6 +24,7 @@ public sealed class RegistrationController : Controller private readonly INotifier _notifier; private readonly ILogger _logger; private readonly IDisplayManager _registerUserDisplayManager; + private readonly RegistrationOptions _registrationOptions; private readonly IUpdateModelAccessor _updateModelAccessor; internal readonly IStringLocalizer S; @@ -35,6 +37,7 @@ public RegistrationController( INotifier notifier, ILogger logger, IDisplayManager registerUserDisplayManager, + IOptions registrationOptions, IUpdateModelAccessor updateModelAccessor, IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer) @@ -45,6 +48,7 @@ public RegistrationController( _notifier = notifier; _logger = logger; _registerUserDisplayManager = registerUserDisplayManager; + _registrationOptions = registrationOptions.Value; _updateModelAccessor = updateModelAccessor; H = htmlLocalizer; S = stringLocalizer; @@ -53,8 +57,7 @@ public RegistrationController( [AllowAnonymous] public async Task Register(string returnUrl = null) { - var settings = await _siteService.GetSettingsAsync(); - if (!settings.AllowSiteRegistration) + if (!_registrationOptions.AllowSiteRegistration) { return NotFound(); } @@ -72,9 +75,7 @@ public async Task Register(string returnUrl = null) [ActionName(nameof(Register))] public async Task RegisterPOST(string returnUrl = null) { - var settings = await _siteService.GetSettingsAsync(); - - if (!settings.AllowSiteRegistration) + if (!_registrationOptions.AllowSiteRegistration) { return NotFound(); } @@ -89,15 +90,15 @@ public async Task RegisterPOST(string returnUrl = null) { var iUser = await this.RegisterUser(model, S["Confirm your account"], _logger); - // If we get a user, redirect to returnUrl + // If we get a user, redirect to returnUrl. if (iUser is User user) { - if (settings.UsersMustValidateEmail && !user.EmailConfirmed) + if (_registrationOptions.UsersMustValidateEmail && !user.EmailConfirmed) { return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), typeof(EmailConfirmationController).ControllerName(), new { ReturnUrl = returnUrl }); } - if (settings.UsersAreModerated && !user.IsEnabled) + if (_registrationOptions.UsersAreModerated && !user.IsEnabled) { return RedirectToAction(nameof(RegistrationPending), new { ReturnUrl = returnUrl }); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs index 2c4a623d321..ae98eb8d60b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs @@ -3,6 +3,7 @@ using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; +using OrchardCore.Environment.Shell; using OrchardCore.Settings; using OrchardCore.Users.Models; @@ -10,41 +11,49 @@ namespace OrchardCore.Users.Drivers; public sealed class ExternalUserLoginSettingsDisplayDriver : SiteDisplayDriver { - public const string GroupId = "userLogin"; - private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthorizationService _authorizationService; + private readonly IShellReleaseManager _shellReleaseManager; public ExternalUserLoginSettingsDisplayDriver( IHttpContextAccessor httpContextAccessor, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, + IShellReleaseManager shellReleaseManager) { _httpContextAccessor = httpContextAccessor; _authorizationService = authorizationService; + _shellReleaseManager = shellReleaseManager; } protected override string SettingsGroupId - => GroupId; + => LoginSettingsDisplayDriver.GroupId; public override IDisplayResult Edit(ISite site, ExternalUserLoginSettings settings, BuildEditorContext context) { return Initialize("ExternalUserLoginSettings_Edit", model => { model.UseExternalProviderIfOnlyOneDefined = settings.UseExternalProviderIfOnlyOneDefined; - }).Location("Content:5#General") + }).Location("Content:7#General") .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.ManageUsers)) .OnGroup(SettingsGroupId); } - public override async Task UpdateAsync(ISite site, ExternalUserLoginSettings section, UpdateEditorContext context) + public override async Task UpdateAsync(ISite site, ExternalUserLoginSettings settings, UpdateEditorContext context) { if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, CommonPermissions.ManageUsers)) { return null; } - await context.Updater.TryUpdateModelAsync(section, Prefix); + var valueBefore = settings.UseExternalProviderIfOnlyOneDefined; + + await context.Updater.TryUpdateModelAsync(settings, Prefix); + + if (valueBefore != settings.UseExternalProviderIfOnlyOneDefined) + { + _shellReleaseManager.RequestRelease(); + } - return await EditAsync(site, section, context); + return Edit(site, settings, context); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs index 6b725a71b98..753761ab900 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs @@ -22,7 +22,7 @@ public ExternalUserRoleLoginSettingsDisplayDriver( } protected override string SettingsGroupId - => ExternalUserLoginSettingsDisplayDriver.GroupId; + => LoginSettingsDisplayDriver.GroupId; public override IDisplayResult Edit(ISite site, ExternalUserRoleLoginSettings settings, BuildEditorContext context) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs index 9d693dc3794..c8c8d4d8ade 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs @@ -1,24 +1,22 @@ +using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Settings; using OrchardCore.Users.Models; namespace OrchardCore.Users.Drivers; public sealed class RegisterUserLoginFormDisplayDriver : DisplayDriver { - private readonly ISiteService _siteService; + private readonly RegistrationOptions _registrationOptions; - public RegisterUserLoginFormDisplayDriver(ISiteService siteService) + public RegisterUserLoginFormDisplayDriver(IOptions registrationOptions) { - _siteService = siteService; + _registrationOptions = registrationOptions.Value; } - public override async Task EditAsync(LoginForm model, BuildEditorContext context) + public override IDisplayResult Edit(LoginForm model, BuildEditorContext context) { - var settings = await _siteService.GetSettingsAsync(); - - if (!settings.AllowSiteRegistration) + if (!_registrationOptions.AllowSiteRegistration) { return null; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs index 4c3917882b3..4b8306e6811 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs @@ -3,6 +3,7 @@ using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; +using OrchardCore.Environment.Shell; using OrchardCore.Settings; using OrchardCore.Users.Models; @@ -14,13 +15,16 @@ public sealed class RegistrationSettingsDisplayDriver : SiteDisplayDriver EditAsync(ISite site, RegistrationSet return null; } + context.AddTenantReloadWarningWrapper(); + return Initialize("RegistrationSettings_Edit", model => { + model.AllowSiteRegistration = settings.AllowSiteRegistration; model.UsersMustValidateEmail = settings.UsersMustValidateEmail; model.UsersAreModerated = settings.UsersAreModerated; model.UseSiteTheme = settings.UseSiteTheme; @@ -53,7 +60,24 @@ public override async Task UpdateAsync(ISite site, RegistrationS return null; } - await context.Updater.TryUpdateModelAsync(settings, Prefix); + var model = new RegistrationSettings(); + + await context.Updater.TryUpdateModelAsync(model, Prefix); + + var hasChange = model.AllowSiteRegistration != settings.AllowSiteRegistration + || model.UsersMustValidateEmail != settings.UsersMustValidateEmail + || model.UsersAreModerated != settings.UsersAreModerated + || model.UseSiteTheme != model.UseSiteTheme; + + settings.AllowSiteRegistration = model.AllowSiteRegistration; + settings.UsersMustValidateEmail = model.UsersMustValidateEmail; + settings.UsersAreModerated = model.UsersAreModerated; + settings.UseSiteTheme = model.UseSiteTheme; + + if (hasChange) + { + _shellReleaseManager.RequestRelease(); + } return await EditAsync(site, settings, context); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs index ff6a21e2dca..fc0e7a22f11 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs @@ -4,12 +4,11 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement; using OrchardCore.Email; -using OrchardCore.Environment.Shell; using OrchardCore.Modules; using OrchardCore.Mvc.Core.Utilities; -using OrchardCore.Settings; using OrchardCore.Users.Events; using OrchardCore.Users.Models; using OrchardCore.Users.Services; @@ -48,55 +47,48 @@ internal static async Task SendEmailAsync(this Controller controller, stri /// internal static async Task RegisterUser(this Controller controller, RegisterUserForm model, string confirmationEmailSubject, ILogger logger) { - var shellFeaturesManager = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService(); + var registrationOptions = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>().Value; - var registrationFeatureIsAvailable = (await shellFeaturesManager.GetAvailableFeaturesAsync()) - .Any(feature => feature.Id == UserConstants.Features.UserRegistration); - - if (!registrationFeatureIsAvailable) + if (!registrationOptions.AllowSiteRegistration) { return null; } - var settings = await controller.ControllerContext.HttpContext.RequestServices.GetRequiredService().GetSettingsAsync(); + var registrationEvents = controller.ControllerContext.HttpContext.RequestServices.GetServices(); - if (settings.AllowSiteRegistration) - { - var registrationEvents = controller.ControllerContext.HttpContext.RequestServices.GetServices(); + await registrationEvents.InvokeAsync((e, modelState) => e.RegistrationValidationAsync((key, message) => modelState.AddModelError(key, message)), controller.ModelState, logger); - await registrationEvents.InvokeAsync((e, modelState) => e.RegistrationValidationAsync((key, message) => modelState.AddModelError(key, message)), controller.ModelState, logger); + if (controller.ModelState.IsValid) + { + var userService = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService(); - if (controller.ModelState.IsValid) + var user = await userService.CreateUserAsync(new User { - var userService = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService(); + UserName = model.UserName, + Email = model.Email, + EmailConfirmed = !registrationOptions.UsersMustValidateEmail, + IsEnabled = !registrationOptions.UsersAreModerated, + }, model.Password, controller.ModelState.AddModelError) as User; - var user = await userService.CreateUserAsync(new User + if (user != null && controller.ModelState.IsValid) + { + if (registrationOptions.UsersMustValidateEmail && !user.EmailConfirmed) { - UserName = model.UserName, - Email = model.Email, - EmailConfirmed = !settings.UsersMustValidateEmail, - IsEnabled = !settings.UsersAreModerated, - }, model.Password, controller.ModelState.AddModelError) as User; - - if (user != null && controller.ModelState.IsValid) + // For more information on how to enable account confirmation and password + // reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 + // Send an email with this link + await controller.SendEmailConfirmationTokenAsync(user, confirmationEmailSubject); + } + else if (!(registrationOptions.UsersAreModerated && !user.IsEnabled)) { - if (settings.UsersMustValidateEmail && !user.EmailConfirmed) - { - // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 - // Send an email with this link - await controller.SendEmailConfirmationTokenAsync(user, confirmationEmailSubject); - } - else if (!(settings.UsersAreModerated && !user.IsEnabled)) - { - var signInManager = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>(); - - await signInManager.SignInAsync(user, isPersistent: false); - } - logger.LogInformation(3, "User created a new account with password."); - await registrationEvents.InvokeAsync((e, user) => e.RegisteredAsync(user), user, logger); - - return user; + var signInManager = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>(); + + await signInManager.SignInAsync(user, isPersistent: false); } + logger.LogInformation(3, "User created a new account with password."); + await registrationEvents.InvokeAsync((e, user) => e.RegisteredAsync(user), user, logger); + + return user; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs new file mode 100644 index 00000000000..22e503a8ac4 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Settings; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public class RegistrationOptionsConfigurations : IConfigureOptions +{ + private readonly ISiteService _siteService; + + public RegistrationOptionsConfigurations(ISiteService siteService) + { + _siteService = siteService; + } + + public void Configure(RegistrationOptions options) + { + var settings = _siteService.GetSettingsAsync() + .GetAwaiter() + .GetResult(); + + options.AllowSiteRegistration = settings.AllowSiteRegistration; + options.UsersMustValidateEmail = settings.UsersMustValidateEmail; + options.UsersAreModerated = settings.UsersAreModerated; + options.UseSiteTheme = settings.UseSiteTheme; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index b337d6ec6d1..42ff485de2e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -57,8 +57,8 @@ public sealed class Startup : StartupBase private static readonly string _accountControllerName = typeof(AccountController).ControllerName(); private static readonly string _emailConfirmationControllerName = typeof(EmailConfirmationController).ControllerName(); - private readonly string _tenantName; + private UserOptions _userOptions; public Startup(ShellSettings shellSettings) @@ -66,79 +66,6 @@ public Startup(ShellSettings shellSettings) _tenantName = shellSettings.Name; } - public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) - { - _userOptions ??= serviceProvider.GetRequiredService>().Value; - - routes.MapAreaControllerRoute( - name: "Login", - areaName: UserConstants.Features.Users, - pattern: _userOptions.LoginPath, - defaults: new - { - controller = _accountControllerName, - action = nameof(AccountController.Login), - } - ); - - routes.MapAreaControllerRoute( - name: "ChangePassword", - areaName: UserConstants.Features.Users, - pattern: _userOptions.ChangePasswordUrl, - defaults: new - { - controller = _accountControllerName, - action = nameof(AccountController.ChangePassword), - } - ); - - routes.MapAreaControllerRoute( - name: "ChangePasswordConfirmation", - areaName: UserConstants.Features.Users, - pattern: _userOptions.ChangePasswordConfirmationUrl, - defaults: new - { - controller = _accountControllerName, - action = nameof(AccountController.ChangePasswordConfirmation), - } - ); - - routes.MapAreaControllerRoute( - name: "UsersLogOff", - areaName: UserConstants.Features.Users, - pattern: _userOptions.LogoffPath, - defaults: new - { - controller = _accountControllerName, - action = nameof(AccountController.LogOff), - } - ); - - routes.MapAreaControllerRoute( - name: "ConfirmEmail", - areaName: UserConstants.Features.Users, - pattern: "ConfirmEmail", - defaults: new - { - controller = _emailConfirmationControllerName, - action = nameof(EmailConfirmationController.ConfirmEmail), - } - ); - - routes.MapAreaControllerRoute( - name: "ConfirmEmailSent", - areaName: UserConstants.Features.Users, - pattern: "ConfirmEmailSent", - defaults: new - { - controller = _emailConfirmationControllerName, - action = nameof(EmailConfirmationController.ConfirmEmailSent), - } - ); - - builder.UseAuthorization(); - } - public override void ConfigureServices(IServiceCollection services) { services.AddDataMigration(); @@ -237,6 +164,79 @@ public override void ConfigureServices(IServiceCollection services) services.AddRecipeExecutionStep(); services.AddScoped, LoginFormDisplayDriver>(); } + + public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + _userOptions ??= serviceProvider.GetRequiredService>().Value; + + routes.MapAreaControllerRoute( + name: "Login", + areaName: UserConstants.Features.Users, + pattern: _userOptions.LoginPath, + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.Login), + } + ); + + routes.MapAreaControllerRoute( + name: "ChangePassword", + areaName: UserConstants.Features.Users, + pattern: _userOptions.ChangePasswordUrl, + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.ChangePassword), + } + ); + + routes.MapAreaControllerRoute( + name: "ChangePasswordConfirmation", + areaName: UserConstants.Features.Users, + pattern: _userOptions.ChangePasswordConfirmationUrl, + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.ChangePasswordConfirmation), + } + ); + + routes.MapAreaControllerRoute( + name: "UsersLogOff", + areaName: UserConstants.Features.Users, + pattern: _userOptions.LogoffPath, + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.LogOff), + } + ); + + routes.MapAreaControllerRoute( + name: "ConfirmEmail", + areaName: UserConstants.Features.Users, + pattern: "ConfirmEmail", + defaults: new + { + controller = _emailConfirmationControllerName, + action = nameof(EmailConfirmationController.ConfirmEmail), + } + ); + + routes.MapAreaControllerRoute( + name: "ConfirmEmailSent", + areaName: UserConstants.Features.Users, + pattern: "ConfirmEmailSent", + defaults: new + { + controller = _emailConfirmationControllerName, + action = nameof(EmailConfirmationController.ConfirmEmailSent), + } + ); + + builder.UseAuthorization(); + } } [Feature(UserConstants.Features.ExternalAuthentication)] @@ -251,6 +251,12 @@ public override void ConfigureServices(IServiceCollection services) services.AddSiteDisplayDriver(); services.AddSiteDisplayDriver(); services.AddSiteDisplayDriver(); + services.AddTransient, ExternalUserLoginOptionsConfigurations>(); + services.PostConfigure(options => + { + // When this feature is enabled, always ensure that site-registration is allowed. + options.AllowSiteRegistration = true; + }); } public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) @@ -413,6 +419,21 @@ public sealed class RegistrationStartup : StartupBase private const string RegistrationPending = nameof(RegistrationController.RegistrationPending); private const string RegistrationControllerName = "Registration"; + public override void ConfigureServices(IServiceCollection services) + { + services.Configure(o => + { + o.MemberAccessStrategy.Register(); + }); + + services.AddSiteDisplayDriver(); + services.AddNavigationProvider(); + + services.AddScoped, RegisterUserLoginFormDisplayDriver>(); + services.AddScoped, RegisterUserFormDisplayDriver>(); + services.AddTransient, RegistrationOptionsConfigurations>(); + } + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { routes.MapAreaControllerRoute( @@ -448,20 +469,6 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro } ); } - - public override void ConfigureServices(IServiceCollection services) - { - services.Configure(o => - { - o.MemberAccessStrategy.Register(); - }); - - services.AddSiteDisplayDriver(); - services.AddNavigationProvider(); - - services.AddScoped, RegisterUserLoginFormDisplayDriver>(); - services.AddScoped, RegisterUserFormDisplayDriver>(); - } } [Feature(UserConstants.Features.UserRegistration)] @@ -483,6 +490,25 @@ public sealed class ResetPasswordStartup : StartupBase private const string ResetPasswordConfirmationPath = "ResetPasswordConfirmation"; private const string ResetPasswordControllerName = "ResetPassword"; + public override void ConfigureServices(IServiceCollection services) + { + services.AddTransient, PasswordResetIdentityOptionsConfigurations>() + .AddTransient() + .AddOptions(); + + services.Configure(o => + { + o.MemberAccessStrategy.Register(); + }); + + services.AddSiteDisplayDriver(); + services.AddNavigationProvider(); + + services.AddScoped, ResetPasswordFormDisplayDriver>(); + services.AddScoped, ForgotPasswordLoginFormDisplayDriver>(); + services.AddScoped, ForgotPasswordFormDisplayDriver>(); + } + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { routes.MapAreaControllerRoute( @@ -526,25 +552,6 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro } ); } - - public override void ConfigureServices(IServiceCollection services) - { - services.AddTransient, PasswordResetIdentityOptionsConfigurations>() - .AddTransient() - .AddOptions(); - - services.Configure(o => - { - o.MemberAccessStrategy.Register(); - }); - - services.AddSiteDisplayDriver(); - services.AddNavigationProvider(); - - services.AddScoped, ResetPasswordFormDisplayDriver>(); - services.AddScoped, ForgotPasswordLoginFormDisplayDriver>(); - services.AddScoped, ForgotPasswordFormDisplayDriver>(); - } } [Feature(UserConstants.Features.ResetPassword)] diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginOptions.cs new file mode 100644 index 00000000000..a92c138097c --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginOptions.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Users.Models; + +public class ExternalUserLoginOptions +{ + public bool UseExternalProviderIfOnlyOneDefined { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/RegistrationOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/RegistrationOptions.cs new file mode 100644 index 00000000000..9c5f6014da4 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/RegistrationOptions.cs @@ -0,0 +1,12 @@ +namespace OrchardCore.Users.Models; + +public class RegistrationOptions +{ + public bool AllowSiteRegistration { get; set; } + + public bool UsersMustValidateEmail { get; set; } + + public bool UsersAreModerated { get; set; } + + public bool UseSiteTheme { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/ExternalUserLoginOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/ExternalUserLoginOptionsConfigurations.cs new file mode 100644 index 00000000000..f87039a6dc8 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/ExternalUserLoginOptionsConfigurations.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Settings; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class ExternalUserLoginOptionsConfigurations : IConfigureOptions +{ + private readonly ISiteService _siteService; + + public ExternalUserLoginOptionsConfigurations(ISiteService siteService) + { + _siteService = siteService; + } + + public void Configure(ExternalUserLoginOptions options) + { + var settings = _siteService.GetSettingsAsync() + .GetAwaiter() + .GetResult(); + + options.UseExternalProviderIfOnlyOneDefined = settings.UseExternalProviderIfOnlyOneDefined; + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/UserManagerHelper.cs b/src/OrchardCore/OrchardCore.Users.Core/UserManagerHelper.cs new file mode 100644 index 00000000000..de2200d0fc4 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/UserManagerHelper.cs @@ -0,0 +1,71 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Settings; +using Microsoft.AspNetCore.Identity; +using OrchardCore.Users.Handlers; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users; + +public static class UserManagerHelper +{ + private static readonly JsonMergeSettings _jsonMergeSettings = new() + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge, + }; + + public static async Task UpdateUserPropertiesAsync(UserManager userManager, User user, UpdateUserContext context) + { + await userManager.AddToRolesAsync(user, context.RolesToAdd.Distinct()); + await userManager.RemoveFromRolesAsync(user, context.RolesToRemove.Distinct()); + + var userNeedUpdate = false; + if (context.PropertiesToUpdate != null) + { + var currentProperties = user.Properties.DeepClone(); + user.Properties.Merge(context.PropertiesToUpdate, _jsonMergeSettings); + userNeedUpdate = !JsonNode.DeepEquals(currentProperties, user.Properties); + } + + var currentClaims = user.UserClaims + .Where(x => !string.IsNullOrEmpty(x.ClaimType)) + .DistinctBy(x => new { x.ClaimType, x.ClaimValue }) + .ToList(); + + var claimsChanged = false; + if (context.ClaimsToRemove?.Count > 0) + { + var claimsToRemove = context.ClaimsToRemove.ToHashSet(); + foreach (var item in claimsToRemove) + { + var exists = currentClaims.FirstOrDefault(claim => claim.ClaimType == item.ClaimType && claim.ClaimValue == item.ClaimValue); + if (exists is not null) + { + currentClaims.Remove(exists); + claimsChanged = true; + } + } + } + + if (context.ClaimsToUpdate?.Count > 0) + { + foreach (var item in context.ClaimsToUpdate) + { + var existing = currentClaims.FirstOrDefault(claim => claim.ClaimType == item.ClaimType && claim.ClaimValue == item.ClaimValue); + if (existing is null) + { + currentClaims.Add(item); + claimsChanged = true; + } + } + } + + if (claimsChanged) + { + user.UserClaims = currentClaims; + userNeedUpdate = true; + } + + return userNeedUpdate; + } +} diff --git a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs index 01353e526ee..8bf2f2523ba 100644 --- a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs +++ b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs @@ -92,7 +92,7 @@ await context.UsingTenantScopeAsync(async scope => }; scriptExternalLoginEventHandler.UpdateUserInternal(context, loginSettings); - if (await AccountController.UpdateUserPropertiesAsync(userManager, user, context)) + if (await UserManagerHelper.UpdateUserPropertiesAsync(userManager, user, context)) { await userManager.UpdateAsync(user); } @@ -150,7 +150,7 @@ await context.UsingTenantScopeAsync(async scope => }; scriptExternalLoginEventHandler.UpdateUserInternal(updateContext, loginSettings); - if (await AccountController.UpdateUserPropertiesAsync(userManager, user, updateContext)) + if (await UserManagerHelper.UpdateUserPropertiesAsync(userManager, user, updateContext)) { await userManager.UpdateAsync(user); } From 11bdb88a70004c4addb4713864cc6eaaae0e8b96 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 09:51:54 -0700 Subject: [PATCH 10/29] Update release notes. --- src/docs/reference/modules/Users/README.md | 3 ++- src/docs/releases/2.1.0.md | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/docs/reference/modules/Users/README.md b/src/docs/reference/modules/Users/README.md index 23298f9c370..16f301772e6 100644 --- a/src/docs/reference/modules/Users/README.md +++ b/src/docs/reference/modules/Users/README.md @@ -7,7 +7,8 @@ The Users module enables authentication UI and user management. The module contains the following features apart from the base feature: - Users Change Email: Allows users to change their email address. -- Users Registration: Allows external users to sign up to the site and ask to confirm their email. +- Users Registration: Allows new users to sign up to the site and ask to confirm their email. +- External User Authentication: Enables a way to authenticate users using an external identity provider. - Reset Password: Allows users to reset their password. - User Time Zone: Provides a way to set the time zone per user. - Custom User Settings: See [its own documentation page](CustomUserSettings/README.md). diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 321c715edb4..2c2269841f9 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -23,6 +23,12 @@ Additionally, the following properties have been moved from `RegistrationSetting The `UsersCanRegister` property in the `RegistrationSettings` class has been marked obsolete, and a new property, `AllowSiteRegistration`, has been introduced. To enable site registration, set `AllowSiteRegistration` to `true`. For external registration, ensure the **External User Authentication** feature is enabled. +!!! note + When configuring `LoginSettings` or `RegistrationSettings` in a recipe, ensure that the recipe's properties are updated to reflect the new settings classes. + +!!! note + When the **External User Authentication** feature is enabled, the `/Register` endpoint will always become available. + ### New 'Azure Communication SMS' feature A new feature was added to allow you to send SMS messages using Azure Communication Services (ACS). Simply enable it then navigate to the admin dashboard > `Configurations` >> `Settings` >> `SMS` to configure the provider. For more information you can refer to the [docs](../reference/modules/Sms.Azure/README.md). From 6b509d2d32153bf4562889d605c8f6f63861dd04 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 10:17:07 -0700 Subject: [PATCH 11/29] seal class --- .../Services/RegistrationOptionsConfigurations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs index 22e503a8ac4..f47a847a598 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs @@ -4,7 +4,7 @@ namespace OrchardCore.Users.Services; -public class RegistrationOptionsConfigurations : IConfigureOptions +public sealed class RegistrationOptionsConfigurations : IConfigureOptions { private readonly ISiteService _siteService; From 9eb21ec057751d5e1fe92212822b799baa864bab Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 17:04:36 -0700 Subject: [PATCH 12/29] Update src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs index 395e11e3802..509f314ccee 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs @@ -22,7 +22,7 @@ [assembly: Feature( Id = UserConstants.Features.ExternalAuthentication, Name = "External User Authentication", - Description = "Provides a way to allow authentication using identity provider.", + Description = "Provides a way to allow authentication using an external identity provider.", Dependencies = [ UserConstants.Features.Users, From 34ee37a3391745787d83abd1765cffcadc5bd3c1 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 17:12:43 -0700 Subject: [PATCH 13/29] update titles --- .../Views/ExternalAuthenticationSettings.Edit.cshtml | 2 +- .../Views/ExternalUserLoginSettings.Edit.cshtml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthenticationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthenticationSettings.Edit.cshtml index 64cd9ab541e..2324500ac82 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthenticationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthenticationSettings.Edit.cshtml @@ -9,7 +9,7 @@ -
@T["External Authentication"] @T["Settings when registering with external authentication providers"]
+
@T["Configuring External Authentication for User Registration with Third-Party Providers"]
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml index 7841ccd2332..bc311cb80c9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml @@ -2,6 +2,8 @@ @model ExternalUserLoginSettings +
@T["Configuring External Authentication for User Registration with Third-Party Providers"]
+
From b0feaa388578acbdc93386f281484d8b20652681 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 17:13:16 -0700 Subject: [PATCH 14/29] update --- .../Views/ExternalUserLoginSettings.Edit.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml index bc311cb80c9..e6cbbdaf91c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml @@ -2,7 +2,7 @@ @model ExternalUserLoginSettings -
@T["Configuring External Authentication for User Registration with Third-Party Providers"]
+
@T["Configuring External Authentication for User Login with Third-Party Providers"]
From e08d177df036d84e1d306bbba9b226aa9dcb69d4 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 18:32:20 -0700 Subject: [PATCH 15/29] Update 2.1.0.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- src/docs/releases/2.1.0.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 2c2269841f9..7b0b9014cee 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -8,7 +8,9 @@ Release date: Not yet released A new feature, **External User Authentication**, has been added. This feature was separated from the existing **Users** feature to provide better dependency management and offer an option to disable external authentication by default. -After upgrading the site, a fast-forward option allows you to enable this feature automatically when needed. As a result, the following settings have been relocated to new classes: +After upgrading the site, a fast-forward option allows you to enable this feature automatically when needed. No action is needed, everything will be migrated automatically. + +As a result, the following settings have been relocated to new classes: - The properties `UseScriptToSyncRoles` and `SyncRolesScript` have been moved from `LoginSettings` to a new class, `ExternalUserRoleLoginSettings`. - The property `UseExternalProviderIfOnlyOneDefined` has been moved from `LoginSettings` to `ExternalUserLoginSettings`. From 98754f71d6c323fa8a5313292b245ff8bdfa6f01 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 20:10:18 -0700 Subject: [PATCH 16/29] add settings to the UI --- .../Views/RegistrationSettings.Edit.cshtml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml index d4a06ff957e..6682cbd7400 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml @@ -9,6 +9,14 @@ +
+
+ + + +
+
+
From a85acdb37d8c0e33c31d7b44d988ca334addda83 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 16 Sep 2024 20:13:26 -0700 Subject: [PATCH 17/29] update docs --- src/docs/releases/2.1.0.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 2c2269841f9..741a4a8d4d1 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -8,6 +8,9 @@ Release date: Not yet released A new feature, **External User Authentication**, has been added. This feature was separated from the existing **Users** feature to provide better dependency management and offer an option to disable external authentication by default. +!!! note + When the **External User Authentication** feature is enabled, the `/Register` endpoint will always become available. + After upgrading the site, a fast-forward option allows you to enable this feature automatically when needed. As a result, the following settings have been relocated to new classes: - The properties `UseScriptToSyncRoles` and `SyncRolesScript` have been moved from `LoginSettings` to a new class, `ExternalUserRoleLoginSettings`. @@ -26,9 +29,6 @@ The `UsersCanRegister` property in the `RegistrationSettings` class has been mar !!! note When configuring `LoginSettings` or `RegistrationSettings` in a recipe, ensure that the recipe's properties are updated to reflect the new settings classes. -!!! note - When the **External User Authentication** feature is enabled, the `/Register` endpoint will always become available. - ### New 'Azure Communication SMS' feature A new feature was added to allow you to send SMS messages using Azure Communication Services (ACS). Simply enable it then navigate to the admin dashboard > `Configurations` >> `Settings` >> `SMS` to configure the provider. For more information you can refer to the [docs](../reference/modules/Sms.Azure/README.md). From 210b025e5cf67425900975917f3392d1338d5e22 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 06:12:10 -0700 Subject: [PATCH 18/29] Simplify settings and fix some bugs --- .../Controllers/AccountController.cs | 19 ++-- ...s => ExternalAuthenticationsController.cs} | 33 ++++--- ...cs => ExternalAuthenticationMigrations.cs} | 8 +- ... => ExternalLoginSettingsDisplayDriver.cs} | 14 +-- ...ernalRegistrationSettingsDisplayDriver.cs} | 12 +-- ...ernalUserRoleLoginSettingsDisplayDriver.cs | 49 --------- .../Drivers/UserMenuDisplayDriver.cs | 30 +++--- .../Extensions/ControllerExtensions.cs | 9 +- .../ScriptExternalLoginEventHandler.cs | 10 +- ...ngs.cs => ExternalRegistrationSettings.cs} | 4 +- .../Services/UsersThemeSelector.cs | 7 +- .../OrchardCore.Users/Startup.cs | 18 ++-- .../Views/Account/Login.cshtml | 2 +- .../ExternalLogins.cshtml | 0 .../LinkExternalLogin.cshtml | 2 +- .../RegisterExternalLogin.cshtml | 2 +- ...html => ExternalLoginSettings.Edit.cshtml} | 99 +++++++++++-------- ... ExternalRegistrationSettings.Edit.cshtml} | 10 +- .../ExternalUserLoginSettings.Edit.cshtml | 14 --- .../Views/UserMenuItems-ExternalLogins.cshtml | 2 +- ...oginOptions.cs => ExternalLoginOptions.cs} | 2 +- .../Models/ExternalLoginSettings.cs | 10 ++ .../Models/ExternalUserLoginSettings.cs | 6 -- .../Models/ExternalUserRoleLoginSettings.cs | 8 -- ... => ExternalLoginOptionsConfigurations.cs} | 8 +- src/docs/releases/2.1.0.md | 24 ++--- .../AccountControllerTests.cs | 12 +-- 27 files changed, 186 insertions(+), 228 deletions(-) rename src/OrchardCore.Modules/OrchardCore.Users/Controllers/{ExternalAuthenticationController.cs => ExternalAuthenticationsController.cs} (97%) rename src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/{ExternalUserMigrations.cs => ExternalAuthenticationMigrations.cs} (87%) rename src/OrchardCore.Modules/OrchardCore.Users/Drivers/{ExternalUserLoginSettingsDisplayDriver.cs => ExternalLoginSettingsDisplayDriver.cs} (74%) rename src/OrchardCore.Modules/OrchardCore.Users/Drivers/{ExternalAuthenticationSettingsDisplayDriver.cs => ExternalRegistrationSettingsDisplayDriver.cs} (79%) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs rename src/OrchardCore.Modules/OrchardCore.Users/Models/{ExternalAuthenticationSettings.cs => ExternalRegistrationSettings.cs} (74%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/{Account => ExternalAuthentications}/ExternalLogins.cshtml (100%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/{Account => ExternalAuthentications}/LinkExternalLogin.cshtml (88%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/{Account => ExternalAuthentications}/RegisterExternalLogin.cshtml (93%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/{ExternalUserRoleLoginSettings.Edit.cshtml => ExternalLoginSettings.Edit.cshtml} (50%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/{ExternalAuthenticationSettings.Edit.cshtml => ExternalRegistrationSettings.Edit.cshtml} (97%) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml rename src/OrchardCore/OrchardCore.Users.Core/Models/{ExternalUserLoginOptions.cs => ExternalLoginOptions.cs} (73%) create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/ExternalLoginSettings.cs delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginSettings.cs delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserRoleLoginSettings.cs rename src/OrchardCore/OrchardCore.Users.Core/Services/{ExternalUserLoginOptionsConfigurations.cs => ExternalLoginOptionsConfigurations.cs} (55%) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index ce7227f8a37..4eaaac1c0d0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -27,7 +27,7 @@ namespace OrchardCore.Users.Controllers; public sealed class AccountController : AccountBaseController { [Obsolete("This property will be removed in v3. Instead use ExternalAuthenticationController.DefaultExternalLoginProtector")] - public const string DefaultExternalLoginProtector = ExternalAuthenticationController.DefaultExternalLoginProtector; + public const string DefaultExternalLoginProtector = ExternalAuthenticationsController.DefaultExternalLoginProtector; private readonly IUserService _userService; private readonly SignInManager _signInManager; @@ -35,7 +35,7 @@ public sealed class AccountController : AccountBaseController private readonly ILogger _logger; private readonly ISiteService _siteService; private readonly IEnumerable _accountEvents; - private readonly ExternalUserLoginSettings _externalUserLoginSettings; + private readonly ExternalLoginOptions _externalLoginOptions; private readonly RegistrationOptions _registrationOptions; private readonly IDataProtectionProvider _dataProtectionProvider; private readonly IDisplayManager _loginFormDisplayManager; @@ -57,7 +57,7 @@ public AccountController( IStringLocalizer stringLocalizer, IEnumerable accountEvents, IOptions registrationOptions, - IOptions externalUserLoginSettings, + IOptions externalLoginOptions, INotifier notifier, IClock clock, IDistributedCache distributedCache, @@ -71,7 +71,7 @@ public AccountController( _logger = logger; _siteService = siteService; _accountEvents = accountEvents; - _externalUserLoginSettings = externalUserLoginSettings.Value; + _externalLoginOptions = externalLoginOptions.Value; _registrationOptions = registrationOptions.Value; _notifier = notifier; _clock = clock; @@ -96,20 +96,23 @@ public async Task Login(string returnUrl = null) // Clear the existing external cookie to ensure a clean login process. await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - if (_externalUserLoginSettings.UseExternalProviderIfOnlyOneDefined) + if (_externalLoginOptions.UseExternalProviderIfOnlyOneDefined) { var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); if (schemes.Count() == 1) { - var dataProtector = _dataProtectionProvider.CreateProtector(ExternalAuthenticationController.DefaultExternalLoginProtector) + var dataProtector = _dataProtectionProvider.CreateProtector(ExternalAuthenticationsController.DefaultExternalLoginProtector) .ToTimeLimitedDataProtector(); var token = Guid.NewGuid(); var expiration = new TimeSpan(0, 0, 5); var protectedToken = dataProtector.Protect(token.ToString(), _clock.UtcNow.Add(expiration)); - await _distributedCache.SetAsync(token.ToString(), token.ToByteArray(), new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = expiration }); + await _distributedCache.SetAsync(token.ToString(), token.ToByteArray(), new DistributedCacheEntryOptions() + { + AbsoluteExpirationRelativeToNow = expiration, + }); - return RedirectToAction(nameof(ExternalAuthenticationController.DefaultExternalLogin), typeof(ExternalAuthenticationController).ControllerName(), new { protectedToken, returnUrl }); + return RedirectToAction(nameof(ExternalAuthenticationsController.DefaultExternalLogin), typeof(ExternalAuthenticationsController).ControllerName(), new { protectedToken, returnUrl }); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs similarity index 97% rename from src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs rename to src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs index 525900d2b3d..d75cc85a9f9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs @@ -21,7 +21,7 @@ namespace OrchardCore.Users.Controllers; [Feature(UserConstants.Features.ExternalAuthentication)] -public sealed class ExternalAuthenticationController : AccountBaseController +public sealed class ExternalAuthenticationsController : AccountBaseController { public const string DefaultExternalLoginProtector = "DefaultExternalLogin"; @@ -34,23 +34,25 @@ public sealed class ExternalAuthenticationController : AccountBaseController private readonly IEnumerable _accountEvents; private readonly IShellFeaturesManager _shellFeaturesManager; private readonly IEnumerable _externalLoginHandlers; + private readonly ExternalLoginOptions _externalLoginOption; private readonly IdentityOptions _identityOptions; internal readonly IHtmlLocalizer H; internal readonly IStringLocalizer S; - public ExternalAuthenticationController( + public ExternalAuthenticationsController( SignInManager signInManager, UserManager userManager, - ILogger logger, + ILogger logger, IDataProtectionProvider dataProtectionProvider, IDistributedCache distributedCache, ISiteService siteService, - IHtmlLocalizer htmlLocalizer, - IStringLocalizer stringLocalizer, + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer, IEnumerable accountEvents, IShellFeaturesManager shellFeaturesManager, IEnumerable externalLoginHandlers, + IOptions externalLoginOption, IOptions identityOptions) { _signInManager = signInManager; @@ -62,18 +64,17 @@ public ExternalAuthenticationController( _accountEvents = accountEvents; _shellFeaturesManager = shellFeaturesManager; _externalLoginHandlers = externalLoginHandlers; + _externalLoginOption = externalLoginOption.Value; _identityOptions = identityOptions.Value; H = htmlLocalizer; S = stringLocalizer; } - [HttpGet] [AllowAnonymous] public async Task DefaultExternalLogin(string protectedToken, string returnUrl = null) { - var loginSettings = await _siteService.GetSettingsAsync(); - if (loginSettings.UseExternalProviderIfOnlyOneDefined) + if (_externalLoginOption.UseExternalProviderIfOnlyOneDefined) { var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); if (schemes.Count() == 1) @@ -115,7 +116,6 @@ public IActionResult ExternalLogin(string provider, string returnUrl = null) return Challenge(properties, provider); } - [HttpGet] [AllowAnonymous] public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) { @@ -190,7 +190,7 @@ public async Task ExternalLoginCallback(string returnUrl = null, return View(nameof(LinkExternalLogin)); } - var settings = await _siteService.GetSettingsAsync(); + var settings = await _siteService.GetSettingsAsync(); var externalLoginViewModel = new RegisterExternalLoginViewModel { @@ -291,7 +291,7 @@ public async Task RegisterExternalLogin(RegisterExternalLoginView ViewData["ReturnUrl"] = returnUrl; ViewData["LoginProvider"] = info.LoginProvider; - var settings = await _siteService.GetSettingsAsync(); + var settings = await _siteService.GetSettingsAsync(); model.NoPassword = settings.NoPassword; model.NoEmail = settings.NoEmail; @@ -435,7 +435,6 @@ public async Task LinkExternalLogin(LinkExternalLoginViewModel mo return RedirectToLogin(); } - [HttpGet] public async Task ExternalLogins() { var user = await _userManager.GetUserAsync(User); @@ -444,11 +443,14 @@ public async Task ExternalLogins() return Forbid(); } - var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) }; + var model = new ExternalLoginsViewModel + { + CurrentLogins = await _userManager.GetLoginsAsync(user), + }; + model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1; model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) .Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) - .ToList(); - model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1; + .ToArray(); CopyTempDataErrorsToModelState(); @@ -469,7 +471,6 @@ public async Task LinkLogin(string provider) return new ChallengeResult(provider, properties); } - [HttpGet] public async Task LinkLoginCallback() { var user = await _userManager.GetUserAsync(User); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalUserMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs similarity index 87% rename from src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalUserMigrations.cs rename to src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs index 5dd72289952..5121cd0b24b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalUserMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs @@ -8,7 +8,7 @@ namespace OrchardCore.Users.DataMigrations; -public sealed class ExternalUserMigrations : DataMigration +public sealed class ExternalAuthenticationMigrations : DataMigration { #pragma warning disable CA1822 // Mark members as static public int Create() @@ -27,7 +27,7 @@ public int Create() site.Put(registrationSettings); - site.Put(new ExternalAuthenticationSettings + site.Put(new ExternalRegistrationSettings { NoUsername = registrationSettings.NoUsernameForExternalUsers, NoEmail = registrationSettings.NoEmailForExternalUsers, @@ -38,9 +38,11 @@ public int Create() var loginSettings = site.As(); - site.Put(new ExternalUserLoginSettings + site.Put(new ExternalLoginSettings { UseExternalProviderIfOnlyOneDefined = loginSettings.UseExternalProviderIfOnlyOneDefined, + UseScriptToSyncProperties = loginSettings.UseScriptToSyncRoles, + SyncPropertiesScript = loginSettings.SyncRolesScript, }); await siteService.UpdateSiteSettingsAsync(site); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalLoginSettingsDisplayDriver.cs similarity index 74% rename from src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs rename to src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalLoginSettingsDisplayDriver.cs index ae98eb8d60b..7b846bed40b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserLoginSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalLoginSettingsDisplayDriver.cs @@ -9,13 +9,13 @@ namespace OrchardCore.Users.Drivers; -public sealed class ExternalUserLoginSettingsDisplayDriver : SiteDisplayDriver +public sealed class ExternalLoginSettingsDisplayDriver : SiteDisplayDriver { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthorizationService _authorizationService; private readonly IShellReleaseManager _shellReleaseManager; - public ExternalUserLoginSettingsDisplayDriver( + public ExternalLoginSettingsDisplayDriver( IHttpContextAccessor httpContextAccessor, IAuthorizationService authorizationService, IShellReleaseManager shellReleaseManager) @@ -28,17 +28,19 @@ public ExternalUserLoginSettingsDisplayDriver( protected override string SettingsGroupId => LoginSettingsDisplayDriver.GroupId; - public override IDisplayResult Edit(ISite site, ExternalUserLoginSettings settings, BuildEditorContext context) + public override IDisplayResult Edit(ISite site, ExternalLoginSettings settings, BuildEditorContext context) { - return Initialize("ExternalUserLoginSettings_Edit", model => + return Initialize("ExternalLoginSettings_Edit", model => { model.UseExternalProviderIfOnlyOneDefined = settings.UseExternalProviderIfOnlyOneDefined; - }).Location("Content:7#General") + model.UseScriptToSyncProperties = settings.UseScriptToSyncProperties; + model.SyncPropertiesScript = settings.SyncPropertiesScript; + }).Location("Content:5#External Login;10") .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.ManageUsers)) .OnGroup(SettingsGroupId); } - public override async Task UpdateAsync(ISite site, ExternalUserLoginSettings settings, UpdateEditorContext context) + public override async Task UpdateAsync(ISite site, ExternalLoginSettings settings, UpdateEditorContext context) { if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, CommonPermissions.ManageUsers)) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalRegistrationSettingsDisplayDriver.cs similarity index 79% rename from src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationSettingsDisplayDriver.cs rename to src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalRegistrationSettingsDisplayDriver.cs index 93008277702..94ebb165930 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalRegistrationSettingsDisplayDriver.cs @@ -8,12 +8,12 @@ namespace OrchardCore.Users.Drivers; -public sealed class ExternalAuthenticationSettingsDisplayDriver : SiteDisplayDriver +public sealed class ExternalRegistrationSettingsDisplayDriver : SiteDisplayDriver { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthorizationService _authorizationService; - public ExternalAuthenticationSettingsDisplayDriver( + public ExternalRegistrationSettingsDisplayDriver( IHttpContextAccessor httpContextAccessor, IAuthorizationService authorizationService) { @@ -24,7 +24,7 @@ public ExternalAuthenticationSettingsDisplayDriver( protected override string SettingsGroupId => RegistrationSettingsDisplayDriver.GroupId; - public override async Task EditAsync(ISite site, ExternalAuthenticationSettings settings, BuildEditorContext context) + public override async Task EditAsync(ISite site, ExternalRegistrationSettings settings, BuildEditorContext context) { var user = _httpContextAccessor.HttpContext?.User; @@ -33,18 +33,18 @@ public override async Task EditAsync(ISite site, ExternalAuthent return null; } - return Initialize("ExternalAuthenticationSettings_Edit", model => + return Initialize("ExternalRegistrationSettings_Edit", model => { model.NoPassword = settings.NoPassword; model.NoUsername = settings.NoUsername; model.NoEmail = settings.NoEmail; model.UseScriptToGenerateUsername = settings.UseScriptToGenerateUsername; model.GenerateUsernameScript = settings.GenerateUsernameScript; - }).Location("Content:10") + }).Location("Content:5#External Authentication;5") .OnGroup(SettingsGroupId); } - public override async Task UpdateAsync(ISite site, ExternalAuthenticationSettings settings, UpdateEditorContext context) + public override async Task UpdateAsync(ISite site, ExternalRegistrationSettings settings, UpdateEditorContext context) { var user = _httpContextAccessor.HttpContext?.User; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs deleted file mode 100644 index 753761ab900..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalUserRoleLoginSettingsDisplayDriver.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using OrchardCore.DisplayManagement.Entities; -using OrchardCore.DisplayManagement.Handlers; -using OrchardCore.DisplayManagement.Views; -using OrchardCore.Settings; -using OrchardCore.Users.Models; - -namespace OrchardCore.Users.Drivers; - -public sealed class ExternalUserRoleLoginSettingsDisplayDriver : SiteDisplayDriver -{ - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IAuthorizationService _authorizationService; - - public ExternalUserRoleLoginSettingsDisplayDriver( - IHttpContextAccessor httpContextAccessor, - IAuthorizationService authorizationService) - { - _httpContextAccessor = httpContextAccessor; - _authorizationService = authorizationService; - } - - protected override string SettingsGroupId - => LoginSettingsDisplayDriver.GroupId; - - public override IDisplayResult Edit(ISite site, ExternalUserRoleLoginSettings settings, BuildEditorContext context) - { - return Initialize("ExternalUserRoleLoginSettings_Edit", model => - { - model.UseScriptToSyncRoles = settings.UseScriptToSyncRoles; - model.SyncRolesScript = settings.SyncRolesScript; - }).Location("Content:10#General") - .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.ManageUsers)) - .OnGroup(SettingsGroupId); - } - - public override async Task UpdateAsync(ISite site, ExternalUserRoleLoginSettings section, UpdateEditorContext context) - { - if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, CommonPermissions.ManageUsers)) - { - return null; - } - - await context.Updater.TryUpdateModelAsync(section, Prefix); - - return await EditAsync(site, section, context); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserMenuDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserMenuDisplayDriver.cs index 9385aefe048..419ac92a746 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserMenuDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserMenuDisplayDriver.cs @@ -8,14 +8,10 @@ namespace OrchardCore.Users.Drivers; public sealed class UserMenuDisplayDriver : DisplayDriver { - private readonly SignInManager _signInManager; private readonly IHttpContextAccessor _httpContextAccessor; - public UserMenuDisplayDriver( - SignInManager signInManager, - IHttpContextAccessor httpContextAccessor) + public UserMenuDisplayDriver(IHttpContextAccessor httpContextAccessor) { - _signInManager = signInManager; _httpContextAccessor = httpContextAccessor; } @@ -37,12 +33,6 @@ public override Task DisplayAsync(UserMenu model, BuildDisplayCo .Location("DetailAdmin", "Content:5") .Differentiator("Profile"), - View("UserMenuItems__ExternalLogins", model) - .RenderWhen(async () => (await _signInManager.GetExternalAuthenticationSchemesAsync()).Any()) - .Location("Detail", "Content:10") - .Location("DetailAdmin", "Content:10") - .Differentiator("ExternalLogins"), - View("UserMenuItems__SignOut", model) .Location("Detail", "Content:100") .Location("DetailAdmin", "Content:100") @@ -59,3 +49,21 @@ public override Task DisplayAsync(UserMenu model, BuildDisplayCo return CombineAsync(results); } } +public sealed class ExternalAuthenticationUserMenuDisplayDriver : DisplayDriver +{ + private readonly SignInManager _signInManager; + + public ExternalAuthenticationUserMenuDisplayDriver(SignInManager signInManager) + { + _signInManager = signInManager; + } + + public override IDisplayResult Display(UserMenu model, BuildDisplayContext context) + { + return View("UserMenuItems__ExternalLogins", model) + .RenderWhen(async () => (await _signInManager.GetExternalAuthenticationSchemesAsync()).Any()) + .Location("Detail", "Content:10") + .Location("DetailAdmin", "Content:10") + .Differentiator("ExternalLogins"); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs index fc0e7a22f11..852b05b4a5a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs @@ -47,19 +47,14 @@ internal static async Task SendEmailAsync(this Controller controller, stri /// internal static async Task RegisterUser(this Controller controller, RegisterUserForm model, string confirmationEmailSubject, ILogger logger) { - var registrationOptions = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>().Value; - - if (!registrationOptions.AllowSiteRegistration) - { - return null; - } - var registrationEvents = controller.ControllerContext.HttpContext.RequestServices.GetServices(); await registrationEvents.InvokeAsync((e, modelState) => e.RegistrationValidationAsync((key, message) => modelState.AddModelError(key, message)), controller.ModelState, logger); if (controller.ModelState.IsValid) { + var registrationOptions = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>().Value; + var userService = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService(); var user = await userService.CreateUserAsync(new User diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs b/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs index 5cc941bc5a8..f8ec263b3c8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs @@ -32,7 +32,7 @@ ILogger logger public async Task GenerateUserName(string provider, IEnumerable claims) { - var registrationSettings = await _siteService.GetSettingsAsync(); + var registrationSettings = await _siteService.GetSettingsAsync(); if (registrationSettings.UseScriptToGenerateUsername) { @@ -58,19 +58,19 @@ public async Task GenerateUserName(string provider, IEnumerable(); + var loginSettings = await _siteService.GetSettingsAsync(); UpdateUserInternal(context, loginSettings); } - public void UpdateUserInternal(UpdateUserContext context, ExternalUserRoleLoginSettings loginSettings) + public void UpdateUserInternal(UpdateUserContext context, ExternalLoginSettings loginSettings) { - if (!loginSettings.UseScriptToSyncRoles) + if (!loginSettings.UseScriptToSyncProperties) { return; } - var script = $"js: function syncRoles(context) {{\n{loginSettings.SyncRolesScript}\n}}\nvar context={JConvert.SerializeObject(context, JOptions.CamelCase)};\nsyncRoles(context);\nreturn context;"; + var script = $"js: function syncRoles(context) {{\n{loginSettings.SyncPropertiesScript}\n}}\nvar context={JConvert.SerializeObject(context, JOptions.CamelCase)};\nsyncRoles(context);\nreturn context;"; dynamic evaluationResult = _scriptingManager.Evaluate(script, null, null, null); context.RolesToAdd.AddRange((evaluationResult.rolesToAdd as object[]).Select(i => i.ToString())); context.RolesToRemove.AddRange((evaluationResult.rolesToRemove as object[]).Select(i => i.ToString())); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalAuthenticationSettings.cs b/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalRegistrationSettings.cs similarity index 74% rename from src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalAuthenticationSettings.cs rename to src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalRegistrationSettings.cs index a9a6ee3f38f..92659a91914 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalAuthenticationSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalRegistrationSettings.cs @@ -1,6 +1,6 @@ -namespace OrchardCore.Users.Models; +namespace OrchardCore.Users.Models; -public class ExternalAuthenticationSettings +public class ExternalRegistrationSettings { public bool NoPassword { get; set; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs index d6dc7dc9444..6e898af65a4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; using OrchardCore.Admin; using OrchardCore.DisplayManagement.Theming; using OrchardCore.Settings; @@ -18,15 +19,18 @@ public class UsersThemeSelector : IThemeSelector { private readonly ISiteService _siteService; private readonly IAdminThemeService _adminThemeService; + private readonly RegistrationOptions _registrationOptions; private readonly IHttpContextAccessor _httpContextAccessor; public UsersThemeSelector( ISiteService siteService, IAdminThemeService adminThemeService, + IOptions registrationOptions, IHttpContextAccessor httpContextAccessor) { _siteService = siteService; _adminThemeService = adminThemeService; + _registrationOptions = registrationOptions.Value; _httpContextAccessor = httpContextAccessor; } @@ -41,6 +45,7 @@ public async Task GetThemeAsync() switch (routeValues["controller"]?.ToString()) { case "Account": + case "ExternalAuthentications": useSiteTheme = (await _siteService.GetSettingsAsync()).UseSiteTheme; break; case "TwoFactorAuthentication": @@ -64,7 +69,7 @@ public async Task GetThemeAsync() } break; case "Registration": - useSiteTheme = (await _siteService.GetSettingsAsync()).UseSiteTheme; + useSiteTheme = _registrationOptions.UseSiteTheme; break; case "ResetPassword": useSiteTheme = (await _siteService.GetSettingsAsync()).UseSiteTheme; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 42ff485de2e..5c81c3275e5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -68,7 +68,7 @@ public Startup(ShellSettings shellSettings) public override void ConfigureServices(IServiceCollection services) { - services.AddDataMigration(); + services.AddDataMigration(); services.Configure(userOptions => { @@ -248,15 +248,11 @@ public sealed class ExternalAuthenticationStartup : StartupBase public override void ConfigureServices(IServiceCollection services) { - services.AddSiteDisplayDriver(); - services.AddSiteDisplayDriver(); - services.AddSiteDisplayDriver(); - services.AddTransient, ExternalUserLoginOptionsConfigurations>(); - services.PostConfigure(options => - { - // When this feature is enabled, always ensure that site-registration is allowed. - options.AllowSiteRegistration = true; - }); + services.AddNavigationProvider(); + services.AddScoped, ExternalAuthenticationUserMenuDisplayDriver>(); + services.AddSiteDisplayDriver(); + services.AddSiteDisplayDriver(); + services.AddTransient, ExternalLoginOptionsConfigurations>(); } public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) @@ -270,7 +266,7 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde defaults: new { controller = _accountControllerName, - action = nameof(ExternalAuthenticationController.ExternalLogins), + action = nameof(ExternalAuthenticationsController.ExternalLogins), } ); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/Login.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/Login.cshtml index 070c6d2614f..81b24312ee3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/Login.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/Login.cshtml @@ -41,7 +41,7 @@

@T["Use another service to log in"]


-
+ @foreach (var provider in loginProviders) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/ExternalLogins.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthentications/ExternalLogins.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Account/ExternalLogins.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthentications/ExternalLogins.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/LinkExternalLogin.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthentications/LinkExternalLogin.cshtml similarity index 88% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Account/LinkExternalLogin.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthentications/LinkExternalLogin.cshtml index d31e28d1d31..237a8c678ca 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/LinkExternalLogin.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthentications/LinkExternalLogin.cshtml @@ -4,7 +4,7 @@ ViewLayout = "Layout__Login"; } - +

@T["Link your account."]

@T["You've successfully authenticated with {0}. You already have an account that can be linked with this external login. Enter your local account password and click the Register button to link the accounts and finish logging in.", ViewData["LoginProvider"]] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/RegisterExternalLogin.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthentications/RegisterExternalLogin.cshtml similarity index 93% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Account/RegisterExternalLogin.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthentications/RegisterExternalLogin.cshtml index 2b52597f6d1..8c0bdede6e1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/RegisterExternalLogin.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalAuthentications/RegisterExternalLogin.cshtml @@ -5,7 +5,7 @@ ViewLayout = "Layout__Login"; } - +

@T["Create a new account."]

@T["You've successfully authenticated with {0}. Please fill the form below and click the Register button to complete the registration.", ViewData["LoginProvider"]] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserRoleLoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalLoginSettings.Edit.cshtml similarity index 50% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserRoleLoginSettings.Edit.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalLoginSettings.Edit.cshtml index 71d634522ce..2da59db6e1b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserRoleLoginSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalLoginSettings.Edit.cshtml @@ -1,20 +1,33 @@ -@model OrchardCore.Users.Models.ExternalUserRoleLoginSettings +@using OrchardCore.Users.Handlers +@model OrchardCore.Users.Models.ExternalLoginSettings -

+
@T["Configuring External Authentication for User Login with Third-Party Providers"]
+ +
+
+ + + + @T["If only one external provider is defined, auto challenge the provider to login. You should also configure registration options"] +
+
+ +
- - - - @T["If selected, any IExternalLoginEventHandlers defined in modules are not triggered"] + + + + + @T["If selected, none of the implemented {0}, if any, will be triggered.", nameof(IExternalLoginEventHandler)] +
-
+
-
+
-
- +
+
@@ -52,34 +65,34 @@ // don't format here. const suggestion = `/** - * Use the loginProvider and externalClaims properties of the context variable to inspect - * who authenticated the user and with what claims. Check currentRoles property and apply - * your business logic to fill the rolesToAdd and rolesToRemove arrays in order to update - */ - const context = {} as Context; - type Context = { - readonly user: { - userName: string; - userRoles: string[]; - userClaims: { claimType: string; claimValue: string; }[]; - userProperties: { [key: string]: string }; - }, - readonly loginProvider: string, - rolesToAdd: string[]; - rolesToRemove: string[]; - claimsToUpdate: { claimType: string; claimValue: string; }[]; - claimsToRemove: { claimType: string; claimValue: string; }[]; - propertiesToUpdate: { [key: string]: string }; - externalClaims: readonly [{ - valueType: string; - type: string; - value: string; - subject: string; - issuer: string; - originalIssuer: string; - properties: { [key: string]: string }; - }] - }` + * Use the loginProvider and externalClaims properties of the context variable to inspect + * who authenticated the user and with what claims. Check currentRoles property and apply + * your business logic to fill the rolesToAdd and rolesToRemove arrays in order to update + */ + const context = {} as Context; + type Context = { + readonly user: { + userName: string; + userRoles: string[]; + userClaims: { claimType: string; claimValue: string; }[]; + userProperties: { [key: string]: string }; + }, + readonly loginProvider: string, + rolesToAdd: string[]; + rolesToRemove: string[]; + claimsToUpdate: { claimType: string; claimValue: string; }[]; + claimsToRemove: { claimType: string; claimValue: string; }[]; + propertiesToUpdate: { [key: string]: string }; + externalClaims: readonly [{ + valueType: string; + type: string; + value: string; + subject: string; + issuer: string; + originalIssuer: string; + properties: { [key: string]: string }; + }] + }` var codeEditor; document.addEventListener('DOMContentLoaded', function () { require(['vs/editor/editor.main'], function () { @@ -98,7 +111,7 @@ } setTheme(); - var editor = monaco.editor.create(document.getElementById('@Html.IdFor(x => x.SyncRolesScript)_editor'), { + var editor = monaco.editor.create(document.getElementById('@Html.IdFor(x => x.SyncPropertiesScript)_editor'), { automaticLayout: true, language: "javascript" }); @@ -120,7 +133,7 @@ handleMouseWheel: false } }); - var textArea = document.getElementById('@Html.IdFor(x => x.SyncRolesScript)'); + var textArea = document.getElementById('@Html.IdFor(x => x.SyncPropertiesScript)'); if (!textArea.value) { resetScript(); } else { @@ -137,7 +150,7 @@ @@ -13,7 +13,7 @@
- + @T["When a new user logs in with an external provider, they are not required to provide a local username. You can customize how it works by providing an IExternalLoginEventHandler or writing a script."] @@ -21,7 +21,7 @@
- + @T["When a new user logs in with an external provider and the email claim is defined, they are not required to provide a local email address."] @@ -29,9 +29,9 @@
- + - + @T["When a new user logs in with an external provider, they are not required to provide a local password."]
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml deleted file mode 100644 index e6cbbdaf91c..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalUserLoginSettings.Edit.cshtml +++ /dev/null @@ -1,14 +0,0 @@ -@using OrchardCore.Users.Models - -@model ExternalUserLoginSettings - -
@T["Configuring External Authentication for User Login with Third-Party Providers"]
- -
-
- - - - @T["If only one external provider is defined, auto challenge the provider to login. You should also configure registration options"] -
-
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ExternalLogins.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ExternalLogins.cshtml index f52842f9684..3e0a99a3145 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ExternalLogins.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ExternalLogins.cshtml @@ -1,5 +1,5 @@
  • - + @T["External Logins"]
  • diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalLoginOptions.cs similarity index 73% rename from src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginOptions.cs rename to src/OrchardCore/OrchardCore.Users.Core/Models/ExternalLoginOptions.cs index a92c138097c..550ca9469aa 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalLoginOptions.cs @@ -1,6 +1,6 @@ namespace OrchardCore.Users.Models; -public class ExternalUserLoginOptions +public class ExternalLoginOptions { public bool UseExternalProviderIfOnlyOneDefined { get; set; } } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalLoginSettings.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalLoginSettings.cs new file mode 100644 index 00000000000..332fa32a036 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalLoginSettings.cs @@ -0,0 +1,10 @@ +namespace OrchardCore.Users.Models; + +public class ExternalLoginSettings +{ + public bool UseExternalProviderIfOnlyOneDefined { get; set; } + + public bool UseScriptToSyncProperties { get; set; } + + public string SyncPropertiesScript { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginSettings.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginSettings.cs deleted file mode 100644 index 5d37aed7c96..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserLoginSettings.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OrchardCore.Users.Models; - -public class ExternalUserLoginSettings -{ - public bool UseExternalProviderIfOnlyOneDefined { get; set; } -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserRoleLoginSettings.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserRoleLoginSettings.cs deleted file mode 100644 index 2d054508dc2..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/ExternalUserRoleLoginSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OrchardCore.Users.Models; - -public class ExternalUserRoleLoginSettings -{ - public bool UseScriptToSyncRoles { get; set; } - - public string SyncRolesScript { get; set; } -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/ExternalUserLoginOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/ExternalLoginOptionsConfigurations.cs similarity index 55% rename from src/OrchardCore/OrchardCore.Users.Core/Services/ExternalUserLoginOptionsConfigurations.cs rename to src/OrchardCore/OrchardCore.Users.Core/Services/ExternalLoginOptionsConfigurations.cs index f87039a6dc8..f11a5e9292d 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/ExternalUserLoginOptionsConfigurations.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/ExternalLoginOptionsConfigurations.cs @@ -4,18 +4,18 @@ namespace OrchardCore.Users.Services; -public sealed class ExternalUserLoginOptionsConfigurations : IConfigureOptions +public sealed class ExternalLoginOptionsConfigurations : IConfigureOptions { private readonly ISiteService _siteService; - public ExternalUserLoginOptionsConfigurations(ISiteService siteService) + public ExternalLoginOptionsConfigurations(ISiteService siteService) { _siteService = siteService; } - public void Configure(ExternalUserLoginOptions options) + public void Configure(ExternalLoginOptions options) { - var settings = _siteService.GetSettingsAsync() + var settings = _siteService.GetSettingsAsync() .GetAwaiter() .GetResult(); diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 741a4a8d4d1..f06032648d6 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -8,23 +8,23 @@ Release date: Not yet released A new feature, **External User Authentication**, has been added. This feature was separated from the existing **Users** feature to provide better dependency management and offer an option to disable external authentication by default. -!!! note - When the **External User Authentication** feature is enabled, the `/Register` endpoint will always become available. - After upgrading the site, a fast-forward option allows you to enable this feature automatically when needed. As a result, the following settings have been relocated to new classes: -- The properties `UseScriptToSyncRoles` and `SyncRolesScript` have been moved from `LoginSettings` to a new class, `ExternalUserRoleLoginSettings`. -- The property `UseExternalProviderIfOnlyOneDefined` has been moved from `LoginSettings` to `ExternalUserLoginSettings`. +- The properties `UseScriptToSyncRoles` and `SyncRolesScript` have been moved from `LoginSettings` to a new class, `ExternalLoginSettings`. +- The property `UseExternalProviderIfOnlyOneDefined` has been moved from `LoginSettings` to `ExternalLoginSettings`. + +Additionally, the following properties have been moved from `RegistrationSettings` to `ExternalRegistrationSettings`: -Additionally, the following properties have been moved from `RegistrationSettings` to `ExternalAuthenticationSettings`: +- `NoPasswordForExternalUsers` is now `ExternalRegistrationSettings.NoPassword`. +- `NoUsernameForExternalUsers` is now `ExternalRegistrationSettings.NoUsername`. +- `NoEmailForExternalUsers` is now `ExternalRegistrationSettings.NoEmail`. +- `UseScriptToGenerateUsername` is now `ExternalRegistrationSettings.UseScriptToGenerateUsername`. +- `GenerateUsernameScript` is now `ExternalRegistrationSettings.GenerateUsernameScript`. -- `NoPasswordForExternalUsers` is now `ExternalAuthenticationSettings.NoPassword`. -- `NoUsernameForExternalUsers` is now `ExternalAuthenticationSettings.NoUsername`. -- `NoEmailForExternalUsers` is now `ExternalAuthenticationSettings.NoEmail`. -- `UseScriptToGenerateUsername` is now `ExternalAuthenticationSettings.UseScriptToGenerateUsername`. -- `GenerateUsernameScript` is now `ExternalAuthenticationSettings.GenerateUsernameScript`. +- Moreover, the following properties have been moved from `LoginSettings` to `ExternalLoginSettings`: -The `UsersCanRegister` property in the `RegistrationSettings` class has been marked obsolete, and a new property, `AllowSiteRegistration`, has been introduced. To enable site registration, set `AllowSiteRegistration` to `true`. For external registration, ensure the **External User Authentication** feature is enabled. +- `UseScriptToSyncRoles` is now `ExternalLoginSettings.UseScriptToSyncProperties`. +- `SyncRolesScript` is now `ExternalLoginSettings.SyncPropertiesScript`. !!! note When configuring `LoginSettings` or `RegistrationSettings` in a recipe, ensure that the recipe's properties are updated to reflect the new settings classes. diff --git a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs index 8bf2f2523ba..3a96f2e78c7 100644 --- a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs +++ b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs @@ -61,10 +61,10 @@ await context.UsingTenantScopeAsync(async scope => var scriptExternalLoginEventHandler = scope.ServiceProvider.GetServices() .FirstOrDefault(x => x.GetType() == typeof(ScriptExternalLoginEventHandler)) as ScriptExternalLoginEventHandler; - var loginSettings = new ExternalUserRoleLoginSettings + var loginSettings = new ExternalLoginSettings { - UseScriptToSyncRoles = true, - SyncRolesScript = """ + UseScriptToSyncProperties = true, + SyncPropertiesScript = """ if(!context.user.userClaims?.find(x=> x.claimType=="lastName" && claimValue=="Zhang")){ context.claimsToUpdate.push({claimType:"lastName", claimValue:"Zhang"}); } @@ -128,10 +128,10 @@ await context.UsingTenantScopeAsync(async scope => var scriptExternalLoginEventHandler = scope.ServiceProvider.GetServices() .FirstOrDefault(x => x.GetType() == typeof(ScriptExternalLoginEventHandler)) as ScriptExternalLoginEventHandler; - var loginSettings = new ExternalUserRoleLoginSettings + var loginSettings = new ExternalLoginSettings { - UseScriptToSyncRoles = true, - SyncRolesScript = """ + UseScriptToSyncProperties = true, + SyncPropertiesScript = """ context.claimsToUpdate.push({claimType:"displayName", claimValue:"Sam Zhang"}); context.claimsToUpdate.push({claimType:"firstName", claimValue:"Sam"}); context.claimsToUpdate.push({claimType:"lastName", claimValue:"Zhang"}); From 03245ebf00951b3b7b8cd76c17a687dbabb11cc9 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 06:13:54 -0700 Subject: [PATCH 19/29] simplify --- .../OrchardCore.Users/Views/Account/Login.cshtml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/Login.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/Login.cshtml index 81b24312ee3..d50f998d470 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/Login.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/Account/Login.cshtml @@ -6,7 +6,6 @@ @using OrchardCore.Users.Models @inject SignInManager SignInManager -@inject ISiteService SiteService @inject UserManager UserManager @inject IDisplayManager LoginFormDisplayManager @inject IUpdateModelAccessor UpdateModelAccessor @@ -15,7 +14,7 @@ ViewLayout = "Layout__Login"; var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); - var disableLocalLogin = (await SiteService.GetSettingsAsync()).DisableLocalLogin; + var disableLocalLogin = Site.As().DisableLocalLogin; } From 02501cd4fef88cc73355396007a31ec888c700a1 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 07:28:05 -0700 Subject: [PATCH 20/29] remove AllowSiteRegistration options --- .../Controllers/RegistrationController.cs | 10 ------ .../ExternalAuthenticationMigrations.cs | 17 +++++++--- .../RegisterUserLoginFormDisplayDriver.cs | 13 -------- .../RegistrationSettingsDisplayDriver.cs | 5 +-- .../Models/RegistrationSettings.cs | 2 -- .../RegistrationOptionsConfigurations.cs | 1 - .../Services/UsersThemeSelector.cs | 5 +-- .../Views/RegistrationSettings.Edit.cshtml | 8 ----- .../ShellFeaturesManagerExtensions.cs | 29 ++++++++++++++++ src/docs/releases/2.1.0.md | 33 ++++++++++++++----- .../AccountControllerTests.cs | 32 ++++-------------- 11 files changed, 74 insertions(+), 81 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs index 1671d15f056..3c9892cc95c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs @@ -57,11 +57,6 @@ public RegistrationController( [AllowAnonymous] public async Task Register(string returnUrl = null) { - if (!_registrationOptions.AllowSiteRegistration) - { - return NotFound(); - } - var shape = await _registerUserDisplayManager.BuildEditorAsync(_updateModelAccessor.ModelUpdater, false, string.Empty, string.Empty); ViewData["ReturnUrl"] = returnUrl; @@ -75,11 +70,6 @@ public async Task Register(string returnUrl = null) [ActionName(nameof(Register))] public async Task RegisterPOST(string returnUrl = null) { - if (!_registrationOptions.AllowSiteRegistration) - { - return NotFound(); - } - var model = new RegisterUserForm(); var shape = await _registerUserDisplayManager.UpdateEditorAsync(model, _updateModelAccessor.ModelUpdater, false, string.Empty, string.Empty); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs index 5121cd0b24b..7534991b8bb 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs @@ -23,8 +23,6 @@ public int Create() var registrationSettings = site.As(); - registrationSettings.AllowSiteRegistration = registrationSettings.UsersCanRegister == UserRegistrationType.AllowRegistration; - site.Put(registrationSettings); site.Put(new ExternalRegistrationSettings @@ -47,7 +45,18 @@ public int Create() await siteService.UpdateSiteSettingsAsync(site); - if (registrationSettings.UsersCanRegister == UserRegistrationType.AllowOnlyExternalUsers) + if (registrationSettings.UsersCanRegister == UserRegistrationType.NoRegistration) + { + var featuresManager = scope.ServiceProvider.GetRequiredService(); + + if (await featuresManager.IsFeatureEnabledAsync(UserConstants.Features.UserRegistration)) + { + return; + } + + await featuresManager.DisableFeaturesAsync(UserConstants.Features.UserRegistration); + } + else if (registrationSettings.UsersCanRegister == UserRegistrationType.AllowOnlyExternalUsers) { var featuresManager = scope.ServiceProvider.GetRequiredService(); @@ -56,7 +65,7 @@ public int Create() return; } - await featuresManager.EnableFeaturesAsync(UserConstants.Features.ExternalAuthentication); + await featuresManager.UpdateFeaturesAsync([UserConstants.Features.UserRegistration], [UserConstants.Features.ExternalAuthentication]); } }); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs index c8c8d4d8ade..c50ce421d16 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegisterUserLoginFormDisplayDriver.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Users.Models; @@ -7,20 +6,8 @@ namespace OrchardCore.Users.Drivers; public sealed class RegisterUserLoginFormDisplayDriver : DisplayDriver { - private readonly RegistrationOptions _registrationOptions; - - public RegisterUserLoginFormDisplayDriver(IOptions registrationOptions) - { - _registrationOptions = registrationOptions.Value; - } - public override IDisplayResult Edit(LoginForm model, BuildEditorContext context) { - if (!_registrationOptions.AllowSiteRegistration) - { - return null; - } - return View("LoginFormRegisterUser", model).Location("Links:10"); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs index 4b8306e6811..2b15c57ab6e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs @@ -43,7 +43,6 @@ public override async Task EditAsync(ISite site, RegistrationSet return Initialize("RegistrationSettings_Edit", model => { - model.AllowSiteRegistration = settings.AllowSiteRegistration; model.UsersMustValidateEmail = settings.UsersMustValidateEmail; model.UsersAreModerated = settings.UsersAreModerated; model.UseSiteTheme = settings.UseSiteTheme; @@ -64,12 +63,10 @@ public override async Task UpdateAsync(ISite site, RegistrationS await context.Updater.TryUpdateModelAsync(model, Prefix); - var hasChange = model.AllowSiteRegistration != settings.AllowSiteRegistration - || model.UsersMustValidateEmail != settings.UsersMustValidateEmail + var hasChange = model.UsersMustValidateEmail != settings.UsersMustValidateEmail || model.UsersAreModerated != settings.UsersAreModerated || model.UseSiteTheme != model.UseSiteTheme; - settings.AllowSiteRegistration = model.AllowSiteRegistration; settings.UsersMustValidateEmail = model.UsersMustValidateEmail; settings.UsersAreModerated = model.UsersAreModerated; settings.UseSiteTheme = model.UseSiteTheme; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Models/RegistrationSettings.cs b/src/OrchardCore.Modules/OrchardCore.Users/Models/RegistrationSettings.cs index 7635b3e003f..aba0d327859 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Models/RegistrationSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Models/RegistrationSettings.cs @@ -5,8 +5,6 @@ public class RegistrationSettings [Obsolete("This property is no longer used and will be removed in the next major release.")] public UserRegistrationType UsersCanRegister { get; set; } - public bool AllowSiteRegistration { get; set; } - public bool UsersMustValidateEmail { get; set; } public bool UsersAreModerated { get; set; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs index f47a847a598..9a783a90edd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/RegistrationOptionsConfigurations.cs @@ -19,7 +19,6 @@ public void Configure(RegistrationOptions options) .GetAwaiter() .GetResult(); - options.AllowSiteRegistration = settings.AllowSiteRegistration; options.UsersMustValidateEmail = settings.UsersMustValidateEmail; options.UsersAreModerated = settings.UsersAreModerated; options.UseSiteTheme = settings.UseSiteTheme; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs index 6e898af65a4..ae649a70c29 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs @@ -19,18 +19,15 @@ public class UsersThemeSelector : IThemeSelector { private readonly ISiteService _siteService; private readonly IAdminThemeService _adminThemeService; - private readonly RegistrationOptions _registrationOptions; private readonly IHttpContextAccessor _httpContextAccessor; public UsersThemeSelector( ISiteService siteService, IAdminThemeService adminThemeService, - IOptions registrationOptions, IHttpContextAccessor httpContextAccessor) { _siteService = siteService; _adminThemeService = adminThemeService; - _registrationOptions = registrationOptions.Value; _httpContextAccessor = httpContextAccessor; } @@ -69,7 +66,7 @@ public async Task GetThemeAsync() } break; case "Registration": - useSiteTheme = _registrationOptions.UseSiteTheme; + useSiteTheme = (await _siteService.GetSettingsAsync()).UseSiteTheme; break; case "ResetPassword": useSiteTheme = (await _siteService.GetSettingsAsync()).UseSiteTheme; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml index 6682cbd7400..d4a06ff957e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/RegistrationSettings.Edit.cshtml @@ -9,14 +9,6 @@ -
    -
    - - - -
    -
    -
    diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellFeaturesManagerExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellFeaturesManagerExtensions.cs index a1f22180bf5..47479d6404d 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellFeaturesManagerExtensions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellFeaturesManagerExtensions.cs @@ -34,6 +34,35 @@ public static async Task EnableFeaturesAsync(this IShellFeaturesManager shellFea await shellFeaturesManager.EnableFeaturesAsync(featuresToEnable, force: false); } + public static async Task UpdateFeaturesAsync(this IShellFeaturesManager shellFeaturesManager, IEnumerable featureIdsToDisable, IEnumerable featureIdsToEnable) + { + ArgumentNullException.ThrowIfNull(featureIdsToEnable); + ArgumentNullException.ThrowIfNull(featureIdsToDisable); + + var availableFeatures = await shellFeaturesManager.GetAvailableFeaturesAsync(); + + var featuresToDisable = availableFeatures.Where(feature => featureIdsToDisable.Contains(feature.Id)); + var featuresToEnable = availableFeatures.Where(feature => featureIdsToEnable.Contains(feature.Id)); + + await shellFeaturesManager.UpdateFeaturesAsync(featuresToDisable, featuresToEnable, force: false); + } + + public static async Task DisableFeaturesAsync(this IShellFeaturesManager shellFeaturesManager, params string[] featureIds) + { + ArgumentNullException.ThrowIfNull(featureIds); + + if (featureIds.Length == 0) + { + return; + } + + var availableFeatures = await shellFeaturesManager.GetAvailableFeaturesAsync(); + + var featuresToEnable = availableFeatures.Where(feature => featureIds.Contains(feature.Id)); + + await shellFeaturesManager.DisableFeaturesAsync(featuresToEnable, force: false); + } + public static Task> DisableFeaturesAsync(this IShellFeaturesManager shellFeaturesManager, IEnumerable features) { diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index f06032648d6..eb37980cb25 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -6,14 +6,14 @@ Release date: Not yet released ### External User Authentication Feature -A new feature, **External User Authentication**, has been added. This feature was separated from the existing **Users** feature to provide better dependency management and offer an option to disable external authentication by default. +We've introduced a new feature called **External User Authentication**. This feature has been separated from the existing **Users** feature to improve dependency management and to provide an option to disable external authentication by default. As a result, the **User Registration** feature no longer needs to be enabled unless you specifically want to allow site registration. -After upgrading the site, a fast-forward option allows you to enable this feature automatically when needed. As a result, the following settings have been relocated to new classes: +We included a fast-forward migration to enable this feature automatically if previously needed. The following settings have been relocated to new classes as part of this update: -- The properties `UseScriptToSyncRoles` and `SyncRolesScript` have been moved from `LoginSettings` to a new class, `ExternalLoginSettings`. -- The property `UseExternalProviderIfOnlyOneDefined` has been moved from `LoginSettings` to `ExternalLoginSettings`. +- The properties `UseScriptToSyncRoles` and `SyncRolesScript` have been moved from `LoginSettings` to the new `ExternalLoginSettings` class. +- The property `UseExternalProviderIfOnlyOneDefined` has also been moved from `LoginSettings` to `ExternalLoginSettings`. -Additionally, the following properties have been moved from `RegistrationSettings` to `ExternalRegistrationSettings`: +In addition, several properties have been moved from `RegistrationSettings` to a new `ExternalRegistrationSettings` class: - `NoPasswordForExternalUsers` is now `ExternalRegistrationSettings.NoPassword`. - `NoUsernameForExternalUsers` is now `ExternalRegistrationSettings.NoUsername`. @@ -21,13 +21,28 @@ Additionally, the following properties have been moved from `RegistrationSetting - `UseScriptToGenerateUsername` is now `ExternalRegistrationSettings.UseScriptToGenerateUsername`. - `GenerateUsernameScript` is now `ExternalRegistrationSettings.GenerateUsernameScript`. -- Moreover, the following properties have been moved from `LoginSettings` to `ExternalLoginSettings`: +Also, note the following updates in `ExternalLoginSettings`: -- `UseScriptToSyncRoles` is now `ExternalLoginSettings.UseScriptToSyncProperties`. -- `SyncRolesScript` is now `ExternalLoginSettings.SyncPropertiesScript`. +- `UseScriptToSyncRoles` has been renamed to `ExternalLoginSettings.UseScriptToSyncProperties`. +- `SyncRolesScript` has been renamed to `ExternalLoginSettings.SyncPropertiesScript`. !!! note - When configuring `LoginSettings` or `RegistrationSettings` in a recipe, ensure that the recipe's properties are updated to reflect the new settings classes. + When updating recipes to configure `LoginSettings` or `RegistrationSettings`, ensure that the settings reflect the new class structure. + +### User Registration Feature + +The **User Registration** feature is no longer required if you only want to enable external authentication. + +The following properties of `RegistrationSettings` are now deprecated and will be removed in the next major release: + +- `UsersCanRegister` +- `NoPasswordForExternalUsers` +- `NoUsernameForExternalUsers` +- `NoEmailForExternalUsers` +- `UseScriptToGenerateUsername` +- `GenerateUsernameScript` + +Previously, the `UsersCanRegister` property controlled which types of registration were allowed. With this update, this property is obsolete and will be removed in a future release. To enable site registration now, simply activate the **User Registration** feature. If you want to enable external authentication, activate the **External Authentication** feature. ### New 'Azure Communication SMS' feature diff --git a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs index 3a96f2e78c7..a9bb72444fe 100644 --- a/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs +++ b/test/OrchardCore.Tests/OrchardCore.Users/AccountControllerTests.cs @@ -19,10 +19,7 @@ public class AccountControllerTests public async Task ExternalLoginSignIn_Test() { // Arrange - var context = await GetSiteContextAsync(new RegistrationSettings() - { - AllowSiteRegistration = true, - }, true, true, true); + var context = await GetSiteContextAsync(new RegistrationSettings(), true, true, true); // Act var model = new RegisterViewModel() @@ -170,10 +167,7 @@ await context.UsingTenantScopeAsync(async scope => public async Task Register_WhenAllowed_RegisterUser() { // Arrange - var context = await GetSiteContextAsync(new RegistrationSettings() - { - AllowSiteRegistration = true, - }); + var context = await GetSiteContextAsync(new RegistrationSettings()); var responseFromGet = await context.Client.GetAsync("Register"); @@ -209,10 +203,7 @@ await context.UsingTenantScopeAsync(async scope => public async Task Register_WhenNotAllowed_ReturnNotFound() { // Arrange - var context = await GetSiteContextAsync(new RegistrationSettings() - { - AllowSiteRegistration = false, - }); + var context = await GetSiteContextAsync(new RegistrationSettings(), false); // Act var response = await context.Client.GetAsync("Register"); @@ -225,10 +216,7 @@ public async Task Register_WhenNotAllowed_ReturnNotFound() public async Task Register_WhenFeatureIsNotEnable_ReturnNotFound() { // Arrange - var context = await GetSiteContextAsync(new RegistrationSettings() - { - AllowSiteRegistration = true, - }, enableRegistrationFeature: false); + var context = await GetSiteContextAsync(new RegistrationSettings(), enableRegistrationFeature: false); // Act var response = await context.Client.GetAsync("Register"); @@ -241,10 +229,7 @@ public async Task Register_WhenFeatureIsNotEnable_ReturnNotFound() public async Task Register_WhenRequireUniqueEmailIsTrue_PreventRegisteringMultipleUsersWithTheSameEmails() { // Arrange - var context = await GetSiteContextAsync(new RegistrationSettings() - { - AllowSiteRegistration = true, - }); + var context = await GetSiteContextAsync(new RegistrationSettings()); var responseFromGet = await context.Client.GetAsync("Register"); @@ -288,10 +273,7 @@ public async Task Register_WhenRequireUniqueEmailIsTrue_PreventRegisteringMultip public async Task Register_WhenRequireUniqueEmailIsFalse_AllowRegisteringMultipleUsersWithTheSameEmails() { // Arrange - var context = await GetSiteContextAsync(new RegistrationSettings() - { - AllowSiteRegistration = true, - }, enableRegistrationFeature: true, requireUniqueEmail: false); + var context = await GetSiteContextAsync(new RegistrationSettings(), enableRegistrationFeature: true, requireUniqueEmail: false); // Register First User var responseFromGet = await context.Client.GetAsync("Register"); @@ -339,7 +321,6 @@ public async Task Register_WhenModeration_RedirectToRegistrationPending() // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - AllowSiteRegistration = true, UsersAreModerated = true, }); @@ -380,7 +361,6 @@ public async Task Register_WhenRequireEmailConfirmation_RedirectToConfirmEmailSe // Arrange var context = await GetSiteContextAsync(new RegistrationSettings() { - AllowSiteRegistration = true, UsersMustValidateEmail = true, }); From 5f5eef2cdc9192dcb2ff9c180fa87ac9fad6ae18 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 07:38:46 -0700 Subject: [PATCH 21/29] EnabledByDependencyOnly --- .../ExternalAuthenticationMigrations.cs | 14 ++------------ .../OrchardCore.Users/Manifest.cs | 3 ++- src/docs/releases/2.1.0.md | 8 +++++--- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs index 7534991b8bb..5a59770db54 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs @@ -45,7 +45,8 @@ public int Create() await siteService.UpdateSiteSettingsAsync(site); - if (registrationSettings.UsersCanRegister == UserRegistrationType.NoRegistration) + if (registrationSettings.UsersCanRegister == UserRegistrationType.NoRegistration || + registrationSettings.UsersCanRegister == UserRegistrationType.AllowOnlyExternalUsers) { var featuresManager = scope.ServiceProvider.GetRequiredService(); @@ -56,17 +57,6 @@ public int Create() await featuresManager.DisableFeaturesAsync(UserConstants.Features.UserRegistration); } - else if (registrationSettings.UsersCanRegister == UserRegistrationType.AllowOnlyExternalUsers) - { - var featuresManager = scope.ServiceProvider.GetRequiredService(); - - if (await featuresManager.IsFeatureEnabledAsync(UserConstants.Features.ExternalAuthentication)) - { - return; - } - - await featuresManager.UpdateFeaturesAsync([UserConstants.Features.UserRegistration], [UserConstants.Features.ExternalAuthentication]); - } }); return 1; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs index 509f314ccee..8c2f0b0ac63 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs @@ -21,8 +21,9 @@ [assembly: Feature( Id = UserConstants.Features.ExternalAuthentication, - Name = "External User Authentication", + Name = "External Authentication", Description = "Provides a way to allow authentication using an external identity provider.", + EnabledByDependencyOnly = true, Dependencies = [ UserConstants.Features.Users, diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index eb37980cb25..320b2042512 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -4,9 +4,11 @@ Release date: Not yet released ## Change Logs -### External User Authentication Feature +### External Authentication Feature -We've introduced a new feature called **External User Authentication**. This feature has been separated from the existing **Users** feature to improve dependency management and to provide an option to disable external authentication by default. As a result, the **User Registration** feature no longer needs to be enabled unless you specifically want to allow site registration. +We've introduced a new feature called **External Authentication**. This feature has been separated from the existing **Users** feature to improve dependency management and to provide an option to disable external authentication by default. As a result, the **User Registration** feature no longer needs to be enabled unless you specifically want to allow site registration. + +This feature is only available on-demand and cannot be manually enabled or disabled. It is automatically enabled when a feature requiring external authentication is activated. We included a fast-forward migration to enable this feature automatically if previously needed. The following settings have been relocated to new classes as part of this update: @@ -42,7 +44,7 @@ The following properties of `RegistrationSettings` are now deprecated and will b - `UseScriptToGenerateUsername` - `GenerateUsernameScript` -Previously, the `UsersCanRegister` property controlled which types of registration were allowed. With this update, this property is obsolete and will be removed in a future release. To enable site registration now, simply activate the **User Registration** feature. If you want to enable external authentication, activate the **External Authentication** feature. +Previously, the `UsersCanRegister` property controlled which types of registration were allowed. With this update, this property is obsolete and will be removed in a future release. To enable site registration now, simply activate the **User Registration** feature. ### New 'Azure Communication SMS' feature From 97b910b42dc940a5e256269cb0488d8f9ea321e5 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 07:57:23 -0700 Subject: [PATCH 22/29] don't use obsolete properties --- .../ExternalAuthenticationMigrations.cs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs index 5a59770db54..729dd533986 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs @@ -10,10 +10,7 @@ namespace OrchardCore.Users.DataMigrations; public sealed class ExternalAuthenticationMigrations : DataMigration { -#pragma warning disable CA1822 // Mark members as static public int Create() -#pragma warning restore CA1822 // Mark members as static -#pragma warning disable CS0618 // Type or member is obsolete { ShellScope.AddDeferredTask(async scope => { @@ -21,32 +18,30 @@ public int Create() var site = await siteService.LoadSiteSettingsAsync(); - var registrationSettings = site.As(); - - site.Put(registrationSettings); + var registrationSettings = site.Properties[nameof(RegistrationSettings)].AsObject(); site.Put(new ExternalRegistrationSettings { - NoUsername = registrationSettings.NoUsernameForExternalUsers, - NoEmail = registrationSettings.NoEmailForExternalUsers, - NoPassword = registrationSettings.NoPasswordForExternalUsers, - GenerateUsernameScript = registrationSettings.GenerateUsernameScript, - UseScriptToGenerateUsername = registrationSettings.UseScriptToGenerateUsername + NoUsername = registrationSettings["NoUsernameForExternalUsers"]?.GetValue() ?? false, + NoEmail = registrationSettings["NoEmailForExternalUsers"]?.GetValue() ?? false, + NoPassword = registrationSettings["NoPasswordForExternalUsers"]?.GetValue() ?? false, + GenerateUsernameScript = registrationSettings["GenerateUsernameScript"]?.ToString(), + UseScriptToGenerateUsername = registrationSettings["UseScriptToGenerateUsername"]?.GetValue() ?? false, }); - var loginSettings = site.As(); + var loginSettings = site.Properties[nameof(LoginSettings)].AsObject(); site.Put(new ExternalLoginSettings { - UseExternalProviderIfOnlyOneDefined = loginSettings.UseExternalProviderIfOnlyOneDefined, - UseScriptToSyncProperties = loginSettings.UseScriptToSyncRoles, - SyncPropertiesScript = loginSettings.SyncRolesScript, + UseExternalProviderIfOnlyOneDefined = loginSettings["UseExternalProviderIfOnlyOneDefined"]?.GetValue() ?? false, + UseScriptToSyncProperties = loginSettings["UseScriptToSyncRoles"]?.GetValue() ?? false, + SyncPropertiesScript = loginSettings["SyncRolesScript"]?.ToString(), }); await siteService.UpdateSiteSettingsAsync(site); + var enumValue = registrationSettings["UsersCanRegister"]?.GetValue(); - if (registrationSettings.UsersCanRegister == UserRegistrationType.NoRegistration || - registrationSettings.UsersCanRegister == UserRegistrationType.AllowOnlyExternalUsers) + if (enumValue is not null && enumValue != 1) { var featuresManager = scope.ServiceProvider.GetRequiredService(); @@ -61,5 +56,4 @@ public int Create() return 1; } -#pragma warning restore CS0618 // Type or member is obsolete } From af9091049997d0080c433ba35acd1e00261cfa26 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 08:14:17 -0700 Subject: [PATCH 23/29] clean up and fix NRE --- .../ExternalAuthenticationMigrations.cs | 5 ++-- ...rnalAuthenticationUserMenuDisplayDriver.cs | 25 +++++++++++++++++++ .../Drivers/UserMenuDisplayDriver.cs | 19 -------------- .../Extensions/ControllerExtensions.cs | 4 +++ .../Models/RegistrationOptions.cs | 2 -- 5 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationUserMenuDisplayDriver.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs index 729dd533986..3a7ff59984f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using Microsoft.Extensions.DependencyInjection; using OrchardCore.Data.Migration; using OrchardCore.Entities; @@ -18,7 +19,7 @@ public int Create() var site = await siteService.LoadSiteSettingsAsync(); - var registrationSettings = site.Properties[nameof(RegistrationSettings)].AsObject(); + var registrationSettings = site.Properties[nameof(RegistrationSettings)]?.AsObject() ?? new JsonObject(); site.Put(new ExternalRegistrationSettings { @@ -29,7 +30,7 @@ public int Create() UseScriptToGenerateUsername = registrationSettings["UseScriptToGenerateUsername"]?.GetValue() ?? false, }); - var loginSettings = site.Properties[nameof(LoginSettings)].AsObject(); + var loginSettings = site.Properties[nameof(LoginSettings)]?.AsObject() ?? new JsonObject(); site.Put(new ExternalLoginSettings { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationUserMenuDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationUserMenuDisplayDriver.cs new file mode 100644 index 00000000000..b3c27c0f82a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalAuthenticationUserMenuDisplayDriver.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Identity; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Drivers; + +public sealed class ExternalAuthenticationUserMenuDisplayDriver : DisplayDriver +{ + private readonly SignInManager _signInManager; + + public ExternalAuthenticationUserMenuDisplayDriver(SignInManager signInManager) + { + _signInManager = signInManager; + } + + public override IDisplayResult Display(UserMenu model, BuildDisplayContext context) + { + return View("UserMenuItems__ExternalLogins", model) + .RenderWhen(async () => (await _signInManager.GetExternalAuthenticationSchemesAsync()).Any()) + .Location("Detail", "Content:10") + .Location("DetailAdmin", "Content:10") + .Differentiator("ExternalLogins"); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserMenuDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserMenuDisplayDriver.cs index 419ac92a746..68fe0a1d66b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserMenuDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserMenuDisplayDriver.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Users.Models; @@ -49,21 +48,3 @@ public override Task DisplayAsync(UserMenu model, BuildDisplayCo return CombineAsync(results); } } -public sealed class ExternalAuthenticationUserMenuDisplayDriver : DisplayDriver -{ - private readonly SignInManager _signInManager; - - public ExternalAuthenticationUserMenuDisplayDriver(SignInManager signInManager) - { - _signInManager = signInManager; - } - - public override IDisplayResult Display(UserMenu model, BuildDisplayContext context) - { - return View("UserMenuItems__ExternalLogins", model) - .RenderWhen(async () => (await _signInManager.GetExternalAuthenticationSchemesAsync()).Any()) - .Location("Detail", "Content:10") - .Location("DetailAdmin", "Content:10") - .Differentiator("ExternalLogins"); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs index 852b05b4a5a..c97ae90868c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs @@ -80,7 +80,9 @@ internal static async Task RegisterUser(this Controller controller, Regis await signInManager.SignInAsync(user, isPersistent: false); } + logger.LogInformation(3, "User created a new account with password."); + await registrationEvents.InvokeAsync((e, user) => e.RegisteredAsync(user), user, logger); return user; @@ -93,7 +95,9 @@ internal static async Task RegisterUser(this Controller controller, Regis internal static async Task SendEmailConfirmationTokenAsync(this Controller controller, User user, string subject) { var userManager = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>(); + var code = await userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = controller.Url.Action(nameof(EmailConfirmationController.ConfirmEmail), typeof(EmailConfirmationController).ControllerName(), new { diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/RegistrationOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/RegistrationOptions.cs index 9c5f6014da4..fffeeef6f9f 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/RegistrationOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/RegistrationOptions.cs @@ -2,8 +2,6 @@ namespace OrchardCore.Users.Models; public class RegistrationOptions { - public bool AllowSiteRegistration { get; set; } - public bool UsersMustValidateEmail { get; set; } public bool UsersAreModerated { get; set; } From b98057c9219559771b6348dca5acae60c3cdc90d Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 09:07:48 -0700 Subject: [PATCH 24/29] Add DisableNewRegistrations settings --- .../ExternalAuthenticationsController.cs | 25 ++++++++++++++++--- .../ExternalAuthenticationMigrations.cs | 4 ++- ...ternalRegistrationSettingsDisplayDriver.cs | 1 + .../Models/ExternalRegistrationSettings.cs | 2 ++ .../ExternalRegistrationSettings.Edit.cshtml | 12 +++++++++ src/docs/releases/2.1.0.md | 2 ++ 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs index d75cc85a9f9..546b3a6e645 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Notify; using OrchardCore.Environment.Shell; using OrchardCore.Modules; using OrchardCore.Mvc.Core.Utilities; @@ -33,6 +34,7 @@ public sealed class ExternalAuthenticationsController : AccountBaseController private readonly ISiteService _siteService; private readonly IEnumerable _accountEvents; private readonly IShellFeaturesManager _shellFeaturesManager; + private readonly INotifier _notifier; private readonly IEnumerable _externalLoginHandlers; private readonly ExternalLoginOptions _externalLoginOption; private readonly IdentityOptions _identityOptions; @@ -51,6 +53,7 @@ public ExternalAuthenticationsController( IStringLocalizer stringLocalizer, IEnumerable accountEvents, IShellFeaturesManager shellFeaturesManager, + INotifier notifier, IEnumerable externalLoginHandlers, IOptions externalLoginOption, IOptions identityOptions) @@ -63,6 +66,7 @@ public ExternalAuthenticationsController( _siteService = siteService; _accountEvents = accountEvents; _shellFeaturesManager = shellFeaturesManager; + _notifier = notifier; _externalLoginHandlers = externalLoginHandlers; _externalLoginOption = externalLoginOption.Value; _identityOptions = identityOptions.Value; @@ -268,6 +272,13 @@ public async Task ExternalLoginCallback(string returnUrl = null, } } + if (settings.DisableNewRegistrations) + { + await _notifier.ErrorAsync(H["New registrations are disabled for this site."]); + + return RedirectToLogin(); + } + return View(nameof(RegisterExternalLogin), externalLoginViewModel); } @@ -279,6 +290,15 @@ public async Task ExternalLoginCallback(string returnUrl = null, [ValidateAntiForgeryToken] public async Task RegisterExternalLogin(RegisterExternalLoginViewModel model, string returnUrl = null) { + var settings = await _siteService.GetSettingsAsync(); + + if (settings.DisableNewRegistrations) + { + await _notifier.ErrorAsync(H["New registrations are disabled for this site."]); + + return RedirectToLogin(); + } + var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) @@ -291,8 +311,6 @@ public async Task RegisterExternalLogin(RegisterExternalLoginView ViewData["ReturnUrl"] = returnUrl; ViewData["LoginProvider"] = info.LoginProvider; - var settings = await _siteService.GetSettingsAsync(); - model.NoPassword = settings.NoPassword; model.NoEmail = settings.NoEmail; model.NoUsername = settings.NoUsername; @@ -372,11 +390,12 @@ public async Task RegisterExternalLogin(RegisterExternalLoginView return await LoggedInActionResultAsync(iUser, returnUrl, info); } } + AddIdentityErrors(identityResult); } } - return View(nameof(RegisterExternalLogin), model); + return View(model); } [HttpPost] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs index 3a7ff59984f..8857ebfe3c9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs @@ -21,8 +21,11 @@ public int Create() var registrationSettings = site.Properties[nameof(RegistrationSettings)]?.AsObject() ?? new JsonObject(); + var enumValue = registrationSettings["UsersCanRegister"]?.GetValue(); + site.Put(new ExternalRegistrationSettings { + DisableNewRegistrations = enumValue == 0, NoUsername = registrationSettings["NoUsernameForExternalUsers"]?.GetValue() ?? false, NoEmail = registrationSettings["NoEmailForExternalUsers"]?.GetValue() ?? false, NoPassword = registrationSettings["NoPasswordForExternalUsers"]?.GetValue() ?? false, @@ -40,7 +43,6 @@ public int Create() }); await siteService.UpdateSiteSettingsAsync(site); - var enumValue = registrationSettings["UsersCanRegister"]?.GetValue(); if (enumValue is not null && enumValue != 1) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalRegistrationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalRegistrationSettingsDisplayDriver.cs index 94ebb165930..99b1d14a80b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalRegistrationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ExternalRegistrationSettingsDisplayDriver.cs @@ -35,6 +35,7 @@ public override async Task EditAsync(ISite site, ExternalRegistr return Initialize("ExternalRegistrationSettings_Edit", model => { + model.DisableNewRegistrations = settings.DisableNewRegistrations; model.NoPassword = settings.NoPassword; model.NoUsername = settings.NoUsername; model.NoEmail = settings.NoEmail; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalRegistrationSettings.cs b/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalRegistrationSettings.cs index 92659a91914..614bb68e18a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalRegistrationSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Models/ExternalRegistrationSettings.cs @@ -2,6 +2,8 @@ namespace OrchardCore.Users.Models; public class ExternalRegistrationSettings { + public bool DisableNewRegistrations { get; set; } + public bool NoPassword { get; set; } public bool NoUsername { get; set; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml index 5bf20aec324..fd208146541 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml @@ -11,6 +11,15 @@
    @T["Configuring External Authentication for User Registration with Third-Party Providers"]
    +
    +
    + + + + @T["If selected, new users will not be able to register using external authentication. This means users without a local account will be unable to authenticate."] +
    +
    +
    @@ -19,6 +28,7 @@ @T["When a new user logs in with an external provider, they are not required to provide a local username. You can customize how it works by providing an IExternalLoginEventHandler or writing a script."]
    +
    @@ -27,6 +37,7 @@ @T["When a new user logs in with an external provider and the email claim is defined, they are not required to provide a local email address."]
    +
    @@ -35,6 +46,7 @@ @T["When a new user logs in with an external provider, they are not required to provide a local password."]
    +
    diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 320b2042512..7ccf6292c83 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -23,6 +23,8 @@ In addition, several properties have been moved from `RegistrationSettings` to a - `UseScriptToGenerateUsername` is now `ExternalRegistrationSettings.UseScriptToGenerateUsername`. - `GenerateUsernameScript` is now `ExternalRegistrationSettings.GenerateUsernameScript`. +Moreover, a new property, `DisableNewRegistrations`, has been added to `ExternalRegistrationSettings`, allowing you to prevent new external users from registering on the site. This property will be set by default if you are currently using the **NoRegistration** value for the obsolete `UsersCanRegister` property. + Also, note the following updates in `ExternalLoginSettings`: - `UseScriptToSyncRoles` has been renamed to `ExternalLoginSettings.UseScriptToSyncProperties`. From 92bede7e359e0d052f90de629cd2806f9db14e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 18 Sep 2024 02:07:01 +0200 Subject: [PATCH 25/29] Removing unnecessary namespace import --- .../OrchardCore.Users/Services/UsersThemeSelector.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs index ae649a70c29..3bd5835deb5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; using OrchardCore.Admin; using OrchardCore.DisplayManagement.Theming; using OrchardCore.Settings; From a76fa95fdfbe323eff574749e9b59ec4a17f8ba7 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 20:09:01 -0700 Subject: [PATCH 26/29] Update src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- .../Views/ExternalRegistrationSettings.Edit.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml index fd208146541..0f05544524c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalRegistrationSettings.Edit.cshtml @@ -16,7 +16,7 @@ - @T["If selected, new users will not be able to register using external authentication. This means users without a local account will be unable to authenticate."] + @T["If selected, only users with existing local accounts will be able to authenticate, and new users will not be able to register using external authentication. Users without a local account will not be able to authenticate."]
    From 5708db15207c30884e3bdb30a8e7fd062bbec729 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 20:09:51 -0700 Subject: [PATCH 27/29] Update src/docs/releases/2.1.0.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- src/docs/releases/2.1.0.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 7ccf6292c83..9ddac2b6a91 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -10,7 +10,9 @@ We've introduced a new feature called **External Authentication**. This feature This feature is only available on-demand and cannot be manually enabled or disabled. It is automatically enabled when a feature requiring external authentication is activated. -We included a fast-forward migration to enable this feature automatically if previously needed. The following settings have been relocated to new classes as part of this update: +We included a fast-forward migration to enable this feature automatically if previously needed. No action is needed from site administrators. + +The following settings have been relocated to new classes as part of this update: - The properties `UseScriptToSyncRoles` and `SyncRolesScript` have been moved from `LoginSettings` to the new `ExternalLoginSettings` class. - The property `UseExternalProviderIfOnlyOneDefined` has also been moved from `LoginSettings` to `ExternalLoginSettings`. From 0d5b6cd068a553ad787b46337e30a544ae756066 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 17 Sep 2024 20:11:54 -0700 Subject: [PATCH 28/29] address feedback --- .../ExternalAuthenticationMigrations.cs | 2 +- .../Views/ExternalLoginSettings.Edit.cshtml | 60 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs index 8857ebfe3c9..83e1fadfa84 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs @@ -48,7 +48,7 @@ public int Create() { var featuresManager = scope.ServiceProvider.GetRequiredService(); - if (await featuresManager.IsFeatureEnabledAsync(UserConstants.Features.UserRegistration)) + if (!await featuresManager.IsFeatureEnabledAsync(UserConstants.Features.UserRegistration)) { return; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalLoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalLoginSettings.Edit.cshtml index 2da59db6e1b..69747b75dd9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalLoginSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ExternalLoginSettings.Edit.cshtml @@ -63,36 +63,36 @@ ); } - // don't format here. - const suggestion = `/** - * Use the loginProvider and externalClaims properties of the context variable to inspect - * who authenticated the user and with what claims. Check currentRoles property and apply - * your business logic to fill the rolesToAdd and rolesToRemove arrays in order to update - */ - const context = {} as Context; - type Context = { - readonly user: { - userName: string; - userRoles: string[]; - userClaims: { claimType: string; claimValue: string; }[]; - userProperties: { [key: string]: string }; - }, - readonly loginProvider: string, - rolesToAdd: string[]; - rolesToRemove: string[]; - claimsToUpdate: { claimType: string; claimValue: string; }[]; - claimsToRemove: { claimType: string; claimValue: string; }[]; - propertiesToUpdate: { [key: string]: string }; - externalClaims: readonly [{ - valueType: string; - type: string; - value: string; - subject: string; - issuer: string; - originalIssuer: string; - properties: { [key: string]: string }; - }] - }` +// don't format here. +const suggestion = `/** +* Use the loginProvider and externalClaims properties of the context variable to inspect +* who authenticated the user and with what claims. Check currentRoles property and apply +* your business logic to fill the rolesToAdd and rolesToRemove arrays in order to update +*/ +const context = {} as Context; +type Context = { + readonly user: { + userName: string; + userRoles: string[]; + userClaims: { claimType: string; claimValue: string; }[]; + userProperties: { [key: string]: string }; + }, + readonly loginProvider: string, + rolesToAdd: string[]; + rolesToRemove: string[]; + claimsToUpdate: { claimType: string; claimValue: string; }[]; + claimsToRemove: { claimType: string; claimValue: string; }[]; + propertiesToUpdate: { [key: string]: string }; + externalClaims: readonly [{ + valueType: string; + type: string; + value: string; + subject: string; + issuer: string; + originalIssuer: string; + properties: { [key: string]: string }; + }] +}` var codeEditor; document.addEventListener('DOMContentLoaded', function () { require(['vs/editor/editor.main'], function () { From 04812f7381b2288813a89681f2094f20c4811e6c Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 18 Sep 2024 10:39:36 -0700 Subject: [PATCH 29/29] improve DisableNewRegistrations migration logic --- .../DataMigrations/ExternalAuthenticationMigrations.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs index 83e1fadfa84..99a71e3eff2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/DataMigrations/ExternalAuthenticationMigrations.cs @@ -15,6 +15,10 @@ public int Create() { ShellScope.AddDeferredTask(async scope => { + var featuresManager = scope.ServiceProvider.GetRequiredService(); + + var isRegistrationFeatureEnabled = await featuresManager.IsFeatureEnabledAsync(UserConstants.Features.UserRegistration); + var siteService = scope.ServiceProvider.GetRequiredService(); var site = await siteService.LoadSiteSettingsAsync(); @@ -25,7 +29,7 @@ public int Create() site.Put(new ExternalRegistrationSettings { - DisableNewRegistrations = enumValue == 0, + DisableNewRegistrations = enumValue == 0 || !isRegistrationFeatureEnabled, NoUsername = registrationSettings["NoUsernameForExternalUsers"]?.GetValue() ?? false, NoEmail = registrationSettings["NoEmailForExternalUsers"]?.GetValue() ?? false, NoPassword = registrationSettings["NoPasswordForExternalUsers"]?.GetValue() ?? false, @@ -46,9 +50,7 @@ public int Create() if (enumValue is not null && enumValue != 1) { - var featuresManager = scope.ServiceProvider.GetRequiredService(); - - if (!await featuresManager.IsFeatureEnabledAsync(UserConstants.Features.UserRegistration)) + if (!isRegistrationFeatureEnabled) { return; }