@@ -17,6 +17,7 @@ import (
17
17
18
18
"github.com/davecgh/go-spew/spew"
19
19
"github.com/go-openapi/strfmt"
20
+ "github.com/golang-jwt/jwt/v4"
20
21
log "github.com/sirupsen/logrus"
21
22
"gopkg.in/tomb.v2"
22
23
@@ -213,8 +214,6 @@ func NewAPIC(ctx context.Context, config *csconfig.OnlineApiClientCfg, dbClient
213
214
shareSignals : * config .Sharing ,
214
215
}
215
216
216
- password := strfmt .Password (config .Credentials .Password )
217
-
218
217
apiURL , err := url .Parse (config .Credentials .URL )
219
218
if err != nil {
220
219
return nil , fmt .Errorf ("while parsing '%s': %w" , config .Credentials .URL , err )
@@ -232,7 +231,7 @@ func NewAPIC(ctx context.Context, config *csconfig.OnlineApiClientCfg, dbClient
232
231
233
232
ret .apiClient , err = apiclient .NewClient (& apiclient.Config {
234
233
MachineID : config .Credentials .Login ,
235
- Password : password ,
234
+ Password : strfmt . Password ( config . Credentials . Password ) ,
236
235
URL : apiURL ,
237
236
PapiURL : papiURL ,
238
237
VersionPrefix : "v3" ,
@@ -243,29 +242,103 @@ func NewAPIC(ctx context.Context, config *csconfig.OnlineApiClientCfg, dbClient
243
242
return nil , fmt .Errorf ("while creating api client: %w" , err )
244
243
}
245
244
246
- // The watcher will be authenticated by the RoundTripper the first time it will call CAPI
247
- // Explicit authentication will provoke a useless supplementary call to CAPI
248
- scenarios , err := ret .FetchScenariosListFromDB (ctx )
245
+ err = ret .Authenticate (ctx , config )
246
+ return ret , err
247
+ }
248
+
249
+ // loadAPICToken attempts to retrieve and validate a JWT token from the local database.
250
+ // It returns the token string, its expiration time, and a boolean indicating whether the token is valid.
251
+ //
252
+ // A token is considered valid if:
253
+ // - it exists in the database,
254
+ // - it is a properly formatted JWT with an "exp" claim,
255
+ // - it is not expired or near expiry.
256
+ func loadAPICToken (ctx context.Context , db * database.Client ) (string , time.Time , bool ) {
257
+ token , err := db .GetConfigItem (ctx , "apic_token" )
258
+ if err != nil {
259
+ log .Debugf ("error fetching token from DB: %s" , err )
260
+ return "" , time.Time {}, false
261
+ }
262
+
263
+ if token == nil {
264
+ log .Debug ("no token found in DB" )
265
+ return "" , time.Time {}, false
266
+ }
267
+
268
+ parser := new (jwt.Parser )
269
+ tok , _ , err := parser .ParseUnverified (* token , jwt.MapClaims {})
270
+ if err != nil {
271
+ log .Debugf ("error parsing token: %s" , err )
272
+ return "" , time.Time {}, false
273
+ }
274
+
275
+ claims , ok := tok .Claims .(jwt.MapClaims )
276
+ if ! ok {
277
+ log .Debugf ("error parsing token claims: %s" , err )
278
+ return "" , time.Time {}, false
279
+ }
280
+
281
+ expFloat , ok := claims ["exp" ].(float64 )
282
+ if ! ok {
283
+ log .Debug ("token missing 'exp' claim" )
284
+ return "" , time.Time {}, false
285
+ }
286
+
287
+ exp := time .Unix (int64 (expFloat ), 0 )
288
+ if time .Now ().UTC ().After (exp .Add (- 1 * time .Minute )) {
289
+ log .Debug ("auth token expired" )
290
+ return "" , time.Time {}, false
291
+ }
292
+
293
+ return * token , exp , true
294
+ }
295
+
296
+ // saveAPICToken stores the given JWT token in the local database under the "apic_token" config item.
297
+ func saveAPICToken (ctx context.Context , db * database.Client , token string ) error {
298
+ if err := db .SetConfigItem (ctx , "apic_token" , token ); err != nil {
299
+ return fmt .Errorf ("saving token to db: %w" , err )
300
+ }
301
+
302
+ return nil
303
+ }
304
+
305
+ // Authenticate ensures the API client is authorized to communicate with the CAPI.
306
+ // It attempts to reuse a previously saved JWT token from the database, falling back to
307
+ // an authentication request if the token is missing, invalid, or expired.
308
+ //
309
+ // If a new token is obtained, it is saved back to the database for caching.
310
+ func (a * apic ) Authenticate (ctx context.Context , config * csconfig.OnlineApiClientCfg ) error {
311
+ if token , exp , valid := loadAPICToken (ctx , a .dbClient ); valid {
312
+ log .Debug ("using valid token from DB" )
313
+ a .apiClient .GetClient ().Transport .(* apiclient.JWTTransport ).Token = token
314
+ a .apiClient .GetClient ().Transport .(* apiclient.JWTTransport ).Expiration = exp
315
+ }
316
+
317
+ log .Debug ("No token found, authenticating" )
318
+
319
+ scenarios , err := a .FetchScenariosListFromDB (ctx )
249
320
if err != nil {
250
- return ret , fmt .Errorf ("get scenario in db: %w" , err )
321
+ return fmt .Errorf ("get scenario in db: %w" , err )
251
322
}
252
323
253
- authResp , _ , err := ret .apiClient .Auth .AuthenticateWatcher (ctx , models.WatcherAuthRequest {
324
+ password := strfmt .Password (config .Credentials .Password )
325
+
326
+ authResp , _ , err := a .apiClient .Auth .AuthenticateWatcher (ctx , models.WatcherAuthRequest {
254
327
MachineID : & config .Credentials .Login ,
255
328
Password : & password ,
256
329
Scenarios : scenarios ,
257
330
})
258
331
if err != nil {
259
- return ret , fmt .Errorf ("authenticate watcher (%s): %w" , config .Credentials .Login , err )
332
+ return fmt .Errorf ("authenticate watcher (%s): %w" , config .Credentials .Login , err )
260
333
}
261
334
262
- if err = ret .apiClient .GetClient ().Transport .(* apiclient.JWTTransport ).Expiration .UnmarshalText ([]byte (authResp .Expire )); err != nil {
263
- return ret , fmt .Errorf ("unable to parse jwt expiration: %w" , err )
335
+ if err = a .apiClient .GetClient ().Transport .(* apiclient.JWTTransport ).Expiration .UnmarshalText ([]byte (authResp .Expire )); err != nil {
336
+ return fmt .Errorf ("unable to parse jwt expiration: %w" , err )
264
337
}
265
338
266
- ret .apiClient .GetClient ().Transport .(* apiclient.JWTTransport ).Token = authResp .Token
339
+ a .apiClient .GetClient ().Transport .(* apiclient.JWTTransport ).Token = authResp .Token
267
340
268
- return ret , err
341
+ return saveAPICToken ( ctx , a . dbClient , authResp . Token )
269
342
}
270
343
271
344
// keep track of all alerts in cache and push it to CAPI every PushInterval.
0 commit comments