CP->v4.85.0: Fix MDM SSO callback 'missing profile' error for Android enrollment - #45252
Merged
JordanMontgomery merged 1 commit intoMay 12, 2026
Conversation
…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. [](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
temporarily deployed
to
Docker Hub
May 12, 2026 16:45 — with
GitHub Actions
Inactive
MagnusHJensen
approved these changes
May 12, 2026
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
JordanMontgomery
marked this pull request as ready for review
May 12, 2026 17:56
There was a problem hiding this comment.
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
merged commit May 12, 2026
c9519ff
into
rc-minor-fleet-v4.85.0
60 of 61 checks passed
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cherry pick of #45046 into v4.85
Closes #45024
Summary
"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."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.goinmdmSSOHandleCallbackAuth().The flow:
/enroll?enroll_secret=xxx→ frontend callsInitiateMDMSSOwith initiator"ota_enroll"(
server/service/frontend.go:248)MDMSSOCallback→ callsmdmSSOHandleCallbackAuthoriginalURL == appleMDMAccountDrivenEnrollmentUrl) → no match for OTAInitiator != "setup_experience"→ true for"ota_enroll"→ enters the blockgetAutomaticEnrollmentProfile()→ returnsnilbecause no Apple MDM is configureddepProf == nil→ returns"missing profile"errorNote that
MDMSSOCallback(the caller) already has a guard at line 931 that correctly skips the Apple MDM verification for/enroll?paths:But
mdmSSOHandleCallbackAuthwas missing the equivalent guard — it unconditionally tried to fetch the Apple DEP profile for any non-setup_experienceinitiator.The fix
Adds an early return for OTA enrollments (where
originalURLstarts 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:fleet.SSOInitiatorOTAEnroll"ota_enroll"/enrollpage (Android, BYOD iPhone/iPad)fleet.SSOInitiatorSetupExperience"setup_experience"fleet.SSOInitiatorAccountDrivenEnroll"account_driven_enroll"Constants are in
server/fleet/(notserver/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 replacementorbit/cmd/orbit/orbit.go— 1 replacementserver/service/testing_client.go— 1 replacementserver/service/integration_mdm_test.go— 1 replacementLocal reproduction
Setup
build/fleet serve --dev --dev_licensedocker compose upentity_id: mdm.test.com, SimpleSAML IdP atlocalhost:9080)enable_end_user_authentication: truedirectly in DB (API blocks this without Apple MDM — matches customer state)Steps
GET https://localhost:8080/enroll?enroll_secret=test_enroll_secret→ 303 redirect to SimpleSAML IdPsso_user, pass:user123#)POST https://localhost:8080/api/v1/fleet/mdm/sso/callbackwith the SAMLResponseBefore fix
After fix
No errors. Successful redirect back to the enrollment page with the enrollment reference.
Integration test
Added
TestOTAEnrollSSOWithoutAppleDEPProfilewhich:/enroll→ SAML IdP login → callback)/enroll?...withenrollment_referenceandinitiator=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
LoginOTAEnrollSSOUsertest helper that drives the complete OTA SSO flow starting fromGET /enrollthrough SAML IdP login to the callback, using a single cookie jar.Test plan
/enroll?path)Initiator == "setup_experience")Summary by CodeRabbit