docs: update authentication.md with Logto setup checklist
This commit is contained in:
@@ -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=<client-id> # From Logto app settings
|
||||
OIDC_CLIENT_SECRET=<client-secret> # From Logto app settings
|
||||
OIDC_AUTH_SECRET=<random-32-char-hex> # 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=<m2m-app-id> # From Logto M2M app
|
||||
LOGTO_M2M_APP_SECRET=<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
|
||||
|
||||
Reference in New Issue
Block a user