curl -X POST "https://auth-api.yourdomain.com/logout" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json"
const response = await fetch('/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
console.log('Logout successful:', result);
// Clear local tokens
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
} else {
console.error('Logout failed');
}
import requests
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
response = requests.post(
'https://auth-api.yourdomain.com/logout',
headers=headers
)
if response.status_code == 200:
result = response.json()
print(f"Logout successful: {result}")
# Clear stored tokens
del access_token
del refresh_token
else:
print("Logout failed")
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func logout(accessToken string) error {
req, err := http.NewRequest("POST", "https://auth-api.yourdomain.com/logout", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
fmt.Println("Logout successful")
return nil
}
return fmt.Errorf("logout failed with status: %d", resp.StatusCode)
}
{
"message": "Logout successful",
"timestamp": "2023-12-01T10:30:00Z"
}
{
"code": 401,
"msg": "Invalid or expired token",
"details": "JWT token is invalid or has expired"
}
User Management
Logout User
Invalidate user’s refresh tokens and end the session
POST
/
logout
curl -X POST "https://auth-api.yourdomain.com/logout" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json"
const response = await fetch('/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
console.log('Logout successful:', result);
// Clear local tokens
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
} else {
console.error('Logout failed');
}
import requests
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
response = requests.post(
'https://auth-api.yourdomain.com/logout',
headers=headers
)
if response.status_code == 200:
result = response.json()
print(f"Logout successful: {result}")
# Clear stored tokens
del access_token
del refresh_token
else:
print("Logout failed")
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func logout(accessToken string) error {
req, err := http.NewRequest("POST", "https://auth-api.yourdomain.com/logout", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
fmt.Println("Logout successful")
return nil
}
return fmt.Errorf("logout failed with status: %d", resp.StatusCode)
}
{
"message": "Logout successful",
"timestamp": "2023-12-01T10:30:00Z"
}
{
"code": 401,
"msg": "Invalid or expired token",
"details": "JWT token is invalid or has expired"
}
Overview
The/logout endpoint invalidates the user’s refresh tokens and ends their session. This is a security best practice that ensures tokens cannot be used after the user has explicitly logged out.
This endpoint requires a valid JWT token in the Authorization header.
Request
curl -X POST "https://auth-api.yourdomain.com/logout" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json"
const response = await fetch('/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
console.log('Logout successful:', result);
// Clear local tokens
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
} else {
console.error('Logout failed');
}
import requests
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
response = requests.post(
'https://auth-api.yourdomain.com/logout',
headers=headers
)
if response.status_code == 200:
result = response.json()
print(f"Logout successful: {result}")
# Clear stored tokens
del access_token
del refresh_token
else:
print("Logout failed")
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func logout(accessToken string) error {
req, err := http.NewRequest("POST", "https://auth-api.yourdomain.com/logout", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
fmt.Println("Logout successful")
return nil
}
return fmt.Errorf("logout failed with status: %d", resp.StatusCode)
}
Response
{
"message": "Logout successful",
"timestamp": "2023-12-01T10:30:00Z"
}
{
"code": 401,
"msg": "Invalid or expired token",
"details": "JWT token is invalid or has expired"
}
Response Fields
| Field | Type | Description |
|---|---|---|
message | string | Success message |
timestamp | string | Logout timestamp |
Implementation Examples
React Hook for Logout
import { useState } from 'react';
import { useAuth } from './useAuth';
export function useLogout() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const { accessToken, clearTokens } = useAuth();
const logout = async () => {
setLoading(true);
setError(null);
try {
// Call logout endpoint to invalidate server-side tokens
const response = await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Logout failed');
}
// Clear local tokens regardless of server response
clearTokens();
// Redirect to login page or home
window.location.href = '/login';
return await response.json();
} catch (err) {
setError(err.message);
// Still clear local tokens even if server call fails
clearTokens();
throw err;
} finally {
setLoading(false);
}
};
return { logout, loading, error };
}
// Usage in component
function LogoutButton() {
const { logout, loading } = useLogout();
const handleLogout = async () => {
try {
await logout();
} catch (error) {
console.error('Logout error:', error);
// Handle error (show toast, etc.)
}
};
return (
<button
onClick={handleLogout}
disabled={loading}
className="logout-button"
>
{loading ? 'Logging out...' : 'Logout'}
</button>
);
}
Complete Auth Context
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [accessToken, setAccessToken] = useState(null);
const [refreshToken, setRefreshToken] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Load tokens from localStorage on app start
const storedAccessToken = localStorage.getItem('access_token');
const storedRefreshToken = localStorage.getItem('refresh_token');
if (storedAccessToken) {
setAccessToken(storedAccessToken);
setRefreshToken(storedRefreshToken);
// Optionally verify token and load user data
loadUserFromToken(storedAccessToken);
}
setLoading(false);
}, []);
const clearTokens = () => {
setAccessToken(null);
setRefreshToken(null);
setUser(null);
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
};
const logout = async () => {
try {
if (accessToken) {
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
}
} catch (error) {
console.error('Server logout failed:', error);
} finally {
// Always clear local tokens
clearTokens();
}
};
const value = {
user,
accessToken,
refreshToken,
loading,
logout,
clearTokens,
isAuthenticated: !!accessToken
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Logout with Confirmation
import { useState } from 'react';
import { useAuth } from './useAuth';
function LogoutWithConfirmation() {
const [showConfirmation, setShowConfirmation] = useState(false);
const { logout } = useAuth();
const handleLogoutClick = () => {
setShowConfirmation(true);
};
const handleConfirmLogout = async () => {
try {
await logout();
setShowConfirmation(false);
} catch (error) {
console.error('Logout failed:', error);
}
};
const handleCancelLogout = () => {
setShowConfirmation(false);
};
if (showConfirmation) {
return (
<div className="logout-confirmation">
<div className="confirmation-dialog">
<h3>Confirm Logout</h3>
<p>Are you sure you want to log out?</p>
<div className="confirmation-buttons">
<button
onClick={handleConfirmLogout}
className="btn-danger"
>
Yes, Logout
</button>
<button
onClick={handleCancelLogout}
className="btn-secondary"
>
Cancel
</button>
</div>
</div>
</div>
);
}
return (
<button onClick={handleLogoutClick} className="logout-button">
Logout
</button>
);
}
Node.js Logout Handler
const jwt = require('jsonwebtoken');
const redis = require('redis');
// Logout endpoint handler
app.post('/logout', authenticateToken, async (req, res) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
const decoded = jwt.decode(token);
if (decoded && decoded.sub) {
const userId = decoded.sub;
// Invalidate all refresh tokens for this user
const refreshTokenPattern = `refresh_token:${userId}:*`;
const keys = await redis.keys(refreshTokenPattern);
if (keys.length > 0) {
await redis.del(keys);
}
// Add access token to blacklist (optional, for extra security)
const tokenId = decoded.jti || token.substring(0, 10);
const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
if (expiresIn > 0) {
await redis.setex(`blacklist:${tokenId}`, expiresIn, 'true');
}
// Clear any reauthentication state
await redis.del(`reauth:${userId}`);
// Log the logout event
console.log(`User ${userId} logged out at ${new Date().toISOString()}`);
}
res.json({
message: 'Logout successful',
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({
code: 500,
msg: 'Internal server error'
});
}
});
// Middleware to check token blacklist
function checkTokenBlacklist(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
const decoded = jwt.decode(token);
const tokenId = decoded?.jti || token.substring(0, 10);
redis.get(`blacklist:${tokenId}`, (err, result) => {
if (result) {
return res.status(401).json({
code: 401,
msg: 'Token has been invalidated'
});
}
next();
});
} else {
next();
}
}
// Apply blacklist check to protected routes
app.use('/user', checkTokenBlacklist);
app.use('/admin', checkTokenBlacklist);
Global Logout (All Devices)
// Logout from all devices
app.post('/logout/all', authenticateToken, async (req, res) => {
try {
const userId = req.user.sub;
// Invalidate all refresh tokens for this user
const refreshTokenPattern = `refresh_token:${userId}:*`;
const keys = await redis.keys(refreshTokenPattern);
if (keys.length > 0) {
await redis.del(keys);
}
// Increment user's token version to invalidate all existing JWTs
await redis.incr(`user_token_version:${userId}`);
// Clear reauthentication state
await redis.del(`reauth:${userId}`);
res.json({
message: 'Logged out from all devices',
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
code: 500,
msg: 'Internal server error'
});
}
});
// React component for logout options
function LogoutOptions() {
const { logout } = useAuth();
const [showOptions, setShowOptions] = useState(false);
const handleLogoutThisDevice = async () => {
await logout();
};
const handleLogoutAllDevices = async () => {
try {
const response = await fetch('/api/auth/logout/all', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
await logout(); // Clear local tokens
}
} catch (error) {
console.error('Global logout failed:', error);
}
};
return (
<div className="logout-options">
<button onClick={() => setShowOptions(!showOptions)}>
Logout Options
</button>
{showOptions && (
<div className="logout-menu">
<button onClick={handleLogoutThisDevice}>
Logout This Device
</button>
<button onClick={handleLogoutAllDevices}>
Logout All Devices
</button>
</div>
)}
</div>
);
}
Use Cases
Standard Logout
- User clicks logout button
- Invalidate refresh tokens
- Clear local storage
- Redirect to login page
Security Logout
- Logout from all devices when password is changed
- Logout when suspicious activity is detected
- Logout when account is compromised
Session Management
- Automatic logout on token expiration
- Logout on browser close (optional)
- Logout after period of inactivity
Security Considerations
- Token Invalidation: Always invalidate refresh tokens on the server
- Local Cleanup: Clear all local tokens and user data
- Blacklisting: Consider blacklisting access tokens for high-security applications
- Audit Logging: Log logout events for security monitoring
- Graceful Degradation: Handle logout failures gracefully
Best Practices
Client-Side
- Always clear local tokens, even if server call fails
- Redirect user to appropriate page after logout
- Show loading state during logout process
- Handle network errors gracefully
Server-Side
- Invalidate all refresh tokens for the user
- Clear any session-related data (reauthentication state, etc.)
- Log logout events for audit purposes
- Return success even if token is already invalid
Rate Limiting
- Endpoint: 20 requests per 5 minutes per user
- Purpose: Prevent abuse while allowing legitimate logout attempts
- Headers: Standard rate limiting headers included in response
Related Endpoints
- Login - Opposite operation to logout
- Reauthenticate - Alternative to logout for security
- Token Refresh - Invalidated by logout
Testing
// Jest test example
describe('Logout', () => {
test('should logout successfully with valid token', async () => {
const response = await request(app)
.post('/logout')
.set('Authorization', `Bearer ${validToken}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('message');
expect(response.body.message).toContain('successful');
});
test('should fail without token', async () => {
const response = await request(app)
.post('/logout');
expect(response.status).toBe(401);
});
test('should invalidate refresh tokens', async () => {
// Login to get tokens
const loginResponse = await request(app)
.post('/token')
.send({
grant_type: 'password',
email: 'test@example.com',
password: 'password123'
});
const { access_token, refresh_token } = loginResponse.body;
// Logout
await request(app)
.post('/logout')
.set('Authorization', `Bearer ${access_token}`);
// Try to use refresh token - should fail
const refreshResponse = await request(app)
.post('/token')
.send({
grant_type: 'refresh_token',
refresh_token: refresh_token
});
expect(refreshResponse.status).toBe(400);
});
test('should handle logout from all devices', async () => {
const response = await request(app)
.post('/logout/all')
.set('Authorization', `Bearer ${validToken}`);
expect(response.status).toBe(200);
expect(response.body.message).toContain('all devices');
});
});
⌘I