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 @@ + + + +