Files
HIPCTF2/docs/api/auth.md
T

5.1 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
api Auth Endpoints Login, refresh, logout, public self-registration, CSRF, and first-admin registration.
api
auth
login
register
refresh
csrf
2026-07-21T18:28:00Z

Endpoints

Method Path Auth CSRF Rate limited Source
POST /api/v1/auth/register @Public() Enforced — caller must first call GET /api/v1/auth/csrf (the SPA's AuthService.ensureCsrf() does this automatically). Yes — RegistrationRateLimitService per IP backend/src/modules/auth/auth.controller.ts
POST /api/v1/auth/login @Public() Enforced — same pattern as register. CSRF skip was removed in this change. Yes — LoginBackoffService per IP + username backend/src/modules/auth/auth.controller.ts
POST /api/v1/auth/refresh Public route, requires a valid refresh cookie. Skipped (the request is a POST carrying an HttpOnly cookie, not a state-changing user action). No backend/src/modules/auth/auth.controller.ts
POST /api/v1/auth/logout Authenticated (JWT). Enforced. No backend/src/modules/auth/auth.controller.ts
GET /api/v1/auth/csrf @Public() n/a (GET). No backend/src/modules/auth/auth.controller.ts
POST /api/v1/auth/register-first-admin @Public() Skipped (path is in CsrfMiddleware.SKIP_PATH_PREFIXES). Yes backend/src/modules/users/users.controller.ts

Public player self-registration — POST /api/v1/auth/register

New endpoint, gated by the registrationsEnabled setting (backend/src/config/env.schema.tsSETTINGS_KEYS.REGISTRATIONS_ENABLED, default "false").

Field Type Constraints
username string 332 chars, regex ^[a-zA-Z0-9_.-]+$
password string 1256 chars; also passes validatePassword (Argon2 policy).
passwordConfirm string Must equal password (zod refine → VALIDATION_FAILED on passwordConfirm).

Validated by backend/src/modules/auth/dto/auth.dto.tsRegisterDtoSchema.

Successful response (201)

{
  "accessToken": "<jwt>",
  "expiresIn": 900,
  "user": { "id": "<uuid>", "username": "player1", "role": "player" }
}

The refresh token is set as an HttpOnly cookie via setRefreshCookie(res, session.refreshToken, config). The new user row has role='player' and status='enabled'.

Error envelope

HTTP code When
400 VALIDATION_FAILED zod body validation failed.
400 WEAK_PASSWORD Argon2 policy rejects the password.
403 REGISTRATIONS_DISABLED setting.registrationsEnabled !== 'true'.
409 USERNAME_TAKEN A user with that username already exists.
429 RATE_LIMITED RegistrationRateLimitService blocked the IP.

Wiring

  • AuthService.registerPlayer(dto, ip) runs inside a single DataSource.transaction:
    1. RegistrationRateLimitService.isAllowed(ip) check.
    2. SettingsService.get(SETTINGS_KEYS.REGISTRATIONS_ENABLED, 'false') check.
    3. Username uniqueness lookup → USERNAME_TAKEN on collision.
    4. Argon2id hash + insert UserEntity({ role: 'player', status: 'enabled' }).
    5. RegistrationRateLimitService.record(ip).
    6. mintSession(manager, user) → access JWT + refresh row.

Frontend consumer

frontend/src/app/core/services/auth.service.ts exposes login() and register(). Both:

  1. Call ensureCsrf()GET /api/v1/auth/csrf to materialise the csrf cookie if it isn't set yet (failures are swallowed because the CsrfMiddleware will mint the cookie on the next response anyway).
  2. POST with { withCredentials: true, observe: 'response' }.
  3. Return a discriminated union:
    • { ok: true, accessToken, expiresIn, user } on success.
    • { ok: false, status, code, message } on failure, built by the private mapAuthError() helper from the standard error envelope.

The LandingComponent (frontend/src/app/features/landing/landing.component.ts) consumes both methods and maps the code to user-facing copy via buildLoginFailureMessage (see Landing Page).

See also