Appearance
User Sessions & Provider Linking
Overview
ODP uses session-based refresh tokens. After login, a session record is created in odp_sessions and the session token is used as the refresh token. Session data is cached in Redis/memory for fast access token validation.
Each session can optionally be delivered via an HttpOnly cookie (SESSION_COOKIE_ENABLED).
Session Management Endpoints
GET /users/me/sessions
List all active sessions for the current authenticated user.
Auth required: Yes
Response:
json
{
"data": [
{
"id": "3f4a5b6c7d8e",
"token": "full-session-token",
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"expires": "2024-01-08T00:00:00.000Z",
"current": true
},
{
"id": "9a8b7c6d5e4f",
"token": "another-session-token",
"ip": "10.0.0.1",
"user_agent": "curl/7.79.1",
"expires": "2024-01-07T12:00:00.000Z",
"current": false
}
]
}Session ID: The id field is the first 16 hex characters of SHA-256(session_token) — a non-reversible short identifier for display purposes.
Current Session: Determined by comparing the session token stored in the current JWT's sid claim.
DELETE /users/me/sessions/:sid
Revoke a specific session.
Auth required: Yes
URL Parameters:
sid— Session identifier (either the 16-char hashed ID or the raw token)
Response: 204 No Content
Restrictions:
- Cannot revoke the current session (use
POST /auth/logoutinstead) - Ownership verified — only the session owner can revoke their sessions
Business Logic:
- Finds session by hashed
idor rawtoken - Validates it belongs to the authenticated user
- Checks it is not the current session
- Deletes from session cache (Redis/memory)
- Deletes from
odp_sessionsDB table
DELETE /users/me/sessions
Revoke all sessions except the current one.
Auth required: Yes
Response: 204 No Content
Business Logic:
- Determines current session
sidfrom JWT - Deletes all session data from cache for the user
- Deletes all sessions from DB EXCEPT the current one
- Re-caches the current session so it remains valid
Static Token Management
A static API token is a simple hex string stored directly on the user record. Unlike sub-tokens, it has no scopes, no expiry, and no audit trail. It is intended for simple integrations.
POST /users/me/token
Generate a new static API token (replaces any existing one).
Auth required: Yes
Response:
json
{
"data": {
"token": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
}
}Note: Generates a new crypto.randomBytes(32).toString('hex') and saves it to odp_users.token. Any previous static token is invalidated.
DELETE /users/me/token
Revoke the static API token.
Auth required: Yes
Response: 204 No Content
Sets odp_users.token = null.
Usage: Authenticate with static token via Authorization: Bearer <token>.
Provider Linking
Provider linking connects an external SSO identity to an existing ODP user account, allowing login via that provider without creating a separate account.
GET /users/me/providers
List linked SSO providers for the current user.
Auth required: Yes
Response:
json
{
"data": [
{
"id": "link-uuid",
"user_id": "user-uuid",
"provider": "google",
"external_identifier": "116634877...",
"provider_email": "user@gmail.com",
"created_at": "2024-01-01T00:00:00.000Z"
}
]
}
auth_datais stripped from the response for security.
POST /users/me/providers/link
Link an OAuth2/OpenID SSO provider to the current user.
Auth required: Yes
Request Body:
json
{
"provider": "github",
"code": "authorization-code",
"redirect_uri": "https://app.example.com/settings/providers",
"code_verifier": "optional-pkce"
}Response: 204 No Content
Business Logic:
- Exchanges authorization code with the provider
- Resolves
SSOUserInfo(identifier, email, etc.) - Checks
allowUnverifiedEmail— rejects if false and email is unverified - Checks email collision — SSO identity email must not belong to a different user
- Creates
odp_user_providersrecord linking the authenticated user to the provider identity
DELETE /users/me/providers/:id
Unlink a specific provider.
Auth required: Yes
URL Parameters:
id— Provider link UUID (fromodp_user_providers)
Response: 204 No Content
Note: Validates ownership before deletion.
Two-Factor Authentication (TFA)
POST /users/me/tfa/enable
Generate TFA secret and QR code for the current user.
Auth required: Yes
Request Body:
json
{
"password": "current-password"
}
passwordis optional for SSO-only users who have no password set.
Response:
json
{
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"otpauth_url": "otpauth://totp/ODP:user@example.com?secret=..."
}
}POST /users/me/tfa/disable
Disable TFA for the current user.
Auth required: Yes
Request Body:
json
{
"otp": "123456"
}Response: 204 No Content
POST /users/:id/tfa/disable (Admin)
Force-disable TFA for any user without requiring OTP.
Auth required: Admin
Response: 204 No Content
Set Initial Password
POST /users/me/set-password
Set a password for users who signed up via SSO and have no password.
Auth required: Yes
Request Body:
json
{
"password": "new-secure-password"
}Response: 204 No Content
Note: Only valid if user does not already have a password (SSO-only accounts).
Session Data Model
odp_sessions
| Column | Type | Description |
|---|---|---|
token | string(64) PK | Session token (used as refresh token) |
user | UUID FK → odp_users | Session owner (CASCADE delete) |
expires | timestamp | Session expiry (REFRESH_TOKEN_TTL) |
ip | string(255) | Client IP |
user_agent | text | Browser/client user-agent |
share | UUID FK → odp_shares | For share-based sessions |
origin | string(255) | Request origin |
next_token | string(64) | Next token (for rolling sessions) |
Session Cache
Session data is cached in Redis (or in-memory) with the session token as key. Cache entry TTL matches REFRESH_TOKEN_TTL.
Cached session data structure:
json
{
"user": "user-uuid",
"role": "role-uuid",
"roles": ["role-uuid"],
"admin": false,
"tech": false,
"app": true
}Configuration
| Variable | Default | Description |
|---|---|---|
SESSION_COOKIE_ENABLED | true | Enable HttpOnly session cookie |
SESSION_COOKIE_NAME | odp_session_token | Cookie name |
ACCESS_TOKEN_TTL | 15m | Access token lifetime |
REFRESH_TOKEN_TTL | 7d | Session/refresh token lifetime |
CACHE_ENABLED | false | Enable Redis/memory cache for sessions |
REDIS_ENABLED | false | Use Redis for session cache |