Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/30871-default-byod-fleet
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added support for defining the default fleet BYO Apple devices enroll into.
25 changes: 25 additions & 0 deletions cmd/fleet/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -2486,3 +2486,28 @@ func newManagedLocalAccountRotationSchedule(

return s, nil
}

func newCleanupExpiredADUEChallengesSchedule(
ctx context.Context,
instanceID string,
ds fleet.Datastore,
logger *slog.Logger,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronCleanupExpiredADUEChallenges)
defaultInterval = 24 * time.Hour
)
logger = logger.With("cron", name)
s := schedule.New(
ctx, name, instanceID, defaultInterval, ds, ds,
schedule.WithLogger(logger),
schedule.WithJob("cleanup_expired_adue_challenges", func(ctx context.Context) error {
if err := ds.CleanupExpiredADUEEnrollmentChallenges(ctx); err != nil {
return ctxerr.Wrap(ctx, err, "cleaning up expired ADUE challenges")
}
return nil
}),
)

return s, nil
}
6 changes: 6 additions & 0 deletions cmd/fleet/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,12 @@ func runServeCmd(cmd *cobra.Command, configManager configpkg.Manager, debug, dev
}); err != nil {
initFatal(err, "failed to register managed local account rotation schedule")
}

if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
return newCleanupExpiredADUEChallengesSchedule(ctx, instanceID, ds, logger)
}); err != nil {
initFatal(err, "failed to register cleanup expired ADUE challenges schedule")
}
}

if license.IsPremium() && config.Activity.EnableAuditLog {
Expand Down
82 changes: 82 additions & 0 deletions ee/server/service/apple_mdm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package service

import (
"context"
"fmt"

"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
mdmcrypto "github.com/fleetdm/fleet/v4/server/mdm/crypto"
)

func (svc *Service) GetMDMAccountDrivenEnrollmentSSOURL(ctx context.Context, enrollmentToken string) (string, error) {
// skipauth: The enroll profile endpoint is unauthenticated.
svc.authz.SkipAuthorization(ctx)

appConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return "", ctxerr.Wrap(ctx, err)
}
url := appConfig.MDMUrl() + "/mdm/apple/account_driven_enroll/sso"

if enrollmentToken != "" {
url = fmt.Sprintf("%s/%s", url, enrollmentToken)
}

return url, nil
}

func (svc *Service) GetMDMAppleAccountEnrollmentProfile(ctx context.Context, enrollRef string) (profile []byte, err error) {
// skipauth: This enrollment endpoint is authenticated only by the enrollment reference.
svc.authz.SkipAuthorization(ctx)

enrollChallenge, err := svc.ds.ConsumeADUEEnrollmentChallenge(ctx, enrollRef)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "consuming account driven enrollment challenge")
}
if enrollChallenge == nil {
return nil, &fleet.BadRequestError{Message: "account driven enrollment challenge not found"}
}
Comment thread
MagnusHJensen marked this conversation as resolved.

idpAccount, err := svc.ds.GetMDMIdPAccountByUUID(ctx, enrollChallenge.IdPAccountUUID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting MDM IdP account by UUID")
}

appConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return nil, ctxerr.Wrap(ctx, err)
}

topic, err := apple_mdm.MDMPushCertTopic(ctx, svc.ds)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "extracting topic from APNs cert")
}

assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{
fleet.MDMAssetSCEPChallenge,
}, nil)
if err != nil {
return nil, fmt.Errorf("loading SCEP challenge from the database: %w", err)
}
enrollURL := appConfig.MDMUrl()

enrollmentProf, err := apple_mdm.GenerateAccountDrivenEnrollmentProfileMobileconfig(
appConfig.OrgInfo.OrgName,
enrollURL,
string(assets[fleet.MDMAssetSCEPChallenge].Value),
topic,
idpAccount.Email,
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "generating enrollment profile")
}

signed, err := mdmcrypto.Sign(ctx, enrollmentProf, svc.ds)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "signing profile")
}

return signed, nil
}
47 changes: 37 additions & 10 deletions ee/server/service/mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,11 +841,9 @@ func (svc *Service) DeleteMDMAppleSetupAssistant(ctx context.Context, teamID *ui
return nil
}

const appleMDMAccountDrivenEnrollmentUrl = "/api/mdm/apple/account_driven_enroll"

func (svc *Service) InitiateMDMSSO(ctx context.Context, initiator, customOriginalURL string, hostUUID string) (sessionID string, sessionDurationSeconds int, idpURL string, err error) {
// skipauth: User context does not yet exist. Unauthenticated users may
// initiate SSO.
// initiate MDM SSO.
svc.authz.SkipAuthorization(ctx)

logging.WithLevel(logging.WithNoUser(ctx), slog.LevelInfo)
Expand Down Expand Up @@ -885,13 +883,24 @@ func (svc *Service) InitiateMDMSSO(ctx context.Context, initiator, customOrigina
}

originalURL := "/"
switch initiator {
case fleet.SSOInitiatorAccountDrivenEnroll:
switch {
case strings.HasPrefix(initiator, fleet.SSOInitiatorAccountDrivenEnroll):
var token string

if uniqueToken, ok := strings.CutPrefix(initiator, fleet.SSOInitiatorAccountDrivenEnroll+":"); ok {
token = uniqueToken
}
// originalURL is unused in the Setup Experience initiated MDM flow
// however because we need slightly different behavior for account driven
// enrollment we use it to signal proper behavior on the callback.
originalURL = appleMDMAccountDrivenEnrollmentUrl
case fleet.SSOInitiatorOTAEnroll:
originalURL = apple_mdm.AccountDrivenEnrollPath // nolint:staticcheck // This is kept for backwards compatibility

if token != "" {
// We need this check for backwards compatibility.
tokenURL := apple_mdm.AccountDrivenEnrollTokenPath
originalURL = strings.Replace(tokenURL, "{token}", token, 1)
}
case initiator == fleet.SSOInitiatorOTAEnroll:
// for ota_enroll, we support the custom original URL argument, as the
// enroll secret used to enroll varies. Other initiators do not support
// a custom original URL (and should receive an empty string).
Expand All @@ -916,7 +925,7 @@ func (svc *Service) InitiateMDMSSO(ctx context.Context, initiator, customOrigina

func (svc *Service) MDMSSOCallback(ctx context.Context, sessionID string, samlResponse []byte) (redirectURL, byodCookieValue string) {
// skipauth: User context does not yet exist. Unauthenticated users may
// hit the SSO callback.
// hit the MDM SSO callback.
svc.authz.SkipAuthorization(ctx)

logging.WithLevel(logging.WithNoUser(ctx), slog.LevelInfo)
Expand Down Expand Up @@ -952,10 +961,28 @@ func (svc *Service) MDMSSOCallback(ctx context.Context, sessionID string, samlRe
q.Add("initiator", ssoRequestData.Initiator)

switch {
case originalURL == appleMDMAccountDrivenEnrollmentUrl:
case strings.HasPrefix(ssoRequestData.Initiator, fleet.SSOInitiatorAccountDrivenEnroll):
var abmTokenID *uint

if uniqueToken, ok := strings.CutPrefix(ssoRequestData.Initiator, fleet.SSOInitiatorAccountDrivenEnroll+":"); ok && uniqueToken != "" {
// Extract the unique token to retrieve the ABM token row id.
token, err := svc.ds.GetABMTokenByUniqueToken(ctx, uniqueToken)
if err != nil {
logging.WithErr(ctx, ctxerr.Wrap(ctx, err, "get ABM token by unique token for account driven enrollment"))
return apple_mdm.FleetUISSOCallbackPath + "?error=true", ""
}
abmTokenID = &token.ID
}

challenge, err := svc.ds.InsertADUEEnrollmentChallenge(ctx, abmTokenID, enrollmentRef, fleet.ADUEEnrollmentChallengeExpiration)
if err != nil {
logging.WithErr(ctx, ctxerr.Wrap(ctx, err, "insert ADUE enrollment challenge for account driven enrollment"))
return apple_mdm.FleetUISSOCallbackPath + "?error=true", ""
}

// For account driven enrollment we have to use this special protocol URL scheme to pass the
// access token back to Apple which it will then use to request the enrollment profile.
return fmt.Sprintf("apple-remotemanagement-user-login://authentication-results?access-token=%s", enrollmentRef), ""
return fmt.Sprintf("apple-remotemanagement-user-login://authentication-results?access-token=%s", challenge), ""

case strings.HasPrefix(originalURL, "/enroll?"):
// redirect to the original URL with a cookie that identifies this device
Expand Down
19 changes: 14 additions & 5 deletions frontend/pages/MDMAppleSSOPage/MDMAppleSSOPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,28 @@ import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
import { IMdmSSOResponse } from "interfaces/mdm";
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
import { Params } from "react-router/lib/Router";

const baseClass = "mdm-apple-sso-page";

const DEPSSOLoginPage = ({
location: { pathname, query },
}: WithRouterProps<object, IMDMSSOParams>) => {
params,
}: WithRouterProps<Params, IMDMSSOParams>) => {
const [clickedLogin, setClickedLogin] = useState(false);
localStorage.setItem("deviceinfo", query.deviceinfo || "");
if (!query.initiator) {
query.initiator =
pathname === "/mdm/apple/account_driven_enroll/sso"
? "account_driven_enroll"
: "mdm_sso";
if (pathname.startsWith("/mdm/apple/account_driven_enroll/sso")) {
// While I acknowledge startsWith for route matching is a bit brittle
// I couldn't find a better way, since the pathname is the actual resolved value and not the placeholder route.
if (params.token) {
query.initiator = `account_driven_enroll:${params.token}`;
} else {
query.initiator = "account_driven_enroll";
}
} else {
query.initiator = "mdm_sso";
}
}
const { error } = useQuery<IMdmSSOResponse, AxiosError>(
["dep_sso"],
Expand Down
4 changes: 4 additions & 0 deletions frontend/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ const routes = (
path="mdm/apple/account_driven_enroll/sso"
component={MDMAppleSSOPage}
/>
<Route
path="mdm/apple/account_driven_enroll/sso/:token"
component={MDMAppleSSOPage}
/>
</Route>
</Route>
<Route component={AuthenticatedRoutes as RouteComponent}>
Expand Down
7 changes: 5 additions & 2 deletions pkg/mdm/mdmtest/apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,11 @@ func (c *TestAppleMDMClient) enrollDevice(awaitingConfiguration bool) error {
return fmt.Errorf("get enrollment profile from OTA URL: %w", err)
}
case c.fetchEnrollmentProfileFromMDMBYOD:
if err := c.fetchEnrollmentProfileFromMDMBYODURL(); err != nil {
return fmt.Errorf("get enrollment profile from MDM BYOD URL: %w", err)
if awaitingConfiguration {
// awaitingConfiguration=true only comes from new enrollments, and for re-enrollments we don't want to refetch the profile.
if err := c.fetchEnrollmentProfileFromMDMBYODURL(); err != nil {
return fmt.Errorf("get enrollment profile from MDM BYOD URL: %w", err)
}
}
default:
if c.EnrollInfo.SCEPURL == "" || c.EnrollInfo.MDMURL == "" || c.EnrollInfo.SCEPChallenge == "" {
Expand Down
Loading
Loading