Skip to content

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-Platform header)
  • 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, or
  • access_token cookie (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)