Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
- [Android](#android)
- [iOS](#ios)
- [Expo](#expo)
- [Allowed Browsers (Android)](#allowed-browsers-android)

## Authentication API

Expand Down Expand Up @@ -1387,6 +1388,66 @@ If you want to support multiple domains, you would have to pass an array of obje

You can skip sending the `customScheme` property if you do not want to customize it.

## Allowed Browsers (Android)

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.

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.

**Behaviour:**
- If the user's default browser is in the list, it is used.
- If the user's default browser is not in the list but another allowed browser is installed, that browser is used instead.
- If no allowed browser is installed, an `a0.browser_not_available` error is returned.

> **Platform Support:** Android only. This option is ignored on iOS.

### Using with Hooks

```typescript
import { useAuth0 } from 'react-native-auth0';

const { authorize } = useAuth0();

await authorize(
{ scope: 'openid profile email' },
{
allowedBrowserPackages: [
'com.android.chrome',
'com.chrome.beta',
'com.microsoft.emmx', // Edge
'com.brave.browser',
'com.sec.android.app.sbrowser', // Samsung Internet
],
}
);
```

### Using with Auth0 Class

```typescript
import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
});

await auth0.webAuth.authorize(
{ scope: 'openid profile email' },
{
allowedBrowserPackages: [
'com.android.chrome',
'com.chrome.beta',
'com.microsoft.emmx', // Edge
'com.brave.browser',
'com.sec.android.app.sbrowser', // Samsung Internet
],
}
);
```

The same `allowedBrowserPackages` option is also accepted by `clearSession` to restrict which browser handles the logout flow.

## DPoP (Demonstrating Proof-of-Possession)

[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.
Expand Down
39 changes: 33 additions & 6 deletions android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import com.auth0.android.authentication.storage.SecureCredentialsManager
import com.auth0.android.authentication.storage.SharedPreferencesStorage
import com.auth0.android.dpop.DPoP
import com.auth0.android.dpop.DPoPException
import com.auth0.android.provider.BrowserPicker
import com.auth0.android.provider.CustomTabsOptions
import com.auth0.android.provider.WebAuthProvider
import com.auth0.android.result.Credentials
import com.facebook.react.bridge.ActivityEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.bridge.WritableNativeMap
Expand Down Expand Up @@ -111,22 +114,23 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
ephemeralSession: Boolean?,
safariViewControllerPresentationStyle: Double?,
additionalParameters: ReadableMap?,
allowedBrowserPackages: ReadableArray?,
promise: Promise
) {
if(this.useDPoP) {
WebAuthProvider.useDPoP(reactContext)
}
webAuthPromise = promise
val cleanedParameters = mutableMapOf<String, String>()

additionalParameters?.let { params ->
params.toHashMap().forEach { (key, value) ->
value?.let { cleanedParameters[key] = it.toString() }
}
}

val builder = WebAuthProvider.login(auth0!!).withScheme(scheme)

builder.apply {
state?.let { withState(it) }
nonce?.let { withNonce(it) }
Expand All @@ -138,6 +142,17 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
invitationUrl?.let { withInvitationUrl(it) }
leeway?.let { if (it.toInt() != 0) withIdTokenVerificationLeeway(it.toInt()) }
redirectUri?.let { withRedirectUri(it) }
allowedBrowserPackages?.let { packages ->
val packageList = (0 until packages.size()).mapNotNull { packages.getString(it) }
val browserPicker = BrowserPicker.newBuilder()
.withAllowedPackages(packageList)
.build()
withCustomTabsOptions(
CustomTabsOptions.newBuilder()
.withBrowserPicker(browserPicker)
.build()
)
}
}

builder.withParameters(cleanedParameters)
Expand Down Expand Up @@ -345,15 +360,27 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
override fun getName(): String = NAME

@ReactMethod
override fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, promise: Promise) {
override fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, allowedBrowserPackages: ReadableArray?, promise: Promise) {
val builder = WebAuthProvider.logout(auth0!!).withScheme(scheme)

if (federated) {
builder.withFederated()
}

redirectUri?.let { builder.withReturnToUrl(it) }


allowedBrowserPackages?.let { packages ->
val packageList = (0 until packages.size()).mapNotNull { packages.getString(it) }
val browserPicker = BrowserPicker.newBuilder()
.withAllowedPackages(packageList)
.build()
builder.withCustomTabsOptions(
CustomTabsOptions.newBuilder()
.withBrowserPicker(browserPicker)
.build()
)
}

builder.start(reactContext.currentActivity as FragmentActivity,
object : com.auth0.android.callback.Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) {
Expand Down
4 changes: 3 additions & 1 deletion android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap

abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
Expand Down Expand Up @@ -83,12 +84,13 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
ephemeralSession: Boolean?,
safariViewControllerPresentationStyle: Double?,
additionalParameters: ReadableMap?,
allowedBrowserPackages: ReadableArray?,
promise: Promise
)

@ReactMethod
@DoNotStrip
abstract fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, promise: Promise)
abstract fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, allowedBrowserPackages: ReadableArray?, promise: Promise)

@ReactMethod
@DoNotStrip
Expand Down
4 changes: 3 additions & 1 deletion ios/A0Auth0.mm
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ - (dispatch_queue_t)methodQueue
ephemeralSession:(nonnull NSNumber *)ephemeralSession
safariViewControllerPresentationStyle:(nonnull NSNumber *)safariViewControllerPresentationStyle
additionalParameters:(NSDictionary * _Nullable)additionalParameters
allowedBrowserPackages:(NSArray * _Nullable)allowedBrowserPackages
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
NSInteger maxAgeValue = maxAge != nil ? (NSInteger)[maxAge doubleValue] : 0;
Expand All @@ -147,8 +148,9 @@ - (dispatch_queue_t)methodQueue
RCT_EXPORT_METHOD(webAuthLogout:(NSString *)scheme
federated:(BOOL)federated
redirectUri:(NSString *)redirectUri
allowedBrowserPackages:(NSArray * _Nullable)allowedBrowserPackages
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
reject:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge webAuthLogoutWithScheme:scheme federated:federated redirectUri:redirectUri resolve:resolve reject:reject];
}

Expand Down
6 changes: 4 additions & 2 deletions src/platforms/native/bridge/NativeBridgeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export class NativeBridgeManager implements INativeBridge {
options.ephemeralSession ?? false,
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.
// //The native layer will check for this and ignore if the value is 99
parameters.additionalParameters ?? {}
parameters.additionalParameters ?? {},
options.allowedBrowserPackages
);
return new CredentialsModel(credential);
}
Expand All @@ -117,7 +118,8 @@ export class NativeBridgeManager implements INativeBridge {
Auth0NativeModule.webAuthLogout.bind(Auth0NativeModule),
options.customScheme,
parameters.federated ?? false,
parameters.returnToUrl
parameters.returnToUrl,
options.allowedBrowserPackages
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ describe('NativeBridgeManager', () => {
options.leeway,
options.ephemeralSession,
1, // presentationStyle
parameters.additionalParameters
parameters.additionalParameters,
undefined // allowedBrowserPackages
);
});

Expand Down Expand Up @@ -118,7 +119,41 @@ describe('NativeBridgeManager', () => {
0, // leeway
false, // ephemeralSession
99, // presentationStyle
{} // additionalParameters
{}, // additionalParameters
undefined // allowedBrowserPackages
);
});

it('should pass allowedBrowserPackages to native webAuth when provided', async () => {
MockedAuth0NativeModule.webAuth.mockResolvedValueOnce(
nativeSuccessCredentials as any
);
const allowedBrowserPackages = [
'com.android.chrome',
'com.brave.browser',
];

await bridge.authorize(
{ redirectUrl: 'com.myapp://cb' },
{ customScheme: 'com.myapp', allowedBrowserPackages }
);

expect(MockedAuth0NativeModule.webAuth).toHaveBeenCalledWith(
'com.myapp',
'com.myapp://cb',
undefined,
undefined,
undefined,
undefined,
undefined,
0,
undefined,
undefined,
0,
false,
99,
{},
allowedBrowserPackages
);
});

Expand Down Expand Up @@ -149,7 +184,26 @@ describe('NativeBridgeManager', () => {
expect(MockedAuth0NativeModule.webAuthLogout).toHaveBeenCalledWith(
options.customScheme,
parameters.federated,
parameters.returnToUrl
parameters.returnToUrl,
undefined // allowedBrowserPackages
);
});

it('should pass allowedBrowserPackages to native webAuthLogout when provided', async () => {
const parameters = {
federated: false,
returnToUrl: 'com.myapp://logout',
};
const allowedBrowserPackages = ['com.android.chrome'];
const options = { customScheme: 'com.myapp', allowedBrowserPackages };

await bridge.clearSession(parameters, options);

expect(MockedAuth0NativeModule.webAuthLogout).toHaveBeenCalledWith(
options.customScheme,
parameters.federated,
parameters.returnToUrl,
allowedBrowserPackages
);
});
});
Expand Down
6 changes: 4 additions & 2 deletions src/specs/NativeA0Auth0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export interface Spec extends TurboModule {
leeway: Int32 | undefined,
ephemeralSession: boolean | undefined,
safariViewControllerPresentationStyle: Int32 | undefined,
additionalParameters: { [key: string]: string } | undefined
additionalParameters: { [key: string]: string } | undefined,
allowedBrowserPackages: string[] | undefined
): Promise<Credentials>;

/**
Expand All @@ -102,7 +103,8 @@ export interface Spec extends TurboModule {
webAuthLogout(
scheme: string,
federated: boolean,
redirectUri: string
redirectUri: string,
allowedBrowserPackages: string[] | undefined
): Promise<void>;

/**
Expand Down
28 changes: 28 additions & 0 deletions src/types/platform-specific.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,28 @@ export interface NativeAuthorizeOptions {
presentationStyle?: SafariViewControllerPresentationStyle;
}
| boolean;
/**
* **Android only:** List of browser package names allowed to handle the web authentication flow.
* When set, only browsers whose package names appear in this list will be used. This is useful
* for excluding browsers that do not correctly handle App Link redirects (e.g. Firefox).
*
* - When the user's default browser is in the list, it is used.
* - When the user's default browser is not in the list but another allowed browser is installed, that browser is used instead.
* - When no allowed browser is installed, an `a0.browser_not_available` error is returned.
*
* @example
* ```typescript
* await authorize({}, {
* allowedBrowserPackages: [
* 'com.android.chrome',
* 'com.chrome.beta',
* 'com.microsoft.emmx', // Edge
* 'com.brave.browser',
* ]
* });
* ```
*/
allowedBrowserPackages?: string[];
}

/**
Expand All @@ -162,6 +184,12 @@ export interface NativeClearSessionOptions {
* See migration guide for details.
*/
useLegacyCallbackUrl?: boolean;

/**
* **Android only:** List of browser package names allowed to handle the web logout flow.
* Mirrors the same option on {@link NativeAuthorizeOptions} — see that field for full details.
*/
allowedBrowserPackages?: string[];
}

// ========= Web-Specific Options =========
Expand Down
Loading