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.
onLogin → authenticate() (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
- Configure
config.yml with:
backend:
name: git-gateway
auth_type: pkce
base_url: <any PKCE-capable git-gateway-compatible auth server>
...
- Log in via the PKCE flow, land on the dashboard.
- 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().
Describe the bug
With
backend.name: git-gatewayandbackend.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-gatewayrestoreUser()session check are wired to two completely disconnected storage mechanisms, sorestoreUser()can never succeed.Root cause (traced against
main)Login (
packages/decap-cms-ui-auth/src/PKCEAuthenticationPage.js,componentDidMount/handleLogin): afterPkceAuthenticatorcompletes the code exchange, the component builds a plain object and passes it straight toonLogin:No
gotrue-jsUserinstance is created here, and nothing persists a session forgotrue-jsto find later.onLogin→authenticate()(packages/decap-cms-backend-git-gateway/src/implementation.ts:279):Since the PKCE-built object has
.token(a string) but no.jwt, it takes the "OAuth" branch —tokenPromiseis a closure over the token string, held only in memory for the life of theGitGatewayClientinstance. TheUserobject this method eventually resolves to (and which decap-cms's ownauthStorepersists tolocalStorage['decap-cms-user']) is built fromuserDataand deliberately excludes the token:Restore on reload (
implementation.ts:389):For the non-widget path,
getAuthClient()constructsnew GoTrue({ APIUrl: this.apiUrl }), andclient.currentUser()callsgotrue-js'sUser.recoverSession(), which readsgotrue-js's ownlocalStoragekey — a key that is only ever written bygotrue-js's own_saveSession()(called fromGoTrue.createUser()in the classicuser.jwt/ Netlify Identity password-grant flow). The PKCE flow above never calls this. SorecoverSession()always returnsnull,currentUser()always returnsnull,restoreUser()always rejects — deterministically, on every reload, regardless of how much time has passed since login.To Reproduce
config.ymlwith: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.restoreUser()failing on a purely synchronous, emptylocalStorageread rather than a failed network refresh.localStorage['decap-cms-user']is removed entirely (decap-cms's core clears it oncerestoreUser()rejects).Environment
decap-cms: 3.11.0 (bundle-inspected; same code path present onmainas of this report)git-gateway,auth_type: pkce, via a third-party git-gateway-compatible bridge server (not Netlify's own Identity service)Suggested fix direction
restoreUser()'s session check needs a PKCE-aware branch — either persisting a real session viagotrue-js's own storage when using the OAuth/PKCE token path, or havinggetAuthClient()know how to recover a token thatPKCEAuthenticationPageitself is responsible for persisting (e.g., tolocalStorage) and reading back on mount, independent ofgotrue-js's Identity-Widget-orientedcurrentUser().