@@ -24,6 +24,7 @@ import (
2424
2525 "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
2626 "github.com/crowdsecurity/crowdsec/pkg/apiclient"
27+ "github.com/crowdsecurity/crowdsec/pkg/apiclient/useragent"
2728 "github.com/crowdsecurity/crowdsec/pkg/appsec"
2829 "github.com/crowdsecurity/crowdsec/pkg/appsec/allowlists"
2930 "github.com/crowdsecurity/crowdsec/pkg/csconfig"
@@ -36,6 +37,11 @@ const (
3637 OutOfBand = "outofband"
3738)
3839
40+ var (
41+ errMissingAPIKey = errors .New ("missing API key" )
42+ errInvalidAPIKey = errors .New ("invalid API key" )
43+ )
44+
3945var DefaultAuthCacheDuration = (1 * time .Minute )
4046
4147// configuration structure of the acquis for the application security engine
@@ -69,6 +75,7 @@ type AppsecSource struct {
6975 AppsecRunners []AppsecRunner // one for each go-routine
7076 appsecAllowlistClient * allowlists.AppsecAllowlist
7177 lapiCACertPool * x509.CertPool
78+ authMutex sync.Mutex
7279}
7380
7481// Struct to handle cache of authentication
@@ -98,6 +105,12 @@ func (ac *AuthCache) Get(apiKey string) (time.Time, bool) {
98105 return expiration , exists
99106}
100107
108+ func (ac * AuthCache ) Delete (apiKey string ) {
109+ ac .mu .Lock ()
110+ delete (ac .APIKeys , apiKey )
111+ ac .mu .Unlock ()
112+ }
113+
101114// @tko + @sbl : we might want to get rid of that or improve it
102115type BodyResponse struct {
103116 Action string `json:"action"`
@@ -454,14 +467,15 @@ func (w *AppsecSource) Dump() interface{} {
454467 return w
455468}
456469
457- func (w * AppsecSource ) IsAuth (ctx context.Context , apiKey string ) bool {
470+ func (w * AppsecSource ) isValidKey (ctx context.Context , apiKey string ) ( bool , error ) {
458471 req , err := http .NewRequestWithContext (ctx , http .MethodHead , w .lapiURL , http .NoBody )
459472 if err != nil {
460473 w .logger .Errorf ("Error creating request: %s" , err )
461- return false
474+ return false , err
462475 }
463476
464477 req .Header .Add ("X-Api-Key" , apiKey )
478+ req .Header .Add ("User-Agent" , useragent .AppsecUserAgent ())
465479
466480 client := & http.Client {
467481 Timeout : 200 * time .Millisecond ,
@@ -478,12 +492,53 @@ func (w *AppsecSource) IsAuth(ctx context.Context, apiKey string) bool {
478492 resp , err := client .Do (req )
479493 if err != nil {
480494 w .logger .Errorf ("Error performing request: %s" , err )
481- return false
495+ return false , err
482496 }
483497
484498 defer resp .Body .Close ()
485499
486- return resp .StatusCode == http .StatusOK
500+ return resp .StatusCode == http .StatusOK , nil
501+ }
502+
503+ func (w * AppsecSource ) checkAuth (ctx context.Context , apiKey string ) error {
504+
505+ if apiKey == "" {
506+ return errMissingAPIKey
507+ }
508+
509+ w .authMutex .Lock ()
510+ defer w .authMutex .Unlock ()
511+
512+ expiration , exists := w .AuthCache .Get (apiKey )
513+ now := time .Now ()
514+
515+ if ! exists { // No key in cache, require a valid check
516+ isAuth , err := w .isValidKey (ctx , apiKey )
517+ if err != nil || ! isAuth {
518+ if err != nil {
519+ w .logger .Errorf ("Error checking auth for API key: %s" , err )
520+ }
521+ return errInvalidAPIKey
522+ }
523+ // Cache the valid API key
524+ w .AuthCache .Set (apiKey , now .Add (* w .config .AuthCacheDuration ))
525+ return nil
526+ }
527+
528+ if now .After (expiration ) { // Key is expired, recheck the value OR keep it if we cannot contact LAPI
529+ isAuth , err := w .isValidKey (ctx , apiKey )
530+ if isAuth {
531+ w .AuthCache .Set (apiKey , now .Add (* w .config .AuthCacheDuration ))
532+ } else if err != nil { // General error when querying LAPI, consider the key still valid
533+ w .logger .Errorf ("Error checking auth for API key: %s, extending cache duration" , err )
534+ w .AuthCache .Set (apiKey , now .Add (* w .config .AuthCacheDuration ))
535+ } else { // Key is not valid, remove it from cache
536+ w .AuthCache .Delete (apiKey )
537+ return errInvalidAPIKey
538+ }
539+ }
540+
541+ return nil
487542}
488543
489544// should this be in the runner ?
@@ -495,27 +550,12 @@ func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
495550 clientIP := r .Header .Get (appsec .IPHeaderName )
496551 remoteIP := r .RemoteAddr
497552
498- if apiKey == "" {
499- w .logger .Errorf ("Unauthorized request from '%s' (real IP = %s)" , remoteIP , clientIP )
553+ if err := w . checkAuth ( ctx , apiKey ); err != nil {
554+ w .logger .Errorf ("Unauthorized request from '%s' (real IP = %s): %s " , remoteIP , clientIP , err )
500555 rw .WriteHeader (http .StatusUnauthorized )
501-
502556 return
503557 }
504558
505- expiration , exists := w .AuthCache .Get (apiKey )
506- // if the apiKey is not in cache or has expired, just recheck the auth
507- if ! exists || time .Now ().After (expiration ) {
508- if ! w .IsAuth (ctx , apiKey ) {
509- rw .WriteHeader (http .StatusUnauthorized )
510- w .logger .Errorf ("Unauthorized request from '%s' (real IP = %s)" , remoteIP , clientIP )
511-
512- return
513- }
514-
515- // apiKey is valid, store it in cache
516- w .AuthCache .Set (apiKey , time .Now ().Add (* w .config .AuthCacheDuration ))
517- }
518-
519559 // parse the request only once
520560 parsedRequest , err := appsec .NewParsedRequestFromRequest (r , w .logger )
521561 if err != nil {
0 commit comments