workload_identity.go raw

   1  //go:build go1.18
   2  // +build go1.18
   3  
   4  // Copyright (c) Microsoft Corporation. All rights reserved.
   5  // Licensed under the MIT License.
   6  
   7  package azidentity
   8  
   9  import (
  10  	"context"
  11  	"errors"
  12  	"os"
  13  	"sync"
  14  	"time"
  15  
  16  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
  17  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
  18  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
  19  )
  20  
  21  const credNameWorkloadIdentity = "WorkloadIdentityCredential"
  22  
  23  // WorkloadIdentityCredential supports Azure workload identity on Kubernetes.
  24  // See [Azure Kubernetes Service documentation] for more information.
  25  //
  26  // [Azure Kubernetes Service documentation]: https://learn.microsoft.com/azure/aks/workload-identity-overview
  27  type WorkloadIdentityCredential struct {
  28  	assertion, file string
  29  	cred            *ClientAssertionCredential
  30  	expires         time.Time
  31  	mtx             *sync.RWMutex
  32  }
  33  
  34  // WorkloadIdentityCredentialOptions contains optional parameters for WorkloadIdentityCredential.
  35  type WorkloadIdentityCredentialOptions struct {
  36  	azcore.ClientOptions
  37  
  38  	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
  39  	// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
  40  	// application is registered.
  41  	AdditionallyAllowedTenants []string
  42  
  43  	// Cache is a persistent cache the credential will use to store the tokens it acquires, making
  44  	// them available to other processes and credential instances. The default, zero value means the
  45  	// credential will store tokens in memory and not share them with any other credential instance.
  46  	Cache Cache
  47  
  48  	// ClientID of the service principal. Defaults to the value of the environment variable AZURE_CLIENT_ID.
  49  	ClientID string
  50  
  51  	// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
  52  	// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
  53  	// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
  54  	// the application responsible for ensuring the configured authority is valid and trustworthy.
  55  	DisableInstanceDiscovery bool
  56  
  57  	// TenantID of the service principal. Defaults to the value of the environment variable AZURE_TENANT_ID.
  58  	TenantID string
  59  
  60  	// TokenFilePath is the path of a file containing a Kubernetes service account token. Defaults to the value of the
  61  	// environment variable AZURE_FEDERATED_TOKEN_FILE.
  62  	TokenFilePath string
  63  }
  64  
  65  // NewWorkloadIdentityCredential constructs a WorkloadIdentityCredential. Service principal configuration is read
  66  // from environment variables as set by the Azure workload identity webhook. Set options to override those values.
  67  func NewWorkloadIdentityCredential(options *WorkloadIdentityCredentialOptions) (*WorkloadIdentityCredential, error) {
  68  	if options == nil {
  69  		options = &WorkloadIdentityCredentialOptions{}
  70  	}
  71  	ok := false
  72  	clientID := options.ClientID
  73  	if clientID == "" {
  74  		if clientID, ok = os.LookupEnv(azureClientID); !ok {
  75  			return nil, errors.New("no client ID specified. Check pod configuration or set ClientID in the options")
  76  		}
  77  	}
  78  	file := options.TokenFilePath
  79  	if file == "" {
  80  		if file, ok = os.LookupEnv(azureFederatedTokenFile); !ok {
  81  			return nil, errors.New("no token file specified. Check pod configuration or set TokenFilePath in the options")
  82  		}
  83  	}
  84  	tenantID := options.TenantID
  85  	if tenantID == "" {
  86  		if tenantID, ok = os.LookupEnv(azureTenantID); !ok {
  87  			return nil, errors.New("no tenant ID specified. Check pod configuration or set TenantID in the options")
  88  		}
  89  	}
  90  	w := WorkloadIdentityCredential{file: file, mtx: &sync.RWMutex{}}
  91  	caco := ClientAssertionCredentialOptions{
  92  		AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
  93  		Cache:                      options.Cache,
  94  		ClientOptions:              options.ClientOptions,
  95  		DisableInstanceDiscovery:   options.DisableInstanceDiscovery,
  96  	}
  97  	cred, err := NewClientAssertionCredential(tenantID, clientID, w.getAssertion, &caco)
  98  	if err != nil {
  99  		return nil, err
 100  	}
 101  	// we want "WorkloadIdentityCredential" in log messages, not "ClientAssertionCredential"
 102  	cred.client.name = credNameWorkloadIdentity
 103  	w.cred = cred
 104  	return &w, nil
 105  }
 106  
 107  // GetToken requests an access token from Microsoft Entra ID. Azure SDK clients call this method automatically.
 108  func (w *WorkloadIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
 109  	var err error
 110  	ctx, endSpan := runtime.StartSpan(ctx, credNameWorkloadIdentity+"."+traceOpGetToken, w.cred.client.azClient.Tracer(), nil)
 111  	defer func() { endSpan(err) }()
 112  	tk, err := w.cred.GetToken(ctx, opts)
 113  	return tk, err
 114  }
 115  
 116  // getAssertion returns the specified file's content, which is expected to be a Kubernetes service account token.
 117  // Kubernetes is responsible for updating the file as service account tokens expire.
 118  func (w *WorkloadIdentityCredential) getAssertion(context.Context) (string, error) {
 119  	w.mtx.RLock()
 120  	if w.expires.Before(time.Now()) {
 121  		// ensure only one goroutine at a time updates the assertion
 122  		w.mtx.RUnlock()
 123  		w.mtx.Lock()
 124  		defer w.mtx.Unlock()
 125  		// double check because another goroutine may have acquired the write lock first and done the update
 126  		if now := time.Now(); w.expires.Before(now) {
 127  			content, err := os.ReadFile(w.file)
 128  			if err != nil {
 129  				return "", err
 130  			}
 131  			w.assertion = string(content)
 132  			// Kubernetes rotates service account tokens when they reach 80% of their total TTL. The shortest TTL
 133  			// is 1 hour. That implies the token we just read is valid for at least 12 minutes (20% of 1 hour),
 134  			// but we add some margin for safety.
 135  			w.expires = now.Add(10 * time.Minute)
 136  		}
 137  	} else {
 138  		defer w.mtx.RUnlock()
 139  	}
 140  	return w.assertion, nil
 141  }
 142