Skip to content

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

Description

@gkanitz

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:

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):

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:

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):

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:
    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().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions