From 0f03461f53aafea8f5514fdb4e24e0919236ddba Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 21 Jun 2026 01:58:27 -0500 Subject: [PATCH] Add setting to auto redirect to external provider When enabled, AutoRedirectToProvider will switch authentication challenges to redirect to the external auth provider instead of displaying the password login form. If more than one external provider is configured, a minimal external auth provider login page will be shown. Ref #410 --- .../Models/AuthenticationSettings.cs | 1 + .../Models/ProviderButtonsModel.cs | 10 ++ .../UI/Components/RedirectToLogin.razor | 26 ++-- .../UI/Pages/Account/ExternalLogin.cshtml | 38 ++++++ .../UI/Pages/Account/ExternalLogin.cshtml.cs | 121 ++++++++++++++++++ .../UI/Pages/Account/Login.cshtml | 27 +--- .../UI/Pages/Account/_ProviderButtons.cshtml | 28 ++++ .../Pages/Settings/Authentication/Index.razor | 4 + 8 files changed, 221 insertions(+), 34 deletions(-) create mode 100644 LANCommander.Server/Models/ProviderButtonsModel.cs create mode 100644 LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml create mode 100644 LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml.cs create mode 100644 LANCommander.Server/UI/Pages/Account/_ProviderButtons.cshtml diff --git a/LANCommander.Server.Settings/Models/AuthenticationSettings.cs b/LANCommander.Server.Settings/Models/AuthenticationSettings.cs index d160dcc5..ab2b867e 100644 --- a/LANCommander.Server.Settings/Models/AuthenticationSettings.cs +++ b/LANCommander.Server.Settings/Models/AuthenticationSettings.cs @@ -6,6 +6,7 @@ public class AuthenticationSettings { public bool RequireApproval { get; set; } = false; public bool AllowRegistration { get; set; } = true; + public bool AutoRedirectToProvider { get; set; } = false; public string TokenSecret { get; set; } = Guid.NewGuid().ToString(); public int TokenLifetime { get; set; } = 30; public bool PasswordRequireNonAlphanumeric { get; set; } = false; diff --git a/LANCommander.Server/Models/ProviderButtonsModel.cs b/LANCommander.Server/Models/ProviderButtonsModel.cs new file mode 100644 index 00000000..4ebc8ce9 --- /dev/null +++ b/LANCommander.Server/Models/ProviderButtonsModel.cs @@ -0,0 +1,10 @@ +using LANCommander.Server.Settings.Models; + +namespace LANCommander.Server.Models +{ + public class ProviderButtonsModel + { + public IEnumerable Providers { get; set; } = new List(); + public string ReturnUrl { get; set; } + } +} diff --git a/LANCommander.Server/UI/Components/RedirectToLogin.razor b/LANCommander.Server/UI/Components/RedirectToLogin.razor index ba3c8dd0..efd90c1f 100644 --- a/LANCommander.Server/UI/Components/RedirectToLogin.razor +++ b/LANCommander.Server/UI/Components/RedirectToLogin.razor @@ -1,8 +1,9 @@ -@using LANCommander.Server.Data +@using LANCommander.Server.Data +@using LANCommander.Server.Settings @using LANCommander.Server.Settings.Enums -@@using LANCommander.Server.Settings.Enums -using LANCommander.Server.Data +@using Microsoft.Extensions.Options @inject NavigationManager NavigationManager +@inject IOptions Settings @code { protected override void OnInitialized() @@ -10,12 +11,21 @@ using LANCommander.Server.Data if (DatabaseContext.Provider == DatabaseProvider.Unknown) { NavigationManager.NavigateTo("/FirstTimeSetup"); + return; } - else - { - var currentUri = new Uri(NavigationManager.Uri); - NavigationManager.NavigateTo($"/Login?ReturnUrl={currentUri.AbsolutePath}", true); - } + var currentUri = new Uri(NavigationManager.Uri); + var returnUrl = currentUri.AbsolutePath; + + var authentication = Settings.Value.Server.Authentication; + var providerCount = authentication.AuthenticationProviders?.Count() ?? 0; + + // Only route through the external provider flow when auto-redirect is enabled and + // at least one provider is configured. The ExternalLogin page will challenge the + // single provider directly or present the list when multiple are available. + if (authentication.AutoRedirectToProvider && providerCount > 0) + NavigationManager.NavigateTo($"/ExternalLogin?ReturnUrl={returnUrl}", true); + else + NavigationManager.NavigateTo($"/Login?ReturnUrl={returnUrl}", true); } } diff --git a/LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml b/LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml new file mode 100644 index 00000000..ab78ed79 --- /dev/null +++ b/LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml @@ -0,0 +1,38 @@ +@page "/ExternalLogin" +@using LANCommander.Server.Extensions +@model LANCommander.Server.UI.Pages.Account.ExternalLoginModel +@{ Layout = "/UI/Views/Shared/_LayoutBasic.cshtml"; } + +@{ + ViewData["Title"] = "Sign in"; +} + + diff --git a/LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml.cs b/LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml.cs new file mode 100644 index 00000000..2296ec9a --- /dev/null +++ b/LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml.cs @@ -0,0 +1,121 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using LANCommander.Server.Data; +using LANCommander.Server.Extensions; +using LANCommander.Server.Models; +using LANCommander.Server.Services; +using LANCommander.Server.Settings.Enums; +using LANCommander.Server.Settings.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using User = LANCommander.Server.Data.Models.User; + +namespace LANCommander.Server.UI.Pages.Account +{ + public class ExternalLoginModel : PageModel + { + private readonly SignInManager SignInManager; + private readonly RoleService RoleService; + private readonly ILogger Logger; + private readonly IOptions Settings; + + public ExternalLoginModel( + SignInManager signInManager, + RoleService roleService, + ILogger logger, + IOptions settings) + { + SignInManager = signInManager; + RoleService = roleService; + Logger = logger; + Settings = settings; + } + + public string ReturnUrl { get; set; } + + public string ScreenshotUrl { get; set; } + + public IEnumerable Providers { get; set; } = new List(); + + public async Task OnGetAsync(string returnUrl = null, string error = null) + { + returnUrl ??= Url.Content("~/"); + ReturnUrl = returnUrl; + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + if (DatabaseContext.Provider == DatabaseProvider.Unknown) + return Redirect("/FirstTimeSetup"); + + var administratorRole = await RoleService + .Include(r => r.UserRoles) + .FirstOrDefaultAsync(r => r.Name == RoleService.AdministratorRoleName); + + if (administratorRole == null || administratorRole.UserRoles != null && !administratorRole.UserRoles.Any()) + return Redirect("/FirstTimeSetup"); + + if (!String.IsNullOrEmpty(error)) + ModelState.AddModelError(string.Empty, error); + + Providers = HttpContext.GetExternalProviders()?.ToList() ?? new List(); + + var providerCount = Providers.Count(); + + // Fall back to the standard login page if auto-redirect is disabled or there + // are no providers to redirect to. + if (!Settings.Value.Server.Authentication.AutoRedirectToProvider || providerCount == 0) + return Redirect($"/Login?ReturnUrl={Uri.EscapeDataString(returnUrl)}"); + + // A single provider can be challenged directly without showing the list. + if (providerCount == 1) + return ChallengeProvider(Providers.First().Slug, returnUrl); + + LoadScreenshot(); + + return Page(); + } + + public async Task OnPostAsync(string returnUrl = null, string provider = null) + { + returnUrl ??= Url.Content("~/"); + + if (returnUrl == "/Logout") + returnUrl = "/"; + + if (!String.IsNullOrWhiteSpace(provider) && await HttpContext.IsProviderSupportedAsync(provider)) + return ChallengeProvider(provider, returnUrl); + + return Redirect($"/ExternalLogin?ReturnUrl={Uri.EscapeDataString(returnUrl)}"); + } + + private IActionResult ChallengeProvider(string provider, string returnUrl) + { + var properties = new AuthenticationProperties(new Dictionary() + { + { "Action", AuthenticationProviderActionType.Login.ToString() } + }); + + properties.RedirectUri = returnUrl; + + return Challenge(properties, provider); + } + + private void LoadScreenshot() + { + var screenshots = Directory.GetFiles(Path.Combine("wwwroot", "static", "login"), "*.jpg"); + + if (screenshots.Any()) + ScreenshotUrl = screenshots[new Random().Next(0, screenshots.Length - 1)].Replace("wwwroot", "").Replace(Path.DirectorySeparatorChar, '/'); + } + } +} diff --git a/LANCommander.Server/UI/Pages/Account/Login.cshtml b/LANCommander.Server/UI/Pages/Account/Login.cshtml index 7dc3a8d0..7ccb1550 100644 --- a/LANCommander.Server/UI/Pages/Account/Login.cshtml +++ b/LANCommander.Server/UI/Pages/Account/Login.cshtml @@ -82,32 +82,7 @@ -
-
- @foreach (var provider in providers) - { -
-
- - - - -
-
- } -
-
+ } diff --git a/LANCommander.Server/UI/Pages/Account/_ProviderButtons.cshtml b/LANCommander.Server/UI/Pages/Account/_ProviderButtons.cshtml new file mode 100644 index 00000000..1419ba11 --- /dev/null +++ b/LANCommander.Server/UI/Pages/Account/_ProviderButtons.cshtml @@ -0,0 +1,28 @@ +@model LANCommander.Server.Models.ProviderButtonsModel + +
+
+ @foreach (var provider in Model.Providers) + { +
+
+ + + + +
+
+ } +
+
diff --git a/LANCommander.Server/UI/Pages/Settings/Authentication/Index.razor b/LANCommander.Server/UI/Pages/Settings/Authentication/Index.razor index 2921da94..ebfe7aac 100644 --- a/LANCommander.Server/UI/Pages/Settings/Authentication/Index.razor +++ b/LANCommander.Server/UI/Pages/Settings/Authentication/Index.razor @@ -27,6 +27,10 @@ + + + +