Authentication: JWT Sessions
JWT-based authentication for web clients and browser-based applications.
Overview
Sibyl uses JWT (JSON Web Tokens) for session authentication:
- Access Tokens: Short-lived (default 60 minutes), used for API authentication
- Refresh Tokens: Long-lived (default 30 days), used to obtain new access tokens
- OAuth Support: GitHub OAuth integration for social login
Token Types
Access Token
Short-lived token for API authentication.
Claims Schema:
{
"sub": "user_uuid", // User ID
"org": "org_uuid", // Organization ID (optional)
"typ": "access", // Token type
"iat": 1704067200, // Issued at (Unix timestamp)
"exp": 1704070800 // Expires at (Unix timestamp)
}Default Expiry: 60 minutes (configurable via SIBYL_ACCESS_TOKEN_EXPIRE_MINUTES)
Refresh Token
Long-lived token for obtaining new access tokens.
Claims Schema:
{
"sub": "user_uuid", // User ID
"org": "org_uuid", // Organization ID (optional)
"sid": "session_uuid", // Session ID (for token rotation)
"typ": "refresh", // Token type
"jti": "unique_token_id", // Unique ID for revocation
"iat": 1704067200, // Issued at
"exp": 1706659200 // Expires at
}Default Expiry: 30 days (configurable via SIBYL_REFRESH_TOKEN_EXPIRE_DAYS)
Configuration
Required
SIBYL_JWT_SECRET=your-secure-secret-key-at-least-32-charsOptional
SIBYL_JWT_ALGORITHM=HS256 # Default: HS256
SIBYL_ACCESS_TOKEN_EXPIRE_MINUTES=60 # Default: 60
SIBYL_REFRESH_TOKEN_EXPIRE_DAYS=30 # Default: 30Authentication Methods
Cookie-Based (Recommended for Web)
Access token is stored in an HTTP-only cookie:
Cookie: sibyl_access_token=eyJhbGciOiJIUzI1NiIs...Advantages:
- Automatic CSRF protection (SameSite=Lax)
- No client-side token storage
- Works with browser redirect flows
Header-Based
Access token passed via Authorization header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...Use Cases:
- API clients
- CLI tools
- Mobile apps
Auth Endpoints
Local Signup
POST /api/auth/local/signupRequest:
{
"email": "user@example.com",
"password": "secure-password",
"name": "User Name"
}Response:
{
"user": {
"id": "user_uuid",
"email": "user@example.com",
"name": "User Name"
},
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer"
}Local Login
POST /api/auth/local/loginRequest:
{
"email": "user@example.com",
"password": "secure-password"
}Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600
}Also sets sibyl_access_token cookie for web clients.
GitHub OAuth
Start OAuth Flow
GET /api/auth/github/authorizeRedirects to GitHub OAuth consent screen.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
redirect_uri | string | Post-login redirect URL |
OAuth Callback
GET /api/auth/github/callbackHandles GitHub OAuth callback, creates/links user account.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
code | string | OAuth authorization code |
state | string | CSRF state token |
Response: Redirects to SIBYL_FRONTEND_URL with tokens set.
Logout
POST /api/auth/logoutClears session and invalidates tokens.
Response: 204 No Content
Also clears sibyl_access_token cookie.
Current User
GET /api/auth/meReturns current authenticated user.
Response:
{
"id": "user_uuid",
"email": "user@example.com",
"name": "User Name",
"organization_id": "org_uuid",
"role": "member"
}Token Refresh
POST /api/auth/refreshExchange refresh token for new access token.
Request:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600
}Token Validation
Validation Flow
- Extract token from cookie or Authorization header
- Verify signature using
SIBYL_JWT_SECRET - Check expiration (
expclaim) - Validate token type (
typclaim) - Load user from
subclaim - Load organization from
orgclaim
Validation Errors
| Error | HTTP Status | Cause |
|---|---|---|
Not authenticated | 401 | Missing token |
Invalid token | 401 | Signature verification failed |
Token expired | 401 | Token past expiration |
User not found | 401 | User ID not in database |
No organization context | 403 | Token missing org claim |
Organization Context
JWT tokens include organization context:
{
"sub": "user_uuid",
"org": "org_uuid"
}All API operations are scoped to this organization:
- Graph queries use org-specific FalkorDB graph
- Document queries filter by org ownership
- Resource access is validated against org membership
Switching Organizations
To switch organizations, obtain a new token with different org context:
POST /api/auth/switch-orgRequest:
{
"organization_id": "new_org_uuid"
}Security Considerations
Token Storage
Web Applications:
- Store in HTTP-only cookies (Sibyl sets this automatically)
- Never store in localStorage (XSS vulnerable)
Native Applications:
- Use secure storage (Keychain, Keystore)
- Encrypt tokens at rest
Token Rotation
Refresh tokens support rotation:
- Use refresh token to get new access token
- Server may issue new refresh token
- Old refresh token is invalidated
Revocation
Tokens can be revoked by:
- Logout (clears session)
- Password change (invalidates all tokens)
- Admin action
MCP Authentication
For MCP endpoints, authentication follows the same pattern:
curl -X POST /mcp \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"method": "tools/call", ...}'MCP auth mode is configurable:
SIBYL_MCP_AUTH_MODE=auto # auto, on, or offauto: Enforce auth whenSIBYL_JWT_SECRETis seton: Always require authoff: Disable auth (development only)
Error Responses
{
"detail": "Not authenticated"
}| Status | Error | Resolution |
|---|---|---|
| 401 | Not authenticated | Provide valid token |
| 401 | Invalid token | Token may be corrupted or tampered |
| 401 | Token expired | Refresh token or re-login |
| 401 | User not found | Account may be deleted |
| 403 | No organization context | Token missing org claim |
| 403 | Forbidden | Insufficient role permissions |
Related
- auth-api-keys.md - API key authentication
- index.md - API overview
