From 1682a123b4e0c21c60b1f661426e567914f17160 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 18 Dec 2025 16:00:46 -0500 Subject: [PATCH] login improvements --- Oqtane.Client/Modules/Admin/Login/Index.razor | 26 +++++++-------- .../Modules/Admin/UserProfile/Index.razor | 12 +++---- Oqtane.Client/Modules/Admin/Users/Edit.razor | 12 +++---- .../Resources/Modules/Admin/Login/Index.resx | 3 ++ Oqtane.Client/Services/UserService.cs | 6 ++-- Oqtane.Server/Controllers/UserController.cs | 8 ++--- Oqtane.Server/Managers/UserManager.cs | 6 ++-- Oqtane.Server/Pages/LoginLink.cshtml.cs | 33 +++++++++++-------- Oqtane.Server/Pages/Logout.cshtml.cs | 3 -- Oqtane.Server/Pages/Passkey.cshtml.cs | 4 ++- Oqtane.Shared/Shared/ExternalLoginStatus.cs | 1 + 11 files changed, 62 insertions(+), 52 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 4a6fee49..773f52a8 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -127,6 +127,7 @@ else private bool _allowsitelogin = true; private bool _allowloginlink = false; private bool _allowpasskeys = false; + private string _returnurl = string.Empty; private ElementReference login; private bool validated = false; @@ -169,6 +170,9 @@ else _registerurl = NavigateUrl("register"); } + // PageState.ReturnUrl is not specified if user navigated directly to login page + _returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path; + _togglepassword = SharedLocalizer["ShowPassword"]; if (PageState.QueryString.ContainsKey("name")) @@ -216,7 +220,7 @@ else { if (PageState.QueryString.ContainsKey("status")) { - AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info); + AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning); } } } @@ -252,7 +256,7 @@ else private void ExternalLogin() { - NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + WebUtility.UrlEncode(PageState.ReturnUrl)), true); + NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + WebUtility.UrlEncode(_returnurl)), true); } private void TogglePassword() @@ -294,20 +298,17 @@ else { await logger.LogInformation(LogFunction.Security, "Login Successful For {Username} From IP Address {IPAddress}", _username, SiteState.RemoteIPAddress); - // return url is not specified if user navigated directly to login page - var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path; - if (hybrid) { // hybrid apps utilize an interactive login var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); authstateprovider.NotifyAuthenticationChanged(); - NavigationManager.NavigateTo(NavigateUrl(returnurl, true)); + NavigationManager.NavigateTo(NavigateUrl(_returnurl, true)); } else { // post back to the Login page so that the cookies are set correctly - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(returnurl) }; + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(_returnurl) }; string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); await interop.SubmitForm(url, fields); } @@ -349,14 +350,14 @@ else private void CancelLogin() { - NavigationManager.NavigateTo(PageState.ReturnUrl); + NavigationManager.NavigateTo(_returnurl); } private async Task PasskeyLogin() { // post back to the Passkey page so that the cookies are set correctly var interop = new Interop(JSRuntime); - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "request", returnurl = NavigateUrl() }; + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "request", returnurl = _returnurl }; string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/"); await interop.SubmitForm(url, fields); } @@ -423,7 +424,7 @@ else { if (!string.IsNullOrEmpty(_email)) { - if (await UserService.SendLoginLinkAsync(_email)) + if (await UserService.SendLoginLinkAsync(_email, _returnurl)) { AddModuleMessage(Localizer["Message.SendLoginLink"], MessageType.Info); await logger.LogInformation(LogFunction.Security, "Login Link Sent To Email {Email}", _email); @@ -457,8 +458,7 @@ else if (!string.IsNullOrEmpty(credential)) { // post back to the Passkey page so that the cookies are set correctly - var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path + "/"; - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "login", credential = credential, returnurl = returnurl }; + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "login", credential = credential, returnurl = _returnurl }; string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/"); await interop.SubmitForm(url, fields); } @@ -497,7 +497,7 @@ else // redirect logged in user to specified page if (PageState.User != null && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) { - NavigationManager.NavigateTo(PageState.ReturnUrl); + NavigationManager.NavigateTo(_returnurl); } } } diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index c8113d11..705de755 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -114,9 +114,9 @@ } @if (_allowpasskeys) { -
+
- @if (_passkeys != null && _passkeys.Count > 0) + @if (_passkeys.Count > 0) {
@@ -142,15 +142,15 @@ } else { -
@Localizer["Message.Passkeys.None"]
+
@Localizer["Message.Passkeys.None"]
}

} @if (_allowexternallogin) { -
- @if (_logins != null && _logins.Count > 0) +
+ @if (_logins.Count > 0) {
@@ -165,7 +165,7 @@ } else { -
@Localizer["Message.Logins.None"]
+
@Localizer["Message.Logins.None"]
}

diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index 09199202..db0ea36b 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -106,8 +106,8 @@

@if (_allowpasskeys) { -
- @if (_passkeys != null && _passkeys.Count > 0) +
+ @if (_passkeys.Count > 0) {
@@ -122,15 +122,15 @@ } else { -
@Localizer["Message.Passkeys.None"]
+
@Localizer["Message.Passkeys.None"]
}

} @if (_allowexternallogin) { -
- @if (_logins != null && _logins.Count > 0) +
+ @if (_logins.Count > 0) {
@@ -145,7 +145,7 @@ } else { -
@Localizer["Message.Logins.None"]
+
@Localizer["Message.Logins.None"]
}

diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index af2a52ba..bbb7bc5d 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -246,6 +246,9 @@ Login Links Are Time Sensitive. Please Request Another Login Link To Complete The Login Process. + + Passkey Login Was Unsuccessful. Please Ensure You Selected The Correct Passkey For This Site. + Register as new user? diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 76de84e0..049a93d5 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -224,7 +224,7 @@ namespace Oqtane.Services /// /// /// - Task SendLoginLinkAsync(string email); + Task SendLoginLinkAsync(string email, string returnurl); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -386,9 +386,9 @@ namespace Oqtane.Services await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}"); } - public async Task SendLoginLinkAsync(string email) + public async Task SendLoginLinkAsync(string email, string returnurl) { - return await GetJsonAsync($"{Apiurl}/loginlink/{WebUtility.UrlEncode(email)}"); + return await GetJsonAsync($"{Apiurl}/loginlink/{WebUtility.UrlEncode(email)}/{WebUtility.UrlEncode(returnurl)}"); } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index ece7d295..aa6aa909 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -563,11 +563,11 @@ namespace Oqtane.Controllers } } - // GET api//loginlink/x - [HttpGet("loginlink/{email}")] - public async Task SendLoginLink(string email) + // GET api//loginlink/x/y + [HttpGet("loginlink/{email}/{returnurl}")] + public async Task SendLoginLink(string email, string returnurl) { - return await _userManager.SendLoginLink(email); + return await _userManager.SendLoginLink(email, returnurl); } } } diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 3e032400..ea76e39a 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -41,7 +41,7 @@ namespace Oqtane.Managers Task> GetLogins(int userId, int siteId); Task AddLogin(User user, string token, string type, string key, string name); Task DeleteLogin(int userId, string provider, string key); - Task SendLoginLink(string email); + Task SendLoginLink(string email, string returnurl); } public class UserManager : IUserManager @@ -960,7 +960,7 @@ namespace Oqtane.Managers } } - public async Task SendLoginLink(string email) + public async Task SendLoginLink(string email, string returnurl) { try { @@ -973,7 +973,7 @@ namespace Oqtane.Managers var alias = _tenantManager.GetAlias(); var user = GetUser(identityuser.UserName, alias.SiteId); - string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token) + "&returnurl=" + WebUtility.UrlEncode(returnurl); string siteName = _sites.GetSite(alias.SiteId).Name; string subject = _localizer["LoginLinkEmailSubject"]; subject = subject.Replace("[SiteName]", siteName); diff --git a/Oqtane.Server/Pages/LoginLink.cshtml.cs b/Oqtane.Server/Pages/LoginLink.cshtml.cs index d090c7ed..16b34d0c 100644 --- a/Oqtane.Server/Pages/LoginLink.cshtml.cs +++ b/Oqtane.Server/Pages/LoginLink.cshtml.cs @@ -27,38 +27,45 @@ namespace Oqtane.Pages _logger = logger; } - public async Task OnGetAsync(string name, string token) + public async Task OnGetAsync(string name, string token, string returnurl) { - var returnurl = "/login"; + returnurl = (returnurl == null) ? "" : WebUtility.UrlDecode(returnurl); if (bool.Parse(HttpContext.GetSiteSettings().GetValue("LoginOptions:LoginLink", "false")) && - !User.Identity.IsAuthenticated && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(token)) + !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(token)) { var validuser = false; - IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name); - if (identityuser != null) + if (!User.Identity.IsAuthenticated) { - var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token); - if (result.Succeeded) + IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name); + if (identityuser != null) { - await _identitySignInManager.SignInAsync(identityuser, false); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name); - validuser = true; - returnurl = "/"; + var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token); + if (result.Succeeded) + { + await _identitySignInManager.SignInAsync(identityuser, false); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name); + validuser = true; + } } } if (!validuser) { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Failed For User {Username}", name); - returnurl += $"?status={ExternalLoginStatus.LoginLinkFailed}"; + returnurl = HttpContext.GetAlias().Path + $"/login?status={ExternalLoginStatus.LoginLinkFailed}"; } } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Login Link Attempt For User {Username}", name); - returnurl = "/"; + returnurl = HttpContext.GetAlias().Path; + } + + if (!returnurl.StartsWith("/")) + { + returnurl = "/" + returnurl; } return LocalRedirect(Url.Content("~" + returnurl)); diff --git a/Oqtane.Server/Pages/Logout.cshtml.cs b/Oqtane.Server/Pages/Logout.cshtml.cs index 3d72d2ec..86263836 100644 --- a/Oqtane.Server/Pages/Logout.cshtml.cs +++ b/Oqtane.Server/Pages/Logout.cshtml.cs @@ -1,8 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -11,7 +9,6 @@ using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Managers; using Oqtane.Shared; -using Radzen.Blazor.Markdown; namespace Oqtane.Pages { diff --git a/Oqtane.Server/Pages/Passkey.cshtml.cs b/Oqtane.Server/Pages/Passkey.cshtml.cs index 8923d78d..89d3ee2e 100644 --- a/Oqtane.Server/Pages/Passkey.cshtml.cs +++ b/Oqtane.Server/Pages/Passkey.cshtml.cs @@ -10,6 +10,7 @@ using Oqtane.Infrastructure; using Oqtane.Managers; using Oqtane.Security; using Oqtane.Shared; +using Oqtane.UI; namespace Oqtane.Pages { @@ -103,7 +104,7 @@ namespace Oqtane.Pages { identityuser = null; var requestOptionsJson = await _identitySignInManager.MakePasskeyRequestOptionsAsync(identityuser); - returnurl += $"?options={WebUtility.UrlEncode(requestOptionsJson)}"; + returnurl = HttpContext.GetAlias().Path + $"/login?options={WebUtility.UrlEncode(requestOptionsJson)}&returnurl={WebUtility.UrlEncode(returnurl)}"; } else { @@ -129,6 +130,7 @@ namespace Oqtane.Pages else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Passkey Login Failed - Invalid Credential"); + returnurl = HttpContext.GetAlias().Path + $"/login?status={ExternalLoginStatus.PasskeyFailed}&returnurl={WebUtility.UrlEncode(returnurl)}"; } } else diff --git a/Oqtane.Shared/Shared/ExternalLoginStatus.cs b/Oqtane.Shared/Shared/ExternalLoginStatus.cs index 8423d799..8acd1694 100644 --- a/Oqtane.Shared/Shared/ExternalLoginStatus.cs +++ b/Oqtane.Shared/Shared/ExternalLoginStatus.cs @@ -11,5 +11,6 @@ namespace Oqtane.Shared { public const string RemoteFailure = "RemoteFailure"; public const string ReviewClaims = "ReviewClaims"; public const string LoginLinkFailed = "LoginLinkFailed"; + public const string PasskeyFailed = "PasskeyFailed"; } }