diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index fc7fc9b09401..b5fd066f2045 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -1249,6 +1249,9 @@ the way that the Fleet server works. ds, logger, ) + + // TODO(pssopoc): determine if we will use a middleware chain here to support a + // Fleet-hosted AuthURL flow } healthCheckers := make(map[string]health.Checker) diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index fe6dac19e9a6..7a1bf073b64e 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -999,6 +999,7 @@ type MDMBootstrapPackageStore interface { type MDMAppleMachineInfo struct { IMEI string `plist:"IMEI,omitempty"` Language string `plist:"LANGUAGE,omitempty"` + MDMCanRequestPSSOConfig bool `plist:"MDM_CAN_REQUEST_PSSO_CONFIG,omitempty"` MDMCanRequestSoftwareUpdate bool `plist:"MDM_CAN_REQUEST_SOFTWARE_UPDATE"` MEID string `plist:"MEID,omitempty"` OSVersion string `plist:"OS_VERSION"` @@ -1026,6 +1027,54 @@ type MDMAppleAccountDrivenUserEnrollDeviceInfo struct { SupplementalBuildVersion string `plist:"SUPPLEMENTAL_BUILD_VERSION,omitempty"` } +// MDMApplePSSORequiredCode is the [code][1] specified by Apple to indicate that the device +// needs to configure PSSO before enrollment and setup can proceed. +// +// [1]: https://developer.apple.com/documentation/devicemanagement/errorcodeplatformssorequired +const MDMApplePSSORequiredCode = "com.apple.psso.required" + +// MDMApplePSSORequiredDetails is the [details][1] specified by Apple for the +// required PSSO. +// +// [1]: https://developer.apple.com/documentation/devicemanagement/errorcodeplatformssorequired/details-data.dictionary +type MDMApplePSSORequiredDetails struct { + AuthURL string `json:"AuthURL"` + Package MDMApplePSSORequiredPackage `json:"Package"` + ProfileURL string `json:"ProfileURL"` +} + +// MDMApplePSSORequiredPackage is the [package][1] specified by Apple for the +// required PSSO. +// +// [1]: https://developer.apple.com/documentation/devicemanagement/errorcodeplatformssorequired/details-data.dictionary/package-data.dictionary +type MDMApplePSSORequiredPackage struct { + ManifestURL string `json:"ManifestURL"` + PinningCerts []string `json:"PinningCerts,omitempty"` + PinningRevocationCheckRequired bool `json:"PinningRevocationCheckRequired,omitempty"` +} + +type MDMApplePSSORequiredConfig struct { + AuthURL string `json:"AuthURL"` + Package string `json:"Package"` + ProfileURL string `json:"ProfileURL"` +} + +// MDMApplePSSORequired is the [error response][1] specified by Apple to indicate that the device +// needs to perform a software update before enrollment and setup can proceed. +// +// [1]: https://developer.apple.com/documentation/devicemanagement/errorcodeplatformssorequired +type MDMApplePSSORequired struct { + Code string `json:"code"` // "com.apple.psso.required" + Details MDMApplePSSORequiredDetails `json:"details"` +} + +// func NewMDMApplePSSORequired(asset MDMApplePSSORequiredConfig) *MDMApplePSSORequired { +// return &MDMApplePSSORequired{ +// Code: MDMApplePSSORequiredCode, +// Details: MDMApplePSSORequiredDetails{AuthURL: asset.AuthURL, Package: asset.Package, ProfileURL: asset.ProfileURL}, +// } +// } + // MDMAppleSoftwareUpdateRequiredCode is the [code][1] specified by Apple to indicate that the device // needs to perform a software update before enrollment and setup can proceed. // diff --git a/server/fleet/service.go b/server/fleet/service.go index b284c163f592..7fab1104ffc1 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1025,6 +1025,16 @@ type Service interface { // CheckMDMAppleEnrollmentWithMinimumOSVersion checks if the minimum OS version is met for a MDM enrollment CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx context.Context, m *MDMAppleMachineInfo) (*MDMAppleSoftwareUpdateRequired, error) + // CheckMDMAppleEnrollmentWithPSSO checks if the PSSO requirement is met for a MDM enrollment + CheckMDMAppleEnrollmentWithPSSO(ctx context.Context, m *MDMAppleMachineInfo) (*MDMApplePSSORequired, error) + + // GetMDMApplePSSOInstaller retrieves the PSSO installer + GetMDMApplePSSOInstaller(ctx context.Context, request interface{}) ([]byte, string, error) + // GetMDMApplePSSOProfile retrieves the PSSO profile + GetMDMApplePSSOProfile(ctx context.Context, request interface{}) (string, error) + // GetMDMApplePSSOManifest retrieves the PSSO manifest + GetMDMApplePSSOManifest(ctx context.Context, request interface{}) (string, error) + // GetOTAProfile gets the OTA (over-the-air) profile for a given team based on the enroll secret provided. GetOTAProfile(ctx context.Context, enrollSecret, idpUUID string) ([]byte, error) diff --git a/server/mock/service/service_mock.go b/server/mock/service/service_mock.go index fd50185d4717..d65b01736552 100644 --- a/server/mock/service/service_mock.go +++ b/server/mock/service/service_mock.go @@ -635,6 +635,14 @@ type TriggerLinuxDiskEncryptionEscrowFunc func(ctx context.Context, host *fleet. type CheckMDMAppleEnrollmentWithMinimumOSVersionFunc func(ctx context.Context, m *fleet.MDMAppleMachineInfo) (*fleet.MDMAppleSoftwareUpdateRequired, error) +type CheckMDMAppleEnrollmentWithPSSOFunc func(ctx context.Context, m *fleet.MDMAppleMachineInfo) (*fleet.MDMApplePSSORequired, error) + +type GetMDMApplePSSOInstallerFunc func(ctx context.Context, request interface{}) ([]byte, string, error) + +type GetMDMApplePSSOProfileFunc func(ctx context.Context, request interface{}) (string, error) + +type GetMDMApplePSSOManifestFunc func(ctx context.Context, request interface{}) (string, error) + type GetOTAProfileFunc func(ctx context.Context, enrollSecret string, idpUUID string) ([]byte, error) type TriggerCronScheduleFunc func(ctx context.Context, name string) error @@ -1759,6 +1767,18 @@ type Service struct { CheckMDMAppleEnrollmentWithMinimumOSVersionFunc CheckMDMAppleEnrollmentWithMinimumOSVersionFunc CheckMDMAppleEnrollmentWithMinimumOSVersionFuncInvoked bool + CheckMDMAppleEnrollmentWithPSSOFunc CheckMDMAppleEnrollmentWithPSSOFunc + CheckMDMAppleEnrollmentWithPSSOFuncInvoked bool + + GetMDMApplePSSOInstallerFunc GetMDMApplePSSOInstallerFunc + GetMDMApplePSSOInstallerFuncInvoked bool + + GetMDMApplePSSOProfileFunc GetMDMApplePSSOProfileFunc + GetMDMApplePSSOProfileFuncInvoked bool + + GetMDMApplePSSOManifestFunc GetMDMApplePSSOManifestFunc + GetMDMApplePSSOManifestFuncInvoked bool + GetOTAProfileFunc GetOTAProfileFunc GetOTAProfileFuncInvoked bool @@ -4219,6 +4239,34 @@ func (s *Service) CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx context.Contex return s.CheckMDMAppleEnrollmentWithMinimumOSVersionFunc(ctx, m) } +func (s *Service) CheckMDMAppleEnrollmentWithPSSO(ctx context.Context, m *fleet.MDMAppleMachineInfo) (*fleet.MDMApplePSSORequired, error) { + s.mu.Lock() + s.CheckMDMAppleEnrollmentWithPSSOFuncInvoked = true + s.mu.Unlock() + return s.CheckMDMAppleEnrollmentWithPSSOFunc(ctx, m) +} + +func (s *Service) GetMDMApplePSSOInstaller(ctx context.Context, request interface{}) ([]byte, string, error) { + s.mu.Lock() + s.GetMDMApplePSSOInstallerFuncInvoked = true + s.mu.Unlock() + return s.GetMDMApplePSSOInstallerFunc(ctx, request) +} + +func (s *Service) GetMDMApplePSSOProfile(ctx context.Context, request interface{}) (string, error) { + s.mu.Lock() + s.GetMDMApplePSSOProfileFuncInvoked = true + s.mu.Unlock() + return s.GetMDMApplePSSOProfileFunc(ctx, request) +} + +func (s *Service) GetMDMApplePSSOManifest(ctx context.Context, request interface{}) (string, error) { + s.mu.Lock() + s.GetMDMApplePSSOManifestFuncInvoked = true + s.mu.Unlock() + return s.GetMDMApplePSSOManifestFunc(ctx, request) +} + func (s *Service) GetOTAProfile(ctx context.Context, enrollSecret string, idpUUID string) ([]byte, error) { s.mu.Lock() s.GetOTAProfileFuncInvoked = true diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index f8d677c29cfe..82804cd7130d 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -5,6 +5,7 @@ import ( "context" "crypto/md5" // nolint:gosec // used for declarative management token "crypto/x509" + _ "embed" "encoding/base64" "encoding/hex" "encoding/json" @@ -1913,6 +1914,18 @@ func (mdmAppleEnrollRequest) DecodeRequest(ctx context.Context, r *http.Request) } } + // // TODO(pssopoc): figure out implementation details for bearer token + // // See https://developer.apple.com/documentation/devicemanagement/implementing-platform-sso-during-device-enrollment#Enroll-the-device + // var bearerToken string + // authHdr := r.Header.Get("Authorization") + // headerParts := strings.Split(authHdr, " ") + // if len(headerParts) == 2 && strings.ToUpper(headerParts[0]) == "BEARER" { + // bearerToken = headerParts[1] + // // TODO(pssopoc): add to the request struct and validate in the endpoint + // } else { + // // TODO(pssopoc): figure out what we want to do if missing/malformed auth header + // } + return &decoded, nil } @@ -1925,6 +1938,7 @@ type mdmAppleEnrollResponse struct { Profile []byte SoftwareUpdateRequired *fleet.MDMAppleSoftwareUpdateRequired + PSSORequired *fleet.MDMApplePSSORequired } func (r mdmAppleEnrollResponse) HijackRender(ctx context.Context, w http.ResponseWriter) { @@ -1937,6 +1951,15 @@ func (r mdmAppleEnrollResponse) HijackRender(ctx context.Context, w http.Respons return } + if r.PSSORequired != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + if err := json.NewEncoder(w).Encode(r.PSSORequired); err != nil { + endpoint_utils.EncodeError(ctx, ctxerr.New(ctx, "failed to encode psso required"), w) + } + return + } + w.Header().Set("Content-Length", strconv.FormatInt(int64(len(r.Profile)), 10)) w.Header().Set("Content-Type", "application/x-apple-aspen-config") w.Header().Set("X-Content-Type-Options", "nosniff") @@ -1968,6 +1991,19 @@ func mdmAppleEnrollEndpoint(ctx context.Context, request interface{}, svc fleet. } } + psso, err := svc.CheckMDMAppleEnrollmentWithPSSO(ctx, req.MachineInfo) + if err != nil { + // // TODO: do we instead want to just log the error and continue to next? + // level.Error(logger).Log("msg", "checking pss enrollment for mdm", "err", err) + // next.ServeHTTP(w, r) + return mdmAppleEnrollResponse{Err: err}, nil + } + if psso != nil { + return mdmAppleEnrollResponse{ + PSSORequired: psso, + }, nil + } + legacyRef, err := svc.ReconcileMDMAppleEnrollRef(ctx, req.EnrollmentReference, req.MachineInfo) if err != nil { return mdmAppleEnrollResponse{Err: err}, nil diff --git a/server/service/apple_mdm_psso.go b/server/service/apple_mdm_psso.go new file mode 100644 index 000000000000..0b736257fd53 --- /dev/null +++ b/server/service/apple_mdm_psso.go @@ -0,0 +1,304 @@ +package service + +import ( + "context" + _ "embed" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/contexts/logging" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/go-kit/log/level" +) + +func (svc *Service) CheckMDMAppleEnrollmentWithPSSO(ctx context.Context, m *fleet.MDMAppleMachineInfo) (*fleet.MDMApplePSSORequired, error) { + // // TODO(pssopoc): confirm how we want to handle authz here + // skipauth: The enroll profile endpoint is unauthenticated. + svc.authz.SkipAuthorization(ctx) + + if m == nil { + // TODO(pssopoc): do we instead want to always fail here if we don't have machine info? + level.Debug(svc.logger).Log("msg", "no machine info, skipping psso check") + return nil, nil + } + + level.Debug(svc.logger).Log("msg", "checking psso", "serial", m.Serial, "current_version", m.OSVersion) + + if !m.MDMCanRequestPSSOConfig { + level.Debug(svc.logger).Log("msg", "mdm cannot request psso config, skipping psso check", "serial", m.Serial) + return nil, nil + } + + appCfg, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "psso: fetching app config") + } + serverURL := strings.TrimSuffix(appCfg.ServerSettings.ServerURL, "/") // TODO(pssopoc): confirm how this should work with general server url prefix setting and/or custom MDM URL + + return &fleet.MDMApplePSSORequired{ + Code: fleet.MDMApplePSSORequiredCode, + Details: fleet.MDMApplePSSORequiredDetails{ + AuthURL: "https://login.microsoft.com/YOUR-TENANT-UUID", // TODO(pssopoc): make configurable and replace with real idp url + ProfileURL: serverURL + "/api/latest/fleet/mdm/apple/psso_installer/profile", + Package: fleet.MDMApplePSSORequiredPackage{ + ManifestURL: serverURL + "/api/latest/fleet/mdm/apple/psso_installer/manifest", + }, + }, + }, nil +} + +type mdmApplePSSOManifestResponse struct { + Manifest string `json:"manifest"` + Err error `json:"error,omitempty"` +} + +func (r mdmApplePSSOManifestResponse) Error() error { return r.Err } + +func (r mdmApplePSSOManifestResponse) HijackRender(ctx context.Context, w http.ResponseWriter) { + w.Header().Set("Content-Length", strconv.FormatInt(int64(len(r.Manifest)), 10)) + w.Header().Set("Content-Type", "application/xml") + + // OK to just log the error here as writing anything on + // `http.ResponseWriter` sets the status code to 200 (and it can't be + // changed.) Clients should rely on matching content-length with the + // header provided. + if n, err := w.Write([]byte(r.Manifest)); err != nil { + logging.WithExtras(ctx, "err", err, "written", n) + } +} + +func mdmApplePSSOManifestEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { + m, err := svc.GetMDMApplePSSOManifest(ctx, request) // TODO(pssopoc): replace with real service method, for PoC just call this to enforce the skipauth + if err != nil { + return mdmApplePSSOManifestResponse{Err: err}, nil + } + + return mdmApplePSSOManifestResponse{Manifest: m}, nil +} + +func (svc *Service) GetMDMApplePSSOManifest(ctx context.Context, request interface{}) (string, error) { + // // TODO(pssopoc): replace with real service method, for PoC just call this to enforce the skipauth + // skipauth: proof of concept endpoint to serve the company portal installer + svc.authz.SkipAuthorization(ctx) + + ac, err := svc.ds.AppConfig(ctx) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "fetching app config") + } + serverURL := strings.TrimSuffix(ac.ServerSettings.ServerURL, "/") // TODO(pssopoc): confirm how this should work with general server url prefix setting and/or custom MDM URL + + level.Debug(svc.logger).Log("msg", "generating psso manifest", "server_url", serverURL) + + return generatePSSOManifest(companyPortalHash, serverURL+"/api/latest/fleet/mdm/apple/psso_installer"), nil +} + +type mdmApplePSSOProfileResponse struct { + Profile string `json:"profile"` + Err error `json:"error,omitempty"` +} + +func (r mdmApplePSSOProfileResponse) Error() error { return r.Err } + +func (r mdmApplePSSOProfileResponse) HijackRender(ctx context.Context, w http.ResponseWriter) { + w.Header().Set("Content-Length", strconv.FormatInt(int64(len(r.Profile)), 10)) + w.Header().Set("Content-Type", "application/x-apple-aspen-config") + + // OK to just log the error here as writing anything on + // `http.ResponseWriter` sets the status code to 200 (and it can't be + // changed.) Clients should rely on matching content-length with the + // header provided. + if n, err := w.Write([]byte(r.Profile)); err != nil { + logging.WithExtras(ctx, "err", err, "written", n) + } +} + +func mdmApplePSSOProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { + p, err := svc.GetMDMApplePSSOProfile(ctx, request) // TODO(pssopoc): replace with real service method, for PoC just call this to enforce the skipauth + if err != nil { + return mdmApplePSSOProfileResponse{Err: err}, nil + } + + return mdmApplePSSOProfileResponse{Profile: p}, nil +} + +func (svc *Service) GetMDMApplePSSOProfile(ctx context.Context, request interface{}) (string, error) { + // // TODO(pssopoc): replace with real service method, for PoC just call this to enforce the skipauth + // skipauth: proof of concept endpoint to serve the company portal installer + svc.authz.SkipAuthorization(ctx) + + level.Debug(svc.logger).Log("msg", "serving psso profile") + + // TODO(pssopoc): replace with a method to generate the profile based on admin-configured values + return pssoProfile, nil +} + +func mdmApplePSSOInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { + installer, name, err := svc.GetMDMApplePSSOInstaller(ctx, request) // TODO(pssopoc): replace with real service method, for PoC just to call this to enforce the skipauth + if err != nil { + return mdmAppleGetInstallerResponse{Err: err}, nil + } + + return mdmAppleGetInstallerResponse{ + head: false, + name: name, + size: int64(len(installer)), + installer: installer, + }, nil +} + +func (svc *Service) GetMDMApplePSSOInstaller(ctx context.Context, request interface{}) ([]byte, string, error) { + // // TODO(pssopoc): replace with real service method, for PoC just call this to enforce the skipauth + // skipauth: proof of concept endpoint to serve the company portal installer + svc.authz.SkipAuthorization(ctx) + return CompanyPortal, "CompanyPortal-Installer.pkg", nil +} + +// // TODO(pssopoc): replace with real service method that allows for uploading the desired PSSO app, for PoC just to call this to enforce the skipauth +// Embed the company portal app for PoC +// +//go:embed testdata/software-installers/CompanyPortal-Installer.pkg +var CompanyPortal []byte + +const companyPortalHash = "2cf89bb6c2af88b0ed44defdcf21a9c023fa6948e4da61e16bec71545a46c329" // TODO(pssopoc): replace with method to obtain hash of the uploaded app + +// TODO(pssopoc): replace with a method to retrieve a pre-computed manifest, stored when the +// installer is uploaded, probably using existing `createManifest` as a starting point +func generatePSSOManifest(hash string, url string) string { + return fmt.Sprintf(` + + + + items + + + assets + + + kind + software-package + sha256-size + 32 + sha256s + + %s + + url + %s + + + + + +`, hash, url) +} + +// TODO(pssopoc): replace this with a method to generate the profile based on admin-configured values +const pssoProfile = ` + + + + PayloadContent + + + ExtensionIdentifier + com.microsoft.CompanyPortalMac.ssoextension + PayloadDisplayName + Extensible Single Sign-On + PayloadIdentifier + com.apple.extensiblesso.4D68D4CF-1250-4FF4-AFFB-1176DB539C49 + PayloadType + com.apple.extensiblesso + PayloadUUID + 4D68D4CF-1250-4FF4-AFFB-1176DB539C49 + PayloadVersion + 1 + PlatformSSO + + AuthenticationMethod + Password + TokenToUserMapping + + AccountName + preferred_username + FullName + name + + UseSharedDeviceKeys + + EnableRegistrationDuringSetup + + EnableCreateFirstUserDuringSetup + + + RegistrationToken + {{DEVICEREGISTRATION}} + ScreenLockedBehavior + DoNotHandle + TeamIdentifier + UBF8T346G9 + Type + Redirect + URLs + + https://login.microsoftonline.com + https://login.microsoft.com + https://sts.windows.net + https://login-us.microsoftonline.com + + + + PayloadDisplayName + PlatformSSO + PayloadIdentifier + com.fleetdm.platformsso.652B07D0-2E08-45CE-9423-1FCAFFAEC390 + PayloadType + Configuration + PayloadUUID + 652B07D0-2E08-45CE-9423-1FCAFFAEC390 + PayloadVersion + 1 + +` + +// // TODO(pssopoc): PoC currently assumes that the AuthURL is hosted by the IdP rather than Fleet, but +// // In the Apple docs example, the request is to `Host: idp.example.com`, but the flow is ambiguous. +// // +// // They say the device creates an ASWebAuthenticationSession using AuthURL and a callback scheme +// // that it sets to apple-remotemanagement-user-login (step 10). This starts an authentication flow +// // with the organization’s identity provider. But then they say the ASWebAuthenticationSession web +// // flow completes when the device management service returns an HTTP 308 permanent redirect +// // response to the device. +// // +// // This is a very rough sketch of how we might try to implement a Fleet-hosted AuthURL similar to +// // OTA or account-driven enrollment where Fleet intermediates so that it can populate the +// // apple-remotemanagement-user-login callback scheme. It would encompass the following steps: +// // that initiates the following steps: +// // https://developer.apple.com/documentation/devicemanagement/implementing-platform-sso-during-device-enrollment#Authenticate-the-user +// // https://developer.apple.com/documentation/devicemanagement/implementing-platform-sso-during-device-enrollment#Process-the-user-authentication-result +// func ServePSSOAuth( +// svc fleet.Service, +// urlPrefix string, +// ds fleet.Datastore, +// logger log.Logger, +// ) http.Handler { +// herr := func(w http.ResponseWriter, err string) { +// logger.Log("err", err) +// http.Error(w, err, http.StatusInternalServerError) +// } + +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// endpoint_utils.WriteBrowserSecurityHeaders(w) + +// // TODO(pssopoc): validate things like required Fleet setup, valid enroll secret, etc + +// // TODO(pssopoc): initiate IdP auth if needed, for PoC we skip this and assume auth is always successful + +// // TODO(pssopoc): if we get here, IdP SSO authentication is either not required, or has +// // been successfully completed (e.g., we have received the IdP access token by some means TBD) +// w.Header().Set("Location", "apple-remotemanagement-user-login://authentication-results?access-token=dXNlci1pZGVudGl0eQ") +// w.WriteHeader(http.StatusPermanentRedirect) +// }) +// } diff --git a/server/service/handler.go b/server/service/handler.go index 80aeff56d96b..e8df46a0dec5 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -943,6 +943,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC neAppleMDM.HEAD(apple_mdm.InstallerPath, mdmAppleHeadInstallerEndpoint, mdmAppleHeadInstallerRequest{}) neAppleMDM.POST("/api/_version_/fleet/ota_enrollment", mdmAppleOTAEndpoint, mdmAppleOTARequest{}) + neAppleMDM.GET("/api/_version_/fleet/mdm/apple/psso_installer", mdmApplePSSOInstallerEndpoint, struct{}{}) + neAppleMDM.GET("/api/_version_/fleet/mdm/apple/psso_installer/manifest", mdmApplePSSOManifestEndpoint, struct{}{}) + neAppleMDM.GET("/api/_version_/fleet/mdm/apple/psso_installer/profile", mdmApplePSSOProfileEndpoint, struct{}{}) + // Deprecated: GET /mdm/bootstrap is now deprecated, replaced by the // GET /bootstrap endpoint. neAppleMDM.GET("/api/_version_/fleet/mdm/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{}) @@ -1320,7 +1324,8 @@ func WithMDMEnrollmentMiddleware(svc fleet.Service, logger kitlog.Logger, next h return } - // if x-apple-aspen-deviceinfo custom header is present, we need to check for minimum os version + // if x-apple-aspen-deviceinfo custom header is present, we need to check for minimum os + // version and PSSO enrollment requirements before proceeding di := r.Header.Get("x-apple-aspen-deviceinfo") if di != "" { parsed, err := apple_mdm.ParseDeviceinfo(di, false) // FIXME: use verify=true when we have better parsing for various Apple certs (https://github.com/fleetdm/fleet/issues/20879) @@ -1352,6 +1357,9 @@ func WithMDMEnrollmentMiddleware(svc fleet.Service, logger kitlog.Logger, next h return } + // TODO(pssopoc): confirm whether Apple intended that the PSSO flow is not supported with + // the `configuration_web_url`, if not we would need to implement support for that here. + // TODO: Do non-Apple devices ever use this route? If so, we probably need to change the // approach below so we don't endlessly redirect non-Apple clients to the same URL.