Skip to main content

Overview

Rodando Backend implements a robust JWT-based authentication system with dual-token architecture, supporting multiple client types (web, mobile, admin) and comprehensive session management.

Authentication Flow

Dual-Token Architecture

The system uses two types of JWT tokens:

Access Token

Short-lived token used for API requests.
{
  sub: string;              // User ID
  email?: string;           // User email
  phoneNumber?: string;     // User phone
  sid: string;              // Session ID (jti)
  aud: AppAudience;         // Audience (DRIVER_APP, PASSENGER_APP, etc.)
  userType: UserType;       // DRIVER, PASSENGER, ADMIN
  iss: string;              // Issuer
  exp: number;              // Expiration timestamp
}

Refresh Token

Longer-lived token used to obtain new access tokens.
// src/modules/auth/services/auth.service.ts:96-105
const {
  token: refreshToken,
  jti,
  expiresIn: refreshTtl,
} = this.tokenService.createRefreshToken({
  sub: user.id,
  email: user.email,
  phoneNumber: user.phoneNumber,
});

Audience-Based Access Control

The system enforces role-based access based on the appAudience field:
src/modules/auth/services/auth.service.ts:34-40
const AUDIENCE_TO_USER_TYPE: Record<AppAudience, UserType> = {
  [AppAudience.DRIVER_APP]: UserType.DRIVER,
  [AppAudience.PASSENGER_APP]: UserType.PASSENGER,
  [AppAudience.ADMIN_PANEL]: UserType.ADMIN,
  [AppAudience.API_CLIENT]: UserType.ADMIN,
};
During login and token refresh, the system validates that the user’s type matches the required audience.

Session Management

Session Entity

Each authenticated session is stored in the database:
Key Session Fields
{
  jti: string;                      // Unique session identifier
  user: User;                       // Associated user
  sessionType: SessionType;         // WEB, MOBILE_IOS, MOBILE_ANDROID
  refreshTokenHash: string;         // Hashed refresh token (security)
  accessTokenExpiresAt: Date;
  refreshTokenExpiresAt: Date;
  deviceInfo: DeviceInfo;           // Device details
  ipAddress: string;
  userAgent: string;
  appAudience: AppAudience;         // Stored for refresh validation
  revoked: boolean;                 // Session revocation flag
  lastActivityAt: Date;
}

Token Reuse Detection

The system prevents refresh token reuse attacks:
src/modules/auth/services/auth.service.ts:264-301
// Anti-reuse: compare against hash
let matched = false;
try {
  matched = await argon2.verify(session.refreshTokenHash, oldRefreshToken);
} catch {
  matched = false;
}

if (!matched) {
  this.logger.warn(
    'Possible refresh token reuse detected — revoking session',
    { jti: session.jti, userId: session.user?.id },
  );

  session.revoked = true;
  session.revokedAt = new Date();
  session.revokedReason = 'token_reuse_detected';
  await this.sessionRepo.save(session);

  this.eventEmitter.emit(AuthEvents.SessionRevoked, {
    userId: session.user?.id ?? payload.sub,
    sid: session.jti,
    reason: 'token_reuse_detected',
    at: new Date().toISOString(),
  });

  throw new UnauthorizedException('Refresh token inválido o revocado');
}
If token reuse is detected, the session is immediately revoked and a security event is emitted.

Authentication Endpoints

Login

curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "securePassword123",
    "appAudience": "DRIVER_APP"
  }'
For web clients, the refresh token is sent as an HttpOnly cookie. For mobile/API clients, it’s included in the response body.

Refresh Token

src/modules/auth/controllers/auth.controller.ts:74-131
@Public()
@Post('refresh')
@HttpCode(HttpStatus.OK)
async refresh(
  @Req() req: Request & { cookies?: Record<string, string> },
  @Body('refreshToken') refreshTokenBody: string,
  @Res({ passthrough: true }) res: ExpressResponse,
): Promise<RefreshResponseDto> {
  const cookieRt = req.cookies?.refreshToken;
  const oldRt = cookieRt ?? refreshTokenBody;

  if (!oldRt) {
    throw new BadRequestException('No refresh token provided');
  }

  const passRes = !!cookieRt;
  const result = await this.authService.refreshTokens(oldRt, passRes ? res : undefined);

  return plainToInstance(RefreshResponseDto, {
    accessToken: result.accessToken,
    ...(result.refreshToken ? { refreshToken: result.refreshToken } : {}),
    accessTokenExpiresAt: result.accessTokenExpiresAt,
    refreshTokenExpiresAt: result.refreshTokenExpiresAt,
    sid: result.sid,
    sessionType: result.sessionType,
  });
}

Logout

src/modules/auth/services/auth.service.ts:445-522
async logout(oldRefreshToken: string, res?: ExpressResponse): Promise<void> {
  // Extract jti from token
  const payload = this.tokenService.verifyRefreshToken<RefreshTokenPayload>(oldRefreshToken);
  const jti = payload.jti;

  // Find and revoke session
  const session = await this.sessionRepo.findOne({ where: { jti } });
  if (session) {
    session.revoked = true;
    session.revokedAt = new Date();
    session.revokedReason = 'user_logout';
    await this.sessionRepo.save(session);

    // Emit event for real-time disconnection
    this.eventEmitter.emit(AuthEvents.SessionRevoked, {
      userId: session.user?.id,
      sid: session.jti,
      reason: 'user_logout',
      at: new Date().toISOString(),
    });
  }

  // Clear cookie if applicable
  if (res) {
    res.clearCookie('refreshToken', { /* ... */ });
  }
}

JWT Strategy

The JWT strategy validates access tokens on protected routes:
src/modules/auth/strategies/jwt.strategy.ts:33-55
async validate(payload: any) {
  // Validate that user still exists and is active
  const resp: ApiResponse<User> = await this.usersService.findById(payload.sub);
  if (!resp?.success || !resp.data) {
    throw new UnauthorizedException('Usuario no encontrado o inválido');
  }
  const user = resp.data;

  // Return data for req.user
  return {
    sub: user.id,
    id: user.id,
    aud: payload.aud,
    userType: payload.userType,
    name: user.name,
    email: user.email,
    phoneNumber: user.phoneNumber,
    profilePictureUrl: user.profilePictureUrl,
  };
}

Guards

JWT Auth Guard

src/modules/auth/guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  // Allow public routes marked with @Public()
  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;
    return super.canActivate(context);
  }
}

Audience Guard

Additional guard to check specific audience requirements on routes.

Security Features

Token Rotation

Every refresh operation generates new tokens and rotates the session ID (jti)

Argon2 Hashing

Refresh tokens are hashed with Argon2 before storage

Session Revocation

Sessions can be revoked instantly on logout or security events

Device Tracking

Device info, IP, and user agent tracked per session

Best Practices

  • Web: Refresh tokens in HttpOnly cookies, access tokens in memory
  • Mobile: Store refresh tokens in secure storage (Keychain/Keystore)
  • Never expose refresh tokens in localStorage or sessionStorage
  • Refresh access tokens proactively before expiration
  • Implement automatic retry with token refresh on 401 errors
  • Handle refresh failures by redirecting to login
  • Always use HTTPS in production
  • Validate audience matches client type
  • Monitor for suspicious session activity
  • Implement rate limiting on auth endpoints

API Reference

Explore authentication endpoints

WebSocket Authentication

Learn about real-time connection auth

Build docs developers (and LLMs) love