♻️ Refactored authorize device system (wip) (skip ci)

This commit is contained in:
2025-08-13 02:04:26 +08:00
parent 96cceafe77
commit 76fdf14e79
8 changed files with 52 additions and 27 deletions

View File

@@ -455,27 +455,11 @@ public class AccountCurrentController(
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
// Group sessions by the related DeviceId, then create an AuthorizedDevice for each group. var devices = await db.AuthDevices
var deviceGroups = await db.AuthSessions .Where(device => device.AccountId == currentUser.Id)
.Where(s => s.Account.Id == currentUser.Id)
.Include(s => s.Challenge)
.GroupBy(s => s.Challenge.DeviceId!)
.Select(g => new AuthorizedDevice
{
DeviceId = g.Key!,
UserAgent = g.First(x => x.Challenge.UserAgent != null).Challenge.UserAgent!,
Platform = g.First().Challenge.Platform!,
Label = g.Where(x => !string.IsNullOrWhiteSpace(x.Label)).Select(x => x.Label).FirstOrDefault(),
Sessions = g
.OrderByDescending(x => x.LastGrantedAt)
.ToList()
})
.ToListAsync(); .ToListAsync();
deviceGroups = deviceGroups
.OrderByDescending(s => s.Sessions.First().LastGrantedAt)
.ToList();
return Ok(deviceGroups); return Ok(devices);
} }
[HttpGet("sessions")] [HttpGet("sessions")]

View File

@@ -456,6 +456,11 @@ public class AccountService(
); );
} }
public async Task<bool> IsDeviceActive(Guid id)
{
return await db.AuthChallenges.AnyAsync(d => d.DeviceId == id);
}
public async Task<AuthSession> UpdateSessionLabel(Account account, Guid sessionId, string label) public async Task<AuthSession> UpdateSessionLabel(Account account, Guid sessionId, string label)
{ {
var session = await db.AuthSessions var session = await db.AuthSessions
@@ -483,6 +488,7 @@ public class AccountService(
{ {
var session = await db.AuthSessions var session = await db.AuthSessions
.Include(s => s.Challenge) .Include(s => s.Challenge)
.ThenInclude(s => s.Device)
.Where(s => s.Id == sessionId && s.AccountId == account.Id) .Where(s => s.Id == sessionId && s.AccountId == account.Id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (session is null) throw new InvalidOperationException("Session was not found."); if (session is null) throw new InvalidOperationException("Session was not found.");
@@ -492,11 +498,10 @@ public class AccountService(
.Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId) .Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId)
.ToListAsync(); .ToListAsync();
if (session.Challenge.DeviceId is not null) if (!await IsDeviceActive(session.Challenge.DeviceId))
await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest() await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
{ { DeviceId = session.Challenge.Device.DeviceId }
DeviceId = session.Challenge.DeviceId );
});
// The current session should be included in the sessions' list // The current session should be included in the sessions' list
await db.AuthSessions await db.AuthSessions

View File

@@ -37,6 +37,7 @@ public class AppDatabase(
public DbSet<AuthSession> AuthSessions { get; set; } public DbSet<AuthSession> AuthSessions { get; set; }
public DbSet<AuthChallenge> AuthChallenges { get; set; } public DbSet<AuthChallenge> AuthChallenges { get; set; }
public DbSet<AuthDevice> AuthDevices { get; set; }
public DbSet<Wallet.Wallet> Wallets { get; set; } public DbSet<Wallet.Wallet> Wallets { get; set; }
public DbSet<WalletPocket> WalletPockets { get; set; } public DbSet<WalletPocket> WalletPockets { get; set; }

View File

@@ -57,6 +57,7 @@ public class AuthController(
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (existingChallenge is not null) return existingChallenge; if (existingChallenge is not null) return existingChallenge;
var device = await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId);
var challenge = new AuthChallenge var challenge = new AuthChallenge
{ {
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)), ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)),
@@ -67,7 +68,7 @@ public class AuthController(
IpAddress = ipAddress, IpAddress = ipAddress,
UserAgent = userAgent, UserAgent = userAgent,
Location = geo.GetPointFromIp(ipAddress), Location = geo.GetPointFromIp(ipAddress),
DeviceId = request.DeviceId, DeviceId = device.Id,
AccountId = account.Id AccountId = account.Id
}.Normalize(); }.Normalize();

View File

@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Pass.Auth;
[Index(nameof(DeviceId), IsUnique = true)]
public class AuthDevice : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string DeviceName { get; set; } = string.Empty;
[MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
}

View File

@@ -101,6 +101,21 @@ public class AuthService(
return session; return session;
} }
public async Task<AuthDevice> GetOrCreateDeviceAsync(Guid accountId, string deviceId)
{
var device = await db.AuthDevices.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId);
if (device is not null) return device;
device = new AuthDevice
{
DeviceId = deviceId,
AccountId = accountId
};
db.AuthDevices.Add(device);
await db.SaveChangesAsync();
return device;
}
public async Task<bool> ValidateCaptcha(string token) public async Task<bool> ValidateCaptcha(string token)
{ {
if (string.IsNullOrWhiteSpace(token)) return false; if (string.IsNullOrWhiteSpace(token)) return false;

View File

@@ -217,6 +217,7 @@ public abstract class OidcService(
// Create a challenge that's already completed // Create a challenge that's already completed
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var device = await auth.GetOrCreateDeviceAsync(account.Id, deviceId);
var challenge = new AuthChallenge var challenge = new AuthChallenge
{ {
ExpiredAt = now.Plus(Duration.FromHours(1)), ExpiredAt = now.Plus(Duration.FromHours(1)),
@@ -226,7 +227,7 @@ public abstract class OidcService(
Audiences = [ProviderName], Audiences = [ProviderName],
Scopes = ["*"], Scopes = ["*"],
AccountId = account.Id, AccountId = account.Id,
DeviceId = deviceId, DeviceId = device.Id,
IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null, IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null,
UserAgent = request.Request.Headers.UserAgent, UserAgent = request.Request.Headers.UserAgent,
}; };

View File

@@ -67,12 +67,13 @@ public class AuthChallenge : ModelBase
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new(); [Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
[MaxLength(128)] public string? IpAddress { get; set; } [MaxLength(128)] public string? IpAddress { get; set; }
[MaxLength(512)] public string? UserAgent { get; set; } [MaxLength(512)] public string? UserAgent { get; set; }
[MaxLength(256)] public string? DeviceId { get; set; }
[MaxLength(1024)] public string? Nonce { get; set; } [MaxLength(1024)] public string? Nonce { get; set; }
public Point? Location { get; set; } public Point? Location { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!; [JsonIgnore] public Account.Account Account { get; set; } = null!;
public Guid DeviceId { get; set; }
public AuthDevice Device { get; set; } = null!;
public AuthChallenge Normalize() public AuthChallenge Normalize()
{ {
@@ -94,7 +95,7 @@ public class AuthChallenge : ModelBase
Scopes = { Scopes }, Scopes = { Scopes },
IpAddress = IpAddress, IpAddress = IpAddress,
UserAgent = UserAgent, UserAgent = UserAgent,
DeviceId = DeviceId, DeviceId = DeviceId.ToString(),
Nonce = Nonce, Nonce = Nonce,
AccountId = AccountId.ToString() AccountId = AccountId.ToString()
}; };