Faz algum tempo que ensino aqui no blog a como fazer APIs com NestJS, um dos frameworks web mais populares para Node.js atualmente. Antes disso também ensinei como o HTTP funciona e a fazer APIs que usam banco de dados com Prisma ORM. No entanto, ainda não falei aqui sobre segurança em APIs, então hoje vamos falar de JSON Web Tokens como uma forma de garantir a autenticação e autorização de APIs de maneira bem simples e segura, sendo o JWT um padrão para segurança de APIs RESTful atualmente.
Veremos neste tutorial:
Vamos lá!
#1 – JSON Web Tokens
JWT, resumidamente, é uma string de caracteres que, caso cliente e servidor estejam sob HTTPS, permite que somente o servidor que conhece o ‘segredo’ possa validar o conteúdo do token e assim confirmar a autenticidade do cliente. O token não é “criptografado”, mas “assinado”, de forma que só com o secret essa assinatura possa ser comprovada, o que impede que atacantes “criem” tokens por conta própria.
Em termos práticos, quando um usuário se autentica no sistema ou web API (com usuário e senha), o servidor gera um token com data de expiração pra ele. Durante as requisições seguintes do cliente, o JWT é enviado no cabeçalho da requisição e, caso esteja válido, a API irá permitir acesso aos recursos solicitados, sem a necessidade de se autenticar novamente.
O diagrama abaixo mostra este fluxo, passo-a-passo:
O conteúdo do JWT é um payload JSON que pode conter a informação que você desejar, que lhe permita mais tarde conceder autorização a determinados recursos para determinados usuários. Minimamente ele terá o ID do usuário autenticado ou da sessão (se estiver trabalhando com este conceito), mas pode conter muito mais do que isso, conforme a sua necessidade, embora guardar conteúdos “sensíveis” no seu interior não é uma boa ideia, pois como disse antes, ele não é criptografado.
#2 – Estruturando a API
Antes de começarmos esta API NestJS usando JWT vale ressaltar que o foco aqui é mostrar o funcionamento do JWT e não o funcionamento de uma API real. Caso você já possua uma web API, pule esta seção. Caso contrário, use a API fake abaixo, que vamos mockar os dados retornados pela API e as credenciais de autenticação inicial para ir logo para a geração e posterior verificação dos tokens.
Crie um novo projeto NestJS com o comando abaixo:
1 2 3 |
npx @nestjs/cli new jwt-example |
Depois, salve o seguinte código no arquivo src/app.service.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello() { return { message: "Tudo ok por aqui!" }; } getClientes() { console.log("Retornou todos clientes!"); return [{ id: 1, nome: 'luiz' }]; } } |
E o código abaixo no src/app.controller.service.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) { } @Get() getHello() { return this.appService.getHello(); } @Get("clientes") getClientes() { return this.appService.getClientes(); } } |
Com esse projeto em mãos, rode a API com “npm start” e ao acessar localhost:3000 deve listar apenas uma mensagem de OK e ao acessar o caminho /clientes no navegador, deve listar um array JSON como abaixo.
Isso mostra que a API está funcionando em ambas as rotas e sem segurança, afinal, não tivemos de nos autenticar para fazer os GETs que fizemos.
#3 – Criando o Auth Service
Agora, vamos instalar o módulo oficial do NestJS para JWT, com o comando abaixo, bem como o módulo para lidar com variáveis de ambiente:
1 2 3 |
npm i @nestjs/jwt @nestjs/config |
Depois, devemos criar o arquivo .env na raiz da aplicação, com o valor do segredo à sua escolha e uma expiração do token em segundos:
1 2 3 4 5 |
#.env, don't commit to repo SECRET=mysecret EXPIRES=1800 |
Este segredo será utilizado pela biblioteca de JWT para assinar o token de modo que somente o servidor consiga validá-lo, então é de bom tom que seja um segredo forte. Sobre o tempo de expiração, isso você define com base no tempo de sessão que deseja para seus usuários.
E agora vamos criar um novo service, dedicado à emissão e verificação de tokens. Crie um arquivo auth.service.ts com o seguinte conteúdo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { Injectable } from "@nestjs/common"; import { JwtService } from "@nestjs/jwt"; @Injectable() export class AuthService { constructor(private readonly jwtService: JwtService) { } async createToken(id: number) { return this.jwtService.sign({ id }); } async checkToken(token: string) { try { return this.jwtService.verify(token.replace("Bearer ", "")); } catch (err) { return false; } } } |
Neste exemplo, temos uma função createToken que usa o jwtService para gerar um token cujo payload será somente o id do usuário. Esse id, bem como a verificação se o usuário pode ter um token gerado ou não é responsabilidade de outro service, como um service de banco de dados, por exemplo, mas que não implementaremos aqui. Assumiremos um exemplo mais simples e didático, que implementarei a seguir.
Já a outra função é a checkToken, responsável por verificar se um dado token é válido, algo que será útil mais tarde na etapa de autorização de rotas. Repare que estou removendo a string “Bearer” do possível token que vai vir, pois isso é algo bem comum e desnecessário do ponto de vista do JWT. Essa função verify do jwt service costuma disparar erros por vários motivos quando a validação não é bem sucedida, então acho uma boa tratarmos com try/catch, devolvendo um false que indica que o token foi recusado.
Não esqueça de reparar também nos imports necessários, no topo do serviço, e no constructor dele.
Para que nosso AuthService possa funcionar, os pacotes que instalamos anteriormente, bem como o próprio auth.service.ts, precisam ser carregados assim que a aplicação iniciar, o que fazemos agora em seu app.module.ts começando pelos imports de pacotes:
1 2 3 4 5 |
import { ConfigModule } from "@nestjs/config"; import { JwtModule } from '@nestjs/jwt'; import { AuthService } from './auth.service'; |
Além disso, para que eles passem a funcionar junto à nossa aplicação, é necessário importarmos eles no módulo que vamos usá-los (AppModule), como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Module({ imports: [ ConfigModule.forRoot(), JwtModule.register({ secret: process.env.SECRET, signOptions: { expiresIn: parseInt(process.env.EXPIRES) } })], controllers: [AppController], providers: [AppService, AuthService], }) export class AppModule { } |
Repare que o import do ConfigModule é bem direto, apenas sinalizando com a função forRoot que nosso .env está na raiz do projeto. Isso vai fazer com que ele seja lido e todas suas variáveis façam parte do process.env.
Já o import do JwtModule exige a chamada da função register, passando as configurações de JWT que iremos utilizar, neste caso o secret e o expiresIn, ambos lidos de variáveis de ambiente de mesmo nome, carregadas do .env.
Por fim, o AuthService é carregado na propriedade providers do decorator @Module. Isso deixa nossa API minimamente preparada para de fato lidar com a autenticação e autorização.
#4 – Autenticação
Caso você não saiba a diferença, autenticação é você provar que você é você mesmo. Já autorização é você provar que possui permissão para fazer ou ver o que você está tentando.
Antes de gerar o JWT é necessário que o usuário passe por uma autenticação tradicional, geralmente com usuário e senha. Essa informação fornecida é validada junto a uma base de dados e somente caso ela esteja ok é que geramos o JWT para ele.
Assim, vamos criar uma nova rota /login no app.controller.ts que vai receber um usuário e senha hipotético e, caso esteja ok, retornará um JWT para o cliente:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Post("login") async login(@Body() body) { if (body.user === 'luiz' && body.password === '123') {//esse teste seria feito no banco //auth ok const id = 1; //esse id viria do banco de dados const token = await this.authService.createToken(id); return { auth: true, token: token }; } throw new UnauthorizedException(); } |
Aqui temos o seguinte cenário: o cliente faz POST na URL /login um user e um password, eu simulo uma ida ao banco meramente verificando se user é igual a luiz e se password é igual a 123. Estando ok, o banco me retornaria o ID deste usuário, que simulei com uma constante.
Esse ID está sendo usado como payload do JWT que está sendo assinado, mas poderia ter mais informações conforme a sua necessidade. Além do payload, internamente o módulo JWT Service já tem um secret e um expiresIn configurados na inicialização do módulo, que são usados nesta geração de token. Caso o user e password não coincidam, será devolvido um erro 401 ao usuário.
Se quiser adicionar uma camada a mais de segurança, você pode ter uma rota de logout com uma blacklist de tokens que fizeram logout, para impedir reuso deles depois do logout realizado. Apenas lembre-se de limpar essa lista depois que eles expirarem ou use um cache em Redis com expiração automática.
Abaixo, um login feito com sucesso, recebendo um token gerado.
Na aplicação que for consumir a sua web API, deverá ser coletado esse token gerado e todas as requisições aos endpoints protegidos devem ser feitas passando o mesmo no header authorization da requisição HTTP.
Mas será que este token está funcionando?
#5 – Autorização
Aí que entra a autorização!
Nós já temos uma função de verificação de token no auth.service.ts então é hora de usarmos ela. Uma forma seria incluir uma verificação de token em cada rota que precisa estar protegida e de certa forma é isso que vamos fazer, mas de uma forma extremamente elegante chamada Guards.
Os Guards são um recurso do NestJS muito semelhante aos middlewares do Express, mas focados exclusivamente em proteger execuções de funções, como se fossem guardas. Nós vamos criar um guard de verificação de token e aí em todas funções que quisermos proteger, usaremos este guard.
Vamos começar criando um arquivo auth.guard.ts e dentro dele vamos colocar o seguinte código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { CanActivate, ExecutionContext, ForbiddenException, Injectable, UnauthorizedException } from "@nestjs/common"; import { Observable } from "rxjs"; import { AuthService } from "./auth.service"; @Injectable() export class AuthGuard implements CanActivate { constructor(private readonly authService: AuthService) { } canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { const { authorization } = context.switchToHttp().getRequest().headers; return this.authService.checkToken(authorization ?? ""); } } |
Todo guard deve implementar a interface CanActivate, que por sua vez exige uma função de mesmo nome que possui o contexto de execução do guard. Como este guard será usado em uma API HTTP, nosso contexto é uma request HTTP, mais especificamente queremos os headers dela. Mais especificamente ainda, o header de nome authorization. Esse acesso bem específico é feito na primeira linha da função.
Já a linha seguinte é muito mais simples e direta: chamamos a função que verifica token lá do nosso auth service, não esquecendo de fazer as devidas importações no topo do arquivo. Agora, temos de usar esse nosso guard em todas as rotas que queremos proteger, como na que lista os clientes, como abaixo:
1 2 3 4 5 6 7 |
@UseGuards(AuthGuard) @Get("clientes") getClientes() { return this.appService.getClientes(); } |
Os guards que criarmos devem ser passados por parâmetro para o decorator UseGuards. Assim, toda vez que essa rota for chamada, um teste do guard será feito para deicidir se ela funcionará ou não. Experimente subir agora o backend e fazer uma requisição GET /clientes sem passar token, e terá um resultado como abaixo.
Assim, antes de responder os GETs de clientes, a API vai criar essa camada intermediária de autorização baseada em JWT, que obviamente vai bloquear requisições que não estejam autenticada e autorizadas, conforme as suas regras para tal.
Bacana, não?!
E com isso encerro o tutorial de hoje. Note que foquei no uso do JWT, sem entrar em muitos detalhes de como você pode estruturar sua autenticação a partir do banco de dados (tabela/coleção login e senha) e nem como você pode estruturar o seu modelo de autorização (perfis de acesso, por exemplo).
Quem sabe não abordo este tema de maneira mais avançada e completa em outra oportunidade?
Quer aprender uma a fazer uma webapi completa em NestJS? Esse tutorial pode ajudar.
E se quiser conhecer outras formas de deixar suas Web APIs seguras, leia este artigo aqui.
Um abraço e até mais!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.