Added EUA to the Fleet MSI installer (#43295)

**Related issue:** Resolves #41381

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
- Forward end-user authentication context (EUA token) to the Fleet MSI
installer and enrollment flow on Windows MDM to avoid duplicate auth
prompts and link devices to hosts.

* **Tests**
- Added comprehensive unit and integration tests for EUA token creation,
validation, and processing to improve reliability.

* **Documentation**
- Added a note describing support for forwarding end-user authentication
context during Windows MDM enrollment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Konstantin Sykulev
2026-04-13 12:17:23 -05:00
committed by GitHub
parent b4a3e975f5
commit 83a886b0ec
11 changed files with 595 additions and 17 deletions
+2
View File
@@ -32,6 +32,8 @@ type EnrollOrbitRequest struct {
ComputerName string `json:"computer_name"`
// HardwareModel is the device's hardware model.
HardwareModel string `json:"hardware_model"`
// EUAToken is a Fleet-signed JWT containing the user's UPN and Windows MDM device ID.
EUAToken string `json:"eua_token,omitempty"`
}
// SetOrbitNodeKeyer is the interface implemented by orbit request types that
+1 -1
View File
@@ -122,7 +122,7 @@ type Service interface {
//
// - If an entry for the host exists (osquery enrolled first) then it will update the host's orbit node key and team.
// - If an entry for the host doesn't exist (osquery enrolls later) then it will create a new entry in the hosts table.
EnrollOrbit(ctx context.Context, hostInfo OrbitHostInfo, enrollSecret string) (orbitNodeKey string, err error)
EnrollOrbit(ctx context.Context, hostInfo OrbitHostInfo, enrollSecret string, euaToken string) (orbitNodeKey string, err error)
// GetOrbitConfig returns team specific flags and extensions in agent options
// if the team id is not nil for host, otherwise it returns flags from global
// agent options. It also returns any notifications that fleet wants to surface
+97 -2
View File
@@ -45,9 +45,17 @@ type CertManager interface {
// NewSTSAuthToken returns an STS auth token for the given UPN claim.
NewSTSAuthToken(upn string) (string, error)
// NewEUAToken returns a Fleet-signed JWT for the given UPN and Windows MDM
// device ID. Used to pass end-user authentication context to the orbit
// installer so the user is not prompted twice.
NewEUAToken(upn string, deviceID string) (string, error)
// GetSTSAuthTokenUPNClaim validates the given token and returns the UPN claim
GetSTSAuthTokenUPNClaim(token string) (string, error)
// GetEUATokenClaims validates the given EUA token and returns the parsed claims.
GetEUATokenClaims(token string) (*EUATokenClaims, error)
// TODO: implement other methods as needed:
// - verify certificate-device association
// - certificate lifecycle management (e.g., renewal, revocation)
@@ -66,6 +74,19 @@ type STSClaims struct {
jwt.RegisteredClaims
}
// euaJWTClaims is the internal JWT struct for signing/parsing EUA tokens.
type euaJWTClaims struct {
UPN string `json:"upn"`
DeviceID string `json:"device_id"`
jwt.RegisteredClaims
}
// EUATokenClaims is the validated result returned to callers of GetEUATokenClaims.
type EUATokenClaims struct {
UPN string
DeviceID string
}
type AzureData struct {
UPN string
Audience []string
@@ -186,8 +207,8 @@ func (m *manager) NewSTSAuthToken(upn string) (string, error) {
// Create claims with upn field populated
claims := STSClaims{
upn,
jwt.RegisteredClaims{
UPN: upn,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
@@ -205,6 +226,80 @@ func (m *manager) NewSTSAuthToken(upn string) (string, error) {
return signedToken, nil
}
// NewEUAToken returns a Fleet-signed JWT for the given UPN and Windows MDM device ID.
func (m *manager) NewEUAToken(upn string, deviceID string) (string, error) {
if m == nil {
return "", errors.New("windows mdm identity keypair was not configured")
}
if m.identityCert == nil || m.identityPrivateKey == nil {
return "", errors.New("invalid identity certificate or private key")
}
if len(upn) == 0 {
return "", errors.New("invalid upn field")
}
if len(deviceID) == 0 {
return "", errors.New("invalid device_id field")
}
claims := euaJWTClaims{
UPN: upn,
DeviceID: deviceID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Subject: "EUAToken",
},
}
token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims)
signedToken, err := token.SignedString(m.identityPrivateKey)
if err != nil {
return "", fmt.Errorf("failed to sign EUA token: %w", err)
}
return signedToken, nil
}
// GetEUATokenClaims validates the given EUA token and returns the parsed claims.
func (m *manager) GetEUATokenClaims(tokenStr string) (*EUATokenClaims, error) {
if m == nil {
return nil, errors.New("windows mdm identity keypair was not configured")
}
if m.identityCert == nil || m.identityPrivateKey == nil {
return nil, errors.New("invalid identity certificate or private key")
}
if len(tokenStr) == 0 {
return nil, errors.New("invalid EUA token")
}
token, err := jwt.ParseWithClaims(tokenStr, &euaJWTClaims{}, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return m.identityCert.PublicKey, nil
})
if err != nil {
return nil, fmt.Errorf("there was an error parsing the EUA token claims: %w", err)
}
if claims, ok := token.Claims.(*euaJWTClaims); ok && token.Valid {
if len(claims.UPN) == 0 {
return nil, errors.New("issue with UPN token claim")
}
if len(claims.DeviceID) == 0 {
return nil, errors.New("issue with device_id token claim")
}
return &EUATokenClaims{UPN: claims.UPN, DeviceID: claims.DeviceID}, nil
}
return nil, errors.New("issue with EUA token validation")
}
// GetSTSAuthToken validates the given token and returns the UPN claim
func (m *manager) GetSTSAuthTokenUPNClaim(tokenStr string) (string, error) {
if m == nil {
+38
View File
@@ -99,6 +99,44 @@ func TestSTSTokenSigningAndVerification(t *testing.T) {
require.ErrorContains(t, err, "invalid upn field")
}
func TestSTSTokenWithDeviceID(t *testing.T) {
var store CertStore
cm, err := NewCertManager(store, testCert, testKey)
require.NoError(t, err)
upn := "user@example.com"
deviceID := "test-device-id-123"
// Generate token with device ID
token, err := cm.NewEUAToken(upn, deviceID)
require.NoError(t, err)
require.NotEmpty(t, token)
// Validate and extract both claims
claims, err := cm.GetEUATokenClaims(token)
require.NoError(t, err)
require.Equal(t, upn, claims.UPN)
require.Equal(t, deviceID, claims.DeviceID)
// Empty UPN is rejected
_, err = cm.NewEUAToken("", deviceID)
require.ErrorContains(t, err, "invalid upn field")
// Empty device ID is rejected
_, err = cm.NewEUAToken(upn, "")
require.ErrorContains(t, err, "invalid device_id field")
// Token signed by NewSTSAuthToken (no device_id) is rejected — device_id is required
oldToken, err := cm.NewSTSAuthToken(upn)
require.NoError(t, err)
_, err = cm.GetEUATokenClaims(oldToken)
require.ErrorContains(t, err, "issue with device_id token claim")
// Tampered token is rejected
_, err = cm.GetEUATokenClaims(token + "tampered")
require.Error(t, err)
}
func TestCertFingerprintHexStr(t *testing.T) {
cases := []struct {
name string
+3 -3
View File
@@ -50,7 +50,7 @@ type GetTransparencyURLFunc func(ctx context.Context) (string, error)
type AuthenticateOrbitHostFunc func(ctx context.Context, nodeKey string) (host *fleet.Host, debug bool, err error)
type EnrollOrbitFunc func(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string) (orbitNodeKey string, err error)
type EnrollOrbitFunc func(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string, euaToken string) (orbitNodeKey string, err error)
type GetOrbitConfigFunc func(ctx context.Context) (fleet.OrbitConfig, error)
@@ -2354,11 +2354,11 @@ func (s *Service) AuthenticateOrbitHost(ctx context.Context, nodeKey string) (ho
return s.AuthenticateOrbitHostFunc(ctx, nodeKey)
}
func (s *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string) (orbitNodeKey string, err error) {
func (s *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string, euaToken string) (orbitNodeKey string, err error) {
s.mu.Lock()
s.EnrollOrbitFuncInvoked = true
s.mu.Unlock()
return s.EnrollOrbitFunc(ctx, hostInfo, enrollSecret)
return s.EnrollOrbitFunc(ctx, hostInfo, enrollSecret, euaToken)
}
func (s *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, error) {
@@ -13,6 +13,7 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
@@ -508,6 +509,7 @@ func (s *integrationMDMTestSuite) recordWindowsHostStatus(
msgID, err := device.GetCurrentMsgID()
require.NoError(t, err)
euaTokenRe := regexp.MustCompile(`EUA_TOKEN="[^"]*"`)
for _, c := range cmds {
cmdID := c.Cmd.CmdID
status := syncml.CmdStatusOK
@@ -522,6 +524,12 @@ func (s *integrationMDMTestSuite) recordWindowsHostStatus(
})
c.Cmd.CmdID.Value = ""
c.Cmd.CmdRef = nil
for i := range c.Cmd.Items {
if c.Cmd.Items[i].Data != nil {
c.Cmd.Items[i].Data.Content = euaTokenRe.ReplaceAllString(
c.Cmd.Items[i].Data.Content, `EUA_TOKEN="<redacted>"`)
}
}
recordedCmds = append(recordedCmds, c)
}
+9 -2
View File
@@ -9173,8 +9173,9 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
var installJob struct {
Product struct {
ContentURL string `xml:"Download>ContentURLList>ContentURL"`
FileHash string `xml:"Validation>FileHash"`
ContentURL string `xml:"Download>ContentURLList>ContentURL"`
FileHash string `xml:"Validation>FileHash"`
CommandLine string `xml:"Enforcement>CommandLine"`
} `xml:"Product"`
}
err = xml.Unmarshal([]byte(fleetdExecCmd.Cmd.Items[0].Data.Content), &installJob)
@@ -9182,6 +9183,12 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
require.Equal(t, s.mockedDownloadFleetdmMeta.MSIURL, installJob.Product.ContentURL)
require.Equal(t, s.mockedDownloadFleetdmMeta.MSISha256, installJob.Product.FileHash)
// The device enrolled with a valid UPN (azureMail), so the command line
// should include an EUA_TOKEN argument.
require.Contains(t, installJob.Product.CommandLine, `EUA_TOKEN="`)
require.Contains(t, installJob.Product.CommandLine, `FLEET_URL="`)
require.Contains(t, installJob.Product.CommandLine, `FLEET_SECRET="`)
// reply with success for both commands
msgID, err := d.GetCurrentMsgID()
require.NoError(t, err)
+28 -1
View File
@@ -1501,6 +1501,28 @@ func (svc *Service) isFleetdPresentOnDevice(ctx context.Context, deviceID string
return true, nil
}
// generateWindowsEUAToken returns a Fleet-signed EUA token for the given Windows
// MDM device ID if the device enrolled with a valid Azure UPN
func (svc *Service) generateWindowsEUAToken(ctx context.Context, deviceID string) string {
if svc.wstepCertManager == nil {
return ""
}
device, err := svc.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID)
if err != nil {
svc.logger.ErrorContext(ctx, "unable to fetch windows mdm enrollment for EUA token generation", "err", err, "device_id", deviceID)
return ""
}
if device == nil || !microsoft_mdm.IsValidUPN(device.MDMEnrollUserID) {
return ""
}
token, err := svc.wstepCertManager.NewEUAToken(device.MDMEnrollUserID, deviceID)
if err != nil {
svc.logger.ErrorContext(ctx, "unable to generate EUA token for fleetd install", "err", err, "device_id", deviceID)
return ""
}
return token
}
func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID string) error {
secrets, err := svc.ds.GetEnrollSecrets(ctx, nil)
if err != nil {
@@ -1530,6 +1552,11 @@ func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID st
addCommandUUID := uuid.NewString()
execCommandUUID := uuid.NewString()
euaTokenArg := ""
if token := svc.generateWindowsEUAToken(ctx, deviceID); token != "" {
euaTokenArg = ` EUA_TOKEN="` + token + `"`
}
rawAddCmd := []byte(`
<Add>
<CmdID>` + addCommandUUID + `</CmdID>
@@ -1562,7 +1589,7 @@ func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID st
<FileHash>` + fleetdMetadata.MSISha256 + `</FileHash>
</Validation>
<Enforcement>
<CommandLine>/quiet FLEET_URL="` + fleetURL + `" FLEET_SECRET="` + globalEnrollSecret + `" ENABLE_SCRIPTS="True"</CommandLine>
<CommandLine>/quiet FLEET_URL="` + fleetURL + `" FLEET_SECRET="` + globalEnrollSecret + `" ENABLE_SCRIPTS="True"` + euaTokenArg + `</CommandLine>
<TimeOut>10</TimeOut>
<RetryCount>1</RetryCount>
<RetryInterval>5</RetryInterval>
+105 -8
View File
@@ -55,7 +55,7 @@ func enrollOrbitEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
OsqueryIdentifier: req.OsqueryIdentifier,
ComputerName: req.ComputerName,
HardwareModel: req.HardwareModel,
}, req.EnrollSecret)
}, req.EnrollSecret, req.EUAToken)
if err != nil {
return enrollOrbitResponse{fleet.EnrollOrbitResponse{Err: err}}, nil
}
@@ -89,8 +89,66 @@ func (svc *Service) AuthenticateOrbitHost(ctx context.Context, orbitNodeKey stri
return host, svc.debugEnabledForHost(ctx, host.ID), nil
}
// processWindowsEUAToken validates a Fleet-signed EUA token from the Windows MSI
// installer, links the user's IdP account to the host, and returns the UPN and
// device ID for use in post-enrollment steps.
func (svc *Service) processWindowsEUAToken(ctx context.Context, hostUUID string, euaToken string) (upn string, deviceID string, err error) {
if svc.wstepCertManager == nil {
// Windows MDM is not configured on this server so the token cannot be validated.
// Fall back to prompting the user for authentication.
return "", "", fleet.NewOrbitIDPAuthRequiredError()
}
claims, tokenErr := svc.wstepCertManager.GetEUATokenClaims(euaToken)
if tokenErr != nil {
svc.logger.WarnContext(ctx, "EUA token validation failed, falling back to end user auth prompt",
"err", tokenErr, "host_uuid", hostUUID)
return "", "", fleet.NewOrbitIDPAuthRequiredError()
}
upn = claims.UPN
deviceID = claims.DeviceID
_, err = svc.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID)
if err != nil {
if fleet.IsNotFound(err) {
svc.logger.WarnContext(ctx, "EUA token device_id not found in windows mdm enrollments, falling back to end user auth prompt",
"device_id", deviceID, "host_uuid", hostUUID)
return "", "", fleet.NewOrbitIDPAuthRequiredError()
}
return "", "", ctxerr.Wrap(ctx, err, "getting windows mdm enrollment for EUA token")
}
// Fetch or create the mdm_idp_accounts row for this email.
// Fetch first so we do not overwrite existing first/last names
// that may have been populated by SCIM provisioning.
acct, err := svc.ds.GetMDMIdPAccountByEmail(ctx, upn)
if err != nil && !fleet.IsNotFound(err) {
return "", "", ctxerr.Wrap(ctx, err, "getting mdm idp account by email for EUA token")
}
if fleet.IsNotFound(err) {
if err := svc.ds.InsertMDMIdPAccount(ctx, &fleet.MDMIdPAccount{Email: upn, Username: upn}); err != nil {
return "", "", ctxerr.Wrap(ctx, err, "inserting mdm idp account for EUA token")
}
// Re-fetch to get the UUID assigned by the DB.
acct, err = svc.ds.GetMDMIdPAccountByEmail(ctxdb.RequirePrimary(ctx, true), upn)
if err != nil {
return "", "", ctxerr.Wrap(ctx, err, "re-fetching mdm idp account after insert for EUA token")
}
}
if acct == nil {
return "", "", ctxerr.New(ctx, "mdm idp account not found for EUA token")
}
// Link the IdP account to this host UUID in host_mdm_idp_accounts.
if err := svc.ds.AssociateHostMDMIdPAccountDB(ctx, hostUUID, acct.UUID); err != nil {
return "", "", ctxerr.Wrap(ctx, err, "associating host with mdm idp account for EUA token")
}
return upn, deviceID, nil
}
// EnrollOrbit enrolls an Orbit instance to Fleet and returns the orbit node key.
func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string) (string, error) {
func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string, euaToken string) (string, error) {
// this is not a user-authenticated endpoint
svc.authz.SkipAuthorization(ctx)
@@ -162,6 +220,8 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
isEndUserAuthRequired = team.Config.MDM.MacOSSetup.EnableEndUserAuthentication
}
var euaDeviceID, euaUPN string
if isEndUserAuthRequired {
if hostInfo.HardwareUUID == "" {
return "", fleet.OrbitError{Message: "failed to get IdP account: hardware uuid is empty"}
@@ -183,12 +243,22 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
if platform == "linux" || platform == "windows" {
// If the Orbit client doesn't support end user auth, complain loudly and let the host enroll.
mp, ok := capabilities.FromContext(ctx)
//nolint:gocritic // ignore ifElseChain
if !ok {
svc.logger.ErrorContext(ctx, "!!! ERR_ALLOWING_UNAUTHENTICATED: host is not authenticated, but fleet could not determine whether orbit supports end-user authentication. proceeding with enrollment. !!! ", "host_uuid", hostInfo.HardwareUUID)
} else if !mp.Has(fleet.CapabilityEndUserAuth) {
svc.logger.WarnContext(ctx, "!!! ERR_ALLOWING_UNAUTHENTICATED: host is not authenticated, but connected with an orbit version that does not support end user authentication. proceeding with enrollment. !!! ", "host_uuid", hostInfo.HardwareUUID)
} else {
switch {
case !ok:
svc.logger.ErrorContext(ctx, "allowing unauthenticated enrollment: could not determine orbit end-user auth capability", "host_uuid", hostInfo.HardwareUUID)
case !mp.Has(fleet.CapabilityEndUserAuth):
svc.logger.WarnContext(ctx, "allowing unauthenticated enrollment: orbit version does not support end-user authentication", "host_uuid", hostInfo.HardwareUUID)
case platform == "windows" && euaToken != "":
// A Windows host already authenticated during MDM enrollment and the
// EUA token was passed by the MSI installer.
upn, deviceID, err := svc.processWindowsEUAToken(ctx, hostInfo.HardwareUUID, euaToken)
if err != nil {
return "", err
}
euaUPN = upn
euaDeviceID = deviceID
// Continue enrollment — do not return END_USER_AUTH_REQUIRED.
default:
// Otherwise report the unauthenticated host and let Orbit handle it (e.g. by prompting the user to authenticate).
return "", fleet.NewOrbitIDPAuthRequiredError()
}
@@ -221,6 +291,33 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
return "", fleet.OrbitError{Message: "failed to enroll " + err.Error()}
}
if euaDeviceID != "" {
updated, err := svc.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, host.UUID, euaDeviceID)
if err != nil {
svc.logger.ErrorContext(ctx, "failed to link windows mdm enrollment to orbit host via EUA token",
"err", err, "host_uuid", host.UUID, "device_id", euaDeviceID)
}
if updated {
scimUser, err := svc.ds.ScimUserByUserNameOrEmail(ctx, euaUPN, euaUPN)
//nolint:gocritic // ignore ifElseChain
if err != nil && !fleet.IsNotFound(err) && err != sql.ErrNoRows {
svc.logger.ErrorContext(ctx, "failed to find SCIM user for EUA token enrollment",
"err", err, "host_id", host.ID)
} else if err == nil && scimUser != nil {
if err := svc.ds.SetOrUpdateHostSCIMUserMapping(ctx, host.ID, scimUser.ID); err != nil {
svc.logger.ErrorContext(ctx, "failed to set SCIM user mapping for EUA token enrollment",
"err", err, "host_id", host.ID)
}
} else {
if err := svc.ds.DeleteHostSCIMUserMapping(ctx, host.ID); err != nil && !fleet.IsNotFound(err) {
svc.logger.ErrorContext(ctx, "failed to delete SCIM user mapping for EUA token enrollment",
"err", err, "host_id", host.ID)
}
}
}
}
// Associate the newly-enrolled host with a SCIM user if applicable.
// Do this only for linux and windows devices, as macOS devices
// are associated during MDM enrollment.
+303
View File
@@ -0,0 +1,303 @@
package service
import (
"context"
"database/sql"
"log/slog"
"strings"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mock"
mysql_errors "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/stretchr/testify/require"
)
// euaTestingKey replaces "TESTING KEY" with "PRIVATE KEY" to prevent secret
// scanners from flagging test keys embedded in source files.
func euaTestingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
// testWSTEPCert and testWSTEPKey are the same certs used in wstep_test.go.
var (
testWSTEPCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDGzCCAgOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAvMQkwBwYD
VQQGEwAxEDAOBgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1NDRVAg
Q0EwHhcNMjIxMjIyMTM0NDMzWhcNMzIxMjIyMTM0NDMzWjAvMQkw
BwYDVQQGEwAxEDAOBgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1ND
RVAgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDV
u9YVfl7gu0UgUkOJoES/XrN0WZdIjgvS2upKfvP4LSJOq1Mnp3bH
wWOA2NkHem/kjOVeotOk1aEYIzxbic6VlvNOz9huOhbJyoV4TO5v
tp/GFFcJ4IXh+f1Q4vm/NeH/XxEWn9S20B9OkSMOUievYsAu6iSi
oWaa74q1mnfpzM29p3dNM82mCKutYdkW0EusixU/CQxcVhdcxC+R
RyM4jzBFIipa7H20UtqdkZ03/9BoowJb/h/r4X7TN4tKg2vcwpZK
uJo7VcTBNPxhBowzg3JUmzjCnxPbuU/Ow5kPGOLJtbf4766ToNTM
/J63i3UPshKUBqAE8mIZO3qb7s25AgMBAAGjQjBAMA4GA1UdDwEB
/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTxPEY4
WvsLCt+HDQfnEPOKrHu0gTANBgkqhkiG9w0BAQsFAAOCAQEAGNf5
R60vRxIfvSOUyV3X7lUk+fVvi1CKC43DsP5OsQ6g5YVGcVXN40U4
2o7JUeb9K1jvqnzWB/3k+lSCkEb0a5KabjZE5Vpdt9xctmgrfNnQ
PBCfDdyb0Upjm61CJeB2SW9+ibT2L+OtL/nZjjlugL7ir9ramQBh
0IY6oB9Yc3TyZyPjnXwbi0jv5cildzIYaYPvPkPPTjezOUqUDgUH
JtdWRBQeJ/6WxAAm9il0KVXOsRPgAsdiDJTF6FdW4lsY8V/R6y0H
hTN1ZSyqklKAuvEZZznfmJsrNYRII2Fv2zOk0Uv/+E+EKTOHbgcC
PQAARDBzDlWvlMGWcbdrdypdeA==
-----END CERTIFICATE-----
`)
testWSTEPKey = []byte(euaTestingKey(`-----BEGIN RSA TESTING KEY-----
MIIEowIBAAKCAQEA1bvWFX5e4LtFIFJDiaBEv16zdFmXSI4L0trqSn7z+C0iTqtT
J6d2x8FjgNjZB3pv5IzlXqLTpNWhGCM8W4nOlZbzTs/YbjoWycqFeEzub7afxhRX
CeCF4fn9UOL5vzXh/18RFp/UttAfTpEjDlInr2LALuokoqFmmu+KtZp36czNvad3
TTPNpgirrWHZFtBLrIsVPwkMXFYXXMQvkUcjOI8wRSIqWux9tFLanZGdN//QaKMC
W/4f6+F+0zeLSoNr3MKWSriaO1XEwTT8YQaMM4NyVJs4wp8T27lPzsOZDxjiybW3
+O+uk6DUzPyet4t1D7ISlAagBPJiGTt6m+7NuQIDAQABAoIBAE6LXL1BV3SW3Wxn
TtKAx0Lcdm5HjkTnjojKUldWGCoXzAfFBiYIcKov83UiO394Cy6eaJxCkix9JVpN
eJzbI8PtWTSZRRwc1MsLVclD3EvJfSW5y9KhZBILYIAdKVKPZqIGOa1qxyz3hsnE
pHFa16KoU5/qA9SQI7jEVuEuBusv4D/dRlEWvva7QOhnLrBPrSnTSZ5LxCFKRviS
XrEQ9AuRJeXCKx4WzXd4IZPpgldYHMJSSGMr0TeVcURbsfveI2IWvOLag0ofTHhx
tolBT2sKzInItLTwt/irZEp5lV08mMGxHuxoCdzhxjFQP8eGOZzPW65c6/D9hEXd
DzWnjdECgYEA9QtTQosOTtAyU1i4Fm76ltT6nywHy23KAMhBaoKgTMccNtjaOCg/
5FCCRD+qoo7TF4jdliP2NrMIbAIhr4jEfHSMKaD/rae1xqInseDCrGi9gzvm8UxG
84VG30Id8s70ZQWZjR/PFFDeNZjNhlk8COO0XoLaqJSZr+A30aSyeUsCgYEA30ok
3EvO1+/gjZv28J9vApdbiEwtO9xoteghElFzdtuEuzA+wL83w8xvKvdb4Rk5xigE
6mV69dBPj8zSyGp0lFTYLFvry5N4S8L6QPzt2nk+Lc3cDKSA5CkAkQ5Dmt5JwhxF
qIPDNZGXmoldIWJ0p/ZSu98/1yXBMQ9gCje/losCgYBwuk4KLbheT27nYsgFIfbL
zpyg/vty/UXRiE53tjISQALdxHLXJMUHvnW++d8Au12m1QLDIDYTQdddALoIa42g
h2k3eWZFuAJqp4xFS1WjROfx6Gu8k8+MFcLd0CfA3K4XjzTtdDWqbe1bkLjz1jdF
C6OdWutGZF4zR53GJtMn8wKBgCfA95cRGB5x4rTTk797YzQ+5lj51wPVVf8s+NZe
EgSTSKpbCJEgejkt6IzpxT3qU9LnxRhGQQIKuF+Nw+lSqrbN9D7RjsWL19sFN7Di
VyaSd3OINyk5EImOkz9AHuEvukoI5o3+B38+EJO+6QnMkaBlxo0UTjVrz12As0Se
cEnJAoGBAOUXjez9oUSzLzqG/WJFrIfHyjDA1vBS1j39XuhDuJGqMdNLlCE8Yr7h
d3gpZeuV3ZC33QAuwAXfRBNnKIDtDGpcrozM1NndcBVDs9GYvobaTiUaODGjsH44
oHwpyQbv9Qs+3bjPOQ7DkwekT+w1cptEKudBCC3WQKui1P0NNL0R
-----END RSA TESTING KEY-----
`))
)
// newTestServiceWithWSTEP returns a Service with a real wstepCertManager built
// from the inline test cert/key, backed by a mock datastore.
func newTestServiceWithWSTEP(t *testing.T, ds *mock.Store) *Service {
t.Helper()
certManager, err := microsoft_mdm.NewCertManager(nil, testWSTEPCert, testWSTEPKey)
require.NoError(t, err)
return &Service{
ds: ds,
wstepCertManager: certManager,
logger: slog.New(slog.DiscardHandler),
}
}
func TestProcessWindowsEUAToken(t *testing.T) {
const (
testUPN = "user@example.com"
testDeviceID = "device-abc-123"
testHostUUID = "host-uuid-xyz"
testAcctUUID = "acct-uuid-456"
)
// Helper to generate a valid token for test cases.
makeToken := func(t *testing.T, svc *Service, upn, deviceID string) string {
t.Helper()
tok, err := svc.wstepCertManager.NewEUAToken(upn, deviceID)
require.NoError(t, err)
return tok
}
t.Run("valid token, new enrollment, account not yet in db", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
token := makeToken(t, svc, testUPN, testDeviceID)
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
require.Equal(t, testDeviceID, mdmDeviceID)
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, HostUUID: ""}, nil
}
// First call returns not-found; second call (after insert) returns the account.
getByEmailCalls := 0
ds.GetMDMIdPAccountByEmailFunc = func(ctx context.Context, email string) (*fleet.MDMIdPAccount, error) {
require.Equal(t, testUPN, email)
getByEmailCalls++
if getByEmailCalls == 1 {
return nil, mysql_errors.NotFound("MDMIdPAccount")
}
return &fleet.MDMIdPAccount{UUID: testAcctUUID, Email: testUPN, Username: testUPN}, nil
}
ds.InsertMDMIdPAccountFunc = func(ctx context.Context, account *fleet.MDMIdPAccount) error {
require.Equal(t, testUPN, account.Email)
return nil
}
ds.AssociateHostMDMIdPAccountDBFunc = func(ctx context.Context, hostUUID, acctUUID string) error {
require.Equal(t, testHostUUID, hostUUID)
require.Equal(t, testAcctUUID, acctUUID)
return nil
}
upn, deviceID, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, token)
require.NoError(t, err)
require.Equal(t, testUPN, upn)
require.Equal(t, testDeviceID, deviceID)
require.True(t, ds.AssociateHostMDMIdPAccountDBFuncInvoked)
})
t.Run("valid token, account already exists in db", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
token := makeToken(t, svc, testUPN, testDeviceID)
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, HostUUID: ""}, nil
}
// Account exists — Insert should NOT be called.
ds.GetMDMIdPAccountByEmailFunc = func(ctx context.Context, email string) (*fleet.MDMIdPAccount, error) {
return &fleet.MDMIdPAccount{UUID: testAcctUUID, Email: testUPN, Username: "existing-username", Fullname: "Existing Name"}, nil
}
ds.AssociateHostMDMIdPAccountDBFunc = func(ctx context.Context, hostUUID, acctUUID string) error {
return nil
}
_, _, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, token)
require.NoError(t, err)
require.False(t, ds.InsertMDMIdPAccountFuncInvoked, "should not insert when account already exists")
require.True(t, ds.AssociateHostMDMIdPAccountDBFuncInvoked)
})
t.Run("valid token, enrollment already has host_uuid — still links idp account", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
token := makeToken(t, svc, testUPN, testDeviceID)
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
// HostUUID already set — device was previously enrolled.
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, HostUUID: "existing-host-uuid"}, nil
}
// Account already exists — re-enrollment after host deletion may
// have left the enrollment row populated but the mapping missing.
ds.GetMDMIdPAccountByEmailFunc = func(ctx context.Context, email string) (*fleet.MDMIdPAccount, error) {
return &fleet.MDMIdPAccount{UUID: testAcctUUID, Email: testUPN, Username: testUPN}, nil
}
ds.AssociateHostMDMIdPAccountDBFunc = func(ctx context.Context, hostUUID, acctUUID string) error {
require.Equal(t, testHostUUID, hostUUID)
require.Equal(t, testAcctUUID, acctUUID)
return nil
}
upn, deviceID, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, token)
require.NoError(t, err)
require.Equal(t, testUPN, upn)
require.Equal(t, testDeviceID, deviceID)
require.True(t, ds.GetMDMIdPAccountByEmailFuncInvoked, "should still fetch idp account even when enrollment has host_uuid")
require.True(t, ds.AssociateHostMDMIdPAccountDBFuncInvoked, "should still link idp account even when enrollment has host_uuid")
})
t.Run("invalid token falls back to END_USER_AUTH_REQUIRED", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
_, _, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, "this.is.not.a.valid.token")
require.Error(t, err)
var orbitErr *fleet.OrbitError
require.ErrorAs(t, err, &orbitErr)
require.Equal(t, "END_USER_AUTH_REQUIRED", orbitErr.Message)
require.False(t, ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked)
})
t.Run("nil wstepCertManager falls back to END_USER_AUTH_REQUIRED without panic", func(t *testing.T) {
ds := new(mock.Store)
svc := &Service{ds: ds, logger: slog.New(slog.DiscardHandler)}
_, _, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, "any.token.value")
require.Error(t, err)
var orbitErr *fleet.OrbitError
require.ErrorAs(t, err, &orbitErr)
require.Equal(t, "END_USER_AUTH_REQUIRED", orbitErr.Message)
require.False(t, ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked)
})
t.Run("device not found falls back to END_USER_AUTH_REQUIRED", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
token := makeToken(t, svc, testUPN, testDeviceID)
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
return nil, mysql_errors.NotFound("MDMWindowsEnrolledDevice")
}
_, _, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, token)
require.Error(t, err)
var orbitErr *fleet.OrbitError
require.ErrorAs(t, err, &orbitErr)
require.Equal(t, "END_USER_AUTH_REQUIRED", orbitErr.Message)
})
}
func TestGenerateWindowsEUAToken(t *testing.T) {
const (
testUPN = "user@example.com"
testDeviceID = "device-abc-123"
)
t.Run("returns token for device with valid UPN", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, MDMEnrollUserID: testUPN}, nil
}
token := svc.generateWindowsEUAToken(context.Background(), testDeviceID)
require.NotEmpty(t, token)
// Token should be valid and contain expected claims.
claims, err := svc.wstepCertManager.GetEUATokenClaims(token)
require.NoError(t, err)
require.Equal(t, testUPN, claims.UPN)
require.Equal(t, testDeviceID, claims.DeviceID)
})
t.Run("returns empty string when device has no UPN", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, MDMEnrollUserID: ""}, nil
}
require.Empty(t, svc.generateWindowsEUAToken(context.Background(), testDeviceID))
})
t.Run("returns empty string when device not found", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
return nil, mysql_errors.NotFound("MDMWindowsEnrolledDevice")
}
require.Empty(t, svc.generateWindowsEUAToken(context.Background(), testDeviceID))
})
t.Run("returns empty string when datastore returns error", func(t *testing.T) {
ds := new(mock.Store)
svc := newTestServiceWithWSTEP(t, ds)
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
return nil, sql.ErrConnDone
}
require.Empty(t, svc.generateWindowsEUAToken(context.Background(), testDeviceID))
})
t.Run("returns empty string when wstepCertManager is nil", func(t *testing.T) {
ds := new(mock.Store)
svc := &Service{ds: ds, logger: slog.New(slog.DiscardHandler)}
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, MDMEnrollUserID: testUPN}, nil
}
require.Empty(t, svc.generateWindowsEUAToken(context.Background(), testDeviceID))
require.False(t, ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked, "should not query db when cert manager is nil")
})
}