azidentity.go

  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
  7package azidentity
  8
  9import (
 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/public"
 26)
 27
 28const (
 29	azureAdditionallyAllowedTenants = "AZURE_ADDITIONALLY_ALLOWED_TENANTS"
 30	azureAuthorityHost              = "AZURE_AUTHORITY_HOST"
 31	azureClientCertificatePassword  = "AZURE_CLIENT_CERTIFICATE_PASSWORD"
 32	azureClientCertificatePath      = "AZURE_CLIENT_CERTIFICATE_PATH"
 33	azureClientID                   = "AZURE_CLIENT_ID"
 34	azureClientSecret               = "AZURE_CLIENT_SECRET"
 35	azureFederatedTokenFile         = "AZURE_FEDERATED_TOKEN_FILE"
 36	azurePassword                   = "AZURE_PASSWORD"
 37	azureRegionalAuthorityName      = "AZURE_REGIONAL_AUTHORITY_NAME"
 38	azureTenantID                   = "AZURE_TENANT_ID"
 39	azureUsername                   = "AZURE_USERNAME"
 40
 41	organizationsTenantID   = "organizations"
 42	developerSignOnClientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
 43	defaultSuffix           = "/.default"
 44
 45	traceNamespace      = "Microsoft.Entra"
 46	traceOpGetToken     = "GetToken"
 47	traceOpAuthenticate = "Authenticate"
 48)
 49
 50var (
 51	// capability CP1 indicates the client application is capable of handling CAE claims challenges
 52	cp1                = []string{"CP1"}
 53	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")
 54)
 55
 56// tokenCachePersistenceOptions contains options for persistent token caching
 57type tokenCachePersistenceOptions = internal.TokenCachePersistenceOptions
 58
 59// setAuthorityHost initializes the authority host for credentials. Precedence is:
 60//  1. cloud.Configuration.ActiveDirectoryAuthorityHost value set by user
 61//  2. value of AZURE_AUTHORITY_HOST
 62//  3. default: Azure Public Cloud
 63func setAuthorityHost(cc cloud.Configuration) (string, error) {
 64	host := cc.ActiveDirectoryAuthorityHost
 65	if host == "" {
 66		if len(cc.Services) > 0 {
 67			return "", errors.New("missing ActiveDirectoryAuthorityHost for specified cloud")
 68		}
 69		host = cloud.AzurePublic.ActiveDirectoryAuthorityHost
 70		if envAuthorityHost := os.Getenv(azureAuthorityHost); envAuthorityHost != "" {
 71			host = envAuthorityHost
 72		}
 73	}
 74	u, err := url.Parse(host)
 75	if err != nil {
 76		return "", err
 77	}
 78	if u.Scheme != "https" {
 79		return "", errors.New("cannot use an authority host without https")
 80	}
 81	return host, nil
 82}
 83
 84// resolveAdditionalTenants returns a copy of tenants, simplified when tenants contains a wildcard
 85func resolveAdditionalTenants(tenants []string) []string {
 86	if len(tenants) == 0 {
 87		return nil
 88	}
 89	for _, t := range tenants {
 90		// a wildcard makes all other values redundant
 91		if t == "*" {
 92			return []string{"*"}
 93		}
 94	}
 95	cp := make([]string, len(tenants))
 96	copy(cp, tenants)
 97	return cp
 98}
 99
100// resolveTenant returns the correct tenant for a token request
101func resolveTenant(defaultTenant, specified, credName string, additionalTenants []string) (string, error) {
102	if specified == "" || specified == defaultTenant {
103		return defaultTenant, nil
104	}
105	if defaultTenant == "adfs" {
106		return "", errors.New("ADFS doesn't support tenants")
107	}
108	if !validTenantID(specified) {
109		return "", errInvalidTenantID
110	}
111	for _, t := range additionalTenants {
112		if t == "*" || t == specified {
113			return specified, nil
114		}
115	}
116	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)
117}
118
119func alphanumeric(r rune) bool {
120	return ('0' <= r && r <= '9') || ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z')
121}
122
123func validTenantID(tenantID string) bool {
124	if len(tenantID) < 1 {
125		return false
126	}
127	for _, r := range tenantID {
128		if !(alphanumeric(r) || r == '.' || r == '-') {
129			return false
130		}
131	}
132	return true
133}
134
135func doForClient(client *azcore.Client, r *http.Request) (*http.Response, error) {
136	req, err := runtime.NewRequest(r.Context(), r.Method, r.URL.String())
137	if err != nil {
138		return nil, err
139	}
140	if r.Body != nil && r.Body != http.NoBody {
141		// create a rewindable body from the existing body as required
142		var body io.ReadSeekCloser
143		if rsc, ok := r.Body.(io.ReadSeekCloser); ok {
144			body = rsc
145		} else {
146			b, err := io.ReadAll(r.Body)
147			if err != nil {
148				return nil, err
149			}
150			body = streaming.NopCloser(bytes.NewReader(b))
151		}
152		err = req.SetBody(body, r.Header.Get("Content-Type"))
153		if err != nil {
154			return nil, err
155		}
156	}
157
158	// copy headers to the new request, ignoring any for which the new request has a value
159	h := req.Raw().Header
160	for key, vals := range r.Header {
161		if _, has := h[key]; !has {
162			for _, val := range vals {
163				h.Add(key, val)
164			}
165		}
166	}
167
168	resp, err := client.Pipeline().Do(req)
169	if err != nil {
170		return nil, err
171	}
172	return resp, err
173}
174
175// enables fakes for test scenarios
176type msalConfidentialClient interface {
177	AcquireTokenSilent(ctx context.Context, scopes []string, options ...confidential.AcquireSilentOption) (confidential.AuthResult, error)
178	AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...confidential.AcquireByAuthCodeOption) (confidential.AuthResult, error)
179	AcquireTokenByCredential(ctx context.Context, scopes []string, options ...confidential.AcquireByCredentialOption) (confidential.AuthResult, error)
180	AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string, options ...confidential.AcquireOnBehalfOfOption) (confidential.AuthResult, error)
181}
182
183// enables fakes for test scenarios
184type msalPublicClient interface {
185	AcquireTokenSilent(ctx context.Context, scopes []string, options ...public.AcquireSilentOption) (public.AuthResult, error)
186	AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username string, password string, options ...public.AcquireByUsernamePasswordOption) (public.AuthResult, error)
187	AcquireTokenByDeviceCode(ctx context.Context, scopes []string, options ...public.AcquireByDeviceCodeOption) (public.DeviceCode, error)
188	AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...public.AcquireByAuthCodeOption) (public.AuthResult, error)
189	AcquireTokenInteractive(ctx context.Context, scopes []string, options ...public.AcquireInteractiveOption) (public.AuthResult, error)
190}