Skip to content

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:

json
{
  "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:

json
{
  "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

bash
SIBYL_JWT_SECRET=your-secure-secret-key-at-least-32-chars

Optional

bash
SIBYL_JWT_ALGORITHM=HS256                    # Default: HS256
SIBYL_ACCESS_TOKEN_EXPIRE_MINUTES=60         # Default: 60
SIBYL_REFRESH_TOKEN_EXPIRE_DAYS=30           # Default: 30

Authentication Methods

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

http
POST /api/auth/local/signup

Request:

json
{
  "email": "user@example.com",
  "password": "secure-password",
  "name": "User Name"
}

Response:

json
{
  "user": {
    "id": "user_uuid",
    "email": "user@example.com",
    "name": "User Name"
  },
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer"
}

Local Login

http
POST /api/auth/local/login

Request:

json
{
  "email": "user@example.com",
  "password": "secure-password"
}

Response:

json
{
  "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

http
GET /api/auth/github/authorize

Redirects to GitHub OAuth consent screen.

Query Parameters:

ParameterTypeDescription
redirect_uristringPost-login redirect URL

OAuth Callback

http
GET /api/auth/github/callback

Handles GitHub OAuth callback, creates/links user account.

Query Parameters:

ParameterTypeDescription
codestringOAuth authorization code
statestringCSRF state token

Response: Redirects to SIBYL_FRONTEND_URL with tokens set.

Logout

http
POST /api/auth/logout

Clears session and invalidates tokens.

Response: 204 No Content

Also clears sibyl_access_token cookie.

Current User

http
GET /api/auth/me

Returns current authenticated user.

Response:

json
{
  "id": "user_uuid",
  "email": "user@example.com",
  "name": "User Name",
  "organization_id": "org_uuid",
  "role": "member"
}

Token Refresh

http
POST /api/auth/refresh

Exchange refresh token for new access token.

Request:

json
{
  "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

Response:

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 3600
}

Token Validation

Validation Flow

  1. Extract token from cookie or Authorization header
  2. Verify signature using SIBYL_JWT_SECRET
  3. Check expiration (exp claim)
  4. Validate token type (typ claim)
  5. Load user from sub claim
  6. Load organization from org claim

Validation Errors

ErrorHTTP StatusCause
Not authenticated401Missing token
Invalid token401Signature verification failed
Token expired401Token past expiration
User not found401User ID not in database
No organization context403Token missing org claim

Organization Context

JWT tokens include organization context:

json
{
  "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:

http
POST /api/auth/switch-org

Request:

json
{
  "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:

  1. Use refresh token to get new access token
  2. Server may issue new refresh token
  3. 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:

bash
curl -X POST /mcp \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"method": "tools/call", ...}'

MCP auth mode is configurable:

bash
SIBYL_MCP_AUTH_MODE=auto  # auto, on, or off
  • auto: Enforce auth when SIBYL_JWT_SECRET is set
  • on: Always require auth
  • off: Disable auth (development only)

Error Responses

json
{
  "detail": "Not authenticated"
}
StatusErrorResolution
401Not authenticatedProvide valid token
401Invalid tokenToken may be corrupted or tampered
401Token expiredRefresh token or re-login
401User not foundAccount may be deleted
403No organization contextToken missing org claim
403ForbiddenInsufficient role permissions

Released under the MIT License.