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"
Response
200 - Success
401 - Unauthorized
{
"message" : "Logout successful" ,
"timestamp" : "2023-12-01T10:30:00Z"
}
Response Fields
Field Type Description messagestring Success message timestampstring 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
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' );
});
});