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 "fmt"
12 "os"
13 "strings"
14 15 "github.com/Azure/azure-sdk-for-go/sdk/azcore"
16 "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
17 "github.com/Azure/azure-sdk-for-go/sdk/internal/log"
18 )
19 20 const azureTokenCredentials = "AZURE_TOKEN_CREDENTIALS"
21 22 // bit flags NewDefaultAzureCredential uses to parse AZURE_TOKEN_CREDENTIALS
23 const (
24 env = uint8(1) << iota
25 workloadIdentity
26 managedIdentity
27 az
28 azd
29 azurePowerShell
30 )
31 32 // DefaultAzureCredentialOptions contains optional parameters for DefaultAzureCredential.
33 // These options may not apply to all credentials in the chain.
34 type DefaultAzureCredentialOptions struct {
35 // ClientOptions has additional options for credentials that use an Azure SDK HTTP pipeline. These options don't apply
36 // to credential types that authenticate via external tools such as the Azure CLI.
37 azcore.ClientOptions
38 39 // AdditionallyAllowedTenants specifies tenants to which the credential may authenticate, in addition to
40 // TenantID. When TenantID is empty, this option has no effect and the credential will authenticate to
41 // any requested tenant. Add the wildcard value "*" to allow the credential to authenticate to any tenant.
42 // This value can also be set as a semicolon delimited list of tenants in the environment variable
43 // AZURE_ADDITIONALLY_ALLOWED_TENANTS.
44 AdditionallyAllowedTenants []string
45 46 // DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
47 // private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
48 // from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
49 // the application responsible for ensuring the configured authority is valid and trustworthy.
50 DisableInstanceDiscovery bool
51 52 // RequireAzureTokenCredentials determines whether NewDefaultAzureCredential returns an error when the environment
53 // variable AZURE_TOKEN_CREDENTIALS has no value.
54 RequireAzureTokenCredentials bool
55 56 // TenantID sets the default tenant for authentication via the Azure CLI, Azure Developer CLI, and workload identity.
57 TenantID string
58 }
59 60 // DefaultAzureCredential simplifies authentication while developing applications that deploy to Azure by
61 // combining credentials used in Azure hosting environments and credentials used in local development. In
62 // production, it's better to use a specific credential type so authentication is more predictable and easier
63 // to debug. For more information, see [DefaultAzureCredential overview].
64 //
65 // DefaultAzureCredential attempts to authenticate with each of these credential types, in the following order,
66 // stopping when one provides a token:
67 //
68 // - [EnvironmentCredential]
69 // - [WorkloadIdentityCredential], if environment variable configuration is set by the Azure workload
70 // identity webhook. Use [WorkloadIdentityCredential] directly when not using the webhook or needing
71 // more control over its configuration.
72 // - [ManagedIdentityCredential]
73 // - [AzureCLICredential]
74 // - [AzureDeveloperCLICredential]
75 // - [AzurePowerShellCredential]
76 //
77 // Consult the documentation for these credential types for more information on how they authenticate.
78 // Once a credential has successfully authenticated, DefaultAzureCredential will use that credential for
79 // every subsequent authentication.
80 //
81 // # Selecting credentials
82 //
83 // Set environment variable AZURE_TOKEN_CREDENTIALS to select a subset of the credential chain described above.
84 // DefaultAzureCredential will try only the specified credential(s), but its other behavior remains the same.
85 // Valid values for AZURE_TOKEN_CREDENTIALS are the name of any single type in the above chain, for example
86 // "EnvironmentCredential" or "AzureCLICredential", and these special values:
87 //
88 // - "dev": try [AzureCLICredential], [AzureDeveloperCLICredential], and [AzurePowerShellCredential], in that order
89 // - "prod": try [EnvironmentCredential], [WorkloadIdentityCredential], and [ManagedIdentityCredential], in that order
90 //
91 // [DefaultAzureCredentialOptions].RequireAzureTokenCredentials controls whether AZURE_TOKEN_CREDENTIALS must be set.
92 // NewDefaultAzureCredential returns an error when RequireAzureTokenCredentials is true and AZURE_TOKEN_CREDENTIALS
93 // has no value.
94 //
95 // [DefaultAzureCredential overview]: https://aka.ms/azsdk/go/identity/credential-chains#defaultazurecredential-overview
96 type DefaultAzureCredential struct {
97 chain *ChainedTokenCredential
98 }
99 100 // NewDefaultAzureCredential creates a DefaultAzureCredential. Pass nil for options to accept defaults.
101 func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*DefaultAzureCredential, error) {
102 if options == nil {
103 options = &DefaultAzureCredentialOptions{}
104 }
105 106 var (
107 creds []azcore.TokenCredential
108 errorMessages []string
109 selected = env | workloadIdentity | managedIdentity | az | azd | azurePowerShell
110 )
111 112 if atc, ok := os.LookupEnv(azureTokenCredentials); ok {
113 switch {
114 case atc == "dev":
115 selected = az | azd | azurePowerShell
116 case atc == "prod":
117 selected = env | workloadIdentity | managedIdentity
118 case strings.EqualFold(atc, credNameEnvironment):
119 selected = env
120 case strings.EqualFold(atc, credNameWorkloadIdentity):
121 selected = workloadIdentity
122 case strings.EqualFold(atc, credNameManagedIdentity):
123 selected = managedIdentity
124 case strings.EqualFold(atc, credNameAzureCLI):
125 selected = az
126 case strings.EqualFold(atc, credNameAzureDeveloperCLI):
127 selected = azd
128 case strings.EqualFold(atc, credNameAzurePowerShell):
129 selected = azurePowerShell
130 default:
131 return nil, fmt.Errorf(`invalid %s value %q. Valid values are "dev", "prod", or the name of any credential type in the default chain. See https://aka.ms/azsdk/go/identity/docs#DefaultAzureCredential for more information`, azureTokenCredentials, atc)
132 }
133 } else if options.RequireAzureTokenCredentials {
134 return nil, fmt.Errorf("%s must be set when RequireAzureTokenCredentials is true. See https://aka.ms/azsdk/go/identity/docs#DefaultAzureCredential for more information", azureTokenCredentials)
135 }
136 137 additionalTenants := options.AdditionallyAllowedTenants
138 if len(additionalTenants) == 0 {
139 if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
140 additionalTenants = strings.Split(tenants, ";")
141 }
142 }
143 if selected&env != 0 {
144 envCred, err := NewEnvironmentCredential(&EnvironmentCredentialOptions{
145 ClientOptions: options.ClientOptions,
146 DisableInstanceDiscovery: options.DisableInstanceDiscovery,
147 additionallyAllowedTenants: additionalTenants,
148 })
149 if err == nil {
150 creds = append(creds, envCred)
151 } else {
152 errorMessages = append(errorMessages, "EnvironmentCredential: "+err.Error())
153 creds = append(creds, &defaultCredentialErrorReporter{credType: credNameEnvironment, err: err})
154 }
155 }
156 if selected&workloadIdentity != 0 {
157 wic, err := NewWorkloadIdentityCredential(&WorkloadIdentityCredentialOptions{
158 AdditionallyAllowedTenants: additionalTenants,
159 ClientOptions: options.ClientOptions,
160 DisableInstanceDiscovery: options.DisableInstanceDiscovery,
161 TenantID: options.TenantID,
162 })
163 if err == nil {
164 creds = append(creds, wic)
165 } else {
166 errorMessages = append(errorMessages, credNameWorkloadIdentity+": "+err.Error())
167 creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err})
168 }
169 }
170 if selected&managedIdentity != 0 {
171 o := &ManagedIdentityCredentialOptions{
172 ClientOptions: options.ClientOptions,
173 // enable special DefaultAzureCredential behavior (IMDS probing) only when the chain contains another credential
174 dac: selected^managedIdentity != 0,
175 }
176 if ID, ok := os.LookupEnv(azureClientID); ok {
177 o.ID = ClientID(ID)
178 }
179 miCred, err := NewManagedIdentityCredential(o)
180 if err == nil {
181 creds = append(creds, miCred)
182 } else {
183 errorMessages = append(errorMessages, credNameManagedIdentity+": "+err.Error())
184 creds = append(creds, &defaultCredentialErrorReporter{credType: credNameManagedIdentity, err: err})
185 }
186 }
187 if selected&az != 0 {
188 azCred, err := NewAzureCLICredential(&AzureCLICredentialOptions{
189 AdditionallyAllowedTenants: additionalTenants,
190 TenantID: options.TenantID,
191 inDefaultChain: true,
192 })
193 if err == nil {
194 creds = append(creds, azCred)
195 } else {
196 errorMessages = append(errorMessages, credNameAzureCLI+": "+err.Error())
197 creds = append(creds, &defaultCredentialErrorReporter{credType: credNameAzureCLI, err: err})
198 }
199 }
200 if selected&azd != 0 {
201 azdCred, err := NewAzureDeveloperCLICredential(&AzureDeveloperCLICredentialOptions{
202 AdditionallyAllowedTenants: additionalTenants,
203 TenantID: options.TenantID,
204 inDefaultChain: true,
205 })
206 if err == nil {
207 creds = append(creds, azdCred)
208 } else {
209 errorMessages = append(errorMessages, credNameAzureDeveloperCLI+": "+err.Error())
210 creds = append(creds, &defaultCredentialErrorReporter{credType: credNameAzureDeveloperCLI, err: err})
211 }
212 }
213 if selected&azurePowerShell != 0 {
214 azurePowerShellCred, err := NewAzurePowerShellCredential(&AzurePowerShellCredentialOptions{
215 AdditionallyAllowedTenants: additionalTenants,
216 TenantID: options.TenantID,
217 inDefaultChain: true,
218 })
219 if err == nil {
220 creds = append(creds, azurePowerShellCred)
221 } else {
222 errorMessages = append(errorMessages, credNameAzurePowerShell+": "+err.Error())
223 creds = append(creds, &defaultCredentialErrorReporter{credType: credNameAzurePowerShell, err: err})
224 }
225 }
226 227 if len(errorMessages) > 0 {
228 log.Writef(EventAuthentication, "NewDefaultAzureCredential failed to initialize some credentials:\n\t%s", strings.Join(errorMessages, "\n\t"))
229 }
230 231 chain, err := NewChainedTokenCredential(creds, nil)
232 if err != nil {
233 return nil, err
234 }
235 chain.name = "DefaultAzureCredential"
236 return &DefaultAzureCredential{chain: chain}, nil
237 }
238 239 // GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
240 func (c *DefaultAzureCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
241 return c.chain.GetToken(ctx, opts)
242 }
243 244 var _ azcore.TokenCredential = (*DefaultAzureCredential)(nil)
245 246 // defaultCredentialErrorReporter is a substitute for credentials that couldn't be constructed.
247 // Its GetToken method always returns a credentialUnavailableError having the same message as
248 // the error that prevented constructing the credential. This ensures the message is present
249 // in the error returned by ChainedTokenCredential.GetToken()
250 type defaultCredentialErrorReporter struct {
251 credType string
252 err error
253 }
254 255 func (d *defaultCredentialErrorReporter) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
256 if _, ok := d.err.(credentialUnavailable); ok {
257 return azcore.AccessToken{}, d.err
258 }
259 return azcore.AccessToken{}, newCredentialUnavailableError(d.credType, d.err.Error())
260 }
261 262 var _ azcore.TokenCredential = (*defaultCredentialErrorReporter)(nil)
263