Improve Microsoft endpoint validation (#38180)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #13698

# 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)
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [x] 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)

- [x] QA'd all new/changed functionality manually
This commit is contained in:
Jordan Montgomery
2026-01-12 15:28:45 -05:00
committed by GitHub
parent 93b57ab632
commit e225ef5791
9 changed files with 254 additions and 38 deletions
+1
View File
@@ -0,0 +1 @@
* Improved SOAP message validation on Windows MDM endpoints
+1
View File
@@ -10,6 +10,7 @@ require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/MicahParks/jwkset v0.11.0
github.com/RobotsAndPencils/buford v0.14.0
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea
+2
View File
@@ -50,6 +50,8 @@ github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7r
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+55 -5
View File
@@ -2,13 +2,16 @@ package mdmtest
import (
"bytes"
"crypto/rsa"
"crypto/tls"
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
@@ -42,6 +45,10 @@ type TestWindowsMDMClient struct {
// queuedCommandResponses tracks the commands that will be sent next
// time the device responds to the server.
queuedCommandResponses map[string]fleet.SyncMLCmd
// jwtSigningKey is the key used to sign JWTs
jwtSigningKey *rsa.PrivateKey
// jwtSigningKeyID is the ID to report in the header for the signing key
jwtSigningKeyID string
}
// This is a test-only enrollment type to force erroneous behavior.
@@ -65,6 +72,13 @@ func TestWindowsMDMClientNotInOOBE() TestWindowsMDMClientOption {
}
}
func TestWindowsMDMClientWithSigningKey(signingKey *rsa.PrivateKey, signingKeyID string) TestWindowsMDMClientOption {
return func(c *TestWindowsMDMClient) {
c.jwtSigningKey = signingKey
c.jwtSigningKeyID = signingKeyID
}
}
func NewTestMDMClientWindowsProgramatic(serverURL string, orbitNodeKey string, opts ...TestWindowsMDMClientOption) *TestWindowsMDMClient {
return newTestMDMClient(serverURL, fleet.WindowsMDMProgrammaticEnrollmentType, orbitNodeKey, opts...)
}
@@ -374,10 +388,21 @@ YioVozr1IWYySwWVzMf/SUwKZkKJCAJmSVcixE+4kxPkyPGyauIrN3wWC0zb+mjF
</s:Envelope>
`)
if _, err := c.request(microsoft_mdm.MDE2EnrollPath, enrollReq); err != nil {
resp, err := c.request(microsoft_mdm.MDE2EnrollPath, enrollReq)
if err != nil {
return err
}
// Check for SOAP fault
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if strings.Contains(string(body), "s:fault") {
return fmt.Errorf("enroll request returned SOAP fault: %s", string(body))
}
return nil
}
@@ -416,10 +441,21 @@ func (c *TestWindowsMDMClient) Discovery() error {
// TODO: parse the response and store the policy and enroll endpoints instead
// of hardcoding them to truly test that the server is behaving as expected.
if _, err := c.request(microsoft_mdm.MDE2DiscoveryPath, discoveryReq); err != nil {
resp, err := c.request(microsoft_mdm.MDE2DiscoveryPath, discoveryReq)
if err != nil {
return err
}
// Check for SOAP fault
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if strings.Contains(string(body), "s:fault") {
return fmt.Errorf("discovery request returned SOAP fault: %s", string(body))
}
return nil
}
@@ -467,9 +503,19 @@ func (c *TestWindowsMDMClient) Policy() error {
// TODO: store the policy requirements to generate a certificate and generate
// one on the fly using them instead of using hardcoded values.
if _, err := c.request(microsoft_mdm.MDE2PolicyPath, policyReq); err != nil {
resp, err := c.request(microsoft_mdm.MDE2PolicyPath, policyReq)
if err != nil {
return err
}
// Check for SOAP fault
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if strings.Contains(string(body), "s:fault") {
return fmt.Errorf("policy request returned SOAP fault: %s", string(body))
}
return nil
}
@@ -503,9 +549,13 @@ func (c *TestWindowsMDMClient) getToken() (binarySecToken string, tokenValueType
"unique_name": "foo_bar",
"scp": "mdm_delegation",
}
if c.jwtSigningKey == nil || c.jwtSigningKeyID == "" {
return "", "", errors.New("jwt signing key is not set")
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte("foo"))
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = c.jwtSigningKeyID
tokenString, err := token.SignedString(c.jwtSigningKey)
if err != nil {
return "", "", err
}
+58 -11
View File
@@ -13,11 +13,14 @@ import (
"errors"
"fmt"
"math/big"
"os"
"strconv"
"strings"
"time"
"github.com/MicahParks/jwkset"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/cryptoutil"
"github.com/golang-jwt/jwt/v4"
@@ -215,7 +218,7 @@ func (m *manager) GetSTSAuthTokenUPNClaim(tokenStr string) (string, error) {
}
// Since we used the private key to sign the tokens, we use the public counterpart to verify the signature
token, err := jwt.ParseWithClaims(tokenStr, &STSClaims{}, func(token *jwt.Token) (interface{}, error) {
token, err := jwt.ParseWithClaims(tokenStr, &STSClaims{}, func(token *jwt.Token) (any, error) {
return m.identityCert.PublicKey, nil
})
if err != nil {
@@ -235,27 +238,71 @@ func (m *manager) GetSTSAuthTokenUPNClaim(tokenStr string) (string, error) {
// GetAzureAuthTokenClaims validates the given Azure AD token and returns
// UPN, TenantID, UniqueName, DeviceID
func GetAzureAuthTokenClaims(tokenStr string) (AzureData, error) {
func GetAzureAuthTokenClaims(ctx context.Context, tokenStr string) (AzureData, error) {
if len(tokenStr) == 0 {
return AzureData{}, errors.New("invalid STS token")
return AzureData{}, ctxerr.New(ctx, "invalid STS token")
}
// Decode base64 token
tokenBytes, err := base64.StdEncoding.DecodeString(tokenStr)
if err != nil {
return AzureData{}, errors.New("invalid Azure JWT token")
return AzureData{}, ctxerr.Wrap(ctx, err, "invalid Azure JWT token")
}
// Validate token format (header.payload.signature)
parts := bytes.Split(tokenBytes, []byte("."))
if len(parts) != 3 {
return AzureData{}, errors.New("invalid Azure JWT format")
return AzureData{}, ctxerr.New(ctx, "invalid Azure JWT format")
}
// Parse JWT token
token, _, err := new(jwt.Parser).ParseUnverified(string(tokenBytes), jwt.MapClaims{})
jwksURI := "https://login.microsoftonline.com/common/discovery/v2.0/keys"
var token *jwt.Token
FLEET_DEV_AZURE_JWT_JWKS_URI := os.Getenv("FLEET_DEV_AZURE_JWT_JWKS_URI")
if FLEET_DEV_AZURE_JWT_JWKS_URI != "" {
jwksURI = FLEET_DEV_AZURE_JWT_JWKS_URI
}
keys, err := jwkset.NewDefaultHTTPClient([]string{jwksURI})
if err != nil {
return AzureData{}, errors.New("parse error Azure JWT content")
return AzureData{}, ctxerr.Wrap(ctx, err, "failed to retrieve Azure JWT signing keys")
}
token, err = jwt.Parse(string(tokenBytes), func(token *jwt.Token) (any, error) {
tokenAlg, ok := token.Header["alg"]
if !ok {
return nil, errors.New("Azure JWT missing alg header")
}
tokenAlgStr, ok := tokenAlg.(string)
if !ok {
return nil, errors.New("invalid alg header in Azure JWT")
}
kid, ok := token.Header["kid"]
if !ok {
return nil, errors.New("Azure JWT missing kid header")
}
kidStr, ok := kid.(string)
if !ok {
return nil, errors.New("invalid kid header in Azure JWT")
}
key, err := keys.KeyRead(ctx, kidStr)
if err != nil {
if errors.Is(err, jwkset.ErrKeyNotFound) {
return nil, fmt.Errorf("Azure JWT signed by unknown key: %w", err)
}
return nil, fmt.Errorf("failed to retrieve Azure JWT signing key: %w", err)
}
// Alg is optional in the JWK but if present must match the token
keyAlg := key.Marshal().ALG.String()
if keyAlg != "" && keyAlg != tokenAlgStr {
return nil, fmt.Errorf("Azure JWT signing key algorithm mismatch: expected %s from key, got %s", keyAlg, tokenAlgStr)
}
return key.Key(), nil
})
if err != nil {
return AzureData{}, ctxerr.Wrap(ctx, err, "parse error Azure JWT content")
}
// Parse JWT token
@@ -264,25 +311,25 @@ func GetAzureAuthTokenClaims(tokenStr string) (AzureData, error) {
// Get UPN claim
upnClaim, ok := claims["upn"].(string)
if !ok || len(upnClaim) == 0 {
return AzureData{}, errors.New("invalid UPN claim")
return AzureData{}, ctxerr.New(ctx, "invalid UPN claim")
}
// Get TenantID claim
tenantIDClaim, ok := claims["tid"].(string)
if !ok || len(tenantIDClaim) == 0 {
return AzureData{}, errors.New("invalid TenantID claim")
return AzureData{}, ctxerr.New(ctx, "invalid TenantID claim")
}
// Get UniqueName claim
uniqueNameClaim, ok := claims["unique_name"].(string)
if !ok {
return AzureData{}, errors.New("invalid UniqueName claim")
return AzureData{}, ctxerr.New(ctx, "invalid UniqueName claim")
}
// Get SCP claim
azureSCPClaim, ok := claims["scp"].(string)
if !ok || azureSCPClaim != "mdm_delegation" {
return AzureData{}, errors.New("invalid SCP claim")
return AzureData{}, ctxerr.New(ctx, "invalid SCP claim")
}
return AzureData{
+4 -5
View File
@@ -371,7 +371,7 @@ func (s *integrationLoggerTestSuite) TestWindowsMDMEnrollEmptyBinarySecurityToke
host := createOrbitEnrolledHost(t, "windows", "", s.ds)
mdmDevice := mdmtest.NewTestMDMClientWindowsEmptyBinarySecurityToken(s.server.URL, *host.OrbitNodeKey)
err = mdmDevice.Enroll()
require.NoError(t, err)
require.Error(t, err)
t.Log(s.buf.String())
@@ -394,12 +394,11 @@ func (s *integrationLoggerTestSuite) TestWindowsMDMEnrollEmptyBinarySecurityToke
require.Equal(t, "info", m["level"])
require.Equal(t, "binarySecurityToken is empty", m["soap_fault"])
case microsoft_mdm.MDE2EnrollPath:
require.Equal(t, "info", m["level"])
require.Equal(t, "binarySecurityToken is empty", m["soap_fault"])
foundEnroll = true
foundEnroll = false
}
}
require.True(t, foundDiscovery)
require.True(t, foundPolicy)
require.True(t, foundEnroll)
// Will not enroll due to soap fault on prior request
require.False(t, foundEnroll)
}
@@ -327,6 +327,16 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() {
// the ack of the message should be the only returned command
require.Len(t, cmds, 1)
// Simulate the host having fleetd installed and reporting back in as un-enrolled
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(context.Background(), `
UPDATE host_mdm
SET enrolled = 0, server_url = ''
WHERE host_id = ?
`, host.ID)
return err
})
// re-enroll
require.NoError(t, device.Enroll())
},
@@ -359,13 +369,28 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() {
&orbitScriptResp,
)
// Simulate the host having fleetd installed after being wiped and reporting back in as un-enrolled
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(context.Background(), `
UPDATE host_mdm
SET enrolled = 0, server_url = ''
WHERE host_id = ?
`, host.ID)
return err
})
require.NoError(t, device.Enroll())
},
},
{
"host turns on MDM features out of the blue",
func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
require.NoError(t, device.Enroll())
if strings.Contains(t.Name(), "automatic") {
require.NoError(t, device.Enroll())
} else {
// A programatically-enrolled host that randomly turns on MDM after already enabled will get a SOAP fault
require.Error(t, device.Enroll())
}
},
},
{
@@ -438,7 +463,7 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() {
host := createOrbitEnrolledHost(t, "windows", "windows_automatic", s.ds)
azureMail := "foo.bar.baz@example.com"
device := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail)
device := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID))
device.HardwareID = host.UUID
device.DeviceID = host.UUID
require.NoError(t, device.Enroll())
+105 -14
View File
@@ -35,12 +35,14 @@ import (
"testing"
"time"
"github.com/MicahParks/jwkset"
"github.com/davecgh/go-spew/spew"
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
"github.com/fleetdm/fleet/v4/server/mdm/android/tests"
"github.com/golang-jwt/jwt/v4"
"google.golang.org/api/androidmanagement/v1"
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
@@ -131,6 +133,7 @@ type integrationMDMTestSuite struct {
androidAPIClient *android_mock.Client
androidSvc android.Service
proxyCallbackURL string
jwtSigningKey *rsa.PrivateKey
}
// appleVPPConfigSrvConf is used to configure the mock server that mocks Apple's VPP endpoints.
@@ -158,6 +161,8 @@ var defaultVPPAssetList = []vpp.Asset{
},
}
const defaultFakeJWTKeyID = "fleetdefaultkeyid"
func (s *integrationMDMTestSuite) SetupSuite() {
s.withDS.SetupSuite("integrationMDMTestSuite")
@@ -734,6 +739,37 @@ func (s *integrationMDMTestSuite) SetupSuite() {
s.T().Cleanup(s.appleVPPConfigSrv.Close)
s.T().Cleanup(s.appleVPPProxySrv.Close)
s.T().Cleanup(s.appleGDMFSrv.Close)
// Setup fake JWKs server for Azure JWT verification
metadata := jwkset.JWKMetadataOptions{
KID: defaultFakeJWTKeyID,
}
options := jwkset.JWKOptions{
Metadata: metadata,
}
// Create the JWK from the key and options.
jwk, err := jwkset.NewJWKFromKey(testKey, options)
require.NoError(s.T(), err)
jwkSet := jwkset.NewMemoryStorage()
err = jwkSet.KeyWrite(ctx, jwk)
require.NoError(s.T(), err)
jwksServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/jwks.json" {
response, err := jwkSet.JSONPublic(r.Context())
require.NoError(s.T(), err)
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(response)
require.NoError(s.T(), err)
}
}))
s.T().Cleanup(jwksServer.Close)
s.jwtSigningKey = testKey
s.T().Setenv("FLEET_DEV_AZURE_JWT_JWKS_URI", jwksServer.URL+"/jwks.json")
}
func (s *integrationMDMTestSuite) TearDownSuite() {
@@ -1491,25 +1527,25 @@ func enrollWindowsHostInMDMViaOrbit(t *testing.T, host *fleet.Host, ds fleet.Dat
}
// Simulates a host being orbit enrolled first then an MDM enrollment coming via the settings app
func createWindowsHostThenEnrollMDMViaSettingsApp(ds fleet.Datastore, fleetServerURL, email string, t *testing.T) (*fleet.Host, *mdmtest.TestWindowsMDMClient) {
host := createOrbitEnrolledHost(t, "windows", uuid.NewString(), ds)
mdmDevice := enrollWindowsMDMViaSettingsApp(t, ds, fleetServerURL, email)
func (s *integrationMDMTestSuite) createWindowsHostThenEnrollMDMViaSettingsApp(fleetServerURL, email string) (*fleet.Host, *mdmtest.TestWindowsMDMClient) {
host := createOrbitEnrolledHost(s.T(), "windows", uuid.NewString(), s.ds)
mdmDevice := s.enrollWindowsMDMViaSettingsApp(fleetServerURL, email)
return host, mdmDevice
}
// Note that this method only creates the MDM Enrollment but it will still need to be linked to the host record either
// via DS methods or by simualting a refetch.
func enrollWindowsMDMViaSettingsApp(t *testing.T, ds fleet.Datastore, fleetServerURL, email string) *mdmtest.TestWindowsMDMClient {
mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email, mdmtest.TestWindowsMDMClientNotInOOBE())
func (s *integrationMDMTestSuite) enrollWindowsMDMViaSettingsApp(fleetServerURL, email string) *mdmtest.TestWindowsMDMClient {
mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email, mdmtest.TestWindowsMDMClientNotInOOBE(), mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID))
err := mdmDevice.Enroll()
require.NoError(t, err)
require.NoError(s.T(), err)
return mdmDevice
}
func enrollWindowsHostInMDMViaAutopilot(t *testing.T, ds fleet.Datastore, fleetServerURL, email string) *mdmtest.TestWindowsMDMClient {
mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email)
func (s *integrationMDMTestSuite) enrollWindowsHostInMDMViaAutopilot(fleetServerURL, email string) *mdmtest.TestWindowsMDMClient {
mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID))
err := mdmDevice.Enroll()
require.NoError(t, err)
require.NoError(s.T(), err)
return mdmDevice
}
@@ -7663,7 +7699,20 @@ func (s *integrationMDMTestSuite) TestValidGetPoliciesRequestWithAzureToken() {
t := s.T()
// Preparing the GetPolicies Request message with Azure JWT token
azureADTok := "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUo5LmV5SmhkV1FpT2lKb2RIUndjem92TDIxaGNtTnZjMnhoWW5NdWIzSm5MeUlzSW1semN5STZJbWgwZEhCek9pOHZjM1J6TG5kcGJtUnZkM011Ym1WMEwyWmhaVFZqTkdZekxXWXpNVGd0TkRRNE15MWlZelptTFRjMU9UVTFaalJoTUdFM01pOGlMQ0pwWVhRaU9qRTJPRGt4TnpBNE5UZ3NJbTVpWmlJNk1UWTRPVEUzTURnMU9Dd2laWGh3SWpveE5qZzVNVGMxTmpZeExDSmhZM0lpT2lJeElpd2lZV2x2SWpvaVFWUlJRWGt2T0ZSQlFVRkJOV2gwUTNFMGRERjNjbHBwUTIxQmVEQlpWaTloZGpGTVMwRkRPRXM1Vm10SGVtNUdXVGxzTUZoYWVrZHVha2N6VVRaMWVIUldNR3QxT1hCeFJXdFRZeUlzSW1GdGNpSTZXeUp3ZDJRaUxDSnljMkVpWFN3aVlYQndhV1FpT2lJeU9XUTVaV1E1T0MxaE5EWTVMVFExTXpZdFlXUmxNaTFtT1RneFltTXhaRFl3TldVaUxDSmhjSEJwWkdGamNpSTZJakFpTENKa1pYWnBZMlZwWkNJNkltRXhNMlkzWVdVd0xURXpPR0V0TkdKaU1pMDVNalF5TFRka09USXlaVGRqTkdGak15SXNJbWx3WVdSa2NpSTZJakU0Tmk0eE1pNHhPRGN1TWpZaUxDSnVZVzFsSWpvaVZHVnpkRTFoY21OdmMweGhZbk1pTENKdmFXUWlPaUpsTTJNMU5XVmtZeTFqTXpRNExUUTBNVFl0T0dZd05TMHlOVFJtWmpNd05qVmpOV1VpTENKd2QyUmZkWEpzSWpvaWFIUjBjSE02THk5d2IzSjBZV3d1YldsamNtOXpiMlowYjI1c2FXNWxMbU52YlM5RGFHRnVaMlZRWVhOemQyOXlaQzVoYzNCNElpd2ljbWdpT2lJd0xrRldTVUU0T0ZSc0xXaHFlbWN3VXpoaU0xZFdXREJ2UzJOdFZGRXpTbHB1ZUUxa1QzQTNUbVZVVm5OV2FYVkhOa0ZRYnk0aUxDSnpZM0FpT2lKdFpHMWZaR1ZzWldkaGRHbHZiaUlzSW5OMVlpSTZJa1pTUTJ4RldURk9ObXR2ZEdWblMzcFplV0pFTjJkdFdGbGxhVTVIUkZrd05FSjJOV3R6ZDJGeGJVRWlMQ0owYVdRaU9pSm1ZV1UxWXpSbU15MW1NekU0TFRRME9ETXRZbU0yWmkwM05UazFOV1kwWVRCaE56SWlMQ0oxYm1seGRXVmZibUZ0WlNJNkluUmxjM1JBYldGeVkyOXpiR0ZpY3k1dmNtY2lMQ0oxY0c0aU9pSjBaWE4wUUcxaGNtTnZjMnhoWW5NdWIzSm5JaXdpZFhScElqb2lNVGg2WkVWSU5UZFRSWFZyYWpseGJqRm9aMlJCUVNJc0luWmxjaUk2SWpFdU1DSjkuVG1FUlRsZktBdWo5bTVvQUc2UTBRblV4VEFEaTNFamtlNHZ3VXo3UTdqUUFVZVZGZzl1U0pzUXNjU2hFTXVxUmQzN1R2VlpQanljdEVoRFgwLVpQcEVVYUlSempuRVEyTWxvc21SZURYZzhrYkhNZVliWi1jb0ZucDEyQkVpQnpJWFBGZnBpaU1GRnNZZ0hSSF9tSWxwYlBlRzJuQ2p0LTZSOHgzYVA5QS1tM0J3eV91dnV0WDFNVEVZRmFsekhGa04wNWkzbjZRcjhURnlJQ1ZUYW5OanlkMjBBZFRMbHJpTVk0RVBmZzRaLThVVTctZkcteElycWVPUmVWTnYwOUFHV192MDd6UkVaNmgxVk9tNl9nelRGcElVVURuZFdabnFLTHlySDlkdkF3WnFFSG1HUmlTNElNWnRFdDJNTkVZSnhDWHhlSi1VbWZJdV9tUVhKMW9R"
// Preparing the SecurityToken Request message with Azure JWT token
claims := &jwt.MapClaims{
"upn": "fleetie@example.com",
"tid": "tenant_id",
"unique_name": "foo_bar",
"scp": "mdm_delegation",
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = defaultFakeJWTKeyID
tokenString, err := token.SignedString(s.jwtSigningKey)
require.NoError(t, err)
azureADTok := base64.URLEncoding.EncodeToString([]byte(tokenString))
requestBytes, err := s.newGetPoliciesMsg(false, azureADTok)
require.NoError(t, err)
@@ -7826,7 +7875,19 @@ func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithAzureT
t := s.T()
// Preparing the SecurityToken Request message with Azure JWT token
azureADTok := "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUo5LmV5SmhkV1FpT2lKb2RIUndjem92TDIxaGNtTnZjMnhoWW5NdWIzSm5MeUlzSW1semN5STZJbWgwZEhCek9pOHZjM1J6TG5kcGJtUnZkM011Ym1WMEwyWmhaVFZqTkdZekxXWXpNVGd0TkRRNE15MWlZelptTFRjMU9UVTFaalJoTUdFM01pOGlMQ0pwWVhRaU9qRTJPRGt4TnpBNE5UZ3NJbTVpWmlJNk1UWTRPVEUzTURnMU9Dd2laWGh3SWpveE5qZzVNVGMxTmpZeExDSmhZM0lpT2lJeElpd2lZV2x2SWpvaVFWUlJRWGt2T0ZSQlFVRkJOV2gwUTNFMGRERjNjbHBwUTIxQmVEQlpWaTloZGpGTVMwRkRPRXM1Vm10SGVtNUdXVGxzTUZoYWVrZHVha2N6VVRaMWVIUldNR3QxT1hCeFJXdFRZeUlzSW1GdGNpSTZXeUp3ZDJRaUxDSnljMkVpWFN3aVlYQndhV1FpT2lJeU9XUTVaV1E1T0MxaE5EWTVMVFExTXpZdFlXUmxNaTFtT1RneFltTXhaRFl3TldVaUxDSmhjSEJwWkdGamNpSTZJakFpTENKa1pYWnBZMlZwWkNJNkltRXhNMlkzWVdVd0xURXpPR0V0TkdKaU1pMDVNalF5TFRka09USXlaVGRqTkdGak15SXNJbWx3WVdSa2NpSTZJakU0Tmk0eE1pNHhPRGN1TWpZaUxDSnVZVzFsSWpvaVZHVnpkRTFoY21OdmMweGhZbk1pTENKdmFXUWlPaUpsTTJNMU5XVmtZeTFqTXpRNExUUTBNVFl0T0dZd05TMHlOVFJtWmpNd05qVmpOV1VpTENKd2QyUmZkWEpzSWpvaWFIUjBjSE02THk5d2IzSjBZV3d1YldsamNtOXpiMlowYjI1c2FXNWxMbU52YlM5RGFHRnVaMlZRWVhOemQyOXlaQzVoYzNCNElpd2ljbWdpT2lJd0xrRldTVUU0T0ZSc0xXaHFlbWN3VXpoaU0xZFdXREJ2UzJOdFZGRXpTbHB1ZUUxa1QzQTNUbVZVVm5OV2FYVkhOa0ZRYnk0aUxDSnpZM0FpT2lKdFpHMWZaR1ZzWldkaGRHbHZiaUlzSW5OMVlpSTZJa1pTUTJ4RldURk9ObXR2ZEdWblMzcFplV0pFTjJkdFdGbGxhVTVIUkZrd05FSjJOV3R6ZDJGeGJVRWlMQ0owYVdRaU9pSm1ZV1UxWXpSbU15MW1NekU0TFRRME9ETXRZbU0yWmkwM05UazFOV1kwWVRCaE56SWlMQ0oxYm1seGRXVmZibUZ0WlNJNkluUmxjM1JBYldGeVkyOXpiR0ZpY3k1dmNtY2lMQ0oxY0c0aU9pSjBaWE4wUUcxaGNtTnZjMnhoWW5NdWIzSm5JaXdpZFhScElqb2lNVGg2WkVWSU5UZFRSWFZyYWpseGJqRm9aMlJCUVNJc0luWmxjaUk2SWpFdU1DSjkuVG1FUlRsZktBdWo5bTVvQUc2UTBRblV4VEFEaTNFamtlNHZ3VXo3UTdqUUFVZVZGZzl1U0pzUXNjU2hFTXVxUmQzN1R2VlpQanljdEVoRFgwLVpQcEVVYUlSempuRVEyTWxvc21SZURYZzhrYkhNZVliWi1jb0ZucDEyQkVpQnpJWFBGZnBpaU1GRnNZZ0hSSF9tSWxwYlBlRzJuQ2p0LTZSOHgzYVA5QS1tM0J3eV91dnV0WDFNVEVZRmFsekhGa04wNWkzbjZRcjhURnlJQ1ZUYW5OanlkMjBBZFRMbHJpTVk0RVBmZzRaLThVVTctZkcteElycWVPUmVWTnYwOUFHV192MDd6UkVaNmgxVk9tNl9nelRGcElVVURuZFdabnFLTHlySDlkdkF3WnFFSG1HUmlTNElNWnRFdDJNTkVZSnhDWHhlSi1VbWZJdV9tUVhKMW9R"
claims := &jwt.MapClaims{
"upn": "fleetie@example.com",
"tid": "tenant_id",
"unique_name": "foo_bar",
"scp": "mdm_delegation",
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = defaultFakeJWTKeyID
tokenString, err := token.SignedString(s.jwtSigningKey)
require.NoError(t, err)
azureADTok := base64.URLEncoding.EncodeToString([]byte(tokenString))
requestBytes, err := s.newSecurityTokenMsg(azureADTok, false, false)
require.NoError(t, err)
@@ -8396,7 +8457,7 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
require.NoError(t, err)
azureMail := "foo.bar.baz@example.com"
d := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail)
d := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID))
require.NoError(t, d.Enroll())
checkinAndAck := func(expectFleetdCmds bool) {
@@ -8487,6 +8548,36 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
checkinAndAck(false)
}
func (s *integrationMDMTestSuite) TestWindowsAzureInitiatedBadKeys() {
t := s.T()
ctx := context.Background()
// define a global enroll secret
err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
require.NoError(t, err)
// We could generate the key differently, this just ensures it's generated the same as the "happy path" tests
_, badKey, err := apple_mdm.NewSCEPCACertKey()
require.NoError(t, err)
autopilotUserMail := "swan@example.com"
// Bad key
mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, autopilotUserMail, mdmtest.TestWindowsMDMClientWithSigningKey(badKey, defaultFakeJWTKeyID))
err = mdmDevice.Enroll()
require.Error(s.T(), err)
// Good key but wrong ID
mdmDevice = mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, autopilotUserMail, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, "bad-key-id"))
err = mdmDevice.Enroll()
require.Error(s.T(), err)
// Happy path to ensure the setup is correct
mdmDevice = mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, autopilotUserMail, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID))
err = mdmDevice.Enroll()
require.NoError(s.T(), err)
}
func (s *integrationMDMTestSuite) TestWindowsAzureInitiatedEnrollmentAndMapping() {
t := s.T()
ctx := context.Background()
@@ -8503,11 +8594,11 @@ func (s *integrationMDMTestSuite) TestWindowsAzureInitiatedEnrollmentAndMapping(
// Enroll another host to ensure the wires don't get crossed somehow
autopilotUserMail := "swan@example.com"
autopilotDevice := enrollWindowsHostInMDMViaAutopilot(t, s.ds, s.server.URL, autopilotUserMail)
autopilotDevice := s.enrollWindowsHostInMDMViaAutopilot(s.server.URL, autopilotUserMail)
require.NoError(t, autopilotDevice.Enroll())
settingsAppUserMail := "fleetie@example.com"
settingsAppHost, settingsAppDevice := createWindowsHostThenEnrollMDMViaSettingsApp(s.ds, s.server.URL, settingsAppUserMail, t)
settingsAppHost, settingsAppDevice := s.createWindowsHostThenEnrollMDMViaSettingsApp(s.server.URL, settingsAppUserMail)
require.NoError(t, settingsAppDevice.Enroll())
// Transfer the host to the team. Ensure it doesn't wind up in "No team" at the end
+1 -1
View File
@@ -1021,7 +1021,7 @@ func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *flee
if authToken.IsAzureJWTToken() {
// Validate the JWT Auth token by retreving its claims
tokenData, err := microsoft_mdm.GetAzureAuthTokenClaims(authToken.Content)
tokenData, err := microsoft_mdm.GetAzureAuthTokenClaims(ctx, authToken.Content)
if err != nil {
return "", "", fmt.Errorf("binary security token claim failed: %v", err)
}