Skip to content
Merged
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
64 changes: 64 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2214,6 +2214,70 @@ await auth0.webAuth.authorize(

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

## Trusted Web Activity (Android)

On Android, web authentication defaults to a Custom Tab, which shows a read-only URL bar at the top of the browser. A **Trusted Web Activity (TWA)** renders the login page full-screen with no address bar, giving a more integrated, app-like experience. Pass `useTrustedWebActivity: true` in the options object to opt in.

**Behaviour:**

- When enabled, the login (and logout) page opens in a Trusted Web Activity instead of a Custom Tab.
- TWA relies on **Digital Asset Links** verification between your app and your Auth0 domain. If verification fails — for example because the setup below is missing — it automatically **falls back to a Custom Tab**, so login still completes (just with the URL bar visible).

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

### Required setup

TWA will only render full-screen if your app's signing certificate is registered with your Auth0 tenant. Without this, Digital Asset Links verification fails and the flow falls back to a Custom Tab.

1. Get your app's **SHA-256 certificate fingerprint**. For a debug build:

```bash
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
```

For a release build, run the same command against your release keystore. Copy the `SHA256` fingerprint.

2. In the **Auth0 Dashboard**, go to **Application → Settings → Advanced → Device Settings → Key Hashes** and add:
- Your app's **package name** (e.g. `com.myapp`).
- The **SHA-256 fingerprint** from step 1.

3. Save. Digital Asset Links verification now succeeds and the login page renders full-screen.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pbly just mention they can validate the same with
{DOMAIN}/.well-known/assetlinks.json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the example

You can confirm the registration by visiting `https://YOUR_AUTH0_DOMAIN/.well-known/assetlinks.json` and checking that your app's package name and SHA-256 fingerprint are listed.

> Register the fingerprint for **every** signing config you ship (debug, release, and Play App Signing if you use it), otherwise TWA silently falls back to a Custom Tab for the unregistered builds.

### Using with Hooks

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

const { authorize } = useAuth0();

await authorize(
{ scope: 'openid profile email' },
{ useTrustedWebActivity: true }
);
```

### 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' },
{ useTrustedWebActivity: true }
);
```

The same `useTrustedWebActivity` option is also accepted by `clearSession` so the logout flow opens in a Trusted Web Activity as well.

## Recovering Login After Process Death (Android)

On Android, the OS can kill your app's process while the user is completing login in the browser — this is common on devices with aggressive memory management (e.g. Samsung One UI, Xiaomi MIUI), especially during MFA when the user switches apps to fetch a code. When the user finishes and the browser redirects back, the app cold-starts and, without recovery, the in-flight login is lost and the user lands back on the login screen.
Expand Down
10 changes: 8 additions & 2 deletions android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
safariViewControllerPresentationStyle: Double?,
additionalParameters: ReadableMap?,
allowedBrowserPackages: ReadableArray?,
useTrustedWebActivity: Boolean,
promise: Promise
) {
if(this.useDPoP) {
Expand Down Expand Up @@ -172,8 +173,9 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
.build()
)
}
if (useTrustedWebActivity) { withTrustedWebActivity() }
}

builder.withParameters(cleanedParameters)

val activity = reactContext.currentActivity
Expand Down Expand Up @@ -458,13 +460,17 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
override fun getName(): String = NAME

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

if (federated) {
builder.withFederated()
}

if (useTrustedWebActivity) {
builder.withTrustedWebActivity()
}

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

allowedBrowserPackages?.let { packages ->
Expand Down
3 changes: 2 additions & 1 deletion android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,13 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
safariViewControllerPresentationStyle: Double?,
additionalParameters: ReadableMap?,
allowedBrowserPackages: ReadableArray?,
useTrustedWebActivity: Boolean,
promise: Promise
)

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

@ReactMethod
@DoNotStrip
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- A0Auth0 (5.8.0):
- A0Auth0 (5.9.0):
- Auth0 (= 2.23.0)
- hermes-engine
- RCTRequired
Expand Down Expand Up @@ -2242,7 +2242,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
A0Auth0: e14195294c028c4ebce21e45a17ee11749e7a943
A0Auth0: 260f4197bc27923b13525663cc625cbb01ac4e4a
Auth0: dd46c309079f995e91dfd7af38e8fefd1a43bf4e
FBLazyVector: b3e7ad108f0d882e30445c5527d774e3fd432f3d
hermes-engine: 4ba8c4848d46c6a441e09d1c62f883ba69bc5b76
Expand Down
26 changes: 22 additions & 4 deletions example/src/screens/class-based/ClassLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,19 @@ const ClassLoginScreen = () => {
const [challenge, setChallenge] = useState<PasswordlessChallenge | null>(
null
);
const [useTrustedWebActivity, setUseTrustedWebActivity] = useState(false);

const onLogin = async () => {
setLoading(true);
setError(null);
try {
const credentials = await auth0.webAuth.authorize({
scope: 'openid profile email offline_access',
audience: `https://${config.domain}/api/v2/`,
});
const credentials = await auth0.webAuth.authorize(
{
scope: 'openid profile email offline_access',
audience: `https://${config.domain}/api/v2/`,
},
{ useTrustedWebActivity }
);
// On success, we save the credentials and navigate to the profile screen.
await auth0.credentialsManager.saveCredentials(credentials);
navigation.replace('ClassProfile', { credentials });
Expand Down Expand Up @@ -118,6 +122,20 @@ const ClassLoginScreen = () => {
<View style={styles.section}>
<Text style={styles.sectionTitle}>Web Auth</Text>
<Button onPress={onLogin} title="Log In" loading={loading} />
<>
<Text style={styles.description}>
Trusted Web Activity opens login full-screen (no URL bar).
Requires registering the app's SHA-256 Key Hash in the Auth0
Dashboard; otherwise it falls back to a Custom Tab. Android only.
</Text>
<Button
onPress={() => setUseTrustedWebActivity((prev) => !prev)}
title={`Trusted Web Activity: ${
useTrustedWebActivity ? 'On' : 'Off'
}`}
style={!useTrustedWebActivity ? styles.inactiveButton : undefined}
/>
</>
</View>

{Platform.OS !== 'web' && (
Expand Down
40 changes: 27 additions & 13 deletions example/src/screens/hooks/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,17 @@ const HomeScreen = () => {
const [otpCode, setOtpCode] = useState('');
const [otpChallenge, setOtpChallenge] =
useState<PasswordlessChallenge | null>(null);
const [useTrustedWebActivity, setUseTrustedWebActivity] = useState(false);

const onLogin = async () => {
try {
await authorize({
scope: 'openid profile email offline_access',
audience: `https://${config.domain}/api/v2/`,
});
await authorize(
{
scope: 'openid profile email offline_access',
audience: `https://${config.domain}/api/v2/`,
},
{ useTrustedWebActivity }
);
} catch (e: any) {
if (e instanceof WebAuthError) {
switch (e.type) {
Expand Down Expand Up @@ -347,15 +351,25 @@ const HomeScreen = () => {

<Section title="Web Auth (Recommended)">
<Button onPress={onLogin} title="Log In" />
{Platform.OS === 'android' && (
<>
<Text style={styles.description}>
Recovers a login that completed after the OS killed the app
process. No-op on iOS/web.
</Text>
<Button onPress={onResumeSession} title="Resume Session" />
</>
)}
<>
<Text style={styles.description}>
Trusted Web Activity opens login full-screen (no URL bar).
Requires registering the app's SHA-256 Key Hash in the Auth0
Dashboard; otherwise it falls back to a Custom Tab. Android only.
</Text>
<Button
onPress={() => setUseTrustedWebActivity((prev) => !prev)}
title={`Trusted Web Activity: ${
useTrustedWebActivity ? 'On' : 'Off'
}`}
style={!useTrustedWebActivity ? styles.inactiveButton : undefined}
/>
<Text style={styles.description}>
Recovers a login that completed after the OS killed the app
process. No-op on iOS/web.
</Text>
<Button onPress={onResumeSession} title="Resume Session" />
</>
</Section>

<Section title="Database Login">
Expand Down
4 changes: 3 additions & 1 deletion ios/A0Auth0.mm
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ - (dispatch_queue_t)methodQueue
safariViewControllerPresentationStyle:(nonnull NSNumber *)safariViewControllerPresentationStyle
additionalParameters:(NSDictionary * _Nullable)additionalParameters
allowedBrowserPackages:(NSArray * _Nullable)allowedBrowserPackages
useTrustedWebActivity:(BOOL)useTrustedWebActivity
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
reject:(RCTPromiseRejectBlock)reject) {
NSInteger maxAgeValue = maxAge != nil ? (NSInteger)[maxAge doubleValue] : 0;
NSInteger leewayValue = leeway != nil ? (NSInteger)[leeway doubleValue] : 0;
NSInteger safariStyleValue = safariViewControllerPresentationStyle != nil ? (NSInteger)[safariViewControllerPresentationStyle doubleValue] : 0;
Expand All @@ -160,6 +161,7 @@ - (dispatch_queue_t)methodQueue
federated:(BOOL)federated
redirectUri:(NSString *)redirectUri
allowedBrowserPackages:(NSArray * _Nullable)allowedBrowserPackages
useTrustedWebActivity:(BOOL)useTrustedWebActivity
resolve:(RCTPromiseResolveBlock)resolve
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 @@ -108,7 +108,8 @@ export class NativeBridgeManager implements INativeBridge {
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 ?? {},
options.allowedBrowserPackages
options.allowedBrowserPackages,
options.useTrustedWebActivity ?? false
);
return new CredentialsModel(credential);
}
Expand All @@ -122,7 +123,8 @@ export class NativeBridgeManager implements INativeBridge {
options.customScheme,
parameters.federated ?? false,
parameters.returnToUrl,
options.allowedBrowserPackages
options.allowedBrowserPackages,
options.useTrustedWebActivity ?? false
);
}

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

Expand Down Expand Up @@ -124,7 +125,8 @@ describe('NativeBridgeManager', () => {
false, // ephemeralSession
99, // presentationStyle
{}, // additionalParameters
undefined // allowedBrowserPackages
undefined, // allowedBrowserPackages
false // useTrustedWebActivity
);
});

Expand Down Expand Up @@ -157,7 +159,38 @@ describe('NativeBridgeManager', () => {
false,
99,
{},
allowedBrowserPackages
allowedBrowserPackages,
false // useTrustedWebActivity
);
});

it('should pass useTrustedWebActivity to native webAuth when provided', async () => {
MockedAuth0NativeModule.webAuth.mockResolvedValueOnce(
nativeSuccessCredentials as any
);

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

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

Expand Down Expand Up @@ -247,7 +280,8 @@ describe('NativeBridgeManager', () => {
options.customScheme,
parameters.federated,
parameters.returnToUrl,
undefined // allowedBrowserPackages
undefined, // allowedBrowserPackages
false // useTrustedWebActivity
);
});

Expand All @@ -265,7 +299,29 @@ describe('NativeBridgeManager', () => {
options.customScheme,
parameters.federated,
parameters.returnToUrl,
allowedBrowserPackages
allowedBrowserPackages,
false // useTrustedWebActivity
);
});

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

await bridge.clearSession(parameters, options);

expect(MockedAuth0NativeModule.webAuthLogout).toHaveBeenCalledWith(
options.customScheme,
parameters.federated,
parameters.returnToUrl,
undefined, // allowedBrowserPackages
true // useTrustedWebActivity
);
});
});
Expand Down
Loading
Loading