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

> Learn how JWT authentication is implemented in NestJS with TREK. Discover how signed JWTs, secure cookies, and session duration controls enhance backend security. Explore middleware verification and centralized configuration fo...

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: deep-dive
- Published: 2026-06-26

---

**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`](https://github.com/mauriceboe/TREK/blob/main/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.

## Token Creation and Cookie Handling

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`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/cookie.ts)) writes to the response.

The cookie configuration respects the **session-duration** constants defined in [`server/src/config.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/config.ts). If the user selects **"Remember me"**, the function switches to the longer `SESSION_DURATION_REMEMBER*` variants.

```typescript
// 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`](https://github.com/mauriceboe/TREK/blob/main/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.

```typescript
// 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

```typescript
// 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`](https://github.com/mauriceboe/TREK/blob/main/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.

```typescript
// 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:

```typescript
@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`](https://github.com/mauriceboe/TREK/blob/main/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.

```typescript
// 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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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.