add passkey and login management to User Management

This commit is contained in:
sbwalker
2025-10-30 11:08:56 -04:00
parent adfd870319
commit ab4bc7e678
7 changed files with 280 additions and 115 deletions

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@ -240,8 +241,8 @@ namespace Oqtane.Controllers
}
}
// POST api/<controller>/login
[HttpPost("login")]
// POST api/<controller>/signin
[HttpPost("signin")]
public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent)
{
if (ModelState.IsValid)
@ -330,22 +331,6 @@ namespace Oqtane.Controllers
return user;
}
// POST api/<controller>/link
[HttpPost("link")]
public async Task<User> Link([FromBody] User user, string token, string type, string key, string name)
{
if (ModelState.IsValid)
{
user = await _userManager.LinkExternalAccount(user, token, type, key, name);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} And Token {Token}", user.Username, token);
user = null;
}
return user;
}
// GET api/<controller>/validate/x
[HttpGet("validateuser")]
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
@ -466,12 +451,21 @@ namespace Oqtane.Controllers
}
}
// GET: api/<controller>/passkey
// GET: api/<controller>/passkey?id=x
[HttpGet("passkey")]
[Authorize]
public async Task<IEnumerable<UserPasskey>> GetPasskeys()
public async Task<IEnumerable<UserPasskey>> GetPasskeys(int id)
{
return await _userManager.GetPasskeys(_userPermissions.GetUser(User).UserId, _tenantManager.GetAlias().SiteId);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
return await _userManager.GetPasskeys(id, _tenantManager.GetAlias().SiteId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Get Attempt {UserId} {SiteId}", id, _tenantManager.GetAlias().SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// PUT api/<controller>/passkey
@ -481,10 +475,17 @@ 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);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == passkey.UserId)
{
// passkey name is prefixed with SiteId for multi-tenancy
passkey.Name = $"{_tenantManager.GetAlias().SiteId}:" + passkey.Name;
await _userManager.UpdatePasskey(passkey);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Put Attempt {PassKey}", passkey);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
else
{
@ -493,28 +494,70 @@ namespace Oqtane.Controllers
}
}
// DELETE api/<controller>/passkey?id=x
// DELETE api/<controller>/passkey?id=x&credential=y
[HttpDelete("passkey")]
[Authorize]
public async Task DeletePasskey(string id)
public async Task DeletePasskey(int id, string credential)
{
await _userManager.DeletePasskey(_userPermissions.GetUser(User).UserId, Base64Url.DecodeFromChars(id));
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
await _userManager.DeletePasskey(id, Base64Url.DecodeFromChars(credential));
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Delete Attempt {UserId} {Credential}", id, credential);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// GET: api/<controller>/login
// GET: api/<controller>/login?id=x
[HttpGet("login")]
[Authorize]
public async Task<IEnumerable<UserLogin>> GetLogins()
public async Task<IEnumerable<UserLogin>> GetLogins(int id)
{
return await _userManager.GetLogins(_userPermissions.GetUser(User).UserId, _tenantManager.GetAlias().SiteId);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
return await _userManager.GetLogins(id, _tenantManager.GetAlias().SiteId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Login Get Attempt {UserId} {SiteId}", id, _tenantManager.GetAlias().SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// DELETE api/<controller>/login?provider=x&key=y
// PUT api/<controller>/login
[HttpPost("login")]
public async Task<User> AddLogin([FromBody] User user, string token, string type, string key, string name)
{
if (ModelState.IsValid)
{
user = await _userManager.AddLogin(user, token, type, key, name);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Login Post Attempt {Username} {Token}", user.Username, token);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
user = null;
}
return user;
}
// DELETE api/<controller>/login?id=x&provider=y&key=z
[HttpDelete("login")]
[Authorize]
public async Task DeleteLogin(string provider, string key)
public async Task DeleteLogin(int id, string provider, string key)
{
await _userManager.DeleteLogin(_userPermissions.GetUser(User).UserId, provider, key);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
await _userManager.DeleteLogin(id, provider, key);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Login Delete Attempt {UserId} {Provider} {Key}", id, provider, key);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@ -33,7 +33,6 @@ namespace Oqtane.Managers
Task ForgotPassword(User user);
Task<User> ResetPassword(User user, string token);
User VerifyTwoFactor(User user, string token);
Task<User> LinkExternalAccount(User user, string token, string type, string key, string name);
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);
@ -41,6 +40,7 @@ namespace Oqtane.Managers
Task UpdatePasskey(UserPasskey passkey);
Task DeletePasskey(int userId, byte[] credentialId);
Task<List<UserLogin>> GetLogins(int userId, int siteId);
Task<User> AddLogin(User user, string token, string type, string key, string name);
Task DeleteLogin(int userId, string provider, string key);
}
@ -588,29 +588,6 @@ namespace Oqtane.Managers
}
return user;
}
public async Task<User> LinkExternalAccount(User user, string token, string type, string key, string name)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
return user;
}
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
{
var validateResult = new UserValidateResult { Succeeded = true };
@ -902,6 +879,29 @@ namespace Oqtane.Managers
return logins;
}
public async Task<User> AddLogin(User user, string token, string type, string key, string name)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
return user;
}
public async Task DeleteLogin(int userId, string provider, string key)
{
var user = _users.GetUser(userId);