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
- OIDC / Enterprise SSO: Corporate OpenID Connect providers with exact per-organization binding
Token Types
Access Token
Short-lived token for API authentication.
Claims Schema:
{
"sub": "user_uuid", // User ID
"org": "org_uuid", // Organization ID (optional)
"sid": "session_uuid", // Session ID (optional)
"typ": "access", // Token type
"iat": 1704067200, // Issued at (Unix timestamp)
"exp": 1704070800 // Expires at (Unix timestamp)
}Access tokens may also carry an org_role claim and a scopes claim when the issuer includes them. The MCP server uses org_role to gate owner-only tools.
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"
},
"organization": {
"id": "org_uuid",
"name": "My Org",
"slug": "my-org"
},
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 3600
}Local Login
POST /api/auth/local/loginRequest:
{
"email": "user@example.com",
"password": "secure-password"
}Response:
{
"user": {
"id": "user_uuid",
"email": "user@example.com",
"name": "User Name"
},
"organization": {
"id": "org_uuid",
"name": "My Org",
"slug": "my-org"
},
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 3600
}Also sets sibyl_access_token cookie for web clients.
GitHub OAuth
Start OAuth Flow
GET /api/auth/githubRedirects 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.
OIDC (Enterprise SSO)
Corporate OpenID Connect providers are configured through the SIBYL_OIDC setting. Each provider is bound to exactly one organization (organization_slug), and OIDC login never chooses an organization from the user's other memberships. Users are provisioned just-in-time on first login, and the IdP role claim is authoritative for the bound organization - an OIDC login carrying a lower role can demote the organization's last owner.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/auth/oidc/{provider}/login | Start the OIDC authorization flow |
| GET | /api/auth/oidc/{provider}/callback | Complete login, set the session cookie, redirect |
| GET | /api/auth/oidc/{provider}/refresh | Silent session refresh (404 unless enabled) |
The refresh endpoint returns 404 unless silent_refresh_enabled is set in SIBYL_OIDC. Provider configuration, role-claim mapping, and deprovisioning are covered in the admin guides:
- Installing Sibyl - OIDC provider setup and
SIBYL_OIDCreference - Inviting Users - JIT provisioning and IdP role claims
- Break-Glass Access - bounded emergency local login for SSO outages
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:
{
"user": {
"id": "user_uuid",
"github_id": 12345,
"email": "user@example.com",
"name": "User Name",
"avatar_url": "https://avatars.githubusercontent.com/u/12345",
"is_admin": false
},
"organization": {
"id": "org_uuid",
"name": "My Org",
"slug": "my-org"
},
"org_role": "owner"
}Token Refresh
POST /api/auth/refreshExchange refresh token for new access token.
Request:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600
}Password Reset
Password management runs through the /api/users router and uses an email-delivered reset token (SMTP). The two reset endpoints are unauthenticated; the in-session change endpoint requires a valid access token.
Request Reset
POST /api/users/password/resetSends a reset email if an account exists for the address. The response is intentionally generic so the endpoint never reveals whether an account exists.
Request:
{
"email": "user@example.com"
}Response: 202 Accepted
{
"message": "If an account exists, a reset email has been sent."
}Confirm Reset
POST /api/users/password/reset/confirmCompletes the reset using the token from the email and sets the new password.
Request:
{
"token": "reset-token-from-email",
"new_password": "new-secure-password"
}Response: 204 No Content
Change Password (Authenticated)
POST /api/users/me/passwordChanges the current user's password. Requires the current password and a valid session.
Request:
{
"current_password": "old-secure-password",
"new_password": "new-secure-password"
}Response: 204 No Content
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 SurrealDB namespaces
- Document queries filter by org ownership
- Resource access is validated against org membership
Switching Organizations
Switch the active organization for the current session:
POST /api/orgs/{slug}/switchReturns rotated access and refresh tokens scoped to the target organization. The user must be a member of that organization.
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
Auth failures use the standard error envelope with a stable error code and an X-Request-ID header:
{
"error": "authentication_required",
"message": "Authentication failed.",
"request_id": "req_a1b2c3d4e5f6",
"remediation": "Run 'sibyl auth login' or set SIBYL_AUTH_TOKEN."
}| Status | Error code | Cause | Resolution |
|---|---|---|---|
| 401 | authentication_required | Missing token | Provide valid token |
| 401 | authentication_required | Signature verification failed | Token may be corrupted or tampered |
| 401 | authentication_required | Token past expiration | Refresh token or re-login |
| 401 | authentication_required | User ID not in database | Account may be deleted |
| 403 | forbidden | Token missing org claim | Re-authenticate with an org token |
| 403 | forbidden | Insufficient role permissions | Check organization and project roles |
| 404 | not_found | Resource does not exist or no access | Check the ID or prefix and retry |
Related
- auth-api-keys.md - API key authentication
- index.md - API overview
