✨ New authorized device
This commit is contained in:
@@ -439,7 +439,7 @@ public class AccountCurrentController(
|
|||||||
|
|
||||||
[HttpGet("devices")]
|
[HttpGet("devices")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<AuthClient>>> GetDevices()
|
public async Task<ActionResult<List<AuthClientWithChallenge>>> GetDevices()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||||
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
|
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
|
||||||
@@ -450,7 +450,18 @@ public class AccountCurrentController(
|
|||||||
.Where(device => device.AccountId == currentUser.Id)
|
.Where(device => device.AccountId == currentUser.Id)
|
||||||
.ToListAsync();
|
.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")]
|
[HttpGet("sessions")]
|
||||||
@@ -727,4 +738,4 @@ public class AccountCurrentController(
|
|||||||
return BadRequest(ex.Message);
|
return BadRequest(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -75,6 +75,7 @@ public class DysonTokenAuthHandler(
|
|||||||
session = await database.AuthSessions
|
session = await database.AuthSessions
|
||||||
.Where(e => e.Id == sessionId)
|
.Where(e => e.Id == sessionId)
|
||||||
.Include(e => e.Challenge)
|
.Include(e => e.Challenge)
|
||||||
|
.ThenInclude(e => e.Client)
|
||||||
.Include(e => e.Account)
|
.Include(e => e.Account)
|
||||||
.ThenInclude(e => e.Profile)
|
.ThenInclude(e => e.Profile)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
@@ -23,7 +23,7 @@ public class AuthController(
|
|||||||
|
|
||||||
public class ChallengeRequest
|
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(256)] public string Account { get; set; } = null!;
|
||||||
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
|
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
|
||||||
public List<string> Audiences { get; set; } = new();
|
public List<string> Audiences { get; set; } = new();
|
||||||
@@ -57,12 +57,11 @@ 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 device = await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId, request.Platform);
|
||||||
var challenge = new AuthChallenge
|
var challenge = new AuthChallenge
|
||||||
{
|
{
|
||||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)),
|
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)),
|
||||||
StepTotal = await auth.DetectChallengeRisk(Request, account),
|
StepTotal = await auth.DetectChallengeRisk(Request, account),
|
||||||
Platform = request.Platform,
|
|
||||||
Audiences = request.Audiences,
|
Audiences = request.Audiences,
|
||||||
Scopes = request.Scopes,
|
Scopes = request.Scopes,
|
||||||
IpAddress = ipAddress,
|
IpAddress = ipAddress,
|
||||||
|
@@ -73,7 +73,8 @@ public class AuthService(
|
|||||||
return totalRequiredSteps;
|
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
|
var challenge = new AuthChallenge
|
||||||
{
|
{
|
||||||
@@ -101,12 +102,17 @@ public class AuthService(
|
|||||||
return session;
|
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);
|
var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId);
|
||||||
if (device is not null) return device;
|
if (device is not null) return device;
|
||||||
device = new AuthClient
|
device = new AuthClient
|
||||||
{
|
{
|
||||||
|
Platform = platform,
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
AccountId = accountId
|
AccountId = accountId
|
||||||
};
|
};
|
||||||
@@ -316,4 +322,4 @@ public class AuthService(
|
|||||||
|
|
||||||
return Convert.FromBase64String(padded);
|
return Convert.FromBase64String(padded);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -30,6 +30,7 @@ public class AuthServiceGrpc(
|
|||||||
session = await db.AuthSessions
|
session = await db.AuthSessions
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(e => e.Challenge)
|
.Include(e => e.Challenge)
|
||||||
|
.ThenInclude(e => e.Client)
|
||||||
.Include(e => e.Account)
|
.Include(e => e.Account)
|
||||||
.ThenInclude(e => e.Profile)
|
.ThenInclude(e => e.Profile)
|
||||||
.FirstOrDefaultAsync(s => s.Id == sessionId);
|
.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||||
|
@@ -43,7 +43,7 @@ public enum ChallengeType
|
|||||||
Oidc // Trying to connect other platforms
|
Oidc // Trying to connect other platforms
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChallengePlatform
|
public enum ClientPlatform
|
||||||
{
|
{
|
||||||
Unidentified,
|
Unidentified,
|
||||||
Web,
|
Web,
|
||||||
@@ -61,7 +61,6 @@ public class AuthChallenge : ModelBase
|
|||||||
public int StepRemain { get; set; }
|
public int StepRemain { get; set; }
|
||||||
public int StepTotal { get; set; }
|
public int StepTotal { get; set; }
|
||||||
public int FailedAttempts { get; set; }
|
public int FailedAttempts { get; set; }
|
||||||
public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified;
|
|
||||||
public ChallengeType Type { get; set; } = ChallengeType.Login;
|
public ChallengeType Type { get; set; } = ChallengeType.Login;
|
||||||
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new();
|
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new();
|
||||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
|
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
|
||||||
@@ -90,14 +89,13 @@ public class AuthChallenge : ModelBase
|
|||||||
StepRemain = StepRemain,
|
StepRemain = StepRemain,
|
||||||
StepTotal = StepTotal,
|
StepTotal = StepTotal,
|
||||||
FailedAttempts = FailedAttempts,
|
FailedAttempts = FailedAttempts,
|
||||||
Platform = (Shared.Proto.ChallengePlatform)Platform,
|
|
||||||
Type = (Shared.Proto.ChallengeType)Type,
|
Type = (Shared.Proto.ChallengeType)Type,
|
||||||
BlacklistFactors = { BlacklistFactors.Select(x => x.ToString()) },
|
BlacklistFactors = { BlacklistFactors.Select(x => x.ToString()) },
|
||||||
Audiences = { Audiences },
|
Audiences = { Audiences },
|
||||||
Scopes = { Scopes },
|
Scopes = { Scopes },
|
||||||
IpAddress = IpAddress,
|
IpAddress = IpAddress,
|
||||||
UserAgent = UserAgent,
|
UserAgent = UserAgent,
|
||||||
DeviceId = Client.DeviceId.ToString(),
|
DeviceId = Client!.DeviceId,
|
||||||
Nonce = Nonce,
|
Nonce = Nonce,
|
||||||
AccountId = AccountId.ToString()
|
AccountId = AccountId.ToString()
|
||||||
};
|
};
|
||||||
@@ -107,6 +105,7 @@ public class AuthChallenge : ModelBase
|
|||||||
public class AuthClient : ModelBase
|
public class AuthClient : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
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 DeviceName { get; set; } = string.Empty;
|
||||||
[MaxLength(1024)] public string? DeviceLabel { get; set; }
|
[MaxLength(1024)] public string? DeviceLabel { get; set; }
|
||||||
[MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
|
[MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
|
||||||
@@ -114,3 +113,21 @@ public class AuthClient : ModelBase
|
|||||||
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 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -223,7 +223,6 @@ public abstract class OidcService(
|
|||||||
ExpiredAt = now.Plus(Duration.FromHours(1)),
|
ExpiredAt = now.Plus(Duration.FromHours(1)),
|
||||||
StepTotal = await auth.DetectChallengeRisk(request.Request, account),
|
StepTotal = await auth.DetectChallengeRisk(request.Request, account),
|
||||||
Type = ChallengeType.Oidc,
|
Type = ChallengeType.Oidc,
|
||||||
Platform = ChallengePlatform.Unidentified,
|
|
||||||
Audiences = [ProviderName],
|
Audiences = [ProviderName],
|
||||||
Scopes = ["*"],
|
Scopes = ["*"],
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
|
1830
DysonNetwork.Pass/Migrations/20250813121421_AddAuthDevicePlatform.Designer.cs
generated
Normal file
1830
DysonNetwork.Pass/Migrations/20250813121421_AddAuthDevicePlatform.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -856,10 +856,6 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("character varying(1024)")
|
.HasColumnType("character varying(1024)")
|
||||||
.HasColumnName("nonce");
|
.HasColumnName("nonce");
|
||||||
|
|
||||||
b.Property<int>("Platform")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasColumnName("platform");
|
|
||||||
|
|
||||||
b.Property<List<string>>("Scopes")
|
b.Property<List<string>>("Scopes")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("jsonb")
|
.HasColumnType("jsonb")
|
||||||
@@ -934,6 +930,10 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("character varying(1024)")
|
.HasColumnType("character varying(1024)")
|
||||||
.HasColumnName("device_name");
|
.HasColumnName("device_name");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("platform");
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
|
@@ -81,7 +81,7 @@ public class NotificationController(
|
|||||||
|
|
||||||
var result =
|
var result =
|
||||||
await nty.SubscribeDevice(
|
await nty.SubscribeDevice(
|
||||||
currentSession.Challenge.DeviceId!,
|
currentSession.Challenge.DeviceId,
|
||||||
request.DeviceToken,
|
request.DeviceToken,
|
||||||
request.Provider,
|
request.Provider,
|
||||||
currentUser
|
currentUser
|
||||||
|
@@ -30,7 +30,6 @@ message AuthChallenge {
|
|||||||
int32 step_remain = 3;
|
int32 step_remain = 3;
|
||||||
int32 step_total = 4;
|
int32 step_total = 4;
|
||||||
int32 failed_attempts = 5;
|
int32 failed_attempts = 5;
|
||||||
ChallengePlatform platform = 6;
|
|
||||||
ChallengeType type = 7;
|
ChallengeType type = 7;
|
||||||
repeated string blacklist_factors = 8;
|
repeated string blacklist_factors = 8;
|
||||||
repeated string audiences = 9;
|
repeated string audiences = 9;
|
||||||
|
Reference in New Issue
Block a user