Skip to content

Commit a45ab78

Browse files
Merge branch 'master' into feat/dpop-credential-state-errors
2 parents a02800f + 938a8c7 commit a45ab78

8 files changed

Lines changed: 193 additions & 15 deletions

File tree

EXAMPLES.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
- [Android](#android)
6464
- [iOS](#ios)
6565
- [Expo](#expo)
66+
- [Allowed Browsers (Android)](#allowed-browsers-android)
6667

6768
## Authentication API
6869

@@ -1387,6 +1388,66 @@ If you want to support multiple domains, you would have to pass an array of obje
13871388
13881389
You can skip sending the `customScheme` property if you do not want to customize it.
13891390
1391+
## Allowed Browsers (Android)
1392+
1393+
On Android, some browsers do not correctly handle App Link redirects. For example, Firefox renders the callback URL as a web page instead of handing the redirect back to your app, causing the authentication flow to fail silently.
1394+
1395+
You can restrict which browsers are allowed to handle the web authentication flow by passing `allowedBrowserPackages` in the options object. When set, only browsers whose package names appear in the list will be used.
1396+
1397+
**Behaviour:**
1398+
- If the user's default browser is in the list, it is used.
1399+
- If the user's default browser is not in the list but another allowed browser is installed, that browser is used instead.
1400+
- If no allowed browser is installed, an `a0.browser_not_available` error is returned.
1401+
1402+
> **Platform Support:** Android only. This option is ignored on iOS.
1403+
1404+
### Using with Hooks
1405+
1406+
```typescript
1407+
import { useAuth0 } from 'react-native-auth0';
1408+
1409+
const { authorize } = useAuth0();
1410+
1411+
await authorize(
1412+
{ scope: 'openid profile email' },
1413+
{
1414+
allowedBrowserPackages: [
1415+
'com.android.chrome',
1416+
'com.chrome.beta',
1417+
'com.microsoft.emmx', // Edge
1418+
'com.brave.browser',
1419+
'com.sec.android.app.sbrowser', // Samsung Internet
1420+
],
1421+
}
1422+
);
1423+
```
1424+
1425+
### Using with Auth0 Class
1426+
1427+
```typescript
1428+
import Auth0 from 'react-native-auth0';
1429+
1430+
const auth0 = new Auth0({
1431+
domain: 'YOUR_AUTH0_DOMAIN',
1432+
clientId: 'YOUR_AUTH0_CLIENT_ID',
1433+
});
1434+
1435+
await auth0.webAuth.authorize(
1436+
{ scope: 'openid profile email' },
1437+
{
1438+
allowedBrowserPackages: [
1439+
'com.android.chrome',
1440+
'com.chrome.beta',
1441+
'com.microsoft.emmx', // Edge
1442+
'com.brave.browser',
1443+
'com.sec.android.app.sbrowser', // Samsung Internet
1444+
],
1445+
}
1446+
);
1447+
```
1448+
1449+
The same `allowedBrowserPackages` option is also accepted by `clearSession` to restrict which browser handles the logout flow.
1450+
13901451
## DPoP (Demonstrating Proof-of-Possession)
13911452
13921453
[DPoP](https://datatracker.ietf.org/doc/html/rfc9449) (Demonstrating Proof-of-Possession) is an OAuth 2.0 extension that cryptographically binds access and refresh tokens to a client-specific key pair. This prevents token theft and replay attacks by ensuring that even if a token is intercepted, it cannot be used from a different device.

android/src/main/java/com/auth0/react/A0Auth0Module.kt

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ import com.auth0.android.authentication.storage.SecureCredentialsManager
1313
import com.auth0.android.authentication.storage.SharedPreferencesStorage
1414
import com.auth0.android.dpop.DPoP
1515
import com.auth0.android.dpop.DPoPException
16+
import com.auth0.android.provider.BrowserPicker
17+
import com.auth0.android.provider.CustomTabsOptions
1618
import com.auth0.android.provider.WebAuthProvider
1719
import com.auth0.android.result.Credentials
1820
import com.facebook.react.bridge.ActivityEventListener
1921
import com.facebook.react.bridge.Promise
2022
import com.facebook.react.bridge.ReactApplicationContext
2123
import com.facebook.react.bridge.ReactMethod
24+
import com.facebook.react.bridge.ReadableArray
2225
import com.facebook.react.bridge.ReadableMap
2326
import com.facebook.react.bridge.UiThreadUtil
2427
import com.facebook.react.bridge.WritableNativeMap
@@ -114,22 +117,23 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
114117
ephemeralSession: Boolean?,
115118
safariViewControllerPresentationStyle: Double?,
116119
additionalParameters: ReadableMap?,
120+
allowedBrowserPackages: ReadableArray?,
117121
promise: Promise
118122
) {
119123
if(this.useDPoP) {
120124
WebAuthProvider.useDPoP(reactContext)
121125
}
122126
webAuthPromise = promise
123127
val cleanedParameters = mutableMapOf<String, String>()
124-
128+
125129
additionalParameters?.let { params ->
126130
params.toHashMap().forEach { (key, value) ->
127131
value?.let { cleanedParameters[key] = it.toString() }
128132
}
129133
}
130134

131135
val builder = WebAuthProvider.login(auth0!!).withScheme(scheme)
132-
136+
133137
builder.apply {
134138
state?.let { withState(it) }
135139
nonce?.let { withNonce(it) }
@@ -141,6 +145,17 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
141145
invitationUrl?.let { withInvitationUrl(it) }
142146
leeway?.let { if (it.toInt() != 0) withIdTokenVerificationLeeway(it.toInt()) }
143147
redirectUri?.let { withRedirectUri(it) }
148+
allowedBrowserPackages?.let { packages ->
149+
val packageList = (0 until packages.size()).mapNotNull { packages.getString(it) }
150+
val browserPicker = BrowserPicker.newBuilder()
151+
.withAllowedPackages(packageList)
152+
.build()
153+
withCustomTabsOptions(
154+
CustomTabsOptions.newBuilder()
155+
.withBrowserPicker(browserPicker)
156+
.build()
157+
)
158+
}
144159
}
145160

146161
builder.withParameters(cleanedParameters)
@@ -348,15 +363,27 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
348363
override fun getName(): String = NAME
349364

350365
@ReactMethod
351-
override fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, promise: Promise) {
366+
override fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, allowedBrowserPackages: ReadableArray?, promise: Promise) {
352367
val builder = WebAuthProvider.logout(auth0!!).withScheme(scheme)
353-
368+
354369
if (federated) {
355370
builder.withFederated()
356371
}
357-
372+
358373
redirectUri?.let { builder.withReturnToUrl(it) }
359-
374+
375+
allowedBrowserPackages?.let { packages ->
376+
val packageList = (0 until packages.size()).mapNotNull { packages.getString(it) }
377+
val browserPicker = BrowserPicker.newBuilder()
378+
.withAllowedPackages(packageList)
379+
.build()
380+
builder.withCustomTabsOptions(
381+
CustomTabsOptions.newBuilder()
382+
.withBrowserPicker(browserPicker)
383+
.build()
384+
)
385+
}
386+
360387
builder.start(reactContext.currentActivity as FragmentActivity,
361388
object : com.auth0.android.callback.Callback<Void?, AuthenticationException> {
362389
override fun onSuccess(result: Void?) {

android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReactApplicationContext
55
import com.facebook.react.bridge.ReactContextBaseJavaModule
66
import com.facebook.react.bridge.Promise
77
import com.facebook.react.bridge.ReactMethod
8+
import com.facebook.react.bridge.ReadableArray
89
import com.facebook.react.bridge.ReadableMap
910

1011
abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
@@ -83,12 +84,13 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
8384
ephemeralSession: Boolean?,
8485
safariViewControllerPresentationStyle: Double?,
8586
additionalParameters: ReadableMap?,
87+
allowedBrowserPackages: ReadableArray?,
8688
promise: Promise
8789
)
8890

8991
@ReactMethod
9092
@DoNotStrip
91-
abstract fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, promise: Promise)
93+
abstract fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, allowedBrowserPackages: ReadableArray?, promise: Promise)
9294

9395
@ReactMethod
9496
@DoNotStrip

ios/A0Auth0.mm

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ - (dispatch_queue_t)methodQueue
133133
ephemeralSession:(nonnull NSNumber *)ephemeralSession
134134
safariViewControllerPresentationStyle:(nonnull NSNumber *)safariViewControllerPresentationStyle
135135
additionalParameters:(NSDictionary * _Nullable)additionalParameters
136+
allowedBrowserPackages:(NSArray * _Nullable)allowedBrowserPackages
136137
resolve:(RCTPromiseResolveBlock)resolve
137138
reject:(RCTPromiseRejectBlock)reject) {
138139
NSInteger maxAgeValue = maxAge != nil ? (NSInteger)[maxAge doubleValue] : 0;
@@ -147,8 +148,9 @@ - (dispatch_queue_t)methodQueue
147148
RCT_EXPORT_METHOD(webAuthLogout:(NSString *)scheme
148149
federated:(BOOL)federated
149150
redirectUri:(NSString *)redirectUri
151+
allowedBrowserPackages:(NSArray * _Nullable)allowedBrowserPackages
150152
resolve:(RCTPromiseResolveBlock)resolve
151-
reject:(RCTPromiseRejectBlock)reject) {
153+
reject:(RCTPromiseRejectBlock)reject) {
152154
[self.nativeBridge webAuthLogoutWithScheme:scheme federated:federated redirectUri:redirectUri resolve:resolve reject:reject];
153155
}
154156

src/platforms/native/bridge/NativeBridgeManager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ export class NativeBridgeManager implements INativeBridge {
104104
options.ephemeralSession ?? false,
105105
presentationStyle ?? 99, // Since we can't pass null to the native layer, and we need a value to represent this parameter is not set, we are using 99.
106106
// //The native layer will check for this and ignore if the value is 99
107-
parameters.additionalParameters ?? {}
107+
parameters.additionalParameters ?? {},
108+
options.allowedBrowserPackages
108109
);
109110
return new CredentialsModel(credential);
110111
}
@@ -117,7 +118,8 @@ export class NativeBridgeManager implements INativeBridge {
117118
Auth0NativeModule.webAuthLogout.bind(Auth0NativeModule),
118119
options.customScheme,
119120
parameters.federated ?? false,
120-
parameters.returnToUrl
121+
parameters.returnToUrl,
122+
options.allowedBrowserPackages
121123
);
122124
}
123125

src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ describe('NativeBridgeManager', () => {
9090
options.leeway,
9191
options.ephemeralSession,
9292
1, // presentationStyle
93-
parameters.additionalParameters
93+
parameters.additionalParameters,
94+
undefined // allowedBrowserPackages
9495
);
9596
});
9697

@@ -118,7 +119,41 @@ describe('NativeBridgeManager', () => {
118119
0, // leeway
119120
false, // ephemeralSession
120121
99, // presentationStyle
121-
{} // additionalParameters
122+
{}, // additionalParameters
123+
undefined // allowedBrowserPackages
124+
);
125+
});
126+
127+
it('should pass allowedBrowserPackages to native webAuth when provided', async () => {
128+
MockedAuth0NativeModule.webAuth.mockResolvedValueOnce(
129+
nativeSuccessCredentials as any
130+
);
131+
const allowedBrowserPackages = [
132+
'com.android.chrome',
133+
'com.brave.browser',
134+
];
135+
136+
await bridge.authorize(
137+
{ redirectUrl: 'com.myapp://cb' },
138+
{ customScheme: 'com.myapp', allowedBrowserPackages }
139+
);
140+
141+
expect(MockedAuth0NativeModule.webAuth).toHaveBeenCalledWith(
142+
'com.myapp',
143+
'com.myapp://cb',
144+
undefined,
145+
undefined,
146+
undefined,
147+
undefined,
148+
undefined,
149+
0,
150+
undefined,
151+
undefined,
152+
0,
153+
false,
154+
99,
155+
{},
156+
allowedBrowserPackages
122157
);
123158
});
124159

@@ -149,7 +184,26 @@ describe('NativeBridgeManager', () => {
149184
expect(MockedAuth0NativeModule.webAuthLogout).toHaveBeenCalledWith(
150185
options.customScheme,
151186
parameters.federated,
152-
parameters.returnToUrl
187+
parameters.returnToUrl,
188+
undefined // allowedBrowserPackages
189+
);
190+
});
191+
192+
it('should pass allowedBrowserPackages to native webAuthLogout when provided', async () => {
193+
const parameters = {
194+
federated: false,
195+
returnToUrl: 'com.myapp://logout',
196+
};
197+
const allowedBrowserPackages = ['com.android.chrome'];
198+
const options = { customScheme: 'com.myapp', allowedBrowserPackages };
199+
200+
await bridge.clearSession(parameters, options);
201+
202+
expect(MockedAuth0NativeModule.webAuthLogout).toHaveBeenCalledWith(
203+
options.customScheme,
204+
parameters.federated,
205+
parameters.returnToUrl,
206+
allowedBrowserPackages
153207
);
154208
});
155209
});

src/specs/NativeA0Auth0.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ export interface Spec extends TurboModule {
9393
leeway: Int32 | undefined,
9494
ephemeralSession: boolean | undefined,
9595
safariViewControllerPresentationStyle: Int32 | undefined,
96-
additionalParameters: { [key: string]: string } | undefined
96+
additionalParameters: { [key: string]: string } | undefined,
97+
allowedBrowserPackages: string[] | undefined
9798
): Promise<Credentials>;
9899

99100
/**
@@ -102,7 +103,8 @@ export interface Spec extends TurboModule {
102103
webAuthLogout(
103104
scheme: string,
104105
federated: boolean,
105-
redirectUri: string
106+
redirectUri: string,
107+
allowedBrowserPackages: string[] | undefined
106108
): Promise<void>;
107109

108110
/**

src/types/platform-specific.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,28 @@ export interface NativeAuthorizeOptions {
144144
presentationStyle?: SafariViewControllerPresentationStyle;
145145
}
146146
| boolean;
147+
/**
148+
* **Android only:** List of browser package names allowed to handle the web authentication flow.
149+
* When set, only browsers whose package names appear in this list will be used. This is useful
150+
* for excluding browsers that do not correctly handle App Link redirects (e.g. Firefox).
151+
*
152+
* - When the user's default browser is in the list, it is used.
153+
* - When the user's default browser is not in the list but another allowed browser is installed, that browser is used instead.
154+
* - When no allowed browser is installed, an `a0.browser_not_available` error is returned.
155+
*
156+
* @example
157+
* ```typescript
158+
* await authorize({}, {
159+
* allowedBrowserPackages: [
160+
* 'com.android.chrome',
161+
* 'com.chrome.beta',
162+
* 'com.microsoft.emmx', // Edge
163+
* 'com.brave.browser',
164+
* ]
165+
* });
166+
* ```
167+
*/
168+
allowedBrowserPackages?: string[];
147169
}
148170

149171
/**
@@ -162,6 +184,12 @@ export interface NativeClearSessionOptions {
162184
* See migration guide for details.
163185
*/
164186
useLegacyCallbackUrl?: boolean;
187+
188+
/**
189+
* **Android only:** List of browser package names allowed to handle the web logout flow.
190+
* Mirrors the same option on {@link NativeAuthorizeOptions} — see that field for full details.
191+
*/
192+
allowedBrowserPackages?: string[];
165193
}
166194

167195
// ========= Web-Specific Options =========

0 commit comments

Comments
 (0)