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.