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