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
This commit is contained in:
parent
ca7601e218
commit
0f03461f53
8 changed files with 221 additions and 34 deletions
|
|
@ -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;
|
||||
|
|
|
|||
10
LANCommander.Server/Models/ProviderButtonsModel.cs
Normal file
10
LANCommander.Server/Models/ProviderButtonsModel.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using LANCommander.Server.Settings.Models;
|
||||
|
||||
namespace LANCommander.Server.Models
|
||||
{
|
||||
public class ProviderButtonsModel
|
||||
{
|
||||
public IEnumerable<AuthenticationProvider> Providers { get; set; } = new List<AuthenticationProvider>();
|
||||
public string ReturnUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml
Normal file
38
LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml
Normal file
|
|
@ -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";
|
||||
}
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-container-background" style="background-image: url('@Model.ScreenshotUrl')"></div>
|
||||
<div class="ant-row login-box">
|
||||
<div class="ant-col ant-col-xs-24 ant-col-md-8 ant-col-lg-6 ant-col-md-push-4 ant-col-lg-push-6 login-form-pane">
|
||||
<div class="login-box-logo">
|
||||
<img src="~/static/logo-dark.svg"/>
|
||||
</div>
|
||||
|
||||
@foreach (var error in ModelState.SelectMany(x => x.Value.Errors))
|
||||
{
|
||||
<div data-show="true" class="ant-alert ant-alert-error ant-alert-no-icon" style="margin-bottom: 16px">
|
||||
<div class="ant-alert-content">
|
||||
<div class="ant-alert-message">@error.ErrorMessage</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<partial name="_ProviderButtons" model="@(new LANCommander.Server.Models.ProviderButtonsModel { Providers = Model.Providers, ReturnUrl = Model.ReturnUrl })"/>
|
||||
|
||||
<div class="ant-flex ant-flex-justify-center" style="margin-top: 16px;">
|
||||
<a href="~/Login?ReturnUrl=@Uri.EscapeDataString(Model.ReturnUrl ?? "/")">Sign in with a password</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ant-col ant-col-xs-24 ant-col-md-8 ant-col-lg-6 ant-col-md-push-4 ant-col-lg-push-6 login-download-pane" style="background-image: url('@Model.ScreenshotUrl')">
|
||||
@(await Html.RenderComponentAsync<LANCommander.Server.UI.Pages.Account.Components.LauncherDownloadButton>(RenderMode.Server))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
121
LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml.cs
Normal file
121
LANCommander.Server/UI/Pages/Account/ExternalLogin.cshtml.cs
Normal file
|
|
@ -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<User> SignInManager;
|
||||
private readonly RoleService RoleService;
|
||||
private readonly ILogger<ExternalLoginModel> Logger;
|
||||
private readonly IOptions<Server.Settings.Settings> Settings;
|
||||
|
||||
public ExternalLoginModel(
|
||||
SignInManager<User> signInManager,
|
||||
RoleService roleService,
|
||||
ILogger<ExternalLoginModel> logger,
|
||||
IOptions<Server.Settings.Settings> settings)
|
||||
{
|
||||
SignInManager = signInManager;
|
||||
RoleService = roleService;
|
||||
Logger = logger;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
public string ScreenshotUrl { get; set; }
|
||||
|
||||
public IEnumerable<AuthenticationProvider> Providers { get; set; } = new List<AuthenticationProvider>();
|
||||
|
||||
public async Task<IActionResult> 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<AuthenticationProvider>();
|
||||
|
||||
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<IActionResult> 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<string, string>()
|
||||
{
|
||||
{ "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, '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,32 +82,7 @@
|
|||
</span>
|
||||
</span>
|
||||
|
||||
<div class="authentication-provider-container">
|
||||
<div class="authentication-provider-button-group">
|
||||
@foreach (var provider in providers)
|
||||
{
|
||||
<div class="authentication-provider-button">
|
||||
<form method="post">
|
||||
<input type="hidden" name="Provider" value="@provider.Slug"/>
|
||||
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl"/>
|
||||
|
||||
<button class="ant-btn ant-btn-primary ant-btn-block" type="submit" style="@(!String.IsNullOrWhiteSpace(provider.Color) ? $"background-color: {provider.Color}; border-color: {provider.Color}" : "")">
|
||||
@if (!String.IsNullOrWhiteSpace(provider.Icon))
|
||||
{
|
||||
<span class="anticon" role="img">
|
||||
<svg focusable="false" width="1em" height="1em" fill="currentColor">
|
||||
<use xlink:href="_content/LANCommander.UI/bootstrap-icons.svg#@provider.Icon"/>
|
||||
</svg>
|
||||
</span>
|
||||
}
|
||||
|
||||
<span>Sign in using @provider.Name</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<partial name="_ProviderButtons" model="@(new LANCommander.Server.Models.ProviderButtonsModel { Providers = providers, ReturnUrl = Model.ReturnUrl })"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
28
LANCommander.Server/UI/Pages/Account/_ProviderButtons.cshtml
Normal file
28
LANCommander.Server/UI/Pages/Account/_ProviderButtons.cshtml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
@model LANCommander.Server.Models.ProviderButtonsModel
|
||||
|
||||
<div class="authentication-provider-container">
|
||||
<div class="authentication-provider-button-group">
|
||||
@foreach (var provider in Model.Providers)
|
||||
{
|
||||
<div class="authentication-provider-button">
|
||||
<form method="post">
|
||||
<input type="hidden" name="Provider" value="@provider.Slug"/>
|
||||
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl"/>
|
||||
|
||||
<button class="ant-btn ant-btn-primary ant-btn-block" type="submit" style="@(!String.IsNullOrWhiteSpace(provider.Color) ? $"background-color: {provider.Color}; border-color: {provider.Color}" : "")">
|
||||
@if (!String.IsNullOrWhiteSpace(provider.Icon))
|
||||
{
|
||||
<span class="anticon" role="img">
|
||||
<svg focusable="false" width="1em" height="1em" fill="currentColor">
|
||||
<use xlink:href="_content/LANCommander.UI/bootstrap-icons.svg#@provider.Icon"/>
|
||||
</svg>
|
||||
</span>
|
||||
}
|
||||
|
||||
<span>Sign in using @provider.Name</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -27,6 +27,10 @@
|
|||
<Switch @bind-Checked="context.Server.Authentication.RequireApproval" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem Label="Auto-redirect to external provider">
|
||||
<Switch @bind-Checked="context.Server.Authentication.AutoRedirectToProvider" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem Label="Token Secret">
|
||||
<InputPassword @bind-Value="context.Server.Authentication.TokenSecret" />
|
||||
</FormItem>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue