Merge pull request #5755 from sbwalker/dev

passkey adjustments
This commit is contained in:
Shaun Walker
2025-10-30 09:15:55 -04:00
committed by GitHub
7 changed files with 42 additions and 30 deletions

View File

@ -52,7 +52,7 @@ else
</div>
<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" />
<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;
_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;
}

View File

@ -115,14 +115,7 @@
@if (_allowpasskeys)
{
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
@if (PageState.Route.Scheme == "https")
{
<button type="button" class="btn btn-primary" @onclick="AddPasskey">@SharedLocalizer["Add"]</button>
}
else
{
<ModuleMessage Type="MessageType.Warning" Message="@Localizer["Message.Passkeys.Insecure"]" />
}
<button type="button" class="btn btn-primary" @onclick="AddPasskey">@SharedLocalizer["Add"]</button>
@if (_passkeys != null && _passkeys.Count > 0)
{
<Pager Items="@_passkeys">
@ -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);
}
}
}

View File

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

View File

@ -279,13 +279,13 @@
<data name="Confirm.Login.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</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">
<value>You Have Not Created Any Passkeys</value>
</data>
<data name="Message.Logins.None" xml:space="preserve">
<value>You Do Not Have Any External Logins For This Site</value>
</data>
<data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Could Not Be Created</value>
</data>
</root>

View File

@ -471,7 +471,7 @@ namespace Oqtane.Controllers
[Authorize]
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
@ -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);
}

View File

@ -37,7 +37,7 @@ namespace Oqtane.Managers
Task<UserValidateResult> ValidateUser(string username, string email, string password);
Task<bool> ValidatePassword(string password);
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 DeletePasskey(int userId, byte[] credentialId);
Task<List<UserLogin>> GetLogins(int userId, int siteId);
@ -826,7 +826,7 @@ namespace Oqtane.Managers
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 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 });
}
}
}
}

View File

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