♻️ Refactored auth service for better security

This commit is contained in:
2025-11-29 18:00:23 +08:00
parent 78f3873a0c
commit 00b3087d6a
13 changed files with 3121 additions and 101 deletions

View File

@@ -571,14 +571,14 @@ public class AccountCurrentController(
.ToListAsync();
var challengeDevices = devices.Select(SnAuthClientWithChallenge.FromClient).ToList();
var deviceIds = challengeDevices.Select(x => x.Id).ToList();
var deviceIds = challengeDevices.Select(x => x.DeviceId).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());
.Where(c => deviceIds.Contains(c.DeviceId))
.GroupBy(c => c.DeviceId)
.ToDictionaryAsync(c => c.Key, c => c.ToList());
foreach (var challengeDevice in challengeDevices)
if (authChallenges.TryGetValue(challengeDevice.Id, out var challenge))
if (authChallenges.TryGetValue(challengeDevice.DeviceId, out var challenge))
challengeDevice.Challenges = challenge;
return Ok(challengeDevices);
@@ -688,7 +688,7 @@ public class AccountCurrentController(
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser ||
HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized();
var device = await db.AuthClients.FirstOrDefaultAsync(d => d.Id == currentSession.Challenge.ClientId);
var device = await db.AuthClients.FirstOrDefaultAsync(d => d.Id == currentSession.ClientId);
if (device is null) return NotFound();
try

View File

@@ -508,9 +508,7 @@ public class AccountService(
private async Task<bool> IsDeviceActive(Guid id)
{
return await db.AuthSessions
.Include(s => s.Challenge)
.AnyAsync(s => s.Challenge.ClientId == id);
return await db.AuthSessions.AnyAsync(s => s.ClientId == id);
}
public async Task<SnAuthClient> UpdateDeviceName(SnAccount account, string deviceId, string label)
@@ -529,8 +527,7 @@ public class AccountService(
public async Task DeleteSession(SnAccount account, Guid sessionId)
{
var session = await db.AuthSessions
.Include(s => s.Challenge)
.ThenInclude(s => s.Client)
.Include(s => s.Client)
.Where(s => s.Id == sessionId && s.AccountId == account.Id)
.FirstOrDefaultAsync();
if (session is null) throw new InvalidOperationException("Session was not found.");
@@ -539,11 +536,11 @@ public class AccountService(
db.AuthSessions.Remove(session);
await db.SaveChangesAsync();
if (session.Challenge.ClientId.HasValue)
if (session.ClientId.HasValue)
{
if (!await IsDeviceActive(session.Challenge.ClientId.Value))
if (!await IsDeviceActive(session.ClientId.Value))
await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
{ DeviceId = session.Challenge.Client!.DeviceId }
{ DeviceId = session.Client!.DeviceId }
);
}
@@ -564,15 +561,13 @@ public class AccountService(
);
var sessions = await db.AuthSessions
.Include(s => s.Challenge)
.Where(s => s.Challenge.ClientId == device.Id && s.AccountId == account.Id)
.Where(s => s.ClientId == device.Id && s.AccountId == account.Id)
.ToListAsync();
// The current session should be included in the sessions' list
var now = SystemClock.Instance.GetCurrentInstant();
await db.AuthSessions
.Include(s => s.Challenge)
.Where(s => s.Challenge.ClientId == device.Id)
.Where(s => s.ClientId == device.Id)
.ExecuteUpdateAsync(p => p.SetProperty(s => s.DeletedAt, s => now));
db.AuthClients.Remove(device);

View File

@@ -30,7 +30,7 @@ public class AuthController(
public class ChallengeRequest
{
[Required] public ClientPlatform Platform { get; set; }
[Required] public Shared.Models.ClientPlatform Platform { get; set; }
[Required] [MaxLength(256)] public string Account { get; set; } = null!;
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
[MaxLength(1024)] public string? DeviceName { get; set; }
@@ -61,9 +61,6 @@ public class AuthController(
request.DeviceName ??= userAgent;
var device =
await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId, request.DeviceName, request.Platform);
// Trying to pick up challenges from the same IP address and user agent
var existingChallenge = await db.AuthChallenges
.Where(e => e.AccountId == account.Id)
@@ -72,7 +69,7 @@ public class AuthController(
.Where(e => e.StepRemain > 0)
.Where(e => e.ExpiredAt != null && now < e.ExpiredAt)
.Where(e => e.Type == Shared.Models.ChallengeType.Login)
.Where(e => e.ClientId == device.Id)
.Where(e => e.DeviceId == request.DeviceId)
.FirstOrDefaultAsync();
if (existingChallenge is not null)
{
@@ -90,7 +87,9 @@ public class AuthController(
IpAddress = ipAddress,
UserAgent = userAgent,
Location = geo.GetPointFromIp(ipAddress),
ClientId = device.Id,
DeviceId = request.DeviceId,
DeviceName = request.DeviceName,
Platform = request.Platform,
AccountId = account.Id
}.Normalize();
@@ -176,7 +175,6 @@ public class AuthController(
{
var challenge = await db.AuthChallenges
.Include(e => e.Account)
.Include(authChallenge => authChallenge.Client)
.FirstOrDefaultAsync(e => e.Id == id);
if (challenge is null) return NotFound("Auth challenge was not found.");
@@ -246,7 +244,7 @@ public class AuthController(
{
Topic = "auth.login",
Title = localizer["NewLoginTitle"],
Body = localizer["NewLoginBody", challenge.Client?.DeviceName ?? "unknown",
Body = localizer["NewLoginBody", challenge.DeviceName ?? "unknown",
challenge.IpAddress ?? "unknown"],
IsSavable = true
},

View File

@@ -157,7 +157,7 @@ public class AuthService(
// 8) Device Trust Assessment
var trustedDeviceIds = recentSessions
.Where(s => s.CreatedAt > now.Minus(Duration.FromDays(30))) // Trust devices from last 30 days
.Select(s => s.Challenge?.ClientId)
.Select(s => s.ClientId)
.Where(id => id.HasValue)
.Distinct()
.ToList();
@@ -182,7 +182,7 @@ public class AuthService(
}
public async Task<SnAuthSession> CreateSessionForOidcAsync(SnAccount account, Instant time,
Guid? customAppId = null)
Guid? customAppId = null, SnAuthSession? parentSession = null)
{
var challenge = new SnAuthChallenge
{
@@ -191,7 +191,10 @@ public class AuthService(
UserAgent = HttpContext.Request.Headers.UserAgent,
StepRemain = 1,
StepTotal = 1,
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc,
DeviceId = Guid.NewGuid().ToString(),
DeviceName = "OIDC/OAuth",
Platform = ClientPlatform.Web,
};
var session = new SnAuthSession
@@ -200,7 +203,8 @@ public class AuthService(
CreatedAt = time,
LastGrantedAt = time,
Challenge = challenge,
AppId = customAppId
AppId = customAppId,
ParentSessionId = parentSession?.Id
};
db.AuthChallenges.Add(challenge);
@@ -288,35 +292,75 @@ public class AuthService(
/// <summary>
/// Immediately revoke a session by setting expiry to now and clearing from cache
/// This provides immediate invalidation of tokens and sessions
/// This provides immediate invalidation of tokens and sessions, including all child sessions recursively.
/// </summary>
/// <param name="sessionId">Session ID to revoke</param>
/// <returns>True if session was found and revoked, false otherwise</returns>
public async Task<bool> RevokeSessionAsync(Guid sessionId)
{
var session = await db.AuthSessions.FirstOrDefaultAsync(s => s.Id == sessionId);
if (session == null)
var sessionsToRevokeIds = new HashSet<Guid>();
await CollectSessionsToRevoke(sessionId, sessionsToRevokeIds);
if (sessionsToRevokeIds.Count == 0)
{
return false;
}
// Set expiry to now (immediate invalidation)
var now = SystemClock.Instance.GetCurrentInstant();
session.ExpiredAt = now;
db.AuthSessions.Update(session);
var accountIdsToClearCache = new HashSet<Guid>();
// Clear from cache immediately
var cacheKey = $"{AuthCachePrefix}{session.Id}";
await cache.RemoveAsync(cacheKey);
// Fetch all sessions to be revoked in one go
var sessions = await db.AuthSessions
.Where(s => sessionsToRevokeIds.Contains(s.Id))
.ToListAsync();
// Clear account-level cache groups that include this session
await cache.RemoveAsync($"{AuthCachePrefix}{session.AccountId}");
foreach (var session in sessions)
{
session.ExpiredAt = now;
accountIdsToClearCache.Add(session.AccountId);
// Clear from cache immediately for each session
await cache.RemoveAsync($"{AuthCachePrefix}{session.Id}");
}
db.AuthSessions.UpdateRange(sessions);
await db.SaveChangesAsync();
// Clear account-level cache groups
foreach (var accountId in accountIdsToClearCache)
{
await cache.RemoveAsync($"{AuthCachePrefix}{accountId}");
}
return true;
}
/// <summary>
/// Recursively collects all session IDs that need to be revoked, starting from a given session.
/// </summary>
/// <param name="currentSessionId">The session ID to start collecting from.</param>
/// <param name="sessionsToRevoke">A HashSet to store the IDs of all sessions to be revoked.</param>
private async Task CollectSessionsToRevoke(Guid currentSessionId, HashSet<Guid> sessionsToRevoke)
{
if (sessionsToRevoke.Contains(currentSessionId))
{
return; // Already processed this session
}
sessionsToRevoke.Add(currentSessionId);
// Find direct children
var childSessions = await db.AuthSessions
.Where(s => s.ParentSessionId == currentSessionId)
.Select(s => s.Id)
.ToListAsync();
foreach (var childId in childSessions)
{
await CollectSessionsToRevoke(childId, sessionsToRevoke);
}
}
/// <summary>
/// Revoke all sessions for an account (logout everywhere)
/// </summary>
@@ -380,13 +424,17 @@ public class AuthService(
if (hasSession)
throw new ArgumentException("Session already exists for this challenge.");
var device = await GetOrCreateDeviceAsync(challenge.AccountId, challenge.DeviceId, challenge.DeviceName,
challenge.Platform);
var now = SystemClock.Instance.GetCurrentInstant();
var session = new SnAuthSession
{
LastGrantedAt = now,
ExpiredAt = now.Plus(Duration.FromDays(7)),
AccountId = challenge.AccountId,
ChallengeId = challenge.Id
ChallengeId = challenge.Id,
ClientId = device.Id,
};
db.AuthSessions.Add(session);
@@ -500,7 +548,7 @@ public class AuthService(
return key;
}
public async Task<SnApiKey> CreateApiKey(Guid accountId, string label, Instant? expiredAt = null)
public async Task<SnApiKey> CreateApiKey(Guid accountId, string label, Instant? expiredAt = null, SnAuthSession? parentSession = null)
{
var key = new SnApiKey
{
@@ -509,7 +557,8 @@ public class AuthService(
Session = new SnAuthSession
{
AccountId = accountId,
ExpiredAt = expiredAt
ExpiredAt = expiredAt,
ParentSessionId = parentSession?.Id
},
};

View File

@@ -342,13 +342,19 @@ public class ConnectionController(
callbackData.State.Split('|').FirstOrDefault() :
string.Empty;
var challenge = await oidcService.CreateChallengeForUserAsync(
if (HttpContext.Items["CurrentSession"] is not SnAuthSession parentSession) parentSession = null;
var session = await oidcService.CreateSessionForUserAsync(
userInfo,
connection.Account,
HttpContext,
deviceId ?? string.Empty);
deviceId ?? string.Empty,
null,
ClientPlatform.Web,
parentSession);
var redirectUrl = QueryHelpers.AddQueryString(redirectBaseUrl, "challenge", challenge.Id.ToString());
var token = auth.CreateToken(session);
var redirectUrl = QueryHelpers.AddQueryString(redirectBaseUrl, "token", token);
logger.LogInformation("OIDC login successful for user {UserId}. Redirecting to {RedirectUrl}", connection.AccountId, redirectUrl);
return Redirect(redirectUrl);
}

View File

@@ -14,6 +14,7 @@ public class OidcController(
IServiceProvider serviceProvider,
AppDatabase db,
AccountService accounts,
AuthService auth,
ICacheService cache,
ILogger<OidcController> logger
)
@@ -22,6 +23,11 @@ public class OidcController(
private const string StateCachePrefix = "oidc-state:";
private static readonly TimeSpan StateExpiration = TimeSpan.FromMinutes(15);
public class TokenExchangeResponse
{
public string Token { get; set; } = string.Empty;
}
[HttpGet("{provider}")]
public async Task<ActionResult> OidcLogin(
[FromRoute] string provider,
@@ -75,7 +81,7 @@ public class OidcController(
/// Handles Apple authentication directly from mobile apps
/// </summary>
[HttpPost("apple/mobile")]
public async Task<ActionResult<SnAuthChallenge>> AppleMobileLogin(
public async Task<ActionResult<TokenExchangeResponse>> AppleMobileLogin(
[FromBody] AppleMobileSignInRequest request
)
{
@@ -98,16 +104,21 @@ public class OidcController(
// Find or create user account using existing logic
var account = await FindOrCreateAccount(userInfo, "apple");
if (HttpContext.Items["CurrentSession"] is not SnAuthSession parentSession) parentSession = null;
// Create session using the OIDC service
var challenge = await appleService.CreateChallengeForUserAsync(
var session = await appleService.CreateSessionForUserAsync(
userInfo,
account,
HttpContext,
request.DeviceId,
request.DeviceName
request.DeviceName,
ClientPlatform.Ios,
parentSession
);
return Ok(challenge);
var token = auth.CreateToken(session);
return Ok(new TokenExchangeResponse { Token = token });
}
catch (SecurityTokenValidationException ex)
{

View File

@@ -1,4 +1,3 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Text;
@@ -250,15 +249,17 @@ public abstract class OidcService(
}
/// <summary>
/// Creates a challenge and session for an authenticated user
/// Creates a session for an authenticated user
/// Also creates or updates the account connection
/// </summary>
public async Task<SnAuthChallenge> CreateChallengeForUserAsync(
public async Task<SnAuthSession> CreateSessionForUserAsync(
OidcUserInfo userInfo,
SnAccount account,
HttpContext request,
string deviceId,
string? deviceName = null
string? deviceName = null,
ClientPlatform platform = ClientPlatform.Web,
SnAuthSession? parentSession = null
)
{
// Create or update the account connection
@@ -282,28 +283,24 @@ public abstract class OidcService(
await Db.AccountConnections.AddAsync(connection);
}
// Create a challenge that's already completed
// Create a session directly
var now = SystemClock.Instance.GetCurrentInstant();
var device = await auth.GetOrCreateDeviceAsync(account.Id, deviceId, deviceName, ClientPlatform.Ios);
var challenge = new SnAuthChallenge
{
ExpiredAt = now.Plus(Duration.FromHours(1)),
StepTotal = await auth.DetectChallengeRisk(request.Request, account),
Type = ChallengeType.Oidc,
Audiences = [ProviderName],
Scopes = ["*"],
AccountId = account.Id,
ClientId = device.Id,
IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null,
UserAgent = request.Request.Headers.UserAgent,
};
challenge.StepRemain--;
if (challenge.StepRemain < 0) challenge.StepRemain = 0;
var device = await auth.GetOrCreateDeviceAsync(account.Id, deviceId, deviceName, platform);
await Db.AuthChallenges.AddAsync(challenge);
var session = new SnAuthSession
{
AccountId = account.Id,
CreatedAt = now,
LastGrantedAt = now,
ParentSessionId = parentSession?.Id,
ClientId = device.Id,
ExpiredAt = now.Plus(Duration.FromDays(30))
};
await Db.AuthSessions.AddAsync(session);
await Db.SaveChangesAsync();
return challenge;
return session;
}
}

View File

@@ -88,7 +88,7 @@ public class TokenAuthService(
session = await db.AuthSessions
.AsNoTracking()
.Include(e => e.Challenge)
.ThenInclude(e => e.Client)
.Include(e => e.Client)
.Include(e => e.Account)
.ThenInclude(e => e.Profile)
.FirstOrDefaultAsync(s => s.Id == sessionId);
@@ -110,7 +110,7 @@ public class TokenAuthService(
"AuthenticateTokenAsync: DB session loaded (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId}, appId={AppId}, scopes={ScopeCount}, ip={Ip}, uaLen={UaLen})",
sessionId,
session.AccountId,
session.Challenge?.ClientId,
session.ClientId,
session.AppId,
session.Challenge?.Scopes.Count,
session.Challenge?.IpAddress,
@@ -143,7 +143,7 @@ public class TokenAuthService(
"AuthenticateTokenAsync: success via DB (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId})",
sessionId,
session.AccountId,
session.Challenge?.ClientId
session.ClientId
);
return (true, session, null);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,143 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class DecoupleAuthSessionAndChallenge : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_auth_challenges_auth_clients_client_id",
table: "auth_challenges");
migrationBuilder.DropIndex(
name: "ix_auth_challenges_client_id",
table: "auth_challenges");
migrationBuilder.DropColumn(
name: "client_id",
table: "auth_challenges");
migrationBuilder.AddColumn<Guid>(
name: "client_id",
table: "auth_sessions",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "parent_session_id",
table: "auth_sessions",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "device_id",
table: "auth_challenges",
type: "character varying(512)",
maxLength: 512,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "device_name",
table: "auth_challenges",
type: "character varying(1024)",
maxLength: 1024,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "platform",
table: "auth_challenges",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "ix_auth_sessions_client_id",
table: "auth_sessions",
column: "client_id");
migrationBuilder.CreateIndex(
name: "ix_auth_sessions_parent_session_id",
table: "auth_sessions",
column: "parent_session_id");
migrationBuilder.AddForeignKey(
name: "fk_auth_sessions_auth_clients_client_id",
table: "auth_sessions",
column: "client_id",
principalTable: "auth_clients",
principalColumn: "id");
migrationBuilder.AddForeignKey(
name: "fk_auth_sessions_auth_sessions_parent_session_id",
table: "auth_sessions",
column: "parent_session_id",
principalTable: "auth_sessions",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_auth_sessions_auth_clients_client_id",
table: "auth_sessions");
migrationBuilder.DropForeignKey(
name: "fk_auth_sessions_auth_sessions_parent_session_id",
table: "auth_sessions");
migrationBuilder.DropIndex(
name: "ix_auth_sessions_client_id",
table: "auth_sessions");
migrationBuilder.DropIndex(
name: "ix_auth_sessions_parent_session_id",
table: "auth_sessions");
migrationBuilder.DropColumn(
name: "client_id",
table: "auth_sessions");
migrationBuilder.DropColumn(
name: "parent_session_id",
table: "auth_sessions");
migrationBuilder.DropColumn(
name: "device_id",
table: "auth_challenges");
migrationBuilder.DropColumn(
name: "device_name",
table: "auth_challenges");
migrationBuilder.DropColumn(
name: "platform",
table: "auth_challenges");
migrationBuilder.AddColumn<Guid>(
name: "client_id",
table: "auth_challenges",
type: "uuid",
nullable: true);
migrationBuilder.CreateIndex(
name: "ix_auth_challenges_client_id",
table: "auth_challenges",
column: "client_id");
migrationBuilder.AddForeignKey(
name: "fk_auth_challenges_auth_clients_client_id",
table: "auth_challenges",
column: "client_id",
principalTable: "auth_clients",
principalColumn: "id");
}
}
}

View File

@@ -778,10 +778,6 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("jsonb")
.HasColumnName("blacklist_factors");
b.Property<Guid?>("ClientId")
.HasColumnType("uuid")
.HasColumnName("client_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
@@ -790,6 +786,17 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("device_id");
b.Property<string>("DeviceName")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("device_name");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
@@ -812,6 +819,10 @@ 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")
@@ -844,9 +855,6 @@ namespace DysonNetwork.Pass.Migrations
b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_challenges_account_id");
b.HasIndex("ClientId")
.HasDatabaseName("ix_auth_challenges_client_id");
b.ToTable("auth_challenges", (string)null);
});
@@ -922,6 +930,10 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("uuid")
.HasColumnName("challenge_id");
b.Property<Guid?>("ClientId")
.HasColumnType("uuid")
.HasColumnName("client_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
@@ -938,6 +950,10 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone")
.HasColumnName("last_granted_at");
b.Property<Guid?>("ParentSessionId")
.HasColumnType("uuid")
.HasColumnName("parent_session_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
@@ -951,6 +967,12 @@ namespace DysonNetwork.Pass.Migrations
b.HasIndex("ChallengeId")
.HasDatabaseName("ix_auth_sessions_challenge_id");
b.HasIndex("ClientId")
.HasDatabaseName("ix_auth_sessions_client_id");
b.HasIndex("ParentSessionId")
.HasDatabaseName("ix_auth_sessions_parent_session_id");
b.ToTable("auth_sessions", (string)null);
});
@@ -2374,14 +2396,7 @@ namespace DysonNetwork.Pass.Migrations
.IsRequired()
.HasConstraintName("fk_auth_challenges_accounts_account_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.HasConstraintName("fk_auth_challenges_auth_clients_client_id");
b.Navigation("Account");
b.Navigation("Client");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b =>
@@ -2410,9 +2425,23 @@ namespace DysonNetwork.Pass.Migrations
.HasForeignKey("ChallengeId")
.HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.HasConstraintName("fk_auth_sessions_auth_clients_client_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthSession", "ParentSession")
.WithMany()
.HasForeignKey("ParentSessionId")
.HasConstraintName("fk_auth_sessions_auth_sessions_parent_session_id");
b.Navigation("Account");
b.Navigation("Challenge");
b.Navigation("Client");
b.Navigation("ParentSession");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b =>

View File

@@ -16,10 +16,18 @@ public class SnAuthSession : ModelBase
public Guid AccountId { get; set; }
[JsonIgnore] public SnAccount Account { get; set; } = null!;
// When the challenge is null, indicates the session is for an API Key
// The challenge that created this session
public Guid? ChallengeId { get; set; }
public SnAuthChallenge? Challenge { get; set; } = null!;
// The client device for this session
public Guid? ClientId { get; set; }
public SnAuthClient? Client { get; set; } = null!;
// For sub-sessions (e.g. OAuth)
public Guid? ParentSessionId { get; set; }
public SnAuthSession? ParentSession { get; set; }
// Indicates the session is for an OIDC connection
public Guid? AppId { get; set; }
@@ -32,6 +40,9 @@ public class SnAuthSession : ModelBase
Account = Account.ToProtoValue(),
ChallengeId = ChallengeId.ToString(),
Challenge = Challenge?.ToProtoValue(),
ClientId = ClientId.ToString(),
Client = Client?.ToProtoValue(),
ParentSessionId = ParentSessionId.ToString(),
AppId = AppId?.ToString()
};
}
@@ -67,13 +78,14 @@ public class SnAuthChallenge : 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(512)] public string DeviceId { get; set; } = null!;
[MaxLength(1024)] public string? DeviceName { get; set; }
public ClientPlatform Platform { get; set; }
[MaxLength(1024)] public string? Nonce { get; set; }
[Column(TypeName = "jsonb")] public GeoPoint? Location { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public SnAccount Account { get; set; } = null!;
public Guid? ClientId { get; set; }
public SnAuthClient? Client { get; set; } = null!;
public SnAuthChallenge Normalize()
{
@@ -94,7 +106,7 @@ public class SnAuthChallenge : ModelBase
Scopes = { Scopes },
IpAddress = IpAddress,
UserAgent = UserAgent,
DeviceId = Client!.DeviceId,
DeviceId = DeviceId,
Nonce = Nonce,
AccountId = AccountId.ToString()
};
@@ -110,6 +122,16 @@ public class SnAuthClient : ModelBase
public Guid AccountId { get; set; }
[JsonIgnore] public SnAccount Account { get; set; } = null!;
public Proto.AuthClient ToProtoValue() => new()
{
Id = Id.ToString(),
Platform = (Proto.ClientPlatform)Platform,
DeviceName = DeviceName,
DeviceLabel = DeviceLabel,
DeviceId = DeviceId,
AccountId = AccountId.ToString()
};
}
public class SnAuthClientWithChallenge : SnAuthClient

View File

@@ -8,7 +8,7 @@ import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/struct.proto";
import 'account.proto';
import "account.proto";
// Represents a user session
message AuthSession {
@@ -20,6 +20,9 @@ message AuthSession {
string challenge_id = 7;
AuthChallenge challenge = 8;
google.protobuf.StringValue app_id = 9;
optional string client_id = 10;
optional string parent_session_id = 11;
AuthClient client = 12;
}
// Represents an authentication challenge
@@ -39,6 +42,17 @@ message AuthChallenge {
google.protobuf.StringValue nonce = 14;
// Point location is omitted as there is no direct proto equivalent.
string account_id = 15;
google.protobuf.StringValue device_name = 16;
ClientPlatform platform = 17;
}
message AuthClient {
string id = 1;
ClientPlatform platform = 2;
google.protobuf.StringValue device_name = 3;
google.protobuf.StringValue device_label = 4;
string device_id = 5;
string account_id = 6;
}
// Enum for challenge types
@@ -49,9 +63,9 @@ enum ChallengeType {
OIDC = 3;
}
// Enum for challenge platforms
enum ChallengePlatform {
CHALLENGE_PLATFORM_UNSPECIFIED = 0;
// Enum for client platforms
enum ClientPlatform {
CLIENT_PLATFORM_UNSPECIFIED = 0;
UNIDENTIFIED = 1;
WEB = 2;
IOS = 3;
@@ -63,7 +77,7 @@ enum ChallengePlatform {
service AuthService {
rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) {}
rpc ValidatePin(ValidatePinRequest) returns (ValidateResponse) {}
rpc ValidateCaptcha(ValidateCaptchaRequest) returns (ValidateResponse) {}
}
@@ -184,6 +198,7 @@ service PermissionService {
rpc AddPermissionNode(AddPermissionNodeRequest) returns (AddPermissionNodeResponse) {}
rpc AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest) returns (AddPermissionNodeToGroupResponse) {}
rpc RemovePermissionNode(RemovePermissionNodeRequest) returns (RemovePermissionNodeResponse) {}
rpc RemovePermissionNodeFromGroup(RemovePermissionNodeFromGroupRequest) returns (RemovePermissionNodeFromGroupResponse) {}
rpc RemovePermissionNodeFromGroup(RemovePermissionNodeFromGroupRequest)
returns (RemovePermissionNodeFromGroupResponse) {}
}