-
Notifications
You must be signed in to change notification settings - Fork 1
Authentication and Security
SecondLayer implements a multi-layered authentication system supporting web users, mobile app users, API clients, MCP protocol clients (ChatGPT, Claude Desktop), and national digital identity (Diia). The system uses a "Dual-Auth" strategy combining JWT/OAuth for interactive sessions and Bearer tokens for programmatic access.
Primary authentication for web users:
- OAuth 2.0 authorization code flow via Passport.js
- Handles both registration and login
- Returns JWT token for subsequent requests
- Supports multiple callback hosts (
legal.org.ua,platform.legal.org.ua,mcp.legal.org.ua,preview.legal.org.ua) - Mobile apps authenticate via
POST /auth/google/mobileusing idToken from Google Sign-In SDK - On registration, provisions user in Authentik (SSO) and Nextcloud
Standard credential-based authentication:
- Registration with email + password (
POST /auth/register) - Login with email + password (
POST /auth/login) - Email verification flow (token sent via email)
- Password reset flow (
POST /auth/forgot-password,POST /auth/reset-password) - Account lockout after repeated failed login attempts (15-minute cooldown)
- Passwords hashed with bcrypt
Ukrainian national digital identity integration:
- Users authenticate via the Diia mobile app
- Provides strong identity verification (equivalent to qualified electronic signature)
- Callback routes:
GET /auth/diia/callback(browser redirect) andPOST /auth/diia/callback(API webhook) - Company registered as TOV "Lex EyAy" in Diia system
- Supports mobile same-device flow (deep linking with session cookie)
- Diia signing (Дія.Підпис):
POST /auth/diia/signinitiates document signing sessions - Polling endpoint:
GET /auth/diia/status/:sessionId - Creates users with
nullgoogle_id (separate from OAuth users)
FIDO2/WebAuthn passwordless authentication:
- Uses
@simplewebauthn/serverlibrary - Supports both platform authenticators (fingerprint, Face ID) and cross-platform (security keys)
- Registration requires existing JWT session (user adds passkey to their account)
- Login via passkey is public (no existing session needed)
- Challenge stored in Redis cache with 5-minute TTL
- Credentials stored in database with friendly names, last-used timestamps
- Users can list and delete their registered credentials
- Multi-origin support for multi-domain setups
OpenID Connect integration with Authentik identity provider:
- Authorization Code Flow with PKCE (code_challenge + code_verifier)
- State stored in Redis for CSRF protection (10-minute TTL)
- Used for SSO across Nextcloud and other Authentik-protected services
- OIDC login route also supports direct password auth (
POST /auth/oidc/login) - On registration, provisions user in Authentik and Nextcloud
- Conditionally enabled when
OIDC_DISCOVERY_URLandOIDC_CLIENT_IDare set
- Used for authenticated web/mobile sessions after any login method
- HS256 algorithm for token signing
- Token includes user ID, email, role
- Token refresh via
POST /auth/refresh - Configured via
JWT_SECRETenvironment variable
- Bearer token for programmatic/API access
- Two tiers:
- Phase 2 billing API keys (stored in database, user-scoped, with rate limits and expiry)
-
Legacy environment keys stored in
SECONDARY_LAYER_KEYS(comma-separated)
- Used by MCP clients (Claude Desktop), external integrations, and SaaS developers
- Format:
Authorization: Bearer <key> - Timing-safe comparison for legacy key validation
OAuth 2.0 Authorization Code Flow for ChatGPT and other MCP clients:
- Authorization endpoint:
GET /oauth/authorize - Token endpoint:
POST /oauth/token - Tokens prefixed with
mcp_token_for identification - Used to authenticate SSE (Server-Sent Events) MCP connections
- Supports PKCE (code_challenge, code_challenge_method)
sequenceDiagram
participant Client
participant Middleware as Dual-Auth Middleware
participant Google as Google OAuth
participant Password as Password Auth
participant Diia as Diia Auth
participant WebAuthn as WebAuthn/Passkey
participant OIDC as Authentik OIDC
participant JWT as JWT Service
participant API as API Key Check
participant OAuth as OAuth 2.0 (MCP)
Client->>Middleware: Request with credentials
alt Has JWT token (contains dots)
Middleware->>JWT: Validate token (HS256)
JWT-->>Middleware: User context
else Has Bearer API key (no dots)
Middleware->>API: Check Phase 2 keys (DB), then legacy keys (env)
API-->>Middleware: API client context
else Has mcp_token_ prefix
Middleware->>OAuth: Verify OAuth access token
OAuth-->>Middleware: User ID + scope
else OAuth login
Middleware->>Google: Authorization code
Google-->>Middleware: User info + JWT
else Password login
Middleware->>Password: Email + password (bcrypt verify)
Password-->>Middleware: User context + JWT
else Diia login
Middleware->>Diia: Auth request (deeplink + polling)
Diia-->>Middleware: Signed identity + JWT
else WebAuthn login
Middleware->>WebAuthn: Challenge-response (FIDO2)
WebAuthn-->>Middleware: Credential verification + JWT
else OIDC login
Middleware->>OIDC: Authorization code (PKCE)
OIDC-->>Middleware: User info + JWT
end
Middleware->>Resource: Authenticated request
The system uses a two-dimensional role model:
| Role | Description |
|---|---|
user |
Standard individual platform user |
company |
Law firm / organization account |
administrator |
Platform administrator (full access) |
Role is stored in role column (migration 090). Constraint enforces valid values.
| User Type | Description |
|---|---|
individual |
Regular user (default) |
attorney |
Verified attorney with extended features |
company_admin |
Administrator of a company account |
Attorneys go through an onboarding flow with offer acceptance (POST /auth/accept-attorney-offer). Developers accept a separate developer offer (POST /auth/accept-developer-offer).
Access to paid features (chat, document upload) is controlled by the Access Gate. A user is allowed if ANY of:
- User is an administrator
- User has
is_beta_tester = true - User has positive balance (UAH or USD)
- User has at least one successful Monobank top-up
API key callers bypass the access gate (they authenticate as service accounts).
- Document vault supports client-side encryption
- Retroactive encryption panel for migrating existing documents
- TLS for all data in transit
- Encryption at rest for sensitive database fields
- Per-user and per-IP rate limits enforced at the middleware level
- Redis-backed with in-memory fallback when Redis is unavailable
- Separate rate limiters for auth endpoints, password reset, upload, and general API
- Localhost/internal traffic (Prometheus, health monitors) exempted
- Budget-aware limits tied to billing balance
- Required for protected endpoints (file upload)
- Enforcement via
requireEmailVerifiedmiddleware (placed after auth middleware) - API key (B2B) clients bypass email verification requirement
- Failed login attempts are tracked per email
- Account locked after threshold exceeded (15-minute cooldown)
- Prevents brute-force attacks on password authentication
- OAuth 2.0 access tokens (
mcp_token_prefix) provide scoped MCP protocol access - Separate from API keys -- issued via OAuth authorization code flow
- Used for ChatGPT and Claude Desktop MCP connections
- Tokens are verifiable and revocable via the OAuth service
- Developers building SaaS products get separate API keys (Phase 2 billing keys)
- Must accept Developer Offer or API Terms before key generation
- Developer contract tracked in database (migration 104)
- Per-key rate limits (per-minute and per-day) stored in database
- Key usage count and last-used timestamp tracked
- Keys can have expiration dates
- On registration (any method), users are automatically provisioned in:
- Authentik (SSO access to ecosystem services)
- Nextcloud (file storage and collaboration)
- Welcome bonus credited on first registration
| Variable | Purpose |
|---|---|
JWT_SECRET |
JWT signing secret (required, server refuses to start without it) |
SECONDARY_LAYER_KEYS |
Comma-separated legacy API keys |
GOOGLE_CLIENT_ID |
Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret |
DIIA_BASE_URL |
Diia API base URL |
DIIA_ACQUIRER_TOKEN |
Diia acquirer token |
DIIA_AUTH_ACQUIRER_TOKEN |
Diia auth acquirer token (enables Diia routes) |
DIIA_BRANCH_ID |
Diia branch identifier |
DIIA_OFFER_ID |
Diia offer identifier |
OIDC_DISCOVERY_URL |
Authentik OIDC discovery URL (enables OIDC routes) |
OIDC_CLIENT_ID |
OIDC client identifier |
OIDC_CLIENT_SECRET |
OIDC client secret |
OIDC_REDIRECT_URI |
OIDC redirect URI |
AUTHENTIK_API_URL |
Authentik API URL for user provisioning |
AUTHENTIK_API_TOKEN |
Authentik API token for provisioning |
WEBAUTHN_RP_ID |
WebAuthn Relying Party ID (domain) |
WEBAUTHN_RP_NAME |
WebAuthn Relying Party display name |
WEBAUTHN_ORIGIN |
WebAuthn allowed origins (comma-separated for multi-domain) |
ALLOWED_ORIGINS |
CORS allowed origins |
| Route | Method | Auth | Description |
|---|---|---|---|
/auth/login |
POST | Public | Email + password login |
/auth/register |
POST | Public | Email + password registration |
/auth/verify-email |
POST | Public | Email verification |
/auth/forgot-password |
POST | Public | Request password reset |
/auth/reset-password |
POST | Public | Reset password with token |
/auth/google |
GET | Public | Initiate Google OAuth |
/auth/google/callback |
GET | Public | Google OAuth callback |
/auth/google/mobile |
POST | Public | Mobile Google Sign-In |
/auth/oidc |
GET | Public | Initiate Authentik OIDC |
/auth/oidc/callback |
GET | Public | OIDC callback |
/auth/oidc/login |
POST | Public | OIDC direct password login |
/auth/diia |
GET | Public | Initiate Diia auth |
/auth/diia/callback |
POST/GET | Public | Diia webhook/redirect |
/auth/diia/status/:id |
GET | Public | Poll Diia session |
/auth/diia/sign |
POST | JWT | Initiate Diia signing |
/auth/diia/sign/status/:id |
GET | JWT | Poll signing session |
/auth/webauthn/register/options |
POST | JWT | Generate passkey registration challenge |
/auth/webauthn/register/verify |
POST | JWT | Verify passkey registration |
/auth/webauthn/auth/options |
POST | Public | Generate passkey login challenge |
/auth/webauthn/auth/verify |
POST | Public | Verify passkey login |
/auth/webauthn/credentials |
GET | JWT | List user credentials |
/auth/webauthn/credentials/:id |
DELETE | JWT | Remove a credential |
/auth/me |
GET | JWT | Current user profile |
/auth/refresh |
POST | JWT | Refresh token |
/auth/profile |
PUT | JWT | Update profile |
/auth/logout |
POST | JWT | Logout |
/auth/accept-attorney-offer |
POST | JWT | Accept attorney offer |
/auth/accept-developer-offer |
POST | JWT | Accept developer offer |
/auth/my-contracts |
GET | JWT | List signed contracts |
/oauth/authorize |
GET | Public | OAuth 2.0 authorization (MCP) |
/oauth/token |
POST | Public | OAuth 2.0 token exchange (MCP) |