Authentication

gitzen supports three authentication methods: browser sessions for the web editor, API tokens for programmatic access, and device code flow for CLI tools.

Browser sessions

When you sign in via the web UI at /app, the CMS redirects to GitHub for OAuth. After authorization, a session cookie (cms_session) is set with a 30-day expiry.

Sessions have full access — they bypass all permission checks. This is because the browser editor is for interactive use by the repository owner.

API tokens

API tokens are designed for automations, CI/CD pipelines, and programmatic access. They support fine-grained permissions and repo scoping. Note: your static site doesn't need API tokens — it reads content files directly from disk.

Creating a token

Create tokens from the CMS web UI under Settings → API Tokens, or via the API if you have a session.

# Via the API (requires session cookie)
curl -X POST https://your-cms.workers.dev/api/tokens \
  -H "Cookie: cms_session=your_session_id" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "build-token",
    "repos": ["owner/repo-name"],
    "permissions": ["content:read", "config:read"],
    "expiresIn": 7776000
  }'

Token format

Tokens look like cms_a1b2c3d4e5.... The format is cms_{tokenId}.{hmac} where the HMAC is a cryptographic signature ensuring the token hasn't been tampered with.

Using a token

Pass the token in the Authorization header:

curl -H "Authorization: Bearer cms_your_token_here" \
  https://your-cms.workers.dev/api/repos/owner%2Frepo-name/content/blog

Permissions

Tokens must specify at least one permission. Available permissions:

Permission Description
content:read List and read content items, view pull requests and diffs
content:write Create and update content items, create/close PRs, update branches
content:delete Delete content items
content:publish Merge (squash) pull requests into the default branch
config:read Read cms.config.json from repos
repos:read List connected repositories

A token for content automation only needs content:read and config:read. A CI token that auto-publishes approved drafts needs content:publish.

Repo scoping

Tokens can be scoped to specific repositories or granted access to all repos:

  • Specific repos: "repos": ["owner/repo-1", "owner/repo-2"]
  • All repos: "repos": ["*"]

Requests to repos outside the token's scope return 403 Forbidden.

Token lifecycle

  • Expiry: Set expiresIn (in seconds) when creating. Pass null for tokens that never expire.
  • Revocation: Delete a token via the UI or API (DELETE /api/tokens/:tokenId). Revoked tokens are immediately invalid.
  • Last used: The CMS tracks when each token was last used.

Restrictions

API tokens cannot:

  • Create, list, or delete other tokens (use a browser session)
  • Access repos outside their scope
  • Perform actions beyond their permissions

Device code flow

For CLI tools and native apps that can't redirect to a browser, the device code flow provides a way to authenticate.

How it works

  1. Your app requests a device code from the CMS
  2. The user visits a URL and enters the code in their browser
  3. Your app polls until the user authorizes
  4. The CMS returns an API token

Step 1: Request a device code

curl -X POST https://your-cms.workers.dev/auth/device

Response:

{
  "deviceCode": "abc123...",
  "userCode": "ABCD-1234",
  "verificationUri": "https://github.com/login/device",
  "expiresIn": 900,
  "interval": 5
}

Step 2: Direct the user

Display the userCode and tell the user to visit the verificationUri in their browser.

Step 3: Poll for completion

curl -X POST https://your-cms.workers.dev/auth/device/token \
  -H "Content-Type: application/json" \
  -d '{"deviceCode": "abc123..."}'

Possible responses:

Status Meaning
"pending" User hasn't authorized yet — keep polling
"slow_down" You're polling too fast — increase interval
"expired" The code expired — start over
"denied" The user denied authorization
"success" Authorization complete — token included

Success response:

{
  "status": "success",
  "token": "cms_a1b2c3...",
  "expiresAt": "2025-04-15T00:00:00.000Z",
  "username": "samducker"
}

Device code tokens are created with 90-day expiry and full permissions on all repos.

Security notes

  • GitHub tokens are encrypted at rest using AES-256-GCM. The CMS never stores plaintext GitHub tokens.
  • API tokens are verified using HMAC-SHA256 with constant-time comparison to prevent timing attacks.
  • Session cookies are HttpOnly, SameSite=Lax, and Secure (in production).
  • GitHub tokens are automatically refreshed when they're within 5 minutes of expiry.