Skip to content

feat: add DPoP core storage layer (ENG-4782)#201

Open
mrudatsprint wants to merge 6 commits into
parent/dpop-in-the-javascript-sdkfrom
miker/eng-4782/core-storage-layer
Open

feat: add DPoP core storage layer (ENG-4782)#201
mrudatsprint wants to merge 6 commits into
parent/dpop-in-the-javascript-sdkfrom
miker/eng-4782/core-storage-layer

Conversation

@mrudatsprint

@mrudatsprint mrudatsprint commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Issue:

Description:

Implement the storage for the Cryptographic Key Pair (DPoPStorage) and the storage for the tokens (DPoPTokenStore)

- Add `dpop` v2.1.1 runtime dependency and `fake-indexeddb` devDependency
  to packages/core/package.json

- SDKConfig: add optional `useDpop` and `dpopTokenStorage` fields

- UrlHelper: add `getAuthorizeUrl(state?, dpopJkt?, codeChallenge?)`
  targeting FusionAuth /oauth2/authorize directly; update UrlHelperTypes
  to include response_type, code_challenge, code_challenge_method, dpop_jkt

- DPoPStorage: IndexedDB abstraction for ES256 CryptoKeyPair persistence
  (db: fusionauth-sdk:dpop, store: keypair, keyed by clientId)

- DPoPTokenStore: localStorage/memory token storage for DPoP-bound tokens
  (key: fusionauth-sdk:tokens:<clientId>); includes getAccessToken() and
  isExpired getter

- packages/core/src/DPoP/index.ts re-exports both classes

- 54 tests passing (21 DPoPTokenStore, 6 DPoPStorage, 16 UrlHelper, 7 SDKCore,
  4 CookieHelpers)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the core building blocks needed to support DPoP mode in @fusionauth-sdk/core, adding a persistent keypair store (IndexedDB), a token store (localStorage/memory), and an authorize URL helper for DPoP + PKCE flows.

Changes:

  • Added DPoP keypair persistence via IndexedDB (DPoPStorage) and corresponding tests.
  • Added a DPoP token storage abstraction (DPoPTokenStore) with localStorage/memory backends and tests.
  • Extended UrlHelper to generate a direct /oauth2/authorize URL for DPoP/PKCE, plus type/test updates and public exports.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/core/src/UrlHelper/UrlHelperTypes.ts Expands query param typing to include DPoP + PKCE authorize parameters.
packages/core/src/UrlHelper/UrlHelper.ts Adds getAuthorizeUrl for direct FusionAuth /oauth2/authorize URL generation.
packages/core/src/UrlHelper/UrlHelper.test.ts Adds coverage for getAuthorizeUrl query string construction.
packages/core/src/SDKConfig/SDKConfig.ts Introduces DPoP opt-in configuration (useDpop, dpopTokenStorage).
packages/core/src/index.ts Exposes the new DPoP module from the package entrypoint.
packages/core/src/DPoP/index.ts Barrel exports for DPoP storage/token store APIs.
packages/core/src/DPoP/DPoPTokenStore.ts Implements token persistence abstraction for DPoP mode.
packages/core/src/DPoP/DPoPTokenStore.test.ts Adds tests for localStorage and memory token persistence behavior.
packages/core/src/DPoP/DPoPStorage.ts Implements IndexedDB-backed persistence for DPoP keypairs.
packages/core/src/DPoP/DPoPStorage.test.ts Adds IndexedDB persistence tests using fake-indexeddb.
packages/core/package.json Adds dpop runtime dependency and fake-indexeddb dev dependency.
AGENTS.md Adds repo agent guidance (workspace layout, testing notes, and conventions).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/DPoP/DPoPStorage.ts
Comment thread packages/core/src/SDKConfig/SDKConfig.ts
- Remove 'as any' cast in catch block — reject() accepts unknown directly
- Add tests for indexedDB unavailable (SSR/non-browser): all three public
  methods (getKeyPair, setKeyPair, clearKeyPair) reject with a descriptive error
- Add test for indexedDB.open() throwing synchronously (e.g. security policy block)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.

Comment thread packages/core/src/DPoP/DPoPStorage.ts Outdated
Comment thread packages/core/src/DPoP/DPoPTokenStore.ts
Comment thread packages/core/src/DPoP/DPoPTokenStore.ts Outdated
Comment thread packages/core/src/SDKConfig/SDKConfig.ts
Comment thread packages/core/src/DPoP/DPoPStorage.test.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.

Comment thread packages/core/src/DPoP/DPoPStorage.ts
Comment thread packages/core/src/DPoP/DPoPStorage.ts
Comment on lines +110 to +114
/**
* Token storage location in DPoP mode. Only meaningful when `useDpop: true`.
* Defaults to `'localStorage'`.
*/
dpopTokenStorage?: 'localStorage' | 'memory';

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should memory be the default instead of localStorage?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut feel is localStorage.

Comment thread packages/core/src/index.ts
@mrudatsprint mrudatsprint marked this pull request as ready for review July 16, 2026 01:22
@mrudatsprint mrudatsprint changed the base branch from miker/dpop-in-the-javascript-sdk to main July 16, 2026 01:22
@mrudatsprint mrudatsprint changed the base branch from main to miker/dpop-in-the-javascript-sdk July 16, 2026 01:24
All three DPoPStorage methods (getKeyPair, setKeyPair, clearKeyPair) now
resolve on tx.oncomplete and reject on tx.onerror / tx.onabort.

Previously, resolving on req.onsuccess meant the caller was told 'success'
before the transaction had fully committed — a transaction abort occurring
after the request succeeded (e.g. quota exceeded) would go undetected.

Applies the same fix consistently to all three methods, including getKeyPair
(readonly, lower risk, but now consistent) and setKeyPair (readwrite, same
durability concern as clearKeyPair).

Adds a test that aborts a clearKeyPair transaction synchronously inside the
request onsuccess handler and verifies the promise rejects and the key pair
is still present in IndexedDB.
@mrudatsprint mrudatsprint marked this pull request as draft July 16, 2026 01:58
@mrudatsprint mrudatsprint changed the base branch from miker/dpop-in-the-javascript-sdk to main July 16, 2026 01:58
@mrudatsprint mrudatsprint marked this pull request as ready for review July 16, 2026 14:40
@mrudatsprint mrudatsprint requested review from a team as code owners July 16, 2026 14:40

@mrudatsprint mrudatsprint left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self-review completed

const originalOpen = globalThis.indexedDB.open.bind(globalThis.indexedDB);

try {
globalThis.indexedDB.open = () => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overwrite the open property so that when getKeyPair is called the DPoPStorage.openDb will fail.

const db = await this.openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite');
tx.objectStore(STORE_NAME).put(keyPair, this.clientId);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyPair is namespaced by clientId to isolate multiple FusionAuth applications. This means multiple applications using DPoP running in for example two separate browser tabs will run successfully.

When using the Hosted Backend API this browser configuration doesn't work as the cookies are stored on the lowest level domain and displayed on all sub domains. Meaning, the next applications cookies overwrite the previous applications cookies.

Comment on lines +110 to +114
/**
* Token storage location in DPoP mode. Only meaningful when `useDpop: true`.
* Defaults to `'localStorage'`.
*/
dpopTokenStorage?: 'localStorage' | 'memory';

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut feel is localStorage.

@mrudatsprint mrudatsprint changed the base branch from main to miker/dpop-in-the-javascript-sdk July 16, 2026 15:54
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.

2 participants