♻️ Refactored authorize device system (wip) (skip ci)
This commit is contained in:
		@@ -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")]
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    {
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,7 @@ public class AppDatabase(
 | 
			
		||||
 | 
			
		||||
    public DbSet<AuthSession> AuthSessions { get; set; }
 | 
			
		||||
    public DbSet<AuthChallenge> AuthChallenges { get; set; }
 | 
			
		||||
    public DbSet<AuthDevice> AuthDevices { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    public DbSet<Wallet.Wallet> Wallets { get; set; }
 | 
			
		||||
    public DbSet<WalletPocket> WalletPockets { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								DysonNetwork.Pass/Auth/AuthDevice.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								DysonNetwork.Pass/Auth/AuthDevice.cs
									
									
									
									
									
										Normal 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!;
 | 
			
		||||
}
 | 
			
		||||
@@ -100,6 +100,21 @@ public class AuthService(
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
@@ -67,12 +67,13 @@ public class AuthChallenge : ModelBase
 | 
			
		||||
    [Column(TypeName = "jsonb")] public List<string> 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()
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user