decaporg / decaporg/decap-cms

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

Open
#7,933 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
19.4k
Forks
3.1k
Avg merge
1d 14h
Merged PRs (30d)
9

Description

## 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`:

```js
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.

**`onLogin` → `authenticate()`** (`packages/decap-cms-backend-git-gateway/src/implementation.ts:279`):

```js
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:

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

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

Contributor guide

Open the contributing guide

Research direction

Start with packages/decap-cms-ui-auth/src/PKCEAuthenticationPage.js and trace its componentDidMount/handleLogin path into authenticate() and restoreUser() in packages/decap-cms-backend-git-gateway/src/implementation.ts. Inspect the existing storage behavior, reproduce a PKCE login followed by reload, and verify that the session is restored without returning to the login screen.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, react
Domain
authentication, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.