Authentication
Outpost supports five overlapping credential types. Most programmatic integrations should use API tokens; the dashboard uses JWT cookies.
Credential types
| Type | Format | Where used |
|---|---|---|
| JWT (HS256) | eyJhbGciOiJIUzI1NiI… |
Authorization: Bearer header or token cookie. Issued by /auth/login. |
| API token | op_<43-char base64url> |
Authorization: Bearer header. Created at /api/tokens. |
| Auth0 ID token | RS256 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 cookie | page_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 registered403 Registration is currently closed422validation (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:
- Stores the JWT in
localStorageas"token". - Sets the
tokencookie (HttpOnly, 24 h). - Redirects to the
nextquery 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"
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):
- If an
Authorization: Bearerheader is present, try API token (op_prefix), then JWT. - If no header but the
tokencookie is set, decode it as a JWT. - Otherwise the user is anonymous (
None).