git-gateway + auth_type: pkce: session never survives reload (restoreUser() disconnected from PKCE login)
- 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
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