Skip to main content

Logout and Token Revocation in React

Logout is the inverse of login: it terminates the user's session, invalidates their tokens, and clears stored credentials. A secure logout prevents an attacker from using a stolen token indefinitely and allows users to "log out from all devices," terminating all active sessions. This article covers React logout UI, server-side token revocation, refresh-token blacklisting, and multi-device logout.

React Logout Component

Simple Logout Button

// LogoutButton.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

function LogoutButton() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const navigate = useNavigate();

const handleLogout = async () => {
setLoading(true);
setError('');

try {
// Call logout endpoint
const response = await fetch('/api/logout', {
method: 'POST',
credentials: 'include' // Include httpOnly cookies
});

if (!response.ok) {
throw new Error('Logout failed');
}

// ✅ Logout successful; clear local state and redirect
// The backend cleared the httpOnly cookie

// If using context/Redux, clear user state:
// dispatch(clearUser());

navigate('/login');

} catch (err) {
setError(err.message || 'Logout failed');
} finally {
setLoading(false);
}
};

return (
<div>
<button onClick={handleLogout} disabled={loading}>
{loading ? 'Logging out...' : 'Log Out'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}

export default LogoutButton;

Logout in Navigation Menu

// Navigation.js
import React from 'react';
import LogoutButton from './LogoutButton';

function Navigation({ user }) {
return (
<nav className="navbar">
<div className="logo">MyApp</div>
<div className="nav-items">
<span>Welcome, {user.name || user.email}</span>
<LogoutButton />
</div>
</nav>
);
}

export default Navigation;

Backend: Logout Endpoint

Single Device Logout

// backend/routes/auth.js (Node.js/Express)
const jwt = require('jsonwebtoken');

app.post('/api/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;

// Optional: Verify the token is valid (to ensure it was the user's token)
if (refreshToken) {
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);

// Revoke the refresh token by marking it in the database
await RefreshToken.updateOne(
{ jti: decoded.jti },
{ revokedAt: new Date() }
);

console.log(`User ${decoded.sub} logged out`);

} catch (err) {
// Token invalid or expired; that is okay, still logout
console.error('Token verification failed on logout:', err);
}
}

// Clear httpOnly cookie on the client
res.clearCookie('accessToken', { path: '/', httpOnly: true });
res.clearCookie('refreshToken', { path: '/api/refresh', httpOnly: true });

return res.json({ message: 'Logged out successfully' });
});

Multi-Device Logout

Allow users to log out from all devices at once:

// backend/routes/auth.js
app.post('/api/logout-all-devices', async (req, res) => {
// Get user from verified access token
const token = req.cookies.accessToken;

if (!token) {
return res.status(401).json({ error: 'Not authenticated' });
}

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.sub;

// Revoke all refresh tokens for this user
const result = await RefreshToken.updateMany(
{ userId, revokedAt: null }, // Only non-revoked tokens
{ revokedAt: new Date() }
);

console.log(`User ${userId} logged out from ${result.modifiedCount} device(s)`);

// Clear cookies on this device
res.clearCookie('accessToken', { path: '/', httpOnly: true });
res.clearCookie('refreshToken', { path: '/api/refresh', httpOnly: true });

return res.json({
message: 'Logged out from all devices',
devicesLoggedOut: result.modifiedCount
});

} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
});

React: Account Settings with Logout Options

// pages/AccountSettings.js
import React, { useState } from 'react';

function AccountSettings({ user }) {
const [logoutLoading, setLogoutLoading] = useState(false);
const [logoutMessage, setLogoutMessage] = useState('');

const handleLogoutAllDevices = async () => {
setLogoutLoading(true);
setLogoutMessage('');

try {
const response = await fetch('/api/logout-all-devices', {
method: 'POST',
credentials: 'include'
});

if (!response.ok) {
throw new Error('Failed to log out from all devices');
}

const data = await response.json();
setLogoutMessage(`Logged out from ${data.devicesLoggedOut} device(s). Redirecting...`);

// Redirect to login after 2 seconds
setTimeout(() => {
window.location.href = '/login';
}, 2000);

} catch (err) {
setLogoutMessage(`Error: ${err.message}`);
} finally {
setLogoutLoading(false);
}
};

return (
<div className="account-settings">
<h2>Account Settings</h2>

<section className="security-section">
<h3>Security</h3>

<div className="logout-option">
<h4>Log Out</h4>
<p>End your session on this device.</p>
<a href="/logout" className="btn-secondary">Log Out</a>
</div>

<div className="logout-all-option">
<h4>Log Out From All Devices</h4>
<p>End all active sessions across all devices. You will be signed out immediately.</p>
<button
onClick={handleLogoutAllDevices}
disabled={logoutLoading}
className="btn-danger"
>
{logoutLoading ? 'Processing...' : 'Log Out From All Devices'}
</button>
{logoutMessage && (
<p className={logoutMessage.startsWith('Error') ? 'error' : 'success'}>
{logoutMessage}
</p>
)}
</div>
</section>
</div>
);
}

export default AccountSettings;

Token Revocation Strategies

Strategy 1: Revocation List (In-Memory or Database)

Store refresh token IDs (jti) that have been revoked. On every refresh request, check if the token is revoked:

// Pros: Simple, no token modification needed
// Cons: Requires database lookup on every refresh

app.post('/api/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;

try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);

// Check if token is revoked
const isRevoked = await RefreshToken.findOne({
jti: decoded.jti,
revokedAt: { $ne: null }
});

if (isRevoked) {
return res.status(401).json({ error: 'Token has been revoked' });
}

// Issue new access token
// ...

} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
});

Strategy 2: Rotating Refresh Tokens

On each refresh, issue a new refresh token and invalidate the old one (if it is used again, reject it):

// Pros: Even if token is stolen, each refresh creates a new one; old ones are invalidated
// Cons: More complex; requires tracking token families (to detect replay attacks)

app.post('/api/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;

try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);

// Check if this refresh token was already used (replay attack)
const tokenRecord = await RefreshToken.findOne({ jti: decoded.jti });

if (!tokenRecord || tokenRecord.revokedAt) {
// Token was revoked or already exchanged; suspect replay attack
// Revoke all tokens in the same family (all this user's tokens)
await RefreshToken.updateMany(
{ userId: decoded.sub },
{ revokedAt: new Date() }
);

return res.status(401).json({ error: 'Token replay detected; all sessions revoked' });
}

// Mark this token as used
await RefreshToken.updateOne(
{ jti: decoded.jti },
{ revokedAt: new Date() }
);

// Issue new access token AND new refresh token
const newAccessToken = jwt.sign(
{ sub: decoded.sub, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);

const newRefreshTokenId = crypto.randomBytes(32).toString('hex');
const newRefreshToken = jwt.sign(
{
sub: decoded.sub,
jti: newRefreshTokenId,
family: decoded.family, // Link to the original family
type: 'refresh'
},
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);

// Store new refresh token
await RefreshToken.create({
userId: decoded.sub,
jti: newRefreshTokenId,
family: decoded.family || newRefreshTokenId,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});

res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});

return res.json({
accessToken: newAccessToken,
expiresIn: 900
});

} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
});

Cleanup: Clear Local State on Logout

If you are using context or state management, clear user state on logout:

// AuthContext.js
import React, { createContext, useCallback, useState } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
const [user, setUser] = useState(null);

const logout = useCallback(async () => {
try {
await fetch('/api/logout', {
method: 'POST',
credentials: 'include'
});
} catch (err) {
console.error('Logout error:', err);
} finally {
// Clear user state regardless of logout success
setUser(null);
}
}, []);

return (
<AuthContext.Provider value={{ user, logout }}>
{children}
</AuthContext.Provider>
);
}

// Usage
import { useContext } from 'react';

function LogoutButton() {
const { logout } = useContext(AuthContext);

const handleClick = async () => {
await logout();
window.location.href = '/login';
};

return <button onClick={handleClick}>Log Out</button>;
}

Graceful Handling: What If Logout Fails?

If the backend is unreachable, still clear local state to log the user out on the client:

async function handleLogout() {
let logoutSuccess = false;

try {
const response = await fetch('/api/logout', {
method: 'POST',
credentials: 'include',
timeout: 5000 // 5-second timeout
});

logoutSuccess = response.ok;
} catch (err) {
console.error('Logout request failed:', err);
// Backend is down; continue with client-side logout
logoutSuccess = false;
}

// Clear local state even if logout request failed
// The httpOnly cookie may not be cleared, but the user is logged out locally
setUser(null);
navigate('/login');

if (!logoutSuccess) {
showNotification('Warning: Could not reach the server. You are logged out locally.');
}
}

Key Takeaways

  • Logout must revoke the refresh token on the server; without revocation, an attacker with a stolen refresh token can request new access tokens indefinitely
  • Always clear httpOnly cookies on logout using res.clearCookie()
  • Store refresh token IDs in a database and check for revocation on every refresh request
  • Provide a "log out from all devices" option that revokes all of the user's refresh tokens at once
  • Use rotating refresh tokens for higher security: issue a new refresh token on each refresh and invalidate the old one
  • Detect replay attacks (token reuse) by tracking which refresh tokens have been exchanged
  • Clear client-side user state on logout, even if the logout request fails; log out locally if the backend is unreachable
  • Test logout by attempting to use an old token after logout; verify it returns 401
  • Consider automatic logout after inactivity (e.g., 30 minutes); combine with "remember me" tokens for longer persistence

Frequently Asked Questions

What if someone forgets to log out on a shared computer?

Use inactivity timeouts: automatically log out after 15–30 minutes of no user interaction. Or, disable "remember me" for shared devices. Alternatively, provide a "log out from all devices" button so the original owner can revoke sessions from another device.

Should I keep a logout audit trail?

Yes. Log when a user logs out (timestamp, device, IP address) for security investigation. If a user reports unauthorized access, you can see if someone logged out at an unusual time or location.

Does logout clear the browser's browsing history or cache?

No. Logout clears only the authentication token/cookie. Browser history and cached assets remain. If privacy is critical, advise users to clear their browsing history manually using "Clear browsing data" in browser settings.

Clear localStorage on logout: localStorage.removeItem('accessToken'). However, using httpOnly cookies for both tokens is more secure and recommended.

How long should a refresh token be valid?

Typical ranges are 7–30 days. Shorter lifetimes (7 days) force more frequent login, improving security but reducing convenience. Longer lifetimes (30 days) improve convenience but increase exposure if stolen. Balance based on your app's risk profile.

Further Reading