Na Aula 09 o UniEventos passou a persistir eventos no MySQL, com a API unieventos-api seguindo a arquitetura controller → service → repository. Qualquer pessoa com acesso à API conseguia criar, editar ou excluir um evento — não havia noção de "quem" fazia a requisição. Hoje isso muda: vamos exigir identidade.
Checklist antes de começar:
[ ] unieventos-web rodando com Vue Router e Pinia configurados (Aulas 04–06).
[ ] unieventos-api rodando com Express 5, endpoints de eventos e persistência MySQL (Aulas 07–09).
[ ] Projeto Firebase criado (Aula 07) — anote o projectId.
São duas perguntas diferentes, e misturar as duas é a origem de muito bug de segurança:
Autenticação responde "quem é você?". O sistema confirma sua identidade — normalmente com e-mail e senha, ou delegando a um provedor como Google.
Autorização responde "o que você pode fazer?". Depois de saber quem você é, o sistema decide se você pode ler, criar, editar ou excluir um recurso.
Um usuário autenticado pode não estar autorizado a excluir um evento — só o administrador está. Um visitante não autenticado pode estar autorizado a ler a lista de eventos, que é pública. As duas coisas são independentes e o back-end precisa checar as duas, sempre, endpoint por endpoint.
Por que não guardar senha no seu próprio banco (se puder evitar)¶
Até aqui o UniEventos não tinha usuários — só eventos. Se fôssemos implementar login "na mão", a tentação seria criar uma tabela usuarios com uma coluna senha. Isso é perigoso por dois motivos:
Nunca se guarda a senha em texto puro. Se o banco vazar, todas as senhas vazam — e como a maioria das pessoas reutiliza senha entre sites, o estrago vai muito além do seu sistema.
Hash não é criptografia. Criptografia é reversível (existe uma chave para desfazer). Hash é uma função de mão única: você transforma a senha em uma sequência de caracteres da qual, na prática, não dá para voltar. No login, você faz o hash da senha digitada e compara com o hash guardado — nunca descriptografa nada. Bibliotecas como bcrypt fazem isso com "salt" (um valor aleatório por usuário) para que duas pessoas com a mesma senha não gerem o mesmo hash, e com um custo computacional propositalmente alto, para dificultar ataques de força bruta.
Fazer isso corretamente — hash com salt, custo ajustável, fluxo de "esqueci minha senha", verificação de e-mail, proteção contra força bruta, login social — é trabalho considerável e cheio de detalhes fáceis de errar. Por isso, nesta trilha (e em grande parte dos projetos reais de pequeno e médio porte) delegamos a identidade a um provedor especializado: o Firebase Authentication. Ele guarda a senha (com hash correto, num banco que não é o seu), emite um token assinado provando quem é o usuário, e você só precisa validar esse token.
⚠️ Atenção
Delegar autenticação não elimina a responsabilidade de proteger seus endpoints. O Firebase resolve "provar quem é o usuário". Decidir "o que esse usuário pode fazer no meu sistema" continua sendo trabalho do seu back-end.
🧠 Você sabia?
"Hash" para senha é técnica dos anos 1970, mas o erro mais comum em vazamentos reais não é a falta de hash — é usar um hash rápido demais. Em 2012, o vazamento do LinkedIn expôs mais de 100 milhões de senhas com hash SHA-1 sem salt; a maioria foi quebrada em poucos dias, porque hardware moderno calcula bilhões de SHA-1 por segundo. bcrypt e Argon2 são desenhados de propósito para serem lentos (milhares de cálculos por segundo, não bilhões) — a lentidão em si é a defesa contra força bruta.
O Firebase (e a grande maioria dos sistemas de autenticação modernos) usa JSON Web Token (JWT) como formato do token de identidade. Um JWT é uma string com três partes separadas por ponto:
Cada parte é um objeto codificado em Base64URL. Decodificando as duas primeiras (a assinatura não se decodifica — ela não é Base64 de um JSON, é um bloco de bytes criptográfico):
Header — diz qual algoritmo assinou o token:
JSON
{"alg":"RS256","kid":"abc123"}
Payload — as "claims" (afirmações) sobre o usuário. É aqui que vive a informação:
iat (issued at) e exp (expiration) são timestamps Unix. admin é um exemplo de custom claim — vamos usar exatamente isso na seção 8.4 para autorização.
🔎 Por baixo do capô
Um JWT é assinado, não é criptografado. Qualquer pessoa pode pegar esse token e decodificar o header e o payload num site como jwt.io ou com atob() no console do navegador — não há segredo nenhum escondido ali, e por isso nunca coloque dados sensíveis no payload (senha, número de cartão, CPF). A assinatura (terceira parte) é o que garante que ninguém alterou o conteúdo sem ter a chave privada do emissor. Se você mudar um único caractere do payload — por exemplo, trocar "admin": false para "admin": true — a assinatura deixa de bater, e quem valida o token (no nosso caso, o firebase-admin no back-end) rejeita o token inteiro.
ID token (access token): de curta duração (1 hora), é o que você envia em cada requisição para provar identidade. É o JWT que acabamos de decodificar.
Refresh token: de longa duração, fica guardado pelo SDK do Firebase e é usado automaticamente, nos bastidores, para pedir um novo ID token quando o atual expira — sem exigir que o usuário faça login de novo.
Essa separação existe porque um token de vida curta limita o estrago se ele vazar (ex.: em um log, em uma extensão maliciosa do navegador), enquanto o refresh token, mais sensível, fica protegido e raramente trafega.
📌 Vale gravar
JWT tem três partes (header.payload.assinatura), é codificado em Base64URL (não criptografado) e assinado (não pode ser alterado sem invalidar a assinatura). ID token expira em 1h; o SDK renova sozinho usando o refresh token.
🔬 Investigue
Faça login no UniEventos, abra o console do navegador e rode:
js
const token = await auth.currentUser.getIdToken()
console.log(token)
console.log(JSON.parse(atob(token.split('.')[1])))
Compare o resultado com o que aparece ao colar o mesmo token em jwt.io. Calcule exp - iat em segundos — deve dar exatamente 3600 (a validade de 1 hora do ID token). Agora rode JSON.parse(atob(token.split('.')[0])): o que aparece é o header, não o payload — qual das duas partes diz qual algoritmo assinou o token?
3. Habilitando autenticação no console do Firebase¶
O pacote já está instalado desde a Aula 07 (firebase@12). Se o seu projeto ainda não tem, instale:
Terminal
npminstallfirebase@12
JavaScript
// src/services/firebase.jsimport{initializeApp}from'firebase/app'import{getAuth}from'firebase/auth'import{getFirestore}from'firebase/firestore'// Cole aqui o objeto de configuração exibido em// Configurações do projeto → Geral → Seus apps → Config SDK.constfirebaseConfig={apiKey:import.meta.env.VITE_FIREBASE_API_KEY,authDomain:import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,projectId:import.meta.env.VITE_FIREBASE_PROJECT_ID,storageBucket:import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,messagingSenderId:import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,appId:import.meta.env.VITE_FIREBASE_APP_ID,}constapp=initializeApp(firebaseConfig)exportconstauth=getAuth(app)exportconstdb=getFirestore(app)
Terminal
# .env (na raiz de unieventos-web, sem aspas, sem espaço ao redor do =)VITE_FIREBASE_API_KEY=AIzaSy...
VITE_FIREBASE_AUTH_DOMAIN=unieventos-xxxxx.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=unieventos-xxxxx
VITE_FIREBASE_STORAGE_BUCKET=unieventos-xxxxx.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789VITE_FIREBASE_APP_ID=1:123456789:web:abcdef
⚠️ Atençãofirebase.auth() com namespace não existe mais. A única forma correta no SDK 12 é a API modular: importar funções soltas (getAuth, signInWithEmailAndPassword, onAuthStateChanged etc.) de 'firebase/auth' e passar a instância auth como primeiro argumento. Se você encontrar tutorial usando firebase.auth().signInWithEmailAndPassword(...), está desatualizado — não copie.
Um serviço dedicado para as operações de autenticação, separado do firebase.js de inicialização:
JavaScript
// src/services/authService.jsimport{createUserWithEmailAndPassword,signInWithEmailAndPassword,signOut,onAuthStateChanged,updateProfile,sendPasswordResetEmail,GoogleAuthProvider,signInWithPopup,}from'firebase/auth'import{auth}from'./firebase'// Traduz os códigos de erro mais comuns do Firebase Auth para mensagens// em português — o usuário final não precisa saber o que é "auth/weak-password".constMENSAGENS_ERRO={'auth/invalid-credential':'E-mail ou senha incorretos.','auth/invalid-email':'E-mail em formato inválido.','auth/email-already-in-use':'Este e-mail já está cadastrado.','auth/weak-password':'A senha precisa ter pelo menos 6 caracteres.','auth/network-request-failed':'Falha de conexão. Verifique sua internet.','auth/too-many-requests':'Muitas tentativas seguidas. Aguarde um instante.','auth/popup-closed-by-user':'Janela de login fechada antes de concluir.',}functiontraduzirErro(erro){constmensagem=MENSAGENS_ERRO[erro.code]returnmensagem??'Não foi possível concluir a operação. Tente novamente.'}exportasyncfunctioncadastrar(nome,email,senha){try{constcredencial=awaitcreateUserWithEmailAndPassword(auth,email,senha)// O Firebase não pede nome no cadastro por e-mail/senha — setamos depois.awaitupdateProfile(credencial.user,{displayName:nome})returncredencial.user}catch(erro){thrownewError(traduzirErro(erro))}}exportasyncfunctionentrar(email,senha){try{constcredencial=awaitsignInWithEmailAndPassword(auth,email,senha)returncredencial.user}catch(erro){thrownewError(traduzirErro(erro))}}exportasyncfunctionentrarComGoogle(){try{constprovedor=newGoogleAuthProvider()constcredencial=awaitsignInWithPopup(auth,provedor)returncredencial.user}catch(erro){thrownewError(traduzirErro(erro))}}exportasyncfunctionsair(){awaitsignOut(auth)}exportasyncfunctionsolicitarRedefinicaoSenha(email){try{awaitsendPasswordResetEmail(auth,email)}catch(erro){thrownewError(traduzirErro(erro))}}// Registra um observador do estado de login. Retorna a função de// cancelamento — quem chamar deve guardá-la e invocar ao desmontar.exportfunctionobservarAutenticacao(callback){returnonAuthStateChanged(auth,callback)}
💡 DicaonAuthStateChanged dispara sempre que o estado de login muda — login, logout, e também na primeira carga da página, depois que o SDK verifica o refresh token salvo no navegador. É esse terceiro caso que vamos explorar na store, a seguir.
5. Store de autenticação: resolvendo o problema do F5¶
Se você guardar o usuário logado só em uma variável reativa comum, ao apertar F5 ela reseta para null — mesmo que o usuário continue logado no Firebase. O SDK vai confirmar isso, mas de forma assíncrona, alguns milissegundos depois do primeiro render. Se o seu guard de rota checar usuario antes desse retorno, ele vai redirecionar um usuário legitimamente logado para a tela de login. É um bug clássico.
A solução: a store expõe uma Promise de inicialização, e o guard de rota aguarda essa Promise antes de decidir.
JavaScript
// src/stores/authStore.jsimport{defineStore}from'pinia'import{ref,computed}from'vue'import{observarAutenticacao}from'@/services/authService'exportconstuseAuthStore=defineStore('auth',()=>{constusuario=ref(null)constcarregando=ref(false)constinicializado=ref(false)// Promise única, compartilhada por todos que chamarem inicializar().// Evita registrar o observador do Firebase mais de uma vez.letpromessaInicializacao=nullfunctioninicializar(){if(promessaInicializacao)returnpromessaInicializacaopromessaInicializacao=newPromise((resolve)=>{observarAutenticacao((usuarioFirebase)=>{usuario.value=usuarioFirebaseif(!inicializado.value){inicializado.value=trueresolve()// só resolve no PRIMEIRO disparo do observador}})})returnpromessaInicializacao}constestaLogado=computed(()=>usuario.value!==null)// Custom claim "admin" só aparece depois de setCustomUserClaims (seção 8.4)// e de o usuário obter um novo ID token — ver observação logo abaixo.// custom claims NÃO vêm no objeto `User`: é preciso decodificar o ID token.// Guardamos o resultado em um ref próprio, atualizado a cada mudança de sessão.constehAdmin=ref(false)asyncfunctionatualizarClaims(){if(!usuario.value){ehAdmin.value=falsereturn}// `true` força a busca de um token novo — necessário logo depois de// marcar o usuário como admin no back-end (seção 8.4)constresultado=awaitusuario.value.getIdTokenResult(true)ehAdmin.value=resultado.claims.admin===true}return{usuario,carregando,inicializado,inicializar,estaLogado,ehAdmin,atualizarClaims}})
🔎 Por baixo do capôonAuthStateChanged dispara de novo toda vez que o token é renovado, mas resolvemos a Promise só na primeira vez (if (!inicializado.value)). Depois disso, os componentes que precisam de reatividade (menu, header) simplesmente leem usuario e estaLogado, que são refs/computed normais e continuam atualizando sozinhos.
Repare no cuidado com ehAdmin: a tentação é escrever usuario.value?.customClaims?.admin, mas o objeto User do Firebase não expõe customClaims — essa propriedade simplesmente não existe ali, e o getter devolveria undefined para todo mundo, sem erro nenhum no console. Claims custom vivem dentro do ID token e só aparecem depois de decodificá-lo com getIdTokenResult(). Por isso ehAdmin é um ref alimentado por atualizarClaims(), chamado no onAuthStateChanged e de novo depois que alguém vira admin no back-end (seção 8.4).
// src/router/index.jsimport{createRouter,createWebHistory}from'vue-router'import{useAuthStore}from'@/stores/authStore'constrouter=createRouter({history:createWebHistory(import.meta.env.BASE_URL),routes:[{path:'/',name:'home',component:()=>import('@/views/HomeView.vue'),},{path:'/login',name:'login',component:()=>import('@/views/LoginView.vue'),},{path:'/cadastro',name:'cadastro',component:()=>import('@/views/CadastroView.vue'),},{path:'/minhas-inscricoes',name:'minhas-inscricoes',component:()=>import('@/views/MinhasInscricoesView.vue'),meta:{requerAuth:true},},{path:'/admin/eventos',name:'admin-eventos',component:()=>import('@/views/admin/EventosAdminView.vue'),meta:{requerAuth:true,requerAdmin:true},},],})router.beforeEach(async(to)=>{constauthStore=useAuthStore()// Aguarda o primeiro retorno do onAuthStateChanged antes de decidir// qualquer coisa — sem isso, um F5 numa rota protegida redireciona// para /login mesmo com o usuário já autenticado.awaitauthStore.inicializar()if(to.meta.requerAuth&&!authStore.estaLogado){return{name:'login',query:{redirect:to.fullPath},}}if(to.meta.requerAdmin&&!authStore.ehAdmin){return{name:'home'}}returntrue})exportdefaultrouter
Depois do login, a tela de login redireciona de volta para onde o usuário queria ir:
⚠️ Atenção
Um beforeEach no Router impede que a interface mostre a tela protegida — mas qualquer pessoa pode desligar o JavaScript, chamar a API diretamente com curl ou editar o guard no DevTools. Guard de rota é UX, não segurança. A única barreira real está no back-end, validando o token em cada requisição — é o que vem na seção 8.
O usuário logado no Firebase tem um método getIdToken() que devolve o JWT atual (renovando-o automaticamente se estiver perto de expirar). Plugamos isso no interceptor de requisição do Axios, criado na Aula 06:
JavaScript
// src/services/http.js — o MESMO arquivo da Aula 06, agora com o token do Firebaseimportaxiosfrom'axios'import{auth}from'./firebase'importrouterfrom'@/router'consthttp=axios.create({baseURL:import.meta.env.VITE_API_URL??'http://localhost:3000/api',})http.interceptors.request.use(async(config)=>{constusuarioAtual=auth.currentUserif(usuarioAtual){// getIdToken() usa o cache do SDK; só bate na rede do Firebase// quando o token está perto de expirar (renovação automática).consttoken=awaitusuarioAtual.getIdToken()config.headers.Authorization=`Bearer ${token}`}returnconfig})http.interceptors.response.use((resposta)=>resposta,(erro)=>{if(erro.response?.status===401){// Token ausente, inválido ou expirado sem chance de renovação// automática (ex.: usuário revogado no console). Mandamos para// o login preservando a rota atual.router.push({name:'login',query:{redirect:router.currentRoute.value.fullPath}})}returnPromise.reject(erro)},)exportdefaulthttp
💡 Dica
Não é preciso gerenciar expiração de token manualmente. O SDK do Firebase renova o ID token sozinho (usando o refresh token) sempre que getIdToken() é chamado e o token atual está a menos de 5 minutos de expirar. O interceptor de requisição, ao chamar getIdToken() antes de cada chamada, já se beneficia disso de graça.
8. Verificando o token no back-end com firebase-admin¶
Do lado do cliente, qualquer um pode afirmar ser quem quiser — inclusive forjar um cabeçalho Authorization. A prova de identidade real só existe quando o back-end valida a assinatura do token contra as chaves públicas do Firebase. É isso que o pacote firebase-admin faz.
No console do Firebase: Configurações do projeto → Contas de serviço → Gerar nova chave privada. Isso baixa um .json com credenciais completas de administrador do projeto — trate como uma senha.
⚠️ AtençãoserviceAccountKey.json nunca vai para o Git. Se você já commitou por engano, o arquivo precisa ser considerado comprometido: revogue a chave no console (Contas de serviço → gerenciar chaves) e gere outra. Em produção (Render, Railway etc.) prefira uma variável de ambiente com o JSON inteiro codificado em base64, decodificada na inicialização — assim nenhum arquivo sensível precisa existir no disco do servidor.
Terminal
# instalação, versão travada conforme especificação desta trilha
npminstallfirebase-admin@14.2.0
JavaScript
// unieventos-api/src/config/firebaseAdmin.jsimport{initializeApp,cert,getApps}from'firebase-admin/app'import{getAuth}from'firebase-admin/auth'importfsfrom'node:fs'functioncarregarCredencial(){// Em produção: variável de ambiente com o JSON em base64.if(process.env.FIREBASE_SERVICE_ACCOUNT_BASE64){constjson=Buffer.from(process.env.FIREBASE_SERVICE_ACCOUNT_BASE64,'base64').toString('utf-8')returnJSON.parse(json)}// Em desenvolvimento: arquivo local, fora do Git.constconteudo=fs.readFileSync(newURL('../../serviceAccountKey.json',import.meta.url),'utf-8')returnJSON.parse(conteudo)}// getApps() evita inicializar duas vezes se este módulo for importado// em mais de um lugar (ex.: em testes).if(getApps().length===0){initializeApp({credential:cert(carregarCredencial())})}exportconstauthAdmin=getAuth()
// unieventos-api/src/middlewares/autenticar.jsimport{authAdmin}from'../config/firebaseAdmin.js'// Envelope de erro: o mesmo { erro: { mensagem, codigo } } da Aula 08.// O front (store da Aula 11) lê `erro.mensagem` — devolver uma string solta aqui// faria a mensagem sumir da tela sem erro nenhum no console.exportasyncfunctionautenticar(req,res,next){constcabecalho=req.headers.authorizationif(!cabecalho?.startsWith('Bearer ')){returnres.status(401).json({erro:{mensagem:'Token de autenticação ausente.',codigo:'NAO_AUTENTICADO'},})}consttoken=cabecalho.replace('Bearer ','')try{consttokenDecodificado=awaitauthAdmin.verifyIdToken(token)// Popula req.usuario para os middlewares e controllers seguintes// usarem — igual fizemos com req.body validado na Aula 08.req.usuario={uid:tokenDecodificado.uid,email:tokenDecodificado.email,admin:tokenDecodificado.admin===true,// custom claim, seção 8.4}next()}catch(erro){// Cobre token expirado, assinatura inválida, token forjado etc.returnres.status(401).json({erro:{mensagem:'Token inválido ou expirado.',codigo:'TOKEN_INVALIDO'},})}}
Express 5 captura erros de handlers async automaticamente (Aula 08), mas aqui usamos try/catch de propósito: um token inválido não é um erro inesperado do servidor (500), é uma resposta de negócio esperada (401). Deixar o errorHandler central tratar isso como 500 estaria errado.
// unieventos-api/src/middlewares/autorizar.jsexportfunctionautorizar(papeis=[]){return(req,res,next)=>{if(!req.usuario){// autenticar() deve sempre rodar antes de autorizar() na cadeiareturnres.status(401).json({erro:{mensagem:'Token de autenticação ausente.',codigo:'NAO_AUTENTICADO'},})}consttemPermissao=papeis.includes('admin')?req.usuario.admin:trueif(!temPermissao){returnres.status(403).json({erro:{mensagem:'Você não tem permissão para esta ação.',codigo:'NAO_AUTORIZADO'},})}next()}}
🔎 Por baixo do capô
401 (Unauthorized) significa "eu não sei quem você é" — token ausente ou inválido. 403 (Forbidden) significa "eu sei quem você é, mas você não pode fazer isso" — token válido, mas sem a permissão necessária. Misturar os dois confunde quem está depurando o front.
8.4 Custom claims: marcando um usuário como admin¶
Custom claims são pares chave-valor extras que o Firebase embute no payload do JWT, definidos pelo back-end (nunca pelo próprio usuário). Um script único, rodado manualmente, promove um usuário a administrador:
⚠️ Atenção
Custom claims só aparecem em um novo ID token. Se o usuário já estava logado quando você rodou o script, ele precisa deslogar e logar de novo (ou o front precisa forçar getIdToken(true), com true pedindo renovação forçada) para o token trazer admin: true. É um erro comum: "rodei o script e continua sem permissão" — o token antigo, em cache no navegador, simplesmente ainda não tem a claim.
Com isso, completamos o ehAdmin da store (seção 5), que ficou pendente. A forma correta de ler a claim no front é via getIdTokenResult(), não pela propriedade customClaims (que não existe no objeto User):
JavaScript
// src/stores/authStore.js — ajuste da action inicializar()functioninicializar(){if(promessaInicializacao)returnpromessaInicializacaopromessaInicializacao=newPromise((resolve)=>{observarAutenticacao(async(usuarioFirebase)=>{if(usuarioFirebase){constresultado=awaitusuarioFirebase.getIdTokenResult()usuario.value=usuarioFirebaseehAdminClaim.value=resultado.claims.admin===true}else{usuario.value=nullehAdminClaim.value=false}if(!inicializado.value){inicializado.value=trueresolve()}})})returnpromessaInicializacao}
JavaScript
// e trocar o computed ehAdmin por uma ref simples atualizada acimaconstehAdminClaim=ref(false)constehAdmin=computed(()=>ehAdminClaim.value)
É o mesmosrc/routes/eventos.routes.js da Aula 09 — mesmos caminhos, mesmos controllers, mesma validação Zod. A única mudança é a cadeia de middlewares que passa a preceder os handlers de escrita:
JavaScript
// unieventos-api/src/routes/eventos.routes.jsimport{Router}from'express'import*aseventosControllerfrom'../controllers/eventosController.js'import{validar}from'../middlewares/validador.js'import{schemaEvento,schemaEventoParcial}from'../schemas/evento.schema.js'import{autenticar}from'../middlewares/autenticar.js'import{autorizar}from'../middlewares/autorizar.js'constrouter=Router()// Leitura pública — qualquer visitante, sem token, vê os eventosrouter.get('/',eventosController.listar)router.get('/:id',eventosController.buscarPorId)// Escrita exige apenas estar autenticado (o `validar` da Aula 09 continua no lugar)router.post('/',autenticar,validar(schemaEvento),eventosController.criar)router.put('/:id',autenticar,validar(schemaEvento),eventosController.substituir)router.patch('/:id',autenticar,validar(schemaEventoParcial),eventosController.atualizarParcial)// Exclusão exige estar autenticado E ser adminrouter.delete('/:id',autenticar,autorizar(['admin']),eventosController.excluir)exportdefaultrouter
💡 Dica
A ordem importa: autenticar vem antes de validar. Não faz sentido gastar validação de corpo em quem nem provou quem é — e o 401 sai mais barato que o 422.
🧩 Padrão de projeto em uso — Proxy de proteção + Guard¶
O Proxy de proteção (variação estrutural do padrão Proxy) intercepta o acesso a um objeto real e decide se o acesso é permitido antes de repassar a chamada. É exatamente o papel do middleware autenticar que construímos na seção 8.2: ele fica na frente do controller real, verifica credenciais, e só deixa a chamada prosseguir se o token for válido — o controller nunca sabe que existe um "porteiro" antes dele.
O Guard (aqui usado no sentido do Vue Router — um "guarda de rota" comportamental, correlato ao Proxy de proteção do lado do front) cumpre o mesmo papel do lado da navegação: intercepta a transição de rota e decide, antes de renderizar, se ela deve prosseguir, ser bloqueada ou redirecionada. Repare que os dois padrões resolvem o mesmo problema — controlar acesso — em duas camadas diferentes da aplicação, e nenhum substitui o outro.
// src/main.jsimport{createApp}from'vue'import{createPinia}from'pinia'importAppfrom'./App.vue'importrouterfrom'./router'import{useAuthStore}from'@/stores/authStore'import'@mdi/font/css/materialdesignicons.css'import'vuetify/styles'import{createVuetify}from'vuetify'constvuetify=createVuetify({theme:{defaultTheme:'light'}})constapp=createApp(App)app.use(createPinia())app.use(router)app.use(vuetify)// Dispara a inicialização o quanto antes; o router aguarda a mesma// Promise no beforeEach, então não há corrida entre os dois.useAuthStore().inicializar()app.mount('#app')
Com a API e o front rodando ao mesmo tempo, confira os seis pontos abaixo, nesta ordem:
Cadastro — crie uma conta pelo formulário do Passo 1. Resultado esperado: redirecionamento para a home já logado, e o usuário novo visível em Firebase Console → Authentication → Users.
F5 com sessão — recarregue a página logada. Resultado esperado: o menu não pisca "Entrar/Cadastrar" antes de mostrar o nome — é o inicializado da store fazendo efeito.
Guard — deslogue e digite /admin na barra de endereços. Resultado esperado: redirecionamento para /login?redirect=/admin; ao entrar, você cai direto em /admin.
Token na requisição — logado, abra DevTools → Network e provoque um POST. Resultado esperado: o cabeçalho Authorization: Bearer eyJ... sai junto, colocado pelo interceptor do http.js.
Back-end — rode os três curl acima. Resultado esperado: 401 sem token, 401 com token forjado, 201 com token válido.
Autorização — com um usuário comum (sem a custom claim), tente o DELETE. Resultado esperado: 403 com { "erro": { "mensagem": "...", "codigo": "..." } }; marque o usuário como admin (Seção 8.4), chame atualizarClaims() e repita — agora sai 204.
A1. Verdadeiro ou falso, com justificativa de uma linha: "Um JWT é criptografado — por isso ninguém além do Firebase consegue ler o que tem dentro do payload."
Resultado esperado: falso. JWT é assinado, não criptografado; qualquer pessoa decodifica o payload em Base64URL (com atob() ou em jwt.io). A assinatura só impede alterar o conteúdo sem invalidar o token — ela não esconde nada.
A2. Complete a linha que falta no trecho abaixo para que o guard de rota não redirecione um usuário já logado para /login logo depois de um F5:
A3. Em uma frase: por que o middleware autorizar(['admin']) precisa sempre vir depois de autenticar na cadeia de uma rota, nunca antes ou sozinho?
Resultado esperado: porque autorizar só lê req.usuario.admin, e é autenticar quem popula req.usuario a partir do token — sem autenticar antes, req.usuario é undefined e o acesso a .admin quebra.
A4. Ache o erro nas linhas abaixo (a rota deveria exigir login e papel de admin para excluir, mas está com a cadeia de middlewares na ordem errada):
Resultado esperado: autorizar está antes de autenticar — a ordem correta é autenticar, autorizar(['admin']), eventosController.remover.
A5. Preveja a saída: um usuário está logado há duas horas, com a aba aberta o tempo todo, sem nunca ter recarregado a página, e faz uma requisição autenticada agora. O ID token que o interceptor enviaria, se nada tivesse mudado, já expirou (dura só 1h). A requisição falha com 401?
Resultado esperado: não. getIdToken() renova o token automaticamente (usando o refresh token) sempre que ele está a menos de 5 minutos de expirar — o interceptor sempre envia um token válido, mesmo em uma aba aberta há horas.
B1. Cadastro e login funcionando. Crie uma conta pelo formulário de cadastro do seu projeto autoral, faça logout e faça login de novo.
Resultado esperado: a conta aparece no console do Firebase (Authentication → Users); depois do logout, a sessão salva pelo Firebase desaparece; um novo login recria a mesma sessão.
Dica
Abra o DevTools → Application → verifique se há chaves salvas pelo Firebase no IndexedDB/LocalStorage após o login.
B2. Login com Google. Habilite o provedor Google no console e teste entrarComGoogle().
Resultado esperado: o popup do Google fecha sozinho, o usuário aparece logado, e displayName/photoURL vêm preenchidos automaticamente pela conta Google.
Dica
Se o popup fechar sozinho sem erro visível, confira o console — geralmente é domínio não autorizado em Authentication → Settings → Authorized domains.
B3. Rota protegida. Crie uma rota meta: { requerAuth: true } no seu projeto e confirme que, deslogado, você é redirecionado para /login?redirect=... e volta para a rota certa após logar.
Resultado esperado: deslogado, a rota redireciona para /login preservando o destino em redirect; logado (inclusive logo após um F5 na própria rota protegida), o conteúdo aparece sem nenhum redirecionamento.
Dica
Teste apertando F5 na rota protegida já logado — não pode redirecionar para login.
B4. Middleware autenticar na API. Proteja um endpoint de escrita do seu projeto autoral e teste os três cenários de curl da seção anterior.
Resultado esperado: sem token → 401; com token inválido/forjado → 401; com token válido → o status de sucesso do endpoint (200/201/204, conforme o método).
Dica
Um token expira em 1h — se testar depois de muito tempo, gere outro logando de novo no front.
B5. Custom claim de admin. Rode o script promoverAdmin.js com seu próprio e-mail, deslogue e logue de novo, e confirme que authStore.ehAdmin fica true e que o menu de administração aparece.
Resultado esperado: depois de deslogar e logar de novo, authStore.ehAdmin vira true e o item de menu de administração passa a aparecer na barra de navegação.
Dica
Se continuar false, o token em cache é o antigo — force getIdTokenResult(true) ou deslogue mesmo.
C1. Sessão comprometida, ponta a ponta. Hoje, se um usuário desconfiar que seu token vazou (ex.: perdeu o notebook destravado), trocar a senha não invalida tokens já emitidos: o ID token continua válido até expirar (até 1h) e o refresh token, que renova automaticamente, também segue válido. Implemente um endpoint POST /api/usuarios/revogar-sessoes (autenticado) que chama authAdmin.revokeRefreshTokens(uid), e prove com curl que o efeito é real de imediato — não só depois de o token expirar sozinho.
Resultado esperado: antes da revogação, uma chamada a um endpoint protegido com um token guardado retorna sucesso normalmente; depois de POST /api/usuarios/revogar-sessoes, a mesma chamada com o mesmo token (ainda dentro da validade de 1h) passa a retornar 401 com uma mensagem clara ({"erro": {"mensagem": "Sessão revogada, faça login novamente.", "codigo": "SESSAO_REVOGADA"}}).
Dica
verifyIdToken(token, true) — o segundo argumento true faz o SDK checar revogação; sem ele, revokeRefreshTokens não tem efeito nenhum sobre um ID token ainda dentro da validade.
authAdmin.revokeRefreshTokens(uid) grava um timestamp no usuário; qualquer ID token emitido antes desse timestamp passa a ser considerado revogado quando a checagem está ligada.
Ajuste o middleware autenticar (Seção 8.2) para authAdmin.verifyIdToken(token, true) e trate o erro específico (erro.code === 'auth/id-token-revoked') com uma mensagem diferente da de "token inválido comum".
Para provar com curl: pegue um token, chame um endpoint protegido (sucesso), chame o novo endpoint de revogação, e chame o mesmo endpoint protegido de novo com o mesmo token (401).
Você já viu que dar F5 não desloga o usuário — mas onde exatamente o SDK guarda essa informação para sobreviver ao recarregamento da página? Abra o DevTools no UniEventos já logado e investigue, sem ler a documentação antes.
Critérios de pronto
Um comentário (ou nota no README do seu projeto) diz em qual mecanismo de armazenamento do navegador (Local Storage, IndexedDB ou cookie) o Firebase Auth guarda a sessão, com o nome exato da chave/banco encontrado.
Uma frase explica por que essa chave não some quando você fecha e reabre a aba, mas some quando você limpa os dados do site.
Um teste documentado: apague manualmente essa entrada pelo DevTools e recarregue a página — confirme que o usuário é deslogado, provando que aquele é de fato o mecanismo responsável.
Uma comparação de uma linha com onde o ID token em si (não a sessão persistente) fica durante a execução da página.
Pistas
No Chrome DevTools, olhe Application → IndexedDB, procurando um banco com nome parecido com "firebaseLocalStorageDb" — e também Application → Local Storage, para comparar.
Depois de apagar a entrada certa, recarregue com F5 e observe authStore.usuario no Vue DevTools.
Para o ID token durante a execução, pense em onde auth.currentUser vive — em disco ou só em memória do processo do navegador?
Ao dar F5 numa página do UniEventos com o usuário já logado, por uma fração de segundo aparecem os botões "Entrar"/"Cadastrar" antes de trocarem para o menu de usuário logado. É rápido demais para notar em conexão boa — mas ative o throttling "Slow 3G" na aba Network do DevTools e o "pisca" fica bem visível e feio. A store já resolve esse mesmo problema para o guard de rota (aguardando inicializar()), mas o componente BarraNavegacao (Seção 6) não faz o mesmo. Corrija o flicker sem duplicar a lógica de aguardar a Promise dentro do template.
Critérios de pronto
Com throttling "Slow 3G" ativo, um F5 numa página logada não mostra mais os botões de "Entrar"/"Cadastrar", nem que seja por um instante.
A solução não usa setTimeout nem "esconder com CSS" — o componente só decide o que renderizar depois que authStore.inicializado é true.
Enquanto inicializado ainda é false, um indicador de carregamento simples aparece no lugar do menu (v-progress-linear ou um spinner pequeno).
Um comentário de uma linha explica por que esse problema não existe na primeira visita (sem sessão salva) — só aparece em F5 com sessão já existente.
Pistas
authStore.inicializado já existe (Seção 5) — falta alguém no template ler essa ref antes de decidir o que mostrar.
v-if="authStore.inicializado" envolvendo o <v-app-bar> inteiro (ou só a parte que depende do login) resolve sem duplicar a Promise do guard de rota.
Ative "Slow 3G" em DevTools → Network → Throttling para conseguir ver o flicker devagar o bastante para testar com calma.
Hoje, qualquer conta criada por e-mail/senha pode criar um evento imediatamente — mesmo com um e-mail inventado (fulano@empresa-que-nao-existe.com) que a pessoa nem é dona de verdade. Ainda não confirmamos que o e-mail é real. Implemente a confirmação de e-mail de ponta a ponta: o cadastro dispara a verificação, o front bloqueia a criação de eventos até o e-mail estar confirmado, e o back-end confirma isso de novo — porque, como vimos no fim da Seção 6, guard de rota é UX, não segurança.
Critérios de pronto
cadastrar() dispara sendEmailVerification(credencial.user) logo após criar a conta.
Uma tela avisa "confirme seu e-mail" e não deixa o formulário de criação de evento habilitado enquanto usuario.emailVerified for false (a store precisa recarregar esse dado com user.reload() depois de o usuário clicar em "já confirmei").
No back-end, o middleware autenticar (ou um novo exigirEmailConfirmado) rejeita POST /api/eventos com 403 e uma mensagem clara se tokenDecodificado.email_verified for false, mesmo que alguém tenha contornado a tela do front.
Um teste com curl, usando um token de conta não confirmada, prova que o back-end bloqueia mesmo sem passar pelo front.
Um parágrafo no README explica por que essa checagem não pode viver só no front.
Pistas
sendEmailVerification vem do mesmo firebase/auth que os outros métodos do authService.js — importe e chame logo depois de createUserWithEmailAndPassword.
usuarioFirebase.emailVerified fica desatualizado até você chamar usuarioFirebase.reload() e ler de novo — o SDK não observa essa mudança automaticamente como faz com login/logout.
O tokenDecodificado que verifyIdToken devolve já traz email_verified (com underscore — é assim que o Firebase nomeia essa claim).
Crie o middleware como uma função separada (exigirEmailConfirmado) para poder aplicá-la só nas rotas de escrita em que fizer sentido, sem misturar com autenticar.
No seu projeto autoral: implemente cadastro, login, logout e proteção de pelo menos uma rota do front (requerAuth: true) e um endpoint de escrita da API (autenticar). Grave um GIF ou vídeo curto (menos de 1 minuto) mostrando: (1) tentativa de acessar a rota protegida deslogado sendo redirecionada, (2) login, (3) acesso liberado, (4) curl sem token retornando 401. Suba o material (código + evidência) no repositório do projeto: commit + push.
Critério de pronto: os quatro passos do vídeo aparecem, e o commit com a implementação está no repositório.
Plano de curso, Unidade 3: Firebase, autenticação e banco de dados.
Na Aula 11 fechamos o ciclo: CRUD completo de eventos, ponta a ponta, autenticado — Vue consumindo a API Express, que persiste no MySQL, tudo validado com o token do Firebase que construímos hoje.