♻️ Refactored to make a simplifier auth session system

This commit is contained in:
2025-12-03 00:38:28 +08:00
parent 74c8f3490d
commit 270c211cb8
18 changed files with 3130 additions and 130 deletions

View File

@@ -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();

View File

@@ -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
{

View File

@@ -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)
{
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)
});

View File

@@ -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();

View File

@@ -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>
{

View File

@@ -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);
}

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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");

View File

@@ -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}";

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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;
}

View File

@@ -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()
{
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(),
ChallengeId = ChallengeId.ToString(),
Challenge = Challenge?.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 },

View File

@@ -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;

View File

@@ -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>