Guards e autenticação JWT no NestJS
Crie login com JWT, proteja rotas com AuthGuard e entenda respostas 401 no NestJS 11, sem colocar chave secreta ou token real no código.
Um guard no NestJS decide se uma requisição pode chegar à rota. Nesta lição,
você vai criar um login de demonstração, emitir um JWT, proteger GET /auth/perfil e confirmar os caminhos 401 e 200 sem expor a chave ou o token.
O projeto já deve ter o ValidationPipe da lição de
DTO e validação. Aqui o foco é o fluxo de
autenticação; o usuário fictício existe apenas na memória do laboratório.
A credencial emitida na recepção e lida na porta
Imagine um espaço de coworking. Na recepção, a pessoa confirma seu cadastro e recebe um crachá com validade. Na porta de uma sala, um leitor confere o crachá antes de liberar a entrada. A sala não refaz o cadastro; ela confia na decisão do leitor.
No mapa técnico, POST /auth/login é a recepção, o JWT é o crachá assinado,
AuthGuard é o leitor e o controller protegido é a sala. Autenticação é
confirmar a identidade. Autorização é decidir o que essa identidade pode
fazer.
O limite da comparação é essencial: um JWT assinado não é uma identidade física nem uma sessão impossível de revogar. Ele comprova que o payload não foi alterado e que ainda está no prazo, desde que a chave e o algoritmo estejam corretos. Conta bloqueada, troca de senha e permissão atual podem exigir consulta adicional.
Instale o módulo JWT e gere os arquivos
Fixe a versão executada nesta lição:
npm install @nestjs/jwt@11.0.2
npx nest generate module auth
npx nest generate controller auth --no-spec
npx nest generate service auth --no-specCrie também src/auth/auth.guard.ts e src/auth/auth.types.ts. O CLI reduz
digitação, mas o desenho da autenticação continua sendo nossa responsabilidade.
Configure o JWT sem publicar a chave
Edite src/auth/auth.module.ts:
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error('JWT_SECRET não definida');
}
@Module({
imports: [
JwtModule.register({
global: true,
secret: jwtSecret,
signOptions: { expiresIn: '15m' },
}),
],
controllers: [AuthController],
providers: [AuthService, AuthGuard],
exports: [AuthGuard],
})
export class AuthModule {}secret assina e verifica os tokens. O ! para fingir que a variável existe
seria apenas silêncio para o TypeScript; a verificação explícita impede iniciar
uma aplicação quebrada. Sem a variável, o teste parou assim:
Em um projeto maior, ConfigModule pode validar todas as variáveis juntas. O
princípio permanece: falhe no bootstrap, não na primeira requisição de um
cliente.
Emita um token num login de laboratório
O service abaixo usa uma credencial fictícia e pública apenas para observar o JWT. Nunca copie a comparação literal para usuários reais.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async entrar(usuario: string, senha: string) {
if (usuario !== 'mentor' || senha !== 'senha-da-aula') {
throw new UnauthorizedException('credenciais inválidas');
}
return {
access_token: await this.jwtService.signAsync({
sub: 7,
usuario,
}),
};
}
}sub é o subject: um identificador estável do usuário. O payload não recebe a
senha. JWT assinado pode ser decodificado por quem tem o token, então coloque
somente os dados mínimos necessários.
Em produção, busque o usuário no banco e compare a senha com hash. A lição de hash de senha com bcrypt mostra essa etapa; rate limiting, recuperação segura e segundo fator também não cabem dentro do guard.
Exponha login e perfil
O controller oferece uma rota pública para obter token e uma rota protegida para ler o usuário verificado:
import {
Body,
Controller,
Get,
HttpCode,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
import type { RequestAutenticada } from './auth.types';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@HttpCode(200)
@Post('login')
entrar(@Body() body: { usuario: string; senha: string }) {
return this.authService.entrar(body.usuario, body.senha);
}
@UseGuards(AuthGuard)
@Get('perfil')
perfil(@Req() request: RequestAutenticada) {
return request.user;
}
}@UseGuards(AuthGuard) associa a decisão à rota. @HttpCode(200) altera o
status padrão do POST, que seria 201, porque o login não criou um recurso de
domínio.
Para o TypeScript conhecer request.user, declare o formato:
import type { Request } from 'express';
export type UsuarioAutenticado = {
sub: number;
usuario: string;
iat: number;
exp: number;
};
export type RequestAutenticada = Request & {
user?: UsuarioAutenticado;
};iat registra quando o token foi emitido e exp quando expira. Ambos são
timestamps em segundos. Eles serão acrescentados pelo pacote ao assinar.
O guard lê Bearer, verifica e anexa o usuário
Crie src/auth/auth.guard.ts:
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import type { RequestAutenticada, UsuarioAutenticado } from './auth.types';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<RequestAutenticada>();
const [type, token] = request.headers.authorization?.split(' ') ?? [];
if (type !== 'Bearer' || !token) {
throw new UnauthorizedException('token ausente ou inválido');
}
try {
request.user =
await this.jwtService.verifyAsync<UsuarioAutenticado>(token);
return true;
} catch {
throw new UnauthorizedException('token ausente ou inválido');
}
}
}ExecutionContext dá acesso ao contexto atual. Como esta aplicação usa HTTP,
switchToHttp() entrega a requisição. O padrão do cabeçalho é
Authorization: Bearer TOKEN; o guard separa as duas partes, verifica assinatura
e prazo e só então devolve true.
Usar a mesma mensagem para token ausente, alterado ou expirado evita revelar detalhes desnecessários. O servidor pode registrar uma categoria interna, sem salvar o token completo.
Importe AuthModule onde o guard será usado
Para proteger também a criação de mentorias, importe o módulo e aplique o guard:
@Module({
imports: [AuthModule],
controllers: [MentoriasController],
providers: [MentoriasService],
})
export class MentoriasModule {}@UseGuards(AuthGuard)
@Post()
criar(@Body() dto: CreateMentoriaDto) {
return this.mentoriasService.criar(dto.titulo, dto.vagas);
}AuthModule exporta AuthGuard; MentoriasModule importa essa interface
pública. Esse é o mesmo caminho de providers, exports e imports estudado
na lição anterior.
Inicie com um segredo efêmero
Gere uma chave apenas para o processo atual, sem mostrá-la no terminal:
JWT_SECRET="$(openssl rand -hex 32)" npm run startAo encerrar o processo, a chave some. Tokens antigos deixam de ser aceitos na próxima chave, o que é adequado ao laboratório. Em produção, rotação precisa ser planejada para não derrubar sessões sem intenção.
Erro real: rota protegida sem token
Consulte o perfil sem Authorization:
curl -i http://localhost:3000/auth/perfil{“message”:“token ausente ou inválido”,“error”:“Unauthorized”,“statusCode”:401}
O controller de perfil não executou. O guard interrompeu a requisição antes. Um login com a senha fictícia errada produz outro 401 controlado:
curl -i -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{"usuario":"mentor","senha":"errada"}'{“message”:“credenciais inválidas”,“error”:“Unauthorized”,“statusCode”:401}
As duas respostas têm o mesmo status por motivos relacionados à autenticação, mas em pontos diferentes: uma falhou ao provar credenciais no login; a outra não apresentou uma credencial de acesso válida.
Confirme o token sem imprimi-lo
Faça o login e use jq para comprovar que o token existe e tem três segmentos,
sem despejar seu conteúdo:
LOGIN_JSON=$(curl -s -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{"usuario":"mentor","senha":"senha-da-aula"}')
printf '%s\n' "$LOGIN_JSON" | jq \
'{temToken: (.access_token | type == "string"), segmentos: (.access_token | split(".") | length)}'Guarde o valor numa variável temporária e chame o perfil. O filtro remove os timestamps variáveis da saída:
SESSION_TOKEN=$(printf '%s\n' "$LOGIN_JSON" | jq -r .access_token)
curl -s http://localhost:3000/auth/perfil \
-H "Authorization: Bearer ${SESSION_TOKEN}" \
| jq '{sub, usuario}'O token nunca apareceu no histórico impresso. A assinatura foi verificada, e o payload ficou disponível para as próximas decisões.
Missão: diferencie autenticação de autorização
Acrescente papel: 'instrutor' ao payload do usuário fictício. Depois crie um
segundo guard que devolve 403 quando request.user.papel !== 'instrutor' e use-o
no POST de mentorias depois do AuthGuard.
curl -i -X POST http://localhost:3000/mentorias \
-H 'Content-Type: application/json' \
-d '{"titulo":"Nest com segurança","vagas":6}'
curl -i -X POST http://localhost:3000/mentorias \
-H "Authorization: Bearer ${SESSION_TOKEN}" \
-H 'Content-Type: application/json' \
-d '{"titulo":"Nest com segurança","vagas":6}'O critério de sucesso é: sem token retorna 401; token válido com papel errado retorna 403; token de instrutor retorna 201. Não logue a chave nem o token para provar o teste. A lição de autenticação JWT no Node aprofunda validade e assinatura; o próximo passo do cluster conecta a rota protegida ao banco com Prisma no NestJS. Você pode voltar ao guia de NestJS para revisar o caminho completo.
Perguntas frequentes
O que é um guard no NestJS?
Qual a diferença entre 401 e 403?
JWT criptografa os dados do payload?
Posso guardar JWT_SECRET no código?
Guard substitui hash de senha?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, NestJS 11.2.1, @nestjs/jwt 11.0.2 e TypeScript 5.9.3, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- NestJS — autenticação — docs.nestjs.com
- NestJS — guards — docs.nestjs.com
- NestJS — autorização — docs.nestjs.com
- npm — pacote @nestjs/jwt — npmjs.com


