Authentication

Outpost supports five overlapping credential types. Most programmatic integrations should use API tokens; the dashboard uses JWT cookies.

Credential types

TypeFormatWhere used
JWT (HS256)eyJhbGciOiJIUzI1NiI… Authorization: Bearer header or token cookie. Issued by /auth/login.
API tokenop_<43-char base64url> Authorization: Bearer header. Created at /api/tokens.
Auth0 ID tokenRS256 JWT If Auth0 is enabled in config.json. Auto-provisions users by email claim.
Signed upload token<base64url-payload>.<16-hex-sig> Path parameter at /upload/{token}. 5-minute TTL.
Page access cookiepage_access_<page_id> Set by the passcode-verify endpoint to unlock a private/shared page.

POST /auth/register

Create a user with an email and password. Returns 403 if registration_enabled is false in config.json.

Request

POST /auth/register
Content-Type: application/json

{
  "email": "alice@example.com",
  "password": "at-least-8-chars",
  "org_id": "team-acme"          // optional
}

Response 200

{
  "id": 42,
  "email": "alice@example.com",
  "org_id": "team-acme"
}

Errors

  • 400 Email already registered
  • 403 Registration is currently closed
  • 422 validation (password must be ≥ 8 chars, email must validate).

cURL

curl -X POST https://outpost.click/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"hunter2hunter2"}'

POST /auth/login

Verify password with bcrypt, return a JWT, and set the token cookie. The JWT TTL is 24 hours.

Request

POST /auth/login
Content-Type: application/json

{ "email": "alice@example.com", "password": "hunter2hunter2" }

Response 200

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Set-Cookie: token=eyJhbGc...; HttpOnly; SameSite=Lax; Max-Age=86400; Path=/

JWT claims

{
  "sub": "42",          // user id as a string
  "org": "team-acme",   // null if user has no org_id
  "iat": 1746290000,
  "exp": 1746376400,    // iat + 24h
  "iss": "outpost",     // config.jwt_issuer
  "aud": "outpost"      // config.jwt_audience
}

Errors

  • 401 Invalid credentials (also returned when the user exists but has no password — e.g. social-login-only accounts).

cURL

curl -X POST https://outpost.click/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"hunter2hunter2"}'

Using the JWT

# Bearer header
curl https://outpost.click/auth/me \
  -H "Authorization: Bearer eyJhbGc..."

# Or via cookie (set automatically by /auth/login)
curl https://outpost.click/auth/me --cookie "token=eyJhbGc..."

GET /auth/me

Return the current user.

GET /auth/me
Authorization: Bearer eyJhbGc...

200 OK
{ "id": 42, "email": "alice@example.com", "org_id": "team-acme" }

POST /auth/logout

Clears the token cookie. Doesn't invalidate JWTs server-side (they're stateless), so clients should also discard their copy.

curl -X POST https://outpost.click/auth/logout

200 OK
{ "message": "Logged out" }

Auth0 integration

Enable by setting auth0.enabled: true and providing domain, client_id, client_secret, and optionally org_claim (default "org_id") in config.json. The server then accepts RS256 ID tokens whose issuer matches https://<domain>/ and audience matches the client_id. Users are auto-provisioned on first login by the email claim, and org_id is updated from the configured org claim if it changes.

GET /auth/auth0/login

Redirects the browser to the Auth0 authorize URL.

GET /auth/auth0/login?next=/dashboard

302 Found
Location: https://<domain>/authorize?client_id=...&redirect_uri=https://outpost.click/auth/callback&response_type=code&scope=openid+email+profile&state=/dashboard

GET /auth/callback

Exchanges the authorization code for an ID token, verifies it, and redirects to /auth/complete?token=<jwt>&next=<state>.

Google OAuth integration

Enable by setting google.enabled: true with client_id and client_secret. Outpost requests openid email profile scopes, fetches userinfo from Google, and auto-provisions users by email.

GET /auth/google/login

GET /auth/google/login?next=/dashboard

302 Found
Location: https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=https://outpost.click/auth/google/callback&response_type=code&scope=openid+email+profile&state=/dashboard&access_type=online&prompt=select_account

GET /auth/google/callback

Same shape as the Auth0 callback — exchanges code, fetches userinfo, finds-or-creates the user, and redirects to /auth/complete.

GET /auth/complete

Returns a tiny HTML page that:

  1. Stores the JWT in localStorage as "token".
  2. Sets the token cookie (HttpOnly, 24 h).
  3. Redirects to the next query parameter.

This bridge exists because the OAuth callback runs server-side but the dashboard expects a token in the browser.

API tokens (op_…)

The recommended way to authenticate any non-browser client. Tokens never expire and can be revoked at any time. See the API tokens page for management endpoints.

curl https://outpost.click/auth/me \
  -H "Authorization: Bearer op_GgFnE3R8...A2"
Storage. Outpost stores SHA-256 hashes of the full token plus a 12-character prefix for display. The original token is shown only once at creation time.

Signed upload tokens

HMAC-signed payloads used for one-shot uploads without an Authorization header. Format:

<base64url(json_payload)>.<hex(hmac_sha256(secret, payload))[:16]>

Payload fields:

{
  "user_id": 42,
  "name": "My Site",
  "visibility": "public",
  "exp": 1746290300,           // unix seconds; +5 min from issue
  "page_id": "Ab12Cd34"        // present only for update tokens
}

See Programmatic upload for how to obtain and use these.

Precedence inside get_optional_user

For routes that accept either a logged-in user or an anonymous viewer (e.g. page serving):

  1. If an Authorization: Bearer header is present, try API token (op_ prefix), then JWT.
  2. If no header but the token cookie is set, decode it as a JWT.
  3. Otherwise the user is anonymous (None).