Skip to content

Auth lifecycle

The prerequisite for almost every other endpoint. Pulled from the live api Swagger (http://localhost:4000/swagger) and the controllers under ebit-api/apps/api/src/auth/.

Cookies set by the API

Cookie Set by Purpose Read by
access_token POST /auth/sign-in, POST /auth/refresh Short-lived JWT. Also accepted as Authorization: Bearer …. API, RT (socket.io handshake)
refresh_token POST /auth/sign-in, POST /auth/refresh Long-lived; opaque session id; Redis-backed. POST /auth/refresh
jwt_access_token admin-fe legacy only Back-compat alias for access_token. New admin-fe code reads/writes both during migration. admin-fe middleware

access_token is HTTP-only + SameSite=Lax. The rt socket.io handshake reads it via extractSocketAuthToken(client, 'socket_token') (cookie or auth.socket_token) — see apps/rt/src/utils.ts. Per project_otel_integration_gotchas.md: the socket.io handshake works via cookie, not via a custom auth header — the rt app reads socket_token/access_token from the cookie jar, not from socket.handshake.headers.authorization.

Captcha bypass (local only)

POST /auth/sign-up and POST /auth/sign-in are protected by RecaptchaGuard. With NODE_ENV=local exported on the API, you can pass x-captcha-token: pass (apps/api/src/captcha/google/recaptcha.service.ts:28) and skip Google reCAPTCHA. The bypass is silently rejected in staging/prod — don't ship k6 scripts that depend on it without overriding NODE_ENV per docs/audits/doppler-perf-audit.md.

Sequence

┌────────┐                          ┌─────┐                   ┌───────┐
│ client │                          │ api │                   │ redis │
└────┬───┘                          └──┬──┘                   └───┬───┘
     │ POST /auth/sign-up              │                          │
     │   x-captcha-token: pass         │                          │
     │   { email, password, ... }      │                          │
     ├────────────────────────────────►│                          │
     │                                 │ create user (Postgres)    │
     │                                 │ enqueue welcome (BullMQ) ├──►
     │ 201  { user, accessToken, ... } │                          │
     │◄────────────────────────────────┤                          │
     │                                 │                          │
     │ POST /auth/sign-in              │                          │
     │   x-captcha-token: pass         │                          │
     │   { email, password }           │                          │
     ├────────────────────────────────►│                          │
     │                                 │ AuthService.login [span]  │
     │                                 │ UserService.authenticate │
     │                                 │   [span]                 │
     │                                 │ session.create (Postgres)│
     │                                 │ session-update (BullMQ)  ├──►
     │ Set-Cookie: access_token=…      │                          │
     │ Set-Cookie: refresh_token=…     │                          │
     │ 200  { user, mfaRequired? }     │                          │
     │◄────────────────────────────────┤                          │
     │                                 │                          │
     │ (if mfaRequired)                │                          │
     │ POST /auth/verify-2fa           │                          │
     │   { code }                      │                          │
     ├────────────────────────────────►│                          │
     │ 200  Set-Cookie: access_token=… │                          │
     │◄────────────────────────────────┤                          │
     │                                 │                          │
     │ GET /user/me                    │                          │
     │   Cookie: access_token=…        │                          │
     ├────────────────────────────────►│                          │
     │                                 │ verify JWT + Redis session│
     │ 200  { id, username, ... }      │                          │
     │◄────────────────────────────────┤                          │
     │                                 │                          │
     │ DELETE /session/log-out/current │                          │
     ├────────────────────────────────►│                          │
     │                                 │ session.delete (Postgres)│
     │                                 │ refresh-token revoke     ├──►
     │ 200  Set-Cookie: …; Max-Age=0   │                          │
     │◄────────────────────────────────┤                          │

Endpoints

1. POST /auth/sign-up

  • Public. Captcha required (use x-captcha-token: pass locally).
  • Body: SignUpDto (email, password, username, country, optional referrerCode, marketingOptIn, …).
  • Returns: 201 with the new user + auth tokens. Sets access_token / refresh_token cookies.
  • Side effects: welcome bonus enqueued to BullMQ; affiliate referral resolution; analytics event.
curl -X POST http://localhost:4000/auth/sign-up \
  -H 'Content-Type: application/json' \
  -H 'x-captcha-token: pass' \
  -d '{"email":"u@example.com","password":"S3cret!!","username":"u_local","country":"DE"}' \
  -c cookies.txt

2. POST /auth/sign-in

  • Public. Captcha required.
  • Body: SignInDto (email, password).
  • Returns: 200 with { user, mfaRequired? }. If mfaRequired === true, no cookies are set until 2FA succeeds — the response carries an MFA challenge token instead.
  • Tracing: wraps AuthService.login (apps/api/src/auth/auth.service.ts:150) and UserService.authenticate (apps/api/src/user/user.service.ts:718) in manual spans. Continuous trace through to the BullMQ session-update enqueue.
curl -X POST http://localhost:4000/auth/sign-in \
  -H 'Content-Type: application/json' \
  -H 'x-captcha-token: pass' \
  -d '{"email":"u@example.com","password":"S3cret!!"}' \
  -c cookies.txt

3. POST /auth/verify-2fa (only if mfaRequired)

  • Public (the MFA challenge token from step 2 is sufficient).
  • Body: VerifyMfaDto (code, mfaToken).
  • Returns: 200 with { user, accessToken, refreshToken }. Sets access_token / refresh_token cookies.
curl -X POST http://localhost:4000/auth/verify-2fa \
  -H 'Content-Type: application/json' \
  -d '{"code":"123456","mfaToken":"<from sign-in>"}' \
  -b cookies.txt -c cookies.txt

4. GET /user/me

  • Auth required.
  • Returns: 200 with PublicUserDto — id, username, country, balances summary, KYC level, MFA status. Use this as a session-validity probe.
curl http://localhost:4000/user/me -b cookies.txt

5. POST /auth/refresh

  • Public (reads refresh_token cookie).
  • Returns: 200 with new access_token cookie + body. Rotates refresh_token.

6. DELETE /session/log-out/current

  • Auth required.
  • Returns: 200, clears cookies on the response.
  • Tracing blind spot (medium): SessionService.logOutSingle has no manual span. Prisma session.delete is auto-spanned but attributed straight to the controller — see docs/audits/perf-trace-coverage-audit.md §logout for the recommended fix.
curl -X DELETE http://localhost:4000/session/log-out/current -b cookies.txt

OAuth providers

Steam (GET /auth/steamGET /auth/steam/return) and Google (GET /auth/googleGET /auth/google/callback) follow the standard redirect flow. New OAuth users without a username are sent to POST /auth/setup-username before they can transact.

Common error codes

HTTP code Meaning
400 AUTH_RECAPTCHA_TOKEN_MISSING x-captcha-token header absent
400 AUTH_INVALID_CAPTCHA Token rejected by Google (or in non-local env, the pass bypass was used)
401 AUTH_INVALID_CREDENTIALS sign-in failure
401 AUTH_MFA_REQUIRED sign-in was correct but 2FA must be completed
403 AUTH_GEO_BLOCKED GEO Restrictions API rule matched
409 AUTH_USER_EXISTS sign-up: email or username already taken