Skip to content

Logout Flow

This page documents POST /api/v1/auth/logout.

This page was rewritten for v5

The previous version of this page described three separate flows — "Single Logout", "Global Logout" (POST /api/v1/auth/logout/all), and "Admin Force Logout" (POST /api/v1/admin/users/{userId}/logout) — with a request body containing refreshToken. None of that matches the current implementation. AuthController exposes exactly one logout endpoint, it takes no body, and — per LogoutCommandHandler's own log message — it is already a global logout: "Processing GLOBAL logout for user {UserId} - all sessions will be terminated". There is no selective/single-session logout, and no /logout/all or admin force-logout endpoint currently exists anywhere in the Auth module. If your product needs admin-initiated forced logout, treat it as a gap to raise with the Auth team, not an existing capability.

Overview

/api/v1/auth/logout is [AllowAnonymous] and idempotent: it always returns 204 No Content, whether or not a valid token was presented, and whether or not the underlying invalidation succeeds. There is no request/response body. The endpoint reads the user id and access-token jti directly from the claims of whatever Authorization header was sent.

Sequence Diagram

100% 💡 Use Ctrl + Scroll para zoom | Arraste para navegar

Step-by-Step Explanation

1. Send Logout Request

http
POST /api/v1/auth/logout
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

No body. If no Authorization header is present, or it doesn't carry a valid user id, the endpoint logs an idempotent no-op and still returns 204.

2. Invalidate the Session — Globally

Logout is global by design: it doesn't just kill the presented token, it invalidates every outstanding token for that user (all devices/sessions). This is implemented with the same two-layer mechanism used elsewhere in GrydAuth's defense-in-depth token validation:

  • the presented access token's jti is blacklisted individually (immediate effect for that exact token);
  • the user's token-version / timestamp blacklist is bumped, so every other previously-issued token — on any device — fails validation on its next use.
csharp
public class User
{
    public int TokenVersion { get; private set; }

    public void InvalidateAllTokens(string reason)
    {
        TokenVersion++;
    }
}

On validation elsewhere in the system:

csharp
if (tokenVersion != user.TokenVersion)
{
    // rejected — fail-secure
}

If your product needs single-device logout

GrydAuth v5 does not support it today — logging out terminates every session for the user. Don't build client UX (e.g. "log out of this device only") that assumes otherwise.

3. Client Cleanup

typescript
async function logout(): Promise<void> {
  try {
    await authApi.logout(); // no body, no refreshToken param
  } catch (error) {
    // Logout is idempotent server-side (always 204) — this really only
    // fires on network failure. Log and continue with client cleanup.
    console.warn('Logout call failed (network):', error);
  } finally {
    authStore.clearAll();
    router.push('/login');
  }
}

Error Scenarios

There isn't really an "error" path from the client's point of view — the endpoint is idempotent and always returns 204 No Content.

SituationHTTP StatusClient action
Valid token, invalidation succeeds204Clear local state, redirect to login
No/invalid token presented204Same — nothing to invalidate, treat as success
Underlying invalidation failure204Same — clear local state regardless; server logs a warning

Because the endpoint never signals failure to the client, always clear local state unconditionally after calling it (or even if the call itself throws on the network layer).

Security Best Practices

1. Always Invalidate Server-Side

typescript
// ❌ Bad - Only client-side cleanup
function logout() {
  localStorage.removeItem('token');
  router.push('/login');
}

// ✅ Good - Server-side + client-side
async function logout() {
  await authApi.logout();
  authStore.clearAll();
  router.push('/login');
}

2. Clear All Sensitive Data

typescript
function clearAll(): void {
  this.accessToken = null;
  this.refreshToken = null;
  this.user = null;
  this.tenant = null;
  this.permissions = [];
  queryClient.clear();
}

Never store tokens in localStorage

Refresh tokens especially should never be stored in localStorage due to XSS exposure. See Refresh Token — Security Considerations.

3. Treat Logout-Equivalent Errors the Same Way

Any authenticated request that comes back with one of these codes means the session is already gone server-side — force the same client cleanup as an explicit logout, without calling /auth/logout again first:

typescript
axios.interceptors.response.use(
  response => response,
  async error => {
    const code = error.response?.data?.code;
    if (error.response?.status === 401 &&
        ['TOKEN_REVOKED', 'SESSION_INVALIDATED'].includes(code)) {
      authStore.clearAll();
      router.push('/login');
      return Promise.reject(error);
    }
    return Promise.reject(error);
  }
);

Released under the MIT License.