first commit
This commit is contained in:
370
JWT_REFERENCE.md
Normal file
370
JWT_REFERENCE.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# 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! 🎉**
|
||||
Reference in New Issue
Block a user