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 the X-Root-Password header 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 + X-Root-Password headerElevate 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
Authorization: Bearer <firebase-id-token>
X-Root-Password: <root-password>

Error Responses​

CodeCause
400Token expired (auth/id-token-expired)
400Token from wrong Firebase project (auth/argument-error audience mismatch)
400Malformed token
400X-Root-Password header 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.