@@ -5,59 +5,270 @@ package azureauthextension // import "github.com/open-telemetry/opentelemetry-co
5
5
6
6
import (
7
7
"context"
8
+ "crypto"
9
+ "crypto/x509"
10
+ "errors"
11
+ "fmt"
8
12
"net/http"
13
+ "os"
14
+ "strings"
15
+ "sync/atomic"
16
+ "time"
9
17
10
18
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
19
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
20
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
11
21
"go.opentelemetry.io/collector/component"
12
22
"go.opentelemetry.io/collector/extension"
13
23
"go.opentelemetry.io/collector/extension/extensionauth"
14
24
"go.uber.org/zap"
15
25
)
16
26
27
+ const (
28
+ scheme = "Bearer"
29
+ authorizationHeader = "Authorization"
30
+ )
31
+
32
+ var (
33
+ errEmptyAuthorizationHeader = errors .New ("empty authorization header" )
34
+ errMissingAuthorizationHeader = errors .New ("missing authorization header" )
35
+ errUnexpectedAuthorizationFormat = errors .New (`unexpected authorization value format, expected to be of format "Bearer <token>"` )
36
+ errUnexpectedToken = errors .New ("received token does not match the expected one" )
37
+ errUnavailableToken = errors .New ("azure authenticator has no access to token" )
38
+ )
39
+
17
40
type authenticator struct {
41
+ scope string
18
42
credential azcore.TokenCredential
19
- logger * zap.Logger
43
+
44
+ stopCh chan int
45
+ token atomic.Value
46
+
47
+ logger * zap.Logger
20
48
}
21
49
22
50
var (
23
51
_ extension.Extension = (* authenticator )(nil )
24
52
_ extensionauth.HTTPClient = (* authenticator )(nil )
25
53
_ extensionauth.Server = (* authenticator )(nil )
54
+ _ azcore.TokenCredential = (* authenticator )(nil )
26
55
)
27
56
28
- func newAzureAuthenticator (_ * Config , logger * zap.Logger ) * authenticator {
57
+ func newAzureAuthenticator (cfg * Config , logger * zap.Logger ) ( * authenticator , error ) {
29
58
var credential azcore.TokenCredential
30
- // TODO
31
- // if cfg.UseDefault {
32
- // }
33
- // if cfg.Workload != nil {
34
- // }
35
- // if cfg.Managed != nil {
36
- // }
37
- // if cfg.ServicePrincipal != nil {
38
- // }
59
+ var err error
60
+ failMsg := "failed to create authenticator using"
61
+
62
+ if cfg .UseDefault {
63
+ if credential , err = azidentity .NewDefaultAzureCredential (nil ); err != nil {
64
+ return nil , fmt .Errorf ("%s default identity: %w" , failMsg , err )
65
+ }
66
+ }
67
+
68
+ if cfg .Workload != nil {
69
+ if credential , err = azidentity .NewWorkloadIdentityCredential (& azidentity.WorkloadIdentityCredentialOptions {
70
+ ClientID : cfg .Workload .ClientID ,
71
+ TenantID : cfg .Workload .TenantID ,
72
+ TokenFilePath : cfg .Workload .FederatedTokenFile ,
73
+ }); err != nil {
74
+ return nil , fmt .Errorf ("%s workload identity: %w" , failMsg , err )
75
+ }
76
+ }
77
+
78
+ if cfg .Managed != nil {
79
+ clientID := cfg .Managed .ClientID
80
+ var options * azidentity.ManagedIdentityCredentialOptions
81
+ if clientID != "" {
82
+ options = & azidentity.ManagedIdentityCredentialOptions {
83
+ ID : azidentity .ClientID (clientID ),
84
+ }
85
+ }
86
+ if credential , err = azidentity .NewManagedIdentityCredential (options ); err != nil {
87
+ return nil , fmt .Errorf ("%s managed identity: %w" , failMsg , err )
88
+ }
89
+ }
90
+
91
+ if cfg .ServicePrincipal != nil {
92
+ if cfg .ServicePrincipal .ClientCertificatePath != "" {
93
+ cert , privateKey , errParse := getCertificateAndKey (cfg .ServicePrincipal .ClientCertificatePath )
94
+ if errParse != nil {
95
+ return nil , fmt .Errorf ("%s service principal with certificate: %w" , failMsg , errParse )
96
+ }
97
+
98
+ if credential , err = azidentity .NewClientCertificateCredential (
99
+ cfg .ServicePrincipal .TenantID ,
100
+ cfg .ServicePrincipal .ClientID ,
101
+ []* x509.Certificate {cert },
102
+ privateKey ,
103
+ nil ,
104
+ ); err != nil {
105
+ return nil , fmt .Errorf ("%s service principal with certificate: %w" , failMsg , err )
106
+ }
107
+ }
108
+ if cfg .ServicePrincipal .ClientSecret != "" {
109
+ if credential , err = azidentity .NewClientSecretCredential (
110
+ cfg .ServicePrincipal .TenantID ,
111
+ cfg .ServicePrincipal .ClientID ,
112
+ cfg .ServicePrincipal .ClientSecret ,
113
+ nil ,
114
+ ); err != nil {
115
+ return nil , fmt .Errorf ("%s service principal with secret: %w" , failMsg , err )
116
+ }
117
+ }
118
+ }
119
+
39
120
return & authenticator {
121
+ scope : cfg .Scope ,
40
122
credential : credential ,
123
+ stopCh : make (chan int , 1 ),
124
+ token : atomic.Value {},
41
125
logger : logger ,
126
+ }, nil
127
+ }
128
+
129
+ // getCertificateAndKey from the file
130
+ func getCertificateAndKey (filename string ) (* x509.Certificate , crypto.PrivateKey , error ) {
131
+ data , err := os .ReadFile (filename )
132
+ if err != nil {
133
+ return nil , nil , fmt .Errorf ("could not read the certificate file: %w" , err )
134
+ }
135
+
136
+ certs , privateKey , err := azidentity .ParseCertificates (data , nil )
137
+ if err != nil {
138
+ return nil , nil , fmt .Errorf ("failed to parse certificates: %w" , err )
42
139
}
140
+
141
+ return certs [0 ], privateKey , nil
43
142
}
44
143
45
- func (a authenticator ) Start (_ context.Context , _ component.Host ) error {
46
- // TODO
144
+ func (a * authenticator ) Start (ctx context.Context , _ component.Host ) error {
145
+ go a . trackToken ( ctx )
47
146
return nil
48
147
}
49
148
50
- func (a authenticator ) Shutdown (_ context.Context ) error {
51
- // TODO
149
+ // updateToken makes a request to get a new token
150
+ // if the authenticator does not have a token or
151
+ // it has expired.
152
+ func (a * authenticator ) updateToken (ctx context.Context ) (time.Time , error ) {
153
+ if a .credential == nil {
154
+ return time.Time {}, errors .New ("authenticator does not have credentials configured" )
155
+ }
156
+ token , err := a .credential .GetToken (ctx , policy.TokenRequestOptions {
157
+ Scopes : []string {a .scope },
158
+ })
159
+ if err != nil {
160
+ // TODO Handle retries
161
+ return time.Time {}, fmt .Errorf ("azure authenticator failed to get token: %w" , err )
162
+ }
163
+ a .token .Store (token )
164
+ return token .ExpiresOn , nil
165
+ }
166
+
167
+ // trackToken runs on the background to refresh
168
+ // the token if it expires
169
+ func (a * authenticator ) trackToken (ctx context.Context ) {
170
+ expiresOn , err := a .updateToken (ctx )
171
+ if err != nil {
172
+ // TODO Handle retries
173
+ a .logger .Error ("failed to update the token" , zap .Error (err ))
174
+ return
175
+ }
176
+
177
+ getRefresh := func (expiresOn time.Time ) time.Duration {
178
+ // Refresh at 95% token lifetime
179
+ return time .Until (expiresOn ) * 95 / 100
180
+ }
181
+
182
+ t := time .NewTicker (getRefresh (expiresOn ))
183
+ defer t .Stop ()
184
+
185
+ for {
186
+ select {
187
+ case <- ctx .Done ():
188
+ a .logger .Info (
189
+ "azure authenticator no longer refreshing the token" ,
190
+ zap .String ("reason" , "context done" ),
191
+ )
192
+ return
193
+ case <- a .stopCh :
194
+ a .logger .Info (
195
+ "azure authenticator no longer refreshing the token" ,
196
+ zap .String ("reason" , "received stop signal" ),
197
+ )
198
+ close (a .stopCh )
199
+ return
200
+ case <- t .C :
201
+ expiresOn , err = a .updateToken (ctx )
202
+ if err != nil {
203
+ // TODO Handle retries
204
+ a .logger .Error ("failed to update the token" , zap .Error (err ))
205
+ a .stopCh <- 1
206
+ } else {
207
+ t .Reset (getRefresh (expiresOn ))
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ func (a * authenticator ) Shutdown (_ context.Context ) error {
214
+ select {
215
+ case a .stopCh <- 1 :
216
+ default : // already stopped
217
+ }
52
218
return nil
53
219
}
54
220
55
- func (a authenticator ) Authenticate (ctx context.Context , _ map [string ][]string ) (context.Context , error ) {
56
- // TODO
221
+ func (a * authenticator ) getCurrentToken () (azcore.AccessToken , error ) {
222
+ token := a .token .Load ()
223
+ if token == nil {
224
+ return azcore.AccessToken {}, errUnavailableToken
225
+ }
226
+
227
+ return token .(azcore.AccessToken ), nil
228
+ }
229
+
230
+ // GetToken returns an access token with a
231
+ // valid token for authorization
232
+ func (a * authenticator ) GetToken (_ context.Context , _ policy.TokenRequestOptions ) (azcore.AccessToken , error ) {
233
+ return a .getCurrentToken ()
234
+ }
235
+
236
+ // Authenticate adds an Authorization header
237
+ // with the bearer token
238
+ func (a * authenticator ) Authenticate (ctx context.Context , headers map [string ][]string ) (context.Context , error ) {
239
+ auth , ok := headers [authorizationHeader ]
240
+ if ! ok {
241
+ auth , ok = headers [strings .ToLower (authorizationHeader )]
242
+ }
243
+ if ! ok {
244
+ return ctx , errMissingAuthorizationHeader
245
+ }
246
+ if len (auth ) == 0 {
247
+ return ctx , errEmptyAuthorizationHeader
248
+ }
249
+
250
+ firstAuth := strings .Split (auth [0 ], " " )
251
+ if len (firstAuth ) != 2 {
252
+ return ctx , errUnexpectedAuthorizationFormat
253
+ }
254
+ if firstAuth [0 ] != scheme {
255
+ return ctx , fmt .Errorf ("expected %q scheme, got %q" , scheme , firstAuth [0 ])
256
+ }
257
+
258
+ currentToken , err := a .getCurrentToken ()
259
+ if err != nil {
260
+ return ctx , err
261
+ }
262
+
263
+ if firstAuth [1 ] != currentToken .Token {
264
+ return ctx , errUnexpectedToken
265
+ }
266
+
57
267
return ctx , nil
58
268
}
59
269
60
- func (a authenticator ) RoundTripper (_ http.RoundTripper ) (http.RoundTripper , error ) {
270
+ func (a * authenticator ) RoundTripper (_ http.RoundTripper ) (http.RoundTripper , error ) {
61
271
// TODO
272
+ // See request header: https://learn.microsoft.com/en-us/rest/api/azure/#request-header
62
273
return nil , nil
63
274
}
0 commit comments