Authentication

UniAuth provides multiple authentication methods, from traditional email/password to modern passkeys and social login. All methods are designed with security best practices including progressive lockout, breach detection, and post-quantum session signatures.

Email & Password Authentication

The primary authentication method uses email and password credentials. Passwords are hashed using Argon2id (via @node-rs/argon2) with the following parameters:

  • Memory cost: 64 MB
  • Time cost: 3 iterations
  • Parallelism: 4 lanes

Legacy bcrypt hashes are transparently migrated to Argon2id on successful login, so existing users are upgraded without any action required.

Registration

New users register via POST /api/auth/register with their email, password, and optional name fields. The password is validated against:

  • zxcvbn strength check — Must score at least 2/4. The check is context-aware: it penalizes passwords that contain the user's email or name.
  • HaveIBeenPwned breach check — Uses k-anonymity to safely check if the password has appeared in known breaches. This is a non-blocking warning (3-second timeout).

After registration, a verification email is sent. The user must click the verification link before certain features become available.

Login Flow

POST /api/auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "securePassword123!"
}

// Success response (200):
{
  "success": true,
  "user": {
    "id": "uuid",
    "email": "[email protected]",
    "firstName": "John",
    "lastName": "Doe"
  }
}

// If 2FA is enabled (200, requires second step):
{
  "success": true,
  "requires2FA": true,
  "userId": "uuid",
  "defaultMethod": "totp",
  "availableMethods": ["totp", "email"]
}

On successful login, an HTTP-only, secure, SameSite cookie (auth_token) is set with a 7-day expiration. The JWT payload contains the user's ID, email, name, avatar URL, and role. Sessions are also fingerprinted using a SHA-256 hash of the client's IP address and User-Agent string.

Threat Detection

Every login attempt is scored by the statistical threat detection system. Risk factors include:

  • Login from a new IP address not previously seen for this user
  • Login from a new User-Agent / device
  • Login at unusual hours (outside the user's historical pattern)
  • Recent failed login attempts
  • Burst detection (many rapid login attempts)

Per-user baselines are stored in the users.metadata.login_stats JSONB field and updated after each login.

Social Login (Google & GitHub)

UniAuth supports OAuth-based social login with Google and GitHub. Both providers use PKCE (S256 code challenge) for security. The flow works as follows:

  1. The user clicks "Sign in with Google" or "Sign in with GitHub" on the login page.
  2. The client calls POST /api/auth/oauth/initiate with the provider name.
  3. The server generates a CSRF state token and PKCE code_verifier, stores them in the user's metadata, and returns the authorization URL.
  4. The user is redirected to the provider's consent screen.
  5. After approval, the provider redirects back to /api/auth/oauth/callback with an authorization code.
  6. The server verifies the state, exchanges the code for tokens, fetches the user profile, and either logs in an existing user or creates a new account.
  7. The provider's access token is encrypted with AES-256-GCM before storage in the connected_services table.

Configuration

To enable social login, set the following environment variables:

# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret

# GitHub OAuth
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

The callback URL to configure in your Google/GitHub OAuth app settings is:

https://your-domain.com/api/auth/oauth/callback

WebAuthn / Passkeys

UniAuth supports WebAuthn passkeys for passwordless authentication. This uses the @simplewebauthn/server and @simplewebauthn/browser (v10) libraries with direct attestation.

Registering a Passkey

  1. The authenticated user navigates to their security settings.
  2. The client calls POST /api/auth/passkey/register-options to get registration options (including a challenge stored in the user's metadata).
  3. The browser's WebAuthn API prompts the user to create a credential (e.g., fingerprint, Face ID, hardware key).
  4. The client sends the attestation response to POST /api/auth/passkey/register-verify.
  5. The server verifies the attestation and stores the credential in the passkeys table.

Authenticating with a Passkey

  1. The client calls POST /api/auth/passkey/authenticate-options to get authentication options.
  2. The browser prompts the user to use their passkey.
  3. The client sends the assertion response to POST /api/auth/passkey/authenticate-verify.
  4. The server verifies the assertion, updates the credential counter, and creates a session.

Conditional UI (Passkey Autofill)

UniAuth supports WebAuthn Conditional UI, which allows passkeys to appear in the browser's autofill suggestions alongside saved passwords. When a user focuses the email field on the login page, the browser may display available passkeys. This provides a seamless login experience without the user having to click a separate "Sign in with Passkey" button.

The Relying Party (RP) ID is derived from the HOST environment variable. Make sure this is set correctly for passkeys to work across your domain.

Multi-Factor Authentication (2FA)

UniAuth supports multiple 2FA methods that can be enabled simultaneously. Users can choose their default method and switch between available methods during verification.

TOTP (Authenticator Apps)

Time-based One-Time Passwords work with any standard authenticator app (Google Authenticator, Authy, 1Password, etc.). Setup flow:

  1. User calls POST /api/auth/2fa/setup with { "method": "totp" }.
  2. Server generates a TOTP secret, encrypts it with AES-256-GCM, and returns a QR code URI.
  3. User scans the QR code with their authenticator app.
  4. User enters the 6-digit code to verify setup via POST /api/auth/2fa/verify.
  5. 2FA is now active. Future logins require the TOTP code after password verification.

Email OTP

A 6-digit code is sent to the user's verified email address. The code is encrypted with AES-256-GCM before storage in the otp_tokens table and expires after a short validity window. To send a code during login:

POST /api/auth/2fa/send-code
Content-Type: application/json

{
  "userId": "uuid",
  "method": "email"
}

SMS OTP (via Twilio)

Similar to email OTP, but the code is sent via SMS using the Twilio API. Requires Twilio credentials to be configured:

TWILIO_ACCOUNT_SID=your-twilio-sid
TWILIO_AUTH_TOKEN=your-twilio-auth-token
TWILIO_PHONE_NUMBER=+1234567890

The user's phone number must be verified before SMS 2FA can be enabled. Phone verification uses the same OTP mechanism.

Default Method Selection

When a user has multiple 2FA methods enabled, they can set a default via:

POST /api/auth/2fa/set-default
Content-Type: application/json

{
  "method": "totp"  // or "email" or "sms"
}

During login, the default method is used first, but the user can switch to any other enabled method.

2FA Verification During Login

POST /api/auth/2fa/verify
Content-Type: application/json

{
  "userId": "uuid",
  "code": "123456",
  "method": "totp"
}

Account Lockout

UniAuth implements progressive account lockout to protect against brute-force attacks. The lockout duration increases with the number of failed attempts:

Failed AttemptsLockout Duration
51 minute
105 minutes
1515 minutes
20+1 hour

When an account is locked, the API returns HTTP 423 with the remaining lockout time. The counter resets on successful login or passkey authentication. Lockout state is stored in the failed_login_attempts and locked_until columns on the users table.

Session Management

Sessions are managed with multiple layers of security:

Session Lifecycle

  • Inactivity timeout: Sessions expire after 24 hours of inactivity. The last_activity_at timestamp is updated on each request via touchSession().
  • Concurrent session limit: A maximum of 10 active sessions per user. When the limit is reached, the oldest session is automatically revoked.
  • Password-change revocation: When a user changes their password, all other sessions are immediately invalidated via invalidateOtherSessions().

Session Fingerprinting

Each session is fingerprinted using a SHA-256 hash of the client's IP address and User-Agent. This fingerprint is stored in the fingerprint_hash column. If a subsequent request's fingerprint does not match the stored value, the system flags a potential session hijack.

Post-Quantum Session Signatures

Sessions are signed using ML-DSA-44 (FIPS 204) post-quantum digital signatures. The server maintains a keypair (generated at startup and stored encrypted in system_settings). Each session token includes a PQC signature that can be verified to ensure the session was genuinely issued by this server instance.

Viewing and Revoking Sessions

Users can view their active sessions and revoke individual sessions or all sessions at once:

// List active sessions
GET /api/user/sessions

// Revoke all sessions except current
POST /api/user/sessions/revoke-all

Data Retention

UniAuth automatically cleans up expired data via a scheduled task that runs every 24 hours (initialized in instrumentation.ts):

  • Expired sessions: purged after 30 days
  • Expired tokens (password reset, email verification): purged after 7 days
  • Expired OTP codes: purged after 24 hours
  • Old activity logs: purged after 1 year