Backend Architecture Series — Jocoso.cl eCommerce · #02

JWT, Refresh Tokens and 2FA: Real Security from Day 1

Short-lived access tokens, refresh token rotation, and TOTP for a multichannel ecommerce


Security Cannot Be an Afterthought

In an ecommerce the consequences of a breach are direct: compromised accounts, fraudulent orders, card data at risk. Designing security from day 1 not only protects users — it protects the business reputation and the regulatory compliance required by payment gateways like MercadoPago.


Dual Token Architecture

■ Technical Decision

Access Token (JWT, 15 min) + rotated Refresh Token (7 days, SHA-256 hashed in DB). The 15 minutes minimize the exposure window if the token is intercepted. The refresh token is never stored in plain text — only its SHA-256 hash.


Refresh Token Rotation — Theft Detection

Each use of the refresh token invalidates the previous token and generates a new one. If an attacker steals the token and uses it before the legitimate user, the second use attempt detects the collision and revokes the entire session:

typescript
// application/auth/use-cases/refresh.usecase.ts
async execute(rawToken: string): Promise<TokenPair> {
  const hash = this.tokenSvc.hashToken(rawToken); // SHA-256
  const record = await this.refreshRepo.findByHash(hash);

  if (!record || record.isRevoked || record.expiresAt < new Date()) {
    throw new UnauthorizedException('Invalid or expired token');
  }

  // Rotation: revoke old, issue new
  await this.refreshRepo.revoke(record.id);
  const newRaw = this.tokenSvc.generateRefreshToken();
  const newHash = this.tokenSvc.hashToken(newRaw);
  await this.refreshRepo.save({ userId: record.userId, hash: newHash, ... });

  return { accessToken: this.tokenSvc.generateAccessToken(user), refreshToken: newRaw };
}

RBAC — Role-Based Access Control

The system handles three roles: ADMIN, SELLER, and CUSTOMER. Authorization is implemented with a declarative decorator and a guard that reads the JWT payload:

typescript
// infrastructure/security/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    const roles = this.reflector.get<Role[]>('roles', ctx.getHandler());
    if (!roles?.length) return true;
    const { user } = ctx.switchToHttp().getRequest();
    return roles.includes(user.role);
  }
}

// Usage in controller:
@Get('admin/users')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
listUsers() { ... }

2FA with TOTP — Google Authenticator

Two-factor authentication uses TOTP (Time-based One-Time Password) compatible with Google Authenticator and Authy. The flow has three steps:

  • Setup: the server generates a secret with speakeasy and returns a QR code URI
  • Verify: the user enters the 6-digit code to confirm setup
  • Login: if 2FA is enabled, login requires the TOTP code in addition to password
typescript
// infrastructure/security/totp.service.ts
@Injectable()
export class TotpService {
  generateSecret(email: string) {
    const secret = speakeasy.generateSecret({
      name: `Jocoso.cl (${email})`,
      issuer: 'Jocoso.cl',
    });
    return { secret: secret.base32, otpauthUrl: secret.otpauth_url };
  }

  verify(secret: string, token: string): boolean {
    return speakeasy.totp.verify({
      secret, encoding: 'base32', token, window: 1,
    });
  }
}

bcrypt with 12 Rounds

Passwords are hashed with bcrypt using 12 rounds. On modern hardware, 12 rounds takes ~250ms — enough to make brute-force attacks prohibitively slow, without degrading the user login experience.


API Hardening

  • Helmet: configures HTTP security headers (CSP, HSTS, X-Frame-Options)
  • Restrictive CORS: only whitelisted origins allowed
  • Global ValidationPipe: whitelist + transform — rejects fields not declared in DTOs
  • class-validator on all DTOs: IsEmail, IsStrongPassword, IsJWT

■ Trade-offs

Stateless JWT vs stateful sessions. JWT allows horizontal scaling without shared store — key for preparing microservices. The downside is that immediate revocation requires a Redis blacklist. For Jocoso.cl the solution is short access tokens (15 min): the impact of a stolen token is bounded by the expiration time. Refresh tokens are revoked in DB on logout and rotation.


Conclusion

Security in an ecommerce is not an additional feature — it is the foundation that allows operation. This design covers three dimensions: robust authentication (JWT + refresh rotation), granular authorization (RBAC), and second factor (TOTP). All implemented in the infrastructure layer, invisible to the domain.