Skip to content
BunBase BunBase BunBase Docs Alpha v0.1.0

Authentication

BunBase uses a JWT access token + refresh token architecture optimized for mobile clients.

Register / Login
→ access_token (JWT, default 15 min, contains sid = session ID)
→ refresh_token (opaque, default 30 days)
Client stores both tokens.
On 401:
POST /api/v1/auth/refresh { refresh_token }
→ new access_token + new refresh_token
On logout:
POST /api/v1/auth/logout → invalidates current refresh token
POST /api/v1/auth/logout-all → invalidates all sessions

Token lifetimes are configurable in Studio → Settings → Auth → Token Lifetimes (settings access_token_ttl_seconds and refresh_token_ttl_days). Changes apply to all new tokens issued after the update.

Each auth method can be independently enabled or disabled in Studio → Settings → Auth → Allowed Auth Methods:

SettingDefaultControls
auth_password_enabledtrueEmail + password login and registration
auth_magic_link_enabledtruePasswordless magic-link login (/auth/magic-link)
auth_totp_enabledtrueTOTP 2FA setup and use
auth_oauth_enabledtrueOAuth provider login (GitHub, Google, OIDC)
auth_api_keys_enabledtrueAPI key creation

Disabling a method returns 403 on new requests that use it. Existing sessions and API keys remain valid. To fully revoke access, use logout-all or delete keys from the admin panel.

Each access token carries a sid (session ID) claim. On every authenticated request, the server validates sid against the active sessions table. If the session was revoked (e.g. from the Studio admin panel), the request is immediately rejected with 401 — even before the 15-minute JWT TTL expires.

Admin impersonation tokens do not carry sid and are not session-bound — they rely solely on their short TTL.

POST /api/v1/auth/logout-all revokes all sessions and all API keys for the authenticated user. The response includes sessions_revoked and api_keys_revoked counts. Password reset also revokes all sessions and API keys.

Password reset (/auth/forgot-password) and magic link (/auth/magic-link) are limited to 5 emails per email address per hour, regardless of source IP. Exceeding the limit returns 429 with a Retry-After header.

POST /api/v1/auth/2fa/setup returns only { "otpauth_url": "otpauth://totp/..." }. The raw secret is not included in the response — the otpauth_url encodes it for QR code scanning. Pass the URL to a QR library (e.g. qrcode) to display it to the user.

PATCH /api/v1/auth/me accepts { "metadata": {...} }. Constraints:

  • Must be a plain JSON object (not an array or primitive)
  • Maximum 64 KB serialized
  • Maximum 3 levels of nesting

Violations return 400 with a descriptive error message.

POST /api/v1/auth/register
Content-Type: application/json
{ "email": "alice@example.com", "password": "hunter2secure" }

Response 201:

{
"access_token": "eyJ...",
"refresh_token": "3a9b...",
"expires_in": 900,
"user": { "id": "...", "email": "alice@example.com", "is_verified": false }
}

A verification email is sent automatically. The account works before verification — you can require it at the collection rule level.

POST /api/v1/auth/login
Content-Type: application/json
{ "email": "alice@example.com", "password": "hunter2secure" }

If 2FA is enabled, the response will be:

{ "totp_required": true, "totp_token": "abc..." }

Then complete login with the TOTP code:

POST /api/v1/auth/2fa/verify
Content-Type: application/json
{ "totp_token": "abc...", "code": "123456" }
POST /api/v1/auth/refresh
Content-Type: application/json
{ "refresh_token": "3a9b..." }

Returns a new token pair. The old refresh token is invalidated.

POST /api/v1/auth/magic-link
Content-Type: application/json
{ "email": "alice@example.com" }

Sends a login link by email. Always returns { "ok": true } to prevent user enumeration.

The email link points to your frontend app at APP_URL/forgot-password?token=.... The frontend extracts the token from the URL and calls the verify endpoint. Set APP_URL in your server config when the frontend runs on a different domain than the backend (see Configuration).

Verify the link (30-minute expiry):

POST /api/v1/auth/magic-link/verify
Content-Type: application/json
{ "token": "<token-from-email>" }

Returns a full session (access + refresh token). Also auto-verifies the email address.

Long-lived tokens for server-to-server use. Pass via X-API-Key header.

# Create
POST /api/v1/auth/api-keys
Authorization: Bearer <access-token>
Content-Type: application/json
{ "name": "My CI pipeline" }
# Response (key shown once)
{ "id": "...", "name": "My CI pipeline", "key": "bb_a1b2c3...", "created_at": ... }
# List
GET /api/v1/auth/api-keys
Authorization: Bearer <access-token>
# Revoke
DELETE /api/v1/auth/api-keys/:id
Authorization: Bearer <access-token>

Use the key:

GET /api/v1/posts
X-API-Key: bb_a1b2c3...
# 0. Check current status
GET /api/v1/auth/2fa/status
Authorization: Bearer <access-token>
→ { "enabled": true }
# 1. Generate secret (displays QR code data)
POST /api/v1/auth/2fa/setup
Authorization: Bearer <access-token>
→ { "secret": "BASE32...", "otpauth_url": "otpauth://totp/..." }
# 2. Enable (verify code from authenticator app)
POST /api/v1/auth/2fa/enable
Authorization: Bearer <access-token>
{ "code": "123456" }
# 3. Disable
DELETE /api/v1/auth/2fa/disable
Authorization: Bearer <access-token>
{ "code": "123456" }

Every user has two optional fields managed by admins:

  • roles — array of strings (e.g. ["admin", "editor"]). Included in the JWT and evaluated against role-based collection access rules.
  • metadata — freeform JSON object for app-defined profile data (e.g. display name, plan tier, preferences).

These fields are set via the Admin API and are never editable by the user directly.

See the full reference at Roles.

After LOCKOUT_MAX_ATTEMPTS (default 10) failed login attempts, the account is locked for LOCKOUT_DURATION_MS (default 15 minutes). The Retry-After header indicates when to retry.

POST /api/v1/auth/verify-email
{ "token": "<token-from-email>" }
# Resend verification email
POST /api/v1/auth/resend-verification
Authorization: Bearer <access-token>
# Request reset link
POST /api/v1/auth/forgot-password
{ "email": "alice@example.com" }
# Reset password (token from email, 1-hour expiry)
POST /api/v1/auth/reset-password
{ "token": "...", "password": "newpassword123" }

BunBase supports GitHub, Google, and any generic OIDC-compatible provider. Configure credentials in Studio → Settings (or directly in _settings), then redirect users to the start URL.

SettingDescription
oauth_github_client_idGitHub OAuth App client ID
oauth_github_client_secretGitHub OAuth App client secret
oauth_google_client_idGoogle OAuth client ID
oauth_google_client_secretGoogle OAuth client secret
oauth_oidc_issuerOIDC issuer URL (e.g. https://accounts.example.com)
oauth_oidc_client_idOIDC client ID
oauth_oidc_client_secretOIDC client secret
oauth_oidc_scopesSpace-separated scopes (default: openid email profile)
  1. Redirect your user to GET /api/v1/auth/oauth/:provider (where :provider is github, google, or oidc).
  2. BunBase redirects the user to the provider.
  3. The provider redirects back to BunBase at GET /api/v1/auth/oauth/:provider/callback.
  4. BunBase exchanges the code, resolves or creates the user, and redirects to {APP_URL}/auth/callback?access_token=...&refresh_token=...&expires_in=....
  5. Your frontend reads the query params and stores the token pair.

On error, BunBase redirects to {APP_URL}/auth/callback?error=<reason> with an error code (e.g. invalid_state, no_email, registration_closed).

The redirect URI you must register with your OAuth provider is:

{PUBLIC_URL}/api/v1/auth/oauth/{provider}/callback
  1. If a _oauth_accounts row exists for (provider, provider_user_id) → use that user.
  2. If the provider returned an email matching an existing _users row → link and use that user.
  3. Otherwise → create a new user (requires registration_open = true). OAuth-created users are automatically email-verified.
# List linked OAuth accounts for the current user
GET /api/v1/auth/oauth/accounts
Authorization: Bearer <access-token>
→ { "accounts": [{ "provider": "github", "email": "...", "name": "...", "avatar_url": "...", "created_at": ... }] }
# Unlink a provider
DELETE /api/v1/auth/oauth/:provider
Authorization: Bearer <access-token>
→ { "ok": true }