Introdução
Neste artigo, criaremos um pequeno projeto que integra o Firebase Authentication em um aplicativo NestJS.
A autenticação é uma parte essencial de qualquer aplicativo, mas pode ser muito estressante para configurar do zero. Este é um problema que o Firebase resolve com seu produto de autenticação.
Firebase inclui uma série de produtos e soluções para facilitar o desenvolvimento de aplicativos. Alguns serviços fornecidos pelo Firebase incluem bancos de dados, autenticação, análise e hospedagem, entre outros. O Firebase pode ser integrado a aplicativos NodeJS usando o módulo npm firebase-admin .
NestJS ajuda a criar aplicativos NodeJS do lado do servidor usando TypeScript. Com mais de 600 mil downloads por semana no npm e 35 mil estrelas no GitHub, o framework é muito popular. Possui uma arquitetura do tipo Angular com recursos como controladores e módulos. O NestJS usa o Express por baixo do capô, embora também possa ser configurado para usar o Fastify.
O projeto
Criaremos um aplicativo simples que permitirá que apenas usuários autenticados tenham acesso a um recurso. Os usuários podem ser autenticados fazendo login e se inscrevendo por meio do cliente Firebase. Na autenticação, um JSON Web Token (JWT) é fornecido ao usuário, que é enviado junto com as solicitações subsequentes ao recurso restrito. O JWT fornecido é validado no lado do servidor usando o SDK firebase-admin
e o acesso é permitido ou rejeitado com base na validade do JWT.
Primeiros passos
Primeiro, vamos criar um aplicativo Firebase. Isso nos fornecerá algumas configurações que usaremos em nosso aplicativo NestJS posteriormente. Você pode fazer isso por meio do console do Firebase aqui . Clique em Adicionar projeto e nomeie seu projeto. Não precisamos do Google Analytics neste projeto, então você não precisa habilitá-lo. Você pode clicar em Criar projeto .
Assim que seu aplicativo for criado, clique no ícone de configurações ao lado de Visão geral do projeto e selecione Projeto Configurações . Na guia contas de serviço, gere uma nova chave privada. Isso deve baixar um arquivo JSON com algumas credenciais que usaremos para inicializar nosso SDK Admin do Firebase no lado do servidor (NestJS).
No mesmo menu Configurações do projeto , na guia Geral , vá até Seus aplicativos para registrar seu aplicativo no Firebase (se você tiver já registrou um aplicativo com Firebase, clique no botão Adicionar aplicativo ).
Nosso aplicativo é baseado na web, então selecione o ícone >
. Em seguida, dê um apelido ao seu aplicativo. Você não precisa selecionar a hospedagem Firebase, a menos que planeje fazer isso.
Você receberá alguns links para scripts, bem como configurações do Firebase, necessários para que seu aplicativo seja executado corretamente. Copie o conteúdo para um local onde você possa acessá-lo facilmente, pois será necessário posteriormente.
Depois disso, clique em Autenticação (localizada na barra lateral Criar ) e, no menu Método de login , ative E-mail/senha . Estaremos autenticando usuários com e-mail e senha.
Inicializando seu aplicativo NestJS
A seguir, instalaremos o pacote Nest CLI globalmente. Isso nos fornecerá alguns comandos, um dos quais é o comando nest
, que podemos usar para inicializar um novo aplicativo NestJS:
npm i-g @ nestjs/cli//instala o pacote nest cli globalmente nest new firebase-auth-project//cria um novo projeto nestjs em uma pasta chamada firebase-auth-project
O processo de instalação para criar um novo projeto pode demorar um pouco, pois todas as dependências necessárias precisam ser instaladas. O novo projeto deve ter o git inicializado com algumas pastas adicionadas a .gitignore
automaticamente. Adicione */**/firebase.config.json
a .gitignore
.
Inicie seu aplicativo em desenvolvimento usando o comando npm run start: dev
. O NestJS é executado na porta 3000 por padrão e o servidor é reiniciado automaticamente quando um arquivo é salvo. Seus arquivos TypeScript são compilados em JavaScript simples na pasta dist
sempre que você inicia o aplicativo.
Usaremos arquivos Handlebars do servidor. Para fazer isso, precisamos do módulo hbs
, que pode ser instalado usando os seguintes comandos:
npm i hbs npm i @ types/hbs
O Handlebars é um mecanismo de template que nos ajuda a escrever HTML reutilizável e dinâmico. Você pode ler mais sobre os mecanismos de modelo aqui .
Agora você pode modificar seu arquivo main.ts
para ficar assim:
importar {NestFactory} de'@ nestjs/core'; importar {NestExpressApplication} de'@ nestjs/platform-express'; import {join} de'path'; importar {Logger} de'@ nestjs/common'; import {AppModule} de'./app.module'; importar * como hbs de'hbs'; bootstrap de função assíncrona () { const app=await NestFactory.create(AppModule); const logger=novo Logger ('App'); app.useStaticAssets (join (__ dirname,'..','public')); app.setBaseViewsDir (join (__ dirname,'..','views')); hbs.registerPartials (join (__ dirname,'..','views/partials')); app.setViewEngine ('hbs'); app.set ('opções de visualização', {layout:'main'}); aguarde app.listen (3000); logger.log ('Aplicativo iniciado na porta 3000'); } bootstrap ();
Você pode ter um erro
Delete`␍`
no final de cada linha em seu arquivo, especialmente se você estiver executando o Windows. Isso ocorre porque no Windows, uma sequência de fim de linha é indicada porCR (caractere de retorno de carro)
e quebras de linha, ouLF (caractere de alimentação de linha)
, enquanto git usa apenas o caractere de nova linhaLF
. Executarnpm run lint
deve corrigir o problema, ou você pode definir manualmente sua sequência de fim de linha paraLF
em seu editor de código.
app.set ('view options', {layout:'main'});
indica que um arquivo main.hbs
servirá como layout para nosso hbs
files.
Existem alguns pacotes que usaremos neste projeto, então vamos instalá-los antes de prosseguir:
npm i @ nestjs/passport class-transformer firebase-admin passport passport-firebase-jwt
Passport é uma biblioteca de autenticação fácil de usar e extremamente popular para NodeJS e funciona muito bem com NestJS por meio do módulo @ nestjs/passport para fornecer um sistema de autenticação robusto.
Criação de rotas e arquivos hbs
Vamos criar nossas primeiras rotas. No arquivo app.controller.ts
, adicione o seguinte código:
import {Controller, Get, Render} de'@ nestjs/common'; importar {AppService} de'./app.service'; @Controlador('') export class AppController { construtor (particular somente leitura appService: AppService) {} @Get ('login') @Render ('login') Conecte-se() { Retorna; } @Get ('inscrição') @Render ('inscrição') inscrever-se() { Retorna; } }
Isso indica que quando enviamos uma solicitação GET
para a rota /login
, o arquivo login.hbs
deve ser renderizado para nós, bem como a rota de inscrição. Vamos criar esses arquivos hbs
agora.
Na raiz do seu projeto, crie as pastas public
e views
. Sua estrutura de pastas deve ser parecida com esta:
├──-público ├──-src ├───teste ├───visualizações
Lembre-se, indicamos main.hbs
como nosso arquivo de layouts, portanto, dentro da pasta de visualizações, crie o arquivo main.hbs
e adicione o seguinte código:
{{{corpo}}}
Observe os dois primeiros scripts na parte inferior do arquivo. Estes são os scripts para usar os recursos do Firebase na web. O primeiro é o SDK do FirebaseJS principal, enquanto o segundo é para o Firebase Authentication. É necessário adicionar os scripts para os recursos do Firebase de que você precisa em seu aplicativo.
Crie um arquivo login.hbs
e signup.hbs
na pasta de visualização e adicione o seguinte código.
login.hbs
:
signup.hbs
:
>
Agora, para os estilos e scripts. Na pasta public
, adicione scripts e subpastas de estilos. Dentro da subpasta de estilos, adicione um arquivo style.css
.
style.css
:
blockquote { posição: relativa; alinhamento de texto: esquerda; preenchimento: 1,2em 0 2em 38px; fronteira: nenhum; margem: 20px auto 20px; largura máxima: 800px; largura: 100%; display: bloco; } blockquote: após { contente:''; display: bloco; largura: 2px; altura: 100%; posição: absoluta; esquerda: 0; cor: # 66cc66; topo: 0; fundo:-moz-linear-gradiente ( principal, # 66cc66 0%, # 66cc66 60%, rgba (255, 255, 255, 0) 100% ); background:-webkit-linear-gradient ( principal, # 66cc66 0%, # 66cc66 60%, rgba (255, 255, 255, 0) 100% ); } blockquote: before { conteúdo:'\ f10d'; família de fontes:'fontawesome'; tamanho da fonte: 20px; display: bloco; margem inferior: 0,8em; peso da fonte: 400; cor: # 66cc66; } blockquote> cite, blockquote> p> cite { display: bloco; tamanho da fonte: 16px; altura da linha: 1,3em; peso da fonte: 700; estilo da fonte: normal; margem superior: 1.1em; espaçamento entre letras: 0; estilo da fonte: itálico; }
Dentro da pasta de scripts, crie os seguintes arquivos: main.js
, login.js
e signup.js
. Você pode deixá-los vazios por enquanto, nós voltaremos para eles. Você deve visitar as rotas /login
e /signup
para garantir que seus arquivos sejam renderizados corretamente.
Criando nosso recurso
O próximo item em nossa lista é criar nosso recurso restrito. Neste caso, será uma lista de citações e seus autores. Para criar uma nova pasta recursos
(com módulo, controlador e serviço todos configurados), execute:
aninham recursos de recursos g
Selecione API REST como a camada de transporte e Não como a resposta para “Gostaria de gerar pontos de entrada CRUD?”
Uma vez feito isso, no arquivo resources.service.ts
, adicione o seguinte código:
importar {Injetável} de'@ nestjs/common'; @Injectable () export class ResourcesService { recursos privados somente leitura: qualquer []; construtor () { this.resources=[ { citação:'Eles têm gosto de... queimando.', personagem:'Ralph Wiggum', }, { citação:'Meus olhos! Os óculos não fazem nada!', personagem:'Rainier Wolfcastle', }, { citar: "Olá, Simpson. Estou no ônibus hoje porque mamãe escondeu as chaves do meu carro para me punir por falar com uma mulher no telefone. Ela acertou.", personagem:'Principal Skinner', }, { citar: 'Eu moro em um quarto individual acima de uma pista de boliche... e abaixo de outra pista de boliche.', personagem:'Frank Grimes', }, { citar: "Só vou usar esta cama para dormir, comer e talvez construir um pequeno forte.", personagem:'Homer Simpson', }, { citação:'Em teoria, o comunismo funciona! Em teoria.', personagem:'Homer Simpson', }, { citação:"Oh, uau, windows. Não acho que teria dinheiro para este lugar.", personagem:'Otto', }, ]; } getAll () { return this.resources; } }
Lá você pode ver nossas citações (do programa de TV “Os Simpsons”) e um método, getAll ()
, que retorna todos eles.
Adicione ao arquivo resources.controller.ts
:
import {Controller, Get} de'@ nestjs/common'; importar {ResourcesService} de'./resources.service'; @Controller ('recursos') export class ResourcesController { construtor (private readonly resourcesService: ResourcesService) {} @Obter('') getAll () { return this.resourcesService.getAll (); } }
O decorador @Controller ()
indica que as rotas que começam com /resources
são direcionadas para este ponto de extremidade. Temos um endpoint GET
que retorna todas as nossas cotações usando o método getAll ()
em resources.service.ts
. Para testar seu aplicativo, enviando uma solicitação GET
para http://localhost: 3000/resources
deve retornar todas as aspas.
Este endpoint atualmente é público e é hora de trabalhar na parte de autenticação de nosso aplicativo.
Cliente Firebase
Para autenticar usuários do lado do cliente com o Firebase, primeiro inicializamos nosso aplicativo usando a configuração da Web do Firebase fornecida quando você criou um novo aplicativo no console do Firebase. Você pode obter isso na guia Geral no menu de configurações do projeto.
Adicione as configurações ao seu arquivo main.js
na pasta pública desta forma:
const aspas=document.getElementById ('aspas'); erro const=document.getElementById ('erro'); var firebaseConfig={ apiKey:'AIzaSyB7oEYDje93lJI5bA1VKNPX9NVqqcubP1Q', authDomain:'fir-auth-dcb9f.firebaseapp.com', projectId:'fir-auth-dcb9f', storageBucket:'fir-auth-dcb9f.appspot.com', messagingSenderId:'793102669717', appId:'1: 793102669717: web: ff4c646e5b2242f518c89c', }; //Inicialize o Firebase firebase.initializeApp (firebaseConfig); firebase.auth (). setPersistence (firebase.auth.Auth.Persistence.NONE); const displayQuotes=(allQuotes)=> { let html=''; para (citação const de allQuotes) { html +=``; } return html; };$ {quote.quote}.
$ {quote.character}
quotes
, error
e displayQuotes
são variáveis que serão usadas por login.js
e signup.js
, por isso é importante que seu arquivo main.js
seja importado antes dos outros dois. O main.js
, por sua vez, tem acesso à variável firebase
porque os scripts do Firebase foram incluídos pela primeira vez no arquivo main.hbs
.
Agora, para lidar com a inscrição do usuário, adicione a signup.js
:
const signupForm=document.getElementById ('formulário de inscrição'); const emailField=document.getElementById ('email'); const passwordField=document.getElementById ('senha'); signupForm.addEventListener ('enviar', (e)=> { e.preventDefault (); const email=emailField.value; const senha=passwordField.value; firebase .auth () .createUserWithEmailAndPassword (e-mail, senha) .então (({usuário})=> { return user.getIdToken (). then ((idToken)=> { return fetch ('/resources', { método:'GET', cabeçalhos: { Aceite:'application/json', Autorização: `Bearer $ {idToken}`, }, }) .então ((resp)=> resp.json ()) .então ((resp)=> { const html=displayQuotes (resp); quotes.innerHTML=html; document.title='aspas'; window.history.pushState ( {html, pageTitle:'aspas'}, '', '/Recursos', ); signupForm.style.display='nenhum'; quotes.classList.remove ('d-none'); }) .catch ((errar)=> { console.error (err.message); error.innerHTML=err.message; }); }); }) .catch ((errar)=> { console.error (err.message); error.innerHTML=err.message; }); });
E faça login em login.js
:
const loginForm=document.getElementById ('formulário de login'); const emailField=document.getElementById ('email'); const passwordField=document.getElementById ('senha'); loginForm.addEventListener ('enviar', (e)=> { e.preventDefault (); const email=emailField.value; const senha=passwordField.value; firebase .auth () .signInWithEmailAndPassword (e-mail, senha) .então (({usuário})=> { return user.getIdToken (). then ((idToken)=> { return fetch ('/resources', { método:'GET', cabeçalhos: { Aceite:'application/json', Autorização: `Bearer $ {idToken}`, }, }) .então ((resp)=> resp.json ()) .então ((resp)=> { const html=displayQuotes (resp); quotes.innerHTML=html; document.title='aspas'; window.history.pushState ( {html, pageTitle:'aspas'}, '', '/Recursos', ); loginForm.style.display='none'; quotes.classList.remove ('d-none'); }) .catch ((errar)=> { console.error (err.message); error.innerHTML=err.message; }); }); }) .catch ((errar)=> { console.error (err.message); error.innerHTML=err.message; }); });
Firebase-admin
Embora os usuários agora possam se inscrever e fazer login em nosso aplicativo, nossa rota de recursos
ainda está aberta e acessível para qualquer pessoa. Lembre-se de que instalamos o firebase-admin
em nosso aplicativo NestJS. Como mencionei anteriormente, este pacote ajudará a verificar o token JWT que é enviado do cliente antes de permitir ou negar o acesso do usuário à rota.
Na pasta src
, crie uma pasta chamada firebase
. Ele conterá todas as nossas configurações do Firebase. Dentro da pasta firebase
, crie um arquivo chamado firebase.config.json
. Isso conterá os valores do arquivo JSON baixado quando você gerou uma chave privada na guia da conta de serviço:
{ "tipo":"service_account", "project_id":"", "private_key_id":"", "chave privada":"", "client_email":"", "ID do Cliente":"", "auth_uri":"", "token_uri":"", "auth_provider_x509_cert_url":"", "client_x509_cert_url":"" }
É importante manter esses valores privados, pois alguns deles são muito confidenciais.
A seguir, criaremos uma estratégia do Passport para o Firebase. Uma estratégia é um mecanismo de autenticação para um serviço específico (neste caso, Firebase) no Passport. Crie um arquivo firebase-auth.strategy.ts
na pasta firebase
e adicione o seguinte código:
import {PassportStrategy} de'@ nestjs/passport'; import {Injectable, UnauthorizedException} de'@ nestjs/common'; import {Strategy, ExtractJwt} from'passport-firebase-jwt'; import * as firebaseConfig from'./firebase.config.json'; import * as firebase from'firebase-admin'; const firebase_params={ tipo: firebaseConfig.type, projectId: firebaseConfig.project_id, privateKeyId: firebaseConfig.private_key_id, privateKey: firebaseConfig.private_key, clientEmail: firebaseConfig.client_email, clientId: firebaseConfig.client_id, authUri: firebaseConfig.auth_uri, tokenUri: firebaseConfig.token_uri, authProviderX509CertUrl: firebaseConfig.auth_provider_x509_cert_url, clientC509CertUrl: firebaseConfig.client_x509_cert_url, }; @Injectable () a classe de exportação FirebaseAuthStrategy estende PassportStrategy ( Estratégia, 'firebase-auth', ) { defaultApp privado: qualquer; construtor () { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken (), }); this.defaultApp=firebase.initializeApp ({ credencial: firebase.credential.cert (firebase_params), }); } validação assíncrona (token: string) { const firebaseUser: any=await this.defaultApp .auth () .verifyIdToken (token, true) .catch ((errar)=> { console.log (errar); lance novo UnauthorizedException (err.message); }); if (! firebaseUser) { lance novo UnauthorizedException (); } return firebaseUser; } }
O que está acontecendo aqui? O JWT é extraído como um token de portador do cabeçalho da solicitação e nosso aplicativo Firebase é usado para verificar o token. Se o token for válido, o resultado será retornado, caso contrário, a solicitação do usuário será negada e uma exceção não autorizada será lançada.
Se você tiver erros de ESLint ao importar a configuração do Firebase, adicione ao seu arquivo
tsconfig.json
:"resolveJsonModule": true
.
Integrando a estratégia
No momento, nossa estratégia de autenticação é uma função autônoma, o que não ajuda muito. Podemos torná-lo middleware e integrá-lo aos endpoints que exigem autenticação, mas o NestJS tem uma maneira mais fácil e melhor de lidar com a autenticação chamada Guardas . Criaremos um guarda para usar nossa estratégia Firebase e, com um decorador simples, envolveremos as rotas que exigem autenticação.
Crie um arquivo chamado firebase-auth.guard.ts
e adicione o seguinte código a ele:
import {ExecutionContext, Injectable} de'@ nestjs/common'; importar {AuthGuard} de'@ nestjs/passport'; importar {Reflector} de'@ nestjs/core'; @Injectable () export class FirebaseAuthGuard extends AuthGuard ('firebase-auth') { construtor (refletor privado: Refletor) { super(); } canActivate (contexto: ExecutionContext) { const isPublic=this.reflector.getAllAndOverride('public', [ context.getHandler (), context.getClass (), ]); if (isPublic) { return true; } return super.canActivate (contexto); } }
A seguir, atualize seu arquivo resources.controller.ts
para ficar assim:
import {Controller, Get, UseGuards} de'@ nestjs/common'; importar {FirebaseAuthGuard} de'src/firebase/firebase-auth.guard'; importar {ResourcesService} de'./resources.service'; @Controller ('recursos') export class ResourcesController { construtor (private readonly resourcesService: ResourcesService) {} @Obter('') @UseGuards (FirebaseAuthGuard) getAll () { return this.resourcesService.getAll (); } }
Você também precisa atualizar seu arquivo app.module.ts
adicionando FirebaseAuthStrategy
à lista de provedores:
import {Module} from'@ nestjs/common'; importar {AppController} de'./app.controller'; importar {AppService} de'./app.service'; import {FirebaseAuthStrategy} de'./firebase/firebase-auth.strategy'; import {ResourcesModule} de'./resources/resources.module'; @Módulo({ importações: [ResourcesModule], controladores: [AppController], provedores: [AppService, FirebaseAuthStrategy], }) exportar classe AppModule {}
Você pode testar seu aplicativo novamente e descobrirá que nossa rota de recursos agora está bem protegida.
Conclusão
Embora este seja um aplicativo básico, você pode desenvolver o conhecimento para criar aplicativos maiores que usam o Firebase Authentication. Você também pode facilmente desconectar um usuário do cliente Firebase chamando firebase.auth (). SignOut ()
. Este repositório está disponível em Github.
A postagem Usando Firebase Authentication em aplicativos NestJS apareceu primeiro no LogRocket Blog .