✨ Add pin code
This commit is contained in:
parent
38abe16ba6
commit
c6450757be
@ -119,12 +119,15 @@ public class AccountAuthFactor : ModelBase
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public AccountAuthFactorType Type { get; set; }
|
public AccountAuthFactorType Type { get; set; }
|
||||||
[JsonIgnore] [MaxLength(8196)] public string? Secret { get; set; }
|
[JsonIgnore] [MaxLength(8196)] public string? Secret { get; set; }
|
||||||
[JsonIgnore] [Column(TypeName = "jsonb")] public Dictionary<string, object>? Config { get; set; } = new();
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[Column(TypeName = "jsonb")]
|
||||||
|
public Dictionary<string, object>? Config { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The trustworthy stands for how safe is this auth factor.
|
/// The trustworthy stands for how safe is this auth factor.
|
||||||
/// Basically, it affects how many steps it can complete in authentication.
|
/// Basically, it affects how many steps it can complete in authentication.
|
||||||
/// Besides, users may need to use some high trustworthy level auth factors when confirming some dangerous operations.
|
/// Besides, users may need to use some high-trustworthy level auth factors when confirming some dangerous operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Trustworthy { get; set; } = 1;
|
public int Trustworthy { get; set; } = 1;
|
||||||
|
|
||||||
@ -148,6 +151,7 @@ public class AccountAuthFactor : ModelBase
|
|||||||
switch (Type)
|
switch (Type)
|
||||||
{
|
{
|
||||||
case AccountAuthFactorType.Password:
|
case AccountAuthFactorType.Password:
|
||||||
|
case AccountAuthFactorType.PinCode:
|
||||||
return BCrypt.Net.BCrypt.Verify(password, Secret);
|
return BCrypt.Net.BCrypt.Verify(password, Secret);
|
||||||
case AccountAuthFactorType.TimedCode:
|
case AccountAuthFactorType.TimedCode:
|
||||||
var otp = new Totp(Base32Encoding.ToBytes(Secret));
|
var otp = new Totp(Base32Encoding.ToBytes(Secret));
|
||||||
@ -172,7 +176,8 @@ public enum AccountAuthFactorType
|
|||||||
Password,
|
Password,
|
||||||
EmailCode,
|
EmailCode,
|
||||||
InAppCode,
|
InAppCode,
|
||||||
TimedCode
|
TimedCode,
|
||||||
|
PinCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AccountConnection : ModelBase
|
public class AccountConnection : ModelBase
|
||||||
|
@ -257,6 +257,18 @@ public class AccountService(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case AccountAuthFactorType.PinCode:
|
||||||
|
if (string.IsNullOrWhiteSpace(secret)) throw new ArgumentNullException(nameof(secret));
|
||||||
|
if (!secret.All(char.IsDigit) || secret.Length != 6)
|
||||||
|
throw new ArgumentException("PIN code must be exactly 6 digits");
|
||||||
|
factor = new AccountAuthFactor
|
||||||
|
{
|
||||||
|
Type = AccountAuthFactorType.PinCode,
|
||||||
|
Trustworthy = 0, // Only for confirming, can't be used for login
|
||||||
|
Secret = secret,
|
||||||
|
EnabledAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
|
}.HashSecret();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
public class AuthService(AppDatabase db, IConfiguration config, IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor)
|
public class AuthService(
|
||||||
|
AppDatabase db,
|
||||||
|
IConfiguration config,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
ICacheService cache
|
||||||
|
)
|
||||||
{
|
{
|
||||||
private HttpContext HttpContext => httpContextAccessor.HttpContext!;
|
private HttpContext HttpContext => httpContextAccessor.HttpContext!;
|
||||||
|
|
||||||
@ -174,6 +182,69 @@ public class AuthService(AppDatabase db, IConfiguration config, IHttpClientFacto
|
|||||||
return $"{payloadBase64}.{signatureBase64}";
|
return $"{payloadBase64}.{signatureBase64}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ValidateSudoMode(Session session, string? pinCode)
|
||||||
|
{
|
||||||
|
// Check if the session is already in sudo mode (cached)
|
||||||
|
var sudoModeKey = $"accounts:{session.Id}:sudo";
|
||||||
|
var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey);
|
||||||
|
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
// Session is already in sudo mode
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has a pin code
|
||||||
|
var hasPinCode = await db.AccountAuthFactors
|
||||||
|
.Where(f => f.AccountId == session.AccountId)
|
||||||
|
.Where(f => f.EnabledAt != null)
|
||||||
|
.Where(f => f.Type == AccountAuthFactorType.PinCode)
|
||||||
|
.AnyAsync();
|
||||||
|
|
||||||
|
if (!hasPinCode)
|
||||||
|
{
|
||||||
|
// User doesn't have a pin code, no validation needed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pin code is not provided, we can't validate
|
||||||
|
if (string.IsNullOrEmpty(pinCode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate the pin code
|
||||||
|
var isValid = await ValidatePinCode(session.AccountId, pinCode);
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
{
|
||||||
|
// Set session in sudo mode for 5 minutes
|
||||||
|
await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
// No pin code enabled for this account, so validation is successful
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ValidatePinCode(Guid accountId, string pinCode)
|
||||||
|
{
|
||||||
|
var factor = await db.AccountAuthFactors
|
||||||
|
.Where(f => f.AccountId == accountId)
|
||||||
|
.Where(f => f.EnabledAt != null)
|
||||||
|
.Where(f => f.Type == AccountAuthFactorType.PinCode)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (factor is null) throw new InvalidOperationException("No pin code enabled for this account.");
|
||||||
|
|
||||||
|
return factor.VerifyPassword(pinCode);
|
||||||
|
}
|
||||||
|
|
||||||
public bool ValidateToken(string token, out Guid sessionId)
|
public bool ValidateToken(string token, out Guid sessionId)
|
||||||
{
|
{
|
||||||
sessionId = Guid.Empty;
|
sessionId = Guid.Empty;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user