passkey adjustments

This commit is contained in:
sbwalker
2025-10-30 09:15:40 -04:00
parent d5ad29be34
commit d774557522
7 changed files with 42 additions and 30 deletions

View File

@@ -52,7 +52,7 @@ else
</div> </div>
<button type="button" class="btn btn-secondary col-12 mt-4" @onclick="Forgot">@Localizer["ForgotPassword"]</button> <button type="button" class="btn btn-secondary col-12 mt-4" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (_allowpasskeys && PageState.Route.Scheme == "https") @if (_allowpasskeys)
{ {
<hr class="app-rule mt-3" /> <hr class="app-rule mt-3" />
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="Passkey">@Localizer["Passkey"]</button> <button type="button" class="btn btn-primary col-12 mt-2" @onclick="Passkey">@Localizer["Passkey"]</button>
@@ -116,7 +116,7 @@ else
{ {
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false; _allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); _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")); _alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false"));
if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", ""))) if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "")))
@@ -365,14 +365,14 @@ else
} }
else else
{ {
await logger.LogError("Error Logging In With Passkey"); await logger.LogError("Passkey Login Was Not Successful");
AddModuleMessage(Localizer["Error.Passkey"], MessageType.Error); AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Logging In With Passkey"); await logger.LogError(ex, "Passkey Login Was Not Successful");
AddModuleMessage(Localizer["Error.Passkey"], MessageType.Error); AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
} }
return; return;
} }

View File

@@ -115,14 +115,7 @@
@if (_allowpasskeys) @if (_allowpasskeys)
{ {
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys"> <Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
@if (PageState.Route.Scheme == "https") <button type="button" class="btn btn-primary" @onclick="AddPasskey">@SharedLocalizer["Add"]</button>
{
<button type="button" class="btn btn-primary" @onclick="AddPasskey">@SharedLocalizer["Add"]</button>
}
else
{
<ModuleMessage Type="MessageType.Warning" Message="@Localizer["Message.Passkeys.Insecure"]" />
}
@if (_passkeys != null && _passkeys.Count > 0) @if (_passkeys != null && _passkeys.Count > 0)
{ {
<Pager Items="@_passkeys"> <Pager Items="@_passkeys">
@@ -669,7 +662,7 @@
{ {
// post back to the Passkey page so that the cookies are set correctly // post back to the Passkey page so that the cookies are set correctly
var interop = new Interop(JSRuntime); 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/"); string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields); await interop.SubmitForm(url, fields);
} }
@@ -694,14 +687,14 @@
} }
else else
{ {
await logger.LogError("Error Adding Passkey"); await logger.LogError("Passkey Could Not Be Created");
AddModuleMessage(Localizer["Error.Passkey"], MessageType.Error); AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Adding Passkey"); await logger.LogError(ex, "Passkey Could Not Be Created");
AddModuleMessage(Localizer["Error.Passkey"], MessageType.Error); AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
} }
} }
} }

View File

@@ -234,4 +234,7 @@
<data name="Passkey" xml:space="preserve"> <data name="Passkey" xml:space="preserve">
<value>Use Passkey</value> <value>Use Passkey</value>
</data> </data>
<data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Login Was Not Successful</value>
</data>
</root> </root>

View File

@@ -279,13 +279,13 @@
<data name="Confirm.Login.Delete" xml:space="preserve"> <data name="Confirm.Login.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value> <value>Are You Sure You Wish To Delete {0}?</value>
</data> </data>
<data name="Message.Passkeys.Insecure" xml:space="preserve">
<value>Passkeys Can Only Be Created Using a Secure Browser Connection</value>
</data>
<data name="Message.Passkeys.None" xml:space="preserve"> <data name="Message.Passkeys.None" xml:space="preserve">
<value>You Have Not Created Any Passkeys</value> <value>You Have Not Created Any Passkeys</value>
</data> </data>
<data name="Message.Logins.None" xml:space="preserve"> <data name="Message.Logins.None" xml:space="preserve">
<value>You Do Not Have Any External Logins For This Site</value> <value>You Do Not Have Any External Logins For This Site</value>
</data> </data>
<data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Could Not Be Created</value>
</data>
</root> </root>

View File

@@ -471,7 +471,7 @@ namespace Oqtane.Controllers
[Authorize] [Authorize]
public async Task<IEnumerable<UserPasskey>> GetPasskeys() public async Task<IEnumerable<UserPasskey>> GetPasskeys()
{ {
return await _userManager.GetPasskeys(_userPermissions.GetUser(User).UserId); return await _userManager.GetPasskeys(_userPermissions.GetUser(User).UserId, _tenantManager.GetAlias().SiteId);
} }
// PUT api/<controller>/passkey // PUT api/<controller>/passkey
@@ -481,6 +481,8 @@ namespace Oqtane.Controllers
{ {
if (ModelState.IsValid) 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; passkey.UserId = _userPermissions.GetUser(User).UserId;
await _userManager.UpdatePasskey(passkey); await _userManager.UpdatePasskey(passkey);
} }

View File

@@ -37,7 +37,7 @@ namespace Oqtane.Managers
Task<UserValidateResult> ValidateUser(string username, string email, string password); Task<UserValidateResult> ValidateUser(string username, string email, string password);
Task<bool> ValidatePassword(string password); Task<bool> ValidatePassword(string password);
Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify); Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify);
Task<List<UserPasskey>> GetPasskeys(int userId); Task<List<UserPasskey>> GetPasskeys(int userId, int siteId);
Task UpdatePasskey(UserPasskey passkey); Task UpdatePasskey(UserPasskey passkey);
Task DeletePasskey(int userId, byte[] credentialId); Task DeletePasskey(int userId, byte[] credentialId);
Task<List<UserLogin>> GetLogins(int userId, int siteId); Task<List<UserLogin>> GetLogins(int userId, int siteId);
@@ -826,7 +826,7 @@ namespace Oqtane.Managers
return result; return result;
} }
public async Task<List<UserPasskey>> GetPasskeys(int userId) public async Task<List<UserPasskey>> GetPasskeys(int userId, int siteId)
{ {
var passkeys = new List<UserPasskey>(); var passkeys = new List<UserPasskey>();
var user = _users.GetUser(userId); var user = _users.GetUser(userId);
@@ -838,7 +838,11 @@ namespace Oqtane.Managers
var userpasskeys = await _identityUserManager.GetPasskeysAsync(identityuser); var userpasskeys = await _identityUserManager.GetPasskeysAsync(identityuser);
foreach (var userpasskey in userpasskeys) 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 });
}
} }
} }
} }

View File

@@ -49,7 +49,7 @@ namespace Oqtane.Pages
Name = identityuser.UserName, Name = identityuser.UserName,
DisplayName = identityuser.UserName DisplayName = identityuser.UserName
}); });
returnurl += $"?options={WebUtility.UrlEncode(creationOptionsJson)}"; returnurl += (!returnurl.Contains("?") ? "?" : "&") + $"options={WebUtility.UrlEncode(creationOptionsJson)}";
} }
else else
{ {
@@ -70,8 +70,18 @@ namespace Oqtane.Pages
var attestationResult = await _identitySignInManager.PerformPasskeyAttestationAsync(credential); var attestationResult = await _identitySignInManager.PerformPasskeyAttestationAsync(credential);
if (attestationResult.Succeeded) if (attestationResult.Succeeded)
{ {
attestationResult.Passkey.Name = identityuser.UserName + "'s Passkey"; var user = _userManager.GetUser(User.Identity.Name, HttpContext.GetAlias().SiteId);
var addPasskeyResult = await _identityUserManager.AddOrUpdatePasskeyAsync(identityuser, attestationResult.Passkey); 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 else
{ {
@@ -113,7 +123,7 @@ namespace Oqtane.Pages
} }
else 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 else