How JWT Authentication Is Implemented in the NestJS Backend with Session Duration Controls

TREK issues a signed JWT stored as an httpOnly cookie named trek_session, verifies it via middleware that checks expiration and password version, and derives both the JWT exp claim and cookie maxAge from centralized session-duration constants in config.ts.

The TREK repository implements a unified authentication layer that bridges legacy Express middleware with modern NestJS guards. This approach ensures that JWT session handling, duration controls, and invalidation logic remain consistent across the entire application.

When a user authenticates, the system generates a JWT and persists it as a secure cookie. The AuthService.loginUser method returns a token string, which setAuthCookie (located in server/src/services/cookie.ts) writes to the response.

The cookie configuration respects the session-duration constants defined in server/src/config.ts. If the user selects "Remember me", the function switches to the longer SESSION_DURATION_REMEMBER* variants.

// server/src/nest/auth/auth.controller.ts
@Post('login')
async login(@Body() body: LoginDto, @Res() res: Response, @Req() req: Request) {
  const { token, remember } = await this.authService.loginUser(body);
  // remember=true triggers SESSION_DURATION_REMEMBER* constants
  this.authService.setAuthCookie(res, token, req, remember);
  return res.json({ success: true });
}

Extracting Tokens from Requests

The extractToken function in server/src/middleware/auth.ts provides a single source of truth for token retrieval. It checks the trek_session cookie first, then falls back to the Authorization: Bearer … header.

// server/src/middleware/auth.ts
export function extractToken(req: Request): string | null {
  const cookieToken = (req as any).cookies?.trek_session;
  if (cookieToken) return cookieToken;
  
  const authHeader = req.headers['authorization'];
  return (authHeader && authHeader.split(' ')[1]) || null;
}

This dual-source approach allows API clients to use Bearer tokens while web browsers automatically send the httpOnly cookie.

JWT Verification and Password Version Invalidation

The verifyJwtAndLoadUser function handles signature validation, expiration checks, and session invalidation upon password changes. It performs three critical validations:

  1. Signature verification using JWT_SECRET (auto-generated on first start-up and stored in data/.jwt_secret)
  2. Expiration checking against the exp claim generated from SESSION_DURATION_SECONDS
  3. Password-version gate comparing the token's pv claim against users.password_version in the database
// server/src/middleware/auth.ts
export function verifyJwtAndLoadUser(token: string): User | null {
  try {
    const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] })
      as { id: number; pv?: number; purpose?: string };
    
    if (decoded.purpose) return null; // Reject purpose-scoped tokens

    const row = db.prepare(
      'SELECT id, username, email, role, password_version FROM users WHERE id = ?'
    ).get(decoded.id) as (User & { password_version?: number }) | undefined;

    if (!row) return null;
    
    const tokenPv = typeof decoded.pv === 'number' ? decoded.pv : 0;
    const currentPv = typeof row.password_version === 'number' ? row.password_version : 0;
    
    if (tokenPv !== currentPv) return null; // Stale token after password change

    const { password_version: _pv, ...user } = row;
    return user as User;
  } catch {
    return null;
  }
}

When a user resets their password, the application increments password_version in the database. All existing JWTs immediately fail the pv check, effectively logging the user out of all sessions.

NestJS Guards for Route Protection

The JwtAuthGuard in server/src/nest/auth/jwt-auth.guard.ts integrates the middleware logic into NestJS's decorator-based routing system. It calls extractToken and verifyJwtAndLoadUser, attaching the validated user to the request object.

// server/src/nest/auth/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest<Request>();
    const token = extractToken(req);
    
    if (!token) {
      throw new HttpException(
        { error: 'Access token required', code: 'AUTH_REQUIRED' }, 
        401
      );
    }
    
    const user = verifyJwtAndLoadUser(token);
    if (!user) {
      throw new HttpException(
        { error: 'Invalid or expired token', code: 'AUTH_REQUIRED' }, 
        401
      );
    }
    
    req.user = user;
    return true;
  }
}

Controllers protect routes by applying the guard:

@UseGuards(JwtAuthGuard)
@Get('trips')
async listTrips(@Req() req: Request) {
  // req.user is guaranteed to be valid and non-stale
  return this.tripService.getAllForUser(req.user.id);
}

Configuring Session Duration

All session length logic resides in server/src/config.ts, ensuring the JWT expiration and cookie maxAge remain synchronized. The configuration exports three time-related constants for both standard and "remember me" sessions:

  • SESSION_DURATION – Human-readable string (e.g., "24h")
  • SESSION_DURATION_MS – Milliseconds for cookie maxAge
  • SESSION_DURATION_SECONDS – Seconds for JWT expiresIn

The parseDurationMs utility converts strings like 1h, 7d, or 30d into milliseconds, validating the format at start-up.

// server/src/config.ts
export const SESSION_DURATION = 
  parsedSessionMs == null ? DEFAULT_SESSION_DURATION : rawSessionDuration;
  
export const SESSION_DURATION_MS = 
  parsedSessionMs ?? parseDurationMs(DEFAULT_SESSION_DURATION)!;
  
export const SESSION_DURATION_SECONDS = Math.floor(SESSION_DURATION_MS / 1000);

Environment variables (SESSION_DURATION, SESSION_DURATION_REMEMBER) override defaults on the next server start, applying immediately to newly issued tokens without affecting existing sessions.

Summary

  • Dual transport: TREK accepts JWTs via trek_session cookie or Authorization: Bearer header through extractToken in server/src/middleware/auth.ts.
  • Unified verification: verifyJwtAndLoadUser validates signatures against JWT_SECRET, checks expiration via SESSION_DURATION_SECONDS, and invalidates tokens via the pv (password version) claim.
  • NestJS integration: The JwtAuthGuard wraps middleware logic into a reusable guard for controller decorators.
  • Synchronized durations: config.ts derives both cookie maxAge and JWT exp from the same source, supporting standard and "remember me" session lengths.
  • Global invalidation: Incrementing password_version in the database instantly rejects all existing JWTs for that user.

Frequently Asked Questions

How does TREK handle session expiration?

TREK generates the JWT exp claim and cookie maxAge from the same SESSION_DURATION_SECONDS constant defined in server/src/config.ts. When the token expires or the cookie ages out, verifyJwtAndLoadUser returns null, triggering a 401 response from the JwtAuthGuard.

What happens to existing sessions when a user changes their password?

All sessions are immediately invalidated. The password reset flow increments the password_version column in the database. Since each JWT contains a pv claim set at creation time, verifyJwtAndLoadUser detects the mismatch and rejects the token, forcing re-authentication.

Can API clients use headers instead of cookies?

Yes. The extractToken function checks for the trek_session cookie first, then falls back to the Authorization: Bearer <token> header. This allows mobile applications and third-party clients to authenticate without cookie-based session management.

How do I configure longer "Remember me" sessions?

Set the SESSION_DURATION_REMEMBER environment variable (e.g., 30d) before starting the server. When setAuthCookie receives the remember=true flag, it uses SESSION_DURATION_REMEMBER_MS for the cookie maxAge and SESSION_DURATION_REMEMBER_SECONDS for the JWT expiration, extending the session duration accordingly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →