New authorized device

This commit is contained in:
2025-08-14 02:10:32 +08:00
parent 4b66e97bda
commit 5f70d53c94
12 changed files with 1923 additions and 20 deletions

View File

@@ -439,7 +439,7 @@ public class AccountCurrentController(
[HttpGet("devices")]
[Authorize]
public async Task<ActionResult<List<AuthClient>>> GetDevices()
public async Task<ActionResult<List<AuthClientWithChallenge>>> GetDevices()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
@@ -450,7 +450,18 @@ public class AccountCurrentController(
.Where(device => device.AccountId == currentUser.Id)
.ToListAsync();
return Ok(devices);
var challengeDevices = devices.Select(AuthClientWithChallenge.FromClient).ToList();
var deviceIds = challengeDevices.Select(x => x.Id).ToList();
var authChallenges = await db.AuthChallenges
.Where(c => c.ClientId != null && deviceIds.Contains(c.ClientId.Value))
.GroupBy(c => c.ClientId)
.ToDictionaryAsync(c => c.Key!.Value, c => c.ToList());
foreach (var challengeDevice in challengeDevices)
if (authChallenges.TryGetValue(challengeDevice.Id, out var challenge))
challengeDevice.Challenges = challenge;
return Ok(challengeDevices);
}
[HttpGet("sessions")]
@@ -727,4 +738,4 @@ public class AccountCurrentController(
return BadRequest(ex.Message);
}
}
}
}

View File

@@ -75,6 +75,7 @@ public class DysonTokenAuthHandler(
session = await database.AuthSessions
.Where(e => e.Id == sessionId)
.Include(e => e.Challenge)
.ThenInclude(e => e.Client)
.Include(e => e.Account)
.ThenInclude(e => e.Profile)
.FirstOrDefaultAsync();

View File

@@ -23,7 +23,7 @@ public class AuthController(
public class ChallengeRequest
{
[Required] public ChallengePlatform Platform { get; set; }
[Required] public ClientPlatform Platform { get; set; }
[Required] [MaxLength(256)] public string Account { get; set; } = null!;
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
public List<string> Audiences { get; set; } = new();
@@ -57,12 +57,11 @@ public class AuthController(
.FirstOrDefaultAsync();
if (existingChallenge is not null) return existingChallenge;
var device = await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId);
var device = await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId, request.Platform);
var challenge = new AuthChallenge
{
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)),
StepTotal = await auth.DetectChallengeRisk(Request, account),
Platform = request.Platform,
Audiences = request.Audiences,
Scopes = request.Scopes,
IpAddress = ipAddress,

View File

@@ -73,7 +73,8 @@ public class AuthService(
return totalRequiredSteps;
}
public async Task<AuthSession> CreateSessionForOidcAsync(Account.Account account, Instant time, Guid? customAppId = null)
public async Task<AuthSession> CreateSessionForOidcAsync(Account.Account account, Instant time,
Guid? customAppId = null)
{
var challenge = new AuthChallenge
{
@@ -101,12 +102,17 @@ public class AuthService(
return session;
}
public async Task<AuthClient> GetOrCreateDeviceAsync(Guid accountId, string deviceId)
public async Task<AuthClient> GetOrCreateDeviceAsync(
Guid accountId,
string deviceId,
ClientPlatform platform = ClientPlatform.Unidentified
)
{
var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId);
if (device is not null) return device;
device = new AuthClient
{
Platform = platform,
DeviceId = deviceId,
AccountId = accountId
};
@@ -316,4 +322,4 @@ public class AuthService(
return Convert.FromBase64String(padded);
}
}
}

View File

@@ -30,6 +30,7 @@ public class AuthServiceGrpc(
session = await db.AuthSessions
.AsNoTracking()
.Include(e => e.Challenge)
.ThenInclude(e => e.Client)
.Include(e => e.Account)
.ThenInclude(e => e.Profile)
.FirstOrDefaultAsync(s => s.Id == sessionId);

View File

@@ -43,7 +43,7 @@ public enum ChallengeType
Oidc // Trying to connect other platforms
}
public enum ChallengePlatform
public enum ClientPlatform
{
Unidentified,
Web,
@@ -61,7 +61,6 @@ public class AuthChallenge : ModelBase
public int StepRemain { get; set; }
public int StepTotal { get; set; }
public int FailedAttempts { get; set; }
public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified;
public ChallengeType Type { get; set; } = ChallengeType.Login;
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new();
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
@@ -90,14 +89,13 @@ public class AuthChallenge : ModelBase
StepRemain = StepRemain,
StepTotal = StepTotal,
FailedAttempts = FailedAttempts,
Platform = (Shared.Proto.ChallengePlatform)Platform,
Type = (Shared.Proto.ChallengeType)Type,
BlacklistFactors = { BlacklistFactors.Select(x => x.ToString()) },
Audiences = { Audiences },
Scopes = { Scopes },
IpAddress = IpAddress,
UserAgent = UserAgent,
DeviceId = Client.DeviceId.ToString(),
DeviceId = Client!.DeviceId,
Nonce = Nonce,
AccountId = AccountId.ToString()
};
@@ -107,6 +105,7 @@ public class AuthChallenge : ModelBase
public class AuthClient : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public ClientPlatform Platform { get; set; } = ClientPlatform.Unidentified;
[MaxLength(1024)] public string DeviceName { get; set; } = string.Empty;
[MaxLength(1024)] public string? DeviceLabel { get; set; }
[MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
@@ -114,3 +113,21 @@ public class AuthClient : ModelBase
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
}
public class AuthClientWithChallenge : AuthClient
{
public List<AuthChallenge> Challenges { get; set; } = [];
public static AuthClientWithChallenge FromClient(AuthClient client)
{
return new AuthClientWithChallenge
{
Id = client.Id,
Platform = client.Platform,
DeviceName = client.DeviceName,
DeviceLabel = client.DeviceLabel,
DeviceId = client.DeviceId,
AccountId = client.AccountId,
};
}
}

View File

@@ -223,7 +223,6 @@ public abstract class OidcService(
ExpiredAt = now.Plus(Duration.FromHours(1)),
StepTotal = await auth.DetectChallengeRisk(request.Request, account),
Type = ChallengeType.Oidc,
Platform = ChallengePlatform.Unidentified,
Audiences = [ProviderName],
Scopes = ["*"],
AccountId = account.Id,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddAuthDevicePlatform : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "platform",
table: "auth_challenges");
migrationBuilder.AddColumn<int>(
name: "platform",
table: "auth_clients",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "platform",
table: "auth_clients");
migrationBuilder.AddColumn<int>(
name: "platform",
table: "auth_challenges",
type: "integer",
nullable: false,
defaultValue: 0);
}
}
}

View File

@@ -856,10 +856,6 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("character varying(1024)")
.HasColumnName("nonce");
b.Property<int>("Platform")
.HasColumnType("integer")
.HasColumnName("platform");
b.Property<List<string>>("Scopes")
.IsRequired()
.HasColumnType("jsonb")
@@ -934,6 +930,10 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("character varying(1024)")
.HasColumnName("device_name");
b.Property<int>("Platform")
.HasColumnType("integer")
.HasColumnName("platform");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");