Skip to content

JWT Strategy

Passport strategy for validating JWT tokens and extracting user information.

Overview

The JWT Strategy is a Passport strategy that validates JWT access tokens and loads the authenticated user. It's automatically invoked by the JwtAuthGuard on protected routes.

Implementation

File: server/src/auth/strategies/jwt.strategy.ts

typescript
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private configService: ConfigService,
    private usersService: UsersService,
  ) {
    const jwtSecret = configService.get<string>('JWT_SECRET', AUTH.JWT_SECRET);
    if (!jwtSecret) {
      throw new Error('JWT_SECRET is not defined in environment variables');
    }

    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtSecret,
    });
  }

  async validate(payload: JwtPayload) {
    const user = await this.usersService.getUserById(payload.sub);
    if (!user) {
      throw new UnauthorizedException('User not found');
    }
    if (!user.isActive) {
      throw new UnauthorizedException('User account is deactivated');
    }
    return user;
  }
}

Configuration

Token Extraction

typescript
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()

Extracts JWT from the Authorization header:

http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Expiration Handling

typescript
ignoreExpiration: false
  • Expired tokens are automatically rejected
  • Returns 401 Unauthorized for expired tokens
  • Client must refresh token using /auth/refresh endpoint

Secret Key

typescript
secretOrKey: configService.get<string>('JWT_SECRET')
  • Loaded from environment variable JWT_SECRET
  • Falls back to AUTH.JWT_SECRET constant if not set
  • Throws error if secret is undefined (security check)

JWT Payload

Structure

typescript
export interface JwtPayload {
  sub: string;      // Subject (user ID)
  email: string;    // User email
  role: string;     // User role ('user' or 'admin')
  iat?: number;     // Issued at (timestamp)
  exp?: number;     // Expiration (timestamp)
}

Example Payload

json
{
  "sub": "507f1f77bcf86cd799439011",
  "email": "user@example.com",
  "role": "user",
  "iat": 1704067200,
  "exp": 1704068100
}

Validation Process

Flow

1. Extract token from Authorization header

2. Verify token signature with JWT_SECRET

3. Check token expiration

4. Extract payload (sub, email, role)

5. Load user from database by ID (payload.sub)

6. Check if user exists

7. Check if user is active

8. Return user object (attached to request)

Validation Method

typescript
async validate(payload: JwtPayload) {
  // 1. Load user from database
  const user = await this.usersService.getUserById(payload.sub);
  
  // 2. Check user exists
  if (!user) {
    throw new UnauthorizedException('User not found');
  }
  
  // 3. Check user is active
  if (!user.isActive) {
    throw new UnauthorizedException('User account is deactivated');
  }
  
  // 4. Return user (attached to request.user)
  return user;
}

Error Handling

Common Errors

ErrorCauseHTTP Status
User not foundUser ID in token doesn't exist401
User account is deactivatedUser.isActive = false401
jwt malformedInvalid token format401
jwt expiredToken past expiration time401
invalid signatureToken signed with wrong secret401
No auth tokenMissing Authorization header401

Error Responses

json
{
  "statusCode": 401,
  "message": "User not found",
  "error": "Unauthorized"
}
json
{
  "statusCode": 401,
  "message": "jwt expired",
  "error": "Unauthorized"
}

Usage in Guards

JWT Auth Guard

The strategy is used by JwtAuthGuard:

typescript
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  // Uses JwtStrategy automatically
}

Protected Route Example

typescript
@Controller('users')
@UseGuards(JwtAuthGuard)  // Invokes JwtStrategy
export class UsersController {
  @Get('profile')
  getProfile(@CurrentUser() user: UserDocument) {
    // user is populated by JwtStrategy.validate()
    return user;
  }
}

Request Flow

Successful Authentication

Client Request
├── Header: Authorization: Bearer <token>

├── JwtAuthGuard intercepts
│   ├── Calls JwtStrategy
│   │   ├── Extracts token from header
│   │   ├── Verifies signature
│   │   ├── Checks expiration
│   │   ├── Loads user from database
│   │   ├── Validates user exists and is active
│   │   └── Returns user object
│   │
│   └── Attaches user to request.user

└── Controller method executes
    └── Access user via @CurrentUser() decorator

Failed Authentication

Client Request
├── Header: Authorization: Bearer <invalid_token>

├── JwtAuthGuard intercepts
│   ├── Calls JwtStrategy
│   │   ├── Token verification fails
│   │   └── Throws UnauthorizedException
│   │
│   └── Returns 401 response

└── Controller method NOT executed

Security Features

1. Token Signature Verification

  • Every token is verified with JWT_SECRET
  • Prevents token tampering
  • Invalid signatures are rejected

2. Expiration Check

  • Tokens expire after 15 minutes
  • Expired tokens are automatically rejected
  • Forces regular token refresh

3. User Validation

  • User existence is verified on every request
  • Deleted users can't access API
  • Deactivated accounts are blocked

4. Active Status Check

  • isActive flag prevents deactivated users
  • Immediate access revocation
  • No need to invalidate tokens

Configuration Options

Environment Variables

bash
# Required
JWT_SECRET=your-secret-key-here-min-32-chars

# Optional (defaults provided)
JWT_EXPIRATION=15m

Default Constants

typescript
export const AUTH = {
  JWT_SECRET: 'default-secret-for-development-only',
  JWT_REFRESH_SECRET: 'default-refresh-secret-for-development-only',
  ACCESS_TOKEN_EXPIRY: '15m',
  REFRESH_TOKEN_EXPIRY: '7d',
  BCRYPT_ROUNDS: 12
};

⚠️ Warning: Never use default secrets in production!

Testing

Unit Tests

typescript
describe('JwtStrategy', () => {
  let strategy: JwtStrategy;
  let usersService: UsersService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        JwtStrategy,
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn().mockReturnValue('test-secret')
          }
        },
        {
          provide: UsersService,
          useValue: {
            getUserById: jest.fn()
          }
        }
      ]
    }).compile();

    strategy = module.get<JwtStrategy>(JwtStrategy);
    usersService = module.get<UsersService>(UsersService);
  });

  it('should validate user and return user object', async () => {
    const mockUser = {
      id: '123',
      email: 'test@example.com',
      isActive: true
    };
    
    jest.spyOn(usersService, 'getUserById').mockResolvedValue(mockUser);

    const payload = { sub: '123', email: 'test@example.com', role: 'user' };
    const result = await strategy.validate(payload);

    expect(result).toEqual(mockUser);
  });

  it('should throw UnauthorizedException if user not found', async () => {
    jest.spyOn(usersService, 'getUserById').mockResolvedValue(null);

    const payload = { sub: '123', email: 'test@example.com', role: 'user' };
    
    await expect(strategy.validate(payload)).rejects.toThrow(
      UnauthorizedException
    );
  });

  it('should throw UnauthorizedException if user is inactive', async () => {
    const mockUser = {
      id: '123',
      email: 'test@example.com',
      isActive: false
    };
    
    jest.spyOn(usersService, 'getUserById').mockResolvedValue(mockUser);

    const payload = { sub: '123', email: 'test@example.com', role: 'user' };
    
    await expect(strategy.validate(payload)).rejects.toThrow(
      UnauthorizedException
    );
  });
});

E2E Tests

typescript
describe('JWT Strategy E2E', () => {
  it('should authenticate with valid token', () => {
    return request(app.getHttpServer())
      .get('/users/profile')
      .set('Authorization', `Bearer ${validToken}`)
      .expect(200);
  });

  it('should reject expired token', () => {
    return request(app.getHttpServer())
      .get('/users/profile')
      .set('Authorization', `Bearer ${expiredToken}`)
      .expect(401);
  });

  it('should reject invalid token', () => {
    return request(app.getHttpServer())
      .get('/users/profile')
      .set('Authorization', 'Bearer invalid-token')
      .expect(401);
  });
});

Best Practices

✅ Do

  1. Use strong secrets (32+ random characters)
  2. Rotate secrets periodically in production
  3. Set appropriate expiration (15 minutes for access tokens)
  4. Validate user on every request (check exists and active)
  5. Use environment variables for secrets
  6. Log authentication failures for security monitoring

❌ Don't

  1. Don't use default secrets in production
  2. Don't ignore expiration (security risk)
  3. Don't skip user validation (deleted users could access)
  4. Don't log tokens (security risk)
  5. Don't share secrets across environments
  6. Don't use weak secrets (easy to crack)

Troubleshooting

Token Not Being Extracted

Problem: 401 even with valid token

Solution: Check Authorization header format:

http
✅ Correct: Authorization: Bearer eyJhbGciOiJIUzI1...
❌ Wrong: Authorization: eyJhbGciOiJIUzI1...
❌ Wrong: Bearer eyJhbGciOiJIUzI1...

User Not Found Error

Problem: Token valid but user not found

Causes:

  • User was deleted after token was issued
  • Token contains wrong user ID
  • Database connection issue

Solution: Refresh token or re-login

Account Deactivated Error

Problem: User exists but can't access

Cause: user.isActive = false

Solution: Contact admin to reactivate account

Built with ❤️ for competitive programmers worldwide.