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