404 Not Found

404 Not Found


nginx

OAuth2 + JWT Authentication

Alice needs to create an account to purchase items, and Bob needs to log in to the admin panel. However, MegaShop does not yet have an authentication system—anyone can access the admin interface. Charlie needs to support both email registration and Google/GitHub social logins, while also ensuring API security.

1. What You'll Learn


2. A True Story of an Architect

(1) Pain Point: A Critical Security Vulnerability Due to Lack of Authentication

Anyone can read Alice's shopping cart data; Bob's management interface lacks authentication; and Charlie discovered that an attacker could forge a request to delete an item. MegaShop must have an authentication system.

(2) JWT + OAuth2 Solution

JWT Stateless Authentication + OAuth 2.0 Social Login: Balancing Security and User Experience:

TYPESCRIPT
// Server: issue JWT after login
const token = jwt.sign({ userId: user.id, role: user.role }, secret, { expiresIn: '15m' })
setCookie(event, 'access-token', token, { httpOnly: true })

(3) Benefits: Safety + Convenience

Alice uses Google Sign-In, Bob accesses the admin panel using an administrator account, and API requests include a JWT for automatic authentication.


3. Comparison of Authentication Schemes

(1) Three Authentication Schemes

Dimension Session + Cookie JWT + Cookie JWT + localStorage
State Stateful (server-side storage) Stateless Stateless
Scalability 🔴 Requires shared sessions 🟢 Intrinsically distributed 🟢 Distributed
SSR Compatibility ✅ Automatic Cookie Persistence ✅ Automatic Cookie Persistence ❌ Manual Handling Required
CSRF Risk 🔴 Requires Protection 🟢 Protected by httpOnly 🟢 No Cookie
XSS Risk 🟢 httpOnly 🟢 httpOnly 🔴 JS Readable
Log Out ✅ Delete Session Immediately ⚠️ Blacklist Required ⚠️ Blacklist Required
💡 Tip: MegaShop uses the JWT + httpOnly cookie approach—stateless architecture is suitable for distributed systems, httpOnly prevents XSS, and cookies are automatically included in SSR requests.

(2) Authentication Process Sequence Diagram

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. JWT Issuance and Verification

(1) ▶ Example: JWT Utility Functions

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
}

Output:

TEXT
// Execution Successful

(2) ▶ Example: Email Registration API

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' })
  }

  // Check if user exists
  const existing = mockUsers.find(u => u.email === email)
  if (existing) {
    throw createError({ statusCode: 409, message: 'Email already registered' })
  }

  // Create user
  const user = {
    id: mockUsers.length + 1,
    email,
    name: name || 'Customer',
    password: await hashPassword(password),
    role: 'customer' as const
  }
  mockUsers.push(user)

  // Issue tokens
  const payload = { userId: user.id, email: user.email, role: user.role }
  const accessToken = signAccessToken(payload)
  const refreshToken = signRefreshToken(payload)

  // Set httpOnly cookies
  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 } }
})

Output:

TEXT
// Execution Successful

(3) ▶ Example: Email Login API

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 } }
})

Output:

TEXT
// Execution Successful

5. OAuth 2.0 Social Login

(1) ▶ Example: Google OAuth 2.0 Login

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)
})

Output:

TEXT
// Execution Successful

(2) ▶ Example: Google OAuth 2.0 Callback Handling

TYPESCRIPT
// server/api/auth/google/callback.get.ts
export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig()
  const code = getQuery(event).code as string

  // Exchange code for 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'
    }
  })

  // Get user info from Google
  const userInfo = await $fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { Authorization: `Bearer ${tokenResponse.access_token}` }
  })

  // Find or create user
  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)
  }

  // Issue JWT tokens
  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, '/')
})

Output:

TEXT
// Execution Successful

6. Dual-Token Rotation Mechanism

(1) Token Lifecycle

Token Validity Period Storage Purpose
Access Token 15 minutes httpOnly Cookie API Authentication
Refresh Token 7 days httpOnly Cookie Refresh Access Token

(1) ▶ Example: Refresh Token API

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 expired - force login
    setCookie(event, 'access-token', '', { maxAge: 0 })
    setCookie(event, 'refresh-token', '', { maxAge: 0 })
    throw createError({ statusCode: 401, message: 'Refresh token expired' })
  }
})

Output:

TEXT
// Execution Successful

(2) ▶ Example: API Authentication Middleware

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

    // Admin-only routes
    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' })
  }
})

Output:

TEXT
// Execution Successful

7. Comprehensive Example: MegaShop Authentication Process

TYPESCRIPT
// nuxt.config.ts - JWT secrets in 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>

    <!-- Email login form -->
    <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>

    <!-- Social login -->
    <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>

❓ FAQ

Q Should JWT be stored in a cookie or in localStorage?
A We recommend using an httpOnly cookie—this prevents XSS attacks from stealing the token, and it is automatically included during server-side rendering (SSR). localStorage is vulnerable to XSS attacks.
Q Why does the Access Token only last 15 minutes?
A A shorter validity period reduces the risk of token leakage. Combined with the Refresh Token, this enables seamless renewal, so users don't have to log in frequently.
Q What should I do if a refresh token is compromised?
A Implement refresh token rotation—each time you use a refresh token to obtain a new access token, issue a new refresh token at the same time, causing the old one to expire immediately.
Q What is the purpose of the state parameter in OAuth 2.0?
A To prevent CSRF attacks. state is a random string that is verified during the callback to ensure the request comes from your application and not an attacker.
Q How do you read cookies for authentication during SSR?
A useCookie('access-token') reads from the request's cookie header during SSR and from document.cookie on the client side; the values are consistent on both sides.
Q What should I do if the JWT cannot be automatically revoked?
A A short expiration time (15 minutes) minimizes the impact. If immediate revocation is required, maintain a token blacklist (stored in Redis); the overhead of checking the blacklist via middleware is minimal.

📖 Summary


📝 Exercises

  1. Basic Problem (Difficulty: ⭐): Implement an API for email registration and login, using an httpOnly cookie to store the JWT
  2. Advanced Exercise (Difficulty: ⭐⭐): Implement a refresh token mechanism so that the access token is automatically refreshed when it expires, without the user noticing.
  3. Challenge (Difficulty: ⭐⭐⭐): Integrate Google OAuth2 social login and implement a complete registration/login/refresh/logout workflow

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏