Auth endpoints are public (no JWT required) except
POST /auth/logout.UAT: OTP is hardcoded as 123456 — no SMS is sent in the sandbox environment.Why No Passwords?
Aarokya’s primary users are gig workers — delivery riders, drivers, domestic workers. Designing for this user base led directly to a password-free system:- Multiple devices: A delivery rider may use a shared household phone in the morning and a company-issued device in the evening. Passwords create friction at every device switch.
- No password manager culture: The target audience does not use password managers. Passwords get forgotten, leading to support calls and account lockouts.
- OTP is already trusted: Every UPI payment in India uses OTP as the authentication primitive. Users already understand the flow — enter your number, get an SMS, enter the code.
- Reduced credential stuffing risk: Without passwords, there are no stored credential pairs that can be harvested from a breach and reused elsewhere.
- Natural alignment with phone number as identity: In India, the mobile number is the primary digital identity. It links to Aadhaar, UPI, and now to Aarokya.
OTP Flow — Step by Step
OTP Timing Constraints
| Parameter | Value | Notes |
|---|---|---|
| OTP validity | 10 minutes | expires_at = created_at + 10 min |
| Max attempts | 5 | Session permanently invalidated after 5 failures |
| Access token lifetime | 24 hours | JWT exp claim |
| Refresh token lifetime | 30 days | user_sessions.expires_at |
| OTP length | 6 digits | Numeric only |
| OTP generation | CSPRNG | Never predictable |
Endpoints
POST /auth/otp/trigger
Send OTP to phone number. UAT always succeeds and returns
otp: "123456" in the response body (dev convenience — not present in production).POST /auth/otp/verify
Verify OTP → JWT pair +
is_new_user flag. Triggers wallet provisioning on success. Increments attempts on every wrong OTP.POST /auth/token/refresh
Exchange a valid refresh token for a new token pair. Old refresh token is revoked immediately (single-use rotation).
POST /auth/logout
Revoke current device session. Requires
Authorization: Bearer header. Other active sessions on other devices are unaffected.Security Model
OTP Security
OTP Hashing
OTP is hashed with bcrypt (cost factor 12) before storage. The plaintext OTP is never written to the database. When the user submits their OTP, it is verified against the bcrypt hash using constant-time comparison.
Single Use
verified_at is set on the first successful verification. Any subsequent call to verify with the same OTP — even the correct one — returns INVALID_OTP. This prevents replay attacks.Brute Force Protection
attempts is incremented atomically on every wrong OTP. After 5 failures, the session is permanently invalidated — no further attempts are accepted regardless of the OTP value. The user must trigger a new OTP.10-Minute Expiry
OTP sessions expire 10 minutes after creation.
expires_at is checked before bcrypt verification, so an expired OTP returns OTP_EXPIRED immediately without doing bcrypt work.Token Security
JWT Access Token
Signed with HS256 using a server-side secret. Claims include
user_id and exp. Validated on every protected request by the middleware — no DB lookup required for access token validation (stateless).Refresh Token — Hash Only
Refresh tokens are 256-bit random hex strings. Only the SHA-256 hash is stored in
user_sessions.refresh_token_hash. The plaintext token is only ever in the HTTP response — the DB never holds it.Token Rotation
Refresh tokens are single-use.
/auth/token/refresh atomically revokes the submitted token and issues a new pair. If the same token is used twice (race condition), the second call returns 401.Per-Device Sessions
Each login creates a new
user_sessions row. Logout revokes only the session associated with the current Authorization token. A user with 3 active devices can log out of one without affecting the others.Token Lifecycle
| Token | Expiry | Recommended Storage |
|---|---|---|
| Access token (JWT) | 24 hours | In-memory only; SecureEnclave/TEE if persisted |
| Refresh token (opaque) | 30 days | iOS Keychain / Android Keystore |
Session Management
Each user can have multiple concurrent sessions — one per device or app install. Sessions are tracked in theuser_sessions table:
is_revoked: Set totrueby logout or by double-use of a refresh token.expires_at: Hard expiry after 30 days regardless of activity.last_used_at: Updated on each token refresh. Used for audit and inactive session cleanup.device_info: Optional client-provided string identifying the device/platform.
What happens on token refresh?
Database Schema
Request / Response Examples
Wallet Provisioning at Login
When a new user successfully verifies their OTP, the Auth module triggers wallet creation in the background:CREATED → PENDING → IN_PROGRESS → COMPLETED as the Juspay API is called in the background. Check /wallet status before initiating payments.