Login with a JWT
An API keeps no sessions: each request must prove by itself who sent it. For that, at login the server issues a JWT (JSON Web Token) — a string that contains the user's id and is signed with the server's secret key. The client sends the token in a header with every request, and a guard checks the signature. Without the key a token cannot be forged.
Installation and the module
npm i @nestjs/jwt
nest g module auth
nest g service auth
nest g controller auth// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
imports: [
UsersModule,
JwtModule.registerAsync({
global: true,
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.getOrThrow<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}The secret comes from .env (the JWT_SECRET variable from the configuration lesson), and the token is valid for one hour. global: true makes JwtService available in every module.
Issuing a token
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async login(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new UnauthorizedException('Wrong email or password');
}
const payload = { sub: user.id, email: user.email, role: user.role };
return { accessToken: await this.jwtService.signAsync(payload) };
}
}payload is the data that goes into the token. By convention the sub (subject) field holds the user id. Never put the password in a token: anyone can read a JWT's contents.
// src/auth/auth.controller.ts
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@HttpCode(200)
login(@Body() dto: LoginDto) {
return this.authService.login(dto.email, dto.password);
}
}LoginDto has the fields email (@IsEmail()) and password (@IsString()). A POST /auth/login request with correct data returns {"accessToken":"eyJhbGciOi..."}.
A guard that checks the token
// src/auth/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const [type, token] = request.headers.authorization?.split(' ') ?? [];
if (type !== 'Bearer' || !token) {
throw new UnauthorizedException();
}
try {
request['user'] = await this.jwtService.verifyAsync(token);
} catch {
throw new UnauthorizedException();
}
return true;
}
}The token arrives in the Authorization: Bearer <token> header. verifyAsync() checks the signature and expiry and returns the payload, which the guard stores in request.user.
Protecting a route
Put @UseGuards(AuthGuard) above the create() method in PostsController instead of the API key guard. Now only a logged-in user can create a post:
curl -X POST http://localhost:3000/posts \
-H "Authorization: Bearer eyJhbGciOi..." \
-H "Content-Type: application/json" \
-d '{"title":"With a token","content":"It works"}'Without the header or with an expired token you get 401.
Common mistake
The JWT secret is written right in the code, for example secret: 'secret', and ends up in the repository. Anyone who reads it can issue tokens in an administrator's name. Keep the secret only in an environment variable, make it long and random, and different on the developer's computer and on the server.