Skip to content

Commit 98226cb

Browse files
Merge branch 'master' into fix/mfa-challenge-oob-code-snake-case
2 parents e43e62e + 2587745 commit 98226cb

9 files changed

Lines changed: 281 additions & 33 deletions

File tree

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v5.5.0
1+
v5.5.1

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Change Log
22

3+
## [v5.5.1](https://github.com/auth0/react-native-auth0/tree/v5.5.1) (2026-04-23)
4+
5+
[Full Changelog](https://github.com/auth0/react-native-auth0/compare/v5.5.0...v5.5.1)
6+
7+
**Fixed**
8+
9+
- fix: remove conflicting broad scheme from MainActivity to prevent Android disambiguation dialog [\#1514](https://github.com/auth0/react-native-auth0/pull/1514) ([subhankarmaiti](https://github.com/subhankarmaiti))
10+
- fix: filter universal link callbacks by Auth0 domain in iOS [\#1512](https://github.com/auth0/react-native-auth0/pull/1512) ([subhankarmaiti](https://github.com/subhankarmaiti))
11+
312
## [v5.5.0](https://github.com/auth0/react-native-auth0/tree/v5.5.0) (2026-04-09)
413

514
[Full Changelog](https://github.com/auth0/react-native-auth0/compare/v5.4.1...v5.5.0)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "react-native-auth0",
33
"title": "React Native Auth0",
4-
"version": "5.5.0",
4+
"version": "5.5.1",
55
"description": "React Native toolkit for Auth0 API",
66
"main": "lib/commonjs/index.js",
77
"module": "lib/module/index.js",

src/core/utils/telemetry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const telemetry = {
22
name: 'react-native-auth0',
3-
version: '5.5.0',
3+
version: '5.5.1',
44
};
55

66
export type Telemetry = {

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 () => {

src/plugin/__tests__/withAuth0-test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,159 @@ describe(addAndroidAuth0Manifest, () => {
213213
expect(dataElement?.$['android:host']).toBe('sample.auth0.com');
214214
});
215215

216+
it(`should remove conflicting broad scheme from other activity intent filter`, () => {
217+
const config = getConfig();
218+
const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(
219+
config.modResults
220+
);
221+
mainApp.activity = mainApp.activity || [];
222+
mainApp.activity.push({
223+
'$': {
224+
'android:name': '.MainActivity',
225+
},
226+
'intent-filter': [
227+
{
228+
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
229+
category: [
230+
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
231+
{
232+
$: { 'android:name': 'android.intent.category.BROWSABLE' },
233+
},
234+
],
235+
data: [
236+
{ $: { 'android:scheme': 'smtest' } },
237+
{ $: { 'android:scheme': 'exp+smtest' } },
238+
],
239+
},
240+
],
241+
} as AndroidConfig.Manifest.ManifestActivity);
242+
243+
const result = addAndroidAuth0Manifest(
244+
[{ domain: 'sample.auth0.com', customScheme: 'smtest' }],
245+
config,
246+
'com.sample.app'
247+
);
248+
249+
const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
250+
result.modResults
251+
);
252+
253+
// MainActivity should have 'smtest' removed but keep 'exp+smtest'
254+
const mainActivity = mainApplication.activity?.find(
255+
(a) => a.$['android:name'] === '.MainActivity'
256+
);
257+
const mainIntentFilter = mainActivity?.['intent-filter']?.[0];
258+
const schemes = mainIntentFilter?.data?.map((d) => d.$['android:scheme']);
259+
expect(schemes).toEqual(['exp+smtest']);
260+
expect(schemes).not.toContain('smtest');
261+
262+
// RedirectActivity should still have the auth0 scheme
263+
const redirectActivity = mainApplication.activity?.find(
264+
(a) =>
265+
a.$['android:name'] === 'com.auth0.android.provider.RedirectActivity'
266+
);
267+
const redirectIntentFilter = redirectActivity?.['intent-filter']?.[0];
268+
const redirectData = redirectIntentFilter?.data?.[0];
269+
expect(redirectData?.$['android:scheme']).toBe('smtest');
270+
expect(redirectData?.$['android:host']).toBe('sample.auth0.com');
271+
expect(redirectData?.$['android:pathPrefix']).toBe(
272+
'/android/com.sample.app/callback'
273+
);
274+
});
275+
276+
it(`should not modify other activities when no scheme conflict exists`, () => {
277+
const config = getConfig();
278+
const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(
279+
config.modResults
280+
);
281+
mainApp.activity = mainApp.activity || [];
282+
mainApp.activity.push({
283+
'$': {
284+
'android:name': '.MainActivity',
285+
},
286+
'intent-filter': [
287+
{
288+
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
289+
category: [
290+
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
291+
{
292+
$: { 'android:name': 'android.intent.category.BROWSABLE' },
293+
},
294+
],
295+
data: [
296+
{ $: { 'android:scheme': 'myapp' } },
297+
{ $: { 'android:scheme': 'exp+myapp' } },
298+
],
299+
},
300+
],
301+
} as AndroidConfig.Manifest.ManifestActivity);
302+
303+
const result = addAndroidAuth0Manifest(
304+
[{ domain: 'sample.auth0.com', customScheme: 'smtest' }],
305+
config,
306+
'com.sample.app'
307+
);
308+
309+
const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
310+
result.modResults
311+
);
312+
const mainActivity = mainApplication.activity?.find(
313+
(a) => a.$['android:name'] === '.MainActivity'
314+
);
315+
const mainIntentFilter = mainActivity?.['intent-filter']?.[0];
316+
const schemes = mainIntentFilter?.data?.map((d) => d.$['android:scheme']);
317+
expect(schemes).toEqual(['myapp', 'exp+myapp']);
318+
});
319+
320+
it(`should not remove scheme data that has a host (specific match)`, () => {
321+
const config = getConfig();
322+
const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(
323+
config.modResults
324+
);
325+
mainApp.activity = mainApp.activity || [];
326+
mainApp.activity.push({
327+
'$': {
328+
'android:name': '.MainActivity',
329+
},
330+
'intent-filter': [
331+
{
332+
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
333+
category: [
334+
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
335+
{
336+
$: { 'android:name': 'android.intent.category.BROWSABLE' },
337+
},
338+
],
339+
data: [
340+
{
341+
$: {
342+
'android:scheme': 'smtest',
343+
'android:host': 'myapp.example.com',
344+
},
345+
},
346+
],
347+
},
348+
],
349+
} as AndroidConfig.Manifest.ManifestActivity);
350+
351+
const result = addAndroidAuth0Manifest(
352+
[{ domain: 'sample.auth0.com', customScheme: 'smtest' }],
353+
config,
354+
'com.sample.app'
355+
);
356+
357+
const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
358+
result.modResults
359+
);
360+
const mainActivity = mainApplication.activity?.find(
361+
(a) => a.$['android:name'] === '.MainActivity'
362+
);
363+
const mainIntentFilter = mainActivity?.['intent-filter']?.[0];
364+
const schemes = mainIntentFilter?.data?.map((d) => d.$['android:scheme']);
365+
// Should keep the data element because it has a host (specific match, won't cause disambiguation)
366+
expect(schemes).toEqual(['smtest']);
367+
});
368+
216369
it(`should add android:autoVerify="true" when customScheme is https`, () => {
217370
const config = getConfig();
218371
const result = addAndroidAuth0Manifest(

src/plugin/withAuth0.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,31 @@ export const addAndroidAuth0Manifest = (
107107
let auth0Scheme =
108108
config.customScheme ?? applicationId + APPLICATION_ID_SUFFIX;
109109

110+
// Remove conflicting broad scheme handlers from other activities to prevent
111+
// Android from showing a disambiguation dialog when both MainActivity and
112+
// RedirectActivity handle the same scheme. This mirrors the iOS dedup logic
113+
// in addIOSAuth0ConfigInInfoPList.
114+
const activities = mainApplication.activity || [];
115+
activities.forEach((activity) => {
116+
if (
117+
activity.$['android:name'] ===
118+
'com.auth0.android.provider.RedirectActivity'
119+
) {
120+
return;
121+
}
122+
const intentFilters = activity['intent-filter'] || [];
123+
intentFilters.forEach((filter) => {
124+
if (!filter.data) return;
125+
filter.data = filter.data.filter((dataElement) => {
126+
const scheme = dataElement.$?.['android:scheme'];
127+
const host = dataElement.$?.['android:host'];
128+
// Remove broad scheme matchers (no host) that would conflict
129+
// with RedirectActivity's more specific intent filter
130+
return !(scheme === auth0Scheme && !host);
131+
});
132+
});
133+
});
134+
110135
const dataElement = {
111136
$: {
112137
'android:scheme': auth0Scheme,

0 commit comments

Comments
 (0)