370 lines
7.8 KiB
Markdown
370 lines
7.8 KiB
Markdown
# JWT Token Reference Guide
|
|
|
|
## 🎯 What's in Your JWT Token
|
|
|
|
Your JWT tokens now contain all essential user information, so you don't need to make database calls for basic user data.
|
|
|
|
---
|
|
|
|
## 📦 Token Payload Contents
|
|
|
|
### Access Token & Refresh Token Include:
|
|
|
|
```javascript
|
|
{
|
|
// User Identification
|
|
userId: "507f1f77bcf86cd799439011", // MongoDB _id
|
|
steamId: "76561198012345678", // Steam ID64
|
|
|
|
// Profile Information (NEW!)
|
|
username: "YourSteamName", // Display name
|
|
avatar: "https://avatars.cloudflare.steamstatic.com/...", // Profile picture URL
|
|
|
|
// Permissions
|
|
staffLevel: 0, // 0=User, 1=Support, 2=Mod, 3=Admin
|
|
|
|
// JWT Standard Claims
|
|
iat: 1704825600, // Issued at (timestamp)
|
|
exp: 1704826500, // Expires at (timestamp)
|
|
iss: "turbotrades", // Issuer
|
|
aud: "turbotrades-api" // Audience
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔍 How to Access Token Data
|
|
|
|
### Frontend (Browser)
|
|
|
|
The tokens are in httpOnly cookies, so JavaScript can't read them directly. But you can:
|
|
|
|
#### Option 1: Decode from API Response
|
|
```javascript
|
|
// After login or on page load, call this endpoint
|
|
const response = await fetch('/auth/decode-token', {
|
|
credentials: 'include' // Send cookies
|
|
});
|
|
|
|
const data = await response.json();
|
|
console.log(data.decoded);
|
|
// {
|
|
// userId: "...",
|
|
// steamId: "...",
|
|
// username: "YourName",
|
|
// avatar: "https://...",
|
|
// staffLevel: 0,
|
|
// ...
|
|
// }
|
|
```
|
|
|
|
#### Option 2: Get from /auth/me Endpoint
|
|
```javascript
|
|
const response = await fetch('/auth/me', {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const data = await response.json();
|
|
console.log(data.user);
|
|
// Full user object from database
|
|
```
|
|
|
|
### Backend (Server-Side)
|
|
|
|
When you use the `authenticate` middleware, the decoded token data is available:
|
|
|
|
```javascript
|
|
import { authenticate } from './middleware/auth.js';
|
|
|
|
fastify.get('/protected', {
|
|
preHandler: authenticate
|
|
}, async (request, reply) => {
|
|
// Full user object from database
|
|
console.log(request.user.username);
|
|
console.log(request.user.avatar);
|
|
|
|
return { message: `Hello ${request.user.username}!` };
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 Frontend Usage Examples
|
|
|
|
### React Component
|
|
|
|
```javascript
|
|
import { useState, useEffect } from 'react';
|
|
|
|
function UserProfile() {
|
|
const [user, setUser] = useState(null);
|
|
|
|
useEffect(() => {
|
|
// Get user data from token
|
|
fetch('/auth/decode-token', { credentials: 'include' })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
setUser(data.decoded);
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
if (!user) return <div>Loading...</div>;
|
|
|
|
return (
|
|
<div>
|
|
<img src={user.avatar} alt={user.username} />
|
|
<h1>{user.username}</h1>
|
|
<p>Steam ID: {user.steamId}</p>
|
|
{user.staffLevel > 0 && <span>Staff</span>}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Vue Component
|
|
|
|
```vue
|
|
<template>
|
|
<div v-if="user">
|
|
<img :src="user.avatar" :alt="user.username" />
|
|
<h1>{{ user.username }}</h1>
|
|
<p>Steam ID: {{ user.steamId }}</p>
|
|
<span v-if="user.staffLevel > 0">Staff</span>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
const user = ref(null);
|
|
|
|
onMounted(async () => {
|
|
const response = await fetch('/auth/decode-token', {
|
|
credentials: 'include'
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
user.value = data.decoded;
|
|
}
|
|
});
|
|
</script>
|
|
```
|
|
|
|
### Vanilla JavaScript
|
|
|
|
```javascript
|
|
// Get user info on page load
|
|
async function loadUserInfo() {
|
|
try {
|
|
const response = await fetch('/auth/decode-token', {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const user = data.decoded;
|
|
|
|
// Update UI
|
|
document.getElementById('username').textContent = user.username;
|
|
document.getElementById('avatar').src = user.avatar;
|
|
|
|
// Store in memory if needed
|
|
window.currentUser = user;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load user:', error);
|
|
}
|
|
}
|
|
|
|
loadUserInfo();
|
|
```
|
|
|
|
---
|
|
|
|
## 🔐 Security Notes
|
|
|
|
### Why httpOnly Cookies?
|
|
|
|
✅ **Prevents XSS attacks** - JavaScript can't access the token
|
|
✅ **Automatic sending** - Browser sends cookies automatically
|
|
✅ **Secure storage** - Tokens stored securely by browser
|
|
|
|
### Token Lifetimes
|
|
|
|
- **Access Token:** 15 minutes (short-lived for security)
|
|
- **Refresh Token:** 7 days (for convenience)
|
|
|
|
When access token expires:
|
|
1. Frontend gets 401 error with "TokenExpired"
|
|
2. Call `/auth/refresh` to get new tokens
|
|
3. Retry the original request
|
|
|
|
---
|
|
|
|
## 📡 API Endpoints Reference
|
|
|
|
### Check Token Contents
|
|
```bash
|
|
GET /auth/decode-token
|
|
|
|
# With cookie (automatic in browser)
|
|
curl http://localhost:3000/auth/decode-token \
|
|
--cookie "accessToken=YOUR_TOKEN"
|
|
|
|
# Response:
|
|
{
|
|
"success": true,
|
|
"decoded": {
|
|
"userId": "...",
|
|
"steamId": "...",
|
|
"username": "...",
|
|
"avatar": "...",
|
|
"staffLevel": 0,
|
|
"iat": 1704825600,
|
|
"exp": 1704826500
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get Full User Profile
|
|
```bash
|
|
GET /auth/me
|
|
|
|
curl http://localhost:3000/auth/me \
|
|
--cookie "accessToken=YOUR_TOKEN"
|
|
|
|
# Response:
|
|
{
|
|
"success": true,
|
|
"user": {
|
|
"_id": "...",
|
|
"username": "...",
|
|
"steamId": "...",
|
|
"avatar": "...",
|
|
"balance": 0,
|
|
"email": {...},
|
|
"staffLevel": 0,
|
|
...
|
|
}
|
|
}
|
|
```
|
|
|
|
### Refresh Tokens
|
|
```bash
|
|
POST /auth/refresh
|
|
|
|
curl -X POST http://localhost:3000/auth/refresh \
|
|
--cookie "refreshToken=YOUR_REFRESH_TOKEN"
|
|
|
|
# Response:
|
|
{
|
|
"success": true,
|
|
"message": "Tokens refreshed successfully",
|
|
"accessToken": "new-token",
|
|
"refreshToken": "new-refresh-token"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 💡 Best Practices
|
|
|
|
### ✅ DO
|
|
|
|
- Store tokens in httpOnly cookies (already done)
|
|
- Use `/auth/decode-token` to get user info for UI
|
|
- Implement automatic token refresh on 401 errors
|
|
- Clear tokens on logout
|
|
- Use HTTPS in production
|
|
|
|
### ❌ DON'T
|
|
|
|
- Don't store tokens in localStorage (XSS vulnerable)
|
|
- Don't store sensitive data in tokens (keep them small)
|
|
- Don't decode tokens client-side if httpOnly (you can't)
|
|
- Don't use long-lived access tokens
|
|
|
|
---
|
|
|
|
## 🔄 Token Refresh Flow
|
|
|
|
```javascript
|
|
async function fetchWithAuth(url, options = {}) {
|
|
// First attempt with existing token
|
|
let response = await fetch(url, {
|
|
...options,
|
|
credentials: 'include'
|
|
});
|
|
|
|
// If token expired, refresh and retry
|
|
if (response.status === 401) {
|
|
const error = await response.json();
|
|
|
|
if (error.error === 'TokenExpired') {
|
|
// Refresh tokens
|
|
const refreshResponse = await fetch('/auth/refresh', {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (refreshResponse.ok) {
|
|
// Retry original request
|
|
response = await fetch(url, {
|
|
...options,
|
|
credentials: 'include'
|
|
});
|
|
} else {
|
|
// Refresh failed, redirect to login
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
// Usage
|
|
const data = await fetchWithAuth('/user/profile');
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Token Size Comparison
|
|
|
|
**Before (without username/avatar):**
|
|
- Token size: ~200 bytes
|
|
- Needs database call to get name/avatar
|
|
|
|
**After (with username/avatar):**
|
|
- Token size: ~350 bytes
|
|
- No database call needed for basic info
|
|
- **Still well within JWT size limits (8KB)**
|
|
|
|
---
|
|
|
|
## 🎯 Summary
|
|
|
|
**What's in the token:**
|
|
- ✅ User ID (database reference)
|
|
- ✅ Steam ID (for Steam API calls)
|
|
- ✅ Username (display name)
|
|
- ✅ Avatar URL (profile picture)
|
|
- ✅ Staff Level (permissions)
|
|
|
|
**How to use it:**
|
|
- Frontend: Call `/auth/decode-token` or `/auth/me`
|
|
- Backend: Access via `request.user` (after authenticate middleware)
|
|
- Automatic: Cookies sent with every request
|
|
|
|
**Benefits:**
|
|
- No database calls for basic user info
|
|
- Faster UI rendering
|
|
- Self-contained authentication
|
|
- Stateless (can scale horizontally)
|
|
|
|
---
|
|
|
|
**Your JWT tokens now include everything needed for displaying user information! 🎉** |