You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-08-06 22:42:56 +02:00
feat(cookie) csrf per request limit (#3134)
* Allow setting maximum number of csrf cookies, deleting the oldest if necessary * Add a test for multiple CSRF cookies to remove the old cookie * Add docs/changelog * If limit is <=0 do not clear Signed-off-by: test <bert@transtrend.com> * Better docs Co-authored-by: Jan Larwig <jan@larwig.com> * direct check of option value Co-authored-by: Jan Larwig <jan@larwig.com> * direct use of option value Co-authored-by: Jan Larwig <jan@larwig.com> * sort based on clock compare vs time compare Co-authored-by: Jan Larwig <jan@larwig.com> * clock.Clock does not implement Compare, fix csrf cookie extraction after rename Signed-off-by: Bert Helderman <bert@transtrend.com> * Linter fix * add method signature documentation and slight formatting Signed-off-by: Jan Larwig <jan@larwig.com> * fix: test case for csrf cookie limit and flag Signed-off-by: Jan Larwig <jan@larwig.com> --------- Signed-off-by: Bert Helderman <bert@transtrend.com> Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: test <bert@transtrend.com> Co-authored-by: bh-tt <71650427+bh-tt@users.noreply.github.com>
This commit is contained in:
@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
## Changes since v7.10.0
|
## Changes since v7.10.0
|
||||||
|
|
||||||
|
- [#2615](https://github.com/oauth2-proxy/oauth2-proxy/pull/2615) feat(cookies): add option to set a limit on the number of per-request CSRF cookies oauth2-proxy sets (@bh-tt)
|
||||||
|
|
||||||
# V7.10.0
|
# V7.10.0
|
||||||
|
|
||||||
## Release Highlights
|
## Release Highlights
|
||||||
|
@ -71,6 +71,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
|
|||||||
| `--config` | path to config file |
|
| `--config` | path to config file |
|
||||||
| `--version` | print version string |
|
| `--version` | print version string |
|
||||||
|
|
||||||
|
|
||||||
### General Provider Options
|
### General Provider Options
|
||||||
|
|
||||||
Provider specific options can be found on their respective subpages.
|
Provider specific options can be found on their respective subpages.
|
||||||
@ -91,7 +92,7 @@ Provider specific options can be found on their respective subpages.
|
|||||||
| flag: `--jwt-key-file`<br/>toml: `jwt_key_file` | string | path to the private key file in PEM format used to sign the JWT so that you can say something like `--jwt-key-file=/etc/ssl/private/jwt_signing_key.pem`: required by login.gov | |
|
| flag: `--jwt-key-file`<br/>toml: `jwt_key_file` | string | path to the private key file in PEM format used to sign the JWT so that you can say something like `--jwt-key-file=/etc/ssl/private/jwt_signing_key.pem`: required by login.gov | |
|
||||||
| flag: `--jwt-key`<br/>toml: `jwt_key` | string | private key in PEM format used to sign JWT, so that you can say something like `--jwt-key="${OAUTH2_PROXY_JWT_KEY}"`: required by login.gov | |
|
| flag: `--jwt-key`<br/>toml: `jwt_key` | string | private key in PEM format used to sign JWT, so that you can say something like `--jwt-key="${OAUTH2_PROXY_JWT_KEY}"`: required by login.gov | |
|
||||||
| flag: `--login-url`<br/>toml: `login_url` | string | Authentication endpoint | |
|
| flag: `--login-url`<br/>toml: `login_url` | string | Authentication endpoint | |
|
||||||
| flag: `--auth-request-response-mode`<br/>toml: `auth-request-response-mode` | string | Response mode to ask for during authentication request | |
|
| flag: `--auth-request-response-mode`<br/>toml: `auth-request-response-mode` | string | Response mode to ask for during authentication request | |
|
||||||
| flag: `--oidc-audience-claim`<br/>toml: `oidc_audience_claims` | string | which OIDC claim contains the audience | `"aud"` |
|
| flag: `--oidc-audience-claim`<br/>toml: `oidc_audience_claims` | string | which OIDC claim contains the audience | `"aud"` |
|
||||||
| flag: `--oidc-email-claim`<br/>toml: `oidc_email_claim` | string | which OIDC claim contains the user's email | `"email"` |
|
| flag: `--oidc-email-claim`<br/>toml: `oidc_email_claim` | string | which OIDC claim contains the user's email | `"email"` |
|
||||||
| flag: `--oidc-extra-audience`<br/>toml: `oidc_extra_audiences` | string \| list | additional audiences which are allowed to pass verification | `"[]"` |
|
| flag: `--oidc-extra-audience`<br/>toml: `oidc_extra_audiences` | string \| list | additional audiences which are allowed to pass verification | `"[]"` |
|
||||||
@ -114,19 +115,20 @@ Provider specific options can be found on their respective subpages.
|
|||||||
|
|
||||||
### Cookie Options
|
### Cookie Options
|
||||||
|
|
||||||
| Flag / Config Field | Type | Description | Default |
|
| Flag / Config Field | Type | Description | Default |
|
||||||
| -------------------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
| --------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
||||||
| flag: `--cookie-csrf-expire`<br/>toml: `cookie_csrf_expire` | duration | expire timeframe for CSRF cookie | 15m |
|
| flag: `--cookie-csrf-expire`<br/>toml: `cookie_csrf_expire` | duration | expire timeframe for CSRF cookie | 15m |
|
||||||
| 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 |
|
| 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 |
|
||||||
| 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). | |
|
| 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" |
|
||||||
| 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 |
|
| 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). | |
|
||||||
| flag: `--cookie-httponly`<br/>toml: `cookie_httponly` | bool | set HttpOnly cookie flag | true |
|
| 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 |
|
||||||
| flag: `--cookie-name`<br/>toml: `cookie_name` | string | the name of the cookie that the oauth_proxy creates. Should be changed to use a [cookie prefix](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes) (`__Host-` or `__Secure-`) if `--cookie-secure` is set. | `"_oauth2_proxy"` |
|
| flag: `--cookie-httponly`<br/>toml: `cookie_httponly` | bool | set HttpOnly cookie flag | true |
|
||||||
| flag: `--cookie-path`<br/>toml: `cookie_path` | string | an optional cookie path to force cookies to (e.g. `/poc/`) | `"/"` |
|
| flag: `--cookie-name`<br/>toml: `cookie_name` | string | the name of the cookie that the oauth_proxy creates. Should be changed to use a [cookie prefix](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes) (`__Host-` or `__Secure-`) if `--cookie-secure` is set. | `"_oauth2_proxy"` |
|
||||||
| flag: `--cookie-refresh`<br/>toml: `cookie_refresh` | duration | refresh the cookie after this duration; `0` to disable; not supported by all providers [^1] | |
|
| flag: `--cookie-path`<br/>toml: `cookie_path` | string | an optional cookie path to force cookies to (e.g. `/poc/`) | `"/"` |
|
||||||
| flag: `--cookie-samesite`<br/>toml: `cookie_samesite` | string | set SameSite cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). | `""` |
|
| flag: `--cookie-refresh`<br/>toml: `cookie_refresh` | duration | refresh the cookie after this duration; `0` to disable; not supported by all providers [^1] | |
|
||||||
| flag: `--cookie-secret`<br/>toml: `cookie_secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
|
| flag: `--cookie-samesite`<br/>toml: `cookie_samesite` | string | set SameSite cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). | `""` |
|
||||||
| flag: `--cookie-secure`<br/>toml: `cookie_secure` | bool | set [secure (HTTPS only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true |
|
| flag: `--cookie-secret`<br/>toml: `cookie_secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
|
||||||
|
| flag: `--cookie-secure`<br/>toml: `cookie_secure` | bool | set [secure (HTTPS only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true |
|
||||||
|
|
||||||
[^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)
|
[^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)
|
||||||
|
|
||||||
@ -188,33 +190,33 @@ Provider specific options can be found on their respective subpages.
|
|||||||
|
|
||||||
### Proxy Options
|
### Proxy Options
|
||||||
|
|
||||||
| Flag / Config Field | Type | Description | Default |
|
| Flag / Config Field | Type | Description | Default |
|
||||||
| ------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||||
| flag: `--allow-query-semicolons`<br/>toml: `allow_query_semicolons` | bool | allow the use of semicolons in query args ([required for some legacy applications](https://github.com/golang/go/issues/25192)) | `false` |
|
| flag: `--allow-query-semicolons`<br/>toml: `allow_query_semicolons` | bool | allow the use of semicolons in query args ([required for some legacy applications](https://github.com/golang/go/issues/25192)) | `false` |
|
||||||
| flag: `--api-route`<br/>toml: `api_routes` | string \| list | Requests to these paths must already be authenticated with a cookie, or a JWT if `--skip-jwt-bearer-tokens` is set. No redirect to login will be done. Return 401 if not. Format: path_regex | |
|
| flag: `--api-route`<br/>toml: `api_routes` | string \| list | Requests to these paths must already be authenticated with a cookie, or a JWT if `--skip-jwt-bearer-tokens` is set. No redirect to login will be done. Return 401 if not. Format: path_regex | |
|
||||||
| flag: `--authenticated-emails-file`<br/>toml: `authenticated_emails_file` | string | authenticate against emails via file (one per line) | |
|
| flag: `--authenticated-emails-file`<br/>toml: `authenticated_emails_file` | string | authenticate against emails via file (one per line) | |
|
||||||
| flag: `--bearer-token-login-fallback`<br/>toml: `bearer_token_login_fallback` | bool | if `--skip-jwt-bearer-tokens` is set, if a request includes an invalid JWT (expired, malformed, missing required audiences, etc), fall back to normal login redirect as if the token were not sent at all. If false, respond 403 | true |
|
| flag: `--bearer-token-login-fallback`<br/>toml: `bearer_token_login_fallback` | bool | if `--skip-jwt-bearer-tokens` is set, if a request includes an invalid JWT (expired, malformed, missing required audiences, etc), fall back to normal login redirect as if the token were not sent at all. If false, respond 403 | true |
|
||||||
| flag: `--email-domain`<br/>toml: `email_domains` | string \| list | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | |
|
| flag: `--email-domain`<br/>toml: `email_domains` | string \| list | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | |
|
||||||
| flag: `--encode-state`<br/>toml: `encode_state` | bool | encode the state parameter as UrlEncodedBase64 | false |
|
| flag: `--encode-state`<br/>toml: `encode_state` | bool | encode the state parameter as UrlEncodedBase64 | false |
|
||||||
| flag: `--extra-jwt-issuers`<br/>toml: `extra_jwt_issuers` | string | if `--skip-jwt-bearer-tokens` is set, a list of extra JWT `issuer=audience` (see a token's `iss`, `aud` fields) pairs (where the issuer URL has a `.well-known/openid-configuration` or a `.well-known/jwks.json`) | |
|
| flag: `--extra-jwt-issuers`<br/>toml: `extra_jwt_issuers` | string | if `--skip-jwt-bearer-tokens` is set, a list of extra JWT `issuer=audience` (see a token's `iss`, `aud` fields) pairs (where the issuer URL has a `.well-known/openid-configuration` or a `.well-known/jwks.json`) | |
|
||||||
| flag: `--force-https`<br/>toml: `force_https` | bool | enforce https redirect | `false` |
|
| flag: `--force-https`<br/>toml: `force_https` | bool | enforce https redirect | `false` |
|
||||||
| flag: `--force-json-errors`<br/>toml: `force_json_errors` | bool | force JSON errors instead of HTTP error pages or redirects | `false` |
|
| flag: `--force-json-errors`<br/>toml: `force_json_errors` | bool | force JSON errors instead of HTTP error pages or redirects | `false` |
|
||||||
| flag: `--htpasswd-file`<br/>toml: `htpasswd_file` | string | additionally authenticate against a htpasswd file. Entries must be created with `htpasswd -B` for bcrypt encryption | |
|
| flag: `--htpasswd-file`<br/>toml: `htpasswd_file` | string | additionally authenticate against a htpasswd file. Entries must be created with `htpasswd -B` for bcrypt encryption | |
|
||||||
| flag: `--htpasswd-user-group`<br/>toml: `htpasswd_user_groups` | string \| list | the groups to be set on sessions for htpasswd users | |
|
| flag: `--htpasswd-user-group`<br/>toml: `htpasswd_user_groups` | string \| list | the groups to be set on sessions for htpasswd users | |
|
||||||
| flag: `--proxy-prefix`<br/>toml: `proxy_prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
|
| flag: `--proxy-prefix`<br/>toml: `proxy_prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
|
||||||
| flag: `--real-client-ip-header`<br/>toml: `real_client_ip_header` | string | Header used to determine the real IP of the client, requires `--reverse-proxy` to be set (one of: X-Forwarded-For, X-Real-IP, X-ProxyUser-IP, X-Envoy-External-Address, or CF-Connecting-IP) | X-Real-IP |
|
| flag: `--real-client-ip-header`<br/>toml: `real_client_ip_header` | string | Header used to determine the real IP of the client, requires `--reverse-proxy` to be set (one of: X-Forwarded-For, X-Real-IP, X-ProxyUser-IP, X-Envoy-External-Address, or CF-Connecting-IP) | X-Real-IP |
|
||||||
| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
|
| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
|
||||||
| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
|
| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
|
||||||
| flag: `--reverse-proxy`<br/>toml: `reverse_proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-IP are accepted and allows X-Forwarded-\{Proto,Host,Uri\} headers to be used on redirect selection | false |
|
| flag: `--reverse-proxy`<br/>toml: `reverse_proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-IP are accepted and allows X-Forwarded-\{Proto,Host,Uri\} headers to be used on redirect selection | false |
|
||||||
| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
|
| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
|
||||||
| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
|
| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
|
||||||
| flag: `--skip-auth-regex`<br/>toml: `skip_auth_regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
|
| flag: `--skip-auth-regex`<br/>toml: `skip_auth_regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
|
||||||
| flag: `--skip-auth-route`<br/>toml: `skip_auth_routes` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex | |
|
| flag: `--skip-auth-route`<br/>toml: `skip_auth_routes` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex | |
|
||||||
| flag: `--skip-jwt-bearer-tokens`<br/>toml: `skip_jwt_bearer_tokens` | bool | will skip requests that have verified JWT bearer tokens (the token must have [`aud`](https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields) that matches this client id or one of the extras from `extra-jwt-issuers`) | false |
|
| flag: `--skip-jwt-bearer-tokens`<br/>toml: `skip_jwt_bearer_tokens` | bool | will skip requests that have verified JWT bearer tokens (the token must have [`aud`](https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields) that matches this client id or one of the extras from `extra-jwt-issuers`) | false |
|
||||||
| flag: `--skip-provider-button`<br/>toml: `skip_provider_button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
|
| flag: `--skip-provider-button`<br/>toml: `skip_provider_button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
|
||||||
| flag: `--ssl-insecure-skip-verify`<br/>toml: `ssl_insecure_skip_verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
|
| flag: `--ssl-insecure-skip-verify`<br/>toml: `ssl_insecure_skip_verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
|
||||||
| flag: `--trusted-ip`<br/>toml: `trusted_ips` | string \| list | list of IPs or CIDR ranges to allow to bypass authentication (may be given multiple times). When combined with `--reverse-proxy` and optionally `--real-client-ip-header` this will evaluate the trust of the IP stored in an HTTP header by a reverse proxy rather than the layer-3/4 remote address. WARNING: trusting IPs has inherent security flaws, especially when obtaining the IP address from an HTTP header (reverse-proxy mode). Use this option only if you understand the risks and how to manage them. | |
|
| flag: `--trusted-ip`<br/>toml: `trusted_ips` | string \| list | list of IPs or CIDR ranges to allow to bypass authentication (may be given multiple times). When combined with `--reverse-proxy` and optionally `--real-client-ip-header` this will evaluate the trust of the IP stored in an HTTP header by a reverse proxy rather than the layer-3/4 remote address. WARNING: trusting IPs has inherent security flaws, especially when obtaining the IP address from an HTTP header (reverse-proxy mode). Use this option only if you understand the risks and how to manage them. | |
|
||||||
| flag: `--whitelist-domain`<br/>toml: `whitelist_domains` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` or a `*.` to allow subdomains (e.g. `.example.com`, `*.example.com`) [^2] | |
|
| flag: `--whitelist-domain`<br/>toml: `whitelist_domains` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` or a `*.` to allow subdomains (e.g. `.example.com`, `*.example.com`) [^2] | |
|
||||||
|
|
||||||
[^2]: When using the `whitelist-domain` option, any domain prefixed with a `.` or a `*.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URL's protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`.
|
[^2]: When using the `whitelist-domain` option, any domain prefixed with a `.` or a `*.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URL's protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`.
|
||||||
|
|
||||||
|
@ -845,13 +845,12 @@ func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, ove
|
|||||||
csrf.HashOIDCNonce(),
|
csrf.HashOIDCNonce(),
|
||||||
extraParams,
|
extraParams,
|
||||||
)
|
)
|
||||||
|
cookies.ClearExtraCsrfCookies(p.CookieOptions, rw, req)
|
||||||
if _, err := csrf.SetCookie(rw, req); err != nil {
|
if _, err := csrf.SetCookie(rw, req); err != nil {
|
||||||
logger.Errorf("Error setting CSRF cookie: %v", err)
|
logger.Errorf("Error setting CSRF cookie: %v", err)
|
||||||
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
|
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(rw, req, loginURL, http.StatusFound)
|
http.Redirect(rw, req, loginURL, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,17 +8,18 @@ import (
|
|||||||
|
|
||||||
// Cookie contains configuration options relating to Cookie configuration
|
// Cookie contains configuration options relating to Cookie configuration
|
||||||
type Cookie struct {
|
type Cookie struct {
|
||||||
Name string `flag:"cookie-name" cfg:"cookie_name"`
|
Name string `flag:"cookie-name" cfg:"cookie_name"`
|
||||||
Secret string `flag:"cookie-secret" cfg:"cookie_secret"`
|
Secret string `flag:"cookie-secret" cfg:"cookie_secret"`
|
||||||
Domains []string `flag:"cookie-domain" cfg:"cookie_domains"`
|
Domains []string `flag:"cookie-domain" cfg:"cookie_domains"`
|
||||||
Path string `flag:"cookie-path" cfg:"cookie_path"`
|
Path string `flag:"cookie-path" cfg:"cookie_path"`
|
||||||
Expire time.Duration `flag:"cookie-expire" cfg:"cookie_expire"`
|
Expire time.Duration `flag:"cookie-expire" cfg:"cookie_expire"`
|
||||||
Refresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh"`
|
Refresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh"`
|
||||||
Secure bool `flag:"cookie-secure" cfg:"cookie_secure"`
|
Secure bool `flag:"cookie-secure" cfg:"cookie_secure"`
|
||||||
HTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"`
|
HTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"`
|
||||||
SameSite string `flag:"cookie-samesite" cfg:"cookie_samesite"`
|
SameSite string `flag:"cookie-samesite" cfg:"cookie_samesite"`
|
||||||
CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"`
|
CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"`
|
||||||
CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"`
|
CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"`
|
||||||
|
CSRFPerRequestLimit int `flag:"cookie-csrf-per-request-limit" cfg:"cookie_csrf_per_request_limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func cookieFlagSet() *pflag.FlagSet {
|
func cookieFlagSet() *pflag.FlagSet {
|
||||||
@ -34,6 +35,7 @@ func cookieFlagSet() *pflag.FlagSet {
|
|||||||
flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
|
flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
|
||||||
flagSet.String("cookie-samesite", "", "set SameSite cookie attribute (ie: \"lax\", \"strict\", \"none\", or \"\"). ")
|
flagSet.String("cookie-samesite", "", "set SameSite cookie attribute (ie: \"lax\", \"strict\", \"none\", or \"\"). ")
|
||||||
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.")
|
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.")
|
||||||
|
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.")
|
||||||
flagSet.Duration("cookie-csrf-expire", time.Duration(15)*time.Minute, "expire timeframe for CSRF cookie")
|
flagSet.Duration("cookie-csrf-expire", time.Duration(15)*time.Minute, "expire timeframe for CSRF cookie")
|
||||||
return flagSet
|
return flagSet
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
||||||
@ -151,6 +153,48 @@ func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cooki
|
|||||||
return cookie, nil
|
return cookie, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearExtraCsrfCookies limits the amount of existing CSRF cookies by deleting
|
||||||
|
// an excess of cookies controlled through the option CSRFPerRequestLimit
|
||||||
|
func ClearExtraCsrfCookies(opts *options.Cookie, rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if !opts.CSRFPerRequest || opts.CSRFPerRequestLimit <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies := req.Cookies()
|
||||||
|
existingCsrfCookies := []*http.Cookie{}
|
||||||
|
startsWith := fmt.Sprintf("%v_", opts.Name)
|
||||||
|
|
||||||
|
// determine how many csrf cookies we have
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if strings.HasPrefix(cookie.Name, startsWith) && strings.HasSuffix(cookie.Name, "_csrf") {
|
||||||
|
existingCsrfCookies = append(existingCsrfCookies, cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// short circuit return
|
||||||
|
if len(existingCsrfCookies) <= opts.CSRFPerRequestLimit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedCookies := []*csrf{}
|
||||||
|
for _, cookie := range existingCsrfCookies {
|
||||||
|
decodedCookie, err := decodeCSRFCookie(cookie, opts)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
decodedCookies = append(decodedCookies, decodedCookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the X oldest cookies
|
||||||
|
slices.SortStableFunc(decodedCookies, func(a, b *csrf) int {
|
||||||
|
return a.time.Now().Compare(b.time.Now())
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := 0; i < len(decodedCookies)-opts.CSRFPerRequestLimit; i++ {
|
||||||
|
decodedCookies[i].ClearCookie(rw, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ClearCookie removes the CSRF cookie
|
// ClearCookie removes the CSRF cookie
|
||||||
func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) {
|
func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) {
|
||||||
http.SetCookie(rw, MakeCookieFromOptions(
|
http.SetCookie(rw, MakeCookieFromOptions(
|
||||||
@ -181,7 +225,7 @@ func (c *csrf) encodeCookie() (string, error) {
|
|||||||
// decodeCSRFCookie validates the signature then decrypts and decodes a CSRF
|
// decodeCSRFCookie validates the signature then decrypts and decodes a CSRF
|
||||||
// cookie into a CSRF struct
|
// cookie into a CSRF struct
|
||||||
func decodeCSRFCookie(cookie *http.Cookie, opts *options.Cookie) (*csrf, error) {
|
func decodeCSRFCookie(cookie *http.Cookie, opts *options.Cookie) (*csrf, error) {
|
||||||
val, _, ok := encryption.Validate(cookie, opts.Secret, opts.Expire)
|
val, t, ok := encryption.Validate(cookie, opts.Secret, opts.Expire)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("CSRF cookie failed validation")
|
return nil, errors.New("CSRF cookie failed validation")
|
||||||
}
|
}
|
||||||
@ -192,7 +236,9 @@ func decodeCSRFCookie(cookie *http.Cookie, opts *options.Cookie) (*csrf, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Valid cookie, Unmarshal the CSRF
|
// Valid cookie, Unmarshal the CSRF
|
||||||
csrf := &csrf{cookieOpts: opts}
|
clock := clock.Clock{}
|
||||||
|
clock.Set(t)
|
||||||
|
csrf := &csrf{cookieOpts: opts, time: clock}
|
||||||
err = msgpack.Unmarshal(decrypted, csrf)
|
err = msgpack.Unmarshal(decrypted, csrf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error unmarshalling data to CSRF: %v", err)
|
return nil, fmt.Errorf("error unmarshalling data to CSRF: %v", err)
|
||||||
|
@ -190,5 +190,84 @@ var _ = Describe("CSRF Cookie with non-fixed name Tests", func() {
|
|||||||
Expect(privateCSRF.cookieName()).To(ContainSubstring(cookieName))
|
Expect(privateCSRF.cookieName()).To(ContainSubstring(cookieName))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("CSRF per request limit", func() {
|
||||||
|
It("clears cookies based on the limit", func() {
|
||||||
|
//needs to be now as pkg/encryption/utils.go uses time.Now()
|
||||||
|
testNow := time.Now()
|
||||||
|
cookieOpts.CSRFPerRequestLimit = 1
|
||||||
|
|
||||||
|
publicCSRF1, err := NewCSRF(cookieOpts, "verifier")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
privateCSRF1 := publicCSRF1.(*csrf)
|
||||||
|
privateCSRF1.time.Set(testNow)
|
||||||
|
|
||||||
|
publicCSRF2, err := NewCSRF(cookieOpts, "verifier")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
privateCSRF2 := publicCSRF2.(*csrf)
|
||||||
|
privateCSRF2.time.Set(testNow.Add(time.Minute))
|
||||||
|
|
||||||
|
publicCSRF3, err := NewCSRF(cookieOpts, "verifier")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
privateCSRF3 := publicCSRF3.(*csrf)
|
||||||
|
privateCSRF3.time.Set(testNow.Add(time.Minute * 2))
|
||||||
|
|
||||||
|
cookies := []string{}
|
||||||
|
for _, csrf := range []*csrf{privateCSRF1, privateCSRF2, privateCSRF3} {
|
||||||
|
encoded, err := csrf.encodeCookie()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
cookie := MakeCookieFromOptions(
|
||||||
|
req,
|
||||||
|
csrf.cookieName(),
|
||||||
|
encoded,
|
||||||
|
csrf.cookieOpts,
|
||||||
|
csrf.cookieOpts.CSRFExpire,
|
||||||
|
)
|
||||||
|
cookies = append(cookies, fmt.Sprintf("%v=%v", cookie.Name, cookie.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
header := make(map[string][]string, 1)
|
||||||
|
header["Cookie"] = cookies
|
||||||
|
req = &http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Host: cookieDomain,
|
||||||
|
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: cookieDomain,
|
||||||
|
Path: cookiePath,
|
||||||
|
},
|
||||||
|
Header: header,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when setting the limit to one csrf cookie but configuring three csrf cookies
|
||||||
|
// then two cookies should be removed / set to expired on the response
|
||||||
|
|
||||||
|
// for this test case we have set all the cookies on a single request,
|
||||||
|
// but in reality this will be multiple requests after another
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
ClearExtraCsrfCookies(cookieOpts, rw, req)
|
||||||
|
|
||||||
|
clearedCookies := rw.Header()["Set-Cookie"]
|
||||||
|
Expect(clearedCookies).To(HaveLen(2))
|
||||||
|
Expect(clearedCookies[0]).To(Equal(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure",
|
||||||
|
privateCSRF1.cookieName(),
|
||||||
|
cookiePath,
|
||||||
|
cookieDomain,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
Expect(clearedCookies[1]).To(Equal(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure",
|
||||||
|
privateCSRF2.cookieName(),
|
||||||
|
cookiePath,
|
||||||
|
cookieDomain,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user