environment_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  	"errors"
  12  	"fmt"
  13  	"os"
  14  	"strings"
  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/internal/log"
  19  )
  20  
  21  const (
  22  	credNameEnvironment = "EnvironmentCredential"
  23  	envVarSendCertChain = "AZURE_CLIENT_SEND_CERTIFICATE_CHAIN"
  24  )
  25  
  26  // EnvironmentCredentialOptions contains optional parameters for EnvironmentCredential
  27  type EnvironmentCredentialOptions struct {
  28  	azcore.ClientOptions
  29  
  30  	// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
  31  	// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
  32  	// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
  33  	// the application responsible for ensuring the configured authority is valid and trustworthy.
  34  	DisableInstanceDiscovery bool
  35  	// additionallyAllowedTenants is used only by NewDefaultAzureCredential() to enable that constructor's explicit
  36  	// option to override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS. Applications using EnvironmentCredential
  37  	// directly should set that variable instead. This field should remain unexported to preserve this credential's
  38  	// unambiguous "all configuration from environment variables" design.
  39  	additionallyAllowedTenants []string
  40  }
  41  
  42  // EnvironmentCredential authenticates a service principal with a secret or certificate, or a user with a password, depending
  43  // on environment variable configuration. It reads configuration from these variables, in the following order:
  44  //
  45  // # Service principal with client secret
  46  //
  47  // AZURE_TENANT_ID: ID of the service principal's tenant. Also called its "directory" ID.
  48  //
  49  // AZURE_CLIENT_ID: the service principal's client ID
  50  //
  51  // AZURE_CLIENT_SECRET: one of the service principal's client secrets
  52  //
  53  // # Service principal with certificate
  54  //
  55  // AZURE_TENANT_ID: ID of the service principal's tenant. Also called its "directory" ID.
  56  //
  57  // AZURE_CLIENT_ID: the service principal's client ID
  58  //
  59  // AZURE_CLIENT_CERTIFICATE_PATH: path to a PEM or PKCS12 certificate file including the private key.
  60  //
  61  // AZURE_CLIENT_CERTIFICATE_PASSWORD: (optional) password for the certificate file.
  62  //
  63  // Note that this credential uses [ParseCertificates] to load the certificate and key from the file. If this
  64  // function isn't able to parse your certificate, use [ClientCertificateCredential] instead.
  65  //
  66  // # Configuration for multitenant applications
  67  //
  68  // To enable multitenant authentication, set AZURE_ADDITIONALLY_ALLOWED_TENANTS with a semicolon delimited list of tenants
  69  // the credential may request tokens from in addition to the tenant specified by AZURE_TENANT_ID. Set
  70  // AZURE_ADDITIONALLY_ALLOWED_TENANTS to "*" to enable the credential to request a token from any tenant.
  71  //
  72  // [Entra ID documentation]: https://aka.ms/azsdk/identity/mfa
  73  type EnvironmentCredential struct {
  74  	cred azcore.TokenCredential
  75  }
  76  
  77  // NewEnvironmentCredential creates an EnvironmentCredential. Pass nil to accept default options.
  78  func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*EnvironmentCredential, error) {
  79  	if options == nil {
  80  		options = &EnvironmentCredentialOptions{}
  81  	}
  82  	tenantID := os.Getenv(azureTenantID)
  83  	if tenantID == "" {
  84  		return nil, errors.New("missing environment variable AZURE_TENANT_ID")
  85  	}
  86  	clientID := os.Getenv(azureClientID)
  87  	if clientID == "" {
  88  		return nil, errors.New("missing environment variable " + azureClientID)
  89  	}
  90  	// tenants set by NewDefaultAzureCredential() override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS
  91  	additionalTenants := options.additionallyAllowedTenants
  92  	if len(additionalTenants) == 0 {
  93  		if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
  94  			additionalTenants = strings.Split(tenants, ";")
  95  		}
  96  	}
  97  	if clientSecret := os.Getenv(azureClientSecret); clientSecret != "" {
  98  		log.Write(EventAuthentication, "EnvironmentCredential will authenticate with ClientSecretCredential")
  99  		o := &ClientSecretCredentialOptions{
 100  			AdditionallyAllowedTenants: additionalTenants,
 101  			ClientOptions:              options.ClientOptions,
 102  			DisableInstanceDiscovery:   options.DisableInstanceDiscovery,
 103  		}
 104  		cred, err := NewClientSecretCredential(tenantID, clientID, clientSecret, o)
 105  		if err != nil {
 106  			return nil, err
 107  		}
 108  		return &EnvironmentCredential{cred: cred}, nil
 109  	}
 110  	if certPath := os.Getenv(azureClientCertificatePath); certPath != "" {
 111  		log.Write(EventAuthentication, "EnvironmentCredential will authenticate with ClientCertificateCredential")
 112  		certData, err := os.ReadFile(certPath)
 113  		if err != nil {
 114  			return nil, fmt.Errorf(`failed to read certificate file "%s": %v`, certPath, err)
 115  		}
 116  		var password []byte
 117  		if v := os.Getenv(azureClientCertificatePassword); v != "" {
 118  			password = []byte(v)
 119  		}
 120  		certs, key, err := ParseCertificates(certData, password)
 121  		if err != nil {
 122  			return nil, fmt.Errorf("failed to parse %q due to error %q. This may be due to a limitation of this module's certificate loader. Consider calling NewClientCertificateCredential instead", certPath, err.Error())
 123  		}
 124  		o := &ClientCertificateCredentialOptions{
 125  			AdditionallyAllowedTenants: additionalTenants,
 126  			ClientOptions:              options.ClientOptions,
 127  			DisableInstanceDiscovery:   options.DisableInstanceDiscovery,
 128  		}
 129  		if v, ok := os.LookupEnv(envVarSendCertChain); ok {
 130  			o.SendCertificateChain = v == "1" || strings.ToLower(v) == "true"
 131  		}
 132  		cred, err := NewClientCertificateCredential(tenantID, clientID, certs, key, o)
 133  		if err != nil {
 134  			return nil, err
 135  		}
 136  		return &EnvironmentCredential{cred: cred}, nil
 137  	}
 138  	if username := os.Getenv(azureUsername); username != "" {
 139  		if password := os.Getenv(azurePassword); password != "" {
 140  			log.Write(EventAuthentication, "EnvironmentCredential will authenticate with UsernamePasswordCredential")
 141  			o := &UsernamePasswordCredentialOptions{
 142  				AdditionallyAllowedTenants: additionalTenants,
 143  				ClientOptions:              options.ClientOptions,
 144  				DisableInstanceDiscovery:   options.DisableInstanceDiscovery,
 145  			}
 146  			cred, err := NewUsernamePasswordCredential(tenantID, clientID, username, password, o)
 147  			if err != nil {
 148  				return nil, err
 149  			}
 150  			return &EnvironmentCredential{cred: cred}, nil
 151  		}
 152  		return nil, errors.New("no value for AZURE_PASSWORD")
 153  	}
 154  	return nil, errors.New("incomplete environment variable configuration. Only AZURE_TENANT_ID and AZURE_CLIENT_ID are set")
 155  }
 156  
 157  // GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
 158  func (c *EnvironmentCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
 159  	return c.cred.GetToken(ctx, opts)
 160  }
 161  
 162  var _ azcore.TokenCredential = (*EnvironmentCredential)(nil)
 163