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.