Implemente autenticação completa em Bun.js: login via magic link, JWT, logout e perfil de usuário. Guia prático com ElysiaJS e cookies seguros. Domine a segurança backend!
GET /auth-links/authenticate
- Autenticar via magic linkPOST /sign-out
- Fazer logoutGET /me
- Obter perfil do usuárioEsta é a rota mais complexa - ela valida o código do magic link e autentica o usuário:
// src/http/routes/authenticate-from-link.ts import { t } from "elysia"; import { db } from "@/application/infra/db/connection"; import { authLinks, users } from "@/application/infra/db/schema"; import { eq, and, gt } from "drizzle-orm"; import dayjs from "dayjs"; import { auth } from "../auth"; export const authenticateFromLink = auth.get( "/auth-links/authenticate", async ({ query, signUser, set }) => { const { code, redirect } = query; // Buscar link de autenticação válido const [authLinkFound] = await db .select({ id: authLinks.id, userId: authLinks.userId, expiresAt: authLinks.expiresAt }) .from(authLinks) .where( and( eq(authLinks.code, code), gt(authLinks.expiresAt, new Date()) // Verificar se não expirou ) ); if (!authLinkFound) { throw new Error("Invalid or expired authentication link"); } // Buscar dados do usuário const [user] = await db .select({ id: users.id, name: users.name, email: users.email, role: users.role }) .from(users) .where(eq(users.id, authLinkFound.userId)); if (!user) { throw new Error("User not found"); } // Gerar JWT e salvar em cookie const token = await signUser(user.id); // Definir cookie de autenticação set.cookie = { auth: { value: token, httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 60 * 60 * 24 * 7, // 7 dias path: "/" } }; // Remover link usado (one-time use) await db.delete(authLinks).where(eq(authLinks.id, authLinkFound.id)); // Redirecionar para o frontend set.redirect = redirect; }, { query: t.Object({ code: t.String(), redirect: t.String() }) } );
Rota simples para fazer logout removendo o cookie de autenticação:
// src/http/routes/sign-out.ts import { auth } from "../auth"; export const signOut = auth.post("/sign-out", async ({ set }) => { // Remover cookie de autenticação set.cookie = { auth: { value: "", httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 0, // Expira imediatamente path: "/" } }; return { message: "Signed out successfully" }; });
Rota protegida que retorna os dados do usuário autenticado:
// src/http/routes/get-profile.ts import { db } from "@/application/infra/db/connection"; import { users } from "@/application/infra/db/schema"; import { auth } from "../auth"; import { eq } from "drizzle-orm"; export const getProfile = auth.get("/me", async ({ getCurrentUser }) => { const { sub: userId } = await getCurrentUser(); const [user] = await db .select({ id: users.id, name: users.name, email: users.email, role: users.role, createdAt: users.createdAt }) .from(users) .where(eq(users.id, userId)); if (!user) { throw new Error("User not found"); } return { user }; });
Vamos melhorar o middleware de autenticação para trabalhar com cookies:
// src/http/auth.ts (atualizado) import { Elysia } from "elysia"; import { jwt } from "@elysiajs/jwt"; import { cookie } from "@elysiajs/cookie"; export const auth = new Elysia({ name: "auth" }) .use( jwt({ name: "jwt", secret: process.env.JWT_SECRET || "your-secret-key" }) ) .use(cookie()) .derive(({ jwt, cookie }) => { return { getCurrentUser: async () => { const token = cookie.auth; if (!token) { throw new Error("Unauthorized"); } const payload = await jwt.verify(token); if (!payload) { throw new Error("Unauthorized"); } return payload as { sub: string }; }, signUser: async (userId: string) => { return await jwt.sign({ sub: userId }); } }; }) .macro(({ onBeforeHandle }) => ({ isSignedIn: (enabled: boolean = true) => { if (!enabled) return; onBeforeHandle(async ({ getCurrentUser, error }) => { try { await getCurrentUser(); } catch { return error(401, "Unauthorized"); } }); } }));
Vamos criar um script para inserir um usuário de teste no banco:
// src/application/infra/db/seed.ts import { db } from "./connection"; import { users } from "./schema"; async function seed() { console.log("🌱 Seeding database..."); // Inserir usuário de teste await db.insert(users).values({ name: "João Silva", email: "joao@example.com", role: "manager" }); console.log("✅ Database seeded successfully!"); process.exit(0); } seed().catch((error) => { console.error("❌ Error seeding database:", error); process.exit(1); });
Vamos adicionar as novas rotas ao servidor:
// src/http/server.ts (atualizado) import { Elysia } from "elysia"; import { cors } from "@elysiajs/cors"; import { auth } from "./auth"; import { csrf } from "./csrf"; import { getCsrfToken } from "./routes/get-csrf-token"; import { sendAuthLink } from "./routes/send-auth-link"; import { authenticateFromLink } from "./routes/authenticate-from-link"; import { signOut } from "./routes/sign-out"; import { getProfile } from "./routes/get-profile"; import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Aula 6 | CrazyStack', description: 'Curso completo sobre aula 6. Aprenda técnicas práticas e melhores práticas para desenvolvimento moderno. Guia atualizado 2025.', keywords: ['aula', '6', 'programação', 'desenvolvimento', 'tutorial', 'crazystack'], openGraph: { title: 'Aula 6', description: 'Curso completo sobre aula 6. Aprenda técnicas práticas e melhores práticas para desenvolvimento moderno. Guia atualizado 2025.', type: 'article' }, }; const app = new Elysia() .use(cors()) .use(csrf) .use(auth) .use(getCsrfToken) .use(sendAuthLink) .use(authenticateFromLink) .use(signOut) .use(getProfile) .get("/", () => ({ message: "Restaurantix API" })) .onError(({ error, code, set }) => { console.error(error); switch (code) { case "VALIDATION": set.status = error.status; return error.toResponse(); default: set.status = 500; console.error(error); return new Response(null, { status: 500 }); } }); app.listen(3000, () => { console.log("Server is running on port 3000"); });
# 1. Executar seed para criar usuário bun run src/application/infra/db/seed.ts # 2. Executar servidor bun run src/http/server.ts # 3. Obter token CSRF curl http://localhost:3000/csrf-token # 4. Solicitar magic link curl -X POST http://localhost:3000/auth-links/authenticate \ -H "Content-Type: application/json" \ -H "x-csrf-token: SEU_TOKEN_CSRF" \ -d '{"email":"joao@example.com"}' # 5. Copiar o link do console e acessar no navegador # O link será algo como: # http://localhost:3000/auth-links/authenticate?code=...&redirect=... # 6. Após autenticação, testar perfil curl http://localhost:3000/me \ -H "Cookie: auth=SEU_JWT_TOKEN" # 7. Testar logout curl -X POST http://localhost:3000/sign-out \ -H "Cookie: auth=SEU_JWT_TOKEN"
// package.json (scripts) { "scripts": { "dev": "bun --watch src/http/server.ts", "build": "bun build src/http/server.ts --target=bun", "start": "NODE_ENV=production bun src/http/server.ts", "generate": "drizzle-kit generate", "migrate": "drizzle-kit migrate", "studio": "drizzle-kit studio", "seed": "bun run src/application/infra/db/seed.ts" } }
Na próxima aula, vamos começar a implementar as rotas de restaurantes: add-restaurant e get-managed-restaurant.
💡 Dica: O JWT é armazenado em cookie httpOnly para maior segurança contra ataques XSS.
✅ Checkpoint: Sistema de autenticação completo funcionando com magic links, JWT e cookies seguros!