opencrvs / opencrvs-core

A global solution to civil registration
https://www.opencrvs.org
Other
88 stars 72 forks source link

R02 Investigate why logout mechanism doesn’t properly invalidate JWT token #7499

Open rikukissa opened 2 months ago

rikukissa commented 2 months ago

Open questions

Security assessment details

Technical Overview

When interacting with GraphQL for application functionality, the application used JSON Web Tokens (JWTs) for authentication purposes instead of common session tokens. Whilst JWTs can help with authorisation, the disadvantage to using them is their inability to be immediately invalidated. As such, it was possible to use the same JWTs to make GraphQL queries even when the user logged out, as the application did not terminate the JWTs on the server side.

Potential Impact if Exploited

Should an attacker compromise a JWT, such as viewing the value in the browser address history as shown in finding R03, it would be possible for them to reuse it at will, even after the user decides to log out.

Recommendations

Once the user either logs out, or is idle for a defined period of inactivity, the JWTs should be added to a deny list to prevent them from being to make GraphQL queries. Alternatively, a session-based approach can be used which can more easily be invalidated server-side to better control user access.

Investigate and improve

There are two options why the logout mechanism did not invalidate JWT. First is simple misconfiguration. Second one is harder to confirm since missing awaits introduces time complexity to software. Are we relying on some things happening in sequence that do not necessarily do so?

  1. CHECK_INVALID_TOKEN env variable was set to false

Improvements:

  1. Non-awaited API call caused the invalidation to fail mid-flight.

Current logout implementation is optimistic. Within removeToken we call async function, authApi.invalidateToken(token), which is never awaited.

Depending on the scenario, token could have been removed from localStorage and user redirected to login without ensuring the invalidation took place. There are multiple places where login action is handled. Some of them may implicitly rely on not awaiting for the invalidation, so just adding the missing await might cause some other failure.

// Page.tsx checkAuth // ProtectedRoute.tsx refreshToken // apolloClient createClient

// packages/client/src/utils/authUtils.ts

export function removeToken() {
  const token = getToken()
  if (token) {
    try {
      authApi.invalidateToken(token) // Where await is missing
    } catch (err) {
      Sentry.captureException(err)
    }
  }
  localStorage.removeItem('opencrvs')
}

// packages/client/src/utils/authUtils.ts

export async function refreshToken() {
  const token = getToken()
  if (isTokenAboutToExpire(token)) {
    const res = await fetch(`${window.config.AUTH_URL}/refreshToken`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        token
      })
    })

    if (!res.ok) {
      return false
    } else {
      const data = await res.json()
      removeToken()
      storeToken(data.token)
    }
  }
  return true
}

Improvements:

naftis commented 1 month ago

What if we handled the logout via a route in the login app? The main app could redirect the user there, and if offline, poll a logout (invalidateToken?) endpoint in gateway until it responds and adds the token to Redis invalidations. Since the login app is offline-capable and cached, offline users could still be redirected.

There's a risk the token never invalidates, but we can mitigate this with shorter token expiration times.

This also enables a configurable LOGOUT_URL (or LOGIN_URL + '/logout' found from window.config), making third-party logout integration with core easier.

rikukissa commented 1 month ago

@naftis I don't think login app can access the token as it's in register. local storage 🤔