From bd93e29e1d267f3830d603a3021a0bb733c3bb78 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 24 Oct 2024 11:34:22 -0700 Subject: [PATCH] Handle Default Authentication using events (#16884) --- .../Handlers/LoginFormEventEventHandler.cs | 12 ++- .../Controllers/AccountController.cs | 39 ++------- .../ExternalAuthenticationsController.cs | 35 -------- .../Services/ExternalLoginFormEvents.cs | 79 +++++++++++++++++++ .../OrchardCore.Users/Startup.cs | 2 + .../Events/ILoginFormEvent.cs | 9 +++ .../Handlers/IExternalLoginEventHandler.cs | 1 - 7 files changed, 106 insertions(+), 71 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Services/ExternalLoginFormEvents.cs diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Users/Handlers/LoginFormEventEventHandler.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Users/Handlers/LoginFormEventEventHandler.cs index 4aa0bd4fad9..97f34bd51e0 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Users/Handlers/LoginFormEventEventHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Users/Handlers/LoginFormEventEventHandler.cs @@ -13,31 +13,37 @@ public LoginFormEventEventHandler(ReCaptchaService reCaptchaService) _reCaptchaService = reCaptchaService; } - public Task IsLockedOutAsync(IUser user) => Task.CompletedTask; + public Task IsLockedOutAsync(IUser user) + => Task.CompletedTask; public Task LoggedInAsync(IUser user) { _reCaptchaService.ThisIsAHuman(); + return Task.CompletedTask; } - public async Task LoggingInAsync(string userName, Action reportError) + public Task LoggingInAsync(string userName, Action reportError) { if (_reCaptchaService.IsThisARobot()) { - await _reCaptchaService.ValidateCaptchaAsync(reportError); + return _reCaptchaService.ValidateCaptchaAsync(reportError); } + + return Task.CompletedTask; } public Task LoggingInFailedAsync(string userName) { _reCaptchaService.MaybeThisIsARobot(); + return Task.CompletedTask; } public Task LoggingInFailedAsync(IUser user) { _reCaptchaService.MaybeThisIsARobot(); + return Task.CompletedTask; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index 58b21b96869..2a5f2e291dc 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -1,10 +1,8 @@ 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; @@ -19,15 +17,14 @@ using OrchardCore.Users.Models; using OrchardCore.Users.Services; using OrchardCore.Users.ViewModels; -using YesSql.Services; namespace OrchardCore.Users.Controllers; [Authorize] public sealed class AccountController : AccountBaseController { - [Obsolete("This property will be removed in v3. Instead use ExternalAuthenticationController.DefaultExternalLoginProtector")] - public const string DefaultExternalLoginProtector = ExternalAuthenticationsController.DefaultExternalLoginProtector; + [Obsolete("This property is no longer used and will be removed in v3.")] + public const string DefaultExternalLoginProtector = "DefaultExternalLogin"; private readonly IUserService _userService; private readonly SignInManager _signInManager; @@ -35,14 +32,10 @@ public sealed class AccountController : AccountBaseController private readonly ILogger _logger; private readonly ISiteService _siteService; private readonly IEnumerable _accountEvents; - private readonly ExternalLoginOptions _externalLoginOptions; private readonly RegistrationOptions _registrationOptions; - private readonly IDataProtectionProvider _dataProtectionProvider; private readonly IDisplayManager _loginFormDisplayManager; private readonly IUpdateModelAccessor _updateModelAccessor; private readonly INotifier _notifier; - private readonly IClock _clock; - private readonly IDistributedCache _distributedCache; internal readonly IHtmlLocalizer H; internal readonly IStringLocalizer S; @@ -57,11 +50,7 @@ public AccountController( IStringLocalizer stringLocalizer, IEnumerable accountEvents, IOptions registrationOptions, - IOptions externalLoginOptions, INotifier notifier, - IClock clock, - IDistributedCache distributedCache, - IDataProtectionProvider dataProtectionProvider, IDisplayManager loginFormDisplayManager, IUpdateModelAccessor updateModelAccessor) { @@ -71,12 +60,8 @@ public AccountController( _logger = logger; _siteService = siteService; _accountEvents = accountEvents; - _externalLoginOptions = externalLoginOptions.Value; _registrationOptions = registrationOptions.Value; _notifier = notifier; - _clock = clock; - _distributedCache = distributedCache; - _dataProtectionProvider = dataProtectionProvider; _loginFormDisplayManager = loginFormDisplayManager; _updateModelAccessor = updateModelAccessor; @@ -96,23 +81,13 @@ public async Task Login(string returnUrl = null) // Clear the existing external cookie to ensure a clean login process. await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - if (_externalLoginOptions.UseExternalProviderIfOnlyOneDefined) + foreach (var handler in _accountEvents) { - var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); - if (schemes.Count() == 1) - { - 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, - }); + var result = await handler.LoggingInAsync(); - return RedirectToAction(nameof(ExternalAuthenticationsController.DefaultExternalLogin), typeof(ExternalAuthenticationsController).ControllerName(), new { protectedToken, returnUrl }); + if (result != null) + { + return result; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs index bdb161eb242..b6d19808f32 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ExternalAuthenticationsController.cs @@ -24,8 +24,6 @@ namespace OrchardCore.Users.Controllers; [Feature(UserConstants.Features.ExternalAuthentication)] public sealed class ExternalAuthenticationsController : AccountBaseController { - public const string DefaultExternalLoginProtector = "DefaultExternalLogin"; - private readonly SignInManager _signInManager; private readonly UserManager _userManager; private readonly ILogger _logger; @@ -75,39 +73,6 @@ public ExternalAuthenticationsController( S = stringLocalizer; } - [AllowAnonymous] - public async Task DefaultExternalLogin(string protectedToken, string returnUrl = null) - { - if (_externalLoginOption.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] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/ExternalLoginFormEvents.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/ExternalLoginFormEvents.cs new file mode 100644 index 00000000000..6a9fa06d650 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/ExternalLoginFormEvents.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Users.Controllers; +using OrchardCore.Users.Events; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class ExternalLoginFormEvents : ILoginFormEvent +{ + private readonly ExternalLoginOptions _externalLoginOptions; + private readonly SignInManager _signInManager; + private readonly LinkGenerator _linkGenerator; + private readonly IHttpContextAccessor _httpContextAccessor; + + public ExternalLoginFormEvents( + IOptions externalLoginOptions, + SignInManager signInManager, + LinkGenerator linkGenerator, + IHttpContextAccessor httpContextAccessor) + { + _externalLoginOptions = externalLoginOptions.Value; + _signInManager = signInManager; + _linkGenerator = linkGenerator; + _httpContextAccessor = httpContextAccessor; + } + + public Task IsLockedOutAsync(IUser user) + => Task.CompletedTask; + + public Task LoggedInAsync(IUser user) + => Task.CompletedTask; + + public async Task LoggingInAsync() + { + if (!_externalLoginOptions.UseExternalProviderIfOnlyOneDefined) + { + return null; + } + + var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); + + if (schemes.Count() == 1) + { + var provider = schemes.First().Name; + + var model = new RouteValueDictionary(); + + if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue("returnUrl", out var returnUrlValue)) + { + model.Add("returnUrl", returnUrlValue); + } + + var redirectUrl = _linkGenerator.GetPathByAction(_httpContextAccessor.HttpContext, + action: nameof(ExternalAuthenticationsController.ExternalLoginCallback), + controller: typeof(ExternalAuthenticationsController).ControllerName(), + values: model); + + return new ChallengeResult( + authenticationScheme: provider, + properties: _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl)); + } + + return null; + } + + public Task LoggingInAsync(string userName, Action reportError) + => Task.CompletedTask; + + public Task LoggingInFailedAsync(string userName) + => Task.CompletedTask; + + public Task LoggingInFailedAsync(IUser user) + => Task.CompletedTask; +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 92446e5da87..4d2f2f4b3e7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -41,6 +41,7 @@ using OrchardCore.Users.DataMigrations; using OrchardCore.Users.Deployment; using OrchardCore.Users.Drivers; +using OrchardCore.Users.Events; using OrchardCore.Users.Handlers; using OrchardCore.Users.Indexes; using OrchardCore.Users.Liquid; @@ -250,6 +251,7 @@ public sealed class ExternalAuthenticationStartup : StartupBase public override void ConfigureServices(IServiceCollection services) { + services.AddScoped(); services.AddNavigationProvider(); services.AddScoped, ExternalAuthenticationUserMenuDisplayDriver>(); services.AddSiteDisplayDriver(); diff --git a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ILoginFormEvent.cs b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ILoginFormEvent.cs index 54d7ccd7cfb..54c2c7c7662 100644 --- a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ILoginFormEvent.cs +++ b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ILoginFormEvent.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Mvc; + namespace OrchardCore.Users.Events; /// @@ -35,4 +37,11 @@ public interface ILoginFormEvent /// /// The . Task LoggedInAsync(IUser user); + + /// + /// Occurs when a user visits the login page. + /// + /// + Task LoggingInAsync() + => Task.FromResult(null); } diff --git a/src/OrchardCore/OrchardCore.Users.Abstractions/Handlers/IExternalLoginEventHandler.cs b/src/OrchardCore/OrchardCore.Users.Abstractions/Handlers/IExternalLoginEventHandler.cs index dfe8c20576f..ea93121522d 100644 --- a/src/OrchardCore/OrchardCore.Users.Abstractions/Handlers/IExternalLoginEventHandler.cs +++ b/src/OrchardCore/OrchardCore.Users.Abstractions/Handlers/IExternalLoginEventHandler.cs @@ -18,5 +18,4 @@ public interface IExternalLoginEventHandler /// /// The . Task UpdateUserAsync(UpdateUserContext context); - }