Skip to content

fix : Handling DPoP enabled WebAuth flow after process death#977

Open
pmathew92 wants to merge 5 commits into
mainfrom
fix_dpop_process_death
Open

fix : Handling DPoP enabled WebAuth flow after process death#977
pmathew92 wants to merge 5 commits into
mainfrom
fix_dpop_process_death

Conversation

@pmathew92

@pmathew92 pmathew92 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Changes

Fixes a bug where the OAuth token exchange fails after process death during a
DPoP-enabled Universal Login flow.

When the Android host process is killed while Chrome Custom Tabs is open, the SDK
persists OAuthManagerState to savedInstanceState and rebuilds the manager via
OAuthManager.fromState on restart. Previously, the DPoP configuration was lost on
this restore path:

  • OAuthManager.toState() did not persist the DPoP-enabled flag.
  • OAuthManager.fromState() reconstructed the manager without DPoP, and the restored
    PKCE's AuthenticationAPIClient was never DPoP-enabled.

As a result, the /oauth/token exchange after process restart was sent without a
DPoP proof header
, and the server rejected it with HTTP 400 invalid_request
("Authorization Code is bound to a DPoP key, but DPoP header is missing or invalid").

This change persists a dPoPEnabled boolean in OAuthManagerState (the DPoP
object holds a Context and cannot be serialized directly), and on restore re-creates
the DPoP instance and re-enables DPoP on the restored API client so the token
exchange carries the proof header.

References

Files changed

  • OAuthManager.kttoState persists dPoPEnabled; fromState takes a Context,
    re-enables DPoP on the restored API client, and rebuilds the DPoP instance.
  • OAuthManagerState.kt — replaced the non-serializable DPoP? field with
    dPoPEnabled: Boolean.
  • WebAuthProvider.ktonRestoreInstanceState accepts a Context and forwards it
    to fromState. PAR restore path is unchanged.
  • AuthenticationActivity.kt — passes the activity Context into
    onRestoreInstanceState.

Tests

  • OAuthManagerStateTest — serialization of dPoPEnabled (true / default false /
    legacy JSON missing the field defaults to false, preserving back-compat with
    previously persisted state).
  • WebAuthProviderTest — added a behavioral test that simulates the process
    death → restore path and asserts DPoP is re-enabled on the restored manager.

Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. Since this library has unit testing, tests should be added for new functionality and existing tests should complete without errors.

  • This change adds unit test coverage

  • This change adds integration test coverage

  • This change has been tested on the latest version of the platform/language or why not

Checklist

Summary by CodeRabbit

  • Bug Fixes

    • DPoP (proof-of-possession) configuration now persists and is reliably re-enabled after app process restarts, improving authentication continuity and security.
    • Improved state restoration during authentication flows to ensure restored sessions behave consistently.
  • Tests

    • Added tests covering DPoP serialization/deserialization and restoration across process death scenarios.

@pmathew92 pmathew92 requested a review from a team as a code owner June 8, 2026 07:18
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR persists DPoP enablement as a boolean, updates serialization/deserialization, threads an Android Context through the restore path so OAuthManager.fromState can re-enable DPoP, and adds tests plus a CI action pin update.

Changes

DPoP State Persistence Across Process Death

Layer / File(s) Summary
DPoP State Model and Serialization
auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt, auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt
OAuthManagerState stores DPoP enablement as a boolean flag instead of a DPoP object. Serialization writes dPoPEnabled to JSON; deserialization reads it back and defaults to false when the field is missing from legacy JSON. Tests verify explicit true/false serialization and legacy JSON compatibility.
OAuth Manager Restore with DPoP Re-enablement
auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt, auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt
OAuthManager.fromState accepts a Context parameter and re-enables DPoP on the AuthenticationAPIClient if dPoPEnabled is true, conditionally constructs the DPoP instance, and restores PKCE, headers, and id-token verification settings. OAuthManager.toState persists the DPoP enabled flag. Regression tests validate restored client's isDPoPEnabled state.
WebAuthProvider State Restoration with Context
auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt
WebAuthProvider.onRestoreInstanceState now requires a Context parameter and passes it to OAuthManager.fromState during OAuth manager reconstruction.
AuthenticationActivity Integration
auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.kt
AuthenticationActivity.onCreate passes itself as the Context when calling WebAuthProvider.onRestoreInstanceState.
Process Death Recovery Integration Test
auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt
New test simulates process death by saving and restoring WebAuthProvider state from a Bundle, verifying that the restored OAuthManager has DPoP re-enabled.
CI Action Pin Update
.github/workflows/test.yml
Update pinned commit SHA for codecov/codecov-action from pin@6.0.1 to pin@7.0.0 in the unit job.

Sequence Diagram

sequenceDiagram
  participant Activity as AuthenticationActivity
  participant WebAuth as WebAuthProvider
  participant OAuthMgr as OAuthManager
  participant APIClient as AuthenticationAPIClient
  participant DPoP as DPoP
  Activity->>WebAuth: onRestoreInstanceState(bundle, context)
  WebAuth->>OAuthMgr: fromState(state, callback, context)
  alt state.dPoPEnabled == true
    OAuthMgr->>APIClient: enableDPoP()
    OAuthMgr->>DPoP: construct DPoP(context)
  end
  OAuthMgr->>OAuthMgr: restore PKCE, headers, id-token verification
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A boolean keeps the DPoP light,
Through bundle dark and process night,
Context sewn along the thread,
Restored again — proof wakes from bed,
Hops on, and carries on the flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main change: fixing handling of DPoP-enabled WebAuth flow after process death, which is the core issue addressed across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix_dpop_process_death

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt (1)

2979-2980: 🏗️ Heavy lift

Validate restored behavior at the token exchange boundary, not only object presence.

A non-null restoredManager.dPoP doesn’t prove /oauth/token uses DPoP proof. Add an assertion that the resumed token request carries the DPoP header to directly cover the original regression.

Suggested test direction
-        val restoredManager = WebAuthProvider.managerInstance as OAuthManager
-        assertThat(restoredManager.dPoP, `is`(notNullValue()))
+        val restoredManager = WebAuthProvider.managerInstance as OAuthManager
+        assertThat(restoredManager.dPoP, `is`(notNullValue()))
+        // Then resume the flow and assert the outgoing /oauth/token request includes a DPoP header.
+        // (Use existing request-capture patterns in this test suite to verify headers.)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt` around
lines 2979 - 2980, The test currently only checks restoredManager.dPoP is
non-null; update it to assert the resumed token-exchange request actually
includes the DPoP header so the /oauth/token boundary is covered. After
obtaining restoredManager from WebAuthProvider.managerInstance (cast to
OAuthManager), simulate or capture the resumed token request the manager sends
during resume (the same place the original regression occurred) and assert that
the HTTP request contains the DPoP header (e.g., "DPoP" or the library's header
constant) and/or the expected proof value; ensure you reference
restoredManager.dPoP and the resumed token request object when adding the
assertion.
auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt (1)

124-126: ⚡ Quick win

Use JSON field removal instead of raw string replacement for legacy payload simulation.

The current replacement is formatting/order-sensitive and can become flaky. Remove dPoPEnabled via a parsed JSON object instead.

Suggested diff
+import com.google.gson.JsonObject
...
-        // Remove the dPoPEnabled field to simulate legacy JSON
-        val legacyJson = json.replace(",\"dPoPEnabled\":false", "")
+        // Remove the dPoPEnabled field to simulate legacy JSON
+        val legacyJsonObject = GsonProvider.gson.fromJson(json, JsonObject::class.java)
+        legacyJsonObject.remove("dPoPEnabled")
+        val legacyJson = legacyJsonObject.toString()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt`
around lines 124 - 126, The test currently generates legacyJson by doing a
fragile string replacement on json; instead parse the JSON into a JSON object,
remove the "dPoPEnabled" field programmatically, then serialize back to a string
so ordering/formatting won't break the test; locate the variables json and
legacyJson in OAuthManagerStateTest.kt and replace the raw string replace with
logic that parses json (e.g., using org.json.JSONObject or your project’s JSON
library), calls remove("dPoPEnabled") on the parsed object, and assigns
legacyJson to the serialized result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt`:
- Around line 124-126: The test currently generates legacyJson by doing a
fragile string replacement on json; instead parse the JSON into a JSON object,
remove the "dPoPEnabled" field programmatically, then serialize back to a string
so ordering/formatting won't break the test; locate the variables json and
legacyJson in OAuthManagerStateTest.kt and replace the raw string replace with
logic that parses json (e.g., using org.json.JSONObject or your project’s JSON
library), calls remove("dPoPEnabled") on the parsed object, and assigns
legacyJson to the serialized result.

In `@auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt`:
- Around line 2979-2980: The test currently only checks restoredManager.dPoP is
non-null; update it to assert the resumed token-exchange request actually
includes the DPoP header so the /oauth/token boundary is covered. After
obtaining restoredManager from WebAuthProvider.managerInstance (cast to
OAuthManager), simulate or capture the resumed token request the manager sends
during resume (the same place the original regression occurred) and assert that
the HTTP request contains the DPoP header (e.g., "DPoP" or the library's header
constant) and/or the expected proof value; ensure you reference
restoredManager.dPoP and the resumed token request object when adding the
assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 90c807f2-792c-4cc3-953f-8b8741d2cd1d

📥 Commits

Reviewing files that changed from the base of the PR and between 47f75ae and cc9983e.

📒 Files selected for processing (6)
  • auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.kt
  • auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt
  • auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt
  • auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt
  • auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt
  • auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt

@pmathew92

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt Outdated
callback = callback,
customAuthorizeUrl = state.customAuthorizeUrl
customAuthorizeUrl = state.customAuthorizeUrl,
dPoP = if (state.dPoPEnabled ) DPoP(context) else null

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.

The real fix is the useDPoP(context) call above — this dPoP arg only gets read in startAuthentication(), which doesn't run on resume. Is it set here just to keep state consistent / for the test to assert on? Want to confirm it's intentional.

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.

Yes, the dPoP arg is intentional — for state-consistency on toState(), round-trip + the test assertion.

Comment thread auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt Outdated
@utkrishtsahu

utkrishtsahu commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@pmathew92 The PR notes the PAR restore path (PARCodeManager.fromState) is left unchanged. If a DPoP + PAR Universal Login flow is supported in this SDK, wouldn't it hit the same process-death bug and not be covered by this fix? Could you confirm whether that combination is possible?

@pmathew92

Copy link
Copy Markdown
Contributor Author

@utkrishtsahu PAR flow doesn't support DPoP since in PAR the jwk exchange is done in the client backend. So the flow is not applicable

@pmathew92 pmathew92 closed this Jun 8, 2026
@pmathew92 pmathew92 reopened this Jun 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt (1)

87-109: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the serialized field explicitly in the default-false test.

Line 87 currently validates only round-trip behavior; it can still pass if serializeToJson() stops emitting dPoPEnabled and deserialization falls back to false.

Suggested patch
     val json = state.serializeToJson()
+    Assert.assertTrue(json.contains("\"dPoPEnabled\":false"))

     val deserializedState = OAuthManagerState.deserializeState(json)

     Assert.assertFalse(deserializedState.dPoPEnabled)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt`
around lines 87 - 109, The test currently only round-trips state via
OAuthManagerState.serializeToJson() and deserializeState(), which can hide
missing serialization of dPoPEnabled; update the test to explicitly assert the
serialized JSON contains the dPoPEnabled field set to false (e.g. check the JSON
string includes the "dPoPEnabled":false key/value) before calling
OAuthManagerState.deserializeState(json), referencing serializeToJson() and
OAuthManagerState.deserializeState so the test fails if the serializer stops
emitting dPoPEnabled.
🧹 Nitpick comments (1)
auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt (1)

131-134: ⚡ Quick win

Use structured JSON mutation for the legacy-payload test.

Line 133 relies on exact JSON text formatting/order. Parsing to a JSON object and removing the key makes this test resilient.

Suggested patch
+import com.google.gson.JsonObject
...
-        val legacyJson = json.replace(",\"dPoPEnabled\":false", "")
+        val jsonObject = GsonProvider.gson.fromJson(json, JsonObject::class.java)
+        jsonObject.remove("dPoPEnabled")
+        val legacyJson = jsonObject.toString()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt`
around lines 131 - 134, The test in OAuthManagerStateTest currently mutates the
serialized JSON string using exact text replacement (the json and legacyJson
variables); instead, parse the serialized string returned by serializeToJson()
into a JSON object/node, remove the "dPoPEnabled" key from that object, then
re-serialize to a string to produce legacyJson so the test no longer depends on
property ordering/formatting; locate the serializeToJson() usage in the test and
replace the string replace call with JSON parsing, key removal, and
toString/serialize back to JSON.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt`:
- Around line 87-109: The test currently only round-trips state via
OAuthManagerState.serializeToJson() and deserializeState(), which can hide
missing serialization of dPoPEnabled; update the test to explicitly assert the
serialized JSON contains the dPoPEnabled field set to false (e.g. check the JSON
string includes the "dPoPEnabled":false key/value) before calling
OAuthManagerState.deserializeState(json), referencing serializeToJson() and
OAuthManagerState.deserializeState so the test fails if the serializer stops
emitting dPoPEnabled.

---

Nitpick comments:
In `@auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt`:
- Around line 131-134: The test in OAuthManagerStateTest currently mutates the
serialized JSON string using exact text replacement (the json and legacyJson
variables); instead, parse the serialized string returned by serializeToJson()
into a JSON object/node, remove the "dPoPEnabled" key from that object, then
re-serialize to a string to produce legacyJson so the test no longer depends on
property ordering/formatting; locate the serializeToJson() usage in the test and
replace the string replace call with JSON parsing, key removal, and
toString/serialize back to JSON.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d5f7c3b-06f0-4fe2-af2d-1ce2d7876bba

📥 Commits

Reviewing files that changed from the base of the PR and between cc9983e and 65be2b4.

📒 Files selected for processing (5)
  • .github/workflows/test.yml
  • auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt
  • auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt
  • auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt
  • auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt
  • auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants