Authentication

The Swappr API uses RS256 JWTs for access tokens and opaque rotating refresh tokens for long-lived sessions. We do not use cookies; clients hold tokens and send them on every request.

Access tokens

  • Algorithm: RS256 (asymmetric). The public key is distributed safely; only Swappr’s API can sign.
  • TTL: 15 minutes.
  • Header: Authorization: Bearer <accessToken>.
  • Claims:
    {
      "sub": "usr_01HZQ7K3M4N5P6Q7R8S9T0V1W2",
      "sid": "01HZQ7K3M4N5P6Q7R8S9T0V1W2",   // session id (ULID); also keyed on the refresh-token row
      "scope": "user",                       // enum: "user" | "admin"
      "iat": 1737486000,
      "exp": 1737486900,
      "jti": "01HZQ7K3M4N5P6Q7R8S9T0V1W3"    // unique per signed token
    }
    
When the access token expires, the API returns 401 UNAUTHENTICATED. Call /auth/refresh to mint a new one.

Refresh tokens

  • Format: 256-bit random, base64url encoded (43 chars). Opaque to clients.
  • Storage at rest: argon2id hash; the raw value is shown to the client ONCE on issue and never again.
  • TTL: 30 days.
  • Rotation: every /auth/refresh call returns a new refresh token and invalidates the old one. Re-using a refresh token is treated as theft — the entire session chain is revoked and the API returns 401 TOKEN_REVOKED.

Socket tickets

Socket.IO handshakes don’t carry an Authorization header well, and putting a JWT in a query string leaks it into access logs. So:
  1. The authenticated client calls POST /api/v1/auth/socket-ticket (REST).
  2. The API issues a one-shot 60-second ticket and stores it in Redis.
  3. The client connects to the WebSocket with ?ticket=<ticket> in the handshake URL.
  4. The realtime server atomically GETDELs the ticket from Redis — exactly one socket admission per ticket, no replay.

Logout

  • POST /auth/logout — revokes the current session’s refresh token. Other devices stay logged in.
  • POST /auth/logout-all — revokes every refresh token for this user. All devices are kicked.

Sign in with Google

Swappr uses the Firebase ID-token flow (not a server-side redirect). The mobile app performs native Google sign-in through the Firebase Auth SDK, obtains a Firebase ID token, and POSTs it to POST /auth/oauth/google. The backend verifies the token with firebase-admin (signature, expiry, issuer, and audience = our Firebase project), finds-or-creates the user, and issues the same access/refresh pair as the email path.
  • First-time Google users are created on the fly (201, isNewUser: true) and flow into the normal onboarding state machine.
  • Returning users get 200 with isNewUser: false.
  • An email already owned by a non-Google account is rejected with 409 ACCOUNT_EXISTS_VIA_OAUTH — Swappr never silently links Google to a pre-existing account.
The same Firebase service account powers both Google sign-in verification and push (FCM) delivery. Apple Sign In is planned and will follow the identical ID-token POST pattern at /auth/oauth/apple.