Real World Example

OTP Login for Indian Mobile Users

Most Indian consumer apps (Swiggy, Zepto, PhonePe) log users in with a mobile OTP instead of a password. The OTP is hashed, short-lived, single-use and rate limited; a verified OTP returns the same JWT pair as a password login.

src/auth/otp.service.ts

import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import type { Cache } from 'cache-manager';
import { createHash, randomInt } from 'node:crypto';
import { SMS_SENDER, SmsSender } from '../notifications/sms.module';
import { UsersService } from '../users/users.service';
import { AuthService } from './auth.service';

const hash = (s: string) => createHash('sha256').update(s).digest('hex');

@Injectable()
export class OtpService {
  constructor(
    @Inject(CACHE_MANAGER) private readonly cache: Cache,
    @Inject(SMS_SENDER) private readonly sms: SmsSender,
    private readonly users: UsersService,
    private readonly auth: AuthService,
  ) {}

  async send(phone: string) {
    const otp = String(randomInt(100000, 1000000));
    await this.cache.set(`otp:${phone}`, { hash: hash(otp), attempts: 0 }, 5 * 60_000);
    await this.sms.send(phone, `${otp} is your ShopEase login OTP. Do not share it.`);
    return { sent: true, expiresIn: 300 };
  }

  async verify(phone: string, otp: string) {
    const entry = await this.cache.get<{ hash: string; attempts: number }>(`otp:${phone}`);
    if (!entry || entry.attempts >= 5) throw new BadRequestException('OTP expired, request a new one');
    if (entry.hash !== hash(otp)) {
      await this.cache.set(`otp:${phone}`, { ...entry, attempts: entry.attempts + 1 }, 5 * 60_000);
      throw new BadRequestException('Incorrect OTP');
    }
    await this.cache.del(`otp:${phone}`);                          // single use
    const user = await this.users.findOrCreateByPhone(phone);
    return this.auth.issueTokens(user);
  }
}

Example

curl -X POST http://localhost:3000/auth/otp/send -H "Content-Type: application/json" -d '{"phone":"+919876543210"}'
curl -X POST http://localhost:3000/auth/otp/verify -H "Content-Type: application/json" -d '{"phone":"+919876543210","otp":"482913"}'

Example output

{"sent":true,"expiresIn":300}
{"accessToken":"eyJhbGciOiJIUzI1NiIs...","refreshToken":"eyJhbGciOiJIUzI1NiIs..."}