mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Improve the error handling for MDM SSO during DEP enrollment (#12966)
For #12692
This commit is contained in:
parent
4940a5e186
commit
442e03b276
8 changed files with 79 additions and 37 deletions
1
changes/12692-macos-helpful-message
Normal file
1
changes/12692-macos-helpful-message
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Improve error handling and messaging of SSO login during AEP(DEP) enrollments.
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -653,21 +654,41 @@ func (svc *Service) InitiateMDMAppleSSO(ctx context.Context) (string, error) {
|
|||
return idpURL, nil
|
||||
}
|
||||
|
||||
func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) (string, error) {
|
||||
func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) string {
|
||||
// skipauth: User context does not yet exist. Unauthenticated users may
|
||||
// hit the SSO callback.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
logging.WithLevel(logging.WithNoUser(ctx), level.Info)
|
||||
|
||||
profileToken, enrollmentRef, eulaToken, err := svc.mdmSSOHandleCallbackAuth(ctx, auth)
|
||||
|
||||
if err != nil {
|
||||
logging.WithErr(ctx, err)
|
||||
return apple_mdm.FleetUISSOCallbackPath + "?error=true"
|
||||
}
|
||||
|
||||
q := url.Values{
|
||||
"profile_token": {profileToken},
|
||||
"enrollment_reference": {enrollmentRef},
|
||||
}
|
||||
|
||||
if eulaToken != "" {
|
||||
q.Add("eula_token", eulaToken)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s?%s", apple_mdm.FleetUISSOCallbackPath, q.Encode())
|
||||
}
|
||||
|
||||
func (svc *Service) mdmSSOHandleCallbackAuth(ctx context.Context, auth fleet.Auth) (string, string, string, error) {
|
||||
appConfig, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "get config for sso")
|
||||
return "", "", "", ctxerr.Wrap(ctx, err, "get config for sso")
|
||||
}
|
||||
|
||||
_, metadata, err := svc.ssoSessionStore.Fullfill(auth.RequestID())
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "validate request in session")
|
||||
return "", "", "", ctxerr.Wrap(ctx, err, "validate request in session")
|
||||
}
|
||||
|
||||
var ssoSettings fleet.SSOSettings
|
||||
|
|
@ -684,7 +705,7 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.
|
|||
)
|
||||
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "validating sso response")
|
||||
return "", "", "", ctxerr.Wrap(ctx, err, "validating sso response")
|
||||
}
|
||||
|
||||
// Store information for automatic account population/creation
|
||||
|
|
@ -704,35 +725,32 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.
|
|||
Fullname: auth.UserDisplayName(),
|
||||
}
|
||||
if err := svc.ds.InsertMDMIdPAccount(ctx, &idpAcc); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "saving account data from IdP")
|
||||
return "", "", "", ctxerr.Wrap(ctx, err, "saving account data from IdP")
|
||||
}
|
||||
|
||||
eula, err := svc.ds.MDMAppleGetEULAMetadata(ctx)
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return "", ctxerr.Wrap(ctx, err, "getting EULA metadata")
|
||||
return "", "", "", ctxerr.Wrap(ctx, err, "getting EULA metadata")
|
||||
}
|
||||
|
||||
var eulaToken string
|
||||
if eula != nil {
|
||||
eulaToken = eula.Token
|
||||
}
|
||||
|
||||
// get the automatic profile to access the authentication token.
|
||||
depProf, err := svc.getAutomaticEnrollmentProfile(ctx)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "listing profiles")
|
||||
return "", "", "", ctxerr.Wrap(ctx, err, "listing profiles")
|
||||
}
|
||||
|
||||
if depProf == nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "missing profile")
|
||||
return "", "", "", ctxerr.Wrap(ctx, err, "missing profile")
|
||||
}
|
||||
|
||||
q := url.Values{
|
||||
"profile_token": {depProf.Token},
|
||||
// using the idp token as a reference just because that's the
|
||||
// only thing we're referencing later on during enrollment.
|
||||
"enrollment_reference": {idpAcc.UUID},
|
||||
}
|
||||
if eula != nil {
|
||||
q.Add("eula_token", eula.Token)
|
||||
}
|
||||
|
||||
return appConfig.ServerSettings.ServerURL + "/mdm/sso/callback?" + q.Encode(), nil
|
||||
// using the idp token as a reference just because that's the
|
||||
// only thing we're referencing later on during enrollment.
|
||||
return depProf.Token, idpAcc.UUID, eulaToken, nil
|
||||
}
|
||||
|
||||
func (svc *Service) mdmAppleSyncDEPProfiles(ctx context.Context) error {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ const SSOError = ({ className }: ISSOErrorProps) => {
|
|||
|
||||
return (
|
||||
<DataError className={classNames}>
|
||||
<p>Please contact your IT admin at +1-(415)-651-2575.</p>
|
||||
<p>
|
||||
Select <strong>Cancel</strong> and try again. If this keeps happening,
|
||||
please contact IT support.
|
||||
</p>
|
||||
</DataError>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,16 +18,18 @@ interface IEnrollmentGateProps {
|
|||
profileToken?: string;
|
||||
eulaToken?: string;
|
||||
enrollmentReference?: string;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
const EnrollmentGate = ({
|
||||
profileToken,
|
||||
eulaToken,
|
||||
enrollmentReference,
|
||||
error,
|
||||
}: IEnrollmentGateProps) => {
|
||||
const [showEULA, setShowEULA] = useState(Boolean(eulaToken));
|
||||
|
||||
if (!profileToken) {
|
||||
if (!profileToken || error) {
|
||||
return <SSOError />;
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +67,7 @@ interface IMDMSSOCallbackQuery {
|
|||
eula_token?: string;
|
||||
profile_token?: string;
|
||||
enrollment_reference?: string;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
const MDMAppleSSOCallbackPage = (
|
||||
|
|
@ -74,6 +77,7 @@ const MDMAppleSSOCallbackPage = (
|
|||
eula_token,
|
||||
profile_token,
|
||||
enrollment_reference,
|
||||
error,
|
||||
} = props.location.query;
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
|
@ -81,6 +85,7 @@ const MDMAppleSSOCallbackPage = (
|
|||
eulaToken={eula_token}
|
||||
profileToken={profile_token}
|
||||
enrollmentReference={enrollment_reference}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -170,9 +170,10 @@ type Service interface {
|
|||
// are valid
|
||||
InitSSOCallback(ctx context.Context, auth Auth) (string, error)
|
||||
|
||||
// InitSSOCallback handles the IDP response and ensures the credentials
|
||||
// are valid, then responds with an enrollment profile.
|
||||
InitiateMDMAppleSSOCallback(ctx context.Context, auth Auth) (string, error)
|
||||
// InitiateMDMAppleSSOCallback handles the IDP response and ensures the
|
||||
// credentials are valid, then responds with an URL to the Fleet UI to
|
||||
// handle next steps based on the query parameters provided.
|
||||
InitiateMDMAppleSSOCallback(ctx context.Context, auth Auth) string
|
||||
|
||||
// GetSSOUser handles retrieval of an user that is trying to authenticate
|
||||
// via SSO
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ const (
|
|||
// InstallerPath is the HTTP path that serves installers to Apple devices.
|
||||
InstallerPath = "/api/mdm/apple/installer"
|
||||
|
||||
// FleetUISSOCallbackPath is the front-end route used to
|
||||
// redirect after the SSO flow is completed.
|
||||
FleetUISSOCallbackPath = "/mdm/sso/callback"
|
||||
|
||||
// FleetPayloadIdentifier is the value for the "<key>PayloadIdentifier</key>"
|
||||
// used by Fleet MDM on the enrollment profile.
|
||||
FleetPayloadIdentifier = "com.fleetdm.fleet.mdm.apple"
|
||||
|
|
|
|||
|
|
@ -2143,6 +2143,10 @@ func (svc *Service) InitiateMDMAppleSSO(ctx context.Context) (string, error) {
|
|||
|
||||
type callbackMDMAppleSSORequest struct{}
|
||||
|
||||
// TODO: these errors will result in JSON being returned, but we should
|
||||
// redirect to the UI and let the UI display an error instead. The errors are
|
||||
// rare enough (malformed data coming from the SSO provider) so they shouldn't
|
||||
// affect many users.
|
||||
func (callbackMDMAppleSSORequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
|
|
@ -2162,8 +2166,6 @@ func (callbackMDMAppleSSORequest) DecodeRequest(ctx context.Context, r *http.Req
|
|||
}
|
||||
|
||||
type callbackMDMAppleSSOResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
|
||||
redirectURL string
|
||||
}
|
||||
|
||||
|
|
@ -2172,25 +2174,23 @@ func (r callbackMDMAppleSSOResponse) hijackRender(ctx context.Context, w http.Re
|
|||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (r callbackMDMAppleSSOResponse) error() error { return r.Err }
|
||||
// Error will always be nil because errors are handled by sending a query
|
||||
// parameter in the URL response, this way the UI is able to display an erorr
|
||||
// message.
|
||||
func (r callbackMDMAppleSSOResponse) error() error { return nil }
|
||||
|
||||
func callbackMDMAppleSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
auth := request.(fleet.Auth)
|
||||
|
||||
// validate that the SSO response is valid
|
||||
redirectURL, err := svc.InitiateMDMAppleSSOCallback(ctx, auth)
|
||||
if err != nil {
|
||||
return callbackMDMAppleSSOResponse{Err: err}, nil
|
||||
}
|
||||
redirectURL := svc.InitiateMDMAppleSSOCallback(ctx, auth)
|
||||
return callbackMDMAppleSSOResponse{redirectURL: redirectURL}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) (string, error) {
|
||||
func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) string {
|
||||
// skipauth: No authorization check needed due to implementation
|
||||
// returning only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return "", fleet.ErrMissingLicense
|
||||
return apple_mdm.FleetUISSOCallbackPath + "?error=true"
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -5167,6 +5167,7 @@ func (s *integrationMDMTestSuite) TestSSO() {
|
|||
require.False(t, q.Has("eula_token"))
|
||||
require.True(t, q.Has("profile_token"))
|
||||
require.True(t, q.Has("enrollment_reference"))
|
||||
require.False(t, q.Has("error"))
|
||||
// the url retrieves a valid profile
|
||||
s.downloadAndVerifyEnrollmentProfile(
|
||||
fmt.Sprintf(
|
||||
|
|
@ -5191,6 +5192,7 @@ func (s *integrationMDMTestSuite) TestSSO() {
|
|||
require.True(t, q.Has("eula_token"))
|
||||
require.True(t, q.Has("profile_token"))
|
||||
require.True(t, q.Has("enrollment_reference"))
|
||||
require.False(t, q.Has("error"))
|
||||
// the url retrieves a valid profile
|
||||
prof := s.downloadAndVerifyEnrollmentProfile(
|
||||
fmt.Sprintf(
|
||||
|
|
@ -5260,9 +5262,17 @@ func (s *integrationMDMTestSuite) TestSSO() {
|
|||
require.Contains(t, lastSubmittedProfile.URL, "https://example.com/api/mdm/apple/enroll?token=")
|
||||
require.Equal(t, "https://example.com/mdm/sso", lastSubmittedProfile.ConfigurationWebURL)
|
||||
|
||||
// hitting the callback with an invalid session id results in a 4xx
|
||||
// hitting the callback with an invalid session id redirects the user to the UI
|
||||
rawSSOResp := base64.StdEncoding.EncodeToString([]byte(`<samlp:Response ID="_7822b394622740aa92878ca6c7d1a28c53e80ec5ef"></samlp:Response>`))
|
||||
s.DoRawNoAuth("POST", "/api/v1/fleet/mdm/sso/callback?SAMLResponse="+url.QueryEscape(rawSSOResp), nil, http.StatusUnauthorized)
|
||||
res = s.DoRawNoAuth("POST", "/api/v1/fleet/mdm/sso/callback?SAMLResponse="+url.QueryEscape(rawSSOResp), nil, http.StatusTemporaryRedirect)
|
||||
require.NotEmpty(t, res.Header.Get("Location"))
|
||||
u, err = url.Parse(res.Header.Get("Location"))
|
||||
require.NoError(t, err)
|
||||
q = u.Query()
|
||||
require.False(t, q.Has("eula_token"))
|
||||
require.False(t, q.Has("profile_token"))
|
||||
require.False(t, q.Has("enrollment_reference"))
|
||||
require.True(t, q.Has("error"))
|
||||
}
|
||||
|
||||
type scepPayload struct {
|
||||
|
|
|
|||
Loading…
Reference in a new issue