Pular para o conteúdo principal

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 depoisOnde
Consumo autenticado desta API pelo cliente webMódulo 3 — Next.js
Consumo autenticado da mesma API pelo cliente mobileMó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 bcrypt compra.
  • 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:

PacotePapel
@nestjs/jwtAssinatura e verificação de tokens JWT
@nestjs/passportIntegração do NestJS com a estratégia Passport
passportBiblioteca de autenticação sobre a qual passport-jwt é construída
passport-jwtEstratégia Passport que extrai e valida um JWT do cabeçalho Authorization
bcryptjsHash de senha, em JavaScript puro — sem compilação nativa
Por que bcryptjs, e não bcrypt

O 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çãoAutorização
PerguntaQuem está fazendo esta requisição?Esta pessoa pode fazer isto?
Falha quandoA identidade não pode ser confirmadaA identidade é conhecida, mas não basta
Status HTTP401 Unauthorized403 Forbidden
Depende deCredenciais (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:

A cadeia de decisão de um guard de autenticação seguido de um guard de autorização, e os dois status HTTP que cada desvio produz.

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:

prisma/schema.prisma
// 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())
}
Decisão de domínio × decisão de padrão

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:

AbordagemProblema
Texto puroUm vazamento do banco expõe a senha de todo mundo, imediatamente
Criptografia reversívelQuem tem a chave consegue recuperar a senha original — e a chave também pode vazar
Hash com bcryptOperaçã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 sal

Diferente 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.

Duas fases: login, em que o cliente envia email e senha, o AuthService busca o Usuario, compara a senha com bcrypt e o JwtService assina um token contendo o id e o papel do usuário; e requisição protegida, em que o cliente envia o token no cabeçalho Authorization, o JwtAuthGuard valida a assinatura e a validade, e anexa request.user antes do handler
Login acontece uma vez e consulta o banco; a requisição protegida acontece a cada chamada e não consulta nada — o guard só verifica a assinatura.

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​

src/auth/auth.service.ts
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:

  • registrar nunca recebe papel do cliente. O campo simplesmente não está em RegistrarDto, e o create nã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.
src/auth/dto/registrar.dto.ts
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:

src/auth/estrategias/jwt.strategy.ts
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ê.
  • validate só roda se a assinatura e a validade já passaram. O que ele devolve vira request.user — daqui em diante, o payload bruto do token não aparece mais em lugar nenhum do código; só o formato de UsuarioAutenticado.
  • config.getOrThrow é o mesmo padrão da aula 7 para DATABASE_URL. Se JWT_SECRET nã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.
.env
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.

src/auth/decorators/public.decorator.ts
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​

src/auth/guards/jwt-auth.guard.ts
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​

src/auth/decorators/roles.decorator.ts
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);
src/auth/guards/papeis.guard.ts
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​

src/auth/decorators/usuario-atual.decorator.ts
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​

src/auth/auth.module.ts
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.

Por que dois guards, e não um só verificando tudo

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:

ControllerRotaPolítica
AuthControllerPOST /auth/registro@Public() — é como alguém consegue a primeira conta
AuthControllerPOST /auth/login@Public() — mesma razão
AuthControllerGET /auth/perfilAutenticado, qualquer papel
LivrosControllerGET /livros, GET /livros/resumo, GET /livros/:id@Public() — o catálogo é de consulta aberta
LivrosControllerPOST /livros, PATCH /livros/:id, DELETE /livros/:id@Roles(Papel.ADMINISTRADOR)
EmprestimosControllerTodasAutenticado, qualquer papel — sem @Roles()

LivrosController, nas rotas de escrita:

src/livros/livros.controller.ts
// 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:

src/emprestimos/emprestimos.controller.ts
// 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():

src/auth/auth.controller.ts
@Get('perfil')
@ApiBearerAuth()
@ApiOkResponse({ description: 'Dados do usuário autenticado' })
@ApiUnauthorizedResponse({ description: 'Token ausente, inválido ou expirado' })
perfil(@UsuarioAtual() usuario: UsuarioAutenticado) {
return usuario;
}
O catálogo aberto é decisão de negócio, não obrigação técnica

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:

src/main.ts
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​

ErroSintomaCorreção
Confundir 401 com 403 no código ou na leitura do testeDebug na rota errada401 = sem identidade confirmada; 403 = identidade confirmada, sem permissão
Esquecer @Public() numa rota que deveria ser abertaCliente legítimo recebe 401 sem token nenhumAdicionar @Public()
Registrar PapeisGuard antes de JwtAuthGuardrequest.user sempre undefined, 403 mesmo autenticadoRespeitar a ordem no array de providers
JWT_SECRET ausente do .envAplicação não sobeCriar a variável antes de rodar start:dev
Aceitar papel vindo do corpo de POST /auth/registroQualquer um se cadastra como ADMINISTRADORNunca incluir papel no DTO de registro
Guardar a senha sem hash, ou com MD5/SHA-256 cruVazamento do banco expõe a senha de todo mundoUsar bcrypt.hash, nunca hash sem sal
Comparar senha com === contra senhaHashLogin 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 contaUma única mensagem para os dois casos
Colocar dado sensível no payload do JWTVaza para quem só tem o token, sem precisar decifrar nadaPayload não é segredo — trate como texto público
Token sem expiresInUm token vazado nunca perde validadeSempre 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​

.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:

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.

Confira o que você NÃO precisou tocar

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.

Acrescente ao seed.ts as duas contas de teste:

prisma/seed.ts
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/resumo e GET /livros/:id respondem sem token;
  • POST /livros sem token responde 401;
  • POST /livros com token de conta LEITOR responde 403;
  • POST /livros com token de conta ADMINISTRADOR responde 201;
  • POST /auth/registro cria sempre uma conta LEITOR, mesmo enviando papel no corpo;
  • POST /auth/registro com email já cadastrado responde 409;
  • POST /auth/login com senha errada responde 401, com a mesma mensagem de email inexistente;
  • GET /auth/perfil sem token responde 401; com token, devolve id e papel;
  • POST /emprestimos e PATCH /emprestimos/:id/devolucao exigem token, mas aceitam qualquer papel;
  • livros.service.ts e emprestimos.service.ts não mudaram nesta aula;
  • /docs mostra 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)​

  1. 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.

  2. Explique por que bcrypt.hash produz um resultado diferente toda vez que é chamado com a mesma senha, e por que isso não impede bcrypt.compare de funcionar.

  3. 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.

  4. 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 },
    });
    }
  5. Explique por que PapeisGuard precisa ser registrado depois de JwtAuthGuard, e descreva o sintoma observável se a ordem for invertida.

  6. 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.

  7. 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.

  8. 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_IN desta aula oferece?

  9. Analise o que aconteceria se AuthController.registrar devolvesse o Usuario inteiro, sem a desestruturação que remove senhaHash. Descreva o vazamento concreto, mesmo sabendo que o hash não é a senha em si.

  10. 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​

Aprofundamento​