forked from microsoft/terraform-provider-azuredevops
/
auth.go
336 lines (293 loc) · 11.2 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package sdk
import (
"context"
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
type GHIdTokenResponse struct {
Value string `json:"value"`
}
type HCPWorkloadToken struct {
RunPhase string `json:"terraform_run_phase"`
}
type TokenGetter interface {
GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error)
}
type IdentityFuncsI interface {
NewClientAssertionCredential(tenantID, clientID string, getAssertion func(context.Context) (string, error), options *azidentity.ClientAssertionCredentialOptions) (TokenGetter, error)
NewClientCertificateCredential(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *azidentity.ClientCertificateCredentialOptions) (TokenGetter, error)
NewClientSecretCredential(tenantID string, clientID string, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (TokenGetter, error)
NewManagedIdentityCredential(options *azidentity.ManagedIdentityCredentialOptions) (TokenGetter, error)
}
type AzIdentityFuncsImpl struct{}
func (a AzIdentityFuncsImpl) NewClientAssertionCredential(tenantID, clientID string, getAssertion func(context.Context) (string, error), options *azidentity.ClientAssertionCredentialOptions) (TokenGetter, error) {
return azidentity.NewClientAssertionCredential(tenantID, clientID, getAssertion, options)
}
func (a AzIdentityFuncsImpl) NewClientCertificateCredential(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *azidentity.ClientCertificateCredentialOptions) (TokenGetter, error) {
return azidentity.NewClientCertificateCredential(tenantID, clientID, certs, key, options)
}
func (a AzIdentityFuncsImpl) NewClientSecretCredential(tenantID string, clientID string, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (TokenGetter, error) {
return azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, options)
}
func (a AzIdentityFuncsImpl) NewManagedIdentityCredential(options *azidentity.ManagedIdentityCredentialOptions) (TokenGetter, error) {
return azidentity.NewManagedIdentityCredential(options)
}
type OIDCCredentialProvder struct {
audience string
clientID string
requestToken string
requestUrl string
tenantID string
azIdentityFuncs IdentityFuncsI
}
func (o *OIDCCredentialProvder) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
client := &http.Client{}
// Assemble the URL with optional audience
parsedUrl, err := url.Parse(o.requestUrl)
if err != nil {
return azcore.AccessToken{}, err
}
query := parsedUrl.Query()
if o.audience != "" {
query.Add("audience", o.audience)
parsedUrl.RawQuery = query.Encode()
}
// Configure the request
req, err := http.NewRequest("GET", parsedUrl.String(), nil)
if err != nil {
return azcore.AccessToken{}, err
}
req.Header.Add("Authorization", "Bearer "+o.requestToken)
req.Header.Add("Accept", "application/json")
// Make the request
response, err := client.Do(req)
if err != nil {
return azcore.AccessToken{}, err
}
// Parse the response
defer response.Body.Close()
oidc_response := GHIdTokenResponse{}
err = json.NewDecoder(response.Body).Decode(&oidc_response)
if err != nil {
return azcore.AccessToken{}, err
}
// Request the access token from Azure AD using the OIDC token
creds, err := o.azIdentityFuncs.NewClientSecretCredential(o.tenantID, o.clientID, oidc_response.Value, nil)
if err != nil {
return azcore.AccessToken{}, err
}
return creds.GetToken(ctx, opts)
}
func GetAuthTokenProvider(ctx context.Context, d *schema.ResourceData, azIdentityFuncs IdentityFuncsI) (func() (string, error), error) {
// Personal Access Token
if personal_access_token, ok := d.GetOk("personal_access_token"); ok {
tokenFunction := func() (string, error) {
auth := "_:" + personal_access_token.(string)
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)), nil
}
return tokenFunction, nil
}
// Azure Authentication Schemes
tenantID := d.Get("tenant_id").(string)
clientID := d.Get("client_id").(string)
AzureDevOpsAppDefaultScope := "499b84ac-1321-427f-aa17-267ca6975798/.default"
tokenOptions := policy.TokenRequestOptions{
Scopes: []string{AzureDevOpsAppDefaultScope},
}
var cred TokenGetter
var err error
if use_oidc, ok := d.GetOk("use_oidc"); ok && use_oidc.(bool) {
if oidc_token, ok := d.GetOk("oidc_token"); ok {
// Provided OIDC Token
cred, err = azIdentityFuncs.NewClientSecretCredential(tenantID, clientID, oidc_token.(string), nil)
if err != nil {
return nil, err
}
} else if oidc_token_file_path, ok := d.GetOk("oidc_token_file_path"); ok {
// OIDC Token From File
fileBytes, err := os.ReadFile(oidc_token_file_path.(string))
if err != nil {
return nil, err
}
cred, err = azIdentityFuncs.NewClientSecretCredential(tenantID, clientID, strings.TrimSpace(string(fileBytes)), nil)
if err != nil {
return nil, err
}
} else if oidc_request_url, ok := d.GetOk("oidc_request_url"); ok && oidc_request_url.(string) != "" {
audience := "api://AzureADTokenExchange"
if oidc_audience, ok := d.GetOk("oidc_audience"); ok && oidc_audience.(string) != "" {
audience = oidc_audience.(string)
}
if _, ok = d.GetOk("oidc_request_token"); !ok {
return nil, errors.New("No oidc_request_token token found.")
}
// OIDC Token from a REST request, ex: Github Action Workflow
cred = &OIDCCredentialProvder{
audience: audience,
requestUrl: oidc_request_url.(string),
requestToken: d.Get("oidc_request_token").(string),
tenantID: tenantID,
clientID: clientID,
azIdentityFuncs: azIdentityFuncs,
}
} else {
// OIDC Token from Terraform Cloud
tfc_token_env_var := "TFC_WORKLOAD_IDENTITY_TOKEN"
if oidc_tfc_tag, ok := d.GetOk("oidc_tfc_tag"); ok && oidc_tfc_tag.(string) != "" {
tfc_token_env_var = "TFC_WORKLOAD_IDENTITY_TOKEN_" + oidc_tfc_tag.(string)
}
workloadIdentityToken := os.Getenv(tfc_token_env_var)
if workloadIdentityToken == "" {
return nil, errors.New("No OIDC token found in " + tfc_token_env_var + " environment variable.")
}
// Check if plan & apply phases use different service principals
if clientIdPlan, ok := d.GetOk("client_id_plan"); ok {
clientIdApply := d.Get("client_id_apply").(string)
tenantIdPlan := d.Get("tenant_id_plan").(string)
tenantIdApply := d.Get("tenant_id_apply").(string)
// Parse which phase we're in from the OIDC token
workloadIdentityTokenUnmarshalled := HCPWorkloadToken{}
jwtParts := strings.Split(workloadIdentityToken, ".")
if len(jwtParts) != 3 {
return nil, errors.New("Unable to split TFC_WORKLOAD_IDENTITY_TOKEN jwt")
}
jwtClaims := jwtParts[1]
if i := len(jwtClaims) % 4; i != 0 {
jwtClaims += strings.Repeat("=", 4-i)
}
tokenClaims, err := base64.StdEncoding.DecodeString(jwtClaims)
if err != nil {
return nil, err
}
err = json.Unmarshal(tokenClaims, &workloadIdentityTokenUnmarshalled)
if err != nil {
return nil, err
}
if strings.EqualFold(workloadIdentityTokenUnmarshalled.RunPhase, "apply") {
clientID = clientIdApply
tenantID = tenantIdApply
} else if strings.EqualFold(workloadIdentityTokenUnmarshalled.RunPhase, "plan") {
clientID = clientIdPlan.(string)
tenantID = tenantIdPlan
} else {
return nil, fmt.Errorf(" Unrecognized workspace run phase: %s", workloadIdentityTokenUnmarshalled.RunPhase)
}
} else if clientID == "" {
return nil, fmt.Errorf(" Either client_id or client_id_plan must be set when using Terraform Cloud Workload Identity Token authentication.")
}
cred, err = azIdentityFuncs.NewClientSecretCredential(tenantID, clientID, workloadIdentityToken, nil)
if err != nil {
return nil, err
}
}
}
// Certificate from a file on disk
if client_certificate_path, ok := d.GetOk("client_certificate_path"); ok {
fileBytes, err := os.ReadFile(client_certificate_path.(string))
if err != nil {
return nil, err
}
certPassword := ([]byte)(nil)
if password, ok := d.GetOk("client_certificate_password"); ok {
certPassword = []byte(password.(string))
}
certs, key, err := azidentity.ParseCertificates(fileBytes, certPassword)
if err != nil {
return nil, err
}
cred, err = azIdentityFuncs.NewClientCertificateCredential(tenantID, clientID, certs, key, nil)
if err != nil {
return nil, err
}
}
// Certificate from a base64 encoded string
if client_certificate, ok := d.GetOk("client_certificate"); ok {
cert_bytes, err := base64.StdEncoding.DecodeString(client_certificate.(string))
if err != nil {
return nil, err
}
certPassword := ([]byte)(nil)
if password, ok := d.GetOk("client_certificate_password"); ok {
certPassword = []byte(password.(string))
}
certs, key, err := azidentity.ParseCertificates(cert_bytes, certPassword)
if err != nil {
return nil, err
}
cred, err = azIdentityFuncs.NewClientCertificateCredential(tenantID, clientID, certs, key, nil)
if err != nil {
return nil, err
}
}
// Client Secret
if client_secret, ok := d.GetOk("client_secret"); ok {
cred, err = azIdentityFuncs.NewClientSecretCredential(tenantID, clientID, client_secret.(string), nil)
if err != nil {
return nil, err
}
}
// Client Secret from a file on disk
if client_secret_path, ok := d.GetOk("client_secret_path"); ok {
fileBytes, err := os.ReadFile(client_secret_path.(string))
if err != nil {
return nil, err
}
cred, err = azIdentityFuncs.NewClientSecretCredential(tenantID, clientID, strings.TrimSpace(string(fileBytes)), nil)
if err != nil {
return nil, err
}
}
// Azure Managed Service Identity
if use_msi, ok := d.GetOk("use_msi"); ok && use_msi.(bool) {
options := &azidentity.ManagedIdentityCredentialOptions{}
if client_id, ok := d.GetOk("client_id"); ok {
options.ID = azidentity.ClientID(client_id.(string))
}
cred, err = azIdentityFuncs.NewManagedIdentityCredential(options)
if err != nil {
return nil, err
}
}
if cred == nil {
return nil, fmt.Errorf(" No valid credentials found.")
}
provider := newAzTokenProvider(cred, context.Background(), tokenOptions)
return provider.GetToken, nil
}
type AzTokenProvider struct {
ctx context.Context
cred TokenGetter
opts policy.TokenRequestOptions
cachedToken *azcore.AccessToken
}
func newAzTokenProvider(cred TokenGetter, ctx context.Context, opts policy.TokenRequestOptions) *AzTokenProvider {
return &AzTokenProvider{
cred: cred,
ctx: ctx,
opts: opts,
cachedToken: nil,
}
}
func (provider *AzTokenProvider) GetToken() (string, error) {
if provider.cachedToken == nil || provider.cachedToken.ExpiresOn.Before(time.Now().Local().Add(-5*time.Minute)) {
cachedToken, err := provider.cred.GetToken(provider.ctx, provider.opts)
provider.cachedToken = &cachedToken
if err != nil {
return "", err
}
}
return "Bearer " + provider.cachedToken.Token, nil
}