Aula 10: Autenticação e autorização no serviço
O serviço da aula 9 está funcionalmente completo e continua completamente aberto: qualquer requisição cadastra, altera ou remove um livro, sem que nada pergunte quem está do outro lado. Esta aula fecha essa lacuna com duas perguntas distintas — quem é você, e o que você pode fazer — e com uma peça que a aula 5 já havia posicionado no pipeline de requisição sem preenchê-la: o guard.
Como no bloco de persistência (aulas 7 a 9), é também um teste de arquitetura. Autenticação e autorização entram como uma camada transversal, aplicada por decorator sobre rotas que já existiam — e não como uma reescrita de controllers e services.
| O que vem depois | Onde |
|---|---|
| Consumo autenticado desta API pelo cliente web | Módulo 3 — Next.js |
| Consumo autenticado da mesma API pelo cliente mobile | Módulo 4 — Flutter |
Objetivos
Ao final desta aula, você deve ser capaz de:
- Distinguir autenticação de autorização, e os status HTTP que cada uma produz quando falha.
- Explicar por que uma senha é fixada com hash, nunca criptografada nem guardada em texto puro, e o que o custo do
bcryptcompra. - Descrever a estrutura de um JWT e por que ela permite autenticação stateless.
- Implementar um fluxo de registro e login que emite um token assinado.
- Implementar um guard de autenticação global, com rotas explicitamente públicas.
- Implementar um guard de autorização por perfil, a partir de metadados de rota.
- Escrever decorators customizados (
@Public,@Roles,@UsuarioAtual) que leem e gravam esses metadados. - Aplicar uma política de acesso coerente sobre os recursos já existentes do serviço.
Ambiente sugerido
O ambiente é o mesmo das aulas 7 a 9, com pacotes novos:
| Pacote | Papel |
|---|---|
@nestjs/jwt | Assinatura e verificação de tokens JWT |
@nestjs/passport | Integração do NestJS com a estratégia Passport |
passport | Biblioteca de autenticação sobre a qual passport-jwt é construída |
passport-jwt | Estratégia Passport que extrai e valida um JWT do cabeçalho Authorization |
bcryptjs | Hash de senha, em JavaScript puro — sem compilação nativa |
bcryptjs, e não bcryptO pacote bcrypt depende de um módulo nativo compilado durante a instalação,
o que costuma exigir ferramentas de build (node-gyp, um compilador C) nem
sempre presentes na máquina de quem está aprendendo. bcryptjs implementa o
mesmo algoritmo em JavaScript puro: instala em qualquer lugar em que o Node
roda, ao custo de ser um pouco mais lento — irrelevante para o volume de
login de um laboratório.
Parte 1 — Autenticação e autorização são perguntas diferentes
| Autenticação | Autorização | |
|---|---|---|
| Pergunta | Quem está fazendo esta requisição? | Esta pessoa pode fazer isto? |
| Falha quando | A identidade não pode ser confirmada | A identidade é conhecida, mas não basta |
| Status HTTP | 401 Unauthorized | 403 Forbidden |
| Depende de | Credenciais (senha, token) | Identidade já confirmada + uma regra de acesso |
A ordem importa: não existe autorização sem autenticação prévia. Por isso o
laboratório implementa as duas como guards separados, nessa ordem, e é também
por isso que confundir os dois status é o erro mais comum desta aula — um
token ausente é 401; um token válido de quem não tem o perfil exigido é
403.
Parte 2 — Onde isso entra no pipeline
O guard, posicionado no pipeline de requisição desde a aula 5, é o lugar exato onde as duas perguntas da Parte 1 são respondidas — antes de qualquer pipe validar o corpo da requisição, e bem antes do handler decidir a regra de negócio. Não faz sentido validar o formato de um corpo que será recusado por falta de autenticação.
Um detalhe de implementação separa as duas perguntas em dois guards distintos, registrados nessa ordem:
JwtAuthGuard resolve autenticação; PapeisGuard resolve autorização. Um
não substitui o outro, e a ordem de registro garante que request.user
exista antes de PapeisGuard precisar lê-lo.
Parte 3 — Duas contas, dois modelos
O acervo já tem Leitor — a pessoa em nome de quem um empréstimo é
registrado. Esta aula acrescenta Usuario — quem tem login no serviço. As
duas coisas são deliberadamente independentes:
// Conta de acesso à API — quem faz login. Deliberadamente SEM relação com
// Leitor: Usuario é quem se autentica no serviço (um bibliotecário no
// balcão, o próprio app agindo em nome de alguém); Leitor é a pessoa em
// nome de quem um empréstimo é registrado. As aulas 5-9 não tinham conta
// nenhuma; esta é a primeira entidade da disciplina que não representa o
// domínio da biblioteca, e sim o controle de acesso ao serviço.
enum Papel {
ADMINISTRADOR
LEITOR
}
model Usuario {
id Int @id @default(autoincrement())
nome String @db.VarChar(120)
email String @unique @db.VarChar(180)
senhaHash String
papel Papel @default(LEITOR)
criadoEm DateTime @default(now())
}
Leitor sem conta e Usuario sem vínculo com Leitor é uma decisão deste
domínio — uma biblioteca em que o balcão empresta em nome de qualquer leitor
cadastrado, autenticado ou não. Outro domínio decidiria diferente: um
e-commerce em que todo cliente é também quem faz login provavelmente uniria
os dois papéis numa única entidade. O padrão de implementação (hash de
senha, JWT, guards) é o mesmo nos dois casos; o modelo de dados, não.
papel tem @default(LEITOR). Essa única linha do schema é a garantia de
que quem se registra sozinho nunca vira administrador — a Parte 6 depende
dela.
Parte 4 — Senha nunca é guardada em texto puro
Duas armadilhas comuns, e por que bcrypt evita as duas:
| Abordagem | Problema |
|---|---|
| Texto puro | Um vazamento do banco expõe a senha de todo mundo, imediatamente |
| Criptografia reversível | Quem tem a chave consegue recuperar a senha original — e a chave também pode vazar |
Hash com bcrypt | Operação de mão única: dá para verificar uma senha, nunca recuperar a original |
import * as bcrypt from 'bcryptjs';
const hash = await bcrypt.hash('umaSenhaForte123', 10);
// $2a$10$N9qo8uLOickgx2ZMRZoMy... — o custo (10) vai embutido no próprio hash
const confere = await bcrypt.compare('umaSenhaForte123', hash);
// true — e é a ÚNICA operação que existe para checar uma senha
O 10 é o fator de custo: cada incremento dobra o tempo de cálculo do
hash. É deliberadamente lento — um ataque de força bruta contra o banco
vazado fica caro na mesma proporção. Baixo demais, o hash fica rápido de
quebrar; alto demais, o login legítimo também fica lento. 10 é razoável
para 2026; a documentação do bcrypt orienta a recalibrar o valor à medida
que o hardware de ataque fica mais barato.
bcrypt.hash já gera o salDiferente de um MD5 ou SHA-256 cru, bcrypt.hash gera um sal
aleatório a cada chamada e o embute no próprio hash resultante — por isso
dois hashes da mesma senha nunca são iguais, e por isso bcrypt.compare
recebe a senha em texto puro e o hash completo, não dois hashes para
comparar por igualdade.
Parte 5 — JWT: um token que carrega sua própria prova
Um JSON Web Token é três blocos separados por ponto, cada um em Base64URL:
cabeçalho.payload.assinatura. O payload é legível por qualquer um que
tenha o token — a assinatura garante que ele não foi alterado, não que ele
esteja em segredo. Nunca coloque senha, ou qualquer dado sensível, dentro de
um payload de JWT.
O que torna isso stateless é justamente a Figura 2: validar um token não
exige nenhuma consulta ao banco nem a um armazenamento de sessão. A mesma
JWT_SECRET usada para assinar é usada para verificar, e a verificação é
puramente matemática. É também a limitação a ter em mente: revogar um
token antes do seu vencimento natural não é trivial — não existe, aqui, uma
tabela de sessões para apagar uma linha. Expirar o token cedo (JWT_EXPIRES_IN)
é o principal controle disponível neste desenho.
Parte 6 — Emitindo o token: AuthService
import { ConflictException, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import { Prisma } from '../generated/prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RegistrarDto } from './dto/registrar.dto';
const CUSTO_HASH = 10;
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
) {}
async registrar(dto: RegistrarDto) {
const senhaHash = await bcrypt.hash(dto.senha, CUSTO_HASH);
try {
const usuario = await this.prisma.usuario.create({
data: { nome: dto.nome, email: dto.email, senhaHash },
});
const { senhaHash: _senhaHash, ...usuarioPublico } = usuario;
return usuarioPublico;
} catch (erro) {
if (erro instanceof Prisma.PrismaClientKnownRequestError && erro.code === 'P2002') {
throw new ConflictException(`Email ${dto.email} já cadastrado`);
}
throw erro;
}
}
async login(dto: LoginDto): Promise<{ accessToken: string }> {
const usuario = await this.prisma.usuario.findUnique({ where: { email: dto.email } });
if (!usuario || !(await bcrypt.compare(dto.senha, usuario.senhaHash))) {
throw new UnauthorizedException('email ou senha inválidos');
}
const payload = { sub: usuario.id, papel: usuario.papel };
return { accessToken: await this.jwtService.signAsync(payload) };
}
}
Três decisões merecem atenção:
registrarnunca recebepapeldo cliente. O campo simplesmente não está emRegistrarDto, e ocreatenão o envia — quem decide é o@default(LEITOR)do schema. Não é uma validação que poderia ser contornada; é um campo que não existe no contrato de entrada.- A resposta do registro nunca inclui
senhaHash, pela mesma técnica de desestruturação com omissão que a aula 6 usou para moldar respostas. - A mesma mensagem de erro serve para email inexistente e senha errada. Diferenciar ("email não encontrado" × "senha incorreta") informaria a um atacante quais emails têm conta — um vazamento pequeno, mas gratuito.
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
export class RegistrarDto {
@ApiProperty({ example: 'Ana Souza', maxLength: 120 })
@IsString()
@IsNotEmpty({ message: 'o nome é obrigatório' })
@MaxLength(120)
nome!: string;
@ApiProperty({ example: 'ana@exemplo.com' })
@IsEmail({}, { message: 'informe um email válido' })
email!: string;
@ApiProperty({ example: 'umaSenhaForte123', minLength: 8 })
@IsString()
@MinLength(8, { message: 'a senha precisa ter pelo menos 8 caracteres' })
senha!: string;
}
LoginDto segue o mesmo padrão, só com email e senha — sem nome, sem
limite mínimo de tamanho (a senha já existe; validar comprimento no login só
vazaria informação sobre a política de senha, sem função nenhuma).
Parte 7 — Validando o token a cada requisição: JwtStrategy
AuthService emite o token; algo precisa validá-lo a cada
requisição protegida. Esse algo é uma strategy do Passport — um adaptador
que o @nestjs/passport sabe como plugar num guard:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Papel } from '../../generated/prisma/client';
import { UsuarioAutenticado } from '../tipos/usuario-autenticado';
interface PayloadJwt {
sub: number;
papel: Papel;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.getOrThrow<string>('JWT_SECRET'),
});
}
validate(payload: PayloadJwt): UsuarioAutenticado {
return { id: payload.sub, papel: payload.papel };
}
}
ignoreExpiration: falseé explícito de propósito: um token vencido deve ser rejeitado, e deixar o padrão implícito convida alguém a "corrigir" isso mais tarde sem entender por quê.validatesó roda se a assinatura e a validade já passaram. O que ele devolve virarequest.user— daqui em diante, o payload bruto do token não aparece mais em lugar nenhum do código; só o formato deUsuarioAutenticado.config.getOrThrowé o mesmo padrão da aula 7 paraDATABASE_URL. SeJWT_SECRETnão estiver definida, a aplicação falha no boot — não na primeira tentativa de login, o que seria muito mais difícil de rastrear.
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/biblioteca?schema=public"
# Só para desenvolvimento. Em produção, um segredo gerado (por exemplo,
# openssl rand -base64 48) e nunca commitado.
JWT_SECRET="segredo-de-desenvolvimento-troque-em-producao"
JWT_EXPIRES_IN="1h"
Parte 8 — Guards e decorators: quem entra, e com que papel
@Public() — a exceção explícita à regra
O padrão desta aula é autenticação por padrão: toda rota exige token, exceto a que declarar o contrário.
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
SetMetadata anexa um par chave-valor ao handler (ou à classe) decorado, do
mesmo jeito que a aula 3 explicou para decorators em geral. JwtAuthGuard lê
essa chave antes de decidir se cobra um token.
JwtAuthGuard — autenticação
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}
canActivate(contexto: ExecutionContext) {
const ehPublica = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
contexto.getHandler(),
contexto.getClass(),
]);
if (ehPublica) {
return true;
}
return super.canActivate(contexto);
}
}
extends AuthGuard('jwt') herda toda a integração com a JwtStrategy da
Parte 7 — inclusive a resposta 401 automática quando o token falta, está
malformado ou expirou. canActivate sobrescreve só o necessário: ler o
metadado de @Public() antes de delegar ao Passport. getAllAndOverride
olha primeiro no método, depois na classe — uma rota pública dentro de um
controller inteiramente protegido continua funcionando.
@Roles(...) e PapeisGuard — autorização
import { SetMetadata } from '@nestjs/common';
import { Papel } from '../../generated/prisma/client';
export const PAPEIS_KEY = 'papeis';
export const Roles = (...papeis: Papel[]) => SetMetadata(PAPEIS_KEY, papeis);
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Papel } from '../../generated/prisma/client';
import { PAPEIS_KEY } from '../decorators/roles.decorator';
import type { RequisicaoAutenticada } from '../tipos/usuario-autenticado';
@Injectable()
export class PapeisGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(contexto: ExecutionContext): boolean {
const papeisNecessarios = this.reflector.getAllAndOverride<Papel[]>(PAPEIS_KEY, [
contexto.getHandler(),
contexto.getClass(),
]);
if (!papeisNecessarios || papeisNecessarios.length === 0) {
return true;
}
const requisicao = contexto.switchToHttp().getRequest<RequisicaoAutenticada>();
const usuario = requisicao.user;
return !!usuario && papeisNecessarios.includes(usuario.papel);
}
}
Autorização é opt-in sobre autenticação: sem @Roles(...) na rota, o
guard devolve true e deixa passar qualquer autenticado — a regra de quem
pode fazer o quê é decidida rota a rota, nunca por padrão implícito.
Devolver false já faz o NestJS responder 403 sozinho; o guard nunca
lança a exceção na mão.
@UsuarioAtual() — lendo quem está autenticado
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import type { RequisicaoAutenticada, UsuarioAutenticado } from '../tipos/usuario-autenticado';
export const UsuarioAtual = createParamDecorator(
(_dado: unknown, contexto: ExecutionContext): UsuarioAutenticado | undefined => {
const requisicao = contexto.switchToHttp().getRequest<RequisicaoAutenticada>();
return requisicao.user;
},
);
Mesma família de @Param() e @Query(), que a aula 5 já apresentou:
createParamDecorator extrai um valor da requisição para injetar direto no
parâmetro do handler, sem o controller nunca chamar request explicitamente.
Registrando os dois guards, globalmente e na ordem certa
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { JwtModule, type JwtModuleOptions, type JwtSignOptions } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './estrategias/jwt.strategy';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { PapeisGuard } from './guards/papeis.guard';
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService): JwtModuleOptions => ({
secret: config.getOrThrow<string>('JWT_SECRET'),
signOptions: {
expiresIn: config.get<string>('JWT_EXPIRES_IN', '1h') as JwtSignOptions['expiresIn'],
},
}),
}),
],
controllers: [AuthController],
providers: [
AuthService,
JwtStrategy,
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: PapeisGuard },
],
})
export class AuthModule {}
APP_GUARD registra um guard globalmente, para toda rota da aplicação,
sem precisar decorar cada controller com @UseGuards(...). A ordem do array
é a ordem de execução: JwtAuthGuard sempre roda antes de PapeisGuard, o
que garante que request.user já existe quando o segundo guard precisa
lê-lo — é exatamente o fluxo da Figura 1.
Um guard misturando "o token é válido" com "o papel é permitido" funcionaria,
mas devolveria sempre 403, mesmo para quem simplesmente esqueceu o
cabeçalho Authorization — perdendo a distinção da Parte 1. Separar em dois
guards de responsabilidade única também deixa cada um testável e reutilizável
sozinho: um endpoint que precisar só de autenticação, sem papel nenhum,
simplesmente não usa @Roles() — o guard já está registrado, pronto para
esse caso.
Parte 9 — Protegendo os recursos que já existiam
A política final desta aula, por controller:
| Controller | Rota | Política |
|---|---|---|
AuthController | POST /auth/registro | @Public() — é como alguém consegue a primeira conta |
AuthController | POST /auth/login | @Public() — mesma razão |
AuthController | GET /auth/perfil | Autenticado, qualquer papel |
LivrosController | GET /livros, GET /livros/resumo, GET /livros/:id | @Public() — o catálogo é de consulta aberta |
LivrosController | POST /livros, PATCH /livros/:id, DELETE /livros/:id | @Roles(Papel.ADMINISTRADOR) |
EmprestimosController | Todas | Autenticado, qualquer papel — sem @Roles() |
LivrosController, nas rotas de escrita:
// A partir daqui, mudar o acervo é trabalho de bibliotecário: as três
// rotas de escrita exigem o perfil ADMINISTRADOR. Sem @Public() e sem
// @Roles(), a rota ficaria aberta a qualquer autenticado — @Roles()
// aqui é o que a torna exclusiva.
@Roles(Papel.ADMINISTRADOR)
@ApiBearerAuth()
@Post()
@ApiUnauthorizedResponse({ description: 'Token ausente, inválido ou expirado' })
@ApiForbiddenResponse({ description: 'Autenticado, mas sem perfil ADMINISTRADOR' })
criar(@Body() dto: CriarLivroDto) {
return this.livrosService.criar(dto);
}
EmprestimosController, sem @Roles() em rota nenhuma:
// Sem @Public() em nenhuma rota, e sem @Roles() em nenhuma: o controller
// inteiro exige só autenticação, aberta aos dois perfis. É a operação de
// balcão — ADMINISTRADOR e LEITOR usam a mesma rota, e o que a diferenciaria
// (quem pode emprestar em nome de quem) fica fora do escopo desta aula.
@ApiTags('emprestimos')
@ApiBearerAuth()
@Controller('emprestimos')
export class EmprestimosController {
E AuthController, expondo GET /auth/perfil com @UsuarioAtual():
@Get('perfil')
@ApiBearerAuth()
@ApiOkResponse({ description: 'Dados do usuário autenticado' })
@ApiUnauthorizedResponse({ description: 'Token ausente, inválido ou expirado' })
perfil(@UsuarioAtual() usuario: UsuarioAutenticado) {
return usuario;
}
Nada no NestJS obriga GET /livros a ser público. A decisão — consultar o
acervo não exige conta, alterá-lo exige — é do domínio, do mesmo jeito que a
aula 8 tratou onDelete: Restrict × onDelete: Cascade como decisão, não
como padrão técnico automático. Um catálogo interno de empresa provavelmente
decidiria o oposto nas três rotas de leitura.
Parte 10 — O cadeado na documentação
O Swagger UI da aula 6 ganha um botão a mais:
const config = new DocumentBuilder()
.setTitle('API da Biblioteca')
.setDescription('Serviço de acervo consumido pelos clientes web e mobile')
.setVersion('1.0.0')
.addTag('livros')
// Habilita o botão "Authorize" no Swagger UI: cole o accessToken do
// POST /auth/login ali uma vez, e o "Try it out" das rotas protegidas
// passa a enviar o cabeçalho Authorization sozinho.
.addBearerAuth()
.build();
Com @ApiBearerAuth() nas rotas protegidas e .addBearerAuth() no
documento, cada uma delas ganha um ícone de cadeado em /docs — e o botão
Authorize, no topo da página, permite testar toda a documentação
autenticada sem copiar o token a cada chamada.
Erros comuns
| Erro | Sintoma | Correção |
|---|---|---|
Confundir 401 com 403 no código ou na leitura do teste | Debug na rota errada | 401 = sem identidade confirmada; 403 = identidade confirmada, sem permissão |
Esquecer @Public() numa rota que deveria ser aberta | Cliente legítimo recebe 401 sem token nenhum | Adicionar @Public() |
Registrar PapeisGuard antes de JwtAuthGuard | request.user sempre undefined, 403 mesmo autenticado | Respeitar a ordem no array de providers |
JWT_SECRET ausente do .env | Aplicação não sobe | Criar a variável antes de rodar start:dev |
Aceitar papel vindo do corpo de POST /auth/registro | Qualquer um se cadastra como ADMINISTRADOR | Nunca incluir papel no DTO de registro |
Guardar a senha sem hash, ou com MD5/SHA-256 cru | Vazamento do banco expõe a senha de todo mundo | Usar bcrypt.hash, nunca hash sem sal |
Comparar senha com === contra senhaHash | Login sempre falha (a senha nunca é igual ao hash) | bcrypt.compare(senha, senhaHash) |
| Mensagens diferentes para "email não existe" e "senha errada" | Vaza quais emails têm conta | Uma única mensagem para os dois casos |
| Colocar dado sensível no payload do JWT | Vaza para quem só tem o token, sem precisar decifrar nada | Payload não é segredo — trate como texto público |
Token sem expiresIn | Um token vazado nunca perde validade | Sempre definir expiração |
Laboratório 10 — Login, token e guards de verdade
Continuação direta do laboratório 9. Todos os passos partem do projeto no estado em que a aula 9 o deixou.
Passo 1 — Instalar as dependências
npm i @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs
npm i -D @types/passport-jwt
bcryptjs já publica seus próprios tipos — não instale @types/bcryptjs.
Passo 2 — Configurar o .env
JWT_SECRET="segredo-de-desenvolvimento-troque-em-producao"
JWT_EXPIRES_IN="1h"
Em qualquer projeto além de um laboratório, gere o segredo (openssl rand -base64 48) e nunca o versione.
Passo 3 — Modelar Usuario e Papel
Acrescente ao schema.prisma o trecho da Parte 3, depois:
npx prisma migrate dev --name criar_usuario
npx prisma generate
Passo 4 — Criar os DTOs
src/auth/dto/registrar.dto.ts e src/auth/dto/login.dto.ts, conforme a
Parte 6. Confirme que RegistrarDto não tem campo papel.
Passo 5 — Gerar o módulo de autenticação
nest g module auth
nest g controller auth --no-spec
nest g service auth --no-spec
Crie também src/auth/tipos/usuario-autenticado.ts:
import type { Request } from 'express';
import type { Papel } from '../../generated/prisma/client';
export interface UsuarioAutenticado {
id: number;
papel: Papel;
}
export interface RequisicaoAutenticada extends Request {
user?: UsuarioAutenticado;
}
Passo 6 — Implementar AuthService
Em praticar/auth.service.ts, complete os TODO 6a a 6e e implemente
registrar e login conforme a Parte 6. Preste atenção especial ao TODO 6d: não envie papel no create.
Passo 7 — Implementar AuthController
Três rotas: POST /auth/registro e POST /auth/login com @Public(),
GET /auth/perfil sem — conforme a Parte 9.
Passo 8 — Criar @Public() e @Roles()
src/auth/decorators/public.decorator.ts e
src/auth/decorators/roles.decorator.ts, conforme a Parte 8.
Passo 9 — Criar @UsuarioAtual()
src/auth/decorators/usuario-atual.decorator.ts, conforme a Parte 8.
Passo 10 — Implementar JwtStrategy
src/auth/estrategias/jwt.strategy.ts, conforme a Parte 7.
Passo 11 — Implementar JwtAuthGuard
Complete os TODO 10a a 10c em praticar/jwt-auth.guard.ts, ou implemente
diretamente conforme a Parte 8. O ponto que mais derruba nesta etapa: chamar
super() no construtor antes de qualquer outra coisa — sem isso, o
AuthGuard('jwt') herdado não tem a JwtStrategy para delegar.
Passo 12 — Implementar PapeisGuard
Complete os TODO 11a a 11c em praticar/papeis.guard.ts, ou implemente
diretamente conforme a Parte 8. Lembrete do próprio arquivo: devolver
false já produz 403 sozinho — não lance a exceção na mão.
Passo 13 — Registrar tudo no AuthModule, e o AuthModule no AppModule
src/auth/auth.module.ts conforme a Parte 8 — reveja a ordem dos dois
APP_GUARD antes de seguir. Depois, importe AuthModule em
src/app.module.ts, junto de PrismaModule e LivrosModule.
Passo 14 — Proteger LivrosController e EmprestimosController
Aplique a tabela da Parte 9: @Public() nas três rotas de leitura de
livros, @Roles(Papel.ADMINISTRADOR) nas três de escrita, e nenhum dos dois
decorators em EmprestimosController.
Depois deste passo, abra livros.service.ts e emprestimos.service.ts e
confirme que nenhum dos dois mudou. Autenticação e autorização são
transversais ao transporte — se um service precisou mudar para "saber" quem
está logado, alguma coisa vazou da camada errada.
Passo 15 — Popular contas de teste e testar de ponta a ponta
Acrescente ao seed.ts as duas contas de teste:
const [senhaAdmin, senhaLeitor] = await Promise.all([
bcrypt.hash('admin123', 10),
bcrypt.hash('leitor123', 10),
]);
await prisma.usuario.createMany({
data: [
{ nome: 'Biblioteca (administração)', email: 'admin@biblioteca.com', senhaHash: senhaAdmin, papel: 'ADMINISTRADOR' },
{ nome: 'Ana Souza', email: 'ana@exemplo.com', senhaHash: senhaLeitor, papel: 'LEITOR' },
],
});
npx prisma db seed
npm run start:dev
Rode, na ordem, os cenários equivalentes a estes (o arquivo do projeto de referência encadeia tudo com captura de variável, sem colar token à mão a cada chamada):
# 1. catálogo sem token — 200
curl -i http://localhost:3000/livros
# 2. cadastrar SEM token — 401
curl -i -X POST http://localhost:3000/livros \
-H 'Content-Type: application/json' \
-d '{"titulo":"Livro sem dono","isbn":"9788571643444","ano":2020,"autorId":1}'
# 3. login como LEITOR
curl -s -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"ana@exemplo.com","senha":"leitor123"}'
# 4. cadastrar com token de LEITOR — 403
curl -i -X POST http://localhost:3000/livros \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token-do-passo-3>' \
-d '{"titulo":"Livro que o leitor não pode criar","isbn":"9788571643451","ano":2021,"autorId":1}'
# 5. login como ADMINISTRADOR, repetir o cadastro — 201
curl -s -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@biblioteca.com","senha":"admin123"}'
Confirme os cinco status antes de seguir: 200, 401, 200 (token
emitido), 403, 201.
Critérios de conclusão
-
GET /livros,GET /livros/resumoeGET /livros/:idrespondem sem token; -
POST /livrossem token responde401; -
POST /livroscom token de contaLEITORresponde403; -
POST /livroscom token de contaADMINISTRADORresponde201; -
POST /auth/registrocria sempre uma contaLEITOR, mesmo enviandopapelno corpo; -
POST /auth/registrocom email já cadastrado responde409; -
POST /auth/logincom senha errada responde401, com a mesma mensagem de email inexistente; -
GET /auth/perfilsem token responde401; com token, devolveidepapel; -
POST /emprestimosePATCH /emprestimos/:id/devolucaoexigem token, mas aceitam qualquer papel; -
livros.service.tseemprestimos.service.tsnão mudaram nesta aula; -
/docsmostra o cadeado nas rotas protegidas, e Authorize funciona.
Fechamento
O serviço fecha aqui um arco que começou na aula 5: transporte separado de regra, regra separada de dados, e agora identidade e permissão separadas do resto — cada uma no lugar do pipeline reservado para ela desde a primeira aula do módulo. Nenhuma dessas camadas soube da outra além do necessário, e é essa separação, mais do que qualquer biblioteca específica, que faz o serviço inteiro caber na cabeça.
Com autenticação e autorização no lugar, o contrato que os módulos 3 e 4 vão consumir está completo: dados, forma de acessá-los e quem tem permissão de alterá-los.
Exercícios (Checkpoints)
-
Distinga autenticação de autorização com um exemplo próprio (fora da biblioteca) para cada uma, e indique o status HTTP correto para cada falha.
-
Explique por que
bcrypt.hashproduz um resultado diferente toda vez que é chamado com a mesma senha, e por que isso não impedebcrypt.comparede funcionar. -
Sobre o payload do JWT:
a. Explique por que ele é considerado legível, mesmo sem que ninguém tenha a
JWT_SECRET, e cite um dado que nunca deveria estar nele.b. Descreva o que a assinatura garante e o que ela não garante.
-
Identifique o defeito no trecho a seguir e reescreva-o corrigido:
async registrar(dto: RegistrarDto) {return this.prisma.usuario.create({data: { nome: dto.nome, email: dto.email, senhaHash: dto.senha, papel: dto.papel },});} -
Explique por que
PapeisGuardprecisa ser registrado depois deJwtAuthGuard, e descreva o sintoma observável se a ordem for invertida. -
Projete a política de acesso (pública, autenticada, ou restrita a um papel) para três rotas do seu estudo de caso, justificando cada escolha como decisão de negócio — não como obrigação técnica.
-
Implemente, no seu projeto, um decorator
@Roles(...)equivalente ao desta aula, e um guard que o leia. Se o seu domínio tiver só um perfil de usuário, descreva qual seria o segundo perfil mais provável de aparecer, e o que ele autorizaria a mais. -
Compare revogar um JWT com encerrar uma sessão tradicional guardada no servidor. Por que o primeiro é mais difícil, e que estratégia (mesmo que parcial) o
JWT_EXPIRES_INdesta aula oferece? -
Analise o que aconteceria se
AuthController.registrardevolvesse oUsuariointeiro, sem a desestruturação que removesenhaHash. Descreva o vazamento concreto, mesmo sabendo que o hash não é a senha em si. -
Explique por que a mensagem de erro de
loginé a mesma para "email não encontrado" e "senha incorreta", e descreva um cenário em que diferenciá-las ajudaria um atacante.
Referências
Principais
- NestJS — Authentication — guia oficial de autenticação com Passport e JWT
- NestJS — Authorization — guards baseados em papel e
Reflector - NestJS — Guards — o mecanismo por trás de
canActivate - NestJS — Custom decorators —
createParamDecoratoreSetMetadata - jwt.io — estrutura do token e depurador interativo
- bcryptjs — npm — API e recomendações de custo
Aprofundamento
- OWASP — Password Storage Cheat Sheet — critérios para escolher o algoritmo e o custo de hash de senha
- OWASP — JSON Web Token Cheat Sheet — riscos comuns no uso de JWT, independentes de linguagem
- RFC 7519 — JSON Web Token (JWT) — especificação formal do formato
- Passport.js — Documentação — o middleware de autenticação sobre o qual
passport-jwté construído