Merge branch 'master' into feat/redis-mtls
This commit is contained in:
commit
df0a78475f
50
CHANGELOG.md
50
CHANGELOG.md
|
|
@ -6,9 +6,57 @@
|
|||
|
||||
## Breaking Changes
|
||||
|
||||
## Changes since v7.15.2
|
||||
|
||||
# V7.15.2
|
||||
|
||||
## Release Highlights
|
||||
|
||||
- 🔵 Golang version upgrade to v1.25.9
|
||||
- Upgrade of all dependencies to their latest versions
|
||||
- [CVE-2026-34986](https://nvd.nist.gov/vuln/detail/CVE-2026-34986)
|
||||
- [CVE-2026-32281](https://nvd.nist.gov/vuln/detail/CVE-2026-32281)
|
||||
- [CVE-2026-32289](https://nvd.nist.gov/vuln/detail/CVE-2026-32289)
|
||||
- [CVE-2026-32288](https://nvd.nist.gov/vuln/detail/CVE-2026-32288)
|
||||
- [CVE-2026-32280](https://nvd.nist.gov/vuln/detail/CVE-2026-32280)
|
||||
- [CVE-2026-32282](https://nvd.nist.gov/vuln/detail/CVE-2026-32282)
|
||||
- [CVE-2026-32283](https://nvd.nist.gov/vuln/detail/CVE-2026-32283)
|
||||
- 🕵️♀️ Vulnerabilities have been addressed
|
||||
|
||||
## Important Notes
|
||||
|
||||
We have had security audits performed on OAuth2 Proxy in the past couple of weeks and as a result we have fixed
|
||||
several CRITICAL vulnerabilities.
|
||||
|
||||
The security vulnerabilities include multiple authentication bypasses and a potential session fixation attack.
|
||||
For more details and to identify if you are effects, we urge all users of OAuth2 Proxy to read the security
|
||||
disclosures.
|
||||
|
||||
- (Critical) [GHSA-5hvv-m4w4-gf6v](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-5hvv-m4w4-gf6v) fix: health check user-agent authentication bypass
|
||||
- (Critical) [GHSA-7x63-xv5r-3p2x](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-7x63-xv5r-3p2x) fix: authentication bypass via X-Forwarded-Uri header spoofing
|
||||
- (High) [GHSA-pxq7-h93f-9jrg](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-pxq7-h93f-9jrg) fix: fragment evaluation as part of the allowed routes
|
||||
- (Moderate) [GHSA-c5c4-8r6x-56w3](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-c5c4-8r6x-56w3) fix: email validation bypass via malformed multi-@ email claims
|
||||
|
||||
Furthermore, for improving the security of OAuth2 Proxy we introduced a new flag `--trusted-proxy-ip` that allows users
|
||||
to explicitly specify trusted reverse proxy IPs for the `X-Forwarded-*` headers. This is an important step to prevent
|
||||
potential header spoofing attacks and to ensure that OAuth2 Proxy only trusts headers from known and trusted sources.
|
||||
We highly recommend users to review their deployment architecture and consider using this flag to enhance the security
|
||||
of their OAuth2 Proxy instances. Check the docs for more details: https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview#proxy-options
|
||||
|
||||
Furthermore, we want to thank everyone who contributed to the audits and reported potential issues to make open source
|
||||
software like OAuth2 Proxy more secure for everyone.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
## Changes since v7.15.1
|
||||
|
||||
- [#3399](https://github.com/oauth2-proxy/oauth2-proxy/pull/3399) feat: add Redis client certificate and key paths for mutual TLS to the Redis session store (@karatkep)
|
||||
- [#3411](https://github.com/oauth2-proxy/oauth2-proxy/pull/3411) chore(deps): update gomod dependencies (@tuunit)
|
||||
- [#3333](https://github.com/oauth2-proxy/oauth2-proxy/pull/3333) fix: invalidate session on fatal OAuth2 refresh errors (@frhack)
|
||||
- [GHSA-f24x-5g9q-753f](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-f24x-5g9q-753f) fix: clear session cookie at beginning of signinpage handler (@fnoehWM / @bella-WI / @tuunit)
|
||||
- [GHSA-5hvv-m4w4-gf6v](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-5hvv-m4w4-gf6v) fix: health check user-agent authentication bypass (@tuunit)
|
||||
- [GHSA-7x63-xv5r-3p2x](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-7x63-xv5r-3p2x) fix: authentication bypass via X-Forwarded-Uri header spoofing (@tuunit)
|
||||
- [GHSA-pxq7-h93f-9jrg](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-pxq7-h93f-9jrg) fix: fragment evaluation as part of the allowed routes (@tuunit)
|
||||
- [GHSA-c5c4-8r6x-56w3](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-c5c4-8r6x-56w3) fix: email validation bypass via malformed multi-@ email claims (@tuunit)
|
||||
|
||||
# V7.15.1
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ by our [project governance](GOVERNANCE.md).
|
|||
| Name | GitHub Handle | Domains of reponsibility | Email Alias | Affiliation |
|
||||
| ---------------- | ------------------------------------------------------ | ------------------------ | -------------------------- | ----------- |
|
||||
| Joel Speed | [@JoelSpeed](https://github.com/joelspeed) | Governance, Core | joel@oauth2-proxy.dev | Red Hat |
|
||||
| Jan Larwig | [@tuunit](https://github.com/tuunit) | Governance, Core | jan@oauth2-proxy.dev | IONOS Cloud |
|
||||
| Jan Larwig | [@tuunit](https://github.com/tuunit) | Governance, Core | jan@oauth2-proxy.dev | STACKIT |
|
||||
| JJ Łakis | [@jjlakis](https://github.com/jjlakis) | Provider | jj@oauth2-proxy.dev | - |
|
||||
| Koen van Zuijlen | [@kvanzuijlen](https://github.com/kvanzuijlen) | CI | koen@oauth2-proxy.dev | - |
|
||||
| Pierluigi Lenoci | [@pierluigilenoci](https://github.com/pierluigilenoci) | Helm | pierluigi@oauth2-proxy.dev | SAP |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ storage:
|
|||
type: etcd
|
||||
config:
|
||||
endpoints:
|
||||
- http://etcd:2379
|
||||
- http://etcd:2379
|
||||
namespace: dex/
|
||||
web:
|
||||
http: 0.0.0.0:5556
|
||||
|
|
@ -16,17 +16,18 @@ expiry:
|
|||
signingKeys: "4h"
|
||||
idTokens: "1h"
|
||||
staticClients:
|
||||
- id: oauth2-proxy
|
||||
redirectURIs:
|
||||
# These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
|
||||
- 'http://oauth2-proxy.localtest.me:4180/oauth2/callback' # For basic proxy example.
|
||||
- 'http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback' # For nginx and traefik example.
|
||||
name: 'OAuth2 Proxy'
|
||||
secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
|
||||
- id: oauth2-proxy
|
||||
redirectURIs:
|
||||
# These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
|
||||
- "http://oauth2-proxy.localtest.me:4180/oauth2/callback" # For basic proxy example.
|
||||
- "http://oauth2-proxy.localtest.me:8080/oauth2/callback" # For nginx example.
|
||||
- "http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback" # For traefik example.
|
||||
name: "OAuth2 Proxy"
|
||||
secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
|
||||
enablePasswordDB: true
|
||||
staticPasswords:
|
||||
- email: "admin@example.com"
|
||||
# bcrypt hash of the string "password"
|
||||
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||
username: "admin"
|
||||
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
|
||||
- email: "admin@example.com"
|
||||
# bcrypt hash of the string "password"
|
||||
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||
username: "admin"
|
||||
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ version: "3.0"
|
|||
services:
|
||||
oauth2-proxy:
|
||||
container_name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
|
||||
command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
|
||||
hostname: oauth2-proxy
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ version: '3.0'
|
|||
services:
|
||||
oauth2-proxy:
|
||||
container_name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
|
||||
command: --config /oauth2-proxy.cfg
|
||||
hostname: oauth2-proxy
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
#
|
||||
# Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password
|
||||
# Access http://keycloak.localtest.me:9080 with the same credentials to check out the settings
|
||||
version: '3.0'
|
||||
version: "3.0"
|
||||
services:
|
||||
oauth2-proxy:
|
||||
container_name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
|
||||
command: --config /oauth2-proxy.cfg
|
||||
hostname: oauth2-proxy
|
||||
volumes:
|
||||
|
|
@ -43,9 +43,9 @@ services:
|
|||
image: keycloak/keycloak:25.0
|
||||
hostname: keycloak
|
||||
command:
|
||||
- 'start-dev'
|
||||
- '--http-port=9080'
|
||||
- '--import-realm'
|
||||
- "start-dev"
|
||||
- "--http-port=9080"
|
||||
- "--import-realm"
|
||||
volumes:
|
||||
- ./keycloak:/opt/keycloak/data/import
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@
|
|||
version: "3.0"
|
||||
services:
|
||||
oauth2-proxy:
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
|
||||
ports: []
|
||||
hostname: oauth2-proxy
|
||||
container_name: oauth2-proxy
|
||||
command: --config /oauth2-proxy.cfg
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- "./oauth2-proxy-nginx.cfg:/oauth2-proxy.cfg"
|
||||
networks:
|
||||
|
|
@ -44,7 +45,7 @@ services:
|
|||
image: nginx:1.29
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80/tcp
|
||||
- 8080:8080/tcp
|
||||
hostname: nginx
|
||||
volumes:
|
||||
- "./nginx.conf:/etc/nginx/conf.d/default.conf"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ version: '3.0'
|
|||
services:
|
||||
|
||||
oauth2-proxy:
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
|
||||
ports: []
|
||||
hostname: oauth2-proxy
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ version: "3.0"
|
|||
services:
|
||||
oauth2-proxy:
|
||||
container_name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
|
||||
command: --config /oauth2-proxy.cfg
|
||||
hostname: oauth2-proxy
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -1,91 +1,44 @@
|
|||
# Reverse proxy to oauth2-proxy
|
||||
server {
|
||||
listen 80;
|
||||
server_name oauth2-proxy.oauth2-proxy.localhost;
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
proxy_pass http://oauth2-proxy:4180/;
|
||||
}
|
||||
}
|
||||
|
||||
# Reverse proxy to httpbin
|
||||
server {
|
||||
listen 80;
|
||||
server_name httpbin.oauth2-proxy.localhost;
|
||||
listen 8080;
|
||||
server_name oauth2-proxy.localtest.me;
|
||||
|
||||
auth_request /internal-auth/oauth2/auth;
|
||||
location /oauth2/ {
|
||||
proxy_pass http://oauth2-proxy:4180;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||
proxy_set_header X-Auth-Request-Redirect $request_uri;
|
||||
}
|
||||
|
||||
# On 401, redirect to the sign_in page via a named location
|
||||
# This ensures a proper 302 redirect that browsers will follow
|
||||
error_page 401 = @oauth2_signin;
|
||||
location = /oauth2/auth {
|
||||
proxy_pass http://oauth2-proxy:4180;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||
# nginx auth_request includes headers but not body
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_pass_request_body off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://httpbin/;
|
||||
}
|
||||
|
||||
# Named location for OAuth2 sign-in redirect
|
||||
# Returns a proper 302 that works with --skip-provider-button
|
||||
location @oauth2_signin {
|
||||
return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
|
||||
}
|
||||
|
||||
# auth_request must be a URI so this allows an internal path to then proxy to
|
||||
# the real auth_request path.
|
||||
# The trailing /'s are required so that nginx strips the prefix before proxying.
|
||||
location /internal-auth/ {
|
||||
internal; # Ensure external users can't access this path
|
||||
|
||||
# Make sure the OAuth2 Proxy knows where the original request came from.
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||
|
||||
proxy_pass http://oauth2-proxy:4180/;
|
||||
}
|
||||
}
|
||||
|
||||
# Statically serve the nginx welcome
|
||||
server {
|
||||
listen 80;
|
||||
server_name oauth2-proxy.localhost;
|
||||
|
||||
location / {
|
||||
auth_request /internal-auth/oauth2/auth;
|
||||
|
||||
# On 401, redirect to the sign_in page via a named location
|
||||
# This ensures a proper 302 redirect that browsers will follow
|
||||
auth_request /oauth2/auth;
|
||||
error_page 401 = @oauth2_signin;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
# pass information via X-User and X-Email headers to backend,
|
||||
# requires running with --set-xauthrequest flag
|
||||
auth_request_set $user $upstream_http_x_auth_request_user;
|
||||
auth_request_set $email $upstream_http_x_auth_request_email;
|
||||
proxy_set_header X-User $user;
|
||||
proxy_set_header X-Email $email;
|
||||
|
||||
proxy_pass http://httpbin/;
|
||||
# or "root /path/to/site;" or "fastcgi_pass ..." etc
|
||||
}
|
||||
|
||||
# Named location for OAuth2 sign-in redirect
|
||||
# Returns a proper 302 that works with --skip-provider-button
|
||||
# Named location for handling OAuth2 sign-in redirects
|
||||
# This ensures the browser receives a proper 302 redirect that it will follow
|
||||
location @oauth2_signin {
|
||||
return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
|
||||
}
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# auth_request must be a URI so this allows an internal path to then proxy to
|
||||
# the real auth_request path.
|
||||
# The trailing /'s are required so that nginx strips the prefix before proxying.
|
||||
location /internal-auth/ {
|
||||
internal; # Ensure external users can't access this path
|
||||
|
||||
# Make sure the OAuth2 Proxy knows where the original request came from.
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||
|
||||
proxy_pass http://oauth2-proxy:4180/;
|
||||
return 302 /oauth2/sign_in?rd=$scheme://$http_host$request_uri;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
http_address="0.0.0.0:4180"
|
||||
cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
|
||||
provider="oidc"
|
||||
email_domains="example.com"
|
||||
oidc_issuer_url="http://dex.localtest.me:5556/dex"
|
||||
cookie_secure="false"
|
||||
upstreams="static://200"
|
||||
cookie_domains=[".localtest.me"] # Required so cookie can be read on all subdomains.
|
||||
whitelist_domains=[".localtest.me"] # Required to allow redirection back to original requested target.
|
||||
|
||||
# dex provider
|
||||
client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
|
||||
client_id="oauth2-proxy"
|
||||
cookie_secure="false"
|
||||
redirect_url="http://oauth2-proxy.localtest.me:8080/oauth2/callback"
|
||||
|
||||
oidc_issuer_url="http://dex.localtest.me:5556/dex"
|
||||
provider="oidc"
|
||||
provider_display_name="Dex"
|
||||
|
||||
redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback"
|
||||
cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains.
|
||||
whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target.
|
||||
# Enables the use of `X-Forwarded-*` headers to determine request correctly
|
||||
reverse_proxy="true"
|
||||
|
|
|
|||
|
|
@ -193,6 +193,10 @@ Provider specific options can be found on their respective subpages.
|
|||
|
||||
### Proxy Options
|
||||
|
||||
:::warning
|
||||
When `--reverse-proxy` is enabled, configure `--trusted-proxy-ip` to the IPs or CIDR ranges of the reverse proxies that are allowed to send `X-Forwarded-*` headers. If you leave it unset, OAuth2 Proxy currently trusts all source IPs for backwards compatibility, which means a client that can reach OAuth2 Proxy directly may be able to spoof forwarded headers.
|
||||
:::
|
||||
|
||||
| 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` |
|
||||
|
|
@ -211,10 +215,11 @@ Provider specific options can be found on their respective subpages.
|
|||
| 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: `--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: `--trusted-proxy-ip`<br/>toml: `trusted_proxy_ips` | string \| list | list of IPs or CIDR ranges allowed to supply `X-Forwarded-*` headers when `--reverse-proxy` is enabled. If not set, OAuth2 Proxy preserves backwards compatibility by trusting all source IPs (`0.0.0.0/0`, `::/0`) and logs a warning at startup. Configure this to your reverse proxy addresses to prevent forwarded header spoofing. | `"0.0.0.0/0", "::/0"` |
|
||||
| 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-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-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). Path matching is performed against the normalized path only; fragment identifiers (`#`) and their URL-encoded form (`%23`) are stripped before evaluation. | |
|
||||
| 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. Path matching is performed against the normalized path only; fragment identifiers (`#`) and their URL-encoded form (`%23`) are stripped before evaluation. | |
|
||||
| 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: `--ssl-insecure-skip-verify`<br/>toml: `ssl_insecure_skip_verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: Installation
|
|||
|
||||
1. Choose how to deploy:
|
||||
|
||||
a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.1`)
|
||||
a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.2`)
|
||||
|
||||
b. Using Go to install the latest release
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -42,5 +42,8 @@
|
|||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"overrides" : {
|
||||
"webpackbar" : "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,6 +193,10 @@ Provider specific options can be found on their respective subpages.
|
|||
|
||||
### Proxy Options
|
||||
|
||||
:::warning
|
||||
When `--reverse-proxy` is enabled, configure `--trusted-proxy-ip` to the IPs or CIDR ranges of the reverse proxies that are allowed to send `X-Forwarded-*` headers. If you leave it unset, OAuth2 Proxy currently trusts all source IPs for backwards compatibility, which means a client that can reach OAuth2 Proxy directly may be able to spoof forwarded headers.
|
||||
:::
|
||||
|
||||
| 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` |
|
||||
|
|
@ -211,6 +215,7 @@ Provider specific options can be found on their respective subpages.
|
|||
| 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: `--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: `--trusted-proxy-ip`<br/>toml: `trusted_proxy_ips` | string \| list | list of IPs or CIDR ranges allowed to supply `X-Forwarded-*` headers when `--reverse-proxy` is enabled. If not set, OAuth2 Proxy preserves backwards compatibility by trusting all source IPs (`0.0.0.0/0`, `::/0`) and logs a warning at startup. Configure this to your reverse proxy addresses to prevent forwarded header spoofing. | `"0.0.0.0/0", "::/0"` |
|
||||
| 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-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) | |
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: Installation
|
|||
|
||||
1. Choose how to deploy:
|
||||
|
||||
a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.1`)
|
||||
a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.2`)
|
||||
|
||||
b. Using Go to install the latest release
|
||||
```bash
|
||||
|
|
|
|||
40
go.mod
40
go.mod
|
|
@ -9,12 +9,12 @@ require (
|
|||
github.com/alicebob/miniredis/v2 v2.37.0
|
||||
github.com/bitly/go-simplejson v0.5.1
|
||||
github.com/bsm/redislock v0.9.4
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/coreos/go-oidc/v3 v3.18.0
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-jose/go-jose/v3 v3.0.4
|
||||
github.com/go-jose/go-jose/v3 v3.0.5
|
||||
github.com/go-jose/go-jose/v4 v4.1.4
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
|
|
@ -32,17 +32,17 @@ require (
|
|||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/api v0.272.0
|
||||
google.golang.org/api v0.275.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
k8s.io/apimachinery v0.35.3
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth v0.20.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
|
@ -53,13 +53,13 @@ require (
|
|||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.19.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
|
|
@ -70,18 +70,18 @@ require (
|
|||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
46
go.sum
46
go.sum
|
|
@ -1,5 +1,7 @@
|
|||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
|
|
@ -33,6 +35,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
|||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -54,6 +58,8 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01
|
|||
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
|
|
@ -65,6 +71,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
|||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
|
|
@ -78,6 +86,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
|
||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
|
@ -87,6 +97,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA
|
|||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
|
||||
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
|
||||
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
|
||||
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||
|
|
@ -119,6 +131,8 @@ github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
|||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
|
@ -175,18 +189,29 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
|||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
|
|
@ -200,10 +225,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
|
|
@ -211,6 +240,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -229,6 +260,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
|
@ -242,6 +275,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
@ -250,19 +285,30 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
|
||||
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
|
||||
google.golang.org/api v0.275.0 h1:vfY5d9vFVJeWEZT65QDd9hbndr7FyZ2+6mIzGAh71NI=
|
||||
google.golang.org/api v0.275.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
|
||||
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
|
||||
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
|
|||
Entry("with bad legacy configuration", loadConfigurationTableInput{
|
||||
configContent: testCoreConfig + "unknown_field=\"something\"",
|
||||
expectedOptions: func() *options.Options { return nil },
|
||||
expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"),
|
||||
expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'options.LegacyOptions' has invalid keys: unknown_field"),
|
||||
}),
|
||||
Entry("with bad alpha configuration", loadConfigurationTableInput{
|
||||
configContent: testCoreConfig,
|
||||
|
|
@ -290,7 +290,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
|
|||
configContent: testCoreConfig + "unknown_field=\"something\"",
|
||||
alphaConfigContent: testAlphaConfig,
|
||||
expectedOptions: func() *options.Options { return nil },
|
||||
expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"),
|
||||
expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'options.LegacyOptions' has invalid keys: unknown_field"),
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
defaultTrustedProxyIPs = []string{"0.0.0.0/0", "::/0"}
|
||||
|
||||
// ErrNeedsLogin means the user should be redirected to the login page
|
||||
ErrNeedsLogin = errors.New("redirect to login page")
|
||||
|
||||
|
|
@ -183,13 +185,14 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
|
|||
|
||||
logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh)
|
||||
|
||||
trustedIPs := ip.NewNetSet()
|
||||
for _, ipStr := range opts.TrustedIPs {
|
||||
if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
|
||||
trustedIPs.AddIPNet(*ipNet)
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
|
||||
}
|
||||
trustedIPs, err := ip.ParseNetSet(opts.TrustedIPs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trustedProxies, err := buildTrustedProxyNetSet(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowedRoutes, err := buildRoutesAllowlist(opts)
|
||||
|
|
@ -202,7 +205,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
|
|||
return nil, err
|
||||
}
|
||||
|
||||
preAuthChain, err := buildPreAuthChain(opts, sessionStore)
|
||||
preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
|
||||
}
|
||||
|
|
@ -355,8 +358,8 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
|
|||
// buildPreAuthChain constructs a chain that should process every request before
|
||||
// the OAuth2 Proxy authentication logic kicks in.
|
||||
// For example forcing HTTPS or health checks.
|
||||
func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) {
|
||||
chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
|
||||
func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore, trustedProxies *ip.NetSet) (alice.Chain, error) {
|
||||
chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader, trustedProxies))
|
||||
|
||||
if opts.ForceHTTPS {
|
||||
_, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
|
||||
|
|
@ -395,6 +398,16 @@ func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionSt
|
|||
return chain, nil
|
||||
}
|
||||
|
||||
func buildTrustedProxyNetSet(opts *options.Options) (*ip.NetSet, error) {
|
||||
trustedProxyIPs := opts.TrustedProxyIPs
|
||||
if opts.ReverseProxy && len(trustedProxyIPs) == 0 {
|
||||
logger.Print("WARNING: --reverse-proxy is enabled but no --trusted-proxy-ip CIDRs were configured. All connecting IPs are trusted to supply X-Forwarded-* headers by default (0.0.0.0/0, ::/0). This preserves backwards compatibility but is a potential security risk; configure --trusted-proxy-ip to match your reverse proxy addresses.")
|
||||
trustedProxyIPs = defaultTrustedProxyIPs
|
||||
}
|
||||
|
||||
return ip.ParseNetSet(trustedProxyIPs)
|
||||
}
|
||||
|
||||
func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
|
||||
chain := alice.New()
|
||||
|
||||
|
|
@ -634,6 +647,10 @@ func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
|
|||
// SignInPage writes the sign in template to the response
|
||||
func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
|
||||
prepareNoCache(rw)
|
||||
|
||||
if err := p.ClearSessionCookie(rw, req); err != nil {
|
||||
logger.Printf("Error clearing session cookie: %v", err)
|
||||
}
|
||||
rw.WriteHeader(code)
|
||||
|
||||
redirectURL, err := p.appDirector.GetRedirect(req)
|
||||
|
|
@ -647,10 +664,6 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
|
|||
redirectURL = "/"
|
||||
}
|
||||
|
||||
if err := p.ClearSessionCookie(rw, req); err != nil {
|
||||
logger.Printf("Error clearing session cookie: %v", err)
|
||||
}
|
||||
|
||||
p.pageWriter.WriteSignInPage(rw, req, redirectURL, code)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -713,6 +713,50 @@ func TestManualSignInCorrectCredentials(t *testing.T) {
|
|||
assert.Equal(t, http.StatusFound, statusCode)
|
||||
}
|
||||
|
||||
func TestSignInPageClearsExistingSessionCookie(t *testing.T) {
|
||||
opts := baseTestOptions()
|
||||
err := validation.Validate(opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy, err := NewOAuthProxy(opts, func(string) bool {
|
||||
return true
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a real session cookie using the actual session store.
|
||||
saveRW := httptest.NewRecorder()
|
||||
saveReq := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
err = proxy.sessionStore.Save(saveRW, saveReq, &sessions.SessionState{
|
||||
Email: "john.doe@example.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cookies := saveRW.Result().Cookies()
|
||||
require.NotEmpty(t, cookies)
|
||||
|
||||
// Send that cookie to the sign-in page.
|
||||
req := httptest.NewRequest(http.MethodGet, "/oauth2/sign_in", nil)
|
||||
for _, c := range cookies {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
proxy.ServeHTTP(rw, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rw.Code)
|
||||
|
||||
cleared := false
|
||||
for _, c := range rw.Result().Cookies() {
|
||||
if c.Name == proxy.CookieOptions.Name {
|
||||
cleared = true
|
||||
assert.Equal(t, "", c.Value)
|
||||
assert.Less(t, c.MaxAge, 0)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, cleared, "expected sign-in page to clear existing session cookie")
|
||||
}
|
||||
|
||||
func TestSignInPageIncludesTargetRedirect(t *testing.T) {
|
||||
sipTest, err := NewSignInPageTest(false)
|
||||
if err != nil {
|
||||
|
|
@ -2635,9 +2679,11 @@ func TestAllowedRequest(t *testing.T) {
|
|||
}
|
||||
opts.SkipAuthRegex = []string{
|
||||
"^/skip/auth/regex$",
|
||||
"^/public/.*/endpoint$",
|
||||
}
|
||||
opts.SkipAuthRoutes = []string{
|
||||
"GET=^/skip/auth/routes/get",
|
||||
"^/foo/.*/bar$",
|
||||
}
|
||||
err := validation.Validate(opts)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -2670,6 +2716,18 @@ func TestAllowedRequest(t *testing.T) {
|
|||
url: "/wrong/denied",
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
name: "Regex allowed with fragment-free path",
|
||||
method: "GET",
|
||||
url: "/public/legit/endpoint",
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
name: "Regex denied when path contains encoded fragment suffix",
|
||||
method: "GET",
|
||||
url: "/public/secret%23/endpoint",
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
name: "Route allowed",
|
||||
method: "GET",
|
||||
|
|
@ -2694,6 +2752,18 @@ func TestAllowedRequest(t *testing.T) {
|
|||
url: "/skip/auth/routes/wrong/path",
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
name: "Route allowed with fragment-free path",
|
||||
method: "GET",
|
||||
url: "/foo/public/bar",
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
name: "Route denied when path contains encoded fragment suffix",
|
||||
method: "GET",
|
||||
url: "/foo/secret%23/bar",
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
@ -2734,9 +2804,11 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
|
|||
}
|
||||
opts.SkipAuthRegex = []string{
|
||||
"^/skip/auth/regex$",
|
||||
"^/public/.*/endpoint$",
|
||||
}
|
||||
opts.SkipAuthRoutes = []string{
|
||||
"GET=^/skip/auth/routes/get",
|
||||
"^/foo/.*/bar$",
|
||||
}
|
||||
err := validation.Validate(opts)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -2769,6 +2841,18 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
|
|||
url: "/wrong/denied",
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
name: "Regex allowed with fragment-free path",
|
||||
method: "GET",
|
||||
url: "/public/legit/endpoint",
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
name: "Regex denied when X-Forwarded-Uri contains an encoded fragment suffix",
|
||||
method: "GET",
|
||||
url: "/public/secret%23/endpoint",
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
name: "Route allowed",
|
||||
method: "GET",
|
||||
|
|
@ -2793,12 +2877,25 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
|
|||
url: "/skip/auth/routes/wrong/path",
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
name: "Route allowed with fragment-free path",
|
||||
method: "GET",
|
||||
url: "/foo/public/bar",
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
name: "Route denied when X-Forwarded-Uri contains an encoded fragment suffix",
|
||||
method: "GET",
|
||||
url: "/foo/secret%23/bar",
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
|
||||
req.Header.Set("X-Forwarded-Uri", tc.url)
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
assert.NoError(t, err)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
|
|
@ -2813,6 +2910,43 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAllowedRequestWithForwardedUriHeaderRequiresTrustedProxy(t *testing.T) {
|
||||
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
t.Cleanup(upstreamServer.Close)
|
||||
|
||||
opts := baseTestOptions()
|
||||
opts.ReverseProxy = true
|
||||
opts.TrustedProxyIPs = []string{"127.0.0.1/32"}
|
||||
opts.UpstreamServers = options.UpstreamConfig{
|
||||
Upstreams: []options.Upstream{
|
||||
{
|
||||
ID: upstreamServer.URL,
|
||||
Path: "/",
|
||||
URI: upstreamServer.URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
opts.SkipAuthRegex = []string{"^/skip/auth/regex$"}
|
||||
|
||||
err := validation.Validate(opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, opts.ProxyPrefix+authOnlyPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = "192.0.2.10:4180"
|
||||
req.Header.Set("X-Forwarded-Uri", "/skip/auth/regex")
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
proxy.ServeHTTP(rw, req)
|
||||
|
||||
assert.Equal(t, 401, rw.Code)
|
||||
}
|
||||
|
||||
func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
|
||||
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
|
|
@ -3404,6 +3538,24 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
|
|||
querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
|
||||
expectedStatusCode: http.StatusAccepted,
|
||||
},
|
||||
{
|
||||
name: "UserWithMultipleAtSignsExactDomain",
|
||||
email: "attacker@evil.com@example.com",
|
||||
querystring: "?allowed_email_domains=example.com",
|
||||
expectedStatusCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "UserWithMultipleAtSignsWildcardDomain",
|
||||
email: "attacker@evil.com@foo.example.com",
|
||||
querystring: "?allowed_email_domains=*.example.com",
|
||||
expectedStatusCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "UserWithMultipleAtSignsDotPrefixedDomain",
|
||||
email: "attacker@evil.com@foo.example.com",
|
||||
querystring: "?allowed_email_domains=.example.com",
|
||||
expectedStatusCode: http.StatusForbidden,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ package middleware
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
)
|
||||
|
||||
type scopeKey string
|
||||
|
|
@ -18,9 +21,13 @@ const RequestScopeKey scopeKey = "request-scope"
|
|||
// within the chain.
|
||||
type RequestScope struct {
|
||||
// ReverseProxy tracks whether OAuth2-Proxy is operating in reverse proxy
|
||||
// mode and if request `X-Forwarded-*` headers should be trusted
|
||||
// mode and if request `X-Forwarded-*` headers may be trusted
|
||||
ReverseProxy bool
|
||||
|
||||
// TrustedProxies tracks which direct callers are allowed to supply
|
||||
// forwarded headers when ReverseProxy mode is enabled.
|
||||
TrustedProxies *ip.NetSet
|
||||
|
||||
// RequestID is set to the request's `X-Request-Id` header if set.
|
||||
// Otherwise a random UUID is set.
|
||||
RequestID string
|
||||
|
|
@ -58,3 +65,43 @@ func AddRequestScope(req *http.Request, scope *RequestScope) *http.Request {
|
|||
ctx := context.WithValue(req.Context(), RequestScopeKey, scope)
|
||||
return req.WithContext(ctx)
|
||||
}
|
||||
|
||||
// CanTrustForwardedHeaders returns whether forwarded headers should be
|
||||
// processed for this request.
|
||||
func (s *RequestScope) CanTrustForwardedHeaders(req *http.Request) bool {
|
||||
if s == nil || req == nil || !s.ReverseProxy || s.TrustedProxies == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if isUnixSocketRemoteAddr(req.RemoteAddr) {
|
||||
return true
|
||||
}
|
||||
|
||||
remoteIP := parseRemoteAddrIP(req.RemoteAddr)
|
||||
if remoteIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.TrustedProxies.Has(remoteIP)
|
||||
}
|
||||
|
||||
func parseRemoteAddrIP(remoteAddr string) net.IP {
|
||||
if remoteAddr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(remoteAddr); ip != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return net.ParseIP(host)
|
||||
}
|
||||
|
||||
func isUnixSocketRemoteAddr(remoteAddr string) bool {
|
||||
return remoteAddr == "@" || strings.HasPrefix(remoteAddr, "/")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
|
@ -53,4 +54,37 @@ var _ = Describe("Scope Suite", func() {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("CanTrustForwardedHeaders", func() {
|
||||
var request *http.Request
|
||||
var scope *middleware.RequestScope
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
request, err = http.NewRequest("", "http://127.0.0.1/", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
scope = &middleware.RequestScope{
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
}
|
||||
})
|
||||
|
||||
It("returns true for a trusted remote address", func() {
|
||||
request.RemoteAddr = "127.0.0.1:4180"
|
||||
Expect(scope.CanTrustForwardedHeaders(request)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns false for an untrusted remote address", func() {
|
||||
request.RemoteAddr = "192.0.2.10:4180"
|
||||
Expect(scope.CanTrustForwardedHeaders(request)).To(BeFalse())
|
||||
})
|
||||
|
||||
It("returns true for unix socket callers", func() {
|
||||
request.RemoteAddr = "@"
|
||||
Expect(scope.CanTrustForwardedHeaders(request)).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ var _ = Describe("Load", func() {
|
|||
Entry("with an unknown option in the config file", &testOptionsTableInput{
|
||||
configFile: []byte(`unknown_option="foo"`),
|
||||
flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
|
||||
expectedErr: fmt.Errorf("error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_option"),
|
||||
expectedErr: fmt.Errorf("error unmarshalling config: decoding failed due to the following error(s):\n\n'options.TestOptions' has invalid keys: unknown_option"),
|
||||
// Viper will unmarshal before returning the error, so this is the default output
|
||||
expectedOutput: &TestOptions{
|
||||
StringOption: "default",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type Options struct {
|
|||
ReadyPath string `flag:"ready-path" cfg:"ready_path"`
|
||||
ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
|
||||
RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
|
||||
TrustedProxyIPs []string `flag:"trusted-proxy-ip" cfg:"trusted_proxy_ips"`
|
||||
TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
|
||||
ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
|
||||
RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
|
||||
|
|
@ -119,6 +120,7 @@ func NewFlagSet() *pflag.FlagSet {
|
|||
|
||||
flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
|
||||
flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, X-ProxyUser-IP, X-Envoy-External-Address, or CF-Connecting-IP)")
|
||||
flagSet.StringSlice("trusted-proxy-ip", []string{}, "list of IPs or CIDR ranges that are allowed to set X-Forwarded-* headers when --reverse-proxy is enabled. Defaults to trusting all IPs for backwards compatibility; configure this to your reverse proxy addresses to prevent header spoofing.")
|
||||
flagSet.StringSlice("trusted-ip", []string{}, "list of IPs or CIDR ranges to allow to bypass authentication. WARNING: trusting by IP has inherent security flaws, read the configuration documentation for more information.")
|
||||
flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
|
||||
flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
|
@ -33,9 +34,16 @@ var _ = Describe("Director Suite", func() {
|
|||
req.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
scope := &middleware.RequestScope{
|
||||
ReverseProxy: in.reverseProxy,
|
||||
})
|
||||
}
|
||||
if in.reverseProxy {
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
scope.TrustedProxies = trustedProxies
|
||||
}
|
||||
req = middleware.AddRequestScope(req, scope)
|
||||
|
||||
redirect, err := appDirector.GetRedirect(req)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -174,4 +182,27 @@ var _ = Describe("Director Suite", func() {
|
|||
expectedRedirect: "https://a-service.example.com/foo/bar",
|
||||
}),
|
||||
)
|
||||
|
||||
It("ignores forwarded headers from an untrusted remote address", func() {
|
||||
appDirector := NewAppDirector(AppDirectorOpts{
|
||||
ProxyPrefix: testProxyPrefix,
|
||||
Validator: testValidator(true),
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "https://oauth.example.com/foo?bar", nil)
|
||||
req.RemoteAddr = "192.0.2.10:4180"
|
||||
req.Header.Add("X-Forwarded-Proto", "https")
|
||||
req.Header.Add("X-Forwarded-Host", "a-service.example.com")
|
||||
req.Header.Add("X-Forwarded-Uri", fooBar)
|
||||
trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
})
|
||||
|
||||
redirect, err := appDirector.GetRedirect(req)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(redirect).To(Equal("/foo?bar"))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
|
@ -30,8 +31,13 @@ var _ = Describe("Cookie Tests", func() {
|
|||
|
||||
if in.xForwardedHost != "" {
|
||||
req.Header.Add("X-Forwarded-Host", in.xForwardedHost)
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
|
||||
ReverseProxy: true,
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -37,3 +38,18 @@ func ParseIPNet(s string) *net.IPNet {
|
|||
return ipNet
|
||||
}
|
||||
}
|
||||
|
||||
func ParseNetSet(ipStrs []string) (*NetSet, error) {
|
||||
netSet := NewNetSet()
|
||||
|
||||
for _, ipStr := range ipStrs {
|
||||
ipNet := ParseIPNet(ipStr)
|
||||
if ipNet == nil {
|
||||
return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
|
||||
}
|
||||
|
||||
netSet.AddIPNet(*ipNet)
|
||||
}
|
||||
|
||||
return netSet, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseIPNet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedIP net.IP
|
||||
expectedMask net.IPMask
|
||||
}{
|
||||
{
|
||||
name: "ipv4 address",
|
||||
input: "127.0.0.1",
|
||||
expectedIP: net.ParseIP("127.0.0.1"),
|
||||
expectedMask: net.CIDRMask(32, 32),
|
||||
},
|
||||
{
|
||||
name: "ipv6 address",
|
||||
input: "::1",
|
||||
expectedIP: net.ParseIP("::1"),
|
||||
expectedMask: net.CIDRMask(128, 128),
|
||||
},
|
||||
{
|
||||
name: "ipv4 cidr",
|
||||
input: "10.0.0.0/24",
|
||||
expectedIP: net.ParseIP("10.0.0.0"),
|
||||
expectedMask: net.CIDRMask(24, 32),
|
||||
},
|
||||
{
|
||||
name: "ipv6 cidr",
|
||||
input: "2001:db8::/64",
|
||||
expectedIP: net.ParseIP("2001:db8::"),
|
||||
expectedMask: net.CIDRMask(64, 128),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ipNet := ParseIPNet(test.input)
|
||||
|
||||
assert.NotNil(t, ipNet)
|
||||
if ipNet == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, test.expectedIP.Equal(ipNet.IP))
|
||||
assert.Equal(t, test.expectedMask, ipNet.Mask)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIPNetRejectsInvalidNetworks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "invalid ip",
|
||||
input: "not-an-ip",
|
||||
},
|
||||
{
|
||||
name: "ipv4 cidr with host bits set",
|
||||
input: "10.0.0.1/24",
|
||||
},
|
||||
{
|
||||
name: "ipv6 cidr with host bits set",
|
||||
input: "2001:db8::1/64",
|
||||
},
|
||||
{
|
||||
name: "invalid cidr mask",
|
||||
input: "10.0.0.0/33",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Nil(t, ParseIPNet(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNetSet(t *testing.T) {
|
||||
netSet, err := ParseNetSet([]string{
|
||||
"127.0.0.1",
|
||||
"10.0.0.0/24",
|
||||
"::1",
|
||||
"2001:db8::/64",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, netSet)
|
||||
if netSet == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, netSet.Has(net.ParseIP("127.0.0.1")))
|
||||
assert.True(t, netSet.Has(net.ParseIP("10.0.0.55")))
|
||||
assert.True(t, netSet.Has(net.ParseIP("::1")))
|
||||
assert.True(t, netSet.Has(net.ParseIP("2001:db8::abcd")))
|
||||
|
||||
assert.False(t, netSet.Has(net.ParseIP("127.0.0.2")))
|
||||
assert.False(t, netSet.Has(net.ParseIP("10.0.1.1")))
|
||||
assert.False(t, netSet.Has(net.ParseIP("::2")))
|
||||
assert.False(t, netSet.Has(net.ParseIP("2001:db9::1")))
|
||||
}
|
||||
|
||||
func TestParseNetSetReturnsErrorForInvalidNetwork(t *testing.T) {
|
||||
netSet, err := ParseNetSet([]string{"127.0.0.1", "10.0.0.1/24"})
|
||||
|
||||
assert.Nil(t, netSet)
|
||||
assert.EqualError(t, err, "could not parse IP network (10.0.0.1/24)")
|
||||
}
|
||||
|
|
@ -43,10 +43,13 @@ func healthCheck(paths, userAgents []string, next http.Handler) http.Handler {
|
|||
|
||||
func isHealthCheckRequest(paths, userAgents map[string]struct{}, req *http.Request) bool {
|
||||
if _, ok := paths[req.URL.EscapedPath()]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
|
||||
return true
|
||||
if len(userAgents) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@ var _ = Describe("HealthCheck suite", func() {
|
|||
healthCheckPaths: []string{"/ping"},
|
||||
healthCheckUserAgents: []string{"hc/1.0"},
|
||||
requestString: "http://example.com/ping",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "hc/1.0",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedBody: "OK",
|
||||
}),
|
||||
Entry("when requesting the healthcheck path with no health check user agents configured", &requestTableInput{
|
||||
healthCheckPaths: []string{"/ping"},
|
||||
healthCheckUserAgents: []string{},
|
||||
requestString: "http://example.com/ping",
|
||||
headers: map[string]string{},
|
||||
expectedStatus: 200,
|
||||
expectedBody: "OK",
|
||||
|
|
@ -85,15 +95,25 @@ var _ = Describe("HealthCheck suite", func() {
|
|||
expectedStatus: 404,
|
||||
expectedBody: "404 page not found\n",
|
||||
}),
|
||||
Entry("with a request from the health check user agent", &requestTableInput{
|
||||
Entry("with a request from the health check user agent on a non-healthcheck path", &requestTableInput{
|
||||
healthCheckPaths: []string{"/ping"},
|
||||
healthCheckUserAgents: []string{"hc/1.0"},
|
||||
requestString: "http://example.com/abc",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "hc/1.0",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedBody: "OK",
|
||||
expectedStatus: 404,
|
||||
expectedBody: "404 page not found\n",
|
||||
}),
|
||||
Entry("when an auth_request endpoint receives the configured health check user agent", &requestTableInput{
|
||||
healthCheckPaths: []string{"/ping"},
|
||||
healthCheckUserAgents: []string{"GoogleHC/1.0"},
|
||||
requestString: "http://example.com/oauth2/auth",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "GoogleHC/1.0",
|
||||
},
|
||||
expectedStatus: 404,
|
||||
expectedBody: "404 page not found\n",
|
||||
}),
|
||||
Entry("when a blank string is configured as a health check agent and a request has no user agent", &requestTableInput{
|
||||
healthCheckPaths: []string{"/ping"},
|
||||
|
|
@ -107,9 +127,11 @@ var _ = Describe("HealthCheck suite", func() {
|
|||
healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
|
||||
healthCheckUserAgents: []string{"hc/1.0"},
|
||||
requestString: "http://example.com/readiness_check",
|
||||
headers: map[string]string{},
|
||||
expectedStatus: 200,
|
||||
expectedBody: "OK",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "hc/1.0",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedBody: "OK",
|
||||
}),
|
||||
Entry("with multiple paths, request none of the healthcheck paths", &requestTableInput{
|
||||
healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
|
||||
|
|
@ -121,15 +143,15 @@ var _ = Describe("HealthCheck suite", func() {
|
|||
expectedStatus: 404,
|
||||
expectedBody: "404 page not found\n",
|
||||
}),
|
||||
Entry("with multiple user agents, request from a health check user agent", &requestTableInput{
|
||||
Entry("with multiple user agents, request from a health check user agent on a non-healthcheck path", &requestTableInput{
|
||||
healthCheckPaths: []string{"/ping"},
|
||||
healthCheckUserAgents: []string{"hc/1.0", "GoogleHC/1.0"},
|
||||
requestString: "http://example.com/abc",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "GoogleHC/1.0",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedBody: "OK",
|
||||
expectedStatus: 404,
|
||||
expectedBody: "404 page not found\n",
|
||||
}),
|
||||
Entry("with multiple user agents, request from none of the health check user agents", &requestTableInput{
|
||||
healthCheckPaths: []string{"/ping"},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http/httptest"
|
||||
|
||||
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
|
@ -39,6 +40,10 @@ var _ = Describe("RedirectToHTTPS suite", func() {
|
|||
scope := &middlewareapi.RequestScope{
|
||||
ReverseProxy: in.reverseProxy,
|
||||
}
|
||||
if in.reverseProxy {
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
scope.TrustedProxies = newRedirectTrustedProxySet("127.0.0.1")
|
||||
}
|
||||
req = middlewareapi.AddRequestScope(req, scope)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
|
|
@ -207,3 +212,13 @@ var _ = Describe("RedirectToHTTPS suite", func() {
|
|||
}),
|
||||
)
|
||||
})
|
||||
|
||||
func newRedirectTrustedProxySet(cidrs ...string) *ip.NetSet {
|
||||
set := ip.NewNetSet()
|
||||
for _, cidr := range cidrs {
|
||||
ipNet := ip.ParseIPNet(cidr)
|
||||
Expect(ipNet).ToNot(BeNil())
|
||||
set.AddIPNet(*ipNet)
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/justinas/alice"
|
||||
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
)
|
||||
|
||||
func NewScope(reverseProxy bool, idHeader string) alice.Constructor {
|
||||
func NewScope(reverseProxy bool, idHeader string, trustedProxies *ip.NetSet) alice.Constructor {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
scope := &middlewareapi.RequestScope{
|
||||
ReverseProxy: reverseProxy,
|
||||
RequestID: genRequestID(req, idHeader),
|
||||
ReverseProxy: reverseProxy,
|
||||
TrustedProxies: trustedProxies,
|
||||
RequestID: genRequestID(req, idHeader),
|
||||
}
|
||||
req = middlewareapi.AddRequestScope(req, scope)
|
||||
next.ServeHTTP(rw, req)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
|
@ -32,7 +33,7 @@ var _ = Describe("Scope Suite", func() {
|
|||
|
||||
Context("ReverseProxy is false", func() {
|
||||
BeforeEach(func() {
|
||||
handler := NewScope(false, testRequestHeader)(
|
||||
handler := NewScope(false, testRequestHeader, nil)(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextRequest = r
|
||||
w.WriteHeader(200)
|
||||
|
|
@ -60,8 +61,15 @@ var _ = Describe("Scope Suite", func() {
|
|||
})
|
||||
|
||||
Context("ReverseProxy is true", func() {
|
||||
var trustedProxies *ip.NetSet
|
||||
|
||||
BeforeEach(func() {
|
||||
handler := NewScope(true, testRequestHeader)(
|
||||
var err error
|
||||
|
||||
trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
handler := NewScope(true, testRequestHeader, trustedProxies)(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextRequest = r
|
||||
w.WriteHeader(200)
|
||||
|
|
@ -74,12 +82,18 @@ var _ = Describe("Scope Suite", func() {
|
|||
Expect(scope).ToNot(BeNil())
|
||||
Expect(scope.ReverseProxy).To(BeTrue())
|
||||
})
|
||||
|
||||
It("stores the trusted proxies on the scope", func() {
|
||||
scope := middlewareapi.GetRequestScope(nextRequest)
|
||||
Expect(scope).ToNot(BeNil())
|
||||
Expect(scope.TrustedProxies).To(BeIdenticalTo(trustedProxies))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Request ID header is present", func() {
|
||||
BeforeEach(func() {
|
||||
request.Header.Add(testRequestHeader, testRequestID)
|
||||
handler := NewScope(false, testRequestHeader)(
|
||||
handler := NewScope(false, testRequestHeader, nil)(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextRequest = r
|
||||
w.WriteHeader(200)
|
||||
|
|
@ -97,7 +111,7 @@ var _ = Describe("Scope Suite", func() {
|
|||
BeforeEach(func() {
|
||||
uuid.SetRand(mockRand{})
|
||||
|
||||
handler := NewScope(true, testRequestHeader)(
|
||||
handler := NewScope(true, testRequestHeader, nil)(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextRequest = r
|
||||
w.WriteHeader(200)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/justinas/alice"
|
||||
|
|
@ -31,6 +32,33 @@ const (
|
|||
sessionRefreshRetryPeriod = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
// isFatalRefreshError checks if a refresh error indicates a revoked or
|
||||
// non-existent session that should be immediately invalidated.
|
||||
// Fatal errors indicate the session is no longer valid at the provider level.
|
||||
// Non-fatal errors (network issues, timeouts) should not invalidate the session.
|
||||
//
|
||||
// Only checks standard OAuth2 error codes (RFC 6749 Section 5.2).
|
||||
// Does NOT check error_description strings as they are optional and provider-specific.
|
||||
func isFatalRefreshError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only check standard OAuth2 error codes (RFC 6749 Section 5.2)
|
||||
// Do NOT check error_description strings as they are optional and provider-specific
|
||||
fatalErrors := []string{
|
||||
"invalid_grant", // refresh token revoked, expired, or session terminated
|
||||
"invalid_client", // client credentials no longer valid
|
||||
}
|
||||
|
||||
for _, fe := range fatalErrors {
|
||||
if strings.Contains(err.Error(), fe) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StoredSessionLoaderOptions contains all of the requirements to construct
|
||||
// a stored session loader.
|
||||
// All options must be provided.
|
||||
|
|
@ -188,9 +216,24 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req
|
|||
// We are holding the lock and the session needs a refresh
|
||||
logger.Printf("Refreshing session - User: %s; SessionAge: %s", session.User, session.Age())
|
||||
if err := s.refreshSession(rw, req, session); err != nil {
|
||||
// If a preemptive refresh fails, we still keep the session
|
||||
// if validateSession succeeds.
|
||||
logger.Errorf("Unable to refresh session: %v", err)
|
||||
|
||||
// Check if this is a fatal error that indicates the session is revoked
|
||||
// or no longer valid at the provider level
|
||||
if isFatalRefreshError(err) {
|
||||
logger.Printf("Fatal refresh error detected (session revoked or invalid), clearing session for user: %s", session.User)
|
||||
|
||||
// Clear the session from storage (Redis) and remove the cookie
|
||||
if err := s.store.Clear(rw, req); err != nil {
|
||||
logger.Errorf("failed clearing session: %v", err)
|
||||
}
|
||||
|
||||
// Return error immediately to force re-authentication
|
||||
return fmt.Errorf("session invalidated due to fatal refresh error: %w", err)
|
||||
}
|
||||
|
||||
// For non-fatal errors (network issues, timeouts), keep the session
|
||||
// and let validateSession determine if it's still usable
|
||||
}
|
||||
|
||||
// Validate all sessions after any Redeem/Refresh operation (fail or success)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
|
|
@ -801,3 +802,53 @@ func (f *fakeSessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
|
|||
func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestIsFatalRefreshError tests the isFatalRefreshError function to ensure
|
||||
// it correctly identifies fatal OAuth2 errors that should invalidate a session.
|
||||
func TestIsFatalRefreshError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "invalid_grant error",
|
||||
err: fmt.Errorf("failed to get token: oauth2: \"invalid_grant\" \"Session not active\""),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_client error",
|
||||
err: fmt.Errorf("invalid_client: client not found"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "network timeout - not fatal",
|
||||
err: fmt.Errorf("Post \"https://keycloak/token\": dial tcp: connect: connection refused"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "server error - not fatal",
|
||||
err: fmt.Errorf("unexpected status code 500"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "generic refresh error - not fatal",
|
||||
err: fmt.Errorf("error refreshing tokens: context deadline exceeded"),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isFatalRefreshError(tt.err)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isFatalRefreshError(%v) = %v, want %v", tt.err, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,30 +15,30 @@ const (
|
|||
)
|
||||
|
||||
// GetRequestProto returns the request scheme or X-Forwarded-Proto if present
|
||||
// and the request is proxied.
|
||||
// and the request came from a trusted reverse proxy.
|
||||
func GetRequestProto(req *http.Request) string {
|
||||
proto := req.Header.Get(XForwardedProto)
|
||||
if !IsProxied(req) || proto == "" {
|
||||
if !CanTrustForwardedHeaders(req) || proto == "" {
|
||||
proto = req.URL.Scheme
|
||||
}
|
||||
return proto
|
||||
}
|
||||
|
||||
// GetRequestHost returns the request host header or X-Forwarded-Host if
|
||||
// present and the request is proxied.
|
||||
// present and the request came from a trusted reverse proxy.
|
||||
func GetRequestHost(req *http.Request) string {
|
||||
host := req.Header.Get(XForwardedHost)
|
||||
if !IsProxied(req) || host == "" {
|
||||
if !CanTrustForwardedHeaders(req) || host == "" {
|
||||
host = req.Host
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// GetRequestURI return the request URI or X-Forwarded-Uri if present and the
|
||||
// request is proxied.
|
||||
// request came from a trusted reverse proxy.
|
||||
func GetRequestURI(req *http.Request) string {
|
||||
uri := req.Header.Get(XForwardedURI)
|
||||
if !IsProxied(req) || uri == "" {
|
||||
if !CanTrustForwardedHeaders(req) || uri == "" {
|
||||
// Use RequestURI to preserve ?query
|
||||
uri = req.URL.RequestURI()
|
||||
}
|
||||
|
|
@ -46,17 +46,29 @@ func GetRequestURI(req *http.Request) string {
|
|||
}
|
||||
|
||||
// GetRequestPath returns the request URI or X-Forwarded-Uri if present and the
|
||||
// request is proxied but always strips the query parameters and only returns
|
||||
// the pure path
|
||||
// request came from a trusted reverse proxy but always strips the query
|
||||
// parameters and fragment suffixes and only returns the pure path.
|
||||
func GetRequestPath(req *http.Request) string {
|
||||
uri := GetRequestURI(req)
|
||||
uri := stripRequestFragment(GetRequestURI(req))
|
||||
|
||||
// Parse URI and return only the path component
|
||||
if parsedURL, err := url.Parse(uri); err == nil {
|
||||
return parsedURL.Path
|
||||
return stripRequestFragment(parsedURL.Path)
|
||||
}
|
||||
|
||||
// Fallback: strip query parameters manually
|
||||
return stripRequestQuery(uri)
|
||||
}
|
||||
|
||||
func stripRequestFragment(uri string) string {
|
||||
if idx := strings.Index(uri, "#"); idx != -1 {
|
||||
return uri[:idx]
|
||||
}
|
||||
|
||||
return uri
|
||||
}
|
||||
|
||||
func stripRequestQuery(uri string) string {
|
||||
if idx := strings.Index(uri, "?"); idx != -1 {
|
||||
return uri[:idx]
|
||||
}
|
||||
|
|
@ -64,17 +76,18 @@ func GetRequestPath(req *http.Request) string {
|
|||
return uri
|
||||
}
|
||||
|
||||
// IsProxied determines if a request was from a proxy based on the RequestScope
|
||||
// ReverseProxy tracker.
|
||||
func IsProxied(req *http.Request) bool {
|
||||
// CanTrustForwardedHeaders determines if forwarded headers should be processed
|
||||
// based on the RequestScope and the direct caller's address.
|
||||
func CanTrustForwardedHeaders(req *http.Request) bool {
|
||||
scope := middlewareapi.GetRequestScope(req)
|
||||
if scope == nil {
|
||||
return false
|
||||
}
|
||||
return scope.ReverseProxy
|
||||
|
||||
return scope.CanTrustForwardedHeaders(req)
|
||||
}
|
||||
|
||||
func IsForwardedRequest(req *http.Request) bool {
|
||||
return IsProxied(req) &&
|
||||
return CanTrustForwardedHeaders(req) &&
|
||||
req.Host != GetRequestHost(req)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http/httptest"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
|
@ -19,8 +20,13 @@ var _ = Describe("Util Suite", func() {
|
|||
uriNoQueryParams = "/test/endpoint"
|
||||
)
|
||||
var req *http.Request
|
||||
var trustedProxies *ip.NetSet
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
req = httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
fmt.Sprintf("%s://%s%s", proto, host, uriWithQueryParams),
|
||||
|
|
@ -29,7 +35,7 @@ var _ = Describe("Util Suite", func() {
|
|||
})
|
||||
|
||||
Context("GetRequestHost", func() {
|
||||
Context("IsProxied is false", func() {
|
||||
Context("trusted forwarded headers are disabled", func() {
|
||||
BeforeEach(func() {
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
||||
})
|
||||
|
|
@ -44,10 +50,12 @@ var _ = Describe("Util Suite", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Context("IsProxied is true", func() {
|
||||
Context("trusted forwarded headers are enabled", func() {
|
||||
BeforeEach(func() {
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
ReverseProxy: true,
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -63,7 +71,7 @@ var _ = Describe("Util Suite", func() {
|
|||
})
|
||||
|
||||
Context("GetRequestProto", func() {
|
||||
Context("IsProxied is false", func() {
|
||||
Context("trusted forwarded headers are disabled", func() {
|
||||
BeforeEach(func() {
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
||||
})
|
||||
|
|
@ -78,10 +86,12 @@ var _ = Describe("Util Suite", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Context("IsProxied is true", func() {
|
||||
Context("trusted forwarded headers are enabled", func() {
|
||||
BeforeEach(func() {
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
ReverseProxy: true,
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -97,7 +107,7 @@ var _ = Describe("Util Suite", func() {
|
|||
})
|
||||
|
||||
Context("GetRequestURI", func() {
|
||||
Context("IsProxied is false", func() {
|
||||
Context("trusted forwarded headers are disabled", func() {
|
||||
BeforeEach(func() {
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
||||
})
|
||||
|
|
@ -112,10 +122,12 @@ var _ = Describe("Util Suite", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Context("IsProxied is true", func() {
|
||||
Context("trusted forwarded headers are enabled", func() {
|
||||
BeforeEach(func() {
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
ReverseProxy: true,
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -131,7 +143,7 @@ var _ = Describe("Util Suite", func() {
|
|||
})
|
||||
|
||||
Context("GetRequestPath", func() {
|
||||
Context("IsProxied is false", func() {
|
||||
Context("trusted forwarded headers are disabled", func() {
|
||||
BeforeEach(func() {
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
||||
})
|
||||
|
|
@ -140,16 +152,35 @@ var _ = Describe("Util Suite", func() {
|
|||
Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
|
||||
})
|
||||
|
||||
It("drops fragment content from a parsed request path", func() {
|
||||
// Simulate net/http ParseRequestURI preserving '#' in URL.Path.
|
||||
req.URL.Path = "/foo/secret#/bar"
|
||||
req.URL.RawPath = "/foo/secret%23/bar"
|
||||
Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
|
||||
})
|
||||
|
||||
It("drops fragment-like suffixes from encoded number signs", func() {
|
||||
req = httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
fmt.Sprintf("%s://%s/foo/secret%%23/bar?query=param", proto, host),
|
||||
nil,
|
||||
)
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
||||
Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
|
||||
})
|
||||
|
||||
It("ignores X-Forwarded-Uri and returns the URI (without query params)", func() {
|
||||
req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
|
||||
Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
|
||||
})
|
||||
})
|
||||
|
||||
Context("IsProxied is true", func() {
|
||||
Context("trusted forwarded headers are enabled", func() {
|
||||
BeforeEach(func() {
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
ReverseProxy: true,
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -161,6 +192,37 @@ var _ = Describe("Util Suite", func() {
|
|||
req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
|
||||
Expect(util.GetRequestPath(req)).To(Equal("/some/other/path"))
|
||||
})
|
||||
|
||||
It("drops fragment-like suffixes from the X-Forwarded-Uri", func() {
|
||||
req.Header.Add("X-Forwarded-Uri", "/foo/secret%23/bar?query=param")
|
||||
Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("CanTrustForwardedHeaders", func() {
|
||||
It("returns false when no scope is present", func() {
|
||||
Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
|
||||
})
|
||||
|
||||
It("returns true when the remote address is trusted", func() {
|
||||
req.RemoteAddr = "127.0.0.1:4180"
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
})
|
||||
|
||||
Expect(util.CanTrustForwardedHeaders(req)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns false when the remote address is untrusted", func() {
|
||||
req.RemoteAddr = "192.0.2.10:4180"
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
ReverseProxy: true,
|
||||
TrustedProxies: trustedProxies,
|
||||
})
|
||||
|
||||
Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -498,7 +498,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
|
|||
|
||||
handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
|
||||
|
||||
proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
|
||||
proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
|
@ -549,7 +549,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
|
||||
noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
|
||||
noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
|
||||
defer noPassHostServer.Close()
|
||||
|
||||
origin := "http://example.localhost"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ func validateAllowlists(o *options.Options) []string {
|
|||
|
||||
msgs = append(msgs, validateAuthRoutes(o)...)
|
||||
msgs = append(msgs, validateAuthRegexes(o)...)
|
||||
msgs = append(msgs, validateTrustedProxyIPs(o)...)
|
||||
msgs = append(msgs, validateTrustedIPs(o)...)
|
||||
|
||||
if len(o.TrustedIPs) > 0 && o.ReverseProxy {
|
||||
|
|
@ -28,6 +29,17 @@ func validateAllowlists(o *options.Options) []string {
|
|||
return msgs
|
||||
}
|
||||
|
||||
// validateTrustedProxyIPs validates IP/CIDRs for trusted reverse proxies.
|
||||
func validateTrustedProxyIPs(o *options.Options) []string {
|
||||
msgs := []string{}
|
||||
for i, ipStr := range o.TrustedProxyIPs {
|
||||
if ip.ParseIPNet(ipStr) == nil {
|
||||
msgs = append(msgs, fmt.Sprintf("trusted_proxy_ips[%d] (%s) could not be recognized", i, ipStr))
|
||||
}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes
|
||||
func validateAuthRoutes(o *options.Options) []string {
|
||||
msgs := []string{}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ var _ = Describe("Allowlist", func() {
|
|||
errStrings []string
|
||||
}
|
||||
|
||||
type validateTrustedProxyIPsTableInput struct {
|
||||
trustedProxyIPs []string
|
||||
errStrings []string
|
||||
}
|
||||
|
||||
DescribeTable("validateRoutes",
|
||||
func(r *validateRoutesTableInput) {
|
||||
opts := &options.Options{
|
||||
|
|
@ -121,4 +126,29 @@ var _ = Describe("Allowlist", func() {
|
|||
},
|
||||
}),
|
||||
)
|
||||
|
||||
DescribeTable("validateTrustedProxyIPs",
|
||||
func(t *validateTrustedProxyIPsTableInput) {
|
||||
opts := &options.Options{
|
||||
TrustedProxyIPs: t.trustedProxyIPs,
|
||||
}
|
||||
Expect(validateTrustedProxyIPs(opts)).To(ConsistOf(t.errStrings))
|
||||
},
|
||||
Entry("Valid trusted proxy IPs", &validateTrustedProxyIPsTableInput{
|
||||
trustedProxyIPs: []string{
|
||||
"127.0.0.1",
|
||||
"10.32.0.1/32",
|
||||
"::1",
|
||||
"2a12:105:ee7:9234:0:0:0:0/64",
|
||||
},
|
||||
errStrings: []string{},
|
||||
}),
|
||||
Entry("Invalid trusted proxy IPs", &validateTrustedProxyIPsTableInput{
|
||||
trustedProxyIPs: []string{"[::1]", "alkwlkbn/32"},
|
||||
errStrings: []string{
|
||||
"trusted_proxy_ips[0] ([::1]) could not be recognized",
|
||||
"trusted_proxy_ips[1] (alkwlkbn/32) could not be recognized",
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ func NewValidator(domains []string, usersFile string) func(string) bool {
|
|||
|
||||
// isEmailValidWithDomains checks if the authenticated email is validated against the provided domain
|
||||
func isEmailValidWithDomains(email string, allowedDomains []string) bool {
|
||||
if strings.Count(email, "@") != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, domain := range allowedDomains {
|
||||
// allow if the domain is perfect suffix match with the email
|
||||
if strings.HasSuffix(email, "@"+domain) {
|
||||
|
|
@ -119,7 +123,6 @@ func isEmailValidWithDomains(email string, allowedDomains []string) bool {
|
|||
// allow if the domain is prefixed with . or *. and
|
||||
// the last element (split on @) has the suffix as the domain
|
||||
atoms := strings.Split(email, "@")
|
||||
|
||||
if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
|
||||
(strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -404,6 +404,27 @@ func TestValidatorCases(t *testing.T) {
|
|||
allowedDomains: []string{"*.company.com"},
|
||||
expectedAuthZ: false,
|
||||
},
|
||||
{
|
||||
name: "CheckThatTwoAtSignsIsInvalid",
|
||||
email: "attacker@evil.com@company.com",
|
||||
allowedEmails: []string(nil),
|
||||
allowedDomains: []string{"company.com"},
|
||||
expectedAuthZ: false,
|
||||
},
|
||||
{
|
||||
name: "CheckThatTwoAtSignsIsInvalidEvenWithDotPrefix",
|
||||
email: "attacker@evil.com@company.com",
|
||||
allowedEmails: []string(nil),
|
||||
allowedDomains: []string{".company.com"},
|
||||
expectedAuthZ: false,
|
||||
},
|
||||
{
|
||||
name: "CheckThatTwoAtSignsIsInvalidEvenWithWildcardPrefix",
|
||||
email: "attacker@evil.com@foo.company.com",
|
||||
allowedEmails: []string(nil),
|
||||
allowedDomains: []string{"*.company.com"},
|
||||
expectedAuthZ: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
|
|||
Loading…
Reference in New Issue