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 - API & Integration Guide
1. Route Surfaces
Auth is split across two surfaces:
- Admin / backoffice auth:
/api/auth/* - Customer mobile auth:
/api/mobile/auth/*
OAuth is customer-only and is exposed only on /api/mobile/auth/*. Two providers are
supported, Google and Facebook, served by one implementation. There is no OAuth endpoint under
/api/auth/*.
2. Endpoint Reference
2.1 Admin auth (/api/auth/*)
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/login/email | Public | Admin email/password login |
| POST | /api/auth/password/forgot | Public | Request password reset |
| POST | /api/auth/password/reset | Public | Reset password via token or OTP |
| POST | /api/auth/password/set | JWT | Set or change password for authenticated user |
| POST | /api/auth/email/verify/resend | Public | Resend email verification |
| POST | /api/auth/email/verify/confirm | Public | Confirm email verification |
| POST | /api/auth/refresh | Public | Rotate refresh/access tokens |
| GET | /api/auth/me | JWT | Get current user profile (includes hasPassword) |
| GET | /api/auth/permissions | JWT | Get current user permissions |
| POST | /api/auth/phone/verify/request | JWT | Send phone verification OTP |
| POST | /api/auth/phone/verify/confirm | JWT | Confirm phone verification OTP |
| DELETE | /api/auth/logout | JWT | Logout current session |
| POST | /api/auth/account-deletion/request | JWT | Request account deletion (7-day grace period) |
| DELETE | /api/auth/account-deletion/cancel | JWT | Cancel a pending account deletion before the 7-day window elapses |
| GET | /api/auth/account-deletion/status | JWT | Get current deletion/recovery status |
| POST | /api/auth/account-deletion/recover | Public | Request account recovery OTP for a pending-deletion account |
| POST | /api/auth/account-deletion/recover/confirm | Public | Confirm account recovery via OTP |
2.2 Customer mobile auth (/api/mobile/auth/*)
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/mobile/auth/register | Public | Customer register (email/password) |
| POST | /api/mobile/auth/login/email | Public | Customer email/password login |
| GET | /api/mobile/auth/google | Public | Start Google OAuth redirect (redirect_uri required) |
| GET | /api/mobile/auth/google/callback | Public | Google callback; redirects to frontend with one-time code |
| POST | /api/mobile/auth/google/exchange | Public | Exchange one-time code for user + tokens |
| GET | /api/mobile/auth/facebook | Public | Start Facebook OAuth redirect (redirect_uri required) |
| GET | /api/mobile/auth/facebook/callback | Public | Facebook callback; redirects to frontend with one-time code |
| POST | /api/mobile/auth/facebook/exchange | Public | Exchange one-time code for user + tokens |
| POST | /api/mobile/auth/facebook/link | JWT | Start attaching Facebook to the signed-in account |
| DELETE | /api/mobile/auth/facebook/link | JWT | Detach Facebook from the signed-in account |
| POST | /api/mobile/auth/refresh | Public | Rotate refresh/access tokens |
| POST | /api/mobile/auth/password/forgot | Public | Request password reset |
| POST | /api/mobile/auth/password/reset | Public | Reset password via token or OTP |
| POST | /api/mobile/auth/password/set | JWT | Set password for authenticated mobile user |
| POST | /api/mobile/auth/email/verify/resend | Public | Resend email verification |
| POST | /api/mobile/auth/email/verify/confirm | Public | Confirm email verification |
| GET | /api/mobile/auth/me | JWT | Get current user profile (includes hasPassword) |
| DELETE | /api/mobile/auth/logout | JWT | Logout current session |
| POST | /api/mobile/auth/account-deletion/request | JWT | Request account deletion (7-day grace period) |
| DELETE | /api/mobile/auth/account-deletion/cancel | JWT | Cancel a pending account deletion before the 7-day window elapses |
| GET | /api/mobile/auth/account-deletion/status | JWT | Get current deletion/recovery status |
| POST | /api/mobile/auth/account-deletion/recover | Public | Request account recovery OTP for a pending-deletion account |
| POST | /api/mobile/auth/account-deletion/recover/confirm | Public | Confirm account recovery via OTP |
3. Key Contracts
3.1 Customer email login
POST /api/mobile/auth/login/email
{
"email": "customer@example.com",
"password": "StrongPass123",
"deviceInfo": {
"deviceId": "android-abc",
"deviceType": "android",
"deviceName": "Pixel"
}
}3.2 Google one-time code exchange success shape (customer)
POST /api/mobile/auth/google/exchange
{
"code": "e2f6d17ea01b5b824db6f7d8f936f1b5e97cef25afd8515b"
}Response:
{
"message": "Google login successful.",
"data": {
"user": {
"id": "uuidv7",
"name": "Tenzin Sherpa",
"email": "tenzin@example.com",
"image": "https://lh3.googleusercontent.com/...",
"emailVerified": true,
"phone": null,
"phoneVerified": false,
"role": "customer"
},
"tokens": {
"sessionId": "uuidv7",
"accessToken": "<jwt>",
"refreshToken": "<opaque-token>"
}
}
}3.3 Customer registration duplicate-email response
POST /api/mobile/auth/register
If the submitted email already belongs to an existing customer account, registration fails with one of three distinct 409 Conflict responses, each carrying a stable errorCode:
errorCode | Meaning | Frontend handling |
|---|---|---|
ACCOUNT_PENDING_DELETION | The account is scheduled for deletion | Direct the user to the account-recovery flow before they can reuse this email |
AUTH_EMAIL_VERIFICATION_PENDING | An account with this email exists but has never been verified | Offer "Resend verification email" — call POST /api/mobile/auth/email/verify/resend |
AUTH_EMAIL_ALREADY_EXISTS | An account with this email exists and is already verified | Offer "Log in instead" — do not attempt another registration |
Example response body (AUTH_EMAIL_VERIFICATION_PENDING):
{
"statusCode": 409,
"errorCode": "AUTH_EMAIL_VERIFICATION_PENDING",
"message": "An account with this email already exists but has not been verified yet. Please verify your email or request a new verification email."
}Registration never auto-resends the verification email on a duplicate attempt — the frontend must call the resend endpoint explicitly if it wants to offer that action.
4. Set Password
Authenticated endpoints for setting or changing a user's password. Google-only users use this to create an EMAIL account with a password. Existing EMAIL users use this to change their password.
4.1 POST /api/auth/password/set
4.2 POST /api/mobile/auth/password/set
Set or change password for an authenticated user.
Authentication: JWT Bearer token
Rate limit: 5 requests per 15 minutes
Request body:
{
"newPassword": "NewPass@1234",
"currentPassword": "CurrentPass@123"
}currentPassword is required only if the user already has a password.
Response (200 OK):
{
"message": "Password set successfully.",
"data": null,
"errorCode": null
}Errors:
400 Bad Request—currentPasswordmissing when user already has password401 Unauthorized— Invalid JWT or incorrectcurrentPassword429 Too Many Requests— Rate limit exceeded
4.5 Account Deletion & Recovery
Request deletion
Authentication: JWT Bearer token
Rate limit: 2 requests per 24 hours
Response (200 OK):
{
"message": "Account deletion requested. You will be logged out.",
"data": {
"success": true,
"deletionScheduledAt": "2026-07-21T10:00:00.000Z"
}
}Cancel deletion
Authentication: JWT Bearer token. Must be called before the 7-day deletion window elapses — returns a conflict error once the window has passed.
Rate limit: 5 requests per 24 hours
Deletion status
Authentication: JWT Bearer token. Returns status, deletionScheduledAt, deleteBlockUntil.
Recover account (request OTP)
Authentication: Public (email only — used when the account is banned/inaccessible during the pending-deletion window)
Rate limit: 3 requests per hour
Request body:
{
"email": "user@example.com"
}Response (200 OK):
{
"message": "If the email exists and the account is in pending-deletion status, a recovery code has been sent.",
"data": {
"success": true
}
}Confirm account recovery
Authentication: Public
Rate limit: 5 requests per 15 minutes
Request body:
{
"email": "user@example.com",
"otp": "123456"
}Recovering resets the account to active and applies a 48-hour cooldown (deleteBlockUntil) before another deletion request can be made.
5. hasPassword Field
All profile responses from:
GET /api/auth/meGET /api/mobile/auth/me
include hasPassword: boolean.
hasPassword: falsemeans no EMAIL provider password exists yet.hasPassword: truemeans EMAIL provider password is set.
Frontend integration:
hasPassword === false→ show "Add Password"hasPassword === true→ show "Change Password"
6. OAuth Behavior Matrix (Actual Runtime)
Both providers are resolved by AuthLoginService.handleOAuthLogin().
6.1 The identity rule
The provider's subject id is the identity. The email is a display attribute and is never a join key. Resolving a login by email is an account takeover for any provider that does not assert the address is verified: an attacker sets their provider profile email to the victim's and matches.
Resolution runs in this order, and stops at the first match:
| # | Condition | Result |
|---|---|---|
| 1 | Profile carries no email | 400 AUTH_{PROVIDER}_EMAIL_REQUIRED |
| 2 | An account row exists for (provider_id, account_id) | That row's customer is the identity. Ban and deletion gates run, then the session is issued. account_id is never rewritten. |
| 3 | The email belongs to an admin | 401 AUTH_{PROVIDER}_LOGIN_NOT_ALLOWED_FOR_ADMIN |
| 4 | The email belongs to a live customer | Provider-dependent — see 6.2 |
| 5 | The email belongs to a customer being deleted | 409 AUTH_OAUTH_ACCOUNT_RECOVERY_REQUIRED, or AUTH_OAUTH_EMAIL_UNAVAILABLE if already deleted |
| 6 | Nobody holds the email | Customer + link created in one transaction |
6.2 Where the providers differ
Exactly one branch, row 4, and it turns on two things: whether the provider is capable of asserting a verified email, and whether it did for this profile.
| Provider | Capable | Row 4 behaviour |
|---|---|---|
| yes | Adopts the existing customer and marks emailVerified = true — but only when the profile carried email_verified: true. Without that claim it behaves like Facebook and returns AUTH_GOOGLE_ACCOUNT_LINK_REQUIRED. | |
| no | Always 409 AUTH_FACEBOOK_ACCOUNT_LINK_REQUIRED. Facebook's profile has no verification signal at all, so it may never adopt an account on an email match. |
A new customer created by Facebook has emailVerified = false; one created by a verified Google
profile has emailVerified = true.
6.3 Adoption revokes unproven links
When a verifying provider adopts a customer row whose email was not previously proved, every link belonging to a non-asserting provider (currently Facebook) is deleted in the same transaction.
This closes an otherwise real takeover: an attacker signs up with Facebook using the victim's
address, creating an unproved row; the victim later signs in with Google, which proves the address
and adopts the row. Without the revocation the attacker keeps Facebook access to it. The real owner
re-links Facebook deliberately via POST /api/mobile/auth/facebook/link.
6.4 Browser binding
Every OAuth start sets __Host-hs_oauth_nonce (HttpOnly, Secure, SameSite=Lax, Path=/),
and the callback rejects a state whose nonce does not match with
AUTH_OAUTH_STATE_NOT_BOUND_TO_BROWSER.
SameSite=Lax is required, not a preference — the callback arrives as a cross-site top-level
navigation from the provider's domain, and Strict would withhold the cookie on exactly that
request, silently failing every login.
Without this the signed state binds only to the redirect URI, which anyone can satisfy because the start route is public — a login-CSRF that lands a victim inside the attacker's account.
6.5 Provider failures redirect, they do not return JSON
Guards run before route handlers, so a provider-side failure (a cancelled consent dialog) never
reached the controller's error handling. It now resolves to a redirect back to the allowlisted
redirect_uri.
The error values the anonymous redirect may carry are deliberately coarse — oauth_failed,
email_required, access_denied. A more precise value would let anyone probe which addresses are
registered.
6.6 Provider linking
POST /api/mobile/auth/facebook/link returns an authorizationUrl. The authenticated customer id
is sealed into the signed state, never taken from the request, so a client cannot ask to link a
provider onto another account. DELETE refuses with
AUTH_LAST_CREDENTIAL_CANNOT_BE_REMOVED when the link is the only way the account can sign in.
Important note for pending email-verification users
A customer who registered by email/password without verifying, then signs in with a verified
Google profile on the same address, is marked verified and issued tokens. A Facebook sign-in on that
address is refused with AUTH_FACEBOOK_ACCOUNT_LINK_REQUIRED instead.
7. Operational Notes
Google — GOOGLE_OAUTH_REDIRECT_ALLOWLIST and GOOGLE_OAUTH_STATE_SECRET are required at
boot; the app will not start without them.
GOOGLE_CALLBACK_URLGOOGLE_OAUTH_REDIRECT_ALLOWLIST(required)GOOGLE_OAUTH_STATE_SECRET(required)GOOGLE_OAUTH_STATE_TTL_SECONDS(default300)GOOGLE_OAUTH_CODE_TTL_SECONDS(default90)GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET— managed at runtime from the admin panel
Facebook — all optional. Facebook is enabled only when the allowlist and the state secret are
both set; otherwise its five routes return 503 AUTH_OAUTH_PROVIDER_NOT_ENABLED and nothing else
is affected. This is deliberate: an unconfigured optional provider must never prevent the API from
booting, which is what making these required would do.
FACEBOOK_CALLBACK_URLFACEBOOK_OAUTH_REDIRECT_ALLOWLISTFACEBOOK_OAUTH_STATE_SECRETFACEBOOK_OAUTH_STATE_TTL_SECONDS(default300)FACEBOOK_OAUTH_CODE_TTL_SECONDS(default90)FACEBOOK_CLIENT_ID/FACEBOOK_CLIENT_SECRET— managed at runtime from the admin panel
Shared:
OAUTH_CODE_DELIVERY—query(default) orfragment. Controls whether the one-time code reaches the storefront as?code=or#code=. A query string lands in browser history, nginx access logs and theRefererof every subresource on the callback page; a fragment never leaves the browser. Ship the backend onquery, teach the storefront to read a fragment, then flip.
Callback URL must match mobile auth callback route:
- Example: if
PORT=5002, usehttp://localhost:5002/api/mobile/auth/google/callback
Redirect allowlist must include exact frontend callback URLs, for example:
https://app.word.navneetverma.com/auth/callback
8. Guardrail Summary
- Superadmin/admin login is email/password only. Both OAuth providers refuse admin addresses.
- Customer OAuth login/signup is mobile auth only.
- There is no OAuth endpoint under
/api/auth/google*or/api/auth/facebook*. - Bearer tokens are never returned in browser redirect URLs — only a single-use 192-bit code with a 90-second TTL, redeemed once over a normal POST.
- Provider access and refresh tokens are not persisted. Nothing in the codebase read them, so storing them was a plaintext credential at rest with no consumer.
- The identity is the provider subject id, never the email.
Technical Introduction
A modern full-stack TypeScript monorepo combining React 19, NestJS, TanStack Router, Drizzle ORM, and PostgreSQL - all managed with Turborepo and pnpm workspaces.
Auth Module Backend Documentation
Internal auth architecture with split admin/customer controllers and Google OAuth customer-only behavior.