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/47343-idp-cookie
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Clear SSO authentication cookie after successful authentication.
7 changes: 7 additions & 0 deletions frontend/templates/enroll-ota.html
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ <h1 class="error-header">
const ANDROID_MDM_ENABLED = "{{.AndroidMDMEnabled}}" === "true";
const MAC_MDM_ENABLED = "{{.MacMDMEnabled}}" == "true";
const ERROR_MESSAGE = "{{.ErrorMessage}}";
const IDP_UUID = "{{.IdpUUID}}";
const isFullyManaged = new URLSearchParams(window.location.search).has(
"fully_managed",
true
Expand Down Expand Up @@ -681,6 +682,12 @@ <h1 class="error-header">
url = url.concat("&fully_managed=true");
}

if (IDP_UUID) {
url = url.concat(
`&idp_uuid=${encodeURIComponent(IDP_UUID)}`
);
}

const response = await fetch(url, {
method: "GET",
headers: {
Expand Down
20 changes: 20 additions & 0 deletions server/mdm/android/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"google.golang.org/api/androidmanagement/v1"
)

const byodIdpCookieName = "__Host-FLEETBYODIDP"

type Service interface {
EnterpriseSignup(ctx context.Context) (*SignupDetails, error)
EnterpriseSignupCallback(ctx context.Context, signupToken string, enterpriseToken string) error
Expand Down Expand Up @@ -100,3 +102,21 @@ type EnrollmentTokenResponse struct {
*EnrollmentToken
DefaultResponse
}

// SetCookies clears the BYOD IdP cookie after an enrollment token is
// successfully created so that subsequent enrollments require a fresh SSO
// authentication instead of reusing a stale identity.
func (r EnrollmentTokenResponse) SetCookies(_ context.Context, w http.ResponseWriter) {
if r.Err != nil {
return
}
http.SetCookie(w, &http.Cookie{
Name: byodIdpCookieName,
Value: "",
Path: "/",
MaxAge: -1,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}
12 changes: 10 additions & 2 deletions server/mdm/android/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,14 +491,22 @@ func (enrollmentTokenRequest) DecodeRequest(ctx context.Context, r *http.Request
}
}

byodIdpCookie, err := r.Cookie(mdm.BYODIdpCookieName)

fullyManaged := false
fullyManagedParam := r.URL.Query().Get("fully_managed")
if fullyManagedParam == "true" || fullyManagedParam == "1" {
fullyManaged = true
}

if idpUUID := r.URL.Query().Get("idp_uuid"); idpUUID != "" {
return &enrollmentTokenRequest{
EnrollSecret: enrollSecret,
IdpUUID: idpUUID,
FullyManaged: fullyManaged,
}, nil
}
Comment on lines +500 to +506

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Do not treat raw idp_uuid query input as authenticated identity proof.

This early return bypasses cookie/session validation. A caller with enroll_secret and any valid IdP UUID can mint enrollment tokens without fresh SSO and bind the enrollment to that identity. idp_uuid should be accepted only when bound to server-trusted state (e.g., validated session/cookie match or signed one-time server token).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/mdm/android/service/service.go` around lines 500 - 506, The idp_uuid
query parameter is being accepted directly without server-side validation,
allowing any caller with an enroll_secret to bind an enrollment to any IdP UUID
without fresh authentication. Remove or significantly restrict the code block
that extracts idp_uuid from the query parameter and directly populates it in the
enrollmentTokenRequest struct. Instead, only accept idp_uuid when it is
cryptographically bound to server-trusted state, such as validation against an
active session cookie or a signed one-time token generated by the server during
a prior authenticated operation. Ensure that the enrollmentTokenRequest
construction validates the identity proof before incorporating any IdP
identifier.


byodIdpCookie, err := r.Cookie(mdm.BYODIdpCookieName)

if err == http.ErrNoCookie {
// We do not fail here if no cookie is found, we validate later down the line if it's required
return &enrollmentTokenRequest{
Expand Down
26 changes: 22 additions & 4 deletions server/service/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ func ServeEndUserEnrollOTA(

errorMsg := r.URL.Query().Get("error")
if errorMsg != "" {
if err := renderEnrollPage(w, appCfg, urlPrefix, "", errorMsg, nonce); err != nil {
if err := renderEnrollPage(w, appCfg, urlPrefix, "", errorMsg, nonce, ""); err != nil {
herr(ctx, w, err.Error())
}
return
}

enrollSecret := r.URL.Query().Get("enroll_secret")
if enrollSecret == "" {
if err := renderEnrollPage(w, appCfg, urlPrefix, "", "This URL is invalid. : Enroll secret is invalid. Please contact your IT admin.", nonce); err != nil {
if err := renderEnrollPage(w, appCfg, urlPrefix, "", "This URL is invalid. : Enroll secret is invalid. Please contact your IT admin.", nonce, ""); err != nil {
herr(ctx, w, err.Error())
}
return
Expand Down Expand Up @@ -171,7 +171,23 @@ func ServeEndUserEnrollOTA(
// if we get here, IdP SSO authentication is either not required, or has
// been successfully completed (we have a cookie with the IdP account
// reference).
if err := renderEnrollPage(w, appCfg, urlPrefix, enrollSecret, "", nonce); err != nil {

// Clear the BYOD IdP cookie now that we are about to render the enrollment page.
var idpUUID string
if authRequired {
idpUUID = r.URL.Query().Get("enrollment_reference")
http.SetCookie(w, &http.Cookie{
Comment on lines +175 to +179
Name: shared_mdm.BYODIdpCookieName,
Value: "",
Path: "/",
MaxAge: -1,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}

if err := renderEnrollPage(w, appCfg, urlPrefix, enrollSecret, "", nonce, idpUUID); err != nil {
herr(ctx, w, err.Error())
return
}
Expand All @@ -195,7 +211,7 @@ func generateEnrollOTAURL(fleetURL string, enrollSecret string) (string, error)
return enrollURL.String(), nil
}

func renderEnrollPage(w io.Writer, appCfg *fleet.AppConfig, urlPrefix, enrollSecret, errorMessage, nonce string) error {
func renderEnrollPage(w io.Writer, appCfg *fleet.AppConfig, urlPrefix, enrollSecret, errorMessage, nonce, idpUUID string) error {
fs := newBinaryFileSystem("/frontend")
file, err := fs.Open("templates/enroll-ota.html")
if err != nil {
Expand Down Expand Up @@ -224,6 +240,7 @@ func renderEnrollPage(w io.Writer, appCfg *fleet.AppConfig, urlPrefix, enrollSec
MacMDMEnabled bool
AndroidFeatureEnabled bool
CSPNonce string
IdpUUID string
}{
URLPrefix: urlPrefix,
EnrollURL: enrollURL,
Expand All @@ -232,6 +249,7 @@ func renderEnrollPage(w io.Writer, appCfg *fleet.AppConfig, urlPrefix, enrollSec
MacMDMEnabled: appCfg.MDM.EnabledAndConfigured,
AndroidFeatureEnabled: true,
CSPNonce: nonce,
IdpUUID: idpUUID,
}); err != nil {
return fmt.Errorf("execute react template: %w", err)
}
Expand Down
Loading