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: passlocally). - Body:
SignUpDto(email,password,username,country, optionalreferrerCode,marketingOptIn, …). - Returns:
201with the new user + auth tokens. Setsaccess_token/refresh_tokencookies. - 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:
200with{ user, mfaRequired? }. IfmfaRequired === 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) andUserService.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:
200with{ user, accessToken, refreshToken }. Setsaccess_token/refresh_tokencookies.
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:
200withPublicUserDto— id, username, country, balances summary, KYC level, MFA status. Use this as a session-validity probe.
5. POST /auth/refresh¶
- Public (reads
refresh_tokencookie). - Returns:
200with newaccess_tokencookie + body. Rotatesrefresh_token.
6. DELETE /session/log-out/current¶
- Auth required.
- Returns:
200, clears cookies on the response. - Tracing blind spot (medium):
SessionService.logOutSinglehas no manual span. Prismasession.deleteis auto-spanned but attributed straight to the controller — seedocs/audits/perf-trace-coverage-audit.md§logout for the recommended fix.
OAuth providers¶
Steam (GET /auth/steam → GET /auth/steam/return) and Google (GET /auth/google → GET /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 |