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
- Authentication Architecture Design: A Comparison of Session, JWT, and Cookie Approaches
- JWT Implementation: Issuance/Verification + Secure Storage of httpOnly Cookies
- OAuth 2.0 Integration: Google/GitHub Social Login Process
- Refresh token mechanism: Access Token + Refresh Token dual-token rotation
- MegaShop Authentication Hands-On: Alice Registration and Login + Bob (Administrator) OAuth2
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:
// 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 |
(2) Authentication Process Sequence Diagram
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
// 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:
// Execution Successful
(2) ▶ Example: Email Registration API
// 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:
// Execution Successful
(3) ▶ Example: Email Login API
// 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:
// Execution Successful
5. OAuth 2.0 Social Login
(1) ▶ Example: Google OAuth 2.0 Login
// 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:
// Execution Successful
(2) ▶ Example: Google OAuth 2.0 Callback Handling
// 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:
// 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
// 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:
// Execution Successful
(2) ▶ Example: API Authentication Middleware
// 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:
// Execution Successful
7. Comprehensive Example: MegaShop Authentication Process
// 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'
}
}
})
<!-- 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
state parameter in OAuth 2.0?state is a random string that is verified during the callback to ensure the request comes from your application and not an attacker.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.📖 Summary
- MegaShop uses JWT + httpOnly cookies: stateless, XSS-resistant, and SSR-friendly
- Dual-token rotation: Access Token (15 minutes) + Refresh Token (7 days), seamless refresh
- OAuth 2.0 Flow: Redirect to Google → User Authorization → Callback to Exchange for a Token → Create User → Issue JWT
- Server middleware authentication: Read the JWT from the cookie → Verify → Inject into event.context.user
- Store all keys in the private
runtimeConfigfield; never expose them to the client.
📝 Exercises
- Basic Problem (Difficulty: ⭐): Implement an API for email registration and login, using an
httpOnlycookie to store the JWT - Advanced Exercise (Difficulty: ⭐⭐): Implement a refresh token mechanism so that the access token is automatically refreshed when it expires, without the user noticing.
- Challenge (Difficulty: ⭐⭐⭐): Integrate Google OAuth2 social login and implement a complete registration/login/refresh/logout workflow
---|



