From 4cb7607e5a9d5623fd581bf4f3a21dbe758ecf03 Mon Sep 17 00:00:00 2001 From: Yogesh Chaudhary Date: Thu, 4 Dec 2025 10:52:43 +0530 Subject: [PATCH 1/2] fix: add fetch polyfill and update types for auth0-spa-js v2.10.0 - Added cross-fetch polyfill to test-setup.ts to support fetch API in tests - Updated handleRedirectCallback return type to include ConnectAccountRedirectResult - Updated test mocks to support new return types - Required for compatibility with @auth0/auth0-spa-js v2.10.0 --- package-lock.json | 102 ++++++++++++++++-- package.json | 3 +- .../src/lib/auth.service.spec.ts | 4 +- .../auth0-angular/src/lib/auth.service.ts | 5 +- projects/auth0-angular/src/test-setup.ts | 3 + 5 files changed, 107 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3d3096a..25819dde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@angular/platform-browser": "^18.2.13", "@angular/platform-browser-dynamic": "^18.2.13", "@angular/router": "^18.2.13", - "@auth0/auth0-spa-js": "^2.1.3", + "@auth0/auth0-spa-js": "^2.10.0", "rxjs": "^6.6.7", "tslib": "^2.8.1", "zone.js": "~0.14.10" @@ -40,11 +40,12 @@ "browserstack-cypress-cli": "^1.32.8", "concurrently": "^6.2.0", "cors": "^2.8.5", + "cross-fetch": "^4.1.0", "cypress": "^13.17.0", "eslint": "^8.57.0", - "eslint-plugin-import": "*", - "eslint-plugin-jsdoc": "*", - "eslint-plugin-prefer-arrow": "*", + "eslint-plugin-import": "latest", + "eslint-plugin-jsdoc": "latest", + "eslint-plugin-prefer-arrow": "latest", "express-jwt": "^8.4.1", "husky": "^4.3.8", "jest": "^29.7.0", @@ -2124,8 +2125,15 @@ } }, "node_modules/@auth0/auth0-spa-js": { - "version": "2.1.3", - "license": "MIT" + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-2.10.0.tgz", + "integrity": "sha512-eQhtxp19foKD7csTUariaU7YgwElVAmSJQSk2USuaP1LCqzN/iWhQS/vtVYiSozvSZPv8IOwN5UkBUt+rJAg8w==", + "license": "MIT", + "dependencies": { + "browser-tabs-lock": "^1.2.15", + "dpop": "^2.1.1", + "es-cookie": "~1.3.2" + } }, "node_modules/@babel/code-frame": { "version": "7.27.1", @@ -9958,6 +9966,16 @@ "dev": true, "license": "ISC" }, + "node_modules/browser-tabs-lock": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-tabs-lock/-/browser-tabs-lock-1.3.0.tgz", + "integrity": "sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "lodash": ">=4.17.21" + } + }, "node_modules/browserslist": { "version": "4.25.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", @@ -11805,6 +11823,16 @@ "node": ">=8" } }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -12876,6 +12904,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dpop": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/dpop/-/dpop-2.1.1.tgz", + "integrity": "sha512-J0Of2JTiM4h5si0tlbPQ/lkqfZ5wAEVkKYBhkwyyANnPJfWH4VsR5uIkZ+T+OSPIwDYUg1fbd5Mmodd25HjY1w==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -13223,6 +13260,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-cookie": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz", + "integrity": "sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -20214,7 +20257,6 @@ }, "node_modules/lodash": { "version": "4.17.21", - "dev": true, "license": "MIT" }, "node_modules/lodash.clonedeep": { @@ -21513,6 +21555,52 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", diff --git a/package.json b/package.json index 1fdd4f31..d84d8fd2 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@angular/platform-browser": "^18.2.13", "@angular/platform-browser-dynamic": "^18.2.13", "@angular/router": "^18.2.13", - "@auth0/auth0-spa-js": "^2.1.3", + "@auth0/auth0-spa-js": "^2.10.0", "rxjs": "^6.6.7", "tslib": "^2.8.1", "zone.js": "~0.14.10" @@ -57,6 +57,7 @@ "browserstack-cypress-cli": "^1.32.8", "concurrently": "^6.2.0", "cors": "^2.8.5", + "cross-fetch": "^4.1.0", "cypress": "^13.17.0", "eslint": "^8.57.0", "eslint-plugin-import": "latest", diff --git a/projects/auth0-angular/src/lib/auth.service.spec.ts b/projects/auth0-angular/src/lib/auth.service.spec.ts index e2a85daa..eb9506bc 100644 --- a/projects/auth0-angular/src/lib/auth.service.spec.ts +++ b/projects/auth0-angular/src/lib/auth.service.spec.ts @@ -58,7 +58,9 @@ describe('AuthService', () => { clientId: '', }); - jest.spyOn(auth0Client, 'handleRedirectCallback').mockResolvedValue({}); + jest.spyOn(auth0Client, 'handleRedirectCallback').mockResolvedValue({ + appState: undefined, + } as any); jest.spyOn(auth0Client, 'loginWithRedirect').mockResolvedValue(); jest.spyOn(auth0Client, 'loginWithPopup').mockResolvedValue(); jest.spyOn(auth0Client, 'checkSession').mockResolvedValue(); diff --git a/projects/auth0-angular/src/lib/auth.service.ts b/projects/auth0-angular/src/lib/auth.service.ts index a2b61f5e..22646c1e 100644 --- a/projects/auth0-angular/src/lib/auth.service.ts +++ b/projects/auth0-angular/src/lib/auth.service.ts @@ -8,6 +8,7 @@ import { GetTokenWithPopupOptions, RedirectLoginResult, GetTokenSilentlyVerboseResponse, + ConnectAccountRedirectResult, } from '@auth0/auth0-spa-js'; import { @@ -304,7 +305,9 @@ export class AuthService */ handleRedirectCallback( url?: string - ): Observable> { + ): Observable< + RedirectLoginResult | ConnectAccountRedirectResult + > { return defer(() => this.auth0Client.handleRedirectCallback(url) ).pipe( diff --git a/projects/auth0-angular/src/test-setup.ts b/projects/auth0-angular/src/test-setup.ts index 58c511e0..eb67306b 100644 --- a/projects/auth0-angular/src/test-setup.ts +++ b/projects/auth0-angular/src/test-setup.ts @@ -1,3 +1,6 @@ import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; setupZoneTestEnv(); + +// Provides fetch, Headers, Response, Request — works in Node + JSDOM +import 'cross-fetch/polyfill'; From abd7a3387963a37fa71f71388051fb8b8970e373 Mon Sep 17 00:00:00 2001 From: Yogesh Chaudhary Date: Thu, 4 Dec 2025 11:07:42 +0530 Subject: [PATCH 2/2] feat: add DPoP support methods from @auth0/auth0-spa-js v2.10.0 - Add getDpopNonce() method to retrieve DPoP nonce for a domain - Add setDpopNonce() method to set DPoP nonce for a domain - Add generateDpopProof() method to generate DPoP proof JWT - Add createFetcher() method to create authenticated HTTP fetcher - Export DPoP-related types: Fetcher, FetcherConfig, CustomFetchMinimalOutput - Export UseDpopNonceError for error handling - Add comprehensive unit tests for all DPoP methods --- .../src/lib/auth.service.spec.ts | 115 ++++++++++++++++++ .../auth0-angular/src/lib/auth.service.ts | 77 ++++++++++++ projects/auth0-angular/src/public-api.ts | 4 + 3 files changed, 196 insertions(+) diff --git a/projects/auth0-angular/src/lib/auth.service.spec.ts b/projects/auth0-angular/src/lib/auth.service.spec.ts index eb9506bc..af00d8a0 100644 --- a/projects/auth0-angular/src/lib/auth.service.spec.ts +++ b/projects/auth0-angular/src/lib/auth.service.spec.ts @@ -76,6 +76,17 @@ describe('AuthService', () => { .spyOn(auth0Client, 'getTokenWithPopup') .mockResolvedValue('__access_token_from_popup__'); + jest + .spyOn(auth0Client, 'getDpopNonce') + .mockResolvedValue('test-nonce-value'); + jest.spyOn(auth0Client, 'setDpopNonce').mockResolvedValue(undefined); + jest + .spyOn(auth0Client, 'generateDpopProof') + .mockResolvedValue('test-proof-jwt'); + jest.spyOn(auth0Client, 'createFetcher').mockReturnValue({ + fetch: jest.fn(), + } as any); + window.history.replaceState(null, '', ''); moduleSetup = { @@ -982,4 +993,108 @@ describe('AuthService', () => { }); }); }); + + describe('getDpopNonce', () => { + it('should retrieve DPoP nonce from the client', (done) => { + const service = createService(); + service.getDpopNonce().subscribe((nonce) => { + expect(nonce).toBe('test-nonce-value'); + expect(auth0Client.getDpopNonce).toHaveBeenCalled(); + done(); + }); + }); + + it('should pass domain identifier to the underlying SDK', (done) => { + const domainId = 'custom-domain'; + const service = createService(); + service.getDpopNonce(domainId).subscribe(() => { + expect(auth0Client.getDpopNonce).toHaveBeenCalledWith(domainId); + done(); + }); + }); + + it('should handle undefined nonce', (done) => { + (auth0Client.getDpopNonce as jest.Mock).mockResolvedValue(undefined); + const service = createService(); + service.getDpopNonce().subscribe((nonce) => { + expect(nonce).toBeUndefined(); + done(); + }); + }); + }); + + describe('setDpopNonce', () => { + it('should set DPoP nonce through the client', (done) => { + const service = createService(); + const nonceValue = 'new-nonce-123'; + service.setDpopNonce(nonceValue).subscribe(() => { + expect(auth0Client.setDpopNonce).toHaveBeenCalledWith( + nonceValue, + undefined + ); + done(); + }); + }); + + it('should pass nonce and domain identifier to the underlying SDK', (done) => { + const service = createService(); + const nonceValue = 'nonce-456'; + const domainId = 'domain-1'; + service.setDpopNonce(nonceValue, domainId).subscribe(() => { + expect(auth0Client.setDpopNonce).toHaveBeenCalledWith( + nonceValue, + domainId + ); + done(); + }); + }); + }); + + describe('generateDpopProof', () => { + it('should generate DPoP proof JWT', (done) => { + const service = createService(); + const params = { + url: 'https://api.example.com/resource', + method: 'POST', + accessToken: 'access-token-123', + }; + service.generateDpopProof(params).subscribe((proof) => { + expect(proof).toBe('test-proof-jwt'); + expect(auth0Client.generateDpopProof).toHaveBeenCalledWith(params); + done(); + }); + }); + + it('should pass all parameters including nonce', (done) => { + const service = createService(); + const params = { + url: 'https://api.example.com/data', + method: 'GET', + nonce: 'server-nonce', + accessToken: 'token-xyz', + }; + service.generateDpopProof(params).subscribe(() => { + expect(auth0Client.generateDpopProof).toHaveBeenCalledWith(params); + done(); + }); + }); + }); + + describe('createFetcher', () => { + it('should create a fetcher instance', () => { + const service = createService(); + const fetcher = service.createFetcher(); + expect(fetcher).toBeDefined(); + expect(auth0Client.createFetcher).toHaveBeenCalled(); + }); + + it('should pass configuration to the underlying SDK', () => { + const service = createService(); + const config = { + baseUrl: 'https://api.example.com', + }; + service.createFetcher(config); + expect(auth0Client.createFetcher).toHaveBeenCalledWith(config); + }); + }); }); diff --git a/projects/auth0-angular/src/lib/auth.service.ts b/projects/auth0-angular/src/lib/auth.service.ts index 22646c1e..340a9dd8 100644 --- a/projects/auth0-angular/src/lib/auth.service.ts +++ b/projects/auth0-angular/src/lib/auth.service.ts @@ -9,6 +9,9 @@ import { RedirectLoginResult, GetTokenSilentlyVerboseResponse, ConnectAccountRedirectResult, + CustomFetchMinimalOutput, + Fetcher, + FetcherConfig, } from '@auth0/auth0-spa-js'; import { @@ -329,6 +332,80 @@ export class AuthService ); } + /** + * ```js + * getDpopNonce(id).subscribe(nonce => ...) + * ``` + * + * Gets the DPoP nonce for the specified domain or the default domain. + * The nonce is used in DPoP proof generation for token binding. + * + * @param id Optional identifier for the domain. If not provided, uses the default domain. + * @returns An Observable that emits the DPoP nonce string or undefined if not available. + */ + getDpopNonce(id?: string): Observable { + return from(this.auth0Client.getDpopNonce(id)); + } + + /** + * ```js + * setDpopNonce(nonce, id).subscribe(() => ...) + * ``` + * + * Sets the DPoP nonce for the specified domain or the default domain. + * This is typically used after receiving a new nonce from the authorization server. + * + * @param nonce The DPoP nonce value to set. + * @param id Optional identifier for the domain. If not provided, uses the default domain. + * @returns An Observable that completes when the nonce is set. + */ + setDpopNonce(nonce: string, id?: string): Observable { + return from(this.auth0Client.setDpopNonce(nonce, id)); + } + + /** + * ```js + * generateDpopProof(params).subscribe(proof => ...) + * ``` + * + * Generates a DPoP (Demonstrating Proof-of-Possession) proof JWT. + * This proof is used to bind access tokens to a specific client, providing + * an additional layer of security for token usage. + * + * @param params Configuration for generating the DPoP proof + * @param params.url The URL of the resource server endpoint + * @param params.method The HTTP method (e.g., 'GET', 'POST') + * @param params.nonce Optional DPoP nonce from the authorization server + * @param params.accessToken The access token to bind to the proof + * @returns An Observable that emits the generated DPoP proof as a JWT string. + */ + generateDpopProof(params: { + url: string; + method: string; + nonce?: string; + accessToken: string; + }): Observable { + return from(this.auth0Client.generateDpopProof(params)); + } + + /** + * ```js + * const fetcher = createFetcher(config); + * ``` + * + * Creates a custom fetcher instance that can be used to make authenticated + * HTTP requests. The fetcher automatically handles token refresh and can + * be configured with custom request/response handling. + * + * @param config Optional configuration for the fetcher + * @returns A Fetcher instance configured with the Auth0 client. + */ + createFetcher( + config?: FetcherConfig + ): Fetcher { + return this.auth0Client.createFetcher(config); + } + private shouldHandleCallback(): Observable { return of(location.search).pipe( map((search) => { diff --git a/projects/auth0-angular/src/public-api.ts b/projects/auth0-angular/src/public-api.ts index f13b4c20..2a17480f 100644 --- a/projects/auth0-angular/src/public-api.ts +++ b/projects/auth0-angular/src/public-api.ts @@ -33,4 +33,8 @@ export { AuthenticationError, PopupCancelledError, MissingRefreshTokenError, + Fetcher, + FetcherConfig, + CustomFetchMinimalOutput, + UseDpopNonceError, } from '@auth0/auth0-spa-js';