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