diff --git a/docs/authentication.md b/docs/authentication.md index a11b316..44c882d 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -1,89 +1,117 @@ # Authentication -GearBox uses a public-read, authenticated-write model. All GET endpoints are publicly accessible with no credentials required. Any request that modifies data (POST, PUT, PATCH, DELETE) requires authentication. - -This is a single-user app. There is exactly one admin account. +GearBox uses a public-read, authenticated-write model. All GET endpoints are publicly accessible. Any request that modifies data (POST, PUT, PATCH, DELETE) requires authentication. ## Table of Contents -- [First-Time Setup](#first-time-setup) -- [Web UI Authentication](#web-ui-authentication) +- [Overview](#overview) +- [OIDC Authentication (Logto)](#oidc-authentication-logto) +- [Logto Setup Checklist](#logto-setup-checklist) - [API Keys](#api-keys) +- [Account Management](#account-management) - [Auth Middleware Behavior](#auth-middleware-behavior) -- [Auth API Reference](#auth-api-reference) - [Frontend Behavior](#frontend-behavior) --- -## First-Time Setup +## Overview -When no users exist, all write endpoints return `403` with `{ "error": "setup_required" }`. To create the admin account, visit `/login` in the browser and complete the setup form, or call the setup endpoint directly: +Authentication is handled by [Logto](https://logto.io/), a self-hosted open-source OIDC provider. Users register and log in through Logto's sign-in experience. GearBox validates OIDC sessions and provides in-app account management via the Logto Management API. -```http -POST /api/auth/setup -Content-Type: application/json +**Auth methods:** +- **Browser sessions** — OIDC via Logto (redirect flow) +- **API keys** — `X-API-Key` header for programmatic access (MCP, scripts) +- **OAuth 2.1 + PKCE** — for Claude mobile/web MCP connections -{ - "username": "admin", - "password": "yourpassword" -} +--- + +## OIDC Authentication (Logto) + +### Required Environment Variables + +```bash +OIDC_ISSUER=https://your-logto-domain/oidc # Logto OIDC issuer URL +OIDC_CLIENT_ID= # From Logto app settings +OIDC_CLIENT_SECRET= # From Logto app settings +OIDC_AUTH_SECRET= # Session encryption key +OIDC_SCOPES="openid profile email" # Must match Logto app scopes +OIDC_REDIRECT_URI=https://your-app/callback # Must match Logto redirect URI ``` -Requirements: -- `username`: any non-empty string -- `password`: minimum 6 characters +### Management API (M2M) -This endpoint only works when no users exist. Subsequent calls return `403 { "error": "Setup already completed" }`. +GearBox uses the Logto Management API for in-app account management (password change, email change, account deletion). This requires a Machine-to-Machine (M2M) application in Logto. -On success, a session cookie is set and `201` is returned: - -```json -{ "username": "admin" } +```bash +LOGTO_MANAGEMENT_API_ENDPOINT=https://your-logto-domain # Logto base URL +LOGTO_M2M_APP_ID= # From Logto M2M app +LOGTO_M2M_APP_SECRET= # From Logto M2M app ``` --- -## Web UI Authentication +## Logto Setup Checklist -Sessions use an `httpOnly` cookie named `gearbox_session`. +Complete these steps in the Logto admin console after deployment. -| Property | Value | -|------------|--------------------| -| Cookie name | `gearbox_session` | -| httpOnly | true | -| sameSite | Lax | -| path | / | -| Max age | 30 days | +### Application Setup -The session expiry is **automatically refreshed** on each authenticated request. As long as the app is used at least once every 30 days, the session stays active. +- [ ] Create a **Traditional Web** application in Logto Console +- [ ] Set redirect URI to `https://your-app/callback` +- [ ] Note the Client ID and Client Secret for env vars +- [ ] Grant user scopes: `openid`, `profile`, `email` -Passwords are hashed with **argon2** via `Bun.password`. +### Machine-to-Machine (M2M) Application -### Changing Your Password +- [ ] Create an **M2M** application in Logto Console +- [ ] Grant access to the **Logto Management API** resource +- [ ] Note the M2M App ID and Secret for env vars -Requires an active session cookie. +### Branding & Sign-in Experience -```http -PUT /api/auth/password -Content-Type: application/json +- [ ] Upload GearBox logo in Logto Console > Sign-in Experience > Branding +- [ ] Set primary brand color to match GearBox (`gray-700` / `#374151`) +- [ ] Set background color to white +- [ ] Customize sign-in page text if desired +- [ ] (Optional) Configure custom domain `auth.gearbox.de` in Logto Console > Settings -{ - "currentPassword": "oldpassword", - "newPassword": "newpassword" -} -``` +### Social Connectors + +- [ ] Add **Google** connector in Logto Console > Connectors > Social + - Create OAuth 2.0 credentials in Google Cloud Console + - Set authorized redirect URI to Logto's callback URL +- [ ] Add **GitHub** connector in Logto Console > Connectors > Social + - Create OAuth App in GitHub Developer Settings + - Set authorization callback URL to Logto's callback URL +- [ ] Enable both connectors in the Sign-in Experience + +### Email Verification & Password Policy + +- [ ] Enable **mandatory email verification** at signup in Logto Console > Sign-up settings +- [ ] Set password policy: minimum 8 characters, require mixed case, require at least one number + - Logto Console > Sign-in Experience > Password policy + +### Verification + +After completing the checklist: + +- [ ] Visit `/login` — GearBox logo and colors appear on Logto sign-in page +- [ ] Google and GitHub sign-in buttons are visible +- [ ] Create a new account — email verification is required before access +- [ ] Try a weak password (e.g., "abc") — rejected by policy +- [ ] Log in — redirected back to GearBox with session active --- ## API Keys -API keys are intended for programmatic access (scripts, MCP clients, integrations). They are managed under **Settings > API Keys** in the web UI, or via the API endpoints listed below. +API keys provide programmatic access for scripts, MCP clients, and integrations. Managed in **Settings > API Keys**. ### Key behavior - Keys are shown **once** at creation time. Store them securely. -- Keys are stored as an argon2 hash. Only the 8-character prefix is stored in plaintext for display and lookup purposes. -- Pass the key via the `X-API-Key` request header on any write request. +- Keys are stored as a hash. Only the 8-character prefix is kept for display. +- Pass the key via the `X-API-Key` header. ```http POST /api/items @@ -93,189 +121,63 @@ Content-Type: application/json { "name": "Revelate Tangle", "categoryId": 2 } ``` -If both a session cookie and an `X-API-Key` header are present, the API key is checked first. +### API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/auth/keys` | List all API keys (name, prefix, date) | +| `POST` | `/api/auth/keys` | Create a new key (returns full key once) | +| `DELETE` | `/api/auth/keys/:id` | Revoke a key | + +--- + +## Account Management + +All account management happens within GearBox — users never interact with Logto directly. + +### Profile (`/profile`) + +The profile page has four sections: +1. **Profile Info** — display name, bio, avatar (editable) +2. **Account Info** — email (editable via Logto Management API), member-since date +3. **Security** — change password +4. **Danger Zone** — delete account + +### API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/auth/me` | Current session state (user, email, createdAt) | +| `PUT` | `/api/auth/profile` | Update display name, bio, avatar | +| `POST` | `/api/account/password` | Change password (requires current password) | +| `POST` | `/api/account/email` | Change email (updates Logto) | +| `DELETE` | `/api/account` | Delete account (anonymizes public content) | + +### Account Deletion + +When a user deletes their account: +- Personal items, threads, and private data are deleted +- Public setups and catalog contributions are preserved, attributed to "Deleted User" +- User is removed from Logto --- ## Auth Middleware Behavior -The middleware applied to `/api/*` (excluding `/api/auth/*`) follows these rules: +The middleware on `/api/*` (excluding `/api/auth/*` and `/api/account/*`) follows: -1. `GET` requests — always allowed, no auth check. -2. No users exist — returns `403 { "error": "setup_required" }`. -3. `X-API-Key` header present — verified against stored hashes; `401` on failure. -4. `gearbox_session` cookie present — verified against sessions table; refreshed on success; `401` on failure. -5. Neither credential present — returns `401 { "error": "Authentication required" }`. - -The `/api/auth/*` routes handle their own auth logic and are excluded from the global middleware. - ---- - -## Auth API Reference - -### `GET /api/auth/me` - -Returns the current session state. Always public. - -**Response when logged in:** -```json -{ - "user": { "id": 1 }, - "setupRequired": false -} -``` - -**Response when logged out, setup complete:** -```json -{ - "user": null, - "setupRequired": false -} -``` - -**Response when no users exist:** -```json -{ - "user": null, - "setupRequired": true -} -``` - ---- - -### `POST /api/auth/setup` - -Create the first admin account. Only works when no users exist. - -**Request:** -```json -{ - "username": "admin", - "password": "yourpassword" -} -``` - -**Response:** `201` -```json -{ "username": "admin" } -``` - -Sets `gearbox_session` cookie. - ---- - -### `POST /api/auth/login` - -Log in with username and password. - -**Request:** -```json -{ - "username": "admin", - "password": "yourpassword" -} -``` - -**Response:** `200` -```json -{ "username": "admin" } -``` - -Sets `gearbox_session` cookie. Returns `401` on invalid credentials. - ---- - -### `POST /api/auth/logout` - -Clear the current session. No request body needed. - -**Response:** -```json -{ "ok": true } -``` - -Clears the `gearbox_session` cookie and deletes the session from the database. - ---- - -### `PUT /api/auth/password` - -Change the admin password. Requires an active session cookie (not API key). - -**Request:** -```json -{ - "currentPassword": "oldpassword", - "newPassword": "newpassword" -} -``` - -**Response:** -```json -{ "ok": true } -``` - -Returns `401` if `currentPassword` is incorrect. - ---- - -### `GET /api/auth/keys` - -List all API keys. Returns name, prefix, and creation timestamp — never the full key. - -Requires auth. - -**Response:** -```json -[ - { - "id": 1, - "name": "Claude Code", - "prefix": "gbk_a1b2", - "createdAt": "2025-03-01T10:00:00.000Z" - } -] -``` - ---- - -### `POST /api/auth/keys` - -Create a new API key. The full key is returned **once** and cannot be retrieved again. - -Requires auth. - -**Request:** -```json -{ "name": "Claude Code" } -``` - -**Response:** `201` -```json -{ - "id": 1, - "name": "Claude Code", - "key": "gbk_a1b2c3d4e5f6g7h8i9j0...", - "prefix": "gbk_a1b2" -} -``` - ---- - -### `DELETE /api/auth/keys/:id` - -Revoke an API key by ID. Requires auth. - -**Response:** -```json -{ "ok": true } -``` +1. `GET` requests — always allowed, no auth check +2. `X-API-Key` header present — verified against stored hashes +3. `Authorization: Bearer` header present — verified as OAuth token +4. OIDC session cookie present — validated via `@hono/oidc-auth` +5. No credentials — returns `401 { "error": "Authentication required" }` --- ## Frontend Behavior -- A login button is shown in the top-right corner of the UI (Gitea-style). -- The floating action button (FAB) for adding items is hidden when not logged in. -- Edit and delete actions on items, threads, and setups require auth. Unauthenticated users see read-only views. -- When `setupRequired` is true, the UI redirects to the setup flow. +- Anonymous visitors see all public content (catalog, public setups, profiles, discovery feed) +- A "Sign in" button appears in the top nav for anonymous users +- Write actions (add to collection, create thread, etc.) show an auth prompt modal +- The floating action button (FAB) is hidden when not authenticated +- Authenticated users see their avatar in the top nav with a dropdown menu linking to Profile and Settings