git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: add same site option for csrf cookies (#3347)
cdbdb11Joost
CHANGELOG.md+1
@@ -16,6 +16,7 @@
16- [#3369](https://github.com/oauth2-proxy/oauth2-proxy/pull/3369) fix: use CSRFExpire instead of Expire for CSRF cookie validation (@Br1an67)
16- [#3369](https://github.com/oauth2-proxy/oauth2-proxy/pull/3369) fix: use CSRFExpire instead of Expire for CSRF cookie validation (@Br1an67)
17- [#3365](https://github.com/oauth2-proxy/oauth2-proxy/pull/3365) fix: filter empty strings from allowed groups (@Br1an67)
17- [#3365](https://github.com/oauth2-proxy/oauth2-proxy/pull/3365) fix: filter empty strings from allowed groups (@Br1an67)
18- [#3338](https://github.com/oauth2-proxy/oauth2-proxy/pull/3338) feat: add --config-test flag for validating configuration (@MayorFaj)
18- [#3338](https://github.com/oauth2-proxy/oauth2-proxy/pull/3338) feat: add --config-test flag for validating configuration (@MayorFaj)
19+- [#3347](https://github.com/oauth2-proxy/oauth2-proxy/pull/3347) feat: add same site option for csrf cookies (@jvnoije)
19
20
20# V7.14.3
21# V7.14.3
21
22
docs/docs/configuration/overview.md+5−4
@@ -120,7 +120,7 @@ Provider specific options can be found on their respective subpages.
120| flag: `--approval-prompt`<br/>toml: `approval_prompt` | string | OAuth approval_prompt | `"force"` |
120| flag: `--approval-prompt`<br/>toml: `approval_prompt` | string | OAuth approval_prompt | `"force"` |
121| flag: `--backend-logout-url`<br/>toml: `backend_logout_url` | string | URL to perform backend logout, if you use `{id_token}` in the url it will be replaced by the actual `id_token` of the user session | |
121| flag: `--backend-logout-url`<br/>toml: `backend_logout_url` | string | URL to perform backend logout, if you use `{id_token}` in the url it will be replaced by the actual `id_token` of the user session | |
122| flag: `--client-id`<br/>toml: `client_id` | string | the OAuth Client ID, e.g. `"123456.apps.googleusercontent.com"` | |
122| flag: `--client-id`<br/>toml: `client_id` | string | the OAuth Client ID, e.g. `"123456.apps.googleusercontent.com"` | |
123−| flag: `--client-secret-file`<br/>toml: `client_secret_file` | string | the file with OAuth Client Secret. The file must contain the secret only, with no trailing newline | |
123+| flag: `--client-secret-file`<br/>toml: `client_secret_file` | string | the file with OAuth Client Secret. The file must contain the secret only, with no trailing newline | |
124| flag: `--client-secret`<br/>toml: `client_secret` | string | the OAuth Client Secret | |
124| flag: `--client-secret`<br/>toml: `client_secret` | string | the OAuth Client Secret | |
125| flag: `--code-challenge-method`<br/>toml: `code_challenge_method` | string | use PKCE code challenges with the specified method. Either 'plain' or 'S256' (recommended) | |
125| flag: `--code-challenge-method`<br/>toml: `code_challenge_method` | string | use PKCE code challenges with the specified method. Either 'plain' or 'S256' (recommended) | |
126| flag: `--insecure-oidc-allow-unverified-email`<br/>toml: `insecure_oidc_allow_unverified_email` | bool | don't fail if an email address in an id_token is not verified | false |
126| flag: `--insecure-oidc-allow-unverified-email`<br/>toml: `insecure_oidc_allow_unverified_email` | bool | don't fail if an email address in an id_token is not verified | false |
@@ -140,7 +140,7 @@ Provider specific options can be found on their respective subpages.
140| flag: `--oidc-enabled-signing-alg`<br/>toml: `oidc_enabled_signing_algs` | string \| list | List of allowed JWT signing algorithms. When oidc discovery is enabled, the effective set is the intersection between this list and the provider's discovered supported algorithms. | |
140| flag: `--oidc-enabled-signing-alg`<br/>toml: `oidc_enabled_signing_algs` | string \| list | List of allowed JWT signing algorithms. When oidc discovery is enabled, the effective set is the intersection between this list and the provider's discovered supported algorithms. | |
141| flag: `--profile-url`<br/>toml: `profile_url` | string | Profile access endpoint | |
141| flag: `--profile-url`<br/>toml: `profile_url` | string | Profile access endpoint | |
142| flag: `--prompt`<br/>toml: `prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` |
142| flag: `--prompt`<br/>toml: `prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` |
143−| flag: `--provider-ca-file`<br/>toml: `provider_ca_files` | string \| list | Paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead. |
143+| flag: `--provider-ca-file`<br/>toml: `provider_ca_files` | string \| list | Paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead. | |
144| flag: `--provider-display-name`<br/>toml: `provider_display_name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
144| flag: `--provider-display-name`<br/>toml: `provider_display_name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
145| flag: `--provider`<br/>toml: `provider` | string | OAuth provider | google |
145| flag: `--provider`<br/>toml: `provider` | string | OAuth provider | google |
146| flag: `--pubjwk-url`<br/>toml: `pubjwk_url` | string | JWK pubkey access endpoint: required by login.gov | |
146| flag: `--pubjwk-url`<br/>toml: `pubjwk_url` | string | JWK pubkey access endpoint: required by login.gov | |
@@ -158,6 +158,7 @@ Provider specific options can be found on their respective subpages.
158| flag: `--cookie-csrf-expire`<br/>toml: `cookie_csrf_expire` | duration | expire timeframe for CSRF cookie | 15m |
158| flag: `--cookie-csrf-expire`<br/>toml: `cookie_csrf_expire` | duration | expire timeframe for CSRF cookie | 15m |
159| flag: `--cookie-csrf-per-request`<br/>toml:`cookie_csrf_per_request` | bool | Enable having different CSRF cookies per request, making it possible to have parallel requests. | false |
159| flag: `--cookie-csrf-per-request`<br/>toml:`cookie_csrf_per_request` | bool | Enable having different CSRF cookies per request, making it possible to have parallel requests. | false |
160| flag: `--cookie-csrf-per-request-limit`<br/>toml: `cookie_csrf_per_request_limit` | int | Sets a limit on the number of CSRF requests cookies that oauth2-proxy will create. The oldest cookie will be removed. Useful if users end up with 431 Request headers too large status codes. Only effective if --cookie-csrf-per-request is true | "infinite" |
160| flag: `--cookie-csrf-per-request-limit`<br/>toml: `cookie_csrf_per_request_limit` | int | Sets a limit on the number of CSRF requests cookies that oauth2-proxy will create. The oldest cookie will be removed. Useful if users end up with 431 Request headers too large status codes. Only effective if --cookie-csrf-per-request is true | "infinite" |
161+| flag: `--cookie-csrf-samesite`<br/>toml: `cookie_csrf_samesite` | string | set SameSite CSRF cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). When using the default setting, the CSRF cookie samesite value is taken from the session cookie configuration. | `""` |
161| flag: `--cookie-domain`<br/>toml: `cookie_domains` | string \| list | Optional cookie domains to force cookies to (e.g. `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match). | |
162| flag: `--cookie-domain`<br/>toml: `cookie_domains` | string \| list | Optional cookie domains to force cookies to (e.g. `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match). | |
162| flag: `--cookie-expire`<br/>toml: `cookie_expire` | duration | expire timeframe for cookie. If set to 0, cookie becomes a session-cookie which will expire when the browser is closed. | 168h0m0s |
163| flag: `--cookie-expire`<br/>toml: `cookie_expire` | duration | expire timeframe for cookie. If set to 0, cookie becomes a session-cookie which will expire when the browser is closed. | 168h0m0s |
163| flag: `--cookie-httponly`<br/>toml: `cookie_httponly` | bool | set HttpOnly cookie flag | true |
164| flag: `--cookie-httponly`<br/>toml: `cookie_httponly` | bool | set HttpOnly cookie flag | true |
@@ -166,7 +167,7 @@ Provider specific options can be found on their respective subpages.
166| flag: `--cookie-refresh`<br/>toml: `cookie_refresh` | duration | refresh the cookie after this duration; `0` to disable; not supported by all providers&nbsp;[^1] | |
167| flag: `--cookie-refresh`<br/>toml: `cookie_refresh` | duration | refresh the cookie after this duration; `0` to disable; not supported by all providers&nbsp;[^1] | |
167| flag: `--cookie-samesite`<br/>toml: `cookie_samesite` | string | set SameSite cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). | `""` |
168| flag: `--cookie-samesite`<br/>toml: `cookie_samesite` | string | set SameSite cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). | `""` |
168| flag: `--cookie-secret`<br/>toml: `cookie_secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
169| flag: `--cookie-secret`<br/>toml: `cookie_secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
169−| flag: `--cookie-secret-file`<br/>toml: `cookie_secret_file` | string | File containing the cookie secret (must be raw binary, exactly 16, 24, or 32 bytes). Use dd if=/dev/urandom bs=32 count=1 > cookie.secret to generate | |
170+| flag: `--cookie-secret-file`<br/>toml: `cookie_secret_file` | string | File containing the cookie secret (must be raw binary, exactly 16, 24, or 32 bytes). Use dd if=/dev/urandom bs=32 count=1 > cookie.secret to generate | |
170| flag: `--cookie-secure`<br/>toml: `cookie_secure` | bool | set [secure (HTTPS only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true |
171| flag: `--cookie-secure`<br/>toml: `cookie_secure` | bool | set [secure (HTTPS only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true |
171
172
172[^1]: The following providers support `--cookie-refresh`: ADFS, Azure, GitLab, Google, Keycloak and all other Identity Providers which support the full [OIDC specification](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens)
173[^1]: The following providers support `--cookie-refresh`: ADFS, Azure, GitLab, Google, Keycloak and all other Identity Providers which support the full [OIDC specification](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens)
@@ -212,7 +213,7 @@ Provider specific options can be found on their respective subpages.
212| Flag / Config Field | Type | Description | Default |
213| Flag / Config Field | Type | Description | Default |
213| ----------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------- | ------- |
214| ----------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------- | ------- |
214| flag: `--banner`<br/>toml: `banner` | string | custom (html) banner string. Use `"-"` to disable default banner. | |
215| flag: `--banner`<br/>toml: `banner` | string | custom (html) banner string. Use `"-"` to disable default banner. | |
215−| flag: `--custom-sign-in-logo`<br/>toml: `custom_sign_in_logo` | string | path or a URL to an custom image for the sign_in page logo. Use `"-"` to disable default logo. |
216+| flag: `--custom-sign-in-logo`<br/>toml: `custom_sign_in_logo` | string | path or a URL to an custom image for the sign_in page logo. Use `"-"` to disable default logo. | |
216| flag: `--custom-templates-dir`<br/>toml: `custom_templates_dir` | string | path to custom html templates | |
217| flag: `--custom-templates-dir`<br/>toml: `custom_templates_dir` | string | path to custom html templates | |
217| flag: `--display-htpasswd-form`<br/>toml: `display_htpasswd_form` | bool | display username / password login form if an htpasswd file is provided | true |
218| flag: `--display-htpasswd-form`<br/>toml: `display_htpasswd_form` | bool | display username / password login form if an htpasswd file is provided | true |
218| flag: `--footer`<br/>toml: `footer` | string | custom (html) footer string. Use `"-"` to disable default footer. (Can be used to obfuscate the version) | |
219| flag: `--footer`<br/>toml: `footer` | string | custom (html) footer string. Use `"-"` to disable default footer. (Can be used to obfuscate the version) | |
pkg/apis/options/cookie.go+3
@@ -24,6 +24,7 @@ type Cookie struct {
24 CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"`
24 CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"`
25 CSRFPerRequestLimit int `flag:"cookie-csrf-per-request-limit" cfg:"cookie_csrf_per_request_limit"`
25 CSRFPerRequestLimit int `flag:"cookie-csrf-per-request-limit" cfg:"cookie_csrf_per_request_limit"`
26 CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"`
26 CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"`
27+ CSRFSameSite string `flag:"cookie-csrf-samesite" cfg:"cookie_csrf_samesite"`
27}
28}
28
29
29func cookieFlagSet() *pflag.FlagSet {
30func cookieFlagSet() *pflag.FlagSet {
@@ -42,6 +43,7 @@ func cookieFlagSet() *pflag.FlagSet {
42 flagSet.Bool("cookie-csrf-per-request", false, "When this property is set to true, then the CSRF cookie name is built based on the state and varies per request. If property is set to false, then CSRF cookie has the same name for all requests.")
43 flagSet.Bool("cookie-csrf-per-request", false, "When this property is set to true, then the CSRF cookie name is built based on the state and varies per request. If property is set to false, then CSRF cookie has the same name for all requests.")
43 flagSet.Int("cookie-csrf-per-request-limit", 0, "Sets a limit on the number of CSRF requests cookies that oauth2-proxy will create. The oldest cookies will be removed. Useful if users end up with 431 Request headers too large status codes.")
44 flagSet.Int("cookie-csrf-per-request-limit", 0, "Sets a limit on the number of CSRF requests cookies that oauth2-proxy will create. The oldest cookies will be removed. Useful if users end up with 431 Request headers too large status codes.")
44 flagSet.Duration("cookie-csrf-expire", time.Duration(15)*time.Minute, "expire timeframe for CSRF cookie")
45 flagSet.Duration("cookie-csrf-expire", time.Duration(15)*time.Minute, "expire timeframe for CSRF cookie")
46+ flagSet.String("cookie-csrf-samesite", "", "set SameSite CSRF cookie attribute (ie: \"lax\", \"strict\", \"none\", or \"\"). When using the default setting, the CSRF cookie samesite value is taken from the session cookie configuration.")
45 return flagSet
47 return flagSet
46}
48}
47
49
@@ -61,6 +63,7 @@ func cookieDefaults() Cookie {
61 CSRFPerRequest: false,
63 CSRFPerRequest: false,
62 CSRFPerRequestLimit: 0,
64 CSRFPerRequestLimit: 0,
63 CSRFExpire: time.Duration(15) * time.Minute,
65 CSRFExpire: time.Duration(15) * time.Minute,
66+ CSRFSameSite: "",
64 }
67 }
65}
68}
66
69
pkg/cookies/cookies.go+18−8
@@ -7,14 +7,24 @@ import (
7 "strings"
7 "strings"
8 "time"
8 "time"
9
9
10− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
12 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
11 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
13)
12)
14
13
14+type CookieOptions struct {
15+ Name string
16+ Value string
17+ Domains []string
18+ Expiration time.Duration
19+ SameSite string
20+ Path string
21+ HTTPOnly bool
22+ Secure bool
23+}
24+
15// MakeCookieFromOptions constructs a cookie based on the given *options.CookieOptions,
25// MakeCookieFromOptions constructs a cookie based on the given *options.CookieOptions,
16// value and creation time
26// value and creation time
17−func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.Cookie, expiration time.Duration) *http.Cookie {
27+func MakeCookieFromOptions(req *http.Request, opts *CookieOptions) *http.Cookie {
18 domain := GetCookieDomain(req, opts.Domains)
28 domain := GetCookieDomain(req, opts.Domains)
19 // If nothing matches, create the cookie with the shortest domain
29 // If nothing matches, create the cookie with the shortest domain
20 if domain == "" && len(opts.Domains) > 0 {
30 if domain == "" && len(opts.Domains) > 0 {
@@ -26,8 +36,8 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, opts *o
26 }
36 }
27
37
28 c := &http.Cookie{
38 c := &http.Cookie{
29− Name: name,
39+ Name: opts.Name,
30− Value: value,
40+ Value: opts.Value,
31 Path: opts.Path,
41 Path: opts.Path,
32 Domain: domain,
42 Domain: domain,
33 HttpOnly: opts.HTTPOnly,
43 HttpOnly: opts.HTTPOnly,
@@ -35,9 +45,9 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, opts *o
35 SameSite: ParseSameSite(opts.SameSite),
45 SameSite: ParseSameSite(opts.SameSite),
36 }
46 }
37
47
38− if expiration > time.Duration(0) {
48+ if opts.Expiration > time.Duration(0) {
39− c.MaxAge = int(expiration.Seconds())
49+ c.MaxAge = int(opts.Expiration.Seconds())
40− } else if expiration < time.Duration(0) {
50+ } else if opts.Expiration < time.Duration(0) {
41 c.MaxAge = -1
51 c.MaxAge = -1
42 }
52 }
43
53
@@ -58,7 +68,7 @@ func GetCookieDomain(req *http.Request, cookieDomains []string) string {
58 return ""
68 return ""
59}
69}
60
70
61−// Parse a valid http.SameSite value from a user supplied string for use of making cookies.
71+// ParseSameSite a valid http.SameSite value from a user supplied string for use of making cookies.
62func ParseSameSite(v string) http.SameSite {
72func ParseSameSite(v string) http.SameSite {
63 switch v {
73 switch v {
64 case "lax":
74 case "lax":
pkg/cookies/cookies_suite_test.go+4
@@ -17,6 +17,10 @@ const (
17 cookieDomain = "o2p.cookies.test"
17 cookieDomain = "o2p.cookies.test"
18 cookiePath = "/cookie-tests"
18 cookiePath = "/cookie-tests"
19
19
20+ sameSiteLax = "lax"
21+ sameSiteStrict = "strict"
22+ sameSiteNone = "none"
23+
20 nowEpoch = 1609366421
24 nowEpoch = 1609366421
21)
25)
22
26
pkg/cookies/cookies_test.go+32−50
@@ -5,8 +5,6 @@ import (
5 "net/http"
5 "net/http"
6 "time"
6 "time"
7
7
8− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9−
10 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
11 . "github.com/onsi/ginkgo/v2"
9 . "github.com/onsi/ginkgo/v2"
12 . "github.com/onsi/gomega"
10 . "github.com/onsi/gomega"
@@ -82,16 +80,12 @@ var _ = Describe("Cookie Tests", func() {
82 Context("MakeCookieFromOptions", func() {
80 Context("MakeCookieFromOptions", func() {
83 type makeCookieFromOptionsTableInput struct {
81 type makeCookieFromOptionsTableInput struct {
84 host string
82 host string
85− name string
83+ opts CookieOptions
86− value string
87− opts options.Cookie
88− expiration time.Duration
89 now time.Time
84 now time.Time
90 expectedOutput int
85 expectedOutput int
91 }
86 }
92
87
93 validName := "_oauth2_proxy"
88 validName := "_oauth2_proxy"
94− validSecret := "secretthirtytwobytes+abcdefghijk"
95 domains := []string{"www.cookies.test"}
89 domains := []string{"www.cookies.test"}
96
90
97 now := time.Now()
91 now := time.Now()
@@ -106,62 +100,50 @@ var _ = Describe("Cookie Tests", func() {
106 )
100 )
107 Expect(err).ToNot(HaveOccurred())
101 Expect(err).ToNot(HaveOccurred())
108
102
109− Expect(MakeCookieFromOptions(req, in.name, in.value, &in.opts, in.expiration).MaxAge).To(Equal(in.expectedOutput))
103+ Expect(MakeCookieFromOptions(req, &in.opts).MaxAge).To(Equal(in.expectedOutput))
110 },
104 },
111 Entry("persistent cookie", makeCookieFromOptionsTableInput{
105 Entry("persistent cookie", makeCookieFromOptionsTableInput{
112− host: "www.cookies.test",
106+ host: "www.cookies.test",
113− name: validName,
107+ opts: CookieOptions{
114− value: "1",
115− opts: options.Cookie{
116− Name: validName,
108+ Name: validName,
117− Secret: validSecret,
109+ Value: "1",
118− Domains: domains,
110+ Domains: domains,
119− Path: "",
120− Expire: time.Hour,
121− Refresh: 15 * time.Minute,
111+ Expiration: 15 * time.Minute,
122− Secure: true,
112+ SameSite: "",
113+ Path: "",
123− HTTPOnly: false,
114+ HTTPOnly: false,
124− SameSite: "",
115+ Secure: true,
125 },
116 },
126− expiration: 15 * time.Minute,
127 now: now,
117 now: now,
128 expectedOutput: int((15 * time.Minute).Seconds()),
118 expectedOutput: int((15 * time.Minute).Seconds()),
129 }),
119 }),
130 Entry("persistent cookie to be cleared", makeCookieFromOptionsTableInput{
120 Entry("persistent cookie to be cleared", makeCookieFromOptionsTableInput{
131− host: "www.cookies.test",
121+ host: "www.cookies.test",
132− name: validName,
122+ opts: CookieOptions{
133− value: "1",
134− opts: options.Cookie{
135− Name: validName,
123+ Name: validName,
136− Secret: validSecret,
124+ Value: "1",
137− Domains: domains,
125+ Domains: domains,
138− Path: "",
139− Expire: time.Hour * -1,
126+ Expiration: time.Hour * -1,
140− Refresh: 15 * time.Minute,
127+ SameSite: "",
141− Secure: true,
128+ Path: "",
142− HTTPOnly: false,
129+ HTTPOnly: false,
143− SameSite: "",
130+ Secure: true,
144 },
131 },
145− expiration: time.Hour * -1,
146 now: now,
132 now: now,
147 expectedOutput: -1,
133 expectedOutput: -1,
148 }),
134 }),
149 Entry("session cookie", makeCookieFromOptionsTableInput{
135 Entry("session cookie", makeCookieFromOptionsTableInput{
150− host: "www.cookies.test",
136+ host: "www.cookies.test",
151− name: validName,
137+ opts: CookieOptions{
152− value: "1",
153− opts: options.Cookie{
154− Name: validName,
138+ Name: validName,
155− Secret: validSecret,
139+ Value: "1",
156− Domains: domains,
140+ Domains: domains,
157− Path: "",
141+ Expiration: 0,
158− Expire: 0,
142+ SameSite: "",
159− Refresh: 15 * time.Minute,
143+ Path: "",
144+ HTTPOnly: false,
160− Secure: true,
145+ Secure: true,
161− HTTPOnly: false,
162− SameSite: "",
163 },
146 },
164− expiration: 0,
165 now: now,
147 now: now,
166 expectedOutput: expectedMaxAge,
148 expectedOutput: expectedMaxAge,
167 }),
149 }),
pkg/cookies/csrf.go+33−14
@@ -134,6 +134,15 @@ func (c *csrf) SetSessionNonce(s *sessions.SessionState) {
134 s.Nonce = c.OIDCNonce
134 s.Nonce = c.OIDCNonce
135}
135}
136
136
137+// getCSRFSameSite get the CSRF same site
138+func getCSRFSameSite(opts *options.Cookie) string {
139+ sameSite := opts.CSRFSameSite
140+ if sameSite == "" {
141+ sameSite = opts.SameSite
142+ }
143+ return sameSite
144+}
145+
137// SetCookie encodes the CSRF to a signed cookie and sets it on the ResponseWriter
146// SetCookie encodes the CSRF to a signed cookie and sets it on the ResponseWriter
138func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cookie, error) {
147func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cookie, error) {
139 encoded, err := c.encodeCookie()
148 encoded, err := c.encodeCookie()
@@ -141,13 +150,18 @@ func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cooki
141 return nil, err
150 return nil, err
142 }
151 }
143
152
144− cookie := MakeCookieFromOptions(
153+ csrfCookieOptions := &CookieOptions{
145− req,
146− c.cookieName(),
154+ Name: c.cookieName(),
147− encoded,
155+ Value: encoded,
148− c.cookieOpts,
156+ Domains: c.cookieOpts.Domains,
149− c.cookieOpts.CSRFExpire,
157+ Expiration: c.cookieOpts.CSRFExpire,
150− )
158+ SameSite: getCSRFSameSite(c.cookieOpts),
159+ Path: c.cookieOpts.Path,
160+ HTTPOnly: c.cookieOpts.HTTPOnly,
161+ Secure: c.cookieOpts.Secure,
162+ }
163+
164+ cookie := MakeCookieFromOptions(req, csrfCookieOptions)
151 http.SetCookie(rw, cookie)
165 http.SetCookie(rw, cookie)
152
166
153 return cookie, nil
167 return cookie, nil
@@ -197,13 +211,18 @@ func ClearExtraCsrfCookies(opts *options.Cookie, rw http.ResponseWriter, req *ht
197
211
198// ClearCookie removes the CSRF cookie
212// ClearCookie removes the CSRF cookie
199func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) {
213func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) {
214+ csrfCookieOptions := &CookieOptions{
215+ Name: c.cookieName(),
216+ Value: "",
217+ Domains: c.cookieOpts.Domains,
218+ Expiration: time.Hour * -1,
219+ SameSite: getCSRFSameSite(c.cookieOpts),
220+ Path: c.cookieOpts.Path,
221+ HTTPOnly: c.cookieOpts.HTTPOnly,
222+ Secure: c.cookieOpts.Secure,
223+ }
224+
200− http.SetCookie(rw, MakeCookieFromOptions(
225+ http.SetCookie(rw, MakeCookieFromOptions(req, csrfCookieOptions))
201− req,
202− c.cookieName(),
203− "",
204− c.cookieOpts,
205− time.Hour*-1,
206− ))
207}
226}
208
227
209// encodeCookie MessagePack encodes and encrypts the CSRF and then creates a
228// encodeCookie MessagePack encodes and encrypts the CSRF and then creates a
pkg/cookies/csrf_per_request_test.go+12−7
@@ -216,13 +216,18 @@ var _ = Describe("CSRF Cookie with non-fixed name Tests", func() {
216 for _, csrf := range []*csrf{privateCSRF1, privateCSRF2, privateCSRF3} {
216 for _, csrf := range []*csrf{privateCSRF1, privateCSRF2, privateCSRF3} {
217 encoded, err := csrf.encodeCookie()
217 encoded, err := csrf.encodeCookie()
218 Expect(err).ToNot(HaveOccurred())
218 Expect(err).ToNot(HaveOccurred())
219− cookie := MakeCookieFromOptions(
219+ csrfCookieOptions := &CookieOptions{
220− req,
221− csrf.cookieName(),
220+ Name: csrf.cookieName(),
222− encoded,
221+ Value: encoded,
223− csrf.cookieOpts,
222+ Domains: csrf.cookieOpts.Domains,
224− csrf.cookieOpts.CSRFExpire,
223+ Expiration: csrf.cookieOpts.CSRFExpire,
225− )
224+ SameSite: getCSRFSameSite(csrf.cookieOpts),
225+ Path: csrf.cookieOpts.Path,
226+ HTTPOnly: csrf.cookieOpts.HTTPOnly,
227+ Secure: csrf.cookieOpts.Secure,
228+ }
229+
230+ cookie := MakeCookieFromOptions(req, csrfCookieOptions)
226 cookies = append(cookies, fmt.Sprintf("%v=%v", cookie.Name, cookie.Value))
231 cookies = append(cookies, fmt.Sprintf("%v=%v", cookie.Name, cookie.Value))
227 }
232 }
228
233
pkg/cookies/csrf_test.go+259
@@ -278,4 +278,263 @@ var _ = Describe("CSRF Cookie Tests", func() {
278 })
278 })
279 })
279 })
280 })
280 })
281+
282+ Context("Test Cookie SameSite", func() {
283+ var req *http.Request
284+ var cookieOpts *options.Cookie
285+
286+ testNow := time.Unix(nowEpoch, 0)
287+
288+ BeforeEach(func() {
289+ // we need to reset the time to ensure the cookie is valid
290+ privateCSRF.clock = time.Now
291+
292+ req = &http.Request{
293+ Method: http.MethodGet,
294+ Proto: "HTTP/1.1",
295+ Host: cookieDomain,
296+
297+ URL: &url.URL{
298+ Scheme: "https",
299+ Host: cookieDomain,
300+ Path: cookiePath,
301+ },
302+ }
303+
304+ cookieOpts = &options.Cookie{
305+ Name: cookieName,
306+ Secret: cookieSecret,
307+ Domains: []string{cookieDomain},
308+ Path: cookiePath,
309+ Expire: time.Hour,
310+ Secure: true,
311+ HTTPOnly: true,
312+ CSRFPerRequest: false,
313+ CSRFExpire: time.Hour,
314+ }
315+ })
316+
317+ It("Call SetCookie when CSRF SameSite is not defined. Expected result: CSRF cookie SameSite is the same as session cookie.", func() {
318+ // prepare
319+ cookieOpts.SameSite = sameSiteLax
320+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
321+ rw := httptest.NewRecorder()
322+ CSRF.(*csrf).clock = func() time.Time { return testNow }
323+
324+ // test
325+ _, err := CSRF.SetCookie(rw, req)
326+
327+ // validate
328+ Expect(err).ToNot(HaveOccurred())
329+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
330+ fmt.Sprintf(
331+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
332+ cookiePath,
333+ cookieDomain,
334+ int(cookieOpts.CSRFExpire.Seconds()),
335+ ),
336+ ))
337+ })
338+
339+ It("Call SetCookie when CSRF SameSite is an empty string. Expected result: CSRF cookie SameSite is the same as session cookie.", func() {
340+ // prepare
341+ cookieOpts.SameSite = sameSiteLax
342+ cookieOpts.CSRFSameSite = ""
343+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
344+ rw := httptest.NewRecorder()
345+ CSRF.(*csrf).clock = func() time.Time { return testNow }
346+
347+ // test
348+ _, err := CSRF.SetCookie(rw, req)
349+
350+ // validate
351+ Expect(err).ToNot(HaveOccurred())
352+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
353+ fmt.Sprintf(
354+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
355+ cookiePath,
356+ cookieDomain,
357+ int(cookieOpts.CSRFExpire.Seconds()),
358+ ),
359+ ))
360+ })
361+
362+ It("Call SetCookie when CSRF SameSite is 'none'. Expected result: CSRF cookie SameSite is None.", func() {
363+ // prepare
364+ cookieOpts.SameSite = sameSiteLax
365+ cookieOpts.CSRFSameSite = sameSiteNone
366+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
367+ rw := httptest.NewRecorder()
368+ CSRF.(*csrf).clock = func() time.Time { return testNow }
369+
370+ // test
371+ _, err := CSRF.SetCookie(rw, req)
372+
373+ // validate
374+ Expect(err).ToNot(HaveOccurred())
375+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
376+ fmt.Sprintf(
377+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=None",
378+ cookiePath,
379+ cookieDomain,
380+ int(cookieOpts.CSRFExpire.Seconds()),
381+ ),
382+ ))
383+ })
384+
385+ It("Call SetCookie when CSRF SameSite is 'strict'. Expected result: CSRF cookie SameSite is Strict.", func() {
386+ // prepare
387+ cookieOpts.SameSite = sameSiteLax
388+ cookieOpts.CSRFSameSite = sameSiteStrict
389+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
390+ rw := httptest.NewRecorder()
391+ CSRF.(*csrf).clock = func() time.Time { return testNow }
392+
393+ // test
394+ _, err := CSRF.SetCookie(rw, req)
395+
396+ // validate
397+ Expect(err).ToNot(HaveOccurred())
398+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
399+ fmt.Sprintf(
400+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Strict",
401+ cookiePath,
402+ cookieDomain,
403+ int(cookieOpts.CSRFExpire.Seconds()),
404+ ),
405+ ))
406+ })
407+
408+ It("Call SetCookie when CSRF SameSite is 'lax'. Expected result: CSRF cookie SameSite is Lax.", func() {
409+ // prepare
410+ cookieOpts.SameSite = sameSiteStrict
411+ cookieOpts.CSRFSameSite = sameSiteLax
412+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
413+ rw := httptest.NewRecorder()
414+ CSRF.(*csrf).clock = func() time.Time { return testNow }
415+
416+ // test
417+ _, err := CSRF.SetCookie(rw, req)
418+
419+ // validate
420+ Expect(err).ToNot(HaveOccurred())
421+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
422+ fmt.Sprintf(
423+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
424+ cookiePath,
425+ cookieDomain,
426+ int(cookieOpts.CSRFExpire.Seconds()),
427+ ),
428+ ))
429+ })
430+
431+ It("Call ClearCookie when CSRF SameSite is not defined. Expected result: CSRF cookie SameSite is the same as session cookie.", func() {
432+ // prepare
433+ cookieOpts.SameSite = sameSiteLax
434+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
435+ rw := httptest.NewRecorder()
436+ CSRF.(*csrf).clock = func() time.Time { return testNow }
437+
438+ // test
439+ CSRF.ClearCookie(rw, req)
440+
441+ // validate
442+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
443+ fmt.Sprintf(
444+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax",
445+ CSRF.(*csrf).cookieName(),
446+ cookiePath,
447+ cookieDomain,
448+ ),
449+ ))
450+ })
451+
452+ It("Call ClearCookie when CSRF SameSite is an empty string. Expected result: CSRF cookie SameSite is the same as session cookie.", func() {
453+ // prepare
454+ cookieOpts.SameSite = sameSiteLax
455+ cookieOpts.CSRFSameSite = ""
456+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
457+ rw := httptest.NewRecorder()
458+ CSRF.(*csrf).clock = func() time.Time { return testNow }
459+
460+ // test
461+ CSRF.ClearCookie(rw, req)
462+
463+ // validate
464+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
465+ fmt.Sprintf(
466+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax",
467+ CSRF.(*csrf).cookieName(),
468+ cookiePath,
469+ cookieDomain,
470+ ),
471+ ))
472+ })
473+
474+ It("Call ClearCookie when CSRF SameSite is 'none'. Expected result: CSRF cookie SameSite is None.", func() {
475+ // prepare
476+ cookieOpts.SameSite = sameSiteLax
477+ cookieOpts.CSRFSameSite = sameSiteNone
478+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
479+ rw := httptest.NewRecorder()
480+ CSRF.(*csrf).clock = func() time.Time { return testNow }
481+
482+ // test
483+ CSRF.ClearCookie(rw, req)
484+
485+ // validate
486+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
487+ fmt.Sprintf(
488+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=None",
489+ CSRF.(*csrf).cookieName(),
490+ cookiePath,
491+ cookieDomain,
492+ ),
493+ ))
494+ })
495+
496+ It("Call ClearCookie when CSRF SameSite is 'strict'. Expected result: CSRF cookie SameSite is Strict.", func() {
497+ // prepare
498+ cookieOpts.SameSite = sameSiteLax
499+ cookieOpts.CSRFSameSite = sameSiteStrict
500+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
501+ rw := httptest.NewRecorder()
502+ CSRF.(*csrf).clock = func() time.Time { return testNow }
503+
504+ // test
505+ CSRF.ClearCookie(rw, req)
506+
507+ // validate
508+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
509+ fmt.Sprintf(
510+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Strict",
511+ CSRF.(*csrf).cookieName(),
512+ cookiePath,
513+ cookieDomain,
514+ ),
515+ ))
516+ })
517+
518+ It("Call ClearCookie when CSRF SameSite is 'lax'. Expected result: CSRF cookie SameSite is Lax.", func() {
519+ // prepare
520+ cookieOpts.SameSite = sameSiteStrict
521+ cookieOpts.CSRFSameSite = sameSiteLax
522+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
523+ rw := httptest.NewRecorder()
524+ CSRF.(*csrf).clock = func() time.Time { return testNow }
525+
526+ // test
527+ CSRF.ClearCookie(rw, req)
528+
529+ // validate
530+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
531+ fmt.Sprintf(
532+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax",
533+ CSRF.(*csrf).cookieName(),
534+ cookiePath,
535+ cookieDomain,
536+ ),
537+ ))
538+ })
539+ })
281})
540})
pkg/sessions/cookie/session_store.go+23−13
@@ -76,7 +76,17 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
76
76
77 for _, c := range req.Cookies() {
77 for _, c := range req.Cookies() {
78 if cookieNameRegex.MatchString(c.Name) {
78 if cookieNameRegex.MatchString(c.Name) {
79− clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1)
79+ sessionCookieOptions := &pkgcookies.CookieOptions{
80+ Name: c.Name,
81+ Value: "",
82+ Domains: s.Cookie.Domains,
83+ Expiration: time.Hour * -1,
84+ SameSite: s.Cookie.SameSite,
85+ Path: s.Cookie.Path,
86+ HTTPOnly: s.Cookie.HTTPOnly,
87+ Secure: s.Cookie.Secure,
88+ }
89+ clearCookie := pkgcookies.MakeCookieFromOptions(req, sessionCookieOptions)
80
90
81 http.SetCookie(rw, clearCookie)
91 http.SetCookie(rw, clearCookie)
82 }
92 }
@@ -117,7 +127,7 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques
117 return nil
127 return nil
118}
128}
119
129
120−// makeSessionCookie creates an http.Cookie containing the authenticated user's
130+// makeSessionCookie creates a http.Cookie containing the authenticated user's
121// authentication details
131// authentication details
122func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now time.Time) ([]*http.Cookie, error) {
132func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now time.Time) ([]*http.Cookie, error) {
123 strValue := string(value)
133 strValue := string(value)
@@ -132,23 +142,23 @@ func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now ti
132 return nil, err
142 return nil, err
133 }
143 }
134 }
144 }
135− c := s.makeCookie(req, s.Cookie.Name, strValue, s.Cookie.Expire)
145+ sessionCookieOptions := &pkgcookies.CookieOptions{
146+ Name: s.Cookie.Name,
147+ Value: strValue,
148+ Domains: s.Cookie.Domains,
149+ Expiration: s.Cookie.Expire,
150+ SameSite: s.Cookie.SameSite,
151+ Path: s.Cookie.Path,
152+ HTTPOnly: s.Cookie.HTTPOnly,
153+ Secure: s.Cookie.Secure,
154+ }
155+ c := pkgcookies.MakeCookieFromOptions(req, sessionCookieOptions)
136 if len(c.String()) > maxCookieLength {
156 if len(c.String()) > maxCookieLength {
137 return splitCookie(c), nil
157 return splitCookie(c), nil
138 }
158 }
139 return []*http.Cookie{c}, nil
159 return []*http.Cookie{c}, nil
140}
160}
141
161
142−func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie {
143− return pkgcookies.MakeCookieFromOptions(
144− req,
145− name,
146− value,
147− s.Cookie,
148− expiration,
149− )
150−}
151−
152// NewCookieSessionStore initialises a new instance of the SessionStore from
162// NewCookieSessionStore initialises a new instance of the SessionStore from
153// the configuration given
163// the configuration given
154func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
164func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
pkg/sessions/persistence/ticket.go+23−14
@@ -221,13 +221,17 @@ func (t *ticket) setCookie(rw http.ResponseWriter, req *http.Request, s *session
221// clearCookie removes any cookies that would be where this ticket
221// clearCookie removes any cookies that would be where this ticket
222// would set them
222// would set them
223func (t *ticket) clearCookie(rw http.ResponseWriter, req *http.Request) {
223func (t *ticket) clearCookie(rw http.ResponseWriter, req *http.Request) {
224+ cookieOptions := &cookies.CookieOptions{
225+ Name: t.options.Name,
226+ Value: "",
227+ Domains: t.options.Domains,
228+ Expiration: time.Hour * -1,
229+ SameSite: t.options.SameSite,
230+ Path: t.options.Path,
231+ HTTPOnly: t.options.HTTPOnly,
232+ Secure: t.options.Secure,
233+ }
224− http.SetCookie(rw, cookies.MakeCookieFromOptions(
234+ http.SetCookie(rw, cookies.MakeCookieFromOptions(req, cookieOptions))
225− req,
226− t.options.Name,
227− "",
228− t.options,
229− time.Hour*-1,
230− ))
231}
235}
232
236
233// makeCookie makes a cookie, signing the value if present
237// makeCookie makes a cookie, signing the value if present
@@ -244,13 +248,18 @@ func (t *ticket) makeCookie(req *http.Request, value string, expires time.Durati
244 }
248 }
245 }
249 }
246
250
251+ cookieOptions := &cookies.CookieOptions{
252+ Name: t.options.Name,
253+ Value: value,
254+ Domains: t.options.Domains,
255+ Expiration: expires,
256+ SameSite: t.options.SameSite,
257+ Path: t.options.Path,
258+ HTTPOnly: t.options.HTTPOnly,
259+ Secure: t.options.Secure,
260+ }
261+
247− return cookies.MakeCookieFromOptions(
262+ return cookies.MakeCookieFromOptions(req, cookieOptions), nil
248− req,
249− t.options.Name,
250− value,
251− t.options,
252− expires,
253− ), nil
254}
263}
255
264
256// makeCipher makes a AES-GCM cipher out of the ticket's secret
265// makeCipher makes a AES-GCM cipher out of the ticket's secret
pkg/sessions/tests/session_store_tests.go+11−1
@@ -422,7 +422,17 @@ func SessionStoreInterfaceTests(in *testInput) {
422 broken := "BrokenSessionFromADifferentSessionImplementation"
422 broken := "BrokenSessionFromADifferentSessionImplementation"
423 value, err := encryption.SignedValue(in.cookieOpts.Secret, in.cookieOpts.Name, []byte(broken), time.Now())
423 value, err := encryption.SignedValue(in.cookieOpts.Secret, in.cookieOpts.Name, []byte(broken), time.Now())
424 Expect(err).ToNot(HaveOccurred())
424 Expect(err).ToNot(HaveOccurred())
425+ cookieOptions := &cookiesapi.CookieOptions{
426+ Name: in.cookieOpts.Name,
427+ Value: value,
428+ Domains: in.cookieOpts.Domains,
429+ Expiration: in.cookieOpts.Expire,
430+ SameSite: in.cookieOpts.SameSite,
431+ Path: in.cookieOpts.Path,
432+ HTTPOnly: in.cookieOpts.HTTPOnly,
433+ Secure: in.cookieOpts.Secure,
434+ }
425− cookie := cookiesapi.MakeCookieFromOptions(in.request, in.cookieOpts.Name, value, in.cookieOpts, in.cookieOpts.Expire)
435+ cookie := cookiesapi.MakeCookieFromOptions(in.request, cookieOptions)
426 in.request.AddCookie(cookie)
436 in.request.AddCookie(cookie)
427
437
428 err = in.ss().Save(in.response, in.request, in.session)
438 err = in.ss().Save(in.response, in.request, in.session)