Auth Module Backend Documentation
Internal auth architecture with split admin/customer controllers and Google OAuth customer-only behavior.
Auth - Backend Documentation
1. Module Scope
Shared AuthModule provides auth primitives used by two HTTP surfaces:
- Admin controller:
/api/auth/* - Mobile customer controller:
/api/mobile/auth/*
Core responsibilities:
- email/password auth
- session/token issuance + refresh/logout
- email/phone verification flows
- password reset flows
- customer Google OAuth login/linking/signup
2. Composition
2.1 Controllers
Admin / backoffice, under apps/api/src/modules/auth/:
auth-session.controller.ts,auth-password.controller.ts,auth-verification.controller.ts,auth-account-deletion.controller.ts- No OAuth endpoints on this surface at all.
Customer / mobile, under apps/api/src/modules/mobile/auth/:
auth-registration.controller.ts— registration onlyauth-oauth.controller.ts— the six public OAuth routes (start / callback / exchange, for both Google and Facebook)auth-oauth-link.controller.ts— the two authenticated link routes. A separate controller because the guard tier differs: everything inauth-oauth.controller.tsis@Public().auth-session,auth-password,auth-verification,auth-account-deletioncontrollers
2.2 Strategies and guards
LocalStrategy+LocalAuthGuardfor admin email loginJwtStrategy+JwtAuthGuardfor protected routesGoogleStrategy+GoogleAuthGuard,FacebookStrategy+FacebookAuthGuard
Both provider guards are thin and delegate to buildOAuthAuthenticateOptions
(guards/oauth-auth.guard.ts). A shared base class is not possible here: AuthGuard(name) bakes
the passport strategy name into the class it returns, so one parent cannot dispatch to two
strategies.
That shared helper does two things the previous Google-only guard did not:
- Detects the callback by path only. The old guard also treated any request carrying a
codequery parameter as a callback, which meantGET /auth/google?code=x&redirect_uri=https://evilskippedcreateState— the only placeredirect_uriis checked against the allowlist.codeis attacker-supplied; the path is not. - Issues the nonce cookie on the start leg, which is what binds the flow to one browser.
2.3 Core services
AuthService— thin facade; delegates to the collaborators belowAuthRegistrationService— registration + registration OTPAuthPasswordService— password reset/set flowsAuthLoginService— credential validation, login, andhandleOAuthLoginfor every providerAccountDeletionService,AuthSessionService,AuthUserQueryService,AuthTokenService,AuthEmailService,VerificationTokenService
OAuth is three services, split because they change for different reasons:
OAuthProviderRegistry— resolves per-provider config once at construction into a frozen record: allowlist, state secret, TTLs, Redis prefix, error-code set, display label,assertsVerifiedEmailand the display-name fallback. A registry rather than aswitch, so adding a provider means adding a row.OAuthStateService— owns the state contract: what is signed, what is asserted, browser binding.OAuthHandoffService— owns the Redis-backed one-time code.
Plus, in mobile/auth/services/: OAuthCallbackService (the shared callback pipeline) and
OAuthLinkService (link/unlink).
3. OAuth Execution Path (Customer Only)
Identical for both providers; {provider} is google or facebook.
GET /api/mobile/auth/{provider}?redirect_uri=<frontend_callback>— the guard validates the redirect against the provider's allowlist, mints a signed state, sets__Host-hs_oauth_nonce, and passport redirects to the provider.- The provider calls
GET /api/mobile/auth/{provider}/callback. - The strategy's
validate()returns the profile. Google's also carriesemailVerified, read from the OIDCemail_verifiedclaim; Facebook's cannot, because the profile has no such signal. OAuthCallbackService.resolveCallback()verifies the state and the nonce cookie, then branches on the sealedintent(loginorlink).- For a login,
AuthLoginService.handleOAuthLogin()applies the identity rule — see the API doc's §6. The provider subject id is the identity; the email is never a join key. - A session is issued, then the
{ user, tokens }payload is stored in Redis under a single-use 192-bit code with a short TTL. - The browser is redirected to the allowlisted
redirect_uriwith the code — as?code=or#code=depending onOAUTH_CODE_DELIVERY. - The storefront calls
POST /api/mobile/auth/{provider}/exchangeand receives{ user, tokens }.
Ordering at step 6 is load-bearing. The session must exist before the code can carry it, so if
issuing the code fails the session is explicitly revoked via AuthSessionService.deleteSession.
Without that a Redis failure leaves a credential valid for the full refresh-token lifetime that no
client ever received, and every retry leaks another. The revocation logs success and failure
separately — swallowing the failure and logging "revoked" regardless would report the leak as a
cleanup.
This Redis key is not a cache. It is the only link between a committed database write and the
client's session, so a failure raises AUTH_OAUTH_HANDOFF_UNAVAILABLE (503) rather than degrading
to a miss.
4. Data Model Effects
Tables touched:
customersadmin_users(lookup check only)accountcustomer_sessions
Write behavior:
- Subject id already linked: touches
updated_atonly.account_idis never rewritten — it is the identity, and re-keying it is what let an attacker evict a victim's link. - Verifying provider adopting an existing customer: inserts the provider link; sets
customers.email_verified = true; if the address had not previously been proved, deletes every link belonging to a non-asserting provider in the same transaction. - Non-verifying provider, email already held: no write. Rejected with
..._ACCOUNT_LINK_REQUIRED. - New customer: inserts
customers+accountin one transaction, withemail_verifiedset from whether the provider proved the address. - Provider access/refresh/id tokens are written as
null. Nothing reads them.
Constraints this relies on (migration 0017)
The identity rule needs the database to enforce it; a lookup-then-branch is a race.
| Constraint | Prevents |
|---|---|
account_actor_provider_account_unique (actor_type, provider_id, account_id) | One provider subject id owning two customers. actor_type is in the key because for provider_id = 'email' the account_id is the email address, and one person may legitimately hold both an admin and a customer account on it. |
account_customer_provider_unique (customer_id, provider_id) partial | A second link for the same provider on one customer |
account_admin_provider_unique (admin_id, provider_id) partial | The same, for admins |
account_actor_matches_target CHECK | actor_type = 'admin' with customer_id populated — legal before, and invisible to every lookup |
account_provider_id_valid CHECK | A typo such as 'Facebook', which inserted cleanly and was then unreachable |
customers_email_unique made partial on deleted_at IS NULL | A soft-deleted customer's address blocking signup permanently with an unmapped 23505 |
Each was confirmed representable before the migration and refused after, by a 17-case constraint probe run in both directions against a real database.
5. Behavior Clarifications
5.1 New user via Google
Customer is auto-signed-up (customer row created), then logged in immediately.
5.2 Existing customer with pending email verification
Successful Google login on same email upgrades account to verified and logs user in.
5.3 Admin account attempting Google login
Rejected with unauthorized response; admin must use email/password flow.
6. Configuration
Google config keys:
GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETGOOGLE_CALLBACK_URLGOOGLE_OAUTH_REDIRECT_ALLOWLISTGOOGLE_OAUTH_STATE_SECRETGOOGLE_OAUTH_STATE_TTL_SECONDSGOOGLE_OAUTH_CODE_TTL_SECONDS
Expected callback route:
http://<host>:<port>/api/mobile/auth/google/callback
7. Security and Operational Controls
- DTO validation via global
ValidationPipe - endpoint throttling via
IpThrottlerGuard+@IpThrottle - banned-user protection via user-query checks
- token/session rotation centralized in session service
- signed/expiring state verification for OAuth callback integrity
- one-time exchange codes stored in Redis and atomically consumed once
This keeps Google flow aligned with the same session issuance policy as email login.
8. Registration Duplicate-Email Handling
AuthService.register() never creates a second customers row for an email that already has one. The duplicate-email check runs inside the same database transaction as the insert, in this fixed precedence order:
- Existing account has
status = "pending_deletion"→409 ACCOUNT_PENDING_DELETION(unchanged, pre-existing behavior). - Existing account has
emailVerified = false→409 AUTH_EMAIL_VERIFICATION_PENDING. - Existing account has
emailVerified = true→409 AUTH_EMAIL_ALREADY_EXISTS.
No new row is inserted and no session/tokens are issued in any of these three cases — the transaction throws before the insert statement runs. Registration does not automatically re-trigger the verification email on a duplicate attempt; that remains the separate, explicit POST /api/mobile/auth/email/verify/resend endpoint's responsibility.
9. Account Deletion & Recovery
Both auth surfaces expose the same account-deletion/recovery operations, all delegated by AuthService to AccountDeletionService (extracted in Subphase 7 to remove a circular module dependency between AuthModule and the mobile user module — no behavior change, same endpoints, same logic):
- Request account deletion (
POST .../account-deletion/request, JWT) — schedules permanent deletion after a 7-day window, bans the account immediately, revokes active sessions. - Recover account via OTP (
POST .../account-deletion/recover, Public) — sends a one-time recovery code to the account's email; available only while the account is inpending_deletionstatus. - Confirm account recovery (
POST .../account-deletion/recover/confirm, Public) — consumes the OTP, re-activates the account and applies a 48-hour cooldown before another deletion request can be made. - Cancel account deletion (
DELETE .../account-deletion/cancel, JWT) — available only before the 7-day window elapses; re-activates the account without requiring OTP verification (the requester is already authenticated). - Get account deletion status (
GET .../account-deletion/status, JWT) — returns currentstatus,deletionScheduledAt, anddeleteBlockUntilfor the authenticated user.
See Auth API Reference for exact routes and request/response shapes.
Auth Module API & Integration Guide
Endpoint reference for admin auth and customer mobile auth, including the Google and Facebook OAuth customer flows, provider linking, and edge-case behavior.
Auth Module Feature Guide
Functional behavior of admin auth, customer mobile auth, Google login/signup/linking, and verification edge cases.