diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 162196ff..02d45a3d 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -52,7 +52,7 @@ else - @if (_allowpasskeys && PageState.Route.Scheme == "https") + @if (_allowpasskeys) {
@@ -116,7 +116,7 @@ else { _allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false; _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); - _allowpasskeys = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowPasskeys", "false")); + _allowpasskeys = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false")); _alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false")); if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", ""))) @@ -365,14 +365,14 @@ else } else { - await logger.LogError("Error Logging In With Passkey"); - AddModuleMessage(Localizer["Error.Passkey"], MessageType.Error); + await logger.LogError("Passkey Login Was Not Successful"); + AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning); } } catch (Exception ex) { - await logger.LogError(ex, "Error Logging In With Passkey"); - AddModuleMessage(Localizer["Error.Passkey"], MessageType.Error); + await logger.LogError(ex, "Passkey Login Was Not Successful"); + AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning); } return; } diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 1bf585f6..6e9a9efe 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -115,14 +115,7 @@ @if (_allowpasskeys) {
- @if (PageState.Route.Scheme == "https") - { - - } - else - { - - } + @if (_passkeys != null && _passkeys.Count > 0) { @@ -669,7 +662,7 @@ { // 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 = "create", returnurl = NavigateUrl() }; + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "create", returnurl = NavigateUrl(PageState.Page.Path, "tab=Security") }; string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/"); await interop.SubmitForm(url, fields); } @@ -694,14 +687,14 @@ } else { - await logger.LogError("Error Adding Passkey"); - AddModuleMessage(Localizer["Error.Passkey"], MessageType.Error); + await logger.LogError("Passkey Could Not Be Created"); + AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning); } } catch (Exception ex) { - await logger.LogError(ex, "Error Adding Passkey"); - AddModuleMessage(Localizer["Error.Passkey"], MessageType.Error); + await logger.LogError(ex, "Passkey Could Not Be Created"); + AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning); } } } diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 84a53b4a..de17269f 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -234,4 +234,7 @@ Use Passkey + + Passkey Login Was Not Successful + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx index 5c2a5261..9433b281 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx @@ -279,13 +279,13 @@ Are You Sure You Wish To Delete {0}? - - Passkeys Can Only Be Created Using a Secure Browser Connection - You Have Not Created Any Passkeys You Do Not Have Any External Logins For This Site + + Passkey Could Not Be Created + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 741b9118..875e6e12 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -471,7 +471,7 @@ namespace Oqtane.Controllers [Authorize] public async Task> GetPasskeys() { - return await _userManager.GetPasskeys(_userPermissions.GetUser(User).UserId); + return await _userManager.GetPasskeys(_userPermissions.GetUser(User).UserId, _tenantManager.GetAlias().SiteId); } // PUT api//passkey @@ -481,6 +481,8 @@ namespace Oqtane.Controllers { if (ModelState.IsValid) { + // passkey name is prefixed with SiteId for multi-tenancy + passkey.Name = $"{_tenantManager.GetAlias().SiteId}:" + passkey.Name; passkey.UserId = _userPermissions.GetUser(User).UserId; await _userManager.UpdatePasskey(passkey); } diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 0ce7fd39..b56a606d 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -37,7 +37,7 @@ namespace Oqtane.Managers Task ValidateUser(string username, string email, string password); Task ValidatePassword(string password); Task> ImportUsers(int siteId, string filePath, bool notify); - Task> GetPasskeys(int userId); + Task> GetPasskeys(int userId, int siteId); Task UpdatePasskey(UserPasskey passkey); Task DeletePasskey(int userId, byte[] credentialId); Task> GetLogins(int userId, int siteId); @@ -826,7 +826,7 @@ namespace Oqtane.Managers return result; } - public async Task> GetPasskeys(int userId) + public async Task> GetPasskeys(int userId, int siteId) { var passkeys = new List(); var user = _users.GetUser(userId); @@ -838,7 +838,11 @@ namespace Oqtane.Managers var userpasskeys = await _identityUserManager.GetPasskeysAsync(identityuser); foreach (var userpasskey in userpasskeys) { - passkeys.Add(new UserPasskey { CredentialId = userpasskey.CredentialId, Name = userpasskey.Name, UserId = userId }); + // passkey name is prefixed with SiteId for multi-tenancy + if (userpasskey.Name.StartsWith($"{siteId}:")) + { + passkeys.Add(new UserPasskey { CredentialId = userpasskey.CredentialId, Name = userpasskey.Name.Split(':')[1], UserId = userId }); + } } } } diff --git a/Oqtane.Server/Pages/Passkey.cshtml.cs b/Oqtane.Server/Pages/Passkey.cshtml.cs index ad6a35d3..8923d78d 100644 --- a/Oqtane.Server/Pages/Passkey.cshtml.cs +++ b/Oqtane.Server/Pages/Passkey.cshtml.cs @@ -49,7 +49,7 @@ namespace Oqtane.Pages Name = identityuser.UserName, DisplayName = identityuser.UserName }); - returnurl += $"?options={WebUtility.UrlEncode(creationOptionsJson)}"; + returnurl += (!returnurl.Contains("?") ? "?" : "&") + $"options={WebUtility.UrlEncode(creationOptionsJson)}"; } else { @@ -70,8 +70,18 @@ namespace Oqtane.Pages var attestationResult = await _identitySignInManager.PerformPasskeyAttestationAsync(credential); if (attestationResult.Succeeded) { - attestationResult.Passkey.Name = identityuser.UserName + "'s Passkey"; - var addPasskeyResult = await _identityUserManager.AddOrUpdatePasskeyAsync(identityuser, attestationResult.Passkey); + var user = _userManager.GetUser(User.Identity.Name, HttpContext.GetAlias().SiteId); + if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered)) + { + // setting a default name and including a SiteId prefix for multi-tenancy + var name = (!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username; + attestationResult.Passkey.Name = HttpContext.GetAlias().SiteId + ":" + name + "'s Passkey"; + var addPasskeyResult = await _identityUserManager.AddOrUpdatePasskeyAsync(identityuser, attestationResult.Passkey); + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Passkey Validation Failed - User {Username} Is Deleted Or Is Not A Registered User For The Site", User.Identity.Name); + } } else { @@ -113,7 +123,7 @@ namespace Oqtane.Pages } else { - _logger.Log(LogLevel.Information, this, LogFunction.Security, "Passkey Login Failed For User {Username}", User.Identity.Name); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Passkey Login Failed - User {Username} Is Deleted Or Is Not A Registered User For The Site", User.Identity.Name); } } else