From 327a39964d280b738a11a7b2fb04b365e67b9383 Mon Sep 17 00:00:00 2001 From: TwiN Date: Sat, 20 Sep 2025 07:29:25 -0400 Subject: [PATCH] fix(security): Make OIDC session TTL configurable (#1280) * fix(security): Increase session cookie from 1h to 8h * fix(security): Make OIDC session TTL configurable * revert accidental change --- README.md | 3 +++ config/config.go | 2 +- config/config_test.go | 2 +- security/config.go | 6 +++--- security/config_test.go | 5 +++-- security/oidc.go | 28 ++++++++++++++++++---------- security/oidc_test.go | 24 ++++++++++++++++++++++-- 7 files changed, 51 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c492ec71..f9724df4 100644 --- a/README.md +++ b/README.md @@ -2579,6 +2579,7 @@ security: | `security.oidc.client-secret` | Client secret | Required `""` | | `security.oidc.scopes` | Scopes to request. The only scope you need is `openid`. | Required `[]` | | `security.oidc.allowed-subjects` | List of subjects to allow. If empty, all subjects are allowed. | `[]` | +| `security.oidc.session-ttl` | Session time-to-live (e.g. `8h`, `1h30m`, `2h`). | `8h` | ```yaml security: @@ -2590,6 +2591,8 @@ security: scopes: ["openid"] # You may optionally specify a list of allowed subjects. If this is not specified, all subjects will be allowed. #allowed-subjects: ["johndoe@example.com"] + # You may optionally specify a session time-to-live. If this is not specified, defaults to 8 hours. + #session-ttl: 8h ``` Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0). diff --git a/config/config.go b/config/config.go index f2fc45a5..b3081bc1 100644 --- a/config/config.go +++ b/config/config.go @@ -514,7 +514,7 @@ func validateUniqueKeys(config *Config) error { func validateSecurityConfig(config *Config) error { if config.Security != nil { - if config.Security.IsValid() { + if config.Security.ValidateAndSetDefaults() { logr.Debug("[config.validateSecurityConfig] Basic security configuration has been validated") } else { // If there was an attempt to configure security, then it must mean that some confidential or private diff --git a/config/config_test.go b/config/config_test.go index e055c6e3..9ce681c7 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1850,7 +1850,7 @@ endpoints: if config.Security == nil { t.Fatal("config.Security shouldn't have been nil") } - if !config.Security.IsValid() { + if !config.Security.ValidateAndSetDefaults() { t.Error("Security config should've been valid") } if config.Security.Basic == nil { diff --git a/security/config.go b/security/config.go index 994fc0c2..4622edc0 100644 --- a/security/config.go +++ b/security/config.go @@ -26,9 +26,9 @@ type Config struct { gate *g8.Gate } -// IsValid returns whether the security configuration is valid or not -func (c *Config) IsValid() bool { - return (c.Basic != nil && c.Basic.isValid()) || (c.OIDC != nil && c.OIDC.isValid()) +// ValidateAndSetDefaults returns whether the security configuration is valid or not and sets default values. +func (c *Config) ValidateAndSetDefaults() bool { + return (c.Basic != nil && c.Basic.isValid()) || (c.OIDC != nil && c.OIDC.ValidateAndSetDefaults()) } // RegisterHandlers registers all handlers required based on the security configuration diff --git a/security/config_test.go b/security/config_test.go index 45fb245b..76ffd50a 100644 --- a/security/config_test.go +++ b/security/config_test.go @@ -9,12 +9,12 @@ import ( "golang.org/x/oauth2" ) -func TestConfig_IsValid(t *testing.T) { +func TestConfig_ValidateAndSetDefaults(t *testing.T) { c := &Config{ Basic: nil, OIDC: nil, } - if c.IsValid() { + if c.ValidateAndSetDefaults() { t.Error("expected empty config to be valid") } } @@ -65,6 +65,7 @@ func TestConfig_ApplySecurityMiddleware(t *testing.T) { RedirectURL: "http://localhost:80/authorization-code/callback", Scopes: []string{"openid"}, AllowedSubjects: []string{"user1@example.com"}, + SessionTTL: DefaultOIDCSessionTTL, oauth2Config: oauth2.Config{}, verifier: nil, }} diff --git a/security/oidc.go b/security/oidc.go index 6505b8ef..821f2210 100644 --- a/security/oidc.go +++ b/security/oidc.go @@ -13,21 +13,29 @@ import ( "golang.org/x/oauth2" ) +const ( + DefaultOIDCSessionTTL = 8 * time.Hour +) + // OIDCConfig is the configuration for OIDC authentication type OIDCConfig struct { - IssuerURL string `yaml:"issuer-url"` // e.g. https://dev-12345678.okta.com - RedirectURL string `yaml:"redirect-url"` // e.g. http://localhost:8080/authorization-code/callback - ClientID string `yaml:"client-id"` - ClientSecret string `yaml:"client-secret"` - Scopes []string `yaml:"scopes"` // e.g. ["openid"] - AllowedSubjects []string `yaml:"allowed-subjects"` // e.g. ["user1@example.com"]. If empty, all subjects are allowed + IssuerURL string `yaml:"issuer-url"` // e.g. https://dev-12345678.okta.com + RedirectURL string `yaml:"redirect-url"` // e.g. http://localhost:8080/authorization-code/callback + ClientID string `yaml:"client-id"` + ClientSecret string `yaml:"client-secret"` + Scopes []string `yaml:"scopes"` // e.g. ["openid"] + AllowedSubjects []string `yaml:"allowed-subjects"` // e.g. ["user1@example.com"]. If empty, all subjects are allowed + SessionTTL time.Duration `yaml:"session-ttl"` // e.g. 8h. Defaults to 8 hours oauth2Config oauth2.Config verifier *oidc.IDTokenVerifier } -// isValid returns whether the basic security configuration is valid or not -func (c *OIDCConfig) isValid() bool { +// ValidateAndSetDefaults returns whether the OIDC configuration is valid and sets default values. +func (c *OIDCConfig) ValidateAndSetDefaults() bool { + if c.SessionTTL <= 0 { + c.SessionTTL = DefaultOIDCSessionTTL + } return len(c.IssuerURL) > 0 && len(c.RedirectURL) > 0 && strings.HasSuffix(c.RedirectURL, "/authorization-code/callback") && len(c.ClientID) > 0 && len(c.ClientSecret) > 0 && len(c.Scopes) > 0 } @@ -131,12 +139,12 @@ func (c *OIDCConfig) callbackHandler(w http.ResponseWriter, r *http.Request) { / func (c *OIDCConfig) setSessionCookie(w http.ResponseWriter, idToken *oidc.IDToken) { // At this point, the user has been confirmed. All that's left to do is create a session. sessionID := uuid.NewString() - sessions.SetWithTTL(sessionID, idToken.Subject, time.Hour) + sessions.SetWithTTL(sessionID, idToken.Subject, c.SessionTTL) http.SetCookie(w, &http.Cookie{ Name: cookieNameSession, Value: sessionID, Path: "/", - MaxAge: int(time.Hour.Seconds()), + MaxAge: int(c.SessionTTL.Seconds()), SameSite: http.SameSiteStrictMode, }) } diff --git a/security/oidc_test.go b/security/oidc_test.go index bd7b7f95..73dd54d3 100644 --- a/security/oidc_test.go +++ b/security/oidc_test.go @@ -4,11 +4,12 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/coreos/go-oidc/v3/oidc" ) -func TestOIDCConfig_isValid(t *testing.T) { +func TestOIDCConfig_ValidateAndSetDefaults(t *testing.T) { c := &OIDCConfig{ IssuerURL: "https://sso.gatus.io/", RedirectURL: "http://localhost:80/authorization-code/callback", @@ -16,10 +17,14 @@ func TestOIDCConfig_isValid(t *testing.T) { ClientSecret: "client-secret", Scopes: []string{"openid"}, AllowedSubjects: []string{"user1@example.com"}, + SessionTTL: 0, // Not set! ValidateAndSetDefaults should set it to DefaultOIDCSessionTTL } - if !c.isValid() { + if !c.ValidateAndSetDefaults() { t.Error("OIDCConfig should be valid") } + if c.SessionTTL != DefaultOIDCSessionTTL { + t.Error("expected SessionTTL to be set to DefaultOIDCSessionTTL") + } } func TestOIDCConfig_callbackHandler(t *testing.T) { @@ -68,3 +73,18 @@ func TestOIDCConfig_setSessionCookie(t *testing.T) { t.Error("expected cookie to be set") } } + +func TestOIDCConfig_setSessionCookieWithCustomTTL(t *testing.T) { + customTTL := 30 * time.Minute + c := &OIDCConfig{SessionTTL: customTTL} + responseRecorder := httptest.NewRecorder() + c.setSessionCookie(responseRecorder, &oidc.IDToken{Subject: "test@example.com"}) + cookies := responseRecorder.Result().Cookies() + if len(cookies) == 0 { + t.Error("expected cookie to be set") + } + sessionCookie := cookies[0] + if sessionCookie.MaxAge != int(customTTL.Seconds()) { + t.Errorf("expected cookie MaxAge to be %d, but was %d", int(customTTL.Seconds()), sessionCookie.MaxAge) + } +}