diff --git a/auth/auth.go b/auth/auth.go index ee324e7f6fbc84369eaf4a0cfc5a47710866f349..8bb10d3cb07814fc2ffff0569652e920e481cf32 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -36,6 +36,7 @@ var auth = &Auth{} type Auth struct { sessions *SessionManager oidc *OIDCBackend + config *Config } func Init(c *Config) (err error) { @@ -43,8 +44,8 @@ func Init(c *Config) (err error) { // authentication is disabled return } - - if auth.sessions, err = NewSessionManager(c.Sessions); err != nil { + auth.config = c + if auth.sessions, err = NewSessionManager(); err != nil { return } diff --git a/auth/config.go b/auth/config.go index 9213f718defb27ff205946d89cfd611d4b0e7180..15f7ac63afe06949bb8e8a96dc50ca2ad1a9b946 100644 --- a/auth/config.go +++ b/auth/config.go @@ -25,12 +25,60 @@ package auth import ( + "errors" + "net/http" "os" + "strings" "time" ) +type SameSite http.SameSite + +func (s SameSite) String() string { + switch http.SameSite(s) { + case http.SameSiteLaxMode: + return "lax" + case http.SameSiteStrictMode: + return "strict" + case http.SameSiteDefaultMode: + return "default" + } + return "unset" +} + +func (s *SameSite) fromString(str string) error { + switch strings.ToLower(os.ExpandEnv(str)) { + case "lax": + *s = SameSite(http.SameSiteLaxMode) + case "strict": + *s = SameSite(http.SameSiteStrictMode) + case "default": + *s = SameSite(http.SameSiteDefaultMode) + default: + return errors.New("invalid same site policy: '" + str + "'") + } + return nil +} + +func (s SameSite) MarshalText() (data []byte, err error) { + data = []byte(s.String()) + return +} + +func (s *SameSite) UnmarshalText(data []byte) (err error) { + return s.fromString(string(data)) +} + +type SessionCookiesConfig struct { + Secure bool `json:"secure" yaml:"secure" toml:"secure"` + Path string `json:"path" yaml:"path" toml:"path"` + Domain string `json:"domain" yaml:"domain" toml:"domain"` + SameSite SameSite `json:"same-site" yaml:"same-site" toml:"same-site"` +} + type SessionsConfig struct { - MaxAge time.Duration `json:"max-age" yaml:"max-age" toml:"max-age"` + MaxAge time.Duration `json:"max-age" yaml:"max-age" toml:"max-age"` + Cookies SessionCookiesConfig `json:"cookies" yaml:"cookies" toml:"cookies"` } type OIDCConfig struct { @@ -46,6 +94,8 @@ type Config struct { } func (c *Config) ExpandEnv() { + c.Sessions.Cookies.Path = os.ExpandEnv(c.Sessions.Cookies.Path) + c.Sessions.Cookies.Domain = os.ExpandEnv(c.Sessions.Cookies.Domain) if c.OIDC != nil { c.OIDC.IssuerURL = os.ExpandEnv(c.OIDC.IssuerURL) c.OIDC.ClientID = os.ExpandEnv(c.OIDC.ClientID) diff --git a/auth/oidc.go b/auth/oidc.go index 45a0d62b1541b0303c87a990ecab383050eb2a75..c3c110af8a405ed5ea265bb5f8264908e2573a3e 100644 --- a/auth/oidc.go +++ b/auth/oidc.go @@ -28,6 +28,7 @@ import ( "context" "encoding/json" "net/http" + "time" oidc "github.com/coreos/go-oidc" "golang.org/x/oauth2" @@ -109,13 +110,12 @@ func (b *OIDCBackend) HandleLogin(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to generate new OIDC session: "+err.Error(), http.StatusInternalServerError) return } - // TODO: this session should expire in ~a minute - s = &Session{oidc: os} + s = &Session{oidc: os, Expires: time.Now().Add(time.Minute)} if sid, err = auth.sessions.insert(s); err != nil { http.Error(w, "Failed to generate new session: "+err.Error(), http.StatusInternalServerError) return } - setSessionCookie(w, sid, s.Expires) + setSessionCookie(w, sid) http.Redirect(w, r, b.oauth2Config.AuthCodeURL(s.oidc.State, oidc.Nonce(s.oidc.Nonce)), http.StatusFound) } @@ -187,8 +187,11 @@ func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) http.Error(w, err.Error(), http.StatusInternalServerError) return } - // TODO: extend this to MaxAge to make short lived initial sessions possible - newS.Expires = s.Expires + maxAge := defaultAge + if auth.config.Sessions.MaxAge > 0 { + maxAge = auth.config.Sessions.MaxAge + } + newS.Expires = time.Now().Add(maxAge * time.Second) newS.oidc = &OIDCSession{State: s.oidc.State, Nonce: s.oidc.Nonce} newS.oidc.token = oauth2Token if err = auth.sessions.update(sid, newS); err != nil { @@ -197,6 +200,7 @@ func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) http.Error(w, "Updating session failed: "+err.Error(), http.StatusInternalServerError) return } + setSessionCookie(w, sid) // reset the cookie to update max-age data, _ := json.MarshalIndent(newS, "", " ") w.Write(data) diff --git a/auth/sessions.go b/auth/sessions.go index bf7e419d2941335c2de1222c83d709f23d15c451..4cefe76225febfdda4cf7676c2831f41923fc3eb 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -27,6 +27,7 @@ package auth import ( "context" "errors" + "math" "net/http" "sync" "time" @@ -73,19 +74,32 @@ func getSessionFromCookie(r *http.Request) (string, *Session) { return sid, s } -func setSessionCookie(w http.ResponseWriter, sid string, expires time.Time) { - // TODO: make cookie settings more secure! +func newSessionCookie(sid string, maxAge int) *http.Cookie { sc := &http.Cookie{Name: SessionCookieName} sc.Value = sid - sc.Expires = expires - sc.Path = "/" - http.SetCookie(w, sc) + sc.HttpOnly = true + sc.MaxAge = maxAge + + sc.Secure = auth.config.Sessions.Cookies.Secure + sc.Path = auth.config.Sessions.Cookies.Path + if sc.Path == "" { + sc.Path = "/" + } + sc.Domain = auth.config.Sessions.Cookies.Domain + sc.SameSite = http.SameSite(auth.config.Sessions.Cookies.SameSite) + return sc +} + +func setSessionCookie(w http.ResponseWriter, sid string) { + maxAge := int(math.Ceil(defaultAge.Seconds())) + if auth.config.Sessions.MaxAge > 0 { + maxAge = int(math.Ceil(auth.config.Sessions.MaxAge.Seconds())) + } + http.SetCookie(w, newSessionCookie(sid, maxAge)) } func invalidateSessionCookie(w http.ResponseWriter) { - // TODO: this needs to improve - sc := &http.Cookie{Name: SessionCookieName, Value: "invalid", MaxAge: -1} - http.SetCookie(w, sc) + http.SetCookie(w, newSessionCookie("invalid", -1)) } func attachSessionToRequest(r *http.Request, s *Session) *http.Request { @@ -100,15 +114,11 @@ func SessionFromRequest(r *http.Request) (*Session, bool) { type SessionManager struct { mutex sync.RWMutex - maxAge time.Duration sessions map[string]*Session } -func NewSessionManager(cfg SessionsConfig) (sm *SessionManager, err error) { - sm = &SessionManager{maxAge: defaultAge} - if cfg.MaxAge > 0 { - sm.maxAge = cfg.MaxAge - } +func NewSessionManager() (sm *SessionManager, err error) { + sm = &SessionManager{} sm.sessions = make(map[string]*Session) go sm.runMaintenance() return @@ -126,7 +136,6 @@ func (sm *SessionManager) insert(s *Session) (id string, err error) { if id, err = generateRandomString(32); err != nil { return } - s.Expires = time.Now().Add(sm.maxAge) sm.mutex.Lock() defer sm.mutex.Unlock() diff --git a/auth/sessions_test.go b/auth/sessions_test.go index 29a08573f2de8fb854b76740a9e379d562ad9afc..3610f6da0bec3ac3a756f7ccb287ff3de7b26795 100644 --- a/auth/sessions_test.go +++ b/auth/sessions_test.go @@ -38,21 +38,18 @@ var ( // func TestCreateSessionManager(t *testing.T) { - cfg := SessionsConfig{} - if _, err := NewSessionManager(cfg); err != nil { + if _, err := NewSessionManager(); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestSessionExpiry(t *testing.T) { - cfg := SessionsConfig{} - cfg.MaxAge = time.Second - sm, err := NewSessionManager(cfg) + sm, err := NewSessionManager() if err != nil { t.Fatalf("unexpected error: %v", err) } - s1 := &Session{Username: "test"} + s1 := &Session{Username: "test", Expires: time.Now().Add(time.Second)} id, err := sm.insert(s1) if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/contrib/sample-cfg.yaml b/contrib/sample-cfg.yaml index 4a9f1bd079d18ca0b96f022f382089e3c48111a4..c7c12b8f78a9b9f3e0ae6515e30feef3972db0f6 100644 --- a/contrib/sample-cfg.yaml +++ b/contrib/sample-cfg.yaml @@ -33,6 +33,12 @@ importer: # sessions: # ## defaults to 24h # max-age: 12h +# # cookies: +# # secure: true +# # path: "/tank/" +# # domain: "aura.example.com" +# # ## allowed values: strict, lax or default +# # same-site: strict # oidc: # issuer-url: http://localhost:8000/openid # client-id: ${OIDC_CLIENT_ID}