Skip to main content

Auth Module

Overview

The auth module is the security foundation of FlowPOS. It handles:

  • Firebase ID token validation via a globally applied AuthGuard
  • User registration / retrieval on first login
  • Firebase custom claims synchronisation (db_user_id, role)
  • Root (sudo) elevation protected by an Argon2-hashed server-side password
  • Param decorators that inject auth context into any controller across the API

Architecture

auth/
├── auth.module.ts
├── application/
│ └── auth.service.ts ← use cases (verify, upsert, status, elevate)
├── domain/
│ └── auth.types.ts ← domain types (UserStatusResult, etc.)
├── infrastructure/
│ ├── auth.guard.ts ← global Firebase token guard
│ ├── auth-root.guard.ts ← root-password Argon2 guard
│ ├── auth.decorators.ts ← param decorators (@FirebaseUser, @UserId, …)
│ ├── auth.providers.ts ← HASHED_ROOT_PASSWORD injection token
│ └── auth.types.ts ← request/token type extensions
└── interfaces/
├── auth.controller.ts ← thin HTTP controller + Swagger
└── dtos/
├── create-or-get-user.dto.ts
├── auth-user-response.dto.ts
└── user-status-response.dto.ts

The module follows Hexagonal Architecture:

LayerResponsibility
Domain (auth.types.ts)Framework-agnostic types: UserStatusResult, UserStatusSuccess, UserStatusError
Application (auth.service.ts)Orchestrates use cases; depends only on domain types, FirebaseService, and UsersService
Infrastructure (guards, providers)Implements transport-level security; framework-dependent
Interface (controller, DTOs)HTTP mapping; thin; delegates entirely to AuthService

Domain Concepts

Firebase custom claims

FlowPOS stores two pieces of information in Firebase custom claims:

ClaimTypeMeaning
db_user_idstring (UUID)Links Firebase UID → FlowPOS DB user row
roleAppRoleNameApp-level role (Admin, Root, etc.)
role_by_business_idRecord<string, string>Business-scoped roles

Claims are written by the backend after token verification and are available in the next token refresh.

Token sources

AuthGuard (and FirebaseIdToken decorator) extract the token from:

  1. Cookie flowpos-id-token (set by the web app)
  2. Authorization: Bearer <token> header (used by the PWA and API clients)

Public routes

Any route decorated with @IsPublic() allows unauthenticated requests through. If a token is present it is still verified and request.firebaseUser is populated, enabling optional personalisation on public endpoints.


Use Cases (AuthService)

createOrGetUser(idToken, fullName?, email?)

  1. Verifies the Firebase ID token
  2. Upserts the user in the database (getOrCreateUser)
  3. Sets the db_user_id custom claim

Used by POST /auth/firebase — called on every first login or app launch.

getUserByToken(idToken)

  1. Verifies the Firebase ID token
  2. Looks up the existing DB user (no creation)

Used by POST /auth/firebase-get-user — for re-auth flows. Does not sync claims (avoids Firebase Admin API bottlenecks). Claims are synced on create and when roles change.

verifyToken(idToken)

Verifies the Firebase ID token and returns the decoded payload. Throws a mapped HttpException on any failure.

Used by POST /auth/validate.

getUserStatus(firebaseUid)

  1. Looks up the DB user by Firebase UID
  2. Fetches onboarding progress
  3. Returns a structured result (success or error payload, never throws)

Used by GET /auth/me. Returns UserStatusResult — a discriminated union so the controller stays thin.

elevateToRoot(uid, currentRole)

Sets role: Root custom claim. Throws 409 Conflict if the user is already Root.

Used by PATCH /auth/sudo-me (root-password gated).


Guards

AuthGuard (global)

Applied to all routes via APP_GUARD. Behaviour:

  • OPTIONS requests: always allowed (CORS preflight)
  • @IsPublic() routes: token verified if present, never blocked
  • Protected routes: valid token required; 401 Unauthorized otherwise

AuthRootGuard

Applied individually with @UseGuards(AuthRootGuard). Reads rootPassword query parameter and verifies it with Argon2 against HASHED_ROOT_PASSWORD from environment config.


Decorators

DecoratorReturnsNotes
@IsPublic()Route decorator; disables auth requirement
@FirebaseUser()ExtendedDecodedIdToken | undefinedFull decoded token incl. custom claims
@FirebaseUid()string | undefineduser_id or uid from the token
@FirebaseIdToken()string | undefinedRaw JWT string
@UserId()string | nulldb_user_id claim, falling back to uid
@MaybeUserId()string | nulldb_user_id claim only; null if not yet onboarded
@IsRoot()booleantrue when role === AppRoleName.Root

API Endpoints

MethodPathAuthDescription
POST/auth/firebasePublic (Bearer)Create or get user + sync claims
POST/auth/firebase-get-userPublic (Bearer)Get existing user
GET/auth/mePublic (Bearer)Current user status + onboarding
POST/auth/validatePublic (Bearer)Token validity check
PATCH/auth/sudo-meBearer + rootPasswordElevate to Root role

Full Swagger documentation is available at GET /api when the app is running.


Example Requests

Login / Register

POST /auth/firebase
Authorization: Bearer <firebase-id-token>
Content-Type: application/json

{
"fullName": "Jane Doe",
"email": "jane@example.com"
}

Response 200:

{
"user": {
"id": "f04b2250-34a0-409c-bb57-612d863851c9",
"firebaseUid": "Z0KRm47UCuOrzqoxi6ZQGQg30Ia2",
"email": "jane@example.com",
"fullName": "Jane Doe"
}
}

Me / Onboarding Status

GET /auth/me
Authorization: Bearer <firebase-id-token>

Response 200:

{
"user": { "id": "f04b2250-...", "email": "jane@example.com" },
"step": "done",
"onboardingComplete": true
}

Sudo Elevation

PATCH /auth/sudo-me?rootPassword=<root-password>
Authorization: Bearer <firebase-id-token>

Error Responses

CodeCause
400Token expired (auth/id-token-expired)
400Token from wrong Firebase project (auth/argument-error audience mismatch)
400Malformed token
400rootPassword query param missing
401No token provided
401Token verification failed (non-Firebase errors)
403Root password incorrect (/sudo-me)
409User is already Root (/sudo-me)
500DB error during user upsert or Firebase claims update

Environment Variables

VariableRequiredDescription
HASHED_ROOT_PASSWORDYesArgon2 hash of the sudo password (with encryption key as pepper)
ENCRYPTION_KEYYes32-byte hex key used as Argon2 pepper
FIREBASE_PROJECT_IDYesFirebase project ID for token validation
FIREBASE_CLIENT_EMAILYesFirebase Admin SDK service account email
FIREBASE_PRIVATE_KEYYesFirebase Admin SDK private key

Design Decisions

Why db_user_id in custom claims?

Firebase tokens are short-lived JWTs that do not carry DB primary keys. By writing db_user_id into custom claims after registration, every subsequent authenticated request has the DB user ID available without a database round-trip. Claims are set during POST /auth/firebase and when roles change; POST /auth/firebase-get-user does not sync claims to avoid Firebase Admin API bottlenecks.

Why Argon2 for the root password?

Argon2 is the winner of the Password Hashing Competition and resistant to GPU/ASIC attacks. The encryption key is used as a pepper (secret known only to the server) in addition to the salt, making offline dictionary attacks infeasible even with the hash.

Why does GET /auth/me return an error payload instead of an HTTP error?

The endpoint is intentionally @IsPublic() so the frontend can call it unconditionally on app load. Returning a structured payload with onboardingComplete: false and an error field avoids error-handling boilerplate on the client side for unauthenticated states.