azidentity.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  	"bytes"
  11  	"context"
  12  	"errors"
  13  	"fmt"
  14  	"io"
  15  	"net/http"
  16  	"net/url"
  17  	"os"
  18  
  19  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
  20  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
  21  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
  22  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
  23  	"github.com/Azure/azure-sdk-for-go/sdk/azidentity/internal"
  24  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
  25  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/managedidentity"
  26  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
  27  )
  28  
  29  const (
  30  	azureAdditionallyAllowedTenants = "AZURE_ADDITIONALLY_ALLOWED_TENANTS"
  31  	azureAuthorityHost              = "AZURE_AUTHORITY_HOST"
  32  	azureClientCertificatePassword  = "AZURE_CLIENT_CERTIFICATE_PASSWORD"
  33  	azureClientCertificatePath      = "AZURE_CLIENT_CERTIFICATE_PATH"
  34  	azureClientID                   = "AZURE_CLIENT_ID"
  35  	azureClientSecret               = "AZURE_CLIENT_SECRET"
  36  	azureFederatedTokenFile         = "AZURE_FEDERATED_TOKEN_FILE"
  37  	azurePassword                   = "AZURE_PASSWORD"
  38  	azureRegionalAuthorityName      = "AZURE_REGIONAL_AUTHORITY_NAME"
  39  	azureTenantID                   = "AZURE_TENANT_ID"
  40  	azureUsername                   = "AZURE_USERNAME"
  41  
  42  	organizationsTenantID   = "organizations"
  43  	developerSignOnClientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
  44  	defaultSuffix           = "/.default"
  45  
  46  	scopeLogFmt = "%s.GetToken() acquired a token for scope %q"
  47  
  48  	traceNamespace      = "Microsoft.Entra"
  49  	traceOpGetToken     = "GetToken"
  50  	traceOpAuthenticate = "Authenticate"
  51  )
  52  
  53  var (
  54  	// capability CP1 indicates the client application is capable of handling CAE claims challenges
  55  	cp1                = []string{"CP1"}
  56  	errInvalidTenantID = errors.New("invalid tenantID. You can locate your tenantID by following the instructions listed here: https://learn.microsoft.com/partner-center/find-ids-and-domain-names")
  57  )
  58  
  59  // Cache represents a persistent cache that makes authentication data available across processes.
  60  // Construct one with [github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache.New]. This package's
  61  // [persistent user authentication example] shows how to use a persistent cache to reuse user
  62  // logins across application runs. For service principal credential types such as
  63  // [ClientCertificateCredential], simply set the Cache field on the credential options.
  64  //
  65  // [persistent user authentication example]: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#example-package-PersistentUserAuthentication
  66  type Cache = internal.Cache
  67  
  68  // setAuthorityHost initializes the authority host for credentials. Precedence is:
  69  //  1. cloud.Configuration.ActiveDirectoryAuthorityHost value set by user
  70  //  2. value of AZURE_AUTHORITY_HOST
  71  //  3. default: Azure Public Cloud
  72  func setAuthorityHost(cc cloud.Configuration) (string, error) {
  73  	host := cc.ActiveDirectoryAuthorityHost
  74  	if host == "" {
  75  		if len(cc.Services) > 0 {
  76  			return "", errors.New("missing ActiveDirectoryAuthorityHost for specified cloud")
  77  		}
  78  		host = cloud.AzurePublic.ActiveDirectoryAuthorityHost
  79  		if envAuthorityHost := os.Getenv(azureAuthorityHost); envAuthorityHost != "" {
  80  			host = envAuthorityHost
  81  		}
  82  	}
  83  	u, err := url.Parse(host)
  84  	if err != nil {
  85  		return "", err
  86  	}
  87  	if u.Scheme != "https" {
  88  		return "", errors.New("cannot use an authority host without https")
  89  	}
  90  	return host, nil
  91  }
  92  
  93  // resolveAdditionalTenants returns a copy of tenants, simplified when tenants contains a wildcard
  94  func resolveAdditionalTenants(tenants []string) []string {
  95  	if len(tenants) == 0 {
  96  		return nil
  97  	}
  98  	for _, t := range tenants {
  99  		// a wildcard makes all other values redundant
 100  		if t == "*" {
 101  			return []string{"*"}
 102  		}
 103  	}
 104  	cp := make([]string, len(tenants))
 105  	copy(cp, tenants)
 106  	return cp
 107  }
 108  
 109  // resolveTenant returns the correct tenant for a token request, or "" when the calling credential doesn't
 110  // have an explicitly configured tenant and the caller didn't specify a tenant for the token request.
 111  //
 112  //   - defaultTenant: tenant set when constructing the credential, if any. "" is valid for credentials
 113  //     having an optional or implicit tenant such as dev tool and interactive user credentials. Those
 114  //     default to the tool's configured tenant or the user's home tenant, respectively.
 115  //   - specified: tenant specified for this token request i.e., TokenRequestOptions.TenantID. May be "".
 116  //   - credName: name of the calling credential type; for error messages
 117  //   - additionalTenants: optional allow list of tenants the credential may acquire tokens from in
 118  //     addition to defaultTenant i.e., the credential's AdditionallyAllowedTenants option
 119  func resolveTenant(defaultTenant, specified, credName string, additionalTenants []string) (string, error) {
 120  	if specified == "" || specified == defaultTenant {
 121  		return defaultTenant, nil
 122  	}
 123  	if defaultTenant == "adfs" {
 124  		return "", errors.New("ADFS doesn't support tenants")
 125  	}
 126  	if !validTenantID(specified) {
 127  		return "", errInvalidTenantID
 128  	}
 129  	for _, t := range additionalTenants {
 130  		if t == "*" || t == specified {
 131  			return specified, nil
 132  		}
 133  	}
 134  	if len(additionalTenants) == 0 {
 135  		switch defaultTenant {
 136  		case "", organizationsTenantID:
 137  			// The application didn't specify a tenant or allow list when constructing the credential. Allow the
 138  			// tenant specified for this token request because we have nothing to compare it to (i.e., it vacuously
 139  			// satisfies the credential's configuration); don't know whether the application is multitenant; and
 140  			// don't want to return an error in the common case that the specified tenant matches the credential's
 141  			// default tenant determined elsewhere e.g., in some dev tool's configuration.
 142  			return specified, nil
 143  		}
 144  	}
 145  	return "", fmt.Errorf(`%s isn't configured to acquire tokens for tenant %q. To enable acquiring tokens for this tenant add it to the AdditionallyAllowedTenants on the credential options, or add "*" to allow acquiring tokens for any tenant`, credName, specified)
 146  }
 147  
 148  func alphanumeric(r rune) bool {
 149  	return ('0' <= r && r <= '9') || ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z')
 150  }
 151  
 152  func validTenantID(tenantID string) bool {
 153  	if len(tenantID) < 1 {
 154  		return false
 155  	}
 156  	for _, r := range tenantID {
 157  		if !(alphanumeric(r) || r == '.' || r == '-') {
 158  			return false
 159  		}
 160  	}
 161  	return true
 162  }
 163  
 164  func doForClient(client *azcore.Client, r *http.Request) (*http.Response, error) {
 165  	req, err := runtime.NewRequest(r.Context(), r.Method, r.URL.String())
 166  	if err != nil {
 167  		return nil, err
 168  	}
 169  	if r.Body != nil && r.Body != http.NoBody {
 170  		// create a rewindable body from the existing body as required
 171  		var body io.ReadSeekCloser
 172  		if rsc, ok := r.Body.(io.ReadSeekCloser); ok {
 173  			body = rsc
 174  		} else {
 175  			b, err := io.ReadAll(r.Body)
 176  			if err != nil {
 177  				return nil, err
 178  			}
 179  			body = streaming.NopCloser(bytes.NewReader(b))
 180  		}
 181  		err = req.SetBody(body, r.Header.Get("Content-Type"))
 182  		if err != nil {
 183  			return nil, err
 184  		}
 185  	}
 186  
 187  	// copy headers to the new request, ignoring any for which the new request has a value
 188  	h := req.Raw().Header
 189  	for key, vals := range r.Header {
 190  		if _, has := h[key]; !has {
 191  			for _, val := range vals {
 192  				h.Add(key, val)
 193  			}
 194  		}
 195  	}
 196  
 197  	resp, err := client.Pipeline().Do(req)
 198  	if err != nil {
 199  		return nil, err
 200  	}
 201  	return resp, err
 202  }
 203  
 204  // enables fakes for test scenarios
 205  type msalConfidentialClient interface {
 206  	AcquireTokenSilent(ctx context.Context, scopes []string, options ...confidential.AcquireSilentOption) (confidential.AuthResult, error)
 207  	AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...confidential.AcquireByAuthCodeOption) (confidential.AuthResult, error)
 208  	AcquireTokenByCredential(ctx context.Context, scopes []string, options ...confidential.AcquireByCredentialOption) (confidential.AuthResult, error)
 209  	AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string, options ...confidential.AcquireOnBehalfOfOption) (confidential.AuthResult, error)
 210  }
 211  
 212  type msalManagedIdentityClient interface {
 213  	AcquireToken(context.Context, string, ...managedidentity.AcquireTokenOption) (managedidentity.AuthResult, error)
 214  }
 215  
 216  // enables fakes for test scenarios
 217  type msalPublicClient interface {
 218  	AcquireTokenSilent(ctx context.Context, scopes []string, options ...public.AcquireSilentOption) (public.AuthResult, error)
 219  	AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username string, password string, options ...public.AcquireByUsernamePasswordOption) (public.AuthResult, error)
 220  	AcquireTokenByDeviceCode(ctx context.Context, scopes []string, options ...public.AcquireByDeviceCodeOption) (public.DeviceCode, error)
 221  	AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...public.AcquireByAuthCodeOption) (public.AuthResult, error)
 222  	AcquireTokenInteractive(ctx context.Context, scopes []string, options ...public.AcquireInteractiveOption) (public.AuthResult, error)
 223  }
 224