Skip to content

CP->v4.85.0: Fix MDM SSO callback 'missing profile' error for Android enrollment - #45252

Merged
JordanMontgomery merged 1 commit into
rc-minor-fleet-v4.85.0from
fix-45024-android-sso-missing-profile-4.85.0
May 12, 2026
Merged

CP->v4.85.0: Fix MDM SSO callback 'missing profile' error for Android enrollment#45252
JordanMontgomery merged 1 commit into
rc-minor-fleet-v4.85.0from
fix-45024-android-sso-missing-profile-4.85.0

Conversation

@JordanMontgomery

Copy link
Copy Markdown
Member

Cherry pick of #45046 into v4.85

Closes #45024

Summary

  • Fixed the MDM SSO callback handler returning a "missing profile: missing profile" error when an Android device enrolls via SSO (OTA enrollment) on a Fleet instance that does not have Apple MDM configured.
  • Refactored all MDM SSO initiator magic strings ("ota_enroll", "setup_experience", "account_driven_enroll") into named constants (fleet.SSOInitiatorOTAEnroll, etc.) to prevent typos and missed cases — which is the class of bug that caused this issue.

Code walkthrough

The bug

The bug is in ee/server/service/mdm.go in
mdmSSOHandleCallbackAuth().

The flow:

  1. Android enrollment hits /enroll?enroll_secret=xxx → frontend calls InitiateMDMSSO with initiator "ota_enroll"
    (server/service/frontend.go:248)
  2. User authenticates at the SAML IdP
  3. The SSO callback arrives at MDMSSOCallback → calls mdmSSOHandleCallbackAuth
  4. After successful SAML auth, the function checks early-exit conditions:
  • Line 1133: account-driven enrollment (originalURL == appleMDMAccountDrivenEnrollmentUrl) → no match for OTA
  • Line 1139: Initiator != "setup_experience"true for "ota_enroll" → enters the block
  1. Line 1140: calls getAutomaticEnrollmentProfile() → returns nil because no Apple MDM is configured
  2. Line 1144–1146: depProf == nilreturns "missing profile" error

Note that MDMSSOCallback (the caller) already has a guard at line 931 that correctly skips the Apple MDM verification for /enroll? paths:

if !strings.HasPrefix(originalURL, "/enroll?") && ssoRequestData.Initiator != "setup_experience" {
    if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { ... }
}

But mdmSSOHandleCallbackAuth was missing the equivalent guard — it unconditionally tried to fetch the Apple DEP profile for any non-setup_experience initiator.

The fix

Adds an early return for OTA enrollments (where originalURL starts with /enroll?), matching the existing pattern for account-driven enrollments right above it. OTA enrollments don't use the Apple DEP profile token.

The refactor

Replaced all raw initiator string literals across the backend with named constants defined in server/fleet/app.go:

Constant Value Used by
fleet.SSOInitiatorOTAEnroll "ota_enroll" /enroll page (Android, BYOD iPhone/iPad)
fleet.SSOInitiatorSetupExperience "setup_experience" Orbit agent (macOS Setup Assistant)
fleet.SSOInitiatorAccountDrivenEnroll "account_driven_enroll" Apple account-driven MDM enrollment

Constants are in server/fleet/ (not server/sso/) so orbit can import them without pulling in Redis dependencies.

Files changed:

  • ee/server/service/mdm.go — 6 string replacements (switch cases + comparisons)
  • server/service/frontend.go — 1 replacement
  • orbit/cmd/orbit/orbit.go — 1 replacement
  • server/service/testing_client.go — 1 replacement
  • server/service/integration_mdm_test.go — 1 replacement

Local reproduction

Setup

  1. Started dev server: build/fleet serve --dev --dev_license
  2. Infrastructure: MySQL, Redis, SimpleSAML IdP via docker compose up
  3. Created admin user and enroll secret
  4. Configured MDM SSO (entity_id: mdm.test.com, SimpleSAML IdP at localhost:9080)
  5. Set enable_end_user_authentication: true directly in DB (API blocks this without Apple MDM — matches customer state)
  6. Did NOT configure Apple MDM — only SSO + EUA, simulating Android-only instance

Steps

  1. GET https://localhost:8080/enroll?enroll_secret=test_enroll_secret → 303 redirect to SimpleSAML IdP
  2. Completed SAML login programmatically (user: sso_user, pass: user123#)
  3. POST https://localhost:8080/api/v1/fleet/mdm/sso/callback with the SAMLResponse

Before fix

=== CALLBACK RESULT ===
Status: HTTP/2 303
Location: /mdm/sso/callback?error=true

=== SERVER LOGS ===
ts=2026-05-08T16:53:49Z level=error component=http method=POST
  uri=/api/v1/fleet/mdm/sso/callback took=12.148708ms
  err="missing profile: missing profile"

After fix

=== CALLBACK RESULT ===
Status: HTTP/2 303
Location: /enroll?enroll_secret=test_enroll_secret&enrollment_reference=7c67326c-...&initiator=ota_enroll&profile_token=

=== SERVER LOGS ===
ts=2026-05-08T17:27:54Z level=info component=http method=POST
  uri=/api/v1/fleet/mdm/sso/callback took=15.973ms

No errors. Successful redirect back to the enrollment page with the enrollment reference.

Integration test

Added TestOTAEnrollSSOWithoutAppleDEPProfile which:

  1. Configures SSO and creates a team with IdP enabled
  2. Deletes all Apple DEP enrollment profiles to simulate an Android-only instance
  3. Runs the full OTA enrollment SSO flow (GET /enroll → SAML IdP login → callback)
  4. Verifies the callback redirects to /enroll?... with enrollment_reference and initiator=ota_enroll (not ?error=true)

Confirmed the test fails without the fix (err="missing profile: missing profile") and passes with the fix.

Also added a LoginOTAEnrollSSOUser test helper that drives the complete OTA SSO flow starting from GET /enroll through SAML IdP login to the callback, using a single cookie jar.

Test plan

  • Verify Android SSO enrollment works on an instance with only Android MDM configured (no Apple MDM)
  • Verify Apple DEP enrollment with SSO still works (the DEP profile path is unchanged)
  • Verify Apple OTA enrollment with SSO still works (also uses /enroll? path)
  • Verify account-driven enrollment with SSO still works (has its own early return)
  • Verify setup experience SSO still works (uses Initiator == "setup_experience")

Summary by CodeRabbit

  • Bug Fixes
  • Resolved a regression where OTA enrollment via SSO could return a "missing profile" error on Android when Apple MDM is not configured; OTA SSO now redirects correctly to the enrollment flow.

Review Change Stack

…45046)

Closes #45024

## Summary

- Fixed the MDM SSO callback handler returning a `"missing profile:
missing profile"` error when an Android device enrolls via SSO (OTA
enrollment) on a Fleet instance that does **not** have Apple MDM
configured.
- Refactored all MDM SSO initiator magic strings (`"ota_enroll"`,
`"setup_experience"`, `"account_driven_enroll"`) into named constants
(`fleet.SSOInitiatorOTAEnroll`, etc.) to prevent typos and missed cases
— which is the class of bug that caused this issue.

## Code walkthrough

### The bug

The bug is in `ee/server/service/mdm.go` in
`mdmSSOHandleCallbackAuth()`.

**The flow:**
1. Android enrollment hits `/enroll?enroll_secret=xxx` → frontend calls
`InitiateMDMSSO` with initiator `"ota_enroll"`
(`server/service/frontend.go:248`)
2. User authenticates at the SAML IdP
3. The SSO callback arrives at `MDMSSOCallback` → calls
`mdmSSOHandleCallbackAuth`
4. After successful SAML auth, the function checks early-exit
conditions:
- Line 1133: account-driven enrollment (`originalURL ==
appleMDMAccountDrivenEnrollmentUrl`) → **no match** for OTA
- Line 1139: `Initiator != "setup_experience"` → **true** for
`"ota_enroll"` → enters the block
5. Line 1140: calls `getAutomaticEnrollmentProfile()` → returns `nil`
because **no Apple MDM is configured**
6. Line 1144–1146: `depProf == nil` → **returns `"missing profile"`
error**

Note that `MDMSSOCallback` (the caller) already has a guard at line 931
that correctly skips the Apple MDM verification for `/enroll?` paths:
```go
if !strings.HasPrefix(originalURL, "/enroll?") && ssoRequestData.Initiator != "setup_experience" {
    if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { ... }
}
```

But `mdmSSOHandleCallbackAuth` was missing the equivalent guard — it
unconditionally tried to fetch the Apple DEP profile for any
non-`setup_experience` initiator.

### The fix

Adds an early return for OTA enrollments (where `originalURL` starts
with `/enroll?`), matching the existing pattern for account-driven
enrollments right above it. OTA enrollments don't use the Apple DEP
profile token.

### The refactor

Replaced all raw initiator string literals across the backend with named
constants defined in `server/fleet/app.go`:

| Constant | Value | Used by |
|---|---|---|
| `fleet.SSOInitiatorOTAEnroll` | `"ota_enroll"` | `/enroll` page
(Android, BYOD iPhone/iPad) |
| `fleet.SSOInitiatorSetupExperience` | `"setup_experience"` | Orbit
agent (macOS Setup Assistant) |
| `fleet.SSOInitiatorAccountDrivenEnroll` | `"account_driven_enroll"` |
Apple account-driven MDM enrollment |

Constants are in `server/fleet/` (not `server/sso/`) so orbit can import
them without pulling in Redis dependencies.

**Files changed:**
- `ee/server/service/mdm.go` — 6 string replacements (switch cases +
comparisons)
- `server/service/frontend.go` — 1 replacement
- `orbit/cmd/orbit/orbit.go` — 1 replacement
- `server/service/testing_client.go` — 1 replacement
- `server/service/integration_mdm_test.go` — 1 replacement

## Local reproduction

### Setup
1. Started dev server: `build/fleet serve --dev --dev_license`
2. Infrastructure: MySQL, Redis, SimpleSAML IdP via `docker compose up`
3. Created admin user and enroll secret
4. Configured MDM SSO (`entity_id: mdm.test.com`, SimpleSAML IdP at
`localhost:9080`)
5. Set `enable_end_user_authentication: true` directly in DB (API blocks
this without Apple MDM — matches customer state)
6. **Did NOT configure Apple MDM** — only SSO + EUA, simulating
Android-only instance

### Steps
1. `GET https://localhost:8080/enroll?enroll_secret=test_enroll_secret`
→ 303 redirect to SimpleSAML IdP
2. Completed SAML login programmatically (user: `sso_user`, pass:
`user123#`)
3. `POST https://localhost:8080/api/v1/fleet/mdm/sso/callback` with the
SAMLResponse

### Before fix
```
=== CALLBACK RESULT ===
Status: HTTP/2 303
Location: /mdm/sso/callback?error=true

=== SERVER LOGS ===
ts=2026-05-08T16:53:49Z level=error component=http method=POST
  uri=/api/v1/fleet/mdm/sso/callback took=12.148708ms
  err="missing profile: missing profile"
```

### After fix
```
=== CALLBACK RESULT ===
Status: HTTP/2 303
Location: /enroll?enroll_secret=test_enroll_secret&enrollment_reference=7c67326c-...&initiator=ota_enroll&profile_token=

=== SERVER LOGS ===
ts=2026-05-08T17:27:54Z level=info component=http method=POST
  uri=/api/v1/fleet/mdm/sso/callback took=15.973ms
```

No errors. Successful redirect back to the enrollment page with the
enrollment reference.

## Integration test

Added `TestOTAEnrollSSOWithoutAppleDEPProfile` which:
1. Configures SSO and creates a team with IdP enabled
2. **Deletes all Apple DEP enrollment profiles** to simulate an
Android-only instance
3. Runs the full OTA enrollment SSO flow (GET `/enroll` → SAML IdP login
→ callback)
4. Verifies the callback redirects to `/enroll?...` with
`enrollment_reference` and `initiator=ota_enroll` (not `?error=true`)

Confirmed the test **fails without the fix** (`err="missing profile:
missing profile"`) and **passes with the fix**.

Also added a `LoginOTAEnrollSSOUser` test helper that drives the
complete OTA SSO flow starting from `GET /enroll` through SAML IdP login
to the callback, using a single cookie jar.

## Test plan

- [ ] Verify Android SSO enrollment works on an instance with **only**
Android MDM configured (no Apple MDM)
- [ ] Verify Apple DEP enrollment with SSO still works (the DEP profile
path is unchanged)
- [ ] Verify Apple OTA enrollment with SSO still works (also uses
`/enroll?` path)
- [ ] Verify account-driven enrollment with SSO still works (has its own
early return)
- [ ] Verify setup experience SSO still works (uses `Initiator ==
"setup_experience"`)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Resolved a regression where OTA enrollment via SSO could return a
"missing profile" error on Android when Apple MDM is not configured; OTA
SSO now redirects correctly to the enrollment flow.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45046)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Magnus Jensen <magnus@fleetdm.com>
@JordanMontgomery JordanMontgomery changed the title Fix MDM SSO callback 'missing profile' error for Android enrollment CP->v4.85.0: Fix MDM SSO callback 'missing profile' error for Android enrollment May 12, 2026
@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.22034% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.60%. Comparing base (9de8af6) to head (222d7c9).
⚠️ Report is 37 commits behind head on rc-minor-fleet-v4.85.0.

Files with missing lines Patch % Lines
server/service/testing_client.go 95.91% 1 Missing and 1 partial ⚠️
ee/server/service/mdm.go 87.50% 0 Missing and 1 partial ⚠️
orbit/cmd/orbit/orbit.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@                    Coverage Diff                     @@
##           rc-minor-fleet-v4.85.0   #45252      +/-   ##
==========================================================
+ Coverage                   66.72%   68.60%   +1.88%     
==========================================================
  Files                        2626     1584    -1042     
  Lines                      211198   183150   -28048     
  Branches                     9428        0    -9428     
==========================================================
- Hits                       140924   125654   -15270     
+ Misses                      57485    44761   -12724     
+ Partials                    12789    12735      -54     
Flag Coverage Δ
android ?
backend 68.60% <93.22%> (+0.10%) ⬆️
backend-activity ?
fleetd-chrome ?
frontend ?

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.

@JordanMontgomery
JordanMontgomery marked this pull request as ready for review May 12, 2026 17:56
@JordanMontgomery
JordanMontgomery requested a review from a team as a code owner May 12, 2026 17:56

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@JordanMontgomery
JordanMontgomery merged commit c9519ff into rc-minor-fleet-v4.85.0 May 12, 2026
60 of 61 checks passed
@JordanMontgomery
JordanMontgomery deleted the fix-45024-android-sso-missing-profile-4.85.0 branch May 12, 2026 17:57
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.

3 participants