From 6d90a592a826b8abbd6b6eddd84bd4f3c84df187 Mon Sep 17 00:00:00 2001 From: Rajshekar Chavakula Date: Wed, 12 Feb 2025 21:50:40 +0530 Subject: [PATCH 1/5] addition of access/refresh token configuration --- backend/api/authentication.go | 19 +++++++-- backend/apid/apid.go | 9 ++++- backend/apid/routers/authentication.go | 15 +++++-- backend/authentication/jwt/jwt.go | 56 ++++++++++++++++++++++++-- backend/backend.go | 2 + backend/cmd/start.go | 19 +++++++-- backend/config.go | 4 ++ 7 files changed, 108 insertions(+), 16 deletions(-) diff --git a/backend/api/authentication.go b/backend/api/authentication.go index 61d54e47fb..5b3b5606a2 100644 --- a/backend/api/authentication.go +++ b/backend/api/authentication.go @@ -4,8 +4,8 @@ import ( "context" "errors" "fmt" - corev2 "github.com/sensu/core/v2" + "time" "github.com/sensu/sensu-go/backend/authentication" "github.com/sensu/sensu-go/backend/authentication/jwt" @@ -51,8 +51,14 @@ func (a *AuthenticationClient) CreateAccessToken(ctx context.Context, username, claims.Issuer = issuer.(string) } + // append configured access token expiry to claims + var accessTokenExpiry time.Duration + if accessTokenExp := ctx.Value("accessTokenExpiry"); accessTokenExp != nil { + accessTokenExpiry = accessTokenExp.(time.Duration) + } + // Create an access token and its signed version - _, tokenString, err := jwt.AccessToken(claims) + _, tokenString, err := jwt.AccessToken(claims, jwt.WithAccessTokenExpiry(accessTokenExpiry)) if err != nil { return nil, fmt.Errorf("error creating access token: %s", err) } @@ -62,7 +68,14 @@ func (a *AuthenticationClient) CreateAccessToken(ctx context.Context, username, StandardClaims: corev2.StandardClaims(claims.Subject), SessionID: sessionID, } - refreshToken, refreshTokenString, err := jwt.RefreshToken(refreshClaims) + + // append configured access token expiry to claims + var refreshTokenExpiry time.Duration + if refreshTokenExp := ctx.Value("refreshTokenExpiry"); refreshTokenExp != nil { + refreshTokenExpiry = refreshTokenExp.(time.Duration) + } + + refreshToken, refreshTokenString, err := jwt.RefreshToken(refreshClaims, jwt.WithRefreshTokenExpiry(refreshTokenExpiry)) if err != nil { return nil, fmt.Errorf("error creating refresh token: %s", err) } diff --git a/backend/apid/apid.go b/backend/apid/apid.go index f80bb59cf8..b8dbdd0cdb 100644 --- a/backend/apid/apid.go +++ b/backend/apid/apid.go @@ -57,6 +57,9 @@ type APId struct { serveWaitTime time.Duration ready func() + + AccessTokenExpiry time.Duration + RefreshTokenExpiry time.Duration } // Option is a functional option. @@ -81,6 +84,8 @@ type Config struct { ClusterVersion string GraphQLService *graphql.Service HealthRouter *routers.HealthRouter + AccessTokenExpiry time.Duration + RefreshTokenExpiry time.Duration } // New creates a new APId. @@ -102,6 +107,8 @@ func New(c Config, opts ...Option) (*APId, error) { clusterVersion: c.ClusterVersion, RequestLimit: c.RequestLimit, serveWaitTime: c.ServeWaitTime, + AccessTokenExpiry: c.AccessTokenExpiry, + RefreshTokenExpiry: c.RefreshTokenExpiry, } // prepare TLS config @@ -174,7 +181,7 @@ func AuthenticationSubrouter(router *mux.Router, cfg Config) *mux.Router { ) mountRouters(subrouter, - routers.NewAuthenticationRouter(cfg.Store, cfg.Authenticator), + routers.NewAuthenticationRouter(cfg.Store, cfg.Authenticator, cfg.AccessTokenExpiry, cfg.RefreshTokenExpiry), ) return subrouter diff --git a/backend/apid/routers/authentication.go b/backend/apid/routers/authentication.go index 9f05338c52..f32d333bcb 100644 --- a/backend/apid/routers/authentication.go +++ b/backend/apid/routers/authentication.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "net/http" + "time" "github.com/sensu/sensu-go/backend/authentication/jwt" @@ -17,13 +18,15 @@ import ( // AuthenticationRouter handles authentication related requests type AuthenticationRouter struct { - store store.Store - authenticator *authentication.Authenticator + store store.Store + authenticator *authentication.Authenticator + accessTokenExpiry time.Duration + refreshTokenExpiry time.Duration } // NewAuthenticationRouter instantiates new router. -func NewAuthenticationRouter(store store.Store, authenticator *authentication.Authenticator) *AuthenticationRouter { - return &AuthenticationRouter{store: store, authenticator: authenticator} +func NewAuthenticationRouter(store store.Store, authenticator *authentication.Authenticator, accessTokenExpiry time.Duration, refreshTokenExpiry time.Duration) *AuthenticationRouter { + return &AuthenticationRouter{store: store, authenticator: authenticator, accessTokenExpiry: accessTokenExpiry, refreshTokenExpiry: refreshTokenExpiry} } // Mount the authentication routes on given mux.Router. @@ -47,6 +50,10 @@ func (a *AuthenticationRouter) login(w http.ResponseWriter, r *http.Request) { // issuer URL ctx := context.WithValue(r.Context(), jwt.IssuerURLKey, issuerURL(r)) + // Not very efficient, but acceptable for simple use cases, ideally we should create a struct and pass the struct + ctx = context.WithValue(r.Context(), "accessTokenExpiry", a.accessTokenExpiry) + ctx = context.WithValue(ctx, "refreshTokenExpiry", a.refreshTokenExpiry) + client := api.NewAuthenticationClient(a.authenticator, a.store) tokens, err := client.CreateAccessToken(ctx, username, password) if err != nil { diff --git a/backend/authentication/jwt/jwt.go b/backend/authentication/jwt/jwt.go index 81fbb84448..35d299f297 100644 --- a/backend/authentication/jwt/jwt.go +++ b/backend/authentication/jwt/jwt.go @@ -25,6 +25,30 @@ const ( IssuerURLKey key = iota ) +// ExpiryOptions Functional Options Pattern +// ExpiryOptions: Define a struct for optional parameters. +type ExpiryOptions struct { + RefreshTokenExpiry time.Duration + AccessTokenExpiry time.Duration +} + +// ExpiryOption Define a functional option type. +type ExpiryOption func(options *ExpiryOptions) + +// WithRefreshTokenExpiry for setting refresh token expiry +func WithRefreshTokenExpiry(expiry time.Duration) ExpiryOption { + return func(o *ExpiryOptions) { + o.RefreshTokenExpiry = expiry + } +} + +// WithAccessTokenExpiry for setting access token expiry +func WithAccessTokenExpiry(expiry time.Duration) ExpiryOption { + return func(o *ExpiryOptions) { + o.AccessTokenExpiry = expiry + } +} + var ( DefaultAccessTokenLifespan = 5 * time.Minute defaultRefreshTokenLifespan = 12 * time.Hour @@ -49,7 +73,7 @@ func init() { // AccessToken creates a new access token and returns it in both JWT and // signed format, along with any error -func AccessToken(claims *corev2.Claims) (*jwt.Token, string, error) { +func AccessToken(claims *corev2.Claims, options ...ExpiryOption) (*jwt.Token, string, error) { // Create a unique identifier for the token jti, err := GenJTI() if err != nil { @@ -57,8 +81,19 @@ func AccessToken(claims *corev2.Claims) (*jwt.Token, string, error) { } claims.Id = jti + // Default options. + opts := ExpiryOptions{ + RefreshTokenExpiry: defaultRefreshTokenLifespan, + AccessTokenExpiry: DefaultAccessTokenLifespan, + } + + // Apply functional options. + for _, option := range options { + option(&opts) + } + // Add an expiration to the token - claims.ExpiresAt = time.Now().Add(DefaultAccessTokenLifespan).Unix() + claims.ExpiresAt = time.Now().Add(opts.AccessTokenExpiry).Unix() token := jwt.NewWithClaims(signingMethod, claims) @@ -246,7 +281,7 @@ func parseToken(tokenString string) (*jwt.Token, error) { } // RefreshToken returns a refresh token for a specific user -func RefreshToken(claims *corev2.Claims) (*jwt.Token, string, error) { +func RefreshToken(claims *corev2.Claims, options ...ExpiryOption) (*jwt.Token, string, error) { // Create a unique identifier for the token jti, err := GenJTI() if err != nil { @@ -254,10 +289,23 @@ func RefreshToken(claims *corev2.Claims) (*jwt.Token, string, error) { } claims.Id = jti + // Default options. + opts := ExpiryOptions{ + RefreshTokenExpiry: defaultRefreshTokenLifespan, + AccessTokenExpiry: DefaultAccessTokenLifespan, + } + + // Apply functional options. + for _, option := range options { + option(&opts) + } + + // Add an expiration to the token + claims.ExpiresAt = time.Now().Add(opts.RefreshTokenExpiry).Unix() + // Add issuance and expiration timestamps to the token now := time.Now() claims.IssuedAt = now.Unix() - claims.ExpiresAt = now.Add(defaultRefreshTokenLifespan).Unix() token := jwt.NewWithClaims(signingMethod, claims) diff --git a/backend/backend.go b/backend/backend.go index c61ac841ce..ec03d63a37 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -632,6 +632,8 @@ func Initialize(ctx context.Context, config *Config) (*Backend, error) { ClusterVersion: clusterVersion, GraphQLService: b.GraphQLService, HealthRouter: b.HealthRouter, + AccessTokenExpiry: config.AccessTokenExpiry, + RefreshTokenExpiry: config.RefreshTokenExpiry, } newApi, err := apid.New(b.APIDConfig) if err != nil { diff --git a/backend/cmd/start.go b/backend/cmd/start.go index 12757449a4..d6db103f43 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -76,6 +76,10 @@ const ( flagMaxSilencedExpiryTimeAllowed = "max-silenced-expiry-time-allowed" flagDefaultSilencedExpiryTime = "default-silenced-expiry-time" + // access token and refresh token expiry time + flagAccessTokenExpiry = "access-token-expiry" + flagRefreshTokenExpiry = "refresh-token-expiry" + // Etcd flag constants flagEtcdClientURLs = "etcd-client-urls" flagEtcdListenClientURLs = "etcd-listen-client-urls" @@ -293,6 +297,9 @@ func StartCommand(initialize InitializeFunc) *cobra.Command { EventLogBufferWait: viper.GetDuration(flagEventLogBufferWait), EventLogFile: viper.GetString(flagEventLogFile), EventLogParallelEncoders: viper.GetBool(flagEventLogParallelEncoders), + + AccessTokenExpiry: viper.GetDuration(flagAccessTokenExpiry), + RefreshTokenExpiry: viper.GetDuration(flagRefreshTokenExpiry), } if flag := cmd.Flags().Lookup(flagLabels); flag != nil && flag.Changed { @@ -455,12 +462,16 @@ func handleConfig(cmd *cobra.Command, arguments []string, server bool) error { viper.SetDefault(flagEventLogBufferSize, 100000) viper.SetDefault(flagEventLogFile, "") viper.SetDefault(flagEventLogParallelEncoders, false) - - // default silenced value are set for 1 day = 1440m - viper.SetDefault(flagMaxSilencedExpiryTimeAllowed, "1440m") - viper.SetDefault(flagDefaultSilencedExpiryTime, "1440m") } + // default silenced value are set for 1 day = 1440m + viper.SetDefault(flagMaxSilencedExpiryTimeAllowed, "1440m") + viper.SetDefault(flagDefaultSilencedExpiryTime, "1440m") + + // Access/Refresh token default expiry values + viper.SetDefault(flagAccessTokenExpiry, "5m") + viper.SetDefault(flagRefreshTokenExpiry, "720m") + // Etcd defaults viper.SetDefault(flagEtcdAdvertiseClientURLs, defaultEtcdAdvertiseClientURL) viper.SetDefault(flagEtcdListenClientURLs, defaultEtcdClientURL) diff --git a/backend/config.go b/backend/config.go index 87f84dfb16..5761a2f887 100644 --- a/backend/config.go +++ b/backend/config.go @@ -136,4 +136,8 @@ type Config struct { // expiry setting for silences DefaultSilencedExpiryTime time.Duration MaxSilencedExpiryTimeAllowed time.Duration + + // Access/Refresh Token Expiry in Minutes + AccessTokenExpiry time.Duration + RefreshTokenExpiry time.Duration } From 99e754cfc72521cfa4bfdd014597d2e337edc1c2 Mon Sep 17 00:00:00 2001 From: Rajshekar Chavakula Date: Thu, 13 Feb 2025 15:27:59 +0530 Subject: [PATCH 2/5] test case changes --- backend/api/authentication.go | 18 +++++++++++++++--- backend/api/authentication_test.go | 14 +++++++++++++- backend/apid/routers/authentication.go | 6 +++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/backend/api/authentication.go b/backend/api/authentication.go index 5b3b5606a2..a04522aec0 100644 --- a/backend/api/authentication.go +++ b/backend/api/authentication.go @@ -69,7 +69,7 @@ func (a *AuthenticationClient) CreateAccessToken(ctx context.Context, username, SessionID: sessionID, } - // append configured access token expiry to claims + // append configured refresh token expiry to claims var refreshTokenExpiry time.Duration if refreshTokenExp := ctx.Value("refreshTokenExpiry"); refreshTokenExp != nil { refreshTokenExpiry = refreshTokenExp.(time.Duration) @@ -211,18 +211,30 @@ func (a *AuthenticationClient) RefreshAccessToken(ctx context.Context) (*corev2. claims.Issuer = issuer.(string) } + // append configured access token expiry to claims + var accessTokenExpiry time.Duration + if accessTokenExp := ctx.Value("accessTokenExpiry"); accessTokenExp != nil { + accessTokenExpiry = accessTokenExp.(time.Duration) + } + // Issue a new access token - _, newAccessTokenString, err := jwt.AccessToken(claims) + _, newAccessTokenString, err := jwt.AccessToken(claims, jwt.WithAccessTokenExpiry(accessTokenExpiry)) if err != nil { return nil, err } + // append configured refresh token expiry to claims + var refreshTokenExpiry time.Duration + if refreshTokenExp := ctx.Value("refreshTokenExpiry"); refreshTokenExp != nil { + refreshTokenExpiry = refreshTokenExp.(time.Duration) + } + // Create a new refresh token, carrying over the session ID newRefreshClaims := &corev2.Claims{ StandardClaims: corev2.StandardClaims(claims.Subject), SessionID: sessionID, } - newRefreshToken, newRefreshTokenString, err := jwt.RefreshToken(newRefreshClaims) + newRefreshToken, newRefreshTokenString, err := jwt.RefreshToken(newRefreshClaims, jwt.WithRefreshTokenExpiry(refreshTokenExpiry)) if err != nil { return nil, fmt.Errorf("error creating refresh token: %s", err) } diff --git a/backend/api/authentication_test.go b/backend/api/authentication_test.go index 6951e8b47a..02ff9b4173 100644 --- a/backend/api/authentication_test.go +++ b/backend/api/authentication_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" corev2 "github.com/sensu/core/v2" "github.com/sensu/sensu-go/backend/authentication" @@ -33,6 +34,10 @@ func contextWithClaims(claims *corev2.Claims) context.Context { ctx := context.Background() ctx = context.WithValue(ctx, corev2.AccessTokenClaims, claims) ctx = context.WithValue(ctx, corev2.RefreshTokenClaims, refreshClaims) + + ctx = context.WithValue(ctx, "accessTokenExpiry", 5*time.Minute) + ctx = context.WithValue(ctx, "refreshTokenExpiry", 12*time.Hour) + return ctx } @@ -205,7 +210,14 @@ func TestRefreshAccessToken(t *testing.T) { Authenticator: defaultAuth, Context: func(claims *corev2.Claims) (context.Context, string) { ctx := contextWithClaims(claims) - refreshToken, refreshTokenString, _ := jwt.RefreshToken(ctx.Value(corev2.RefreshTokenClaims).(*corev2.Claims)) + + // append configured access token expiry to claims + var refreshTokenExpiry time.Duration + if refreshTokenExp := ctx.Value("refreshTokenExpiry"); refreshTokenExp != nil { + refreshTokenExpiry = refreshTokenExp.(time.Duration) + } + + refreshToken, refreshTokenString, _ := jwt.RefreshToken(ctx.Value(corev2.RefreshTokenClaims).(*corev2.Claims), jwt.WithRefreshTokenExpiry(refreshTokenExpiry)) refreshTokenClaims, _ := jwt.GetClaims(refreshToken) ctx = context.WithValue(ctx, corev2.RefreshTokenString, refreshTokenString) return ctx, refreshTokenClaims.Id diff --git a/backend/apid/routers/authentication.go b/backend/apid/routers/authentication.go index f32d333bcb..ca8f983fe8 100644 --- a/backend/apid/routers/authentication.go +++ b/backend/apid/routers/authentication.go @@ -51,7 +51,7 @@ func (a *AuthenticationRouter) login(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), jwt.IssuerURLKey, issuerURL(r)) // Not very efficient, but acceptable for simple use cases, ideally we should create a struct and pass the struct - ctx = context.WithValue(r.Context(), "accessTokenExpiry", a.accessTokenExpiry) + ctx = context.WithValue(ctx, "accessTokenExpiry", a.accessTokenExpiry) ctx = context.WithValue(ctx, "refreshTokenExpiry", a.refreshTokenExpiry) client := api.NewAuthenticationClient(a.authenticator, a.store) @@ -113,6 +113,10 @@ func (a *AuthenticationRouter) token(w http.ResponseWriter, r *http.Request) { // issuer URL ctx := context.WithValue(r.Context(), jwt.IssuerURLKey, issuerURL(r)) + // Not very efficient, but acceptable for simple use cases, ideally we should create a struct and pass the struct + ctx = context.WithValue(ctx, "accessTokenExpiry", a.accessTokenExpiry) + ctx = context.WithValue(ctx, "refreshTokenExpiry", a.refreshTokenExpiry) + tokens, err := client.RefreshAccessToken(ctx) if err != nil { if err == corev2.ErrInvalidToken { From fd4f8da4cdf5d74366c637c1bfcee9af88ee5477 Mon Sep 17 00:00:00 2001 From: Rajshekar Chavakula Date: Thu, 13 Feb 2025 16:18:21 +0530 Subject: [PATCH 3/5] update config flags --- backend/cmd/start.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/cmd/start.go b/backend/cmd/start.go index d6db103f43..bc99ba98cd 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -563,6 +563,14 @@ func flagSet(server bool) *pflag.FlagSet { flagSet.String(flagEtcdClientURLs, viper.GetString(flagEtcdClientURLs), "client URLs to use when operating as an etcd client") _ = flagSet.SetAnnotation(flagEtcdClientURLs, "categories", []string{"store"}) + // silenced configuration flags + flagSet.Duration(flagDefaultSilencedExpiryTime, viper.GetDuration(flagDefaultSilencedExpiryTime), "Default expiry time for silenced if not set in minutes") + flagSet.Duration(flagMaxSilencedExpiryTimeAllowed, viper.GetDuration(flagMaxSilencedExpiryTimeAllowed), "Maximum expiry time allowed for silenced in minutes") + + // Access/Token configuration flags + flagSet.Duration(flagAccessTokenExpiry, viper.GetDuration(flagAccessTokenExpiry), "Set Access Token expiry in minutes") + flagSet.Duration(flagRefreshTokenExpiry, viper.GetDuration(flagRefreshTokenExpiry), "Set Refresh Token expiry in minutes") + if server { // Main Flags flagSet.String(flagAgentHost, viper.GetString(flagAgentHost), "agent listener host") @@ -605,10 +613,6 @@ func flagSet(server bool) *pflag.FlagSet { flagSet.Duration(flagPlatformMetricsLoggingInterval, viper.GetDuration(flagPlatformMetricsLoggingInterval), "platform metrics logging interval") flagSet.String(flagPlatformMetricsLogFile, viper.GetString(flagPlatformMetricsLogFile), "platform metrics log file path") - // silenced configuration flags - flagSet.Duration(flagDefaultSilencedExpiryTime, viper.GetDuration(flagDefaultSilencedExpiryTime), "Default expiry time for silenced if not set in minutes") - flagSet.Duration(flagMaxSilencedExpiryTimeAllowed, viper.GetDuration(flagMaxSilencedExpiryTimeAllowed), "Maximum expiry time allowed for silenced in minutes") - // Etcd server flags flagSet.StringSlice(flagEtcdPeerURLs, viper.GetStringSlice(flagEtcdPeerURLs), "list of URLs to listen on for peer traffic") _ = flagSet.SetAnnotation(flagEtcdPeerURLs, "categories", []string{"store"}) From 2356fb35aeb6d0b81af12ba33aa6ec41d8bf56f9 Mon Sep 17 00:00:00 2001 From: Rajshekar Chavakula Date: Thu, 13 Feb 2025 16:32:42 +0530 Subject: [PATCH 4/5] Changelog addition --- CHANGELOG-6.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG-6.md b/CHANGELOG-6.md index 986c1b901e..87814c2571 100644 --- a/CHANGELOG-6.md +++ b/CHANGELOG-6.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [6.12.1] - Unreleased + +### Added +- Added `access-token-expiry` (in minutes) backend configuration variable to control expiry of access token. +- Added `refresh-token-expiry` (in minutes) backend configuration variable to control expiry of refresh token. ## [6.12.0] - 2024-11-13 From 0d109b7be4f0e7105fc00cddc50fdcab2dc77fb8 Mon Sep 17 00:00:00 2001 From: Rajshekar Chavakula Date: Fri, 14 Feb 2025 11:53:49 +0530 Subject: [PATCH 5/5] changelog update --- CHANGELOG-6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG-6.md b/CHANGELOG-6.md index 87814c2571..d866f9d263 100644 --- a/CHANGELOG-6.md +++ b/CHANGELOG-6.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [6.12.1] - Unreleased +## [6.13.0] - Unreleased ### Added - Added `access-token-expiry` (in minutes) backend configuration variable to control expiry of access token.