Skip to content

Commit ddd4b02

Browse files
fix: filter universal link callbacks by Auth0 domain in iOS (#1512)
1 parent aa82e3b commit ddd4b02

2 files changed

Lines changed: 49 additions & 3 deletions

File tree

src/platforms/native/adapters/NativeWebAuthProvider.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,16 @@ export class NativeWebAuthProvider implements IWebAuthProvider {
4444
let linkSubscription: EmitterSubscription | null = null;
4545
if (Platform.OS === 'ios') {
4646
linkSubscription = Linking.addEventListener('url', async (event) => {
47-
// This listener catches the deep link and forwards it to the native side.
47+
// Only forward URLs whose hostname matches the Auth0 domain.
48+
// This prevents universal links on other domains from being
49+
// incorrectly treated as Auth0 callbacks (e.g. when using
50+
// customScheme: 'https' alongside app-specific universal links).
51+
try {
52+
const url = new URL(event.url);
53+
if (url.hostname !== this.domain) return;
54+
} catch {
55+
return;
56+
}
4857
linkSubscription?.remove();
4958
await this.bridge.resumeWebAuth(event.url);
5059
});

src/platforms/native/adapters/__tests__/NativeWebAuthProvider.spec.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,49 @@ describe('NativeWebAuthProvider', () => {
139139
// We don't await this, as it would "hang" until the listener is called.
140140
provider.authorize({});
141141

142-
// Simulate the deep link event.
143-
const resumeUrl = 'my-app://callback?code=123';
142+
// Simulate the deep link event with a URL matching the Auth0 domain.
143+
const resumeUrl = `https://${domain}/ios/com.my-app/callback?code=123`;
144144
await listenerCallback({ url: resumeUrl });
145145

146146
expect(mockBridge.resumeWebAuth).toHaveBeenCalledWith(resumeUrl);
147147
});
148+
149+
it('should ignore URLs whose hostname does not match the Auth0 domain', async () => {
150+
let listenerCallback: (event: { url: string }) => void = () => {};
151+
const mockSubscription = { remove: jest.fn() };
152+
mockAddEventListener.mockImplementation((_event, callback) => {
153+
listenerCallback = callback;
154+
return mockSubscription;
155+
});
156+
157+
provider.authorize({});
158+
159+
// Simulate a universal link from a different domain.
160+
await listenerCallback({
161+
url: 'https://app.example.com/some-path',
162+
});
163+
164+
expect(mockBridge.resumeWebAuth).not.toHaveBeenCalled();
165+
// The subscription should NOT be removed, so it can still catch the real callback.
166+
expect(mockSubscription.remove).not.toHaveBeenCalled();
167+
});
168+
169+
it('should ignore URLs that cannot be parsed', async () => {
170+
let listenerCallback: (event: { url: string }) => void = () => {};
171+
const mockSubscription = { remove: jest.fn() };
172+
mockAddEventListener.mockImplementation((_event, callback) => {
173+
listenerCallback = callback;
174+
return mockSubscription;
175+
});
176+
177+
provider.authorize({});
178+
179+
// Simulate a malformed URL.
180+
await listenerCallback({ url: 'not-a-valid-url' });
181+
182+
expect(mockBridge.resumeWebAuth).not.toHaveBeenCalled();
183+
expect(mockSubscription.remove).not.toHaveBeenCalled();
184+
});
148185
});
149186

150187
it('should NOT add a Linking listener on Android', async () => {

0 commit comments

Comments
 (0)