Skip to content

Android app re-enroll on 401#38690

Merged
mostlikelee merged 7 commits intomainfrom
38035-android-reenroll
Jan 27, 2026
Merged

Android app re-enroll on 401#38690
mostlikelee merged 7 commits intomainfrom
38035-android-reenroll

Conversation

@mostlikelee
Copy link
Copy Markdown
Contributor

@mostlikelee mostlikelee commented Jan 23, 2026

Related issue: Resolves #38035

When an Android host is deleted from Fleet and comes back, the app's stored node key becomes invalid. Previously, API calls would fail with 401 and never recover because the app assumed the enroll secret was
one-time.

This PR adds automatic re-enrollment on 401:

  • Detects 401 responses from Fleet server
  • Clears the invalid node key
  • Re-enrolls to get a new node key
  • Retries the original request

This change was AI assisted, so please be critical.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

  • Added/updated automated tests

  • QA'd all new/changed functionality manually

Summary by CodeRabbit

  • Bug Fixes
    • The Android certificate app now automatically re-enrolls if the host was deleted in Fleet, improving recovery from authorization errors and ensuring seamless operation when devices are re-registered.

✏️ Tip: You can customize this high-level summary in your review settings.

@mostlikelee mostlikelee changed the title 38035 android reenroll Android app re-enroll on 401 Jan 23, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 45.83333% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.14%. Comparing base (7deade8) to head (4a01ae8).
⚠️ Report is 88 commits behind head on main.

Files with missing lines Patch % Lines
...d/app/src/main/java/com/fleetdm/agent/ApiClient.kt 40.00% 23 Missing and 1 partial ⚠️
...src/main/java/com/fleetdm/agent/KeystoreManager.kt 75.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #38690      +/-   ##
==========================================
+ Coverage   66.08%   66.14%   +0.06%     
==========================================
  Files        2414     2414              
  Lines      192657   192678      +21     
  Branches     8424     8427       +3     
==========================================
+ Hits       127309   127454     +145     
+ Misses      53788    53648     -140     
- Partials    11560    11576      +16     
Flag Coverage Δ
android 53.11% <45.83%> (+10.47%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mostlikelee
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 23, 2026

✅ Actions performed

Full review triggered.

@mostlikelee
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 23, 2026

✅ Actions performed

Review triggered.

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.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 23, 2026

Walkthrough

This PR implements automatic re-enrollment for the Android certificate app when the Fleet server returns HTTP 401 Unauthorized responses. When a deleted and re-added host's API key becomes invalid, the app now clears the key, re-enrolls to obtain a new one, and retries the failed request. Changes include new re-enrollment wrapper logic in ApiClient, test mode support in KeystoreManager, comprehensive integration tests, and dependency additions.

Changes

Cohort / File(s) Summary
Build and Dependency Configuration
android/app/build.gradle.kts, android/gradle/libs.versions.toml
Added OKHttp MockWebServer test dependency (v4.12.0) and registered version reference in gradle catalog. Enables HTTP mock testing infrastructure.
API Client - Re-enrollment Logic
android/app/src/main/java/com/fleetdm/agent/ApiClient.kt
Introduced UnauthorizedException class and withReenrollOnUnauthorized() wrapper function that handles 401 responses by clearing the stored API key, re-enrolling once, and retrying. Applied wrapper to getOrbitConfig(), getCertificateTemplate(), and updateCertificateStatus(). Added clearApiKey() utility method.
Keystore Manager - Test Support
android/app/src/main/java/com/fleetdm/agent/KeystoreManager.kt
Added enableTestMode() and disableTestMode() public functions to support in-memory test key usage. getOrCreateKey() now short-circuits to in-memory key when test mode is active.
Integration Tests
android/app/src/test/java/com/fleetdm/agent/ApiClientReenrollTest.kt
Added comprehensive integration test suite with 242 lines covering: initial enrollment, 401 handling with automatic re-enrollment and retry, certificate fetch flows, non-401 error handling, re-enrollment failure propagation, and request/header validation across retry cycles using Robolectric and MockWebServer.
Changelog Entry
changes/38035-android-reenroll
Added descriptive changelog note documenting automatic re-enrollment behavior when host is deleted and re-added.

Sequence Diagram

sequenceDiagram
    actor App as Android App
    participant ApiClient
    participant DataStore as Data Store<br/>(API Key)
    participant FleetServer as Fleet Server

    rect rgba(200, 150, 255, 0.5)
    Note over App,FleetServer: Initial Request with Valid Key
    App->>ApiClient: getOrbitConfig()
    ApiClient->>DataStore: Retrieve API Key
    DataStore-->>ApiClient: Return Valid Key (e.g., node_key_v1)
    ApiClient->>FleetServer: GET /api/fleet/orbit/config<br/>Header: Authorization: node_key_v1
    FleetServer-->>ApiClient: 200 OK + Config
    ApiClient-->>App: Success(OrbitConfig)
    end

    rect rgba(255, 150, 150, 0.5)
    Note over App,FleetServer: Host Deleted & Re-added<br/>Key Becomes Invalid
    App->>ApiClient: getOrbitConfig()
    ApiClient->>DataStore: Retrieve API Key
    DataStore-->>ApiClient: Return Expired Key (e.g., node_key_v1)
    ApiClient->>FleetServer: GET /api/fleet/orbit/config<br/>Header: Authorization: node_key_v1
    FleetServer-->>ApiClient: 401 Unauthorized
    ApiClient->>ApiClient: withReenrollOnUnauthorized detects 401
    end

    rect rgba(150, 200, 150, 0.5)
    Note over App,FleetServer: Clear Key & Re-enroll
    ApiClient->>DataStore: clearApiKey()
    DataStore-->>ApiClient: Key cleared
    ApiClient->>FleetServer: POST /api/fleet/orbit/enroll<br/>Body: enroll_secret
    FleetServer-->>ApiClient: 200 OK + New Node Key (node_key_v2)
    ApiClient->>DataStore: Store New Key (node_key_v2)
    DataStore-->>ApiClient: Stored
    end

    rect rgba(150, 200, 150, 0.5)
    Note over App,FleetServer: Retry Original Request
    ApiClient->>FleetServer: GET /api/fleet/orbit/config<br/>Header: Authorization: node_key_v2
    FleetServer-->>ApiClient: 200 OK + Config
    ApiClient-->>App: Success(OrbitConfig)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • getvictor
  • dantecatalfamo
  • georgekarrv
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% 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
Title check ✅ Passed The title clearly summarizes the main change: implementing automatic re-enrollment when the Android app receives a 401 response.
Linked Issues check ✅ Passed Code changes fully address issue #38035: 401 detection (ApiClient.kt), node key clearing, re-enrollment logic, and retry mechanism are all implemented with comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing 401 re-enrollment: exception handling, key management, API client modifications, test infrastructure, and changelog entry.
Description check ✅ Passed The PR description includes a clear problem statement, solution overview, and completed checklist items addressing the main requirements.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 38035-android-reenroll

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@android/app/src/main/java/com/fleetdm/agent/ApiClient.kt`:
- Around line 88-92: The code currently auto-retries enrollment using the stored
enroll secret on 401 responses which conflicts with the one-time secret policy;
update the 401 handling to stop automatic re-enrollment and require manual
re-enroll (or confirm backend supports reuse). Locate the API client 401
handling logic and remove or disable the path that reads the stored secret and
retries enrollment; instead call clearApiKey() to remove the secret and return
an unrecoverable/auth-failure state or surface a user-visible error prompting
manual re-enrollment. Ensure any helper/utility that previously invoked the
retry path is updated to not call the enroll retry function and to propagate the
auth-failure up for manual intervention.

In `@android/gradle/libs.versions.toml`:
- Line 25: Update the okhttp version declared under the okhttp key by replacing
"4.12.0" with "5.3.2" (or the chosen recent 5.x release) in libs.versions.toml,
then run a full Gradle build to surface breakages and resolve them (adjust
artifact coordinates/imports and API changes where code references okhttp
classes). Search for the okhttp dependency key "okhttp" and all usages of the
old version "4.12.0" to ensure consistency, fix compilation/test failures, and
update any Gradle module dependency declarations that consume the version
constant.
🧹 Nitpick comments (2)
android/app/src/main/java/com/fleetdm/agent/KeystoreManager.kt (1)

19-42: Make test-mode API idempotent and test‑scoped.

Regenerating the key on repeated calls can break decrypts if any encrypted values exist, and public visibility risks accidental production use. Consider making it idempotent and internal.

♻️ Proposed refactor
-    fun enableTestMode() {
-        testMode = true
-        testKey = KeyGenerator.getInstance("AES").apply {
-            init(256)
-        }.generateKey()
-    }
+    internal fun enableTestMode() {
+        if (testMode && testKey != null) return
+        testMode = true
+        testKey = KeyGenerator.getInstance("AES").apply {
+            init(256)
+        }.generateKey()
+    }

-    fun disableTestMode() {
+    internal fun disableTestMode() {
         testMode = false
         testKey = null
     }
android/app/build.gradle.kts (1)

279-279: MockWebServer version is well-aligned but outdated.

The OkHttp and MockWebServer versions are correctly pinned to the same version (4.12.0), which maintains compatibility. However, this version is over 2 years old; the latest release is 5.3.2 (Nov 18, 2025). Consider upgrading to the current version if the project's Android API minimum is 5.0+ (API 21+), as OkHttp 5.x has new features and security improvements. If the project targets a lower API level, the current 4.12.0 is acceptable.

Comment on lines +88 to +92
private suspend fun clearApiKey() {
dataStore.edit { preferences ->
preferences.remove(API_KEY)
}
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Get the ApiClient.kt file to examine the withReenrollOnUnauthorized wrapper at lines 179-210
cd android/app/src/main/java/com/fleetdm/agent
wc -l ApiClient.kt

Repository: fleetdm/fleet

Length of output: 73


🏁 Script executed:

# View the withReenrollOnUnauthorized wrapper and surrounding context
cd android/app/src/main/java/com/fleetdm/agent
sed -n '175,215p' ApiClient.kt

Repository: fleetdm/fleet

Length of output: 1751


🏁 Script executed:

# Also check the clearApiKey usage and how enrollment is triggered
cd android/app/src/main/java/com/fleetdm/agent
grep -n "clearApiKey\|enroll\|enrollSecret" ApiClient.kt | head -30

Repository: fleetdm/fleet

Length of output: 2170


🏁 Script executed:

# Check AgentApplication.kt to understand the enrollment flow and secret handling
cd android/app/src/main/java/com/fleetdm/agent
sed -n '70,90p' AgentApplication.kt

Repository: fleetdm/fleet

Length of output: 1042


Automatic re-enrollment contradicts one-time enroll secret policy.

This code auto-retries enrollment with the stored enroll secret when receiving a 401 (lines 202–210, 238–242, 264–271, 304–310). However, the stored secret is designed as a one-time credential with an intended manual unenroll/re-enroll workflow—automatic retry will fail and leave the device unrecovered. Either align the implementation with the one-time policy (remove auto-retry or require manual re-enroll) or confirm the backend now supports secret reuse and update the enrollment contract accordingly.

🤖 Prompt for AI Agents
In `@android/app/src/main/java/com/fleetdm/agent/ApiClient.kt` around lines 88 -
92, The code currently auto-retries enrollment using the stored enroll secret on
401 responses which conflicts with the one-time secret policy; update the 401
handling to stop automatic re-enrollment and require manual re-enroll (or
confirm backend supports reuse). Locate the API client 401 handling logic and
remove or disable the path that reads the stored secret and retries enrollment;
instead call clearApiKey() to remove the secret and return an
unrecoverable/auth-failure state or surface a user-visible error prompting
manual re-enrollment. Ensure any helper/utility that previously invoked the
retry path is updated to not call the enroll retry function and to propagate the
auth-failure up for manual intervention.

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.

I believe this is ok, we don't expect the enroll secret to change

navigationCompose = "2.9.6"
json = "20231013"
mockk = "1.13.13"
okhttp = "4.12.0"
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

latest OkHttp version release 2025 2026

💡 Result:

As of January 23, 2026, the latest OkHttp release on Maven Central is 5.3.2, released November 18, 2025. [1] (The OkHttp changelog also lists 5.3.2 — 2025-11-18.) [2]

Recent 2025 stable releases (changelog):

  • 5.0.0 — 2025-07-03 [2]
  • 5.1.0 — 2025-07-07 [1]
  • 5.2.0 — 2025-10-07 [2]
  • 5.3.0 — 2025-10-30 [2]
  • 5.3.2 — 2025-11-18 [2]

No newer (2026-dated) OkHttp releases are shown in Maven Central’s directory listing up through 5.3.2. [1]

Sources

  1. Maven Central directory for com.squareup.okhttp3:okhttp (shows latest version and timestamps) [1]
  2. OkHttp changelog (lists version release dates, including 5.3.2) [2]

Consider upgrading OkHttp to 5.3.2 or a recent 5.x release.

Version 4.12.0 is significantly outdated. The latest OkHttp release is 5.3.2 (November 18, 2025), with multiple stable 5.x versions available since July 2025. Evaluate compatibility and breaking changes if upgrading.

Also applies to: 59-59

🤖 Prompt for AI Agents
In `@android/gradle/libs.versions.toml` at line 25, Update the okhttp version
declared under the okhttp key by replacing "4.12.0" with "5.3.2" (or the chosen
recent 5.x release) in libs.versions.toml, then run a full Gradle build to
surface breakages and resolve them (adjust artifact coordinates/imports and API
changes where code references okhttp classes). Search for the okhttp dependency
key "okhttp" and all usages of the old version "4.12.0" to ensure consistency,
fix compilation/test failures, and update any Gradle module dependency
declarations that consume the version constant.

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.

fixed

@mostlikelee mostlikelee marked this pull request as ready for review January 23, 2026 16:35
Copy link
Copy Markdown
Member

@dantecatalfamo dantecatalfamo left a comment

Choose a reason for hiding this comment

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

Cool way of re-executing blocks after a failure!

@mostlikelee mostlikelee merged commit 3f1b8ba into main Jan 27, 2026
16 checks passed
@mostlikelee mostlikelee deleted the 38035-android-reenroll branch January 27, 2026 16:01
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.

Android app stops working when IT admins deletes Android host and it is automatically re-added

2 participants