Skip to content
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
a3e1e66
Add dataTaskWithURL swizzle; supoort detection for when our IMP is ov…
Mar 7, 2026
0932e4c
Redefine IMP CONFLICT as error level loga
Mar 7, 2026
51dd4d9
Swizzle sessionWithConfiguration:delegate:delegateQueue: and support …
Mar 7, 2026
1cb83f0
Add document explaining architecture for the ios service layer
Mar 8, 2026
9d306df
Fortify network interception, initialization races, and expand SDK do…
Mar 8, 2026
22a4d6f
Bump version to 3.5.10 and link ARCHITECTURE.md in README
Mar 8, 2026
066b2ac
Update REFERENCE.md to include setTraceIDHeader and getTraceIDHeader
Mar 8, 2026
cfb8597
Update TROUBLESHOOTING.md with concrete developer integration workflo…
Mar 8, 2026
b4c6612
Update CHANGELOG.md for 3.5.10 release
Mar 8, 2026
2af8e6b
Expose native logMessage to JS and trigger native output upon JS Init…
Mar 8, 2026
a307c70
feat: iOS configurable swizzle auto-recovery attempts
Mar 8, 2026
b345d7c
docs: emphatically recommend Android pre-flight okhttp healthcheck
Mar 8, 2026
2799a3f
fix: use dynamic max attempts for exhaustion log
Mar 8, 2026
d5488b7
fix(ios): correct rejectionARC typo and method signature in ApproovSe…
Mar 9, 2026
93a68ae
docs: fix incorrect package name in USAGE.md snippets
Mar 9, 2026
8771255
fix(android): ensure fetchWithApproov provides empty body for POST/PU…
Mar 9, 2026
117e4a1
fix(android): refine fetchWithApproov body logic for better clarity a…
Mar 9, 2026
2061b4b
fix(ios): initialize nil synchronization locks for thread safety
Mar 9, 2026
6764feb
fix: update getPinningDiagnostics type definition to match platform r…
Mar 9, 2026
9a8c9c5
docs: correct addAllowedDelegate matching description in REFERENCE.md
Mar 9, 2026
53c5689
docs: update README.md to reflect supported RN version 0.76+
Mar 9, 2026
b38b901
fix(ios): prevent dataTaskWithRequest on Retry in fetchWithApproov
Mar 9, 2026
07fdeef
fix(ios): support message signing in fetchWithApproov
Mar 9, 2026
28262dd
fix: align NO_APPROOV_SERVICE behavior across platforms and docs
Mar 9, 2026
260a2e1
fix(ios): use thread-safe snapshot for header substitution lookup
Mar 9, 2026
678cc17
fix(js): support async onInit in ApproovProvider and add mount guards
Mar 9, 2026
fad4398
fix(types): add explicit types for ApproovProvider and useApproov
Mar 9, 2026
221bddd
fix(android): add no-op addAllowedDelegate for cross-platform safety
Mar 9, 2026
eb99b75
docs: correct getPinningDiagnostics payload description for iOS
Mar 9, 2026
94796b1
docs: refine setUseApproovStatusIfNoToken behavior and add missing br…
Mar 9, 2026
7592e3a
fix(ios): honor custom mutator errors to allow blocking on NO_APPROOV…
Mar 9, 2026
f785208
fix: decouple setUseApproovStatusIfNoToken from network-risk blocking…
Mar 9, 2026
de76b54
docs: clarify fetchWithApproov limitations and supported body types
Mar 9, 2026
56bdf53
fix(ios): only set mutation keys when headers are present and update …
Mar 9, 2026
a6e7759
Comment on fetchWithApproov limitations
Mar 9, 2026
05e416f
chore: consolidated version 3.5.11 entries into 3.5.10 and reverted v…
Mar 9, 2026
b2fcad0
Remove reference to 3rd party SDKs
Mar 9, 2026
fdfc95e
fix(ios): resolve build errors by standardizing logging and fixing de…
Mar 9, 2026
d91312d
Import Request to java service layer
Mar 10, 2026
a97fa10
fix(ios): use dynamic token header name in message signing gate check
Mar 10, 2026
8fe4c65
fix(android): prevent PinChangeListener leak in fetchWithApproov by u…
Mar 10, 2026
117a524
fix(android): prevent interceptor stacking on repeated updateClientFa…
Mar 10, 2026
613a56e
fix(ios): copy back all request properties from mutator, not just hea…
Mar 10, 2026
ad4856c
Fix request mutator round-tripping and recovery builder listener leaks
Mar 10, 2026
427af83
Update CHANGELOG.md
ivolz Mar 10, 2026
77f1586
Update ARCHITECTURE.md
ivolz Mar 10, 2026
140a1d0
Fix recovery-thread cleanup and secure fetch response handling
Mar 10, 2026
0f09dc4
Address comments
Mar 10, 2026
81c5c80
Guard Request type by checking object
ivolz Mar 10, 2026
a02a102
call finishTaskAndInvalidate upon completion
Mar 11, 2026
eda97f6
Add a check parsedURL != nil to prevent incorrect js URL
Mar 11, 2026
58e4bae
Recover early iOS sessions with shared service lookup
Mar 11, 2026
d697105
Update documentation
Mar 11, 2026
4d662f8
Validate fetchWithApproov method and body inputs on Android
Mar 11, 2026
10bf3a4
docs: add CHANGELOG entries for recovery flag cleanup, session leak f…
Mar 11, 2026
5624880
Validate iOS fetchWithApproov bridge inputs
Mar 11, 2026
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
115 changes: 115 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Approov React Native Service Layer Architecture

This document explains the design principles, historical challenges, and current robustness of the Approov React Native SDK’s network interception layer on iOS and Android.

## 1. Our Objective: Seamless Integration
Our primary goal is to provide a seamless, "free ride" integration experience for React Native developers. Instead of requiring developers to rewrite their application’s network requests using a proprietary, custom API, our service layer aims to automatically secure the standard, global React Native `fetch()` and `XMLHttpRequest` calls.

By hooking into the underlying native networking components that React Native uses, we can invisibly inject Approov tokens into headers and enforce dynamic certificate pinning. Developers get advanced security for all their existing API requests with minimal code changes.

To achieve this, the Approov service layer intercepts the following:
- **iOS:** React Native uses `NSURLSession` under the hood (specifically `RCTHTTPRequestHandler`). We use Objective-C Method Swizzling to intercept these sessions and inject our logic.
- **Android:** React Native uses a singleton `OkHttpClient`. We inject a custom `ApproovInterceptor` and `ApproovCertificatePinner` into the OkHttp builder factory to secure the requests.

---

## 2. iOS Interception Challenges and Solutions

On iOS, the interception relies on **Method Swizzling** — a technique that changes the implementation of Objective-C methods at runtime.

### The Architectural Timeline Constraint
While React Native itself exclusively builds `NSURLRequest` objects and calls `dataTaskWithRequest:`, the primary interception challenge comes from the inherent timeline of how React Native initializes on iOS:

1. App launches
2. iOS creates the `RCTBridge`
3. The `RCTNetworking` module loads and **creates its `NSURLSession` instance immediately** (securing its internal delegate).
4. The React Native JavaScript bundle executes.
5. `ApproovService.initialize(configString)` is eventually called from JavaScript.

Because the core React Native networking session is created *before* the Javascript code even executes to initialize Approov, we cannot hook the `sessionWithConfiguration:delegate:delegateQueue:` method to capture that initial session creation.

### Protection Scenarios and Edge Cases
To handle this timeline and the presence of third-party SDKs, our swizzling strategy is designed to intercept tasks right as they are launched, even if we missed the session creation. The table below outlines exactly which React Native networking scenarios we successfully protect, and the edge cases that bypass interception.

| Scenario | Network Call Source | Swizzle Result | Is Protected? | Notes |
| :--- | :--- | :--- | :--- | :--- |
| **Standard Fetch (Post-Init)** | Standard JS `fetch()` called after `ApproovService.initialize()` | `dataTaskWithRequest:` is intercepted. Interceptor recognizes the React Native `RCTHTTPRequestHandler` session. | ✅ **Yes** | The happy path. Token is added, and pinning delegate processes the TLS handshake. |
| **Early Fetch (Pre-Init)** | JS `fetch()` called *before* `ApproovService.initialize()` finishes. | Interceptor is bypassed because swizzles are not active yet, or because Approov refuses to add a token before config is ready. | ❌ **No** | Developers must ensure `fetch` calls await the `ApproovProvider` initialization completion. |
| **Late URL Convenience Methods** | A specialized 3rd party React Native native module (e.g., a fast image downloader) makes calls reusing the React Native `RCTHTTPRequestHandler` session, but chooses to call `dataTaskWithURL:` internally instead of `dataTaskWithRequest:`. | `dataTaskWithURL:` is intercepted, converted to an `NSURLRequest`, and funneled into standard processing. | ✅ **Yes** | Previously an edge case bypass. Because our old hook only watched the `*WithRequest:` door, traffic entering the same protected session through the `*WithURL:` door snuck out without an Approov token. |
| **Custom 3rd Party Session (Unrecognized Delegate)** | A 3rd party native module creates its completely own `NSURLSession` instance with a custom delegate. | Task is intercepted at the global class level, but Approov recognizes the delegate does not match React Native's `RCTHTTPRequestHandler`. | ❌ **No** | Approov explicitly skips interception to prevent crashing third-party logic. Developers must add the custom delegate class via `ApproovService.addAllowedDelegate()` to protect it. |
| **Aggressive Swizzling Conflicts** | An observability SDK (like Datadog) globally swizzles all `NSURLSession` methods to track metrics. If they swizzle *after* Approov, or wrap/hijack the specific React Native task delegates globally, they sever our hooks. | Our hook is bypassed or our Delegate is swallowed by the observability SDK. | ❌ **No** | We log `IMP CONFLICT` warnings. Use `fetchWithApproov` to guarantee protection if the race condition cannot be won. |
| **Custom Network Stack** | 3rd party framework completely bypasses `NSURLSession` (e.g., uses low-level sockets or CFNetwork directly). | No swizzles are triggered at all. | ❌ **No** | Approov only protects `NSURLSession`-based networking on iOS. |

### What Could Go Wrong Before
Previously, the Approov React Native interceptor had several vulnerabilities to these complex environments:
1. **Late Initialization:** Swizzling was performed when the React Native module initialized natively (`startWithApproovService:`). This could be delayed or happen after other aggressive SDKs had already hooked the network stack.
2. **Missing Swizzle Targets:** We primarily swizzled `dataTaskWithRequest:`, occasionally missing alternative convenience APIs like `dataTaskWithURL:` which some libraries or modified React Native networking stacks might use.
3. **Silent Failures:** If another SDK overwrote our swizzled implementation pointers (IMPs) later in the app's lifecycle, there was no visibility into the broken state. This led to silent failures where Approov appeared active but was simply being bypassed.

### Current State and Fixes
To fortify the iOS interceptor against these race conditions, several critical improvements were implemented:
1. **`+load` Time Swizzling:** We migrated the swizzle installation to the Objective-C class `+load` method. This guarantees our hooks are installed at the absolute earliest possible moment during runtime initialization, ensuring we sit securely at the bottom of the swizzle stack and intercept before other SDKs.
2. **Comprehensive Hooking:** We expanded the swizzles to cover `dataTaskWithURL:` and its completion handler variants, ensuring all entry points to session tasks are intercepted and routed through our protection logic.
3. **IMP Integrity Checking:** We implemented a system that records the implementation pointers (IMPs) of the original methods during `+load`. When sessions are fired later, the interceptor verifies that the IMPs have not been overwritten and logs a conspicuous warning (`IMP CONFLICT`) if another SDK has compromised our hooks.

---

## 3. Android Interception Challenges and Solutions

While iOS relies on Objective-C method swizzling, the Android networking layer in React Native is built entirely upon OkHttp. The networking stack uses a singleton `OkHttpClient` that is dynamically managed by the `OkHttpClientProvider`.

### How Interception Works on Android
OkHttp utilizes an **Interceptor Chain**. When a network request is fired, it passes through a sequence of registered interceptors before hitting the actual network.

To protect React Native traffic on Android, Approov injects itself into this pipeline by:
1. Registering a custom `OkHttpClientFactory` with `OkHttpClientProvider`.
2. When the React Native networking module requests an HTTP client, our factory builds one that includes the `ApproovInterceptor` (to inject tokens and handle header mutations) and the `ApproovCertificatePinner` (to handle TLS pinning natively).

### The Interference: The OkHttpClientFactory Race
Similar to the swizzling race condition on iOS, there is a fundamental initialization race condition on Android when multiple SDKs (like Datadog, New Relic, or Firebase) try to monitor network traffic.

Third-party observability SDKs also need to inject their own OkHttp interceptors to track network analytics. They do this exactly the same way Approov does: by calling `OkHttpClientProvider.setOkHttpClientFactory()` to register a custom factory that attaches their interceptors.

**The Fatal Override:**
The `OkHttpClientProvider` only holds a **single** factory at a time. The last SDK to call `setOkHttpClientFactory()` wins.

If a 3rd party SDK initializes *after* Approov (for instance, dynamically from Javascript or later in the native React application lifecycle), their factory completely overwrites Approov's factory. When React Native natively constructs its HTTP client, it uses the 3rd party SDK's factory, resulting in a client that completely lacks the `ApproovInterceptor` and `ApproovCertificatePinner`.

The result is exactly the same as an iOS bypass: React Native traffic flows normally, but it flows without Approov tokens and completely circumvents dynamic TLS pinning because the active OkHttp instance knows nothing about Approov.

### Current State and Fixes: Diagnosis and Healing
Because Android provides no global `+load` equivalent that guarantees absolute first-execution-order natively out of the box, we resolve this factory override problem dynamically and safely using **Diagnostics and Healing**:

1. **Diagnostics (Active Chain Verification):**
Because we cannot prevent a 3rd party SDK from overwriting the factory later in the lifecycle, we provide native diagnostic routines so the app can verify its own protection status. This checks the *currently active* `OkHttpClient` singleton inside React Native to verify that the `ApproovInterceptor` and `ApproovCertificatePinner` class instances are genuinely present in the OkHttp execution chain.

2. **Safe Healing (`updateClientFactory` API):**
If a bypass is detected (i.e., another SDK stole the factory registration), Approov provides a recovery mechanism.
When the healing API is invoked, Approov retrieves the *currently active* custom factory (the one injected by the 3rd party SDK), proxies it to append the `ApproovInterceptor` and `ApproovCertificatePinner` to the output of their builder, and re-registers this combined factory back into the `OkHttpClientProvider`.

This safely layers Approov directly on top of the foreign SDK's modifications. It ensures that observability metrics and tracing continue to function correctly for the 3rd party, while mathematically guaranteeing that Approov Token injection and TLS Pinning fire natively on every single Android `fetch()` request.

---

## 4. The `fetchWithApproov` Alternative

Despite our robust hooking mechanisms, highly aggressive third-party SDKs might still find ways to circumvent global interceptions or severely mutate the global network clients, leading to persistent drops in protection for critical API calls.

To provide a guaranteed, conflict-free path for sensitive requests, we introduced the **`fetchWithApproov` API**.

### How it Fits as an Alternative
`ApproovService.fetchWithApproov` is a secure `fetch()`-compatible API for sensitive calls. Instead of routing through React Native's global `NetworkingModule` (which is subject to swizzling and OkHttp factory overrides), it bridges directly to isolated, natively protected HTTP clients:
- On Android, it builds and utilizes an independent `OkHttpClient` configured directly with the `ApproovClientBuilder`.
- On iOS, it uses a standalone `NSURLSession` with its own dedicated pinning delegate.

By completely bypassing the shared React Native networking stack, these isolated clients are immune to the global swizzling or factory overrides applied by other SDKs. For critical authentication or transaction endpoints, `fetchWithApproov` ensures 100% protection reliability.

### Inconveniences and Trade-offs
Because `fetchWithApproov` bypasses the core React Native networking bridge events and handles data transformation independently across the native bridge, it has major limitations compared to the standard `fetch()`:
- **No `FormData` Streaming:** It does not support streaming or uploading multipart `FormData` (e.g. uploading large images via URIs).
- **No Binary `Blob` Support:** It cannot handle binary `Blob` or `ArrayBuffer` bodies natively; everything must be stringified or base64 encoded by the caller.
- **No Request Cancellation:** It does not support the `AbortController` API to cancel inflight requests.
- **No React Native Networking Event Model:** Features tied to RN `NetworkingModule`/`XMLHttpRequest` events (such as progress callbacks) are not available.
- **Memory Buffering Constraints:** Large responses are buffered entirely in memory before crossing the React Native bridge, rather than streaming in chunks.

As a result, `fetchWithApproov` is specifically designed for standard JSON / Text REST API payloads where guaranteed security is paramount, serving as a reliable fallback when global interception is untenable in complex app environments.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.



## [3.5.10]
- **iOS Mutator Bridge Refinement**: Fixed a condition in the iOS mutator bridge where trace ID components were incorrectly required for signing even when the trace header was absent. Header mutation keys are now only set if the corresponding header is present and non-empty.
- **Custom iOS Mutator Reliability**: Fixed an issue where custom iOS mutators could not reliably block requests during `NO_APPROOV_SERVICE` events. The native interceptor now correctly honors and propagates errors from the mutator bridge.
- **Network-Risk Handling Standardized**: Refined the default mutator behavior on both Android and iOS to consistently block/retry for `NO_NETWORK`, `POOR_NETWORK`, and `MITM_DETECTED` statuses, independent of the `setUseApproovStatusIfNoToken` flag.
- **Async ApproovProvider**: The `ApproovProvider` component now correctly `await`s asynchronous `onInit` functions and includes mount-state guards to prevent state updates on unmounted components.
- **Android fetchWithApproov Fix**: Resolved an issue on Android where `fetchWithApproov` incorrectly handled POST/PUT request bodies in some scenarios.
- **Simplified Cross-Platform API**: Added a no-op `addAllowedDelegate` method on Android to match the iOS bridge, ensuring JS-level calls are safe across both platforms.
- **Documentation & Types Sync**: Synchronized `REFERENCE.md` and TypeScript definitions with actual native diagnostic payloads, improving accuracy for `getPinningDiagnostics`.
- **iOS Swizzle Auto-Recovery**: The iOS interceptor now actively monitors its hooks during execution. If a third-party SDK overwrites the Approov hooks at runtime, it will perform an auto-recovery by re-swizzling itself back to the top of the chain.
- **Configurable Reswizzle Attempts**: Introduced `ApproovService.setMaxReswizzleAttempts(attempts)` and `ApproovService.getMaxReswizzleAttempts()` to allow developers to configure the number of times the iOS auto-recovery will trigger (defaults to 3).
- **dataTaskWithURL Coverage**: Added swizzle interception for `dataTaskWithURL:` on iOS, ensuring that 3rd party native React Native modules (like image downloaders or video players) that bypass the standard `NSMutableURLRequest` flow are still funneled through the Approov core.
- **Initialization Race Conditions Fixed**: Added early `isInitialized()` checks in the Android OkHttp interceptor and iOS `NSURLSession` interceptor to bypass the existing `Thread.sleep` / `[NSThread sleepForTimeInterval:]` blocking loops once JS React Native initialization has completed. If initialization is not awaited or never completes, both platforms still fall back gracefully with a one-time critical error log.
- **Trace ID Documentation**: Documented the `setTraceIDHeader` and `getTraceIDHeader` JS/native bridging methods in `REFERENCE.md`.
- **Developer Documentation Guides**: Significantly expanded `ARCHITECTURE.md` and `TROUBLESHOOTING.md` to explain 3rd party SDK conflicts (Android OkHttp factory overrides, iOS custom delegates), how to diagnose them via native logs/stats, and exact workflows to safely resolve them during integration.
- **iOS Message Signing with Custom Token Header**: Fixed a bug where iOS message signing (Signature/Signature-Input headers) was silently skipped when the token header was customized via `setTokenHeader`. The signing gate now uses the dynamic header name instead of the hardcoded `Approov-Token`.
- **Android PinChangeListener Leak**: Fixed a memory leak where each `fetchWithApproov` call created a new `ApproovClientBuilder` that permanently registered as a `PinChangeListener`. Ephemeral builders used for one-shot fetches now skip listener registration.
- **Android Interceptor Stacking**: Fixed a bug where repeated `updateClientFactory(true)` calls would duplicate Approov interceptors in the OkHttp chain, causing multiple token fetches and signature generations per request. The cloned builder now strips existing `ApproovInterceptor` instances before adding a fresh one.
- **iOS Mutator Bridge Full Request Copy-Back**: The iOS `ApproovServiceMutatorBridge` now copies back all request properties (URL, method, body, timeout) from the mutator's processed request, not just headers. This ensures custom mutators that modify non-header fields work correctly on iOS.

## [3.5.9]
- **iOS Bridging Header Fix**: Resolved an issue where React Native apps failed to compile with the error `'approov_service_react_native-Swift.h' file not found`. The import now correctly prefers modular framework headers (`<approov_service_react_native/approov_service_react_native-Swift.h>`) and falls back to the quoted header when needed during CocoaPods compilation.
- **iOS Dynamic Framework Support**: Added `s.static_framework = true` and `s.dependency "React-Core"` to the `approov-service-react-native.podspec`. This prevents `_RCTRegisterModule` linker errors when consumers explicitly enable `use_frameworks! :linkage => :dynamic` in their Podfile, ensuring broad compatibility across standard and dynamic React Native setups.
Expand Down
Loading
Loading