Skip to content

Fix ID token and session token JWK conflcit in KV store#32

Open
valeriangalliat wants to merge 1 commit into
Code-Hex:mainfrom
EveToolsHQ:fix-id-token-session-token-jwk-conflict
Open

Fix ID token and session token JWK conflcit in KV store#32
valeriangalliat wants to merge 1 commit into
Code-Hex:mainfrom
EveToolsHQ:fix-id-token-session-token-jwk-conflict

Conversation

@valeriangalliat

@valeriangalliat valeriangalliat commented Jun 21, 2026

Copy link
Copy Markdown

Fixes #31

Overview

ID token verifier and session cookie verifier use different and incompatible signing keys in the same cache entry.

Verifying an ID token prevents verifying session cookies and vice versa.

This PR adds a .scoped(scope) function to the store that adds a :id-token and :session-cookie suffix to the provided cache key, so that both verification methods can work at the same time.

Problem

The current implementation uses a signle WorkersKVStore for the user-provided KV key. Both the ID token verifier and session cookie verifier use this same store, but they each need a different JWK.

The ID token verification keys are fetched from
https://www.googleapis.com/robot/v1/metadata/jwk/securetoken@system.gserviceaccount.com while the session cookie verification keys are fetched from https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys.

Whichever one gets used first gets cached in the KV store, then both ID token and session cookie verifiers will use those same cached keys until TTL expires. This means one of them will get the wrong keys and reject all verifications.

Solution

This PR fixes this. I tried to go with the approach that is the least breaking possible. I didn't want to change the public interface of Auth nor WorkersKVStore.

Instead this PR adds a scoped(scope: string) function to KeyStorer (optional for backwards compatibility), and implements it in both WorkersKVStore and WorkersKVStoreSingle. It returns an instance of the same class with :${scope} appended to the original key.

In the case of WorkersKVStoreSingle, .scoped() also registers a singleton for that scope.

Then in BaseAuth, we scope the KeyStorer in the contructor:

function scopedKeyStorer(keyStorer: KeyStorer, scope: string): KeyStorer {
  if (typeof keyStorer.scoped === 'function') {
    return keyStorer.scoped(scope);
  }
  return keyStorer;
}

this.idTokenVerifier = createIdTokenVerifier(projectId, scopedKeyStorer(keyStore, 'id-token'));
this.sessionCookieVerifier = createSessionCookieVerifier(projectId, scopedKeyStorer(keyStore, 'session-cookie'));

Note: the scopedKeyStorer function is used in case the user passes a custom KeyStorer that doesn't have the scoped method we just added. Then it'll work just like before with the ID token and session cookie conflict.

Backwards compatibility

Upgrading to this patched version will result in the previous cache key being ignored, and the appropriate keys to be fetched in the new scoped stores on first use. The previous cache key will stay in the KV until its TTL, then it'll be garbage collected without any manual cleanup necessary.

Notes

In order to run the test suite with Node 26 and pnpm 11, I've had to add the following patch (not committed here):
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..75aa0ab
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,8 @@
+allowBuilds:
+  esbuild: true
+  protobufjs: true
+  re2: true
+  workerd: true
+
+overrides:
+  jwa: 2.0.1
diff --git a/tests/setup.ts b/tests/setup.ts
index 9f628c5..a8dc0c0 100644
--- a/tests/setup.ts
+++ b/tests/setup.ts
@@ -1,9 +1,10 @@
 import crypto from 'node:crypto';
-import { setGlobalDispatcher } from 'undici';
+import { fetch as undiciFetch, setGlobalDispatcher } from 'undici';
 import { vi, beforeAll, afterAll } from 'vitest';
 import { fetchMock } from './fetch';

 vi.stubGlobal('crypto', crypto);
+vi.stubGlobal('fetch', undiciFetch);

 beforeAll(() => {
   setGlobalDispatcher(fetchMock);

All tests are green.

This PR was AI-assisted but I've carefully steered and reviewed the code, including the new tests. PR description written by hand.

Happy to revisit with a different approach if you think there's a better way to solve this.

Also no rush for merging this, just gonna use my fork in the meantime for my own projects.

Fixes Code-Hex#31

The current implementation uses a signle `WorkersKVStore` for the
user-provided KV key. Both the ID token verifier and session cookie
verifier use this same store, but they each need a different JWK.

The ID token verification keys are fetched from
<https://www.googleapis.com/robot/v1/metadata/jwk/securetoken@system.gserviceaccount.com>
while the session cookie verification keys are fetched from
<https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys>.

Whichever one gets used first gets cached in the KV store, then both
ID token and session cookie verifiers will use those same cached keys
until TTL expires. This means one of them will get the wrong keys and
reject all verifications.

This PR fixes this. I tried to go with the approach that is the least
breaking possible. I didn't want to change the public interface of
`Auth` nor `WorkersKVStore`.

Instead this PR adds a `scoped(scope: string)` function to `KeyStorer`
(optional for backwards compatibility), and implements it in both
`WorkersKVStore` and `WorkersKVStoreSingle`. It returns an instance of
the same class with `:${scope}` appended to the original key.

In the case of `WorkersKVStoreSingle`, `.scoped()` also registers a
singleton for that scope.

Then in `BaseAuth`, we scope the `KeyStorer` in the contructor:

```js
function scopedKeyStorer(keyStorer: KeyStorer, scope: string): KeyStorer {
  if (typeof keyStorer.scoped === 'function') {
    return keyStorer.scoped(scope);
  }
  return keyStorer;
}

this.idTokenVerifier = createIdTokenVerifier(projectId, scopedKeyStorer(keyStore, 'id-token'));
this.sessionCookieVerifier = createSessionCookieVerifier(projectId, scopedKeyStorer(keyStore, 'session-cookie'));
```

Note: the `scopedKeyStorer` function is used in case the user passes a
custom `KeyStorer` that doesn't have the `scoped` method we just added.
Then it'll work just like before with the ID token and session cookie
conflict.

Upgrading to this patched version will result in the previous cache key
being ignored, and the appropriate keys to be fetched in the new scoped
stores on first use. The previous cache key will stay in the KV until
its TTL, then it'll be garbage collected without any manual cleanup
necessary.
@valeriangalliat valeriangalliat force-pushed the fix-id-token-session-token-jwk-conflict branch from 740b585 to 3f744d5 Compare June 21, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot use both idToken and sessionToken verification at the same time

1 participant