404 Not Found

404 Not Found


nginx

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


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:

TYPESCRIPT
// 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
💡 Dica: O MegaShop usa a abordagem JWT + cookie httpOnly — arquitetura stateless adequada para sistemas distribuídos, httpOnly previne XSS, e cookies são automaticamente incluídos em requisições SSR.

(2) Diagrama de Sequência do Fluxo de Autenticação

100%
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

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

(2) ▶Exemplo: API de Registro por Email

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

(3) ▶Exemplo: API de Login por Email

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

5. Login Social OAuth 2.0

(1) ▶Exemplo: Login Google OAuth 2.0

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

(2) ▶Exemplo: Tratamento do Callback Google OAuth 2.0

TYPESCRIPT
// 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:

TEXT
// 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

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

(2) ▶Exemplo: Middleware de Autenticação de API

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

7. Exemplo Completo: Fluxo de Autenticação do MegaShop

TYPESCRIPT
// 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'
    }
  }
})
VUE
<!-- 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

P O JWT deve ser armazenado em um cookie ou no localStorage?
R Recomendamos usar um cookie httpOnly — isso previne ataques XSS de roubar o token, e ele é automaticamente incluído durante a renderização do lado do servidor (SSR). localStorage é vulnerável a ataques XSS.
P Por que o Access Token dura apenas 15 minutos?
R Um período de validade mais curto reduz o risco de vazamento do token. Combinado com o Refresh Token, isso permite renovação transparente, para que os usuários não precisem fazer login com frequência.
P O que devo fazer se um refresh token for comprometido?
R Implemente rotação de refresh token — cada vez que usar um refresh token para obter um novo access token, emita um novo refresh token ao mesmo tempo, fazendo o antigo expirar imediatamente.
P Qual é o propósito do parâmetro state no OAuth 2.0?
R Prevenir ataques CSRF. 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.
P Como ler cookies para autenticação durante o SSR?
R 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.
P O que devo fazer se o JWT não puder ser revogado automaticamente?
R Um tempo de expiração curto (15 minutos) minimiza o impacto. Se a revogação imediata for necessária, mantenha uma blacklist de tokens (armazenada no Redis); a sobrecarga de verificar a blacklist via middleware é mínima.

📖Resumo


📝Exercícios

  1. Exercício Básico (Dificuldade: ⭐): Implemente uma API de registro e login por email, usando um cookie httpOnly para armazenar o JWT
  2. 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.
  3. Desafio (Dificuldade: ⭐⭐⭐): Integre login social Google OAuth2 e implemente um fluxo completo de registro/login/refresh/logout

---|

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%