#7933·decap-cms

git-gateway + auth_type: pkce: session never survives reload (restoreUser() disconnected from PKCE login)

Author: gkanitzCreated Jul 30, 2026Updated Aug 31, 2026

Describe the bug

With backend.name: git-gateway and backend.auth_type: pkce, the session never survives a page reload — the user is bounced straight back to the login screen, immediately, every time. This isn't a token-expiry or refresh-timing issue; it's a code path gap: the PKCE login flow and the git-gateway restoreUser() session check are wired to two completely disconnected storage mechanisms, so restoreUser() can never succeed.

Root cause (traced against main)

Login (packages/decap-cms-ui-auth/src/PKCEAuthenticationPage.js, componentDidMount/handleLogin): after PkceAuthenticator completes the code exchange, the component builds a plain object and passes it straight to onLogin:

javascript
data.user_metadata = {};
if (data.access_token) {
  data.token = data.access_token;
  ...
}
this.props.onLogin(data);

No gotrue-js User instance is created here, and nothing persists a session for gotrue-js to find later.

onLoginauthenticate() (packages/decap-cms-backend-git-gateway/src/implementation.ts:279):

javascript
authenticate(credentials: Credentials) {
  const user = credentials as GitGatewayUser;
  if (user.jwt) {
    // Netlify auth
    ...
  } else {
    // OAuth
    this.tokenPromise = async () => (typeof user.token === 'string' ? user.token : '');
  }
  ...

Since the PKCE-built object has .token (a string) but no .jwt, it takes the "OAuth" branch — tokenPromise is a closure over the token string, held only in memory for the life of the GitGatewayClient instance. The User object this method eventually resolves to (and which decap-cms's own authStore persists to localStorage['decap-cms-user']) is built from userData and deliberately excludes the token:

javascript
return {
  name: userData.name,
  login: userData.email,
  email: userData.email,
  avatar_url: userData.avatar_url,
} as unknown as User;

Restore on reload (implementation.ts:389):

javascript
async restoreUser() {
  const client = await this.getAuthClient();
  const user = client.currentUser();
  if (!user) return Promise.reject();
  return this.authenticate(user as Credentials);
}

For the non-widget path, getAuthClient() constructs new GoTrue({ APIUrl: this.apiUrl }), and client.currentUser() calls gotrue-js's User.recoverSession(), which reads gotrue-js's own localStorage key — a key that is only ever written by gotrue-js's own _saveSession() (called from GoTrue.createUser() in the classic user.jwt / Netlify Identity password-grant flow). The PKCE flow above never calls this. So recoverSession() always returns null, currentUser() always returns null, restoreUser() always rejects — deterministically, on every reload, regardless of how much time has passed since login.

To Reproduce

  1. Configure config.yml with:
    yaml
    backend:
      name: git-gateway
      auth_type: pkce
      base_url: <any PKCE-capable git-gateway-compatible auth server>
      ...
  2. Log in via the PKCE flow, land on the dashboard.
  3. Reload the page (or close/reopen the tab).

Expected: session is restored, still logged in. Actual: immediately back at the login screen — even 1 second after logging in, with no elapsed-time dependency.

Confirmed live (production reproduction)

  • localStorage['decap-cms-user'] right after login contains only {name, login, avatar_url, backendName} — no token field at all.
  • On reload, no network request to any token/refresh/git-gateway endpoint is made at all (confirmed via DevTools) — consistent with restoreUser() failing on a purely synchronous, empty localStorage read rather than a failed network refresh.
  • After the reload, localStorage['decap-cms-user'] is removed entirely (decap-cms's core clears it once restoreUser() rejects).

Environment

  • decap-cms: 3.11.0 (bundle-inspected; same code path present on main as of this report)
  • Backend: git-gateway, auth_type: pkce, via a third-party git-gateway-compatible bridge server (not Netlify's own Identity service)
  • Browser: Chrome (desktop)

Suggested fix direction

restoreUser()'s session check needs a PKCE-aware branch — either persisting a real session via gotrue-js's own storage when using the OAuth/PKCE token path, or having getAuthClient() know how to recover a token that PKCEAuthenticationPage itself is responsible for persisting (e.g., to localStorage) and reading back on mount, independent of gotrue-js's Identity-Widget-oriented currentUser().