diff --git a/server/internal/config/session.go b/server/internal/config/session.go index 54861627..2fa0950d 100644 --- a/server/internal/config/session.go +++ b/server/internal/config/session.go @@ -8,6 +8,16 @@ import ( "github.com/spf13/viper" ) +type SessionCookie struct { + Enabled bool + Name string + Expiration time.Duration + Secure bool + HTTPOnly bool + Domain string + Path string +} + type Session struct { File string @@ -21,10 +31,7 @@ type Session struct { HeartbeatInterval int APIToken string - CookieEnabled bool - CookieName string - CookieExpiration time.Duration - CookieSecure bool + Cookie SessionCookie } func (Session) Init(cmd *cobra.Command) error { @@ -89,7 +96,7 @@ func (Session) Init(cmd *cobra.Command) error { return err } - cmd.PersistentFlags().Int("session.cookie.expiration", 365*24, "expiration of the cookie in hours") + cmd.PersistentFlags().Duration("session.cookie.expiration", 24*time.Hour, "expiration of the cookie") if err := viper.BindPFlag("session.cookie.expiration", cmd.PersistentFlags().Lookup("session.cookie.expiration")); err != nil { return err } @@ -99,6 +106,21 @@ func (Session) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("session.cookie.http_only", true, "use http only cookies") + if err := viper.BindPFlag("session.cookie.http_only", cmd.PersistentFlags().Lookup("session.cookie.http_only")); err != nil { + return err + } + + cmd.PersistentFlags().String("session.cookie.domain", "", "domain of the cookie") + if err := viper.BindPFlag("session.cookie.domain", cmd.PersistentFlags().Lookup("session.cookie.domain")); err != nil { + return err + } + + cmd.PersistentFlags().String("session.cookie.path", "", "path of the cookie") + if err := viper.BindPFlag("session.cookie.path", cmd.PersistentFlags().Lookup("session.cookie.path")); err != nil { + return err + } + return nil } @@ -139,10 +161,13 @@ func (s *Session) Set() { s.HeartbeatInterval = viper.GetInt("session.heartbeat_interval") s.APIToken = viper.GetString("session.api_token") - s.CookieEnabled = viper.GetBool("session.cookie.enabled") - s.CookieName = viper.GetString("session.cookie.name") - s.CookieExpiration = time.Duration(viper.GetInt("session.cookie.expiration")) * time.Hour - s.CookieSecure = viper.GetBool("session.cookie.secure") + s.Cookie.Enabled = viper.GetBool("session.cookie.enabled") + s.Cookie.Name = viper.GetString("session.cookie.name") + s.Cookie.Expiration = viper.GetDuration("session.cookie.expiration") + s.Cookie.Secure = viper.GetBool("session.cookie.secure") + s.Cookie.HTTPOnly = viper.GetBool("session.cookie.http_only") + s.Cookie.Domain = viper.GetString("session.cookie.domain") + s.Cookie.Path = viper.GetString("session.cookie.path") } func (s *Session) SetV2() { diff --git a/server/internal/session/auth.go b/server/internal/session/auth.go index 682c03bb..af4a4bd9 100644 --- a/server/internal/session/auth.go +++ b/server/internal/session/auth.go @@ -11,22 +11,24 @@ import ( func (manager *SessionManagerCtx) CookieSetToken(w http.ResponseWriter, token string) { sameSite := http.SameSiteDefaultMode - if manager.config.CookieSecure { + if manager.config.Cookie.Secure { sameSite = http.SameSiteNoneMode } http.SetCookie(w, &http.Cookie{ - Name: manager.config.CookieName, + Name: manager.config.Cookie.Name, Value: token, - Expires: time.Now().Add(manager.config.CookieExpiration), - Secure: manager.config.CookieSecure, + Expires: time.Now().Add(manager.config.Cookie.Expiration), + Secure: manager.config.Cookie.Secure, SameSite: sameSite, - HttpOnly: true, + HttpOnly: manager.config.Cookie.HTTPOnly, + Domain: manager.config.Cookie.Domain, + Path: manager.config.Cookie.Path, }) } func (manager *SessionManagerCtx) CookieClearToken(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie(manager.config.CookieName) + cookie, err := r.Cookie(manager.config.Cookie.Name) if err != nil { return } @@ -57,7 +59,7 @@ func (manager *SessionManagerCtx) Authenticate(r *http.Request) (types.Session, func (manager *SessionManagerCtx) getToken(r *http.Request) (string, bool) { if manager.CookieEnabled() { // get from Cookie - cookie, err := r.Cookie(manager.config.CookieName) + cookie, err := r.Cookie(manager.config.Cookie.Name) if err == nil { return cookie.Value, true } diff --git a/server/internal/session/manager.go b/server/internal/session/manager.go index 40f1470d..22c5ddaa 100644 --- a/server/internal/session/manager.go +++ b/server/internal/session/manager.go @@ -476,7 +476,7 @@ func (manager *SessionManagerCtx) Settings() types.Settings { } func (manager *SessionManagerCtx) CookieEnabled() bool { - return manager.config.CookieEnabled + return manager.config.Cookie.Enabled } // --- diff --git a/webpage/docs/getting-started/configuration/authentication.md b/webpage/docs/getting-started/configuration/authentication.md index 8bca9e57..2b51f38d 100644 --- a/webpage/docs/getting-started/configuration/authentication.md +++ b/webpage/docs/getting-started/configuration/authentication.md @@ -349,16 +349,21 @@ If you disable the cookies, the token will be sent to the client in the login re ```yaml title="config.yaml" session: cookie: - # Whether the cookies are enabled or not. enabled: true - # Name of the cookie used to store the session. name: "NEKO_SESSION" - # Expiration time of the cookie in seconds. - expiration: 86400 - # Whether the cookie is secure (HTTPS only) or not. + expiration: "24h" secure: true + http_only: true + domain: "" + path: "" ``` +- `enabled` - Whether the cookies are enabled or not. +- `name` - Name of the cookie used to store the session. +- `expiration` - Expiration time of the cookie, use [go duration format](https://pkg.go.dev/time#ParseDuration) (e.g., `24h`, `1h30m`, `60m`). +- `secure` and `http_only` - Ensures that the cookie is only sent over HTTPS and cannot be accessed by JavaScript, see [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#block_access_to_your_cookies) for more information. +- `domain` and `path` - Define where the cookie is valid, see [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_where_cookies_are_sent) for more information. + :::info -The `session.cookie.secure` is set to `true` by default, which means that the cookie is only sent over HTTPS. If you are using HTTP, you should really consider using HTTPS. Only for testing and development purposes should you consider setting it to `false`. +The `secure` and `http_only` are set to `true` by default, which means that the cookie is only sent over HTTPS. If you are using HTTP, you should really consider using HTTPS. Only for testing and development purposes should you consider setting it to `false`. ::: diff --git a/webpage/docs/getting-started/configuration/help.json b/webpage/docs/getting-started/configuration/help.json index d1e8b850..c3ed0e53 100644 --- a/webpage/docs/getting-started/configuration/help.json +++ b/webpage/docs/getting-started/configuration/help.json @@ -486,6 +486,15 @@ "defaultValue": "true", "description": "users can gain control only if at least one admin is in the room" }, + { + "key": [ + "session", + "cookie", + "domain" + ], + "type": "string", + "description": "domain of the cookie" + }, { "key": [ "session", @@ -502,9 +511,19 @@ "cookie", "expiration" ], - "type": "int", - "defaultValue": "8760", - "description": "expiration of the cookie in hours" + "type": "duration", + "defaultValue": "24h0m0s", + "description": "expiration of the cookie" + }, + { + "key": [ + "session", + "cookie", + "http_only" + ], + "type": "boolean", + "defaultValue": "true", + "description": "use http only cookies" }, { "key": [ @@ -516,6 +535,15 @@ "defaultValue": "NEKO_SESSION", "description": "name of the cookie that holds token" }, + { + "key": [ + "session", + "cookie", + "path" + ], + "type": "string", + "description": "path of the cookie" + }, { "key": [ "session", diff --git a/webpage/docs/getting-started/configuration/help.txt b/webpage/docs/getting-started/configuration/help.txt index 79d18dd0..9ca36858 100644 --- a/webpage/docs/getting-started/configuration/help.txt +++ b/webpage/docs/getting-started/configuration/help.txt @@ -50,9 +50,12 @@ --server.static string path to neko client files to serve --session.api_token string API token for interacting with external services --session.control_protection users can gain control only if at least one admin is in the room + --session.cookie.domain string domain of the cookie --session.cookie.enabled whether cookies authentication should be enabled (default true) - --session.cookie.expiration int expiration of the cookie in hours (default 8760) + --session.cookie.expiration duration expiration of the cookie (default 24h0m0s) + --session.cookie.http_only use http only cookies (default true) --session.cookie.name string name of the cookie that holds token (default "NEKO_SESSION") + --session.cookie.path string path of the cookie --session.cookie.secure use secure cookies (default true) --session.file string if sessions should be stored in a file, otherwise they will be stored only in memory --session.heartbeat_interval int interval in seconds for sending heartbeat messages (default 120)