FormidableLabs / FormidableLabs/react-native-app-auth

[Android] AuthorizationService is never disposed: Custom Tabs bind leak breaks refresh() permanently after ~1000 calls

Offen
#1,126 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
Java
Sterne
2.3k
Forks
473
PR-Merge-Kennzahlen
Keine gemergten PRs in 30 T.

Beschreibung

## Issue

On Android, the native module creates `new AuthorizationService(...)` at every call site and never calls `dispose()` on it. The string `dispose` does not appear anywhere in `RNAppAuthModule.java` — I checked `main` today as well as the published 8.1.0 and 8.4.1 artifacts.

`AuthorizationService`'s constructor eagerly binds to the selected browser's Custom Tabs service:

```
new AuthorizationService(context, config)
-> BrowserSelector.select(...)
-> CustomTabManager.bind(browserPackage)
-> CustomTabsClient.bindCustomTabsService(...)
-> Context.bindService(...)
```

That bind is released *only* by `AuthorizationService.dispose()` -> `CustomTabManager.dispose()` -> `unbindService`. AppAuth states the contract explicitly in the `AuthorizationService` javadoc:

> instances of this class **must be manually disposed** when no longer required, to avoid leaks

So each call through this library leaks one `ServiceConnection` to the default browser's `CustomTabsService`. Android's ActivityManager caps outstanding bind requests per process/service at ~1000. Once that is crossed, `bindService` throws:

```
Error: Too many bind requests(999+) for service Intent { act=android.support.customtabs.action.CustomTabsService pkg=com.sec.android.app.sbrowser }
```

The exception is thrown synchronously from the `AuthorizationService` constructor, so the JS promise rejects. Crucially, the bind counter only resets when the process dies — so from that moment on **every** subsequent call fails for the rest of the process lifetime. In a long-lived app the user is silently unable to refresh their token until they force-stop it.

`refresh()` is by far the worst affected, because it is the call an app makes repeatedly and unattended — and it needs no browser at all. The Custom Tabs bind is pure collateral damage from the constructor.

### Affected call sites

All six `new AuthorizationService(...)` in `packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java` on `main` (lines ~532, 614, 740, 753, 799, 849). None are disposed. The one in `refreshWithConfiguration` (~799) is the one that bites in practice.

### Reproduction

1. Android device with a Custom Tabs–capable default browser (seen with Samsung Internet).
2. Call `refresh()` repeatedly within a single process — e.g. an OP with a short access-token lifetime, refreshed on demand before API calls.
3. After ~1000 refreshes without the process being killed, every `refresh()` rejects with the error above and never recovers.

Observed in production on a device whose app process had stayed alive for ~10 days.

### Suggested fix

Minimal, per call site — dispose in the response callback:

```java
final AuthorizationService authService = new AuthorizationService(context, appAuthConfiguration);

AuthorizationService.TokenResponseCallback tokenResponseCallback = new AuthorizationService.TokenResponseCallback() {
@Override
public void onTokenRequestCompleted(@Nullable TokenResponse response, @Nullable AuthorizationException ex) {
authService.dispose();
// ...existing handling
}
};
```

Note that the `authorize` / `endSession` flows hand the service off to an Activity-based redirect flow, so disposal there has to happen after the redirect is handled rather than immediately.

Better still, mirror what [`flutter_appauth`](https://github.com/MaikuB/flutter_appauth) does with the same underlying `net.openid:appauth` library: keep a **single** lazily-created `AuthorizationService`, reuse it across calls, and dispose it on lifecycle teardown (`onDetachedFromEngine` in their case). See `FlutterAppauthPlugin.java` — `createAuthorizationServices()` / `disposeAuthorizationServices()`, plus their hardening PRs [#170](https://github.com/MaikuB/flutter_appauth/pull/170), [#556](https://github.com/MaikuB/flutter_appauth/pull/556) and [#600](https://github.com/MaikuB/flutter_appauth/pull/600).

### Prior art

- [auth0/Auth0.Android#517](https://github.com/auth0/Auth0.Android/pull/517) — "Fix memory leak in CustomTabsService", the same class of leak in another OAuth SDK's Custom Tabs controller.
- [openid/AppAuth-Android#1085](https://github.com/openid/AppAuth-Android/issues/1085), [#91](https://github.com/openid/AppAuth-Android/issues/91), [#134](https://github.com/openid/AppAuth-Android/issues/134), [#166](https://github.com/openid/AppAuth-Android/issues/166) — recurring reports around the bind/unbind side of `CustomTabManager`.
- `expo-web-browser` pairs `bindCustomTabsService` with `unbindService` and exposes `warmUp` / `coolDown` to callers, so the contract is well established elsewhere in the React Native ecosystem.

I'm happy to open a PR for the `refresh` / token-request paths if you'd like — let me know whether you'd prefer the minimal per-call `dispose()` or the cached-instance approach.

---

## Environment

* **Your Identity Provider**: Keycloak (not provider-specific — any OP with short-lived access tokens will reach the threshold)
* **Platform that you're experiencing the issue on**: Android
* **Your `react-native` Version**: 0.81.5 (Hermes, New Architecture)
* **Your `react-native-app-auth` Version**: 8.1.0 — but confirmed still present on `main` / 8.4.1
* **Are you using Expo?** Yes
* **Device**: Samsung Galaxy Tab A7 (SM-T505), Android 12, default browser Samsung Internet

Beitragsleitfaden

Beitragsleitfaden öffnen

Rechercherichtung

Beginne in packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java und untersuche die sechs AuthorizationService-Aufrufstellen, insbesondere refreshWithConfiguration und dessen Token-Response-Callback. Vergleiche den Vorschlag, pro Aufruf dispose aufzurufen, mit dem Ansatz eines gecachten Service-Lebenszyklus, einschließlich der Redirect-Behandlung von authorize und endSession. Als erledigt gilt dies, wenn wiederholte Refresh-Aufrufe keine Custom Tabs-Bindings mehr anhäufen und die betroffenen Flows weiterhin korrekt abgeschlossen werden.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
android, java
Bereich
authentication, mobile
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Ruhig
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
62/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.