Skip to content

Documentation

Authentication & Identity API Reference

Programmatic API reference for the SveltyCMS identity layer — login, profiles, 2FA, SAML SSO, OIDC login/callback/logout, RBAC permissions, API Keys, Magic Links, and WebAuthn/Passkeys.

8/4/2026
16 min read Edit on GitHub

The Authentication API handles the complete lifecycle of a user identity within SveltyCMS for programmatic access (SDK, mobile apps, external services). It manages everything from standard password-based authentication and Two-Factor Authentication (2FA) to enterprise Single Sign-On (SSO) via SAML 2.0.

Important: The browser-based login flow (/login) now uses type-safe Remote Functions defined in auth.server.ts. These provide full TypeScript inference between components and server logic. The REST API endpoints below remain available for programmatic access (SDK, mobile apps, external services). See Login Security for the page-level authentication documentation.


⚡ Quick Reference

Feature HTTP Endpoint Method Permission Required
Login /api/auth/login POST Public
Logout /api/auth/logout POST Authenticated
OIDC Login /api/auth/oidc-login GET Public
OIDC Callback /api/auth/oidc-callback GET Public
OIDC Logout /api/auth/oidc-logout GET, POST Public
SSO Providers /api/auth/sso-providers GET, POST Public (exempt)
Current User /api/auth GET Authenticated
List Users /api/user GET user:read
Create User /api/auth/create-user POST user:write
API Keys /api/api-keys GET system:read
Create Key /api/api-keys POST system:settings
Revoke Key /api/api-keys/{id} DELETE Owner or Admin
Magic Link auth.remote.ts (remote fn) POST Public
Passkey Auth auth.remote.ts (remote fn) POST Public
Passkey Reg auth.remote.ts (remote fn) POST Authenticated
2FA Status /api/auth/2fa/status GET Authenticated
2FA Setup /api/auth/2fa/setup POST Authenticated
2FA Enable /api/auth/2fa/enable POST Authenticated
2FA Verify /api/auth/2fa/verify POST Authenticated
2FA Disable /api/auth/2fa/disable POST Authenticated
Backup Codes /api/auth/2fa/backup-codes GET, POST Authenticated
SAML Login /api/auth/saml/login GET Public
SAML ACS /api/auth/saml/acs POST Public
SAML Config /api/auth/saml/config POST Admin (handler-enforced)
Permissions /api/permission/list GET system:admin

Trusted Devices: The browser-based flow (auth.remote.ts verify2FA) supports trustDevice: true to set a __Host-2fa-trusted-device cookie (30-day TTL, HMAC-SHA256 signed). On subsequent logins from the same device, 2FA is silently skipped.

TOTP Secret Encryption: TOTP secrets are encrypted at rest using AES-256-GCM under the instance’s ENCRYPTION_KEY. Legacy plaintext secrets are handled transparently — no migration needed.

Two permission vocabularies — do not mix them. Namespaced API routes are authorized fail-closed against ENDPOINT_PERMISSIONS in src/routes/api/[...path]/+server.ts (unmapped namespace ⇒ 403). That map speaks system:read / system:settings / system:admin / collection:read / media:* / user:read|write, e.g. api-keys ⇒ system:read (read) or system:settings (write), permission ⇒ system:admin, media ⇒ media:read / media:write / media:delete, gdpr ⇒ user:read / user:write. The admin UI tiles use a different vocabulary (manage:system, manage:user, manage:theme, …) declared in CONFIG_TILE_PERMISSIONS (src/routes/(app)/config/+page.server.ts) — those are UI-tile gates, not the API contract.

user and auth namespaces have their own branch (+server.ts:325-369): me, login, logout, oidc-*, sso-providers, frontchannel-logout, backchannel-logout, saml and 2fa are exempted by action name (their handlers still apply their own checks), a bare GET /api/auth is self-service, update-roles is denied outright for non-admins (the handler fail-closes too), and every other user-management endpoint requires user:read (GET) or user:write (mutations).


1. Core Authentication

User Lifecycle

Standard authentication uses secure HttpOnly cookies to maintain session state across requests. Session cookies use the __Host- prefix on HTTPS connections per RFC 6265bis.

  • Login: POST /api/auth/login — Validates credentials (email + password), starts a session, sets __Host-auth_sessions cookie, and rotates the CSRF token.
  • Logout: POST /api/auth/logout — Invalidates the current session, clears cookies, and invalidates the session cache.
  • Current User: GET /api/auth — Returns the authenticated user object and their assigned permissions.

Profile Management

Users can update their own security attributes and metadata via dedicated endpoints:

  • Update Attributes: POST /api/auth/update-user-attributes (also accepts PUT and PATCH) — Updates profile fields. Pass user_id: "self" for the current user. Non-admins may only update themselves; privilege fields (role, isAdmin, permissions, lockout/2FA secrets, etc.) are stripped for non-admin callers.
  • Save Avatar: POST /api/auth/save-avatar — Supports both Multipart form-data (file upload) and JSON payloads (URL string).

2. Secure Identity Layer (2FA)

SveltyCMS supports mandatory or optional Two-Factor Authentication via Time-based One-Time Passwords (TOTP).

Security Architecture

  • AES-256-GCM Encryption: TOTP secrets are encrypted at rest using the instance’s ENCRYPTION_KEY. Secrets are never stored in plaintext in the database. Legacy plaintext secrets are handled transparently.
  • TOTP Replay Protection: A consumed-codes registry prevents replay attacks within the 90-second validity window (30s TOTP — 30s clock skew). Fail-closed design — registry errors reject the code.
  • Timing-Safe Comparisons: All code comparisons use crypto.timingSafeEqual to neutralize side-channel attacks.
  • Configurable Window: Set TOTP_WINDOW=2 (env var, default 1) for — 60s tolerance on clock-drifted devices. Max: 5.

2FA Setup Flow (with Pending State)

  1. Initiate Setup: POST /api/auth/2fa/setup — Generates a TOTP secret and QR code. The encrypted secret is persisted immediately with twoFactorPending: true. If setup is interrupted (page close, browser crash), re-initiating resumes the existing pending setup.
  2. Verify & Enable: POST /api/auth/2fa/enable — Verifies the TOTP code with the pending secret and enables 2FA. Payload: { "code": "123456", "secret": "...", "backupCodes": [...] }. The secret field is optional — if omitted, the stored pending secret is used. On success: twoFactorPending cleared, is2FAEnabled: true.

2FA Verification Flow

When 2FA is enabled, a standard login will return a 2FA_REQUIRED status, requiring a second step. Plugins can also force 2FA via the afterAuthenticate hook — see Plugin Architecture.

Endpoint: POST /api/auth/2fa/verify
Payload: { "userId": "...", "code": "123456", "pending2faToken": "<signed token>", "trustDevice": false }

The pending2faToken is a short-lived signed token issued only after a successful password login (the requires2FA login response). It is bound to the claimed userId — a TOTP code alone cannot mint a session, and requests without a valid token are rejected fail-closed with 401.

Trusted Devices (“Remember This Device”)

After successful 2FA verification, the browser flow can pass trustDevice: true:

  • Sets a __Host-2fa-trusted-device cookie (httpOnly, secure, strict, 30-day TTL)
  • Cookie is HMAC-SHA256 signed under the instance’s ENCRYPTION_KEY — forgery-proof
  • Binds to device fingerprint (IP prefix + user-agent hash)
  • On subsequent logins from the same device, 2FA is silently skipped
  • Supports up to 5 trusted devices per user (FIFO eviction)
  • Disabling 2FA clears all trusted devices

Backup Codes

  • Retrieve: GET /api/auth/2fa/backup-codes — Returns backup codes if 2FA is enabled.
  • Regenerate: POST /api/auth/2fa/regenerate-backup-codes — Generates new backup codes.

Disable 2FA

Endpoint: POST /api/auth/2fa/disable
Payload: { "password": "current_password" } — Requires current password verification before disabling.

sequenceDiagram participant User participant Auth as Auth Handler (auth.ts) participant Plugin as Auth Hooks (plugins) participant SDK as Auth Service participant DB as Database User->>Auth: POST /api/auth/login Auth->>Auth: Validate Password (Argon2id) Auth->>Plugin: afterAuthenticate(event) Plugin-->>Auth: deny | requires2FA | pass alt 2FA enabled or plugin-required Auth-->>User: 200 OK (status: 2FA_REQUIRED) User->>Auth: POST /api/auth/2fa/verify (OTP + trustDevice) Auth->>SDK: verifyTOTP(userId, code, fingerprint?) SDK->>DB: Decrypt Secret (AES-256-GCM) SDK-->>Auth: success + trustedDeviceToken? Auth->>Auth: Set __Host-2fa-trusted-device cookie Auth-->>User: 200 OK (Session Started) else trusted device valid Auth->>Auth: Verify __Host-2fa-trusted-device cookie Auth-->>User: 200 OK (2FA skipped) end

3. Enterprise SSO (SAML 2.0)

For enterprise environments, SveltyCMS acts as a Service Provider (SP) and integrates with Identity Providers (IdP) like Okta or Azure AD via the lightweight @node-saml/node-saml library — zero database dependencies.

SAML Configuration

To connect an external IdP, the admin must provide the XML metadata.

Endpoint: POST /api/auth/saml/config Payload: { "tenant": "...", "rawMetadata": "<XML_CONTENT>" }

SAML Assertion Consumer Service

The IdP redirects the user to the ACS endpoint after authentication.

Endpoint: POST /api/auth/saml/acs — Processes the SAML response, validates the assertion, and starts a session.

Just-In-Time (JIT) Provisioning

The system automatically creates user records upon the first successful SAML login if they don’t already exist, mapping IdP attributes to SveltyCMS roles.


4. OIDC SSO (OpenID Connect)

SveltyCMS acts as an OpenID Connect relying party for enterprise SSO: authorization-code login with OIDC discovery + JWKS verification, local session creation, and RP-Initiated Logout.

Implementation: src/databases/auth/sso-session.ts · handlers in handlers/auth.ts (oidc-login, oidc-callback, oidc-logout).

Authorization-code login

Start — GET /api/auth/oidc-login

Query Required Description
provider Yes Provider id from SSO_PROVIDERS (e.g. azure-ad, auth0)
redirect_uri No Defaults to {origin}/api/auth/oidc-callback

Flow:

  1. Load providers from settings; 404 if provider unknown
  2. Generate RFC 7636 PKCE code_verifier (32-byte CSPRNG) and code_challenge (BASE64URL(SHA256(verifier))), along with CSRF state + nonce; store in short-lived HttpOnly cookie oidc_login_state (10 min, SameSite=Lax)
  3. OIDC discovery (/.well-known/openid-configuration) populates authorize/token/jwks/end_session when not set manually
  4. 302 redirect to OP authorization_endpoint with code_challenge, code_challenge_method=S256, state, nonce, and scopes (openid profile email by default)

Callback — GET /api/auth/oidc-callback

Query Required Description
code Yes Authorization code from OP
state Yes Must match cookie state
error No OP error → 400

Flow:

  1. Validate state against cookie; retrieve code_verifier; clear cookie
  2. Exchange code at token endpoint sending code_verifier (RFC 7636 PKCE verification)
  3. Verify id_token signature via JWKS (RS256/ES256; 1h JWKS cache). Without jwks_uri, only structural claim checks
  4. Resolve email from email or preferred_username claims
  5. Just-In-Time (JIT) Auto-Provisioning & Role Sync:
    • If user does not exist locally and JIT is enabled (jitProvisioning !== false), automatically create the user account.
    • Resolve target role using configured Claim-to-Role Mapping rules (e.g. evaluating the groups or roles claim against defined patterns), falling back to defaultRole (or "user").
    • If user already exists and syncRolesOnLogin is enabled, synchronize their local role with their updated directory groups.
  6. auth.createSession({ user_id, tenantId, expires }) + session cookie (getSessionCookieName)
  7. Attach SSO metadata (id_token_hint, provider) for RP logout

SSO Providers Management

Endpoint: GET /api/auth/sso-providers, POST /api/auth/sso-providers

Access Control:

  • GET: Returns public provider metadata (PublicSsoProvider[]) for unauthenticated or non-admin callers (safe for login buttons). Returns full configs with client secrets masked (••••••••) for authenticated admins.
  • POST: Restricted to Administrators (isAdmin === true). Validates provider configurations, preserves existing secrets when placeholder •••••••• is passed, and persists to encrypted system preferences.

Provider Configuration Schema (SsoProviderConfig):

Field Type Description
id string Unique provider key (e.g. google, azure-ad, okta, auth0)
name string Display label for login buttons (e.g. “Google Workspace”)
icon string Iconify identifier (e.g. flat-color-icons:google, logos:okta)
issuer string OIDC issuer URL (used for discovery)
clientId string Client ID registered with the identity provider
clientSecret string Client Secret (stored securely, masked as •••••••• in API reads)
allowedRedirectUris string[] Allowed redirect URIs for login and RP-initiated logout
authorizationEndpoint string? Optional override for authorize endpoint
tokenEndpoint string? Optional override for token exchange endpoint
jwksUri string? Optional override for JWKS certificate endpoint
endSessionEndpoint string? Optional override for federated logout endpoint
scopes string[] OAuth scopes requested (default ["openid", "profile", "email"])
jitProvisioning boolean Enable Just-In-Time auto-provisioning for new users (default true)
defaultRole string Default role assigned to new JIT accounts (default "user")
roleMapping object { claimField: string, rules: { claimValue: string, role: string }[] }
syncRolesOnLogin boolean Re-evaluate and synchronize roles on subsequent logins

RP-Initiated Logout

Endpoint: GET|POST /api/auth/oidc-logout

Parameters: | Param | Required | Description | | :---- | :------- | :---------- | | id_token_hint | Recommended | The ID Token from the original authentication, used by the OP to identify the session | | post_logout_redirect_uri | Optional | Where to redirect the browser after logout. Must match the provider’s allowlist | | state | Optional | Opaque value for CSRF protection, echoed back by the OP |

Flow:

  1. Browser sends logout request with OIDC params
  2. SveltyCMS terminates the local session and clears cookies
  3. If the OP has an end_session_endpoint (config or discovery), the browser is redirected there for federated logout
  4. On success, the browser lands at post_logout_redirect_uri (if provided and valid)

Security:

  • RFC 7636 PKCE with S256 SHA-256 method on all authorization code exchanges
  • Login CSRF mitigated via short-lived HttpOnly state cookie; nonce bound into authorize request
  • post_logout_redirect_uri validated against provider allowlist
  • Client secrets never returned in plaintext to the browser; JWKS fetch timeouts (5s)
  • Multiple OIDC identity providers supported concurrently alongside SAML 2.0 and WebAuthn/Passkeys

5. RBAC & Permissions

The authorization system is built on granular permissions assigned to user roles.

  • Check Permissions: GET /api/permission/list returns the full registry of available permissions in the system.
  • Role Assignment: Managed via POST /api/auth/update-roles (admin only — the dispatcher denies non-admins outright and the handler fail-closes, because coarse user:write must not allow rewriting the role matrix) or PATCH /api/user/{id} (requires user:write, with privilege fields stripped for non-admins).

6. API Keys (Machine-to-Machine)

API Keys enable programmatic access for CI/CD pipelines, external services, and headless clients. Keys use the sck_* prefix and are stored as HMAC-SHA-256 digests keyed with the instance secret (hashApiKey(), src/databases/auth/api-keys.ts) — never a plain unkeyed digest (AGENTS.md §2.9 forbids bare createHash("sha256") for secrets), and plaintext is never persisted.

Key Management

Operation Endpoint Method Description
List Keys /api/api-keys GET Returns all keys for authenticated user (no hash)
Create Key /api/api-keys POST Returns plaintext sck_* key once
Revoke Key /api/api-keys/{id} DELETE Owner or admin only; purges credential cache

Security: Plaintext keys are shown exactly once at creation. After that, only the keyed HMAC digest is stored. Revoked keys stop authenticating within 60 seconds (CREDENTIAL_AUTH_CACHE_TTL_S = 60, src/databases/auth/credential-auth-cache.ts).

Measured (2026-09-22, SQLite): the API-key surface is a full credential path, not a shortcut — api-keys.ts (keyed digest) plus api-permissions.ts (per-key scopes) and credential-auth-cache.ts (60 s auth cache + Bloom negative filter). In tests/benchmarks/auth-performance.test.ts an unknown Bearer sck_… key rejects in 0.35–0.40 ms, statistically identical to an unknown session cookie (0.36–0.47 ms) and to no credentials at all (0.36–0.38 ms) — and on par with the authenticated rows, after the 401/403 envelopes were given a content-length (they used to pay a zstd stream per reject for a 64-byte body; see Server Hooks). Switching a client from session cookies to API keys therefore does not change per-request latency on this path.

Create Key Request

POST /api/api-keys
{
  "name": "Staging CI",
  "scopes": ["content:read", "media:read"],
  "expiresAt": "2026-12-31T23:59:59Z"
}

Create Key Response

{
  "success": true,
  "data": {
    "_id": "key_abc123",
    "name": "Staging CI",
    "prefix": "sck_a1b2c3d4",
    "key": "sck_a1b2c3d4e5f6...",
    "scopes": ["content:read", "media:read"]
  }
}

7. Magic Links (Passwordless)

Magic Links provide passwordless authentication via email. Implemented as SvelteKit Remote Functions in auth.remote.ts for type-safe client-server communication.

Flow

  1. Request: Client calls requestMagicLink({ email }) remote function
  2. Token Creation: Server creates a single-use magic_link token (15-min TTL) and emails it
  3. Verification: +page.server.ts load function detects magic_token query param, calls verifyMagicLink()
  4. Session: On success, a session is created and the user is redirected

Security Properties

  • Tokens are consumed atomically (TOCTOU-safe consumeToken)
  • Uniform response prevents email enumeration
  • Account lockout is checked before sending
  • Every request and verification is audit-logged

8. WebAuthn / Passkeys

WebAuthn provides phishing-resistant biometric authentication via platform authenticators. Implemented as SvelteKit Remote Functions.

Remote Functions

Function Auth Required Description
getPasskeyAuthOptions No Returns challenge for login
verifyPasskeyAuth No Validates assertion, creates session
getPasskeyRegisterOptions Yes Returns challenge for registration
verifyPasskeyRegister Yes Validates attestation, stores credential

Database

Authenticators are stored as a JSON array on the User record:

interface Authenticator {
  credentialId: string;
  publicKey: JsonWebKey; // COSE to JWK converted
  counter: number; // Clone detection
  transports?: string[]; // ["internal", "usb", "nfc", "ble"]
}

9. Guest / Anonymous Auth

Public routes automatically receive an ephemeral ANONYMOUS_USER identity with the guest role. No session is created — access is stateless and read-only.

Permission Granted
content:read ✅
content:write ❌
media:read ✅
media:write ❌
admin:access ❌

Guest identity is assigned by handle-authentication.ts middleware when no valid session or bearer token is present. On subsequent authentication, the guest context is transparently upgraded to the real user session.


10. GDPR Compliance API (/api/gdpr)

The GDPR compliance endpoint implements regulatory data portability (Article 20) and Right to Erasure / Right to be Forgotten (Article 17). It is available as an authenticated endpoint for self-service or for administrators.

POST /api/gdpr

Action Payload Description
export { "action": "export", "userId": "<id>" } Article 20 Data Portability: Exports user profile metadata and audit trail history.
anonymize { "action": "anonymize", "userId": "<id>", "reason": "..." } Article 17 Soft Erasure: Replaces PII with pseudonymized ghost identities while maintaining schema foreign key integrity.
erase / wipe { "action": "erase", "userId": "<id>", "reason": "..." } Article 17 Deep Wipe: Permanently erases user identity and cascades across active sessions, tokens, and audit logs with 0 orphans.

Related Documents

apiauth2fasamlsecurityapi-keysmagic-linkswebauthnoidcopenid-connecttrusted-devicestotp-encryption
Was this page helpful?