Merge branch 'master' into feat/redis-mtls

This commit is contained in:
Piotr Karatkevich 2026-04-25 02:25:39 +03:00 committed by GitHub
commit df0a78475f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 984 additions and 207 deletions

View File

@ -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

View File

@ -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 |

View File

@ -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"

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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"

View File

@ -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:

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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"

View File

@ -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 |

View File

@ -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

View File

@ -42,5 +42,8 @@
},
"engines": {
"node": ">=18.0"
},
"overrides" : {
"webpackbar" : "^7.0.0"
}
}

View File

@ -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) | |

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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"),
}),
)

View File

@ -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)
}

View File

@ -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 {

View File

@ -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, "/")
}

View File

@ -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())
})
})
})

View File

@ -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",

View File

@ -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\"")

View File

@ -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"))
})
})

View File

@ -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,
})
}

View File

@ -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
}

118
pkg/ip/parse_ip_net_test.go Normal file
View File

@ -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)")
}

View File

@ -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
}

View File

@ -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"},

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
})
}
}

View File

@ -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)
}

View File

@ -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())
})
})
})

View File

@ -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"

View File

@ -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{}

View File

@ -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",
},
}),
)
})

View File

@ -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

View File

@ -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 {