@@ -538,6 +547,7 @@ else
private string _registerurl;
private string _profileurl;
private string _requireconfirmedemail;
+ private string _passkeys;
private string _twofactor;
private string _cookiename;
private string _cookiedomain;
@@ -609,12 +619,13 @@ else
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
- _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
- _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
- _requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
+ _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
+ _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
+ _requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
+ _passkeys = SettingService.GetSetting(settings, "LoginOptions:Passkeys", "false");
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_cookiedomain = SettingService.GetSetting(settings, "LoginOptions:CookieDomain", "");
@@ -753,6 +764,7 @@ else
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false);
+ settings = SettingService.SetSetting(settings, "LoginOptions:Passkeys", _passkeys, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieDomain", _cookiedomain, true);
diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx
index a60a2044..84a53b4a 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx
@@ -118,7 +118,7 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- Forgot Password
+ Forgot Password?
User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password.
@@ -231,4 +231,7 @@
Register as new user?
+
+ Use Passkey
+
\ 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 2a6862bb..e2a1552f 100644
--- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx
@@ -168,9 +168,6 @@
Are You Sure You Wish To Delete This Notification?
-
- Identity
-
If you are changing your password you must enter it again to confirm it matches the value entered above
@@ -211,7 +208,7 @@
Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in.
- Two Factor?
+ Use Two Factor?
Clear Notifications
@@ -249,4 +246,40 @@
Your time zone
+
+ Identity
+
+
+ Security
+
+
+ Multi-Factor Authenticationxxx
+
+
+ Passkeys
+
+
+ Logins
+
+
+ Passkey
+
+
+ Delete Passkey
+
+
+ Are You Sure You Wish To Delete {0}?
+
+
+ Login
+
+
+ Delete Login
+
+
+ Are You Sure You Wish To Delete {0}?
+
+
+ Passkeys Can Only Be Created Using a Secure Browser Connection
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
index 310e6dc3..6e7fb3e0 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
@@ -561,4 +561,10 @@
Specify if users should be logged out of both the application and provider (the default is false indicating they will only be logged out of the application)
+
+ Allow Passkeys?
+
+
+ Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)?
+
\ No newline at end of file
diff --git a/Oqtane.Client/Services/ServiceBase.cs b/Oqtane.Client/Services/ServiceBase.cs
index f7302cb7..ec5b4422 100644
--- a/Oqtane.Client/Services/ServiceBase.cs
+++ b/Oqtane.Client/Services/ServiceBase.cs
@@ -3,12 +3,14 @@ using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
+using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Shared;
+using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Oqtane.Services
{
@@ -206,6 +208,17 @@ namespace Oqtane.Services
await CheckResponse(response, uri);
}
+ protected async Task PostStringAsync(string uri)
+ {
+ var response = await GetHttpClient().PostAsync(uri, null);
+ if (await CheckResponse(response, uri) && ValidateJsonContent(response.Content))
+ {
+ var result = await response.Content.ReadAsStringAsync();
+ return result;
+ }
+ return default;
+ }
+
protected async Task PostJsonAsync(string uri, T value)
{
return await PostJsonAsync(uri, value);
diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs
index 6d249b5a..1086348b 100644
--- a/Oqtane.Client/Services/UserService.cs
+++ b/Oqtane.Client/Services/UserService.cs
@@ -1,11 +1,12 @@
-using Oqtane.Shared;
-using Oqtane.Models;
+using System.Buffers.Text;
+using System.Collections.Generic;
+using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
-using Oqtane.Documentation;
-using System.Net;
-using System.Collections.Generic;
using Microsoft.Extensions.Localization;
+using Oqtane.Documentation;
+using Oqtane.Models;
+using Oqtane.Shared;
namespace Oqtane.Services
{
@@ -177,21 +178,14 @@ namespace Oqtane.Services
/// Get passkeys for a user
///
///
- Task> GetPasskeysAsync();
-
- ///
- /// Add a user passkey
- ///
- ///
- ///
- Task AddPasskeyAsync(Passkey passkey);
+ Task> GetPasskeysAsync();
///
/// Update a user passkey
///
///
///
- Task UpdatePasskeyAsync(Passkey passkey);
+ Task UpdatePasskeyAsync(UserPasskey passkey);
///
/// Delete a user passkey
@@ -199,6 +193,20 @@ namespace Oqtane.Services
///
///
Task DeletePasskeyAsync(byte[] credentialId);
+
+ ///
+ /// Get logins for a user
+ ///
+ ///
+ Task> GetLoginsAsync();
+
+ ///
+ /// Delete a user login
+ ///
+ ///
+ ///
+ ///
+ Task DeleteLoginAsync(string provider, string key);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -330,24 +338,29 @@ namespace Oqtane.Services
return await PostJsonAsync>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}¬ify={notify}", null);
}
- public async Task> GetPasskeysAsync()
+ public async Task> GetPasskeysAsync()
{
- return await GetJsonAsync>($"{Apiurl}/passkey");
+ return await GetJsonAsync>($"{Apiurl}/passkey");
}
- public async Task AddPasskeyAsync(Passkey passkey)
+ public async Task UpdatePasskeyAsync(UserPasskey passkey)
{
- return await PostJsonAsync($"{Apiurl}/passkey", passkey);
- }
-
- public async Task UpdatePasskeyAsync(Passkey passkey)
- {
- return await PutJsonAsync($"{Apiurl}/passkey", passkey);
+ return await PutJsonAsync($"{Apiurl}/passkey", passkey);
}
public async Task DeletePasskeyAsync(byte[] credentialId)
{
- await DeleteAsync($"{Apiurl}/passkey?id={credentialId}");
+ await DeleteAsync($"{Apiurl}/passkey?id={Base64Url.EncodeToString(credentialId)}");
+ }
+
+ public async Task> GetLoginsAsync()
+ {
+ return await GetJsonAsync>($"{Apiurl}/login");
+ }
+
+ public async Task DeleteLoginAsync(string provider, string key)
+ {
+ await DeleteAsync($"{Apiurl}/login?provider={provider}&key={key}");
}
}
}
diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs
index 3a56783f..c8d09e33 100644
--- a/Oqtane.Client/UI/Interop.cs
+++ b/Oqtane.Client/UI/Interop.cs
@@ -417,5 +417,30 @@ namespace Oqtane.UI
return Task.CompletedTask;
}
}
+
+ public ValueTask CreateCredential(string optionsResponse)
+ {
+ try
+ {
+ return _jsRuntime.InvokeAsync("Oqtane.Interop.createCredential", optionsResponse);
+ }
+ catch
+ {
+ return new ValueTask(Task.FromResult(string.Empty));
+ }
+ }
+
+ public ValueTask RequestCredential(string optionsResponse)
+ {
+ try
+ {
+ return _jsRuntime.InvokeAsync("Oqtane.Interop.requestCredential", optionsResponse);
+ }
+ catch
+ {
+ return new ValueTask(Task.FromResult(string.Empty));
+ }
+ }
+
}
}
diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs
index 2141acc6..373a354a 100644
--- a/Oqtane.Server/Controllers/UserController.cs
+++ b/Oqtane.Server/Controllers/UserController.cs
@@ -1,19 +1,21 @@
-using Microsoft.AspNetCore.Mvc;
+using System.Buffers.Text;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Security.Claims;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
-using Oqtane.Models;
-using System.Threading.Tasks;
-using System.Linq;
-using System.Security.Claims;
-using Oqtane.Shared;
-using System.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
+using Oqtane.Extensions;
using Oqtane.Infrastructure;
+using Oqtane.Managers;
+using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Security;
-using Oqtane.Extensions;
-using Oqtane.Managers;
-using System.Collections.Generic;
+using Oqtane.Shared;
namespace Oqtane.Controllers
{
@@ -467,32 +469,15 @@ namespace Oqtane.Controllers
// GET: api//passkey
[HttpGet("passkey")]
[Authorize]
- public async Task> GetPasskeys()
+ public async Task> GetPasskeys()
{
return await _userManager.GetPasskeys(_userPermissions.GetUser(User).UserId);
}
- // POST api//passkey
- [HttpPost("passkey")]
- [Authorize]
- public async Task AddPasskey([FromBody] Passkey passkey)
- {
- if (ModelState.IsValid)
- {
- passkey.UserId = _userPermissions.GetUser(User).UserId;
- await _userManager.AddPasskey(passkey);
- }
- else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Post Attempt {PassKey}", passkey);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
- }
- }
-
// PUT api//passkey
[HttpPut("passkey")]
[Authorize]
- public async Task UpdatePasskey([FromBody] Passkey passkey)
+ public async Task UpdatePasskey([FromBody] UserPasskey passkey)
{
if (ModelState.IsValid)
{
@@ -509,9 +494,25 @@ namespace Oqtane.Controllers
// DELETE api//passkey?id=x
[HttpDelete("passkey")]
[Authorize]
- public async Task DeletePasskey(byte[] id)
+ public async Task DeletePasskey(string id)
{
- await _userManager.DeletePasskey(_userPermissions.GetUser(User).UserId, id);
+ await _userManager.DeletePasskey(_userPermissions.GetUser(User).UserId, Base64Url.DecodeFromChars(id));
+ }
+
+ // GET: api//login
+ [HttpGet("login")]
+ [Authorize]
+ public async Task> GetLogins()
+ {
+ return await _userManager.GetLogins(_userPermissions.GetUser(User).UserId);
+ }
+
+ // DELETE api//login?provider=x&key=y
+ [HttpDelete("login")]
+ [Authorize]
+ public async Task DeleteLogin(string provider, string key)
+ {
+ await _userManager.DeleteLogin(_userPermissions.GetUser(User).UserId, provider, key);
}
}
}
diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
index 115abee0..e72ddc4e 100644
--- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
+++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
@@ -102,7 +102,8 @@ namespace Microsoft.Extensions.DependencyInjection
})
.AddCookie(Constants.AuthenticationScheme)
.AddOpenIdConnect(AuthenticationProviderTypes.OpenIDConnect, options => { })
- .AddOAuth(AuthenticationProviderTypes.OAuth2, options => { });
+ .AddOAuth(AuthenticationProviderTypes.OAuth2, options => { })
+ .AddTwoFactorUserIdCookie();
services.ConfigureOqtaneCookieOptions();
services.ConfigureOqtaneAuthenticationOptions(configuration);
diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs
index 8c4bf457..3c28081d 100644
--- a/Oqtane.Server/Managers/UserManager.cs
+++ b/Oqtane.Server/Managers/UserManager.cs
@@ -4,9 +4,9 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
-using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
using Oqtane.Enums;
@@ -36,10 +36,11 @@ 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 AddPasskey(Passkey passkey);
- Task UpdatePasskey(Passkey passkey);
+ Task> GetPasskeys(int userId);
+ Task UpdatePasskey(UserPasskey passkey);
Task DeletePasskey(int userId, byte[] credentialId);
+ Task> GetLogins(int userId);
+ Task DeleteLogin(int userId, string provider, string key);
}
public class UserManager : IUserManager
@@ -824,9 +825,9 @@ namespace Oqtane.Managers
return result;
}
- public async Task> GetPasskeys(int userId)
+ public async Task> GetPasskeys(int userId)
{
- var passkeys = new List();
+ var passkeys = new List();
var user = _users.GetUser(userId);
if (user != null)
{
@@ -836,31 +837,14 @@ namespace Oqtane.Managers
var userpasskeys = await _identityUserManager.GetPasskeysAsync(identityuser);
foreach (var userpasskey in userpasskeys)
{
- passkeys.Add(new Passkey { CredentialId = userpasskey.CredentialId, Name = userpasskey.Name, UserId = userId });
+ passkeys.Add(new UserPasskey { CredentialId = userpasskey.CredentialId, Name = userpasskey.Name, UserId = userId });
}
}
}
return passkeys;
}
- public async Task AddPasskey(Passkey passkey)
- {
- var user = _users.GetUser(passkey.UserId);
- if (user != null)
- {
- var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser != null)
- {
- var attestationResult = await _identitySignInManager.PerformPasskeyAttestationAsync(passkey.CredentialJson);
- if (attestationResult.Succeeded)
- {
- var addPasskeyResult = await _identityUserManager.AddOrUpdatePasskeyAsync(identityuser, attestationResult.Passkey);
- }
- }
- }
- }
-
- public async Task UpdatePasskey(Passkey passkey)
+ public async Task UpdatePasskey(UserPasskey passkey)
{
var user = _users.GetUser(passkey.UserId);
if (user != null)
@@ -890,5 +874,37 @@ namespace Oqtane.Managers
}
}
}
+
+ public async Task> GetLogins(int userId)
+ {
+ var logins = new List();
+ var user = _users.GetUser(userId);
+ if (user != null)
+ {
+ var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null)
+ {
+ var userlogins = await _identityUserManager.GetLoginsAsync(identityuser);
+ foreach (var userlogin in userlogins)
+ {
+ logins.Add(new UserLogin { Provider = userlogin.LoginProvider, Key = userlogin.ProviderKey, Name = userlogin.ProviderDisplayName });
+ }
+ }
+ }
+ return logins;
+ }
+
+ public async Task DeleteLogin(int userId, string provider, string key)
+ {
+ var user = _users.GetUser(userId);
+ if (user != null)
+ {
+ var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null)
+ {
+ await _identityUserManager.RemoveLoginAsync(identityuser, provider, key);
+ }
+ }
+ }
}
}
diff --git a/Oqtane.Server/Pages/Passkey.cshtml b/Oqtane.Server/Pages/Passkey.cshtml
new file mode 100644
index 00000000..43bac572
--- /dev/null
+++ b/Oqtane.Server/Pages/Passkey.cshtml
@@ -0,0 +1,3 @@
+@page "/pages/passkey"
+@namespace Oqtane.Pages
+@model Oqtane.Pages.PasskeyModel
diff --git a/Oqtane.Server/Pages/Passkey.cshtml.cs b/Oqtane.Server/Pages/Passkey.cshtml.cs
new file mode 100644
index 00000000..ad6a35d3
--- /dev/null
+++ b/Oqtane.Server/Pages/Passkey.cshtml.cs
@@ -0,0 +1,144 @@
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Oqtane.Enums;
+using Oqtane.Extensions;
+using Oqtane.Infrastructure;
+using Oqtane.Managers;
+using Oqtane.Security;
+using Oqtane.Shared;
+
+namespace Oqtane.Pages
+{
+ [AllowAnonymous]
+ public class PasskeyModel : PageModel
+ {
+ private readonly UserManager _identityUserManager;
+ private readonly SignInManager _identitySignInManager;
+ private readonly IUserManager _userManager;
+ private readonly ILogManager _logger;
+
+ public PasskeyModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager, ILogManager logger)
+ {
+ _identityUserManager = identityUserManager;
+ _identitySignInManager = identitySignInManager;
+ _userManager = userManager;
+ _logger = logger;
+ }
+
+ public async Task OnPostAsync(string operation, string credential, string returnurl)
+ {
+ if (HttpContext.GetSiteSettings().GetValue("LoginOptions:Passkeys", "false") == "true")
+ {
+ IdentityUser identityuser;
+
+ switch (operation.ToLower())
+ {
+ case "create":
+ if (User.Identity.IsAuthenticated)
+ {
+ identityuser = await _identityUserManager.FindByNameAsync(User.Identity.Name);
+ if (identityuser != null)
+ {
+ var creationOptionsJson = await _identitySignInManager.MakePasskeyCreationOptionsAsync(new()
+ {
+ Id = identityuser.Id,
+ Name = identityuser.UserName,
+ DisplayName = identityuser.UserName
+ });
+ returnurl += $"?options={WebUtility.UrlEncode(creationOptionsJson)}";
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Create Attempt - User {User} Does Not Exist", User.Identity.Name);
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Create Attempt - User Not Authenticated");
+ }
+ break;
+ case "validate":
+ if (User.Identity.IsAuthenticated && !string.IsNullOrEmpty(credential))
+ {
+ identityuser = await _identityUserManager.FindByNameAsync(User.Identity.Name);
+ if (identityuser != null)
+ {
+ var attestationResult = await _identitySignInManager.PerformPasskeyAttestationAsync(credential);
+ if (attestationResult.Succeeded)
+ {
+ attestationResult.Passkey.Name = identityuser.UserName + "'s Passkey";
+ var addPasskeyResult = await _identityUserManager.AddOrUpdatePasskeyAsync(identityuser, attestationResult.Passkey);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Passkey Validation Failed For User {Username} - {Message}", User.Identity.Name, attestationResult.Failure.Message);
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Validation Attempt - User {User} Does Not Exist", User.Identity.Name);
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Validation Attempt - User Not Authenticated Or Credential Not Provided");
+ }
+ break;
+ case "request":
+ if (!User.Identity.IsAuthenticated)
+ {
+ identityuser = null;
+ var requestOptionsJson = await _identitySignInManager.MakePasskeyRequestOptionsAsync(identityuser);
+ returnurl += $"?options={WebUtility.UrlEncode(requestOptionsJson)}";
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Request Attempt - User Is Already Authenticated");
+ }
+ break;
+ case "login":
+ if (!User.Identity.IsAuthenticated && !string.IsNullOrEmpty(credential))
+ {
+ var result = await _identitySignInManager.PasskeySignInAsync(credential);
+ if (result.Succeeded)
+ {
+ var user = _userManager.GetUser(User.Identity.Name, HttpContext.GetAlias().SiteId);
+ if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered))
+ {
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "Passkey Login Successful For User {Username}", User.Identity.Name);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "Passkey Login Failed For User {Username}", User.Identity.Name);
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Passkey Login Failed - Invalid Credential");
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Login Attempt");
+ }
+ break;
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Request - Passkeys Are Not Enabled For Site");
+ }
+
+ if (!returnurl.StartsWith("/"))
+ {
+ returnurl = "/" + returnurl;
+ }
+
+ return LocalRedirect(Url.Content("~" + returnurl));
+ }
+ }
+}
diff --git a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css
index 086b246b..38c1a2c9 100644
--- a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css
+++ b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css
@@ -1,5 +1,5 @@
/* Login Module Custom Styles */
.Oqtane-Modules-Admin-Login {
- width: 200px;
+ width: 220px;
}
diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js
index fecc4c99..ab29231b 100644
--- a/Oqtane.Server/wwwroot/js/interop.js
+++ b/Oqtane.Server/wwwroot/js/interop.js
@@ -516,5 +516,17 @@ Oqtane.Interop = {
}
}
}
+ },
+ createCredential: async function (optionsResponse) {
+ const optionsJson = JSON.parse(optionsResponse);
+ const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
+ const credential = await navigator.credentials.create({ publicKey: options });
+ return JSON.stringify(credential);
+ },
+ requestCredential: async function (optionsResponse) {
+ const optionsJson = JSON.parse(optionsResponse);
+ const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
+ const credential = await navigator.credentials.get({ publicKey: options, undefined });
+ return JSON.stringify(credential);
}
};
diff --git a/Oqtane.Shared/Models/UserLogin.cs b/Oqtane.Shared/Models/UserLogin.cs
new file mode 100644
index 00000000..d22311df
--- /dev/null
+++ b/Oqtane.Shared/Models/UserLogin.cs
@@ -0,0 +1,23 @@
+namespace Oqtane.Models
+{
+ ///
+ /// Passkey properties
+ ///
+ public class UserLogin
+ {
+ ///
+ /// the login provider for this login
+ ///
+ public string Provider { get; set; }
+
+ ///
+ /// The key for this login
+ ///
+ public string Key { get; set; }
+
+ ///
+ /// The friendly name for the login provider
+ ///
+ public string Name { get; set; }
+ }
+}
diff --git a/Oqtane.Shared/Models/Passkey.cs b/Oqtane.Shared/Models/UserPasskey.cs
similarity index 96%
rename from Oqtane.Shared/Models/Passkey.cs
rename to Oqtane.Shared/Models/UserPasskey.cs
index 31671890..9ee69221 100644
--- a/Oqtane.Shared/Models/Passkey.cs
+++ b/Oqtane.Shared/Models/UserPasskey.cs
@@ -3,7 +3,7 @@ namespace Oqtane.Models
///
/// Passkey properties
///
- public class Passkey
+ public class UserPasskey
{
///
/// the credential ID for this passkey