mirror of
https://github.com/TwiN/gatus.git
synced 2026-02-04 12:56:48 +00:00
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
This commit is contained in:
@@ -2579,6 +2579,7 @@ security:
|
|||||||
| `security.oidc.client-secret` | Client secret | Required `""` |
|
| `security.oidc.client-secret` | Client secret | Required `""` |
|
||||||
| `security.oidc.scopes` | Scopes to request. The only scope you need is `openid`. | 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.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
|
```yaml
|
||||||
security:
|
security:
|
||||||
@@ -2590,6 +2591,8 @@ security:
|
|||||||
scopes: ["openid"]
|
scopes: ["openid"]
|
||||||
# You may optionally specify a list of allowed subjects. If this is not specified, all subjects will be allowed.
|
# You may optionally specify a list of allowed subjects. If this is not specified, all subjects will be allowed.
|
||||||
#allowed-subjects: ["johndoe@example.com"]
|
#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).
|
Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0).
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ func validateUniqueKeys(config *Config) error {
|
|||||||
|
|
||||||
func validateSecurityConfig(config *Config) error {
|
func validateSecurityConfig(config *Config) error {
|
||||||
if config.Security != nil {
|
if config.Security != nil {
|
||||||
if config.Security.IsValid() {
|
if config.Security.ValidateAndSetDefaults() {
|
||||||
logr.Debug("[config.validateSecurityConfig] Basic security configuration has been validated")
|
logr.Debug("[config.validateSecurityConfig] Basic security configuration has been validated")
|
||||||
} else {
|
} else {
|
||||||
// If there was an attempt to configure security, then it must mean that some confidential or private
|
// If there was an attempt to configure security, then it must mean that some confidential or private
|
||||||
|
|||||||
@@ -1850,7 +1850,7 @@ endpoints:
|
|||||||
if config.Security == nil {
|
if config.Security == nil {
|
||||||
t.Fatal("config.Security shouldn't have been 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")
|
t.Error("Security config should've been valid")
|
||||||
}
|
}
|
||||||
if config.Security.Basic == nil {
|
if config.Security.Basic == nil {
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ type Config struct {
|
|||||||
gate *g8.Gate
|
gate *g8.Gate
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the security configuration is valid or not
|
// ValidateAndSetDefaults returns whether the security configuration is valid or not and sets default values.
|
||||||
func (c *Config) IsValid() bool {
|
func (c *Config) ValidateAndSetDefaults() bool {
|
||||||
return (c.Basic != nil && c.Basic.isValid()) || (c.OIDC != nil && c.OIDC.isValid())
|
return (c.Basic != nil && c.Basic.isValid()) || (c.OIDC != nil && c.OIDC.ValidateAndSetDefaults())
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterHandlers registers all handlers required based on the security configuration
|
// RegisterHandlers registers all handlers required based on the security configuration
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_IsValid(t *testing.T) {
|
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||||
c := &Config{
|
c := &Config{
|
||||||
Basic: nil,
|
Basic: nil,
|
||||||
OIDC: nil,
|
OIDC: nil,
|
||||||
}
|
}
|
||||||
if c.IsValid() {
|
if c.ValidateAndSetDefaults() {
|
||||||
t.Error("expected empty config to be valid")
|
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",
|
RedirectURL: "http://localhost:80/authorization-code/callback",
|
||||||
Scopes: []string{"openid"},
|
Scopes: []string{"openid"},
|
||||||
AllowedSubjects: []string{"user1@example.com"},
|
AllowedSubjects: []string{"user1@example.com"},
|
||||||
|
SessionTTL: DefaultOIDCSessionTTL,
|
||||||
oauth2Config: oauth2.Config{},
|
oauth2Config: oauth2.Config{},
|
||||||
verifier: nil,
|
verifier: nil,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultOIDCSessionTTL = 8 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
// OIDCConfig is the configuration for OIDC authentication
|
// OIDCConfig is the configuration for OIDC authentication
|
||||||
type OIDCConfig struct {
|
type OIDCConfig struct {
|
||||||
IssuerURL string `yaml:"issuer-url"` // e.g. https://dev-12345678.okta.com
|
IssuerURL string `yaml:"issuer-url"` // e.g. https://dev-12345678.okta.com
|
||||||
@@ -21,13 +25,17 @@ type OIDCConfig struct {
|
|||||||
ClientSecret string `yaml:"client-secret"`
|
ClientSecret string `yaml:"client-secret"`
|
||||||
Scopes []string `yaml:"scopes"` // e.g. ["openid"]
|
Scopes []string `yaml:"scopes"` // e.g. ["openid"]
|
||||||
AllowedSubjects []string `yaml:"allowed-subjects"` // e.g. ["user1@example.com"]. If empty, all subjects are allowed
|
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
|
oauth2Config oauth2.Config
|
||||||
verifier *oidc.IDTokenVerifier
|
verifier *oidc.IDTokenVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValid returns whether the basic security configuration is valid or not
|
// ValidateAndSetDefaults returns whether the OIDC configuration is valid and sets default values.
|
||||||
func (c *OIDCConfig) isValid() bool {
|
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
|
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) {
|
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.
|
// At this point, the user has been confirmed. All that's left to do is create a session.
|
||||||
sessionID := uuid.NewString()
|
sessionID := uuid.NewString()
|
||||||
sessions.SetWithTTL(sessionID, idToken.Subject, time.Hour)
|
sessions.SetWithTTL(sessionID, idToken.Subject, c.SessionTTL)
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: cookieNameSession,
|
Name: cookieNameSession,
|
||||||
Value: sessionID,
|
Value: sessionID,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: int(time.Hour.Seconds()),
|
MaxAge: int(c.SessionTTL.Seconds()),
|
||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOIDCConfig_isValid(t *testing.T) {
|
func TestOIDCConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||||
c := &OIDCConfig{
|
c := &OIDCConfig{
|
||||||
IssuerURL: "https://sso.gatus.io/",
|
IssuerURL: "https://sso.gatus.io/",
|
||||||
RedirectURL: "http://localhost:80/authorization-code/callback",
|
RedirectURL: "http://localhost:80/authorization-code/callback",
|
||||||
@@ -16,10 +17,14 @@ func TestOIDCConfig_isValid(t *testing.T) {
|
|||||||
ClientSecret: "client-secret",
|
ClientSecret: "client-secret",
|
||||||
Scopes: []string{"openid"},
|
Scopes: []string{"openid"},
|
||||||
AllowedSubjects: []string{"user1@example.com"},
|
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")
|
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) {
|
func TestOIDCConfig_callbackHandler(t *testing.T) {
|
||||||
@@ -68,3 +73,18 @@ func TestOIDCConfig_setSessionCookie(t *testing.T) {
|
|||||||
t.Error("expected cookie to be set")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user