managedidentity.go raw

   1  // Copyright (c) Microsoft Corporation.
   2  // Licensed under the MIT license.
   3  
   4  /*
   5  Package managedidentity provides a client for retrieval of Managed Identity applications.
   6  The Managed Identity Client is used to acquire a token for managed identity assigned to
   7  an azure resource such as Azure function, app service, virtual machine, etc. to acquire a token
   8  without using credentials.
   9  */
  10  package managedidentity
  11  
  12  import (
  13  	"context"
  14  	"encoding/json"
  15  	"fmt"
  16  	"io"
  17  	"net/http"
  18  	"net/url"
  19  	"os"
  20  	"path/filepath"
  21  	"runtime"
  22  	"strings"
  23  	"sync/atomic"
  24  	"time"
  25  
  26  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
  27  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base"
  28  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage"
  29  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
  30  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
  31  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
  32  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
  33  )
  34  
  35  // AuthResult contains the results of one token acquisition operation.
  36  // For details see https://aka.ms/msal-net-authenticationresult
  37  type AuthResult = base.AuthResult
  38  
  39  type TokenSource = base.TokenSource
  40  
  41  const (
  42  	TokenSourceIdentityProvider = base.TokenSourceIdentityProvider
  43  	TokenSourceCache            = base.TokenSourceCache
  44  )
  45  
  46  const (
  47  	// DefaultToIMDS indicates that the source is defaulted to IMDS when no environment variables are set.
  48  	DefaultToIMDS Source = "DefaultToIMDS"
  49  	AzureArc      Source = "AzureArc"
  50  	ServiceFabric Source = "ServiceFabric"
  51  	CloudShell    Source = "CloudShell"
  52  	AzureML       Source = "AzureML"
  53  	AppService    Source = "AppService"
  54  
  55  	// General request query parameter names
  56  	metaHTTPHeaderName           = "Metadata"
  57  	apiVersionQueryParameterName = "api-version"
  58  	resourceQueryParameterName   = "resource"
  59  	wwwAuthenticateHeaderName    = "www-authenticate"
  60  
  61  	// UAMI query parameter name
  62  	miQueryParameterClientId       = "client_id"
  63  	miQueryParameterObjectId       = "object_id"
  64  	miQueryParameterPrincipalId    = "principal_id"
  65  	miQueryParameterResourceIdIMDS = "msi_res_id"
  66  	miQueryParameterResourceId     = "mi_res_id"
  67  
  68  	// IMDS
  69  	imdsDefaultEndpoint           = "http://169.254.169.254/metadata/identity/oauth2/token"
  70  	imdsAPIVersion                = "2018-02-01"
  71  	systemAssignedManagedIdentity = "system_assigned_managed_identity"
  72  
  73  	// Azure Arc
  74  	azureArcEndpoint               = "http://127.0.0.1:40342/metadata/identity/oauth2/token"
  75  	azureArcAPIVersion             = "2020-06-01"
  76  	azureArcFileExtension          = ".key"
  77  	azureArcMaxFileSizeBytes int64 = 4096
  78  	linuxTokenPath                 = "/var/opt/azcmagent/tokens" // #nosec G101
  79  	linuxHimdsPath                 = "/opt/azcmagent/bin/himds"
  80  	azureConnectedMachine          = "AzureConnectedMachineAgent"
  81  	himdsExecutableName            = "himds.exe"
  82  	tokenName                      = "Tokens"
  83  
  84  	// App Service
  85  	appServiceAPIVersion = "2019-08-01"
  86  
  87  	// AzureML
  88  	azureMLAPIVersion = "2017-09-01"
  89  	// Service Fabric
  90  	serviceFabricAPIVersion = "2019-07-01-preview"
  91  
  92  	// Environment Variables
  93  	identityEndpointEnvVar              = "IDENTITY_ENDPOINT"
  94  	identityHeaderEnvVar                = "IDENTITY_HEADER"
  95  	azurePodIdentityAuthorityHostEnvVar = "AZURE_POD_IDENTITY_AUTHORITY_HOST"
  96  	imdsEndVar                          = "IMDS_ENDPOINT"
  97  	msiEndpointEnvVar                   = "MSI_ENDPOINT"
  98  	msiSecretEnvVar                     = "MSI_SECRET"
  99  	identityServerThumbprintEnvVar      = "IDENTITY_SERVER_THUMBPRINT"
 100  
 101  	defaultRetryCount = 3
 102  )
 103  
 104  var retryCodesForIMDS = []int{
 105  	http.StatusNotFound,                      // 404
 106  	http.StatusGone,                          // 410
 107  	http.StatusTooManyRequests,               // 429
 108  	http.StatusInternalServerError,           // 500
 109  	http.StatusNotImplemented,                // 501
 110  	http.StatusBadGateway,                    // 502
 111  	http.StatusServiceUnavailable,            // 503
 112  	http.StatusGatewayTimeout,                // 504
 113  	http.StatusHTTPVersionNotSupported,       // 505
 114  	http.StatusVariantAlsoNegotiates,         // 506
 115  	http.StatusInsufficientStorage,           // 507
 116  	http.StatusLoopDetected,                  // 508
 117  	http.StatusNotExtended,                   // 510
 118  	http.StatusNetworkAuthenticationRequired, // 511
 119  }
 120  
 121  var retryStatusCodes = []int{
 122  	http.StatusRequestTimeout,      // 408
 123  	http.StatusTooManyRequests,     // 429
 124  	http.StatusInternalServerError, // 500
 125  	http.StatusBadGateway,          // 502
 126  	http.StatusServiceUnavailable,  // 503
 127  	http.StatusGatewayTimeout,      // 504
 128  }
 129  
 130  var getAzureArcPlatformPath = func(platform string) string {
 131  	switch platform {
 132  	case "windows":
 133  		return filepath.Join(os.Getenv("ProgramData"), azureConnectedMachine, tokenName)
 134  	case "linux":
 135  		return linuxTokenPath
 136  	default:
 137  		return ""
 138  	}
 139  }
 140  
 141  var getAzureArcHimdsFilePath = func(platform string) string {
 142  	switch platform {
 143  	case "windows":
 144  		return filepath.Join(os.Getenv("ProgramData"), azureConnectedMachine, himdsExecutableName)
 145  	case "linux":
 146  		return linuxHimdsPath
 147  	default:
 148  		return ""
 149  	}
 150  }
 151  
 152  type Source string
 153  
 154  type ID interface {
 155  	value() string
 156  }
 157  
 158  type systemAssignedValue string // its private for a reason to make the input consistent.
 159  type UserAssignedClientID string
 160  type UserAssignedObjectID string
 161  type UserAssignedResourceID string
 162  
 163  func (s systemAssignedValue) value() string    { return string(s) }
 164  func (c UserAssignedClientID) value() string   { return string(c) }
 165  func (o UserAssignedObjectID) value() string   { return string(o) }
 166  func (r UserAssignedResourceID) value() string { return string(r) }
 167  func SystemAssigned() ID {
 168  	return systemAssignedValue(systemAssignedManagedIdentity)
 169  }
 170  
 171  // cache never uses the client because instance discovery is always disabled.
 172  var cacheManager *storage.Manager = storage.New(nil)
 173  
 174  type Client struct {
 175  	httpClient         ops.HTTPClient
 176  	miType             ID
 177  	source             Source
 178  	authParams         authority.AuthParams
 179  	retryPolicyEnabled bool
 180  	canRefresh         *atomic.Value
 181  }
 182  
 183  type AcquireTokenOptions struct {
 184  	claims string
 185  }
 186  
 187  type ClientOption func(*Client)
 188  
 189  type AcquireTokenOption func(o *AcquireTokenOptions)
 190  
 191  // WithClaims sets additional claims to request for the token, such as those required by token revocation or conditional access policies.
 192  // Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded.
 193  func WithClaims(claims string) AcquireTokenOption {
 194  	return func(o *AcquireTokenOptions) {
 195  		o.claims = claims
 196  	}
 197  }
 198  
 199  // WithHTTPClient allows for a custom HTTP client to be set.
 200  func WithHTTPClient(httpClient ops.HTTPClient) ClientOption {
 201  	return func(c *Client) {
 202  		c.httpClient = httpClient
 203  	}
 204  }
 205  
 206  func WithRetryPolicyDisabled() ClientOption {
 207  	return func(c *Client) {
 208  		c.retryPolicyEnabled = false
 209  	}
 210  }
 211  
 212  // Client to be used to acquire tokens for managed identity.
 213  // ID: [SystemAssigned], [UserAssignedClientID], [UserAssignedResourceID], [UserAssignedObjectID]
 214  //
 215  // Options: [WithHTTPClient]
 216  func New(id ID, options ...ClientOption) (Client, error) {
 217  	source, err := GetSource()
 218  	if err != nil {
 219  		return Client{}, err
 220  	}
 221  
 222  	// Check for user-assigned restrictions based on the source
 223  	switch source {
 224  	case AzureArc:
 225  		switch id.(type) {
 226  		case UserAssignedClientID, UserAssignedResourceID, UserAssignedObjectID:
 227  			return Client{}, errors.New("Azure Arc doesn't support user-assigned managed identities")
 228  		}
 229  	case AzureML:
 230  		switch id.(type) {
 231  		case UserAssignedObjectID, UserAssignedResourceID:
 232  			return Client{}, errors.New("Azure ML supports specifying a user-assigned managed identity by client ID only")
 233  		}
 234  	case CloudShell:
 235  		switch id.(type) {
 236  		case UserAssignedClientID, UserAssignedResourceID, UserAssignedObjectID:
 237  			return Client{}, errors.New("Cloud Shell doesn't support user-assigned managed identities")
 238  		}
 239  	case ServiceFabric:
 240  		switch id.(type) {
 241  		case UserAssignedClientID, UserAssignedResourceID, UserAssignedObjectID:
 242  			return Client{}, errors.New("Service Fabric API doesn't support specifying a user-assigned identity. The identity is determined by cluster resource configuration. See https://aka.ms/servicefabricmi")
 243  		}
 244  	}
 245  
 246  	switch t := id.(type) {
 247  	case UserAssignedClientID:
 248  		if len(string(t)) == 0 {
 249  			return Client{}, fmt.Errorf("empty %T", t)
 250  		}
 251  	case UserAssignedResourceID:
 252  		if len(string(t)) == 0 {
 253  			return Client{}, fmt.Errorf("empty %T", t)
 254  		}
 255  	case UserAssignedObjectID:
 256  		if len(string(t)) == 0 {
 257  			return Client{}, fmt.Errorf("empty %T", t)
 258  		}
 259  	case systemAssignedValue:
 260  	default:
 261  		return Client{}, fmt.Errorf("unsupported type %T", id)
 262  	}
 263  	zero := atomic.Value{}
 264  	zero.Store(false)
 265  	client := Client{
 266  		miType:             id,
 267  		httpClient:         shared.DefaultClient,
 268  		retryPolicyEnabled: true,
 269  		source:             source,
 270  		canRefresh:         &zero,
 271  	}
 272  	for _, option := range options {
 273  		option(&client)
 274  	}
 275  	fakeAuthInfo, err := authority.NewInfoFromAuthorityURI("https://login.microsoftonline.com/managed_identity", false, true)
 276  	if err != nil {
 277  		return Client{}, err
 278  	}
 279  	client.authParams = authority.NewAuthParams(client.miType.value(), fakeAuthInfo)
 280  	return client, nil
 281  }
 282  
 283  // GetSource detects and returns the managed identity source available on the environment.
 284  func GetSource() (Source, error) {
 285  	identityEndpoint := os.Getenv(identityEndpointEnvVar)
 286  	identityHeader := os.Getenv(identityHeaderEnvVar)
 287  	identityServerThumbprint := os.Getenv(identityServerThumbprintEnvVar)
 288  	msiEndpoint := os.Getenv(msiEndpointEnvVar)
 289  	msiSecret := os.Getenv(msiSecretEnvVar)
 290  	imdsEndpoint := os.Getenv(imdsEndVar)
 291  
 292  	if identityEndpoint != "" && identityHeader != "" {
 293  		if identityServerThumbprint != "" {
 294  			return ServiceFabric, nil
 295  		}
 296  		return AppService, nil
 297  	} else if msiEndpoint != "" {
 298  		if msiSecret != "" {
 299  			return AzureML, nil
 300  		} else {
 301  			return CloudShell, nil
 302  		}
 303  	} else if isAzureArcEnvironment(identityEndpoint, imdsEndpoint) {
 304  		return AzureArc, nil
 305  	}
 306  
 307  	return DefaultToIMDS, nil
 308  }
 309  
 310  // This function wraps time.Now() and is used for refreshing the application
 311  // was created to test the function against refreshin
 312  var now = time.Now
 313  
 314  // Acquires tokens from the configured managed identity on an azure resource.
 315  //
 316  // Resource: scopes application is requesting access to
 317  // Options: [WithClaims]
 318  func (c Client) AcquireToken(ctx context.Context, resource string, options ...AcquireTokenOption) (AuthResult, error) {
 319  	resource = strings.TrimSuffix(resource, "/.default")
 320  	o := AcquireTokenOptions{}
 321  	for _, option := range options {
 322  		option(&o)
 323  	}
 324  	c.authParams.Scopes = []string{resource}
 325  
 326  	// ignore cached access tokens when given claims
 327  	if o.claims == "" {
 328  		stResp, err := cacheManager.Read(ctx, c.authParams)
 329  		if err != nil {
 330  			return AuthResult{}, err
 331  		}
 332  		ar, err := base.AuthResultFromStorage(stResp)
 333  		if err == nil {
 334  			if !stResp.AccessToken.RefreshOn.T.IsZero() && !stResp.AccessToken.RefreshOn.T.After(now()) && c.canRefresh.CompareAndSwap(false, true) {
 335  				defer c.canRefresh.Store(false)
 336  				if tr, er := c.getToken(ctx, resource); er == nil {
 337  					return tr, nil
 338  				}
 339  			}
 340  			ar.AccessToken, err = c.authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
 341  			return ar, err
 342  		}
 343  	}
 344  	return c.getToken(ctx, resource)
 345  }
 346  
 347  func (c Client) getToken(ctx context.Context, resource string) (AuthResult, error) {
 348  	switch c.source {
 349  	case AzureArc:
 350  		return c.acquireTokenForAzureArc(ctx, resource)
 351  	case AzureML:
 352  		return c.acquireTokenForAzureML(ctx, resource)
 353  	case CloudShell:
 354  		return c.acquireTokenForCloudShell(ctx, resource)
 355  	case DefaultToIMDS:
 356  		return c.acquireTokenForIMDS(ctx, resource)
 357  	case AppService:
 358  		return c.acquireTokenForAppService(ctx, resource)
 359  	case ServiceFabric:
 360  		return c.acquireTokenForServiceFabric(ctx, resource)
 361  	default:
 362  		return AuthResult{}, fmt.Errorf("unsupported source %q", c.source)
 363  	}
 364  }
 365  
 366  func (c Client) acquireTokenForAppService(ctx context.Context, resource string) (AuthResult, error) {
 367  	req, err := createAppServiceAuthRequest(ctx, c.miType, resource)
 368  	if err != nil {
 369  		return AuthResult{}, err
 370  	}
 371  	tokenResponse, err := c.getTokenForRequest(req, resource)
 372  	if err != nil {
 373  		return AuthResult{}, err
 374  	}
 375  	return authResultFromToken(c.authParams, tokenResponse)
 376  }
 377  
 378  func (c Client) acquireTokenForIMDS(ctx context.Context, resource string) (AuthResult, error) {
 379  	req, err := createIMDSAuthRequest(ctx, c.miType, resource)
 380  	if err != nil {
 381  		return AuthResult{}, err
 382  	}
 383  	tokenResponse, err := c.getTokenForRequest(req, resource)
 384  	if err != nil {
 385  		return AuthResult{}, err
 386  	}
 387  	return authResultFromToken(c.authParams, tokenResponse)
 388  }
 389  
 390  func (c Client) acquireTokenForCloudShell(ctx context.Context, resource string) (AuthResult, error) {
 391  	req, err := createCloudShellAuthRequest(ctx, resource)
 392  	if err != nil {
 393  		return AuthResult{}, err
 394  	}
 395  	tokenResponse, err := c.getTokenForRequest(req, resource)
 396  	if err != nil {
 397  		return AuthResult{}, err
 398  	}
 399  	return authResultFromToken(c.authParams, tokenResponse)
 400  }
 401  
 402  func (c Client) acquireTokenForAzureML(ctx context.Context, resource string) (AuthResult, error) {
 403  	req, err := createAzureMLAuthRequest(ctx, c.miType, resource)
 404  	if err != nil {
 405  		return AuthResult{}, err
 406  	}
 407  	tokenResponse, err := c.getTokenForRequest(req, resource)
 408  	if err != nil {
 409  		return AuthResult{}, err
 410  	}
 411  	return authResultFromToken(c.authParams, tokenResponse)
 412  }
 413  
 414  func (c Client) acquireTokenForServiceFabric(ctx context.Context, resource string) (AuthResult, error) {
 415  	req, err := createServiceFabricAuthRequest(ctx, resource)
 416  	if err != nil {
 417  		return AuthResult{}, err
 418  	}
 419  	tokenResponse, err := c.getTokenForRequest(req, resource)
 420  	if err != nil {
 421  		return AuthResult{}, err
 422  	}
 423  	return authResultFromToken(c.authParams, tokenResponse)
 424  }
 425  
 426  func (c Client) acquireTokenForAzureArc(ctx context.Context, resource string) (AuthResult, error) {
 427  	req, err := createAzureArcAuthRequest(ctx, resource, "")
 428  	if err != nil {
 429  		return AuthResult{}, err
 430  	}
 431  
 432  	response, err := c.httpClient.Do(req)
 433  	if err != nil {
 434  		return AuthResult{}, err
 435  	}
 436  	defer response.Body.Close()
 437  
 438  	if response.StatusCode != http.StatusUnauthorized {
 439  		return AuthResult{}, fmt.Errorf("expected a 401 response, received %d", response.StatusCode)
 440  	}
 441  
 442  	secret, err := c.getAzureArcSecretKey(response, runtime.GOOS)
 443  	if err != nil {
 444  		return AuthResult{}, err
 445  	}
 446  
 447  	secondRequest, err := createAzureArcAuthRequest(ctx, resource, string(secret))
 448  	if err != nil {
 449  		return AuthResult{}, err
 450  	}
 451  
 452  	tokenResponse, err := c.getTokenForRequest(secondRequest, resource)
 453  	if err != nil {
 454  		return AuthResult{}, err
 455  	}
 456  	return authResultFromToken(c.authParams, tokenResponse)
 457  }
 458  
 459  func authResultFromToken(authParams authority.AuthParams, token accesstokens.TokenResponse) (AuthResult, error) {
 460  	if cacheManager == nil {
 461  		return AuthResult{}, errors.New("cache instance is nil")
 462  	}
 463  	account, err := cacheManager.Write(authParams, token)
 464  	if err != nil {
 465  		return AuthResult{}, err
 466  	}
 467  	// if refreshOn is not set, set it to half of the time until expiry if expiry is more than 2 hours away
 468  	if token.RefreshOn.T.IsZero() {
 469  		if lifetime := time.Until(token.ExpiresOn); lifetime > 2*time.Hour {
 470  			token.RefreshOn.T = time.Now().Add(lifetime / 2)
 471  		}
 472  	}
 473  	ar, err := base.NewAuthResult(token, account)
 474  	if err != nil {
 475  		return AuthResult{}, err
 476  	}
 477  	ar.AccessToken, err = authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
 478  	return ar, err
 479  }
 480  
 481  // contains checks if the element is present in the list.
 482  func contains[T comparable](list []T, element T) bool {
 483  	for _, v := range list {
 484  		if v == element {
 485  			return true
 486  		}
 487  	}
 488  	return false
 489  }
 490  
 491  // retry performs an HTTP request with retries based on the provided options.
 492  func (c Client) retry(maxRetries int, req *http.Request) (*http.Response, error) {
 493  	var resp *http.Response
 494  	var err error
 495  	for attempt := 0; attempt < maxRetries; attempt++ {
 496  		tryCtx, tryCancel := context.WithTimeout(req.Context(), time.Minute)
 497  		defer tryCancel()
 498  		if resp != nil && resp.Body != nil {
 499  			_, _ = io.Copy(io.Discard, resp.Body)
 500  			resp.Body.Close()
 501  		}
 502  		cloneReq := req.Clone(tryCtx)
 503  		resp, err = c.httpClient.Do(cloneReq)
 504  		retrylist := retryStatusCodes
 505  		if c.source == DefaultToIMDS {
 506  			retrylist = retryCodesForIMDS
 507  		}
 508  		if err == nil && !contains(retrylist, resp.StatusCode) {
 509  			return resp, nil
 510  		}
 511  		select {
 512  		case <-time.After(time.Second):
 513  		case <-req.Context().Done():
 514  			err = req.Context().Err()
 515  			return resp, err
 516  		}
 517  	}
 518  	return resp, err
 519  }
 520  
 521  func (c Client) getTokenForRequest(req *http.Request, resource string) (accesstokens.TokenResponse, error) {
 522  	r := accesstokens.TokenResponse{}
 523  	var resp *http.Response
 524  	var err error
 525  
 526  	if c.retryPolicyEnabled {
 527  		resp, err = c.retry(defaultRetryCount, req)
 528  	} else {
 529  		resp, err = c.httpClient.Do(req)
 530  	}
 531  	if err != nil {
 532  		return r, err
 533  	}
 534  	responseBytes, err := io.ReadAll(resp.Body)
 535  	defer resp.Body.Close()
 536  	if err != nil {
 537  		return r, err
 538  	}
 539  	switch resp.StatusCode {
 540  	case http.StatusOK, http.StatusAccepted:
 541  	default:
 542  		sd := strings.TrimSpace(string(responseBytes))
 543  		if sd != "" {
 544  			return r, errors.CallErr{
 545  				Req:  req,
 546  				Resp: resp,
 547  				Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d:\n%s",
 548  					req.URL.String(),
 549  					req.Method,
 550  					resp.StatusCode,
 551  					sd),
 552  			}
 553  		}
 554  		return r, errors.CallErr{
 555  			Req:  req,
 556  			Resp: resp,
 557  			Err:  fmt.Errorf("http call(%s)(%s) error: reply status code was %d", req.URL.String(), req.Method, resp.StatusCode),
 558  		}
 559  	}
 560  
 561  	err = json.Unmarshal(responseBytes, &r)
 562  	if err != nil {
 563  		return r, errors.InvalidJsonErr{
 564  			Err: fmt.Errorf("error parsing the json error: %s", err),
 565  		}
 566  	}
 567  	r.GrantedScopes.Slice = append(r.GrantedScopes.Slice, resource)
 568  
 569  	return r, err
 570  }
 571  
 572  func createAppServiceAuthRequest(ctx context.Context, id ID, resource string) (*http.Request, error) {
 573  	identityEndpoint := os.Getenv(identityEndpointEnvVar)
 574  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, identityEndpoint, nil)
 575  	if err != nil {
 576  		return nil, err
 577  	}
 578  	req.Header.Set("X-IDENTITY-HEADER", os.Getenv(identityHeaderEnvVar))
 579  	q := req.URL.Query()
 580  	q.Set("api-version", appServiceAPIVersion)
 581  	q.Set("resource", resource)
 582  	switch t := id.(type) {
 583  	case UserAssignedClientID:
 584  		q.Set(miQueryParameterClientId, string(t))
 585  	case UserAssignedResourceID:
 586  		q.Set(miQueryParameterResourceId, string(t))
 587  	case UserAssignedObjectID:
 588  		q.Set(miQueryParameterObjectId, string(t))
 589  	case systemAssignedValue:
 590  	default:
 591  		return nil, fmt.Errorf("unsupported type %T", id)
 592  	}
 593  	req.URL.RawQuery = q.Encode()
 594  	return req, nil
 595  }
 596  
 597  func createIMDSAuthRequest(ctx context.Context, id ID, resource string) (*http.Request, error) {
 598  	msiEndpoint, err := url.Parse(imdsDefaultEndpoint)
 599  	if err != nil {
 600  		return nil, fmt.Errorf("couldn't parse %q: %s", imdsDefaultEndpoint, err)
 601  	}
 602  	msiParameters := msiEndpoint.Query()
 603  	msiParameters.Set(apiVersionQueryParameterName, imdsAPIVersion)
 604  	msiParameters.Set(resourceQueryParameterName, resource)
 605  
 606  	switch t := id.(type) {
 607  	case UserAssignedClientID:
 608  		msiParameters.Set(miQueryParameterClientId, string(t))
 609  	case UserAssignedResourceID:
 610  		msiParameters.Set(miQueryParameterResourceIdIMDS, string(t))
 611  	case UserAssignedObjectID:
 612  		msiParameters.Set(miQueryParameterObjectId, string(t))
 613  	case systemAssignedValue: // not adding anything
 614  	default:
 615  		return nil, fmt.Errorf("unsupported type %T", id)
 616  	}
 617  
 618  	msiEndpoint.RawQuery = msiParameters.Encode()
 619  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, msiEndpoint.String(), nil)
 620  	if err != nil {
 621  		return nil, fmt.Errorf("error creating http request %s", err)
 622  	}
 623  	req.Header.Set(metaHTTPHeaderName, "true")
 624  	return req, nil
 625  }
 626  
 627  func createAzureArcAuthRequest(ctx context.Context, resource string, key string) (*http.Request, error) {
 628  	identityEndpoint := os.Getenv(identityEndpointEnvVar)
 629  	if identityEndpoint == "" {
 630  		identityEndpoint = azureArcEndpoint
 631  	}
 632  	msiEndpoint, parseErr := url.Parse(identityEndpoint)
 633  
 634  	if parseErr != nil {
 635  		return nil, fmt.Errorf("couldn't parse %q: %s", identityEndpoint, parseErr)
 636  	}
 637  
 638  	msiParameters := msiEndpoint.Query()
 639  	msiParameters.Set(apiVersionQueryParameterName, azureArcAPIVersion)
 640  	msiParameters.Set(resourceQueryParameterName, resource)
 641  
 642  	msiEndpoint.RawQuery = msiParameters.Encode()
 643  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, msiEndpoint.String(), nil)
 644  	if err != nil {
 645  		return nil, fmt.Errorf("error creating http request %s", err)
 646  	}
 647  	req.Header.Set(metaHTTPHeaderName, "true")
 648  
 649  	if key != "" {
 650  		req.Header.Set("Authorization", fmt.Sprintf("Basic %s", key))
 651  	}
 652  
 653  	return req, nil
 654  }
 655  
 656  func isAzureArcEnvironment(identityEndpoint, imdsEndpoint string) bool {
 657  	if identityEndpoint != "" && imdsEndpoint != "" {
 658  		return true
 659  	}
 660  	himdsFilePath := getAzureArcHimdsFilePath(runtime.GOOS)
 661  	if himdsFilePath != "" {
 662  		if _, err := os.Stat(himdsFilePath); err == nil {
 663  			return true
 664  		}
 665  	}
 666  	return false
 667  }
 668  
 669  func (c *Client) getAzureArcSecretKey(response *http.Response, platform string) (string, error) {
 670  	wwwAuthenticateHeader := response.Header.Get(wwwAuthenticateHeaderName)
 671  
 672  	if len(wwwAuthenticateHeader) == 0 {
 673  		return "", errors.New("response has no www-authenticate header")
 674  	}
 675  
 676  	// check if the platform is supported
 677  	expectedSecretFilePath := getAzureArcPlatformPath(platform)
 678  	if expectedSecretFilePath == "" {
 679  		return "", errors.New("platform not supported, expected linux or windows")
 680  	}
 681  
 682  	parts := strings.Split(wwwAuthenticateHeader, "Basic realm=")
 683  	if len(parts) < 2 {
 684  		return "", fmt.Errorf("basic realm= not found in the string, instead found: %s", wwwAuthenticateHeader)
 685  	}
 686  
 687  	secretFilePath := parts
 688  
 689  	// check that the file in the file path is a .key file
 690  	fileName := filepath.Base(secretFilePath[1])
 691  	if !strings.HasSuffix(fileName, azureArcFileExtension) {
 692  		return "", fmt.Errorf("invalid file extension, expected %s, got %s", azureArcFileExtension, filepath.Ext(fileName))
 693  	}
 694  
 695  	// check that file path from header matches the expected file path for the platform
 696  	if expectedSecretFilePath != filepath.Dir(secretFilePath[1]) {
 697  		return "", fmt.Errorf("invalid file path, expected %s, got %s", expectedSecretFilePath, filepath.Dir(secretFilePath[1]))
 698  	}
 699  
 700  	fileInfo, err := os.Stat(secretFilePath[1])
 701  	if err != nil {
 702  		return "", fmt.Errorf("failed to get metadata for %s due to error: %s", secretFilePath[1], err)
 703  	}
 704  
 705  	// Throw an error if the secret file's size is greater than 4096 bytes
 706  	if s := fileInfo.Size(); s > azureArcMaxFileSizeBytes {
 707  		return "", fmt.Errorf("invalid secret file size, expected %d, file size was %d", azureArcMaxFileSizeBytes, s)
 708  	}
 709  
 710  	// Attempt to read the contents of the secret file
 711  	secret, err := os.ReadFile(secretFilePath[1])
 712  	if err != nil {
 713  		return "", fmt.Errorf("failed to read %q due to error: %s", secretFilePath[1], err)
 714  	}
 715  
 716  	return string(secret), nil
 717  }
 718