From 76fdf14e79172289bcccebee0d19b3a5a2e9c818 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 13 Aug 2025 02:04:26 +0800 Subject: [PATCH] :recycle: Refactored authorize device system (wip) (skip ci) --- .../Account/AccountCurrentController.cs | 22 +++---------------- DysonNetwork.Pass/Account/AccountService.cs | 13 +++++++---- DysonNetwork.Pass/AppDatabase.cs | 1 + DysonNetwork.Pass/Auth/AuthController.cs | 3 ++- DysonNetwork.Pass/Auth/AuthDevice.cs | 17 ++++++++++++++ DysonNetwork.Pass/Auth/AuthService.cs | 15 +++++++++++++ DysonNetwork.Pass/Auth/OpenId/OidcService.cs | 3 ++- DysonNetwork.Pass/Auth/Session.cs | 5 +++-- 8 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 DysonNetwork.Pass/Auth/AuthDevice.cs diff --git a/DysonNetwork.Pass/Account/AccountCurrentController.cs b/DysonNetwork.Pass/Account/AccountCurrentController.cs index 4d62245..5f92ffa 100644 --- a/DysonNetwork.Pass/Account/AccountCurrentController.cs +++ b/DysonNetwork.Pass/Account/AccountCurrentController.cs @@ -455,27 +455,11 @@ public class AccountCurrentController( Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); - // Group sessions by the related DeviceId, then create an AuthorizedDevice for each group. - var deviceGroups = await db.AuthSessions - .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() - }) + var devices = await db.AuthDevices + .Where(device => device.AccountId == currentUser.Id) .ToListAsync(); - deviceGroups = deviceGroups - .OrderByDescending(s => s.Sessions.First().LastGrantedAt) - .ToList(); - return Ok(deviceGroups); + return Ok(devices); } [HttpGet("sessions")] diff --git a/DysonNetwork.Pass/Account/AccountService.cs b/DysonNetwork.Pass/Account/AccountService.cs index 1c2da5b..d8a8831 100644 --- a/DysonNetwork.Pass/Account/AccountService.cs +++ b/DysonNetwork.Pass/Account/AccountService.cs @@ -456,6 +456,11 @@ public class AccountService( ); } + public async Task IsDeviceActive(Guid id) + { + return await db.AuthChallenges.AnyAsync(d => d.DeviceId == id); + } + public async Task UpdateSessionLabel(Account account, Guid sessionId, string label) { var session = await db.AuthSessions @@ -483,6 +488,7 @@ public class AccountService( { var session = await db.AuthSessions .Include(s => s.Challenge) + .ThenInclude(s => s.Device) .Where(s => s.Id == sessionId && s.AccountId == account.Id) .FirstOrDefaultAsync(); 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) .ToListAsync(); - if (session.Challenge.DeviceId is not null) + if (!await IsDeviceActive(session.Challenge.DeviceId)) await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest() - { - DeviceId = session.Challenge.DeviceId - }); + { DeviceId = session.Challenge.Device.DeviceId } + ); // The current session should be included in the sessions' list await db.AuthSessions diff --git a/DysonNetwork.Pass/AppDatabase.cs b/DysonNetwork.Pass/AppDatabase.cs index 99c3d8c..04967ea 100644 --- a/DysonNetwork.Pass/AppDatabase.cs +++ b/DysonNetwork.Pass/AppDatabase.cs @@ -37,6 +37,7 @@ public class AppDatabase( public DbSet AuthSessions { get; set; } public DbSet AuthChallenges { get; set; } + public DbSet AuthDevices { get; set; } public DbSet Wallets { get; set; } public DbSet WalletPockets { get; set; } diff --git a/DysonNetwork.Pass/Auth/AuthController.cs b/DysonNetwork.Pass/Auth/AuthController.cs index 1181c74..1be7502 100644 --- a/DysonNetwork.Pass/Auth/AuthController.cs +++ b/DysonNetwork.Pass/Auth/AuthController.cs @@ -57,6 +57,7 @@ public class AuthController( .FirstOrDefaultAsync(); if (existingChallenge is not null) return existingChallenge; + var device = await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId); var challenge = new AuthChallenge { ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)), @@ -67,7 +68,7 @@ public class AuthController( IpAddress = ipAddress, UserAgent = userAgent, Location = geo.GetPointFromIp(ipAddress), - DeviceId = request.DeviceId, + DeviceId = device.Id, AccountId = account.Id }.Normalize(); diff --git a/DysonNetwork.Pass/Auth/AuthDevice.cs b/DysonNetwork.Pass/Auth/AuthDevice.cs new file mode 100644 index 0000000..1c14a67 --- /dev/null +++ b/DysonNetwork.Pass/Auth/AuthDevice.cs @@ -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!; +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Auth/AuthService.cs b/DysonNetwork.Pass/Auth/AuthService.cs index cb35d35..6d64aba 100644 --- a/DysonNetwork.Pass/Auth/AuthService.cs +++ b/DysonNetwork.Pass/Auth/AuthService.cs @@ -100,6 +100,21 @@ public class AuthService( return session; } + + public async Task 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 ValidateCaptcha(string token) { diff --git a/DysonNetwork.Pass/Auth/OpenId/OidcService.cs b/DysonNetwork.Pass/Auth/OpenId/OidcService.cs index d79ad29..17900f5 100644 --- a/DysonNetwork.Pass/Auth/OpenId/OidcService.cs +++ b/DysonNetwork.Pass/Auth/OpenId/OidcService.cs @@ -217,6 +217,7 @@ public abstract class OidcService( // Create a challenge that's already completed var now = SystemClock.Instance.GetCurrentInstant(); + var device = await auth.GetOrCreateDeviceAsync(account.Id, deviceId); var challenge = new AuthChallenge { ExpiredAt = now.Plus(Duration.FromHours(1)), @@ -226,7 +227,7 @@ public abstract class OidcService( Audiences = [ProviderName], Scopes = ["*"], AccountId = account.Id, - DeviceId = deviceId, + DeviceId = device.Id, IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null, UserAgent = request.Request.Headers.UserAgent, }; diff --git a/DysonNetwork.Pass/Auth/Session.cs b/DysonNetwork.Pass/Auth/Session.cs index 35e69f4..ee6e04b 100644 --- a/DysonNetwork.Pass/Auth/Session.cs +++ b/DysonNetwork.Pass/Auth/Session.cs @@ -67,12 +67,13 @@ public class AuthChallenge : ModelBase [Column(TypeName = "jsonb")] public List Scopes { get; set; } = new(); [MaxLength(128)] public string? IpAddress { get; set; } [MaxLength(512)] public string? UserAgent { get; set; } - [MaxLength(256)] public string? DeviceId { get; set; } [MaxLength(1024)] public string? Nonce { get; set; } public Point? Location { get; set; } public Guid AccountId { get; set; } [JsonIgnore] public Account.Account Account { get; set; } = null!; + public Guid DeviceId { get; set; } + public AuthDevice Device { get; set; } = null!; public AuthChallenge Normalize() { @@ -94,7 +95,7 @@ public class AuthChallenge : ModelBase Scopes = { Scopes }, IpAddress = IpAddress, UserAgent = UserAgent, - DeviceId = DeviceId, + DeviceId = DeviceId.ToString(), Nonce = Nonce, AccountId = AccountId.ToString() };