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
  • OIDC / Enterprise SSO: Corporate OpenID Connect providers with exact per-organization binding

Token Types

Access Token

Short-lived token for API authentication.

Claims Schema:

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

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"
  },
  "organization": {
    "id": "org_uuid",
    "name": "My Org",
    "slug": "my-org"
  },
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_in": 3600
}

Local Login

http
POST /api/auth/local/login

Request:

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

Response:

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

http
GET /api/auth/github

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.

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.

MethodPathPurpose
GET/api/auth/oidc/{provider}/loginStart the OIDC authorization flow
GET/api/auth/oidc/{provider}/callbackComplete login, set the session cookie, redirect
GET/api/auth/oidc/{provider}/refreshSilent 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:

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

http
POST /api/auth/refresh

Exchange refresh token for new access token.

Request:

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

Response:

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

http
POST /api/users/password/reset

Sends 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:

json
{
  "email": "user@example.com"
}

Response: 202 Accepted

json
{
  "message": "If an account exists, a reset email has been sent."
}

Confirm Reset

http
POST /api/users/password/reset/confirm

Completes the reset using the token from the email and sets the new password.

Request:

json
{
  "token": "reset-token-from-email",
  "new_password": "new-secure-password"
}

Response: 204 No Content

Change Password (Authenticated)

http
POST /api/users/me/password

Changes the current user's password. Requires the current password and a valid session.

Request:

json
{
  "current_password": "old-secure-password",
  "new_password": "new-secure-password"
}

Response: 204 No Content

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 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}/switch

Returns 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:

  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

Auth failures use the standard error envelope with a stable error code and an X-Request-ID header:

json
{
  "error": "authentication_required",
  "message": "Authentication failed.",
  "request_id": "req_a1b2c3d4e5f6",
  "remediation": "Run 'sibyl auth login' or set SIBYL_AUTH_TOKEN."
}
StatusError codeCauseResolution
401authentication_requiredMissing tokenProvide valid token
401authentication_requiredSignature verification failedToken may be corrupted or tampered
401authentication_requiredToken past expirationRefresh token or re-login
401authentication_requiredUser ID not in databaseAccount may be deleted
403forbiddenToken missing org claimRe-authenticate with an org token
403forbiddenInsufficient role permissionsCheck organization and project roles
404not_foundResource does not exist or no accessCheck the ID or prefix and retry

Released under the Apache-2.0 License.