♻️ Refactored to make a simplifier auth session system
This commit is contained in:
@@ -597,7 +597,6 @@ public class AccountCurrentController(
|
||||
|
||||
var query = db.AuthSessions
|
||||
.Include(session => session.Account)
|
||||
.Include(session => session.Challenge)
|
||||
.Where(session => session.Account.Id == currentUser.Id);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
@@ -70,7 +70,7 @@ public class DysonTokenAuthHandler(
|
||||
};
|
||||
|
||||
// Add scopes as claims
|
||||
session.Challenge?.Scopes.ForEach(scope => claims.Add(new Claim("scope", scope)));
|
||||
session.Scopes.ForEach(scope => claims.Add(new Claim("scope", scope)));
|
||||
|
||||
// Add superuser claim if applicable
|
||||
if (session.Account.IsSuperuser)
|
||||
@@ -117,16 +117,17 @@ public class DysonTokenAuthHandler(
|
||||
{
|
||||
if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var token = authHeader["Bearer ".Length..].Trim();
|
||||
var parts = token.Split('.');
|
||||
var tokenText = authHeader["Bearer ".Length..].Trim();
|
||||
var parts = tokenText.Split('.');
|
||||
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = token,
|
||||
Token = tokenText,
|
||||
Type = parts.Length == 3 ? TokenType.OidcKey : TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
else if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
@@ -134,7 +135,8 @@ public class DysonTokenAuthHandler(
|
||||
Type = TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
else if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
|
||||
@@ -34,8 +34,8 @@ public class AuthController(
|
||||
[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; }
|
||||
public List<string> Audiences { get; set; } = new();
|
||||
public List<string> Scopes { get; set; } = new();
|
||||
public List<string> Audiences { get; set; } = [];
|
||||
public List<string> Scopes { get; set; } = [];
|
||||
}
|
||||
|
||||
[HttpPost("challenge")]
|
||||
@@ -68,15 +68,9 @@ public class AuthController(
|
||||
.Where(e => e.UserAgent == userAgent)
|
||||
.Where(e => e.StepRemain > 0)
|
||||
.Where(e => e.ExpiredAt != null && now < e.ExpiredAt)
|
||||
.Where(e => e.Type == Shared.Models.ChallengeType.Login)
|
||||
.Where(e => e.DeviceId == request.DeviceId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingChallenge is not null)
|
||||
{
|
||||
var existingSession = await db.AuthSessions.Where(e => e.ChallengeId == existingChallenge.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingSession is null) return existingChallenge;
|
||||
}
|
||||
if (existingChallenge is not null) return existingChallenge;
|
||||
|
||||
var challenge = new SnAuthChallenge
|
||||
{
|
||||
@@ -111,14 +105,11 @@ public class AuthController(
|
||||
.ThenInclude(e => e.Profile)
|
||||
.FirstOrDefaultAsync(e => e.Id == id);
|
||||
|
||||
if (challenge is null)
|
||||
{
|
||||
logger.LogWarning("GetChallenge: challenge not found (challengeId={ChallengeId}, ip={IpAddress})",
|
||||
id, HttpContext.Connection.RemoteIpAddress?.ToString());
|
||||
return NotFound("Auth challenge was not found.");
|
||||
}
|
||||
if (challenge is not null) return challenge;
|
||||
logger.LogWarning("GetChallenge: challenge not found (challengeId={ChallengeId}, ip={IpAddress})",
|
||||
id, HttpContext.Connection.RemoteIpAddress?.ToString());
|
||||
return NotFound("Auth challenge was not found.");
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
[HttpGet("challenge/{id:guid}/factors")]
|
||||
@@ -216,7 +207,7 @@ public class AuthController(
|
||||
throw new ArgumentException("Invalid password.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
challenge.FailedAttempts++;
|
||||
db.Update(challenge);
|
||||
@@ -229,8 +220,11 @@ public class AuthController(
|
||||
);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
logger.LogWarning("DoChallenge: authentication failure (challengeId={ChallengeId}, factorId={FactorId}, accountId={AccountId}, failedAttempts={FailedAttempts}, factorType={FactorType}, ip={IpAddress}, uaLength={UaLength})",
|
||||
challenge.Id, factor.Id, challenge.AccountId, challenge.FailedAttempts, factor.Type, HttpContext.Connection.RemoteIpAddress?.ToString(), (HttpContext.Request.Headers.UserAgent.ToString() ?? "").Length);
|
||||
logger.LogWarning(
|
||||
"DoChallenge: authentication failure (challengeId={ChallengeId}, factorId={FactorId}, accountId={AccountId}, failedAttempts={FailedAttempts}, factorType={FactorType}, ip={IpAddress}, uaLength={UaLength})",
|
||||
challenge.Id, factor.Id, challenge.AccountId, challenge.FailedAttempts, factor.Type,
|
||||
HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
HttpContext.Request.Headers.UserAgent.ToString().Length);
|
||||
|
||||
return BadRequest("Invalid password.");
|
||||
}
|
||||
@@ -240,7 +234,7 @@ public class AuthController(
|
||||
AccountService.SetCultureInfo(challenge.Account);
|
||||
await pusher.SendPushNotificationToUserAsync(new SendPushNotificationToUserRequest
|
||||
{
|
||||
Notification = new PushNotification()
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "auth.login",
|
||||
Title = localizer["NewLoginTitle"],
|
||||
@@ -279,7 +273,7 @@ public class AuthController(
|
||||
{
|
||||
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
|
||||
[MaxLength(1024)] public string? DeviceName { get; set; }
|
||||
[Required] public DysonNetwork.Shared.Models.ClientPlatform Platform { get; set; }
|
||||
[Required] public Shared.Models.ClientPlatform Platform { get; set; }
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -338,8 +332,9 @@ public class AuthController(
|
||||
[Microsoft.AspNetCore.Authorization.Authorize] // Use full namespace to avoid ambiguity with DysonNetwork.Pass.Permission.Authorize
|
||||
public async Task<ActionResult<TokenExchangeResponse>> LoginFromSession([FromBody] NewSessionRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser ||
|
||||
HttpContext.Items["CurrentSession"] is not Shared.Models.SnAuthSession currentSession) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount ||
|
||||
HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession)
|
||||
return Unauthorized();
|
||||
|
||||
var newSession = await auth.CreateSessionFromParentAsync(
|
||||
currentSession,
|
||||
@@ -352,13 +347,12 @@ public class AuthController(
|
||||
var tk = auth.CreateToken(newSession);
|
||||
|
||||
// Set cookie using HttpContext, similar to CreateSessionAndIssueToken
|
||||
var cookieDomain = _cookieDomain;
|
||||
HttpContext.Response.Cookies.Append(AuthConstants.CookieTokenName, tk, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true,
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Domain = cookieDomain,
|
||||
Domain = _cookieDomain,
|
||||
Expires = request.ExpiredAt?.ToDateTimeOffset() ?? DateTime.UtcNow.AddYears(20)
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public class AuthService(
|
||||
{
|
||||
// 1) Find out how many authentication factors the account has enabled.
|
||||
var enabledFactors = await db.AccountAuthFactors
|
||||
.Where(f => f.AccountId == account.Id)
|
||||
.Where(f => f.AccountId == account.Id && f.Type != AccountAuthFactorType.PinCode)
|
||||
.Where(f => f.EnabledAt != null)
|
||||
.ToListAsync();
|
||||
var maxSteps = enabledFactors.Count;
|
||||
@@ -42,13 +42,15 @@ public class AuthService(
|
||||
|
||||
// 2) Get login context from recent sessions
|
||||
var recentSessions = await db.AuthSessions
|
||||
.Include(s => s.Challenge)
|
||||
.Where(s => s.AccountId == account.Id)
|
||||
.Where(s => s.LastGrantedAt != null)
|
||||
.OrderByDescending(s => s.LastGrantedAt)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var recentChallengeIds = recentSessions.Where(s => s.ChallengeId != null).Select(s => s.ChallengeId.Value).ToList();
|
||||
var recentChallenges = await db.AuthChallenges.Where(c => recentChallengeIds.Contains(c.Id)).ToListAsync();
|
||||
|
||||
var ipAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
var userAgent = request.Headers.UserAgent.ToString();
|
||||
|
||||
@@ -60,14 +62,14 @@ public class AuthService(
|
||||
else
|
||||
{
|
||||
// Check if IP has been used before
|
||||
var ipPreviouslyUsed = recentSessions.Any(s => s.Challenge?.IpAddress == ipAddress);
|
||||
var ipPreviouslyUsed = recentChallenges.Any(c => c.IpAddress == ipAddress);
|
||||
if (!ipPreviouslyUsed)
|
||||
{
|
||||
riskScore += 8;
|
||||
}
|
||||
|
||||
// Check geographical distance for last known location
|
||||
var lastKnownIp = recentSessions.FirstOrDefault(s => !string.IsNullOrWhiteSpace(s.Challenge?.IpAddress))?.Challenge?.IpAddress;
|
||||
var lastKnownIp = recentChallenges.FirstOrDefault(c => !string.IsNullOrWhiteSpace(c.IpAddress))?.IpAddress;
|
||||
if (!string.IsNullOrWhiteSpace(lastKnownIp) && lastKnownIp != ipAddress)
|
||||
{
|
||||
riskScore += 6;
|
||||
@@ -81,9 +83,9 @@ public class AuthService(
|
||||
}
|
||||
else
|
||||
{
|
||||
var uaPreviouslyUsed = recentSessions.Any(s =>
|
||||
!string.IsNullOrWhiteSpace(s.Challenge?.UserAgent) &&
|
||||
string.Equals(s.Challenge.UserAgent, userAgent, StringComparison.OrdinalIgnoreCase));
|
||||
var uaPreviouslyUsed = recentChallenges.Any(c =>
|
||||
!string.IsNullOrWhiteSpace(c.UserAgent) &&
|
||||
string.Equals(c.UserAgent, userAgent, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!uaPreviouslyUsed)
|
||||
{
|
||||
@@ -184,30 +186,18 @@ public class AuthService(
|
||||
public async Task<SnAuthSession> CreateSessionForOidcAsync(SnAccount account, Instant time,
|
||||
Guid? customAppId = null, SnAuthSession? parentSession = null)
|
||||
{
|
||||
var challenge = new SnAuthChallenge
|
||||
{
|
||||
AccountId = account.Id,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
UserAgent = HttpContext.Request.Headers.UserAgent,
|
||||
StepRemain = 1,
|
||||
StepTotal = 1,
|
||||
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc,
|
||||
DeviceId = Guid.NewGuid().ToString(),
|
||||
DeviceName = "OIDC/OAuth",
|
||||
Platform = ClientPlatform.Web,
|
||||
};
|
||||
|
||||
var session = new SnAuthSession
|
||||
{
|
||||
AccountId = account.Id,
|
||||
CreatedAt = time,
|
||||
LastGrantedAt = time,
|
||||
Challenge = challenge,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
UserAgent = HttpContext.Request.Headers.UserAgent,
|
||||
AppId = customAppId,
|
||||
ParentSessionId = parentSession?.Id
|
||||
ParentSessionId = parentSession?.Id,
|
||||
Type = customAppId is not null ? SessionType.OAuth : SessionType.Oidc,
|
||||
};
|
||||
|
||||
db.AuthChallenges.Add(challenge);
|
||||
db.AuthSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
@@ -419,11 +409,6 @@ public class AuthService(
|
||||
if (challenge.StepRemain != 0)
|
||||
throw new ArgumentException("Challenge not yet completed.");
|
||||
|
||||
var hasSession = await db.AuthSessions
|
||||
.AnyAsync(e => e.ChallengeId == challenge.Id);
|
||||
if (hasSession)
|
||||
throw new ArgumentException("Session already exists for this challenge.");
|
||||
|
||||
var device = await GetOrCreateDeviceAsync(challenge.AccountId, challenge.DeviceId, challenge.DeviceName,
|
||||
challenge.Platform);
|
||||
|
||||
@@ -433,6 +418,10 @@ public class AuthService(
|
||||
LastGrantedAt = now,
|
||||
ExpiredAt = now.Plus(Duration.FromDays(7)),
|
||||
AccountId = challenge.AccountId,
|
||||
IpAddress = challenge.IpAddress,
|
||||
UserAgent = challenge.UserAgent,
|
||||
Scopes = challenge.Scopes,
|
||||
Audiences = challenge.Audiences,
|
||||
ChallengeId = challenge.Id,
|
||||
ClientId = device.Id,
|
||||
};
|
||||
@@ -457,7 +446,7 @@ public class AuthService(
|
||||
return tk;
|
||||
}
|
||||
|
||||
private string CreateCompactToken(Guid sessionId, RSA rsa)
|
||||
private static string CreateCompactToken(Guid sessionId, RSA rsa)
|
||||
{
|
||||
// Create the payload: just the session ID
|
||||
var payloadBytes = sessionId.ToByteArray();
|
||||
|
||||
@@ -306,7 +306,7 @@ public class OidcProviderController(
|
||||
HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized();
|
||||
|
||||
// Get requested scopes from the token
|
||||
var scopes = currentSession.Challenge?.Scopes ?? [];
|
||||
var scopes = currentSession.Scopes;
|
||||
|
||||
var userInfo = new Dictionary<string, object>
|
||||
{
|
||||
|
||||
@@ -72,7 +72,6 @@ public class OidcProviderService(
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
var queryable = db.AuthSessions
|
||||
.Include(s => s.Challenge)
|
||||
.AsQueryable();
|
||||
if (withAccount)
|
||||
queryable = queryable
|
||||
@@ -85,8 +84,7 @@ public class OidcProviderService(
|
||||
.Where(s => s.AccountId == accountId &&
|
||||
s.AppId == clientId &&
|
||||
(s.ExpiredAt == null || s.ExpiredAt > now) &&
|
||||
s.Challenge != null &&
|
||||
s.Challenge.Type == Shared.Models.ChallengeType.OAuth)
|
||||
s.Type == Shared.Models.SessionType.OAuth)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
@@ -511,7 +509,6 @@ public class OidcProviderService(
|
||||
{
|
||||
return await db.AuthSessions
|
||||
.Include(s => s.Account)
|
||||
.Include(s => s.Challenge)
|
||||
.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ public class TokenAuthService(
|
||||
"AuthenticateTokenAsync: success via cache (sessionId={SessionId}, accountId={AccountId}, scopes={ScopeCount}, expiresAt={ExpiresAt})",
|
||||
sessionId,
|
||||
session.AccountId,
|
||||
session.Challenge?.Scopes.Count,
|
||||
session.Scopes.Count,
|
||||
session.ExpiredAt
|
||||
);
|
||||
return (true, session, null);
|
||||
@@ -87,7 +87,6 @@ public class TokenAuthService(
|
||||
|
||||
session = await db.AuthSessions
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Challenge)
|
||||
.Include(e => e.Client)
|
||||
.Include(e => e.Account)
|
||||
.ThenInclude(e => e.Profile)
|
||||
@@ -112,9 +111,9 @@ public class TokenAuthService(
|
||||
session.AccountId,
|
||||
session.ClientId,
|
||||
session.AppId,
|
||||
session.Challenge?.Scopes.Count,
|
||||
session.Challenge?.IpAddress,
|
||||
(session.Challenge?.UserAgent ?? string.Empty).Length
|
||||
session.Scopes.Count,
|
||||
session.IpAddress,
|
||||
(session.UserAgent ?? string.Empty).Length
|
||||
);
|
||||
|
||||
logger.LogDebug("AuthenticateTokenAsync: enriching account with subscription (accountId={AccountId})", session.AccountId);
|
||||
|
||||
2882
DysonNetwork.Pass/Migrations/20251202160759_SimplifiedAuthSession.Designer.cs
generated
Normal file
2882
DysonNetwork.Pass/Migrations/20251202160759_SimplifiedAuthSession.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Pass.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SimplifiedAuthSession : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_auth_sessions_auth_challenges_challenge_id",
|
||||
table: "auth_sessions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_auth_sessions_challenge_id",
|
||||
table: "auth_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "type",
|
||||
table: "auth_challenges");
|
||||
|
||||
migrationBuilder.AddColumn<List<string>>(
|
||||
name: "audiences",
|
||||
table: "auth_sessions",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValue: new List<string>());
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ip_address",
|
||||
table: "auth_sessions",
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<List<string>>(
|
||||
name: "scopes",
|
||||
table: "auth_sessions",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValue: new List<string>());
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "type",
|
||||
table: "auth_sessions",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "user_agent",
|
||||
table: "auth_sessions",
|
||||
type: "character varying(512)",
|
||||
maxLength: 512,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "audiences",
|
||||
table: "auth_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ip_address",
|
||||
table: "auth_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "scopes",
|
||||
table: "auth_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "type",
|
||||
table: "auth_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "user_agent",
|
||||
table: "auth_sessions");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "type",
|
||||
table: "auth_challenges",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_sessions_challenge_id",
|
||||
table: "auth_sessions",
|
||||
column: "challenge_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_auth_sessions_auth_challenges_challenge_id",
|
||||
table: "auth_sessions",
|
||||
column: "challenge_id",
|
||||
principalTable: "auth_challenges",
|
||||
principalColumn: "id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -933,10 +933,6 @@ namespace DysonNetwork.Pass.Migrations
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("step_total");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
@@ -1023,6 +1019,11 @@ namespace DysonNetwork.Pass.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("app_id");
|
||||
|
||||
b.Property<List<string>>("Audiences")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("audiences");
|
||||
|
||||
b.Property<Guid?>("ChallengeId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("challenge_id");
|
||||
@@ -1043,6 +1044,11 @@ namespace DysonNetwork.Pass.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("ip_address");
|
||||
|
||||
b.Property<Instant?>("LastGrantedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_granted_at");
|
||||
@@ -1051,19 +1057,30 @@ namespace DysonNetwork.Pass.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("parent_session_id");
|
||||
|
||||
b.Property<List<string>>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("user_agent");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_sessions");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.HasDatabaseName("ix_auth_sessions_account_id");
|
||||
|
||||
b.HasIndex("ChallengeId")
|
||||
.HasDatabaseName("ix_auth_sessions_challenge_id");
|
||||
|
||||
b.HasIndex("ClientId")
|
||||
.HasDatabaseName("ix_auth_sessions_client_id");
|
||||
|
||||
@@ -2537,11 +2554,6 @@ namespace DysonNetwork.Pass.Migrations
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_sessions_accounts_account_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnAuthChallenge", "Challenge")
|
||||
.WithMany()
|
||||
.HasForeignKey("ChallengeId")
|
||||
.HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
@@ -2554,8 +2566,6 @@ namespace DysonNetwork.Pass.Migrations
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Challenge");
|
||||
|
||||
b.Navigation("Client");
|
||||
|
||||
b.Navigation("ParentSession");
|
||||
|
||||
@@ -4,7 +4,6 @@ using DysonNetwork.Shared.Stream;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Net;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using WebSocketPacket = DysonNetwork.Shared.Models.WebSocketPacket;
|
||||
|
||||
@@ -40,10 +39,10 @@ public class WebSocketController(
|
||||
}
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id!);
|
||||
var deviceId = currentSession.Challenge?.DeviceId ?? Guid.NewGuid().ToString();
|
||||
var deviceId = currentSession.ClientId;
|
||||
|
||||
// TODO temporary fix due to the server update
|
||||
if (string.IsNullOrEmpty(deviceId)) deviceId = Guid.NewGuid().ToString().Replace("-", "");
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
return BadRequest("Unable to determine device id");
|
||||
if (deviceAlt is not null)
|
||||
deviceId = $"{deviceId}+{deviceAlt}";
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ public class NotificationController(
|
||||
|
||||
var result =
|
||||
await nty.SubscribeDevice(
|
||||
currentSession.Challenge.DeviceId,
|
||||
currentSession.ClientId,
|
||||
request.DeviceToken,
|
||||
request.Provider,
|
||||
currentUser
|
||||
@@ -117,7 +117,7 @@ public class NotificationController(
|
||||
var affectedRows = await db.PushSubscriptions
|
||||
.Where(s =>
|
||||
s.AccountId == accountId &&
|
||||
s.DeviceId == currentSession.Challenge.DeviceId
|
||||
s.DeviceId == currentSession.ClientId
|
||||
).ExecuteDeleteAsync();
|
||||
return Ok(affectedRows);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class DysonTokenAuthHandler(
|
||||
};
|
||||
|
||||
// Add scopes as claims
|
||||
session.Challenge?.Scopes.ToList().ForEach(scope => claims.Add(new Claim("scope", scope)));
|
||||
session.Scopes.ToList().ForEach(scope => claims.Add(new Claim("scope", scope)));
|
||||
|
||||
// Add superuser claim if applicable
|
||||
if (session.Account.IsSuperuser)
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||
<PackageReference Include="MessagePack" Version="2.5.192" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="MessagePack.NodaTime" Version="3.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.0" />
|
||||
|
||||
@@ -70,7 +70,7 @@ public static class Extensions
|
||||
return RedLockFactory.Create(new List<RedLockMultiplexer> { new(mux) });
|
||||
});
|
||||
builder.Services.AddSingleton<ICacheService, CacheServiceRedis>();
|
||||
builder.Services.AddSingleton<ICacheSerializer, JsonCacheSerializer>();
|
||||
builder.Services.AddSingleton<ICacheSerializer, MessagePackCacheSerializer>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -2,24 +2,34 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.GeoIp;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public enum SessionType
|
||||
{
|
||||
Login,
|
||||
OAuth, // Trying to authorize other platforms
|
||||
Oidc // Trying to connect other platforms
|
||||
}
|
||||
|
||||
public class SnAuthSession : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public SessionType Type { get; set; } = SessionType.Login;
|
||||
public Instant? LastGrantedAt { get; set; }
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = [];
|
||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = [];
|
||||
[MaxLength(128)] public string? IpAddress { get; set; }
|
||||
[MaxLength(512)] public string? UserAgent { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public SnAccount Account { get; set; } = null!;
|
||||
|
||||
// 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!;
|
||||
@@ -28,30 +38,41 @@ public class SnAuthSession : ModelBase
|
||||
public Guid? ParentSessionId { get; set; }
|
||||
public SnAuthSession? ParentSession { get; set; }
|
||||
|
||||
// The origin challenge for this session
|
||||
public Guid? ChallengeId { get; set; }
|
||||
|
||||
// Indicates the session is for an OIDC connection
|
||||
public Guid? AppId { get; set; }
|
||||
|
||||
public Proto.AuthSession ToProtoValue() => new()
|
||||
public AuthSession ToProtoValue()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
LastGrantedAt = LastGrantedAt?.ToTimestamp(),
|
||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
||||
AccountId = AccountId.ToString(),
|
||||
Account = Account.ToProtoValue(),
|
||||
ChallengeId = ChallengeId.ToString(),
|
||||
Challenge = Challenge?.ToProtoValue(),
|
||||
ClientId = ClientId.ToString(),
|
||||
Client = Client?.ToProtoValue(),
|
||||
ParentSessionId = ParentSessionId.ToString(),
|
||||
AppId = AppId?.ToString()
|
||||
};
|
||||
}
|
||||
var proto = new AuthSession
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
LastGrantedAt = LastGrantedAt?.ToTimestamp(),
|
||||
Type = Type switch
|
||||
{
|
||||
SessionType.Login => Proto.SessionType.Login,
|
||||
SessionType.OAuth => Proto.SessionType.Oauth,
|
||||
SessionType.Oidc => Proto.SessionType.Oidc,
|
||||
_ => Proto.SessionType.ChallengeTypeUnspecified
|
||||
},
|
||||
IpAddress = IpAddress,
|
||||
UserAgent = UserAgent,
|
||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
||||
AccountId = AccountId.ToString(),
|
||||
Account = Account.ToProtoValue(),
|
||||
ClientId = ClientId.ToString(),
|
||||
Client = Client?.ToProtoValue(),
|
||||
ParentSessionId = ParentSessionId.ToString(),
|
||||
AppId = AppId?.ToString()
|
||||
};
|
||||
|
||||
public enum ChallengeType
|
||||
{
|
||||
Login,
|
||||
OAuth, // Trying to authorize other platforms
|
||||
Oidc // Trying to connect other platforms
|
||||
proto.Audiences.AddRange(Audiences);
|
||||
proto.Scopes.AddRange(Scopes);
|
||||
|
||||
return proto;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ClientPlatform
|
||||
@@ -72,10 +93,9 @@ public class SnAuthChallenge : ModelBase
|
||||
public int StepRemain { get; set; }
|
||||
public int StepTotal { get; set; }
|
||||
public int FailedAttempts { get; set; }
|
||||
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();
|
||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = [];
|
||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = [];
|
||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = [];
|
||||
[MaxLength(128)] public string? IpAddress { get; set; }
|
||||
[MaxLength(512)] public string? UserAgent { get; set; }
|
||||
[MaxLength(512)] public string DeviceId { get; set; } = null!;
|
||||
@@ -93,14 +113,13 @@ public class SnAuthChallenge : ModelBase
|
||||
return this;
|
||||
}
|
||||
|
||||
public Proto.AuthChallenge ToProtoValue() => new()
|
||||
public AuthChallenge ToProtoValue() => new()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
||||
StepRemain = StepRemain,
|
||||
StepTotal = StepTotal,
|
||||
FailedAttempts = FailedAttempts,
|
||||
Type = (Proto.ChallengeType)Type,
|
||||
BlacklistFactors = { BlacklistFactors.Select(x => x.ToString()) },
|
||||
Audiences = { Audiences },
|
||||
Scopes = { Scopes },
|
||||
|
||||
@@ -17,12 +17,15 @@ message AuthSession {
|
||||
optional google.protobuf.Timestamp expired_at = 4;
|
||||
string account_id = 5;
|
||||
Account account = 6;
|
||||
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;
|
||||
repeated string audiences = 13;
|
||||
repeated string scopes = 14;
|
||||
google.protobuf.StringValue ip_address = 15;
|
||||
google.protobuf.StringValue user_agent = 16;
|
||||
SessionType type = 17;
|
||||
}
|
||||
|
||||
// Represents an authentication challenge
|
||||
@@ -32,7 +35,6 @@ message AuthChallenge {
|
||||
int32 step_remain = 3;
|
||||
int32 step_total = 4;
|
||||
int32 failed_attempts = 5;
|
||||
ChallengeType type = 7;
|
||||
repeated string blacklist_factors = 8;
|
||||
repeated string audiences = 9;
|
||||
repeated string scopes = 10;
|
||||
@@ -56,7 +58,7 @@ message AuthClient {
|
||||
}
|
||||
|
||||
// Enum for challenge types
|
||||
enum ChallengeType {
|
||||
enum SessionType {
|
||||
CHALLENGE_TYPE_UNSPECIFIED = 0;
|
||||
LOGIN = 1;
|
||||
OAUTH = 2;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConcurrentDictionary_00602_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1f443143201742669eeb211a435e32ae4c600_003F24_003F59c4e69f_003FConcurrentDictionary_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConnectionMultiplexer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F2ed0e2f073b1d77b98dadb822da09ee8a9dfb91bf29bf2bbaecb8750d7e74cc9_003FConnectionMultiplexer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AContainerResourceBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F19256b6d2a8a458692f07fe8d98d79e9161628_003Fd7_003F266d041b_003FContainerResourceBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AContractlessStandardResolver_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0fb7a5f343ed4578a15c716986a3f17950c00_003F78_003F264ef090_003FContractlessStandardResolver_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003Ff6_003Fdf150bb3_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AController_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb320290c1b964c3e88434ff5505d9086c9a00_003Fdf_003F95b535f9_003FController_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACookieOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F663f33943e4c4e889dc7050c1e97e703e000_003F89_003Fb06980d7_003FCookieOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@@ -107,6 +108,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMarkdownExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F941558ce147e451baff49677c95dde8d75000_003F67_003F8c0cc1a5_003FMarkdownExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMarkdownExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5cf0a036d23b7f3f04eba9a3fe5c9b5dfb37a2b8c23602bc5176317374cb1c_003FMarkdownExtensions_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMediaAnalysis_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fd7_003F5c138865_003FMediaAnalysis_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMessagePackSerializerOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0fb7a5f343ed4578a15c716986a3f17950c00_003Ffc_003F34de33e4_003FMessagePackSerializerOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMicrosoftDependencyInjectionJobFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003Fa8_003F91b091de_003FMicrosoftDependencyInjectionJobFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANodaExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F472c5156daf14d04a5b30e00cd80dbf85ac0_003Ff4_003F9e841463_003FNodaExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFoundResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F28_003F290250f5_003FNotFoundResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
||||
Reference in New Issue
Block a user