default_azure_credential.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  	"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