diff --git a/.version b/.version index ff9c6e144..3834a7ed2 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -v5.11.0 +v5.11.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5052bfc79..61f16bbb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,25 @@ # Change Log +## [v5.11.1](https://github.com/auth0/react-native-auth0/tree/v5.11.1) (2026-09-08) + +[Full Changelog](https://github.com/auth0/react-native-auth0/compare/v5.11.0...v5.11.1) + +**Deprecated** + +- refactor: deprecate the client-side Management API [\#1624](https://github.com/auth0/react-native-auth0/pull/1624) ([subhankarmaiti](https://github.com/subhankarmaiti)) +- refactor: deprecate the legacy MFA methods on the auth client [\#1625](https://github.com/auth0/react-native-auth0/pull/1625) ([subhankarmaiti](https://github.com/subhankarmaiti)) + +**Fixed** + +- security fixes +- fix(android): skip explicit Kotlin plugin when AGP registers the kotlin extension [\#1654](https://github.com/auth0/react-native-auth0/pull/1654) ([gabrieldonadel](https://github.com/gabrieldonadel)) + ## [v5.11.0](https://github.com/auth0/react-native-auth0/tree/v5.11.0) (2026-07-31) + [Full Changelog](https://github.com/auth0/react-native-auth0/compare/v5.10.0...v5.11.0) **Added** + - feat: enforce IPSIE session_expiry with a SESSION_EXPIRED error [\#1597](https://github.com/auth0/react-native-auth0/pull/1597) ([subhankarmaiti](https://github.com/subhankarmaiti)) - feat: add passkeys support for web [\#1604](https://github.com/auth0/react-native-auth0/pull/1604) ([NandanPrabhu](https://github.com/NandanPrabhu)) - feat: add My Account API support on the web platform [\#1608](https://github.com/auth0/react-native-auth0/pull/1608) ([subhankarmaiti](https://github.com/subhankarmaiti)) diff --git a/android/build.gradle b/android/build.gradle index 97b9728d4..91958571a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,7 +21,13 @@ def reactNativeArchitectures() { } apply plugin: "com.android.library" -apply plugin: "kotlin-android" +// AGP 9 ships built-in Kotlin support and registers the `kotlin` extension +// itself. Applying the Kotlin plugin on top of it fails configuration with +// "Cannot add extension with name 'kotlin'". Only apply it when nothing has +// registered that extension yet. +if (project.extensions.findByName('kotlin') == null) { + apply plugin: "kotlin-android" +} apply plugin: "com.facebook.react" def getExtOrIntegerDefault(name) { diff --git a/package.json b/package.json index 79790fd5d..67e3d4aa2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-auth0", "title": "React Native Auth0", - "version": "5.11.0", + "version": "5.11.1", "description": "React Native toolkit for Auth0 API", "main": "lib/commonjs/index.js", "module": "lib/module/index.js", diff --git a/src/core/utils/telemetry.ts b/src/core/utils/telemetry.ts index 539f87d5e..ecb977474 100644 --- a/src/core/utils/telemetry.ts +++ b/src/core/utils/telemetry.ts @@ -1,6 +1,6 @@ export const telemetry = { name: 'react-native-auth0', - version: '5.11.0', + version: '5.11.1', }; export type Telemetry = { diff --git a/src/factory/Auth0ClientFactory.web.ts b/src/factory/Auth0ClientFactory.web.ts index 2a6dbe555..2d152a03f 100644 --- a/src/factory/Auth0ClientFactory.web.ts +++ b/src/factory/Auth0ClientFactory.web.ts @@ -6,6 +6,9 @@ import { validateAuth0Options, getConfigSignature } from '../core/utils'; // This file ONLY imports the Web client. import { WebAuth0Client } from '../platforms/web'; +const isBrowser = + typeof window !== 'undefined' && typeof window.document !== 'undefined'; + /** * Creates the Web-specific Auth0Client; selected by bundlers when targeting web. * Clients are cached by config signature so remounts reuse the same instance @@ -23,6 +26,12 @@ export class Auth0ClientFactory { static createClient(options: Auth0Options): Auth0Client { validateAuth0Options(options); + // Off the browser (SSR, route handlers) the cache would be shared across + // requests, so don't use it. + if (!isBrowser) { + return new WebAuth0Client(options as WebAuth0Options); + } + const cacheKey = getConfigSignature(options); let client = Auth0ClientFactory.clientCache.get(cacheKey); if (!client) { diff --git a/src/factory/__tests__/Auth0ClientFactory.web.spec.ts b/src/factory/__tests__/Auth0ClientFactory.web.spec.ts new file mode 100644 index 000000000..338c619a1 --- /dev/null +++ b/src/factory/__tests__/Auth0ClientFactory.web.spec.ts @@ -0,0 +1,49 @@ +import { Auth0ClientFactory } from '../Auth0ClientFactory.web'; +import type { WebAuth0Client } from '../../platforms/web'; + +// A real Auth0Client won't construct on jsdom's insecure origin, and we need a +// distinct object per construction to tell shared instances apart. +jest.mock('@auth0/auth0-spa-js', () => ({ + Auth0Client: jest.fn().mockImplementation(() => ({ mfa: {}, passkey: {} })), +})); + +const options = { + domain: 'tenant-a.us.auth0.com', + clientId: 'client-a', + useDPoP: false, +}; + +const spaClientOf = (client: unknown) => (client as WebAuth0Client).client; + +describe('Auth0ClientFactory (web) in a browser', () => { + beforeEach(() => { + Auth0ClientFactory.resetClientCache(); + }); + + it('reuses the cached client when the config has not changed', () => { + const first = Auth0ClientFactory.createClient(options); + const second = Auth0ClientFactory.createClient(options); + + expect(second).toBe(first); + }); + + it('creates a new client when the config signature changes', () => { + const tenantA = Auth0ClientFactory.createClient(options); + const tenantB = Auth0ClientFactory.createClient({ + ...options, + domain: 'tenant-b.eu.auth0.com', + }); + + expect(tenantB).not.toBe(tenantA); + }); + + it('gives the new client its own spa-js client', () => { + const tenantA = Auth0ClientFactory.createClient(options); + const tenantB = Auth0ClientFactory.createClient({ + ...options, + domain: 'tenant-b.eu.auth0.com', + }); + + expect(spaClientOf(tenantB)).not.toBe(spaClientOf(tenantA)); + }); +}); diff --git a/src/factory/__tests__/Auth0ClientFactory.web.ssr.spec.ts b/src/factory/__tests__/Auth0ClientFactory.web.ssr.spec.ts new file mode 100644 index 000000000..f780fd40f --- /dev/null +++ b/src/factory/__tests__/Auth0ClientFactory.web.ssr.spec.ts @@ -0,0 +1,44 @@ +/** + * @jest-environment node + */ +import { Auth0ClientFactory } from '../Auth0ClientFactory.web'; +import type { WebAuth0Client } from '../../platforms/web'; + +const options = { + domain: 'tenant-a.us.auth0.com', + clientId: 'client-a', + useDPoP: false, +}; + +const spaClientOf = (client: unknown) => (client as WebAuth0Client).client; + +describe('Auth0ClientFactory (web) outside a browser', () => { + it('runs without browser globals', () => { + expect(typeof window).toBe('undefined'); + }); + + it('creates a new client on every call', () => { + const first = Auth0ClientFactory.createClient(options); + const second = Auth0ClientFactory.createClient(options); + + expect(second).not.toBe(first); + }); + + it('does not share the spa-js client, so token caches stay separate', () => { + const first = Auth0ClientFactory.createClient(options); + const second = Auth0ClientFactory.createClient(options); + + expect(spaClientOf(second)).not.toBe(spaClientOf(first)); + }); + + it('does not share the spa-js client across tenants', () => { + const tenantA = Auth0ClientFactory.createClient(options); + const tenantB = Auth0ClientFactory.createClient({ + domain: 'tenant-b.eu.auth0.com', + clientId: 'client-b', + useDPoP: false, + }); + + expect(spaClientOf(tenantB)).not.toBe(spaClientOf(tenantA)); + }); +}); diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts index c2b499d87..470ea6266 100644 --- a/src/platforms/web/adapters/WebAuth0Client.ts +++ b/src/platforms/web/adapters/WebAuth0Client.ts @@ -45,33 +45,9 @@ export class WebAuth0Client implements Auth0Client { private readonly httpClient: HttpClient; private readonly tokenType: TokenType; public readonly client: SpaAuth0Client; - private static spaClient: SpaAuth0Client | null = null; private logoutInProgress = false; - /** - * Factory method to get a singleton instance of SpaAuth0Client. - * This ensures that the client is only created once and reused. - * - * @param options - The Auth0ClientOptions to configure the client. - * @returns An instance of SpaAuth0Client. - */ - private static getSpaClient(options: Auth0ClientOptions): SpaAuth0Client { - if (WebAuth0Client.spaClient) { - return WebAuth0Client.spaClient; - } - WebAuth0Client.spaClient = new SpaAuth0Client(options); - return WebAuth0Client.spaClient; - } - - /** - * Reset the singleton instance. Used for testing purposes. - * @internal - */ - public static resetSpaClientSingleton(): void { - WebAuth0Client.spaClient = null; - } - constructor(options: WebAuth0Options) { const baseUrl = `https://${options.domain}`; const useDPoP = options.useDPoP ?? false; @@ -99,9 +75,9 @@ export class WebAuth0Client implements Auth0Client { }, }; - // Use the singleton factory to get the spa-js client instance. - const client = WebAuth0Client.getSpaClient(clientOptions); - this.client = client; + // One client per instance: a shared one would leak its token cache + // to the next request on a server. + this.client = new SpaAuth0Client(clientOptions); // Create a bound getDPoPHeaders function for the orchestrator const getDPoPHeadersForOrchestrator = async ( diff --git a/src/platforms/web/adapters/__tests__/WebAuth0Client.getDPoPHeaders.spec.ts b/src/platforms/web/adapters/__tests__/WebAuth0Client.getDPoPHeaders.spec.ts index c581fb9de..fa97d2963 100644 --- a/src/platforms/web/adapters/__tests__/WebAuth0Client.getDPoPHeaders.spec.ts +++ b/src/platforms/web/adapters/__tests__/WebAuth0Client.getDPoPHeaders.spec.ts @@ -82,7 +82,6 @@ describe('WebAuth0Client - getDPoPHeaders', () => { beforeEach(() => { jest.clearAllMocks(); - WebAuth0Client.resetSpaClientSingleton(); // Setup window.location mock Object.defineProperty(window, 'location', { diff --git a/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts b/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts index 52630cdac..d1c5f8190 100644 --- a/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts +++ b/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts @@ -122,9 +122,6 @@ describe('WebAuth0Client', () => { // Clear all mocks first jest.clearAllMocks(); - // Reset the singleton to ensure fresh instances - WebAuth0Client.resetSpaClientSingleton(); - // Setup window.location mock Object.defineProperty(window, 'location', { value: { @@ -166,9 +163,7 @@ describe('WebAuth0Client', () => { }); afterEach(() => { - // Clear all mocks and reset singleton jest.clearAllMocks(); - WebAuth0Client.resetSpaClientSingleton(); }); describe('constructor', () => {