Auth Sessions & Token Management

Deep-dive into the six session and authentication entities: RefreshToken, OtpCode, QrLoginSession, WebAuthnChallenge, AdminPasskey, and ExternalLogin — including all fields, security semantics, and the QR login flow.

SCRIPE supports multiple concurrent authentication mechanisms. Each mechanism has a dedicated entity in the Identity module. RefreshToken manages sliding session windows. OtpCode handles time-limited one-time codes. QrLoginSession enables cross-device login via QR scanning. WebAuthnChallenge powers FIDO2 passkey ceremonies. AdminPasskey stores registered FIDO2 credentials. ExternalLogin links third-party identity providers to Admin and User accounts.

RefreshToken Entity

RefreshToken stores a long-lived opaque token issued alongside a JWT access token. Tokens are rotated on each use — the old token is revoked with a ReplacedByToken pointer, and a new token is issued. Impersonation sessions are tracked via ImpersonatorAdminId, enabling the StopImpersonation flow to restore the original admin's session.

FieldTypeNotes
UserIdGuid?FK → User. Mutually exclusive with AdminId
AdminIdGuid?FK → Admin. Mutually exclusive with UserId
ImpersonatorAdminIdGuid?Set when this token represents an impersonation session; points to the admin who initiated it
Tokenstring (max 500)Opaque refresh token value — cryptographically random
ExpiresAtDateTimeHard expiry timestamp
RevokedAtDateTime?Set when the token is explicitly revoked
ReplacedByTokenstring? (max 500)New token value that replaced this one on rotation
RevokedReasonstring? (max 200)Human-readable reason for revocation
DeviceInfostring? (max 500)User-agent / device string for the session
IpAddressstring? (max 50)IP address at token issuance
StaySignedInboolWhether the user opted into extended session duration
IsActivebool (computed)true when RevokedAt == null && ExpiresAt > UtcNow
Identity.Domain/Entities/RefreshToken.csC#
public class RefreshToken : AuditableEntity<Guid>
{
    public Guid? UserId { get; set; }
    public Guid? AdminId { get; set; }

    /// If set, this token represents an impersonation session.
    /// Points to the admin who initiated the impersonation.
    public Guid? ImpersonatorAdminId { get; set; }

    [MaxLength(500)]
    public string Token { get; set; } = null!;

    public DateTime ExpiresAt { get; set; }
    public DateTime? RevokedAt { get; set; }

    [MaxLength(500)]
    public string? ReplacedByToken { get; set; }

    [MaxLength(200)]
    public string? RevokedReason { get; set; }

    public bool StaySignedIn { get; set; }

    public new bool IsActive => RevokedAt == null && ExpiresAt > DateTime.UtcNow;

    public void Revoke(string reason, string? ipAddress = null, string? replacedByToken = null)
    {
        RevokedAt = DateTime.UtcNow;
        RevokedReason = reason;
        IpAddress = ipAddress;
        ReplacedByToken = replacedByToken;
    }
}

OtpCode Entity

OtpCode is a polymorphic one-time code that serves both users and admins. The Purpose integer maps to an enum (email verification, password reset, 2FA, etc.). Codes are invalidated via Invalidate() after successful use. The Attempts / MaxAttempts pair implements brute-force protection.

FieldTypeNotes
UserIdGuid?FK → User. Set for user-facing OTP flows
AdminIdGuid?FK → Admin. Set for admin-facing OTP flows
Codestring (max 10)The numeric OTP value (e.g., '483921')
PurposeintEnum integer: identifies the OTP purpose (email verify, password reset, etc.)
ExpiresAtDateTimeOTP validity window end
AttemptsintNumber of failed validation attempts so far
MaxAttemptsint (default 5)Maximum allowed failed attempts before lock-out
IsUsedboolSet to true by Invalidate() after successful use
Emailstring (max 200)Email address the OTP was sent to

QrLoginSession Entity

QrLoginSession orchestrates cross-device login: a desktop browser creates a session (status = Pending) and displays a QR code containing the SessionToken. An authenticated mobile device scans the QR (Scanned), the user approves (Approved), the backend generates tokens, and the polling desktop browser consumes them (Consumed). Sessions are cleaned up by QrSessionCleanupJob after the 5-minute TTL.

FieldTypeNotes
SessionTokenstring (max 200)64-byte cryptographically random token embedded in the QR code (base64url)
StatusQrSessionStatusPending → Scanned → Approved → Consumed (or Rejected / Expired)
InitiatorIpstring? (max 50)IP address of the desktop browser that generated the QR
InitiatorUserAgentstring? (max 500)User-Agent of the initiating browser (shown to approver for verification)
ApprovedByAdminIdGuid?Admin who approved the session from their authenticated mobile device
ApprovedAtDateTime?Timestamp of mobile approval
AccessTokenstring? (max 2000)JWT access token generated for the desktop after approval; consumed once
RefreshTokenValuestring? (max 500)Refresh token value issued for the new desktop session
TokenExpiresAtDateTime?Expiry of the issued JWT access token
ExpiresAtDateTimeSession TTL — 5 minutes from creation
TenantIdGuid?Optional tenant scope for the login session
CreatedAtDateTimeSession creation timestamp (UTC)
Desktop: create session (Pending)
Display QR code
Mobile: scan QR (Scanned)
Mobile: user approves (Approved)
Rejected / Expired
Desktop: poll & receive tokens (Consumed)

WebAuthnChallenge Entity

WebAuthnChallenge is a short-lived (5-minute) server-side nonce generated at the start of each WebAuthn ceremony (registration or authentication). The challenge is sent to the browser, signed by the authenticator, and verified on return. IsUsed = true prevents replay attacks. Origin binding prevents cross-origin ceremony hijacking.

FieldTypeNotes
Challengestring (max 200)Cryptographically random bytes (base64url) sent to the authenticator
AdminIdGuid?Admin performing the ceremony. Null during discoverable (usernameless) authentication
CeremonyTypestring (max 20)"registration" or "authentication" — used to match the challenge at verification
ExpiresAtDateTime5-minute TTL from creation
IsUsedboolMarked true after ceremony completion to prevent replay
Originstring? (max 200)Origin (scheme + host) that initiated the ceremony; verified to prevent cross-origin attacks

AdminPasskey Entity

AdminPasskey stores a registered FIDO2/WebAuthn credential for an admin. Each admin can have multiple passkeys (Touch ID, YubiKey, Windows Hello, etc.). The SignatureCounter is incremented by the authenticator on each use — a counter that goes backwards indicates a cloned credential. IsDiscoverable = true enables true passwordless login (no username entry required).

FieldTypeNotes
AdminIdGuidFK → Admin who owns this passkey
CredentialIdstring (max 1024)Unique credential ID from the authenticator (base64url). Used to look up the credential during authentication
PublicKeystring (max 2048)COSE public key (base64url). Used to verify assertion signatures
SignatureCounteruintIncremented by the authenticator on each use. Backwards counter = cloned credential
Aaguidstring? (max 36)Authenticator model identifier (e.g., YubiKey 5, iCloud Keychain). All-zeros for privacy-preserving devices
DeviceNamestring (max 100)User-assigned friendly name (e.g., 'MacBook Touch ID', 'YubiKey 5C NFC')
AttestationTypestring (max 20)Attestation type used at registration: none / indirect / direct / enterprise
Transportsstring? (max 100)Comma-separated transport hints: usb, nfc, ble, internal
LastUsedAtDateTime?When this passkey was last successfully used
IsDiscoverablebooltrue = resident credential enabling passwordless login without a username

ExternalLogin Entity

ExternalLogin creates a polymorphic link between an external identity (any OAuth / OIDC / SAML provider) and an Admin or User. AdminId and UserId are mutually exclusive — an external login linked to an Admin cannot authenticate a User. IdentityProviderId is null for built-in social providers (Google, Facebook, Apple, Microsoft) and set for custom OIDC/SAML providers configured per-tenant.

FieldTypeNotes
AdminIdGuid?FK → Admin. Mutually exclusive with UserId
UserIdGuid?FK → User. Mutually exclusive with AdminId
ProviderNamestring (max 50)Provider identifier: 'google', 'facebook', 'apple', 'microsoft', 'azure-ad', or an IdentityProvider.Slug for custom OIDC
ProviderKeystring (max 500)The user's unique ID in the external system (the 'sub' claim in OIDC)
Emailstring? (max 200)Email from the external provider — used for initial account matching
DisplayNamestring? (max 200)Display name from the provider (for UI purposes)
IdentityProviderIdGuid?FK → IdentityProvider config. Null for built-in social providers (Google, Facebook, Apple, Microsoft)
LinkedAtDateTimeWhen this external identity was linked to the Admin/User
LastUsedAtDateTime?When this external identity was last used to authenticate