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
61 changes: 60 additions & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
- [Allowed Browsers (Android)](#allowed-browsers-android)
- [Using with Hooks](#using-with-hooks)
- [Using with Auth0 Class](#using-with-auth0-class-1)
- [Recovering Login After Process Death (Android)](#recovering-login-after-process-death-android)
- [Using with Hooks](#recovering-login-using-hooks)
- [Using with Auth0 Class](#recovering-login-using-the-auth0-class)
- [DPoP (Demonstrating Proof-of-Possession)](#dpop-demonstrating-proof-of-possession)
- [Enabling DPoP](#enabling-dpop)
- [Making API calls with DPoP](#making-api-calls-with-dpop)
Expand Down Expand Up @@ -145,7 +148,9 @@ If you already have credentials (e.g. from `webAuth.authorize()` or `credentials
import Auth0, { parseIdToken } from 'react-native-auth0';

const auth0 = new Auth0({ domain, clientId });
const credentials = await auth0.webAuth.authorize({ scope: 'openid profile email' });
const credentials = await auth0.webAuth.authorize({
scope: 'openid profile email',
});
const user = parseIdToken(credentials.idToken);
// user.sub, user.name, user.email, etc.
```
Expand Down Expand Up @@ -1977,6 +1982,60 @@ await auth0.webAuth.authorize(

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

## 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.

`resumeSession()` recovers that login. The underlying native SDK finishes the token exchange after the process restarts and buffers the result; calling `resumeSession()` once on cold start drains it and returns the recovered `Credentials` (or `null` if there was nothing to recover).

> This is an Android-only concern. On iOS and web `resumeSession()` is a no-op that resolves with `null`, so it is safe to call unconditionally. It requires `react-native-auth0` bundling Auth0.Android 3.19.0+ (included). No native `MainActivity` changes are needed, so it works the same in bare React Native and Expo.

### Recovering Login Using Hooks

Call `resumeSession()` once when your app mounts. If it returns credentials, the hook updates the auth state and persists them automatically, so `user` becomes populated.

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

const App = () => {
const { resumeSession } = useAuth0();

useEffect(() => {
resumeSession()
.then((credentials) => {
if (credentials) {
// A login interrupted by process death was recovered.
console.log('Recovered session', credentials.accessToken);
}
})
.catch((error) => {
console.log('Failed to recover session', error);
});
}, [resumeSession]);

// ...
};
```

### Recovering Login Using the Auth0 Class

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

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

// Call once on cold start, before showing the login screen.
const credentials = await auth0.webAuth.resumeSession();
if (credentials) {
await auth0.credentialsManager.saveCredentials(credentials);
// The user is logged in — route them into the app.
}
```

## 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
19 changes: 18 additions & 1 deletion FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,24 @@ See these issues for more information:
- [possibility to run with launchMode:singleTop?](https://github.com/auth0/react-native-auth0/issues/170)
- [Android singleTask launch mode is required for react-native deep links](https://github.com/auth0/react-native-auth0/issues/556)

### The solution
### Recovering the login (recommended)

If the OS kills your app's process entirely while the browser is open (rather than just pausing the activity), the SDK can still complete the login on restart. Call `resumeSession()` once on cold start to recover it:

```js
// With the Auth0 class
const credentials = await auth0.webAuth.resumeSession();

// Or with hooks
const { resumeSession } = useAuth0();
useEffect(() => {
resumeSession();
}, [resumeSession]);
```

If a login was interrupted by process death, `resumeSession()` returns the recovered `Credentials`; otherwise it returns `null`. It is Android-only (a no-op resolving `null` on iOS/web) and requires no `MainActivity` changes, so it works in both bare React Native and Expo. See [Recovering Login After Process Death (Android)](EXAMPLES.md#recovering-login-after-process-death-android) for full examples.

### The solution (for `singleTask` launch mode)

If your Android `launchMode` is set to `singleTask` (check your `AndroidManifest.xml`), that's why this is occurring. Unfortunately, this is not addressable by the react-native-auth0 library.

Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.browser:browser:1.2.0"
implementation 'com.auth0.android:auth0:3.18.0'
implementation 'com.auth0.android:auth0:3.19.0'
}

if (isNewArchitectureEnabled()) {
Expand Down
118 changes: 101 additions & 17 deletions android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package com.auth0.react
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LifecycleOwner
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
Expand Down Expand Up @@ -38,6 +41,11 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0

companion object {
const val NAME = "A0Auth0"

// Grace window (ms) for an in-flight restored token exchange to complete before
// resumeWebAuthSession resolves null.
private const val RESUME_SESSION_GRACE_MS = 5000L

private const val CREDENTIAL_MANAGER_ERROR_CODE = "CREDENTIAL_MANAGER_ERROR"
private const val BIOMETRICS_AUTHENTICATION_ERROR_CODE = "BIOMETRICS_CONFIGURATION_ERROR"

Expand Down Expand Up @@ -166,19 +174,86 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
}

builder.withParameters(cleanedParameters)
builder.start(reactContext.currentActivity as Activity,
object : com.auth0.android.callback.Callback<Credentials, AuthenticationException> {

val activity = reactContext.currentActivity
if (activity == null) {
promise.reject("a0.activity_not_available", "Current Activity is not available")
webAuthPromise = null
return
}
// start() registers a LifecycleObserver internally (Auth0.Android 3.19.0+ for
// process-death recovery), which must happen on the main thread.
UiThreadUtil.runOnUiThread {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

where is UiThreadUtil located? is it auth0.android api or inbuilt api from android SDK

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.

its from com.facebook.react.bridge.UiThreadUtil a react native built-in library

@pmathew92 pmathew92 Jun 18, 2026

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.

Whats the need to explicitly put it to uiThread here ? Isn't this callback executed in UI thread ? Would there be any issues if this is not added ?

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.

as auth0.android usage LifecycleObserver, that can't be run on without main thread, otherwise it throws IllegalStateException.

builder.start(activity,
object : com.auth0.android.callback.Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) {
val map = CredentialsParser.toMap(result)
promise.resolve(map)
webAuthPromise = null
}

override fun onFailure(error: AuthenticationException) {
handleError(error, promise)
webAuthPromise = null
}
})
}
}

@ReactMethod
override fun resumeWebAuthSession(promise: Promise) {
val activity = reactContext.currentActivity
if (activity !is LifecycleOwner) {
// No lifecycle owner to register against; nothing to recover.
promise.resolve(null)
return
}
val lifecycleOwner = activity as LifecycleOwner

UiThreadUtil.runOnUiThread {
val resolved = java.util.concurrent.atomic.AtomicBoolean(false)
// Holds the pending grace-window timeout so a callback that settles the promise
// first can cancel it, releasing the retained promise/activity references instead
// of leaking them until the delay elapses.
val timeoutHandler = Handler(Looper.getMainLooper())
val loginCallback = object :
com.auth0.android.callback.Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) {
val map = CredentialsParser.toMap(result)
promise.resolve(map)
webAuthPromise = null
if (resolved.compareAndSet(false, true)) {
timeoutHandler.removeCallbacksAndMessages(null)
promise.resolve(CredentialsParser.toMap(result))
}
}

override fun onFailure(error: AuthenticationException) {
handleError(error, promise)
webAuthPromise = null
if (resolved.compareAndSet(false, true)) {
timeoutHandler.removeCallbacksAndMessages(null)
handleError(error, promise)
}
}
})
}
// Logout recovery is out of scope; provide a no-op logout callback.
val logoutCallback = object :
com.auth0.android.callback.Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) {}
override fun onFailure(error: AuthenticationException) {}
}

// Registering against an already-RESUMED owner synchronously replays onResume,
// draining any result buffered after process-death recovery.
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback, logoutCallback)

// Safety net for the rare case where the restored token exchange is still in
// flight: give it a short grace window, then resolve null if nothing arrived.
// The login callback cancels this timeout if it settles first.
if (!resolved.get()) {
timeoutHandler.postDelayed({
if (resolved.compareAndSet(false, true)) {
promise.resolve(null)
}
}, RESUME_SESSION_GRACE_MS)
}
}
}

@ReactMethod
Expand Down Expand Up @@ -392,16 +467,25 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
)
}

builder.start(reactContext.currentActivity as FragmentActivity,
object : com.auth0.android.callback.Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) {
promise.resolve(true)
}
val activity = reactContext.currentActivity
if (activity !is FragmentActivity) {
promise.reject("a0.activity_not_available", "Current Activity is not a FragmentActivity")
return
}
// start() registers a LifecycleObserver internally (Auth0.Android 3.19.0+),
// which must happen on the main thread.
UiThreadUtil.runOnUiThread {
builder.start(activity,
object : com.auth0.android.callback.Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) {
promise.resolve(true)
}

override fun onFailure(e: AuthenticationException) {
handleError(e, promise)
}
})
override fun onFailure(e: AuthenticationException) {
handleError(e, promise)
}
})
}
}

@ReactMethod
Expand Down
4 changes: 4 additions & 0 deletions android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
@DoNotStrip
abstract fun resumeWebAuth(url: String, promise: Promise)

@ReactMethod
@DoNotStrip
abstract fun resumeWebAuthSession(promise: Promise)

@ReactMethod
@DoNotStrip
abstract fun cancelWebAuth(promise: Promise)
Expand Down
4 changes: 4 additions & 0 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,8 @@ dependencies {
} else {
implementation jscFlavor
}

// Required by PasskeyModule.kt (androidx.credentials.*)
implementation("androidx.credentials:credentials:1.3.0")
implementation("androidx.credentials:credentials-play-services-auth:1.3.0")
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.auth0example

import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
Expand All @@ -13,6 +14,17 @@ class MainActivity : ReactActivity() {
*/
override fun getMainComponentName(): String = "Auth0Example"

/**
* react-native-screens requires that fragment state is never restored, otherwise the app
* crashes with "Screen fragments should never be restored" when the OS recreates the
* Activity (e.g. after the Auth0 redirect or process death). Passing null discards the
* saved fragment hierarchy so React Native rebuilds it fresh.
* See https://github.com/software-mansion/react-native-screens/issues/17
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}

/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
Expand Down
25 changes: 22 additions & 3 deletions example/src/navigation/HooksDemoNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import {
Auth0Provider,
useAuth0,
Expand All @@ -8,7 +8,7 @@ import {
} from 'react-native-auth0';
import AuthStackNavigator from './AuthStackNavigator';
import MainTabNavigator from './MainTabNavigator';
import { ActivityIndicator, View, StyleSheet } from 'react-native';
import { ActivityIndicator, View, StyleSheet, Platform } from 'react-native';
import config from '../auth0-configuration';

const AUTH0_DOMAIN = config.domain;
Expand All @@ -20,7 +20,26 @@ const AUTH0_CLIENT_ID = config.clientId;
* It's rendered inside the Auth0Provider so it can use the useAuth0 hook.
*/
const AppContent = () => {
const { user, isLoading } = useAuth0();
const { user, isLoading, resumeSession } = useAuth0();

// On Android the OS can kill the app process while the user is completing
// login in the browser. When the app cold-starts, resumeSession() recovers
// any login that finished after the process was killed. It is a safe no-op
// that resolves null on iOS and web, so it can be called unconditionally.
useEffect(() => {
if (Platform.OS !== 'android') {
return;
}
resumeSession()
.then((credentials) => {
if (credentials) {
console.log('Recovered login after process death');
}
})
.catch((e) => {
console.warn('resumeSession failed', e);
});
}, [resumeSession]);

if (isLoading) {
return (
Expand Down
Loading
Loading