AuthAPI
Handles authentication and session management for all PlayTelly clients. Issues RS256 access tokens and HS256 refresh tokens, supports both web (cookie-based) and mobile (token-in-body) clients, and manages multi-org membership claims inside the JWT.
Handles:
- Login — platform-aware; cookies for web, tokens in body for mobile (via
PlayTelly-Platformheader) - Signup — public self-registration with pending-approval state
- Token refresh — silent rotation for web and mobile
- Logout — single session revocation
- Logout all — full session wipe for a user
- Session listing — enumerate active refresh tokens
- Session revocation — revoke a specific session by ID
Architecture
graph LR
Client["Client (Web / Mobile)"]
AuthAPI["AuthAPI (Go/Fiber)"]
DB["PostgreSQL<br />(users, orgs, refresh_tokens)"]
RSA["RSA Key Pair<br />(access token signing)"]
HMAC["Shared Secret<br />(refresh token signing)"]
Client -->|POST /login, /signup, /refresh| AuthAPI
AuthAPI --> DB
AuthAPI --> RSA
AuthAPI --> HMAC
AuthAPI -->|Bearer JWT| Client
Token Design
| Token | Algorithm | Expiry (default) | Storage |
|---|---|---|---|
| Access Token | RS256 | 15 min | Not persisted — validated via public key |
| Refresh Token | HS256 | 30 days | Hash stored in refresh_tokens table |
Access tokens carry the full org membership claim (orgs[]) so downstream services can authorise without a DB round-trip. Refresh tokens are rotated on every use — the old token is deleted, a new pair is issued.
JWT Payload (access token)
{
"iss": "authapi",
"sub": "<user-uuid>",
"uid": "<user-uuid>",
"iat": 1719144000,
"exp": 1719144900,
"jti": "<uuid>",
"orgs": [
{
"org_id": "<uuid>",
"org_slug": "acme",
"role": "member",
"permissions": ["channels:read", "channels:write"]
}
]
}
role is one of member, admin, or sysadmin. For admin and sysadmin, permissions is omitted — they bypass all permission checks.
Client Differences
sequenceDiagram
participant Web
participant Mobile
participant AuthAPI
Web->>AuthAPI: POST /login<br/>PlayTelly-Platform: web
AuthAPI-->>Web: 200 { user, orgs }<br/>Set-Cookie: access_token, refresh_token (HttpOnly)
Mobile->>AuthAPI: POST /login<br/>PlayTelly-Platform: mobile
AuthAPI-->>Mobile: 201 { access_token, refresh_token, expires_in, user, orgs }
Both clients use the same POST /api/v1/login endpoint. The PlayTelly-Platform header controls the response behaviour: web clients receive tokens as HttpOnly cookies (never visible in the body); mobile clients receive tokens in the response body and are responsible for storage.
Endpoints
Health
GET /health
Liveness probe. Returns 200 when the service is up.
Response 200
{ "status": "ok" }
Public — no authentication required
POST /api/v1/login
Authenticate a user. Behaviour is controlled by the required PlayTelly-Platform request header.
Request
| Header | Required | Values |
|---|---|---|
PlayTelly-Platform |
Yes | web or mobile |
{
"username": "alice",
"password": "s3cur3p@ss",
"org_name": "acme"
}
PlayTelly-Platform: web — tokens are set as HttpOnly cookies; the body only contains user and org info.
Cookies set
| Cookie | Path | Max-Age |
|---|---|---|
access_token |
/ |
900 s (15 min) |
refresh_token |
/api/v1 |
604 800 s (7 days) |
Both cookies are HttpOnly, Secure, SameSite=Lax.
Response 200 (web)
{
"user": {
"id": "<uuid>",
"username": "alice",
"email": "alice@acme.io",
"org_id": "<uuid>",
"password_change_required": false
},
"orgs": [
{
"org_id": "<uuid>",
"org_slug": "acme",
"role": "member",
"permissions": ["channels:read"]
}
]
}
PlayTelly-Platform: mobile — returns a full token pair in the response body.
Response 201 (mobile)
{
"access_token": "<RS256 JWT>",
"refresh_token": "<HS256 JWT>",
"expires_in": 900,
"token_type": "Bearer",
"user": {
"id": "<uuid>",
"username": "alice",
"email": "alice@acme.io",
"org_id": "<uuid>",
"password_change_required": false
},
"orgs": [
{
"org_id": "<uuid>",
"org_slug": "acme",
"role": "member",
"permissions": ["channels:read"]
}
]
}
| Status | Meaning |
|---|---|
400 |
Missing or malformed fields, or missing PlayTelly-Platform header |
401 |
Invalid credentials |
403 |
Account pending approval or disabled |
404 |
Org not found |
500 |
Internal error |
POST /api/v1/signup
Public self-registration. The created account starts in pending state — it must be approved by an admin before it can access protected resources.
Request
{
"username": "bob",
"given_name": "Bob",
"family_name": "Smith",
"email": "bob@acme.io",
"password": "s3cur3p@ss",
"org_name": "acme"
}
given_name and family_name are optional.
Response 201 — same shape as /login/mobile with role: "member" and empty permissions.
| Status | Meaning |
|---|---|
400 |
Missing fields or weak password |
404 |
Org not found |
409 |
Username or email already taken |
500 |
Internal error |
POST /api/v1/refresh
Silent token rotation. Accepts both web and mobile clients via client_type.
Request
{
"client_type": "mobile",
"refresh_token": "<HS256 JWT>"
}
For client_type: "web", refresh_token in the body is ignored — the token is read from the refresh_token cookie.
Response 200 — mobile
{
"access_token": "<RS256 JWT>",
"refresh_token": "<HS256 JWT>",
"expires_in": 900,
"token_type": "Bearer",
"orgs": [...]
}
Response 200 — web — new cookies are set (same as /login), body contains only:
{ "orgs": [...] }
| Status | Meaning |
|---|---|
400 |
Invalid client_type or missing body |
401 |
Missing, invalid, or expired refresh token |
500 |
Internal error |
Refresh token rotation
The submitted refresh token is invalidated immediately. Concurrent refresh calls with the same token will result in a 401 for the second caller.
Authenticated — requires valid access token
All endpoints below require either:
Authorization: Bearer <access_token>header, oraccess_tokencookie (web clients).
POST /api/v1/logout
Revoke the current session (single refresh token) and clear auth cookies.
Request (optional body)
{ "refresh_token": "<HS256 JWT>" }
If refresh_token is omitted, the token is read from the cookie. If neither is present, all sessions for the authenticated user are revoked.
Response 200
{ "success": true }
POST /api/v1/logout/all
Revoke all active refresh tokens for the authenticated user across all devices.
Request — no body
Response 200
{ "success": true }
GET /api/v1/users/me/sessions
List all active refresh token sessions for the authenticated user.
Response 200
{
"sessions": [
{
"id": "<uuid>",
"user_id": "<uuid>",
"org_id": "<uuid>",
"expires_at": "2026-06-30T12:00:00Z",
"created_at": "2026-06-23T12:00:00Z"
}
]
}
DELETE /api/v1/users/me/sessions/:id
Revoke a specific session by its refresh token ID. Useful for removing a single device without logging out everywhere.
URL parameter — :id is the UUID from GET /users/me/sessions.
Response 200
{ "success": true }
| Status | Meaning |
|---|---|
400 |
Invalid UUID format |
404 |
Session not found or already revoked |
500 |
Internal error |
Login Flow
sequenceDiagram
participant Client
participant AuthAPI
participant PostgreSQL
Client->>AuthAPI: POST /login (PlayTelly-Platform: web | mobile)
AuthAPI->>PostgreSQL: Lookup user by username + org
PostgreSQL-->>AuthAPI: user record + org membership
AuthAPI->>AuthAPI: Verify bcrypt password
AuthAPI->>AuthAPI: Sign RS256 access token (15 min)
AuthAPI->>AuthAPI: Sign HS256 refresh token (7 days)
AuthAPI->>PostgreSQL: Store hashed refresh token
AuthAPI-->>Client: tokens (body or cookie)