Skip to main content
POST
/
logout
curl -X POST "https://auth-api.yourdomain.com/logout" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json"
{
  "message": "Logout successful",
  "timestamp": "2023-12-01T10:30:00Z"
}

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

{
  "message": "Logout successful",
  "timestamp": "2023-12-01T10:30:00Z"
}

Response Fields

FieldTypeDescription
messagestringSuccess message
timestampstringLogout 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');
  });
});
I