Autenticação OAuth2 + JWT
Alice precisa criar uma conta para comprar itens, e Bob precisa fazer login no painel administrativo. No entanto, o MegaShop ainda não possui um sistema de autenticação — qualquer pessoa pode acessar a interface de administração. Charlie precisa suportar tanto registro por email quanto logins sociais Google/GitHub, garantindo também a segurança da API.
1. O Que Você Vai Aprender
- Design de Arquitetura de Autenticação: Comparação entre Abordagens Session, JWT e Cookie
- Implementação JWT: Emissão/Verificação + Armazenamento Seguro em Cookies httpOnly
- Integração OAuth 2.0: Fluxo de Login Social Google/GitHub
- Mecanismo de refresh token: Rotação dual-token Access Token + Refresh Token
- Prática de Autenticação do MegaShop: Registro e Login da Alice + Bob (Administrador) OAuth2
2. Uma História Real de um Arquiteto
(1) Ponto Crítico: Uma Vulnerabilidade de Segurança Crítica Devido à Falta de Autenticação
Qualquer pessoa pode ler os dados do carrinho de compras de Alice; a interface de gerenciamento de Bob carece de autenticação; e Charlie descobriu que um invasor poderia forjar uma requisição para excluir um item. O MegaShop deve ter um sistema de autenticação.
(2) Solução JWT + OAuth2
Autenticação Stateless JWT + Login Social OAuth 2.0: Equilibrando Segurança e Experiência do Usuário:
// Servidor: emitir JWT após login
const token = jwt.sign({ userId: user.id, role: user.role }, secret, { expiresIn: '15m' })
setCookie(event, 'access-token', token, { httpOnly: true })
(3) Benefícios: Segurança + Conveniência
Alice usa o Google Sign-In, Bob acessa o painel administrativo usando uma conta de administrador, e requisições de API incluem um JWT para autenticação automática.
3. Comparação de Esquemas de Autenticação
(1) Três Esquemas de Autenticação
| Dimensão | Session + Cookie | JWT + Cookie | JWT + localStorage |
|---|---|---|---|
| Estado | Com estado (armazenamento no servidor) | Sem estado | Sem estado |
| Escalabilidade | 🔴 Requer sessões compartilhadas | 🟢 Inerentemente distribuído | 🟢 Distribuído |
| Compatibilidade SSR | ✅ Persistência Automática de Cookie | ✅ Persistência Automática de Cookie | ❌ Tratamento Manual Necessário |
| Risco CSRF | 🔴 Requer Proteção | 🟢 Protegido por httpOnly | 🟢 Sem Cookie |
| Risco XSS | 🟢 httpOnly | 🟢 httpOnly | 🔴 Legível por JS |
| Logout | ✅ Excluir Sessão Imediatamente | ⚠️ Requer Blacklist | ⚠️ Requer Blacklist |
(2) Diagrama de Sequência do Fluxo de Autenticação
sequenceDiagram
participant A as Alice/Browser
participant N as Nuxt Server
participant G as Google OAuth2
participant DB as Database
A->>N: Click "Sign in with Google"
N->>G: Redirect to Google consent screen
G-->>A: User grants permission
A->>N: Callback with authorization code
N->>G: Exchange code for user info
G-->>N: User profile (email, name)
N->>DB: Create/find user
DB-->>N: User record
N->>N: Sign JWT (access + refresh)
N-->>A: Set httpOnly cookies
A->>A: Redirect to dashboard
4. Emissão e Verificação JWT
(1) ▶Exemplo: Funções Utilitárias JWT
// server/utils/jwt.ts
import jwt from 'jsonwebtoken'
const config = useRuntimeConfig()
interface JwtPayload {
userId: number
email: string
role: 'customer' | 'admin'
}
export function signAccessToken(payload: JwtPayload): string {
return jwt.sign(payload, config.jwtAccessSecret, { expiresIn: '15m' })
}
export function signRefreshToken(payload: JwtPayload): string {
return jwt.sign(payload, config.jwtRefreshSecret, { expiresIn: '7d' })
}
export function verifyAccessToken(token: string): JwtPayload {
return jwt.verify(token, config.jwtAccessSecret) as JwtPayload
}
export function verifyRefreshToken(token: string): JwtPayload {
return jwt.verify(token, config.jwtRefreshSecret) as JwtPayload
}
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: API de Registro por Email
// server/api/auth/register.post.ts
export default defineEventHandler(async (event) => {
const { email, password, name } = await readBody(event)
if (!email || !password) {
throw createError({ statusCode: 400, message: 'Email and password required' })
}
// Verificar se o usuário existe
const existing = mockUsers.find(u => u.email === email)
if (existing) {
throw createError({ statusCode: 409, message: 'Email already registered' })
}
// Criar usuário
const user = {
id: mockUsers.length + 1,
email,
name: name || 'Customer',
password: await hashPassword(password),
role: 'customer' as const
}
mockUsers.push(user)
// Emitir tokens
const payload = { userId: user.id, email: user.email, role: user.role }
const accessToken = signAccessToken(payload)
const refreshToken = signRefreshToken(payload)
// Definir cookies httpOnly
setCookie(event, 'access-token', accessToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900 // 15m
})
setCookie(event, 'refresh-token', refreshToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 604800 // 7d
})
return { user: { id: user.id, email: user.email, name: user.name, role: user.role } }
})
Saída:
// Execução Bem-sucedida
(3) ▶Exemplo: API de Login por Email
// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event)
const user = mockUsers.find(u => u.email === email)
if (!user || !await verifyPassword(password, user.password)) {
throw createError({ statusCode: 401, message: 'Invalid credentials' })
}
const payload = { userId: user.id, email: user.email, role: user.role }
const accessToken = signAccessToken(payload)
const refreshToken = signRefreshToken(payload)
setCookie(event, 'access-token', accessToken, { httpOnly: true, secure: true, maxAge: 900 })
setCookie(event, 'refresh-token', refreshToken, { httpOnly: true, secure: true, maxAge: 604800 })
return { user: { id: user.id, email: user.email, name: user.name, role: user.role } }
})
Saída:
// Execução Bem-sucedida
5. Login Social OAuth 2.0
(1) ▶Exemplo: Login Google OAuth 2.0
// server/api/auth/google.get.ts
export default defineEventHandler((event) => {
const config = useRuntimeConfig()
const redirectUri = `${config.public.apiBase}/auth/google/callback`
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${config.googleClientId}&` +
`redirect_uri=${redirectUri}&` +
`response_type=code&` +
`scope=openid email profile&` +
`state=${generateState()}`
return sendRedirect(event, googleAuthUrl)
})
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Tratamento do Callback Google OAuth 2.0
// server/api/auth/google/callback.get.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const code = getQuery(event).code as string
// Trocar código por tokens
const tokenResponse = await $fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: {
code,
client_id: config.googleClientId,
client_secret: config.googleClientSecret,
redirect_uri: `${config.public.apiBase}/auth/google/callback`,
grant_type: 'authorization_code'
}
})
// Obter informações do usuário do Google
const userInfo = await $fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${tokenResponse.access_token}` }
})
// Encontrar ou criar usuário
let user = mockUsers.find(u => u.email === userInfo.email)
if (!user) {
user = {
id: mockUsers.length + 1,
email: userInfo.email,
name: userInfo.name,
avatar: userInfo.picture,
provider: 'google',
role: 'customer'
}
mockUsers.push(user)
}
// Emitir tokens JWT
const payload = { userId: user.id, email: user.email, role: user.role }
setCookie(event, 'access-token', signAccessToken(payload), { httpOnly: true, secure: true, maxAge: 900 })
setCookie(event, 'refresh-token', signRefreshToken(payload), { httpOnly: true, secure: true, maxAge: 604800 })
return sendRedirect(event, '/')
})
Saída:
// Execução Bem-sucedida
6. Mecanismo de Rotação Dual-Token
(1) Ciclo de Vida do Token
| Token | Período de Validade | Armazenamento | Propósito |
|---|---|---|---|
| Access Token | 15 minutos | Cookie httpOnly | Autenticação de API |
| Refresh Token | 7 dias | Cookie httpOnly | Atualizar Access Token |
(1) ▶Exemplo: API de Refresh Token
// server/api/auth/refresh.post.ts
export default defineEventHandler((event) => {
const refreshToken = getCookie(event, 'refresh-token')
if (!refreshToken) {
throw createError({ statusCode: 401, message: 'No refresh token' })
}
try {
const payload = verifyRefreshToken(refreshToken)
const newAccessToken = signAccessToken({
userId: payload.userId,
email: payload.email,
role: payload.role
})
setCookie(event, 'access-token', newAccessToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900
})
return { message: 'Token refreshed' }
} catch {
// Refresh token expirado - forçar login
setCookie(event, 'access-token', '', { maxAge: 0 })
setCookie(event, 'refresh-token', '', { maxAge: 0 })
throw createError({ statusCode: 401, message: 'Refresh token expired' })
}
})
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Middleware de Autenticação de API
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const url = getRequestURL(event)
if (!url.pathname.startsWith('/api/admin') && !url.pathname.startsWith('/api/cart')) return
const token = getCookie(event, 'access-token')
if (!token) {
throw createError({ statusCode: 401, message: 'Authentication required' })
}
try {
const payload = verifyAccessToken(token)
event.context.user = payload
// Rotas somente para administradores
if (url.pathname.startsWith('/api/admin') && payload.role !== 'admin') {
throw createError({ statusCode: 403, message: 'Admin access required' })
}
} catch {
throw createError({ statusCode: 401, message: 'Invalid or expired token' })
}
})
Saída:
// Execução Bem-sucedida
7. Exemplo Completo: Fluxo de Autenticação do MegaShop
// nuxt.config.ts - Segredos JWT no runtimeConfig
export default defineNuxtConfig({
runtimeConfig: {
jwtAccessSecret: process.env.JWT_ACCESS_SECRET || 'dev-access-secret',
jwtRefreshSecret: process.env.JWT_REFRESH_SECRET || 'dev-refresh-secret',
googleClientId: process.env.GOOGLE_CLIENT_ID,
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
public: {
apiBase: process.env.API_BASE || 'http://localhost:3000/api'
}
}
})
<!-- pages/login.vue -->
<template>
<div class="login-page">
<h1>Sign In to MegaShop</h1>
<!-- Formulário de login por email -->
<form @submit.prevent="loginWithEmail">
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="password" type="password" placeholder="Password" required />
<button type="submit">Sign In</button>
</form>
<!-- Login social -->
<div class="social-login">
<p>Or sign in with:</p>
<a href="/api/auth/google" class="btn-google">Sign in with Google</a>
<a href="/api/auth/github" class="btn-github">Sign in with GitHub</a>
</div>
</div>
</template>
<script setup lang="ts">
const email = ref('')
const password = ref('')
async function loginWithEmail() {
await $fetch('/api/auth/login', {
method: 'POST',
body: { email: email.value, password: password.value }
})
navigateTo('/')
}
</script>
❓Perguntas Frequentes
state no OAuth 2.0?state é uma string aleatória que é verificada durante o callback para garantir que a requisição vem da sua aplicação e não de um invasor.useCookie('access-token') lê do cabeçalho cookie da requisição durante o SSR e de document.cookie no lado do cliente; os valores são consistentes em ambos os lados.📖Resumo
- MegaShop usa JWT + cookies httpOnly: stateless, resistente a XSS e compatível com SSR
- Rotação dual-token: Access Token (15 minutos) + Refresh Token (7 dias), renovação transparente
- Fluxo OAuth 2.0: Redirecionar para Google → Autorização do Usuário → Callback para Trocar por Token → Criar Usuário → Emitir JWT
- Autenticação via middleware do servidor: Ler o JWT do cookie → Verificar → Injetar em event.context.user
- Armazenar todas as chaves no campo privado
runtimeConfig; nunca as exponha ao cliente
📝Exercícios
- Exercício Básico (Dificuldade: ⭐): Implemente uma API de registro e login por email, usando um cookie
httpOnlypara armazenar o JWT - Exercício Avançado (Dificuldade: ⭐⭐): Implemente um mecanismo de refresh token para que o access token seja atualizado automaticamente quando expirar, sem que o usuário perceba.
- Desafio (Dificuldade: ⭐⭐⭐): Integre login social Google OAuth2 e implemente um fluxo completo de registro/login/refresh/logout
---|



