From 4ad107d891853f1ccb3a130328e871cc08f867e3 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Mon, 20 Apr 2026 10:04:06 +0530 Subject: [PATCH] fix: filter universal link callbacks by Auth0 domain in iOS --- .../native/adapters/NativeWebAuthProvider.ts | 11 ++++- .../__tests__/NativeWebAuthProvider.spec.ts | 41 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/platforms/native/adapters/NativeWebAuthProvider.ts b/src/platforms/native/adapters/NativeWebAuthProvider.ts index 23b47919a..3b5bdaba5 100644 --- a/src/platforms/native/adapters/NativeWebAuthProvider.ts +++ b/src/platforms/native/adapters/NativeWebAuthProvider.ts @@ -44,7 +44,16 @@ export class NativeWebAuthProvider implements IWebAuthProvider { let linkSubscription: EmitterSubscription | null = null; if (Platform.OS === 'ios') { linkSubscription = Linking.addEventListener('url', async (event) => { - // This listener catches the deep link and forwards it to the native side. + // Only forward URLs whose hostname matches the Auth0 domain. + // This prevents universal links on other domains from being + // incorrectly treated as Auth0 callbacks (e.g. when using + // customScheme: 'https' alongside app-specific universal links). + try { + const url = new URL(event.url); + if (url.hostname !== this.domain) return; + } catch { + return; + } linkSubscription?.remove(); await this.bridge.resumeWebAuth(event.url); }); diff --git a/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.spec.ts b/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.spec.ts index 45689b89d..57207fecc 100644 --- a/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.spec.ts @@ -139,12 +139,49 @@ describe('NativeWebAuthProvider', () => { // We don't await this, as it would "hang" until the listener is called. provider.authorize({}); - // Simulate the deep link event. - const resumeUrl = 'my-app://callback?code=123'; + // Simulate the deep link event with a URL matching the Auth0 domain. + const resumeUrl = `https://${domain}/ios/com.my-app/callback?code=123`; await listenerCallback({ url: resumeUrl }); expect(mockBridge.resumeWebAuth).toHaveBeenCalledWith(resumeUrl); }); + + it('should ignore URLs whose hostname does not match the Auth0 domain', async () => { + let listenerCallback: (event: { url: string }) => void = () => {}; + const mockSubscription = { remove: jest.fn() }; + mockAddEventListener.mockImplementation((_event, callback) => { + listenerCallback = callback; + return mockSubscription; + }); + + provider.authorize({}); + + // Simulate a universal link from a different domain. + await listenerCallback({ + url: 'https://app.example.com/some-path', + }); + + expect(mockBridge.resumeWebAuth).not.toHaveBeenCalled(); + // The subscription should NOT be removed, so it can still catch the real callback. + expect(mockSubscription.remove).not.toHaveBeenCalled(); + }); + + it('should ignore URLs that cannot be parsed', async () => { + let listenerCallback: (event: { url: string }) => void = () => {}; + const mockSubscription = { remove: jest.fn() }; + mockAddEventListener.mockImplementation((_event, callback) => { + listenerCallback = callback; + return mockSubscription; + }); + + provider.authorize({}); + + // Simulate a malformed URL. + await listenerCallback({ url: 'not-a-valid-url' }); + + expect(mockBridge.resumeWebAuth).not.toHaveBeenCalled(); + expect(mockSubscription.remove).not.toHaveBeenCalled(); + }); }); it('should NOT add a Linking listener on Android', async () => {