mirror of
https://github.com/crowdsecurity/crowdsec.git
synced 2025-05-10 20:05:55 +02:00
feat(apic): add ApicAuth client and token re-authentication logic (#3522)
This commit is contained in:
parent
3b9130469c
commit
e6b85b641c
3 changed files with 92 additions and 15 deletions
|
@ -17,6 +17,7 @@ import (
|
|||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/tomb.v2"
|
||||
|
||||
|
@ -213,8 +214,6 @@ func NewAPIC(ctx context.Context, config *csconfig.OnlineApiClientCfg, dbClient
|
|||
shareSignals: *config.Sharing,
|
||||
}
|
||||
|
||||
password := strfmt.Password(config.Credentials.Password)
|
||||
|
||||
apiURL, err := url.Parse(config.Credentials.URL)
|
||||
if err != nil {
|
||||
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
|
|||
|
||||
ret.apiClient, err = apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: config.Credentials.Login,
|
||||
Password: password,
|
||||
Password: strfmt.Password(config.Credentials.Password),
|
||||
URL: apiURL,
|
||||
PapiURL: papiURL,
|
||||
VersionPrefix: "v3",
|
||||
|
@ -243,29 +242,103 @@ func NewAPIC(ctx context.Context, config *csconfig.OnlineApiClientCfg, dbClient
|
|||
return nil, fmt.Errorf("while creating api client: %w", err)
|
||||
}
|
||||
|
||||
// The watcher will be authenticated by the RoundTripper the first time it will call CAPI
|
||||
// Explicit authentication will provoke a useless supplementary call to CAPI
|
||||
scenarios, err := ret.FetchScenariosListFromDB(ctx)
|
||||
err = ret.Authenticate(ctx, config)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// loadAPICToken attempts to retrieve and validate a JWT token from the local database.
|
||||
// It returns the token string, its expiration time, and a boolean indicating whether the token is valid.
|
||||
//
|
||||
// A token is considered valid if:
|
||||
// - it exists in the database,
|
||||
// - it is a properly formatted JWT with an "exp" claim,
|
||||
// - it is not expired or near expiry.
|
||||
func loadAPICToken(ctx context.Context, db *database.Client) (string, time.Time, bool) {
|
||||
token, err := db.GetConfigItem(ctx, "apic_token")
|
||||
if err != nil {
|
||||
return ret, fmt.Errorf("get scenario in db: %w", err)
|
||||
log.Debugf("error fetching token from DB: %s", err)
|
||||
return "", time.Time{}, false
|
||||
}
|
||||
|
||||
authResp, _, err := ret.apiClient.Auth.AuthenticateWatcher(ctx, models.WatcherAuthRequest{
|
||||
if token == nil {
|
||||
log.Debug("no token found in DB")
|
||||
return "", time.Time{}, false
|
||||
}
|
||||
|
||||
parser := new(jwt.Parser)
|
||||
tok, _, err := parser.ParseUnverified(*token, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
log.Debugf("error parsing token: %s", err)
|
||||
return "", time.Time{}, false
|
||||
}
|
||||
|
||||
claims, ok := tok.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
log.Debugf("error parsing token claims: %s", err)
|
||||
return "", time.Time{}, false
|
||||
}
|
||||
|
||||
expFloat, ok := claims["exp"].(float64)
|
||||
if !ok {
|
||||
log.Debug("token missing 'exp' claim")
|
||||
return "", time.Time{}, false
|
||||
}
|
||||
|
||||
exp := time.Unix(int64(expFloat), 0)
|
||||
if time.Now().UTC().After(exp.Add(-1*time.Minute)) {
|
||||
log.Debug("auth token expired")
|
||||
return "", time.Time{}, false
|
||||
}
|
||||
|
||||
return *token, exp, true
|
||||
}
|
||||
|
||||
// saveAPICToken stores the given JWT token in the local database under the "apic_token" config item.
|
||||
func saveAPICToken(ctx context.Context, db *database.Client, token string) error {
|
||||
if err := db.SetConfigItem(ctx, "apic_token", token); err != nil {
|
||||
return fmt.Errorf("saving token to db: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate ensures the API client is authorized to communicate with the CAPI.
|
||||
// It attempts to reuse a previously saved JWT token from the database, falling back to
|
||||
// an authentication request if the token is missing, invalid, or expired.
|
||||
//
|
||||
// If a new token is obtained, it is saved back to the database for caching.
|
||||
func (a *apic) Authenticate(ctx context.Context, config *csconfig.OnlineApiClientCfg) error {
|
||||
if token, exp, valid := loadAPICToken(ctx, a.dbClient); valid {
|
||||
log.Debug("using valid token from DB")
|
||||
a.apiClient.GetClient().Transport.(*apiclient.JWTTransport).Token = token
|
||||
a.apiClient.GetClient().Transport.(*apiclient.JWTTransport).Expiration = exp
|
||||
}
|
||||
|
||||
log.Debug("No token found, authenticating")
|
||||
|
||||
scenarios, err := a.FetchScenariosListFromDB(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get scenario in db: %w", err)
|
||||
}
|
||||
|
||||
password := strfmt.Password(config.Credentials.Password)
|
||||
|
||||
authResp, _, err := a.apiClient.Auth.AuthenticateWatcher(ctx, models.WatcherAuthRequest{
|
||||
MachineID: &config.Credentials.Login,
|
||||
Password: &password,
|
||||
Scenarios: scenarios,
|
||||
})
|
||||
if err != nil {
|
||||
return ret, fmt.Errorf("authenticate watcher (%s): %w", config.Credentials.Login, err)
|
||||
return fmt.Errorf("authenticate watcher (%s): %w", config.Credentials.Login, err)
|
||||
}
|
||||
|
||||
if err = ret.apiClient.GetClient().Transport.(*apiclient.JWTTransport).Expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {
|
||||
return ret, fmt.Errorf("unable to parse jwt expiration: %w", err)
|
||||
if err = a.apiClient.GetClient().Transport.(*apiclient.JWTTransport).Expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {
|
||||
return fmt.Errorf("unable to parse jwt expiration: %w", err)
|
||||
}
|
||||
|
||||
ret.apiClient.GetClient().Transport.(*apiclient.JWTTransport).Token = authResp.Token
|
||||
a.apiClient.GetClient().Transport.(*apiclient.JWTTransport).Token = authResp.Token
|
||||
|
||||
return ret, err
|
||||
return saveAPICToken(ctx, a.dbClient, authResp.Token)
|
||||
}
|
||||
|
||||
// keep track of all alerts in cache and push it to CAPI every PushInterval.
|
||||
|
|
|
@ -148,7 +148,7 @@ var (
|
|||
{Name: "created_at", Type: field.TypeTime},
|
||||
{Name: "updated_at", Type: field.TypeTime},
|
||||
{Name: "name", Type: field.TypeString, Unique: true},
|
||||
{Name: "value", Type: field.TypeString},
|
||||
{Name: "value", Type: field.TypeString, SchemaType: map[string]string{"mysql": "longtext", "postgres": "text"}},
|
||||
}
|
||||
// ConfigItemsTable holds the schema information for the "config_items" table.
|
||||
ConfigItemsTable = &schema.Table{
|
||||
|
|
|
@ -2,6 +2,7 @@ package schema
|
|||
|
||||
import (
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
|
@ -22,7 +23,10 @@ func (ConfigItem) Fields() []ent.Field {
|
|||
Default(types.UtcNow).
|
||||
UpdateDefault(types.UtcNow).StructTag(`json:"updated_at"`),
|
||||
field.String("name").Unique().StructTag(`json:"name"`).Immutable(),
|
||||
field.String("value").StructTag(`json:"value"`), // a json object
|
||||
field.String("value").SchemaType(map[string]string{
|
||||
dialect.MySQL: "longtext",
|
||||
dialect.Postgres: "text",
|
||||
}).StructTag(`json:"value"`), // a json object
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue