git.iko.soy/typeable/oauth2-proxy
on master help sign in
99 files changed+2490−725
since splitsnapshots
unifiedsplit
.devcontainer/Dockerfile+1−1 .github/ISSUE_TEMPLATE/config.yml+2−2 .github/PULL_REQUEST_TEMPLATE.md+3−2 .github/renovate.json5+9 .github/workflows/ci.yml+8−14 .github/workflows/codeql.yml+4−4 .github/workflows/create-release.yml+2−2 .github/workflows/docs.yml+7−7 .github/workflows/labeler.yml+1−1 .github/workflows/nightly.yml+4−4 .github/workflows/publish-release.yml+7−13 .github/workflows/stale.yml+1−1 CHANGELOG.md+124−4 MAINTAINERS.md+1−1 Makefile+9−4 README.md+4−4 contrib/local-environment/Makefile+2−2 contrib/local-environment/dex-traefik.yaml+25 contrib/local-environment/dex.yaml+13−17 contrib/local-environment/docker-compose-alpha-config.yaml+2−15 contrib/local-environment/docker-compose-gitea.yaml+2−2 contrib/local-environment/docker-compose-keycloak.yaml+6−6 contrib/local-environment/docker-compose-nginx.yaml+6−30 contrib/local-environment/docker-compose-traefik.yaml+42−18 contrib/local-environment/docker-compose.yaml+2−15 contrib/local-environment/kubernetes/Chart.lock+3−3 contrib/local-environment/kubernetes/Chart.yaml+1−1 contrib/local-environment/nginx.conf+29−76 contrib/local-environment/oauth2-proxy-nginx.cfg+11−6 contrib/local-environment/oauth2-proxy-traefik.cfg+4−4 contrib/local-environment/traefik/dynamic.yaml+11−4 contrib/local-environment/traefik/traefik.yaml+2 docs/docs/configuration/alpha_config.md+12−1 docs/docs/configuration/alpha_config.md.tmpl+11 docs/docs/configuration/integrations/nginx.md+27−11 docs/docs/configuration/overview.md+44−39 docs/docs/configuration/providers/atlassian.md+26 docs/docs/configuration/providers/dtt_sso.md+45 docs/docs/configuration/providers/index.md+2 docs/docs/configuration/systemd_socket.md+8 docs/docs/installation.md+1−1 docs/docusaurus.config.js+1−1 docs/package.json+3 docs/sidebars.js+2 docs/versioned_docs/version-7.15.x/configuration/overview.md+42−37 docs/versioned_docs/version-7.15.x/configuration/systemd_socket.md+8 docs/versioned_docs/version-7.15.x/installation.md+1−1 go.mod+42−46 go.sum+100−106 main_test.go+2−2 oauthproxy.go+31−20 oauthproxy_test.go+243−65 pkg/apis/middleware/scope.go+48−1 pkg/apis/middleware/scope_test.go+34 pkg/apis/options/load.go+1−1 pkg/apis/options/load_test.go+2−2 pkg/apis/options/options.go+2 pkg/apis/options/providers.go+6 pkg/apis/options/upstreams.go+7−5 pkg/apis/sessions/session_state.go+2 pkg/apis/sessions/session_state_test.go+34−5 pkg/app/redirect/director_test.go+33−2 pkg/authentication/hmacauth/hmacauth_test.go+1−1 pkg/cookies/cookies_test.go+7−1 pkg/ip/parse_ip_net.go+16 pkg/ip/parse_ip_net_test.go+118 pkg/ip/realclientip.go+7−1 pkg/ip/realclientip_test.go+4 pkg/middleware/healthcheck.go+7−4 pkg/middleware/healthcheck_test.go+31−9 pkg/middleware/redirect_to_https_test.go+15 pkg/middleware/scope.go+5−3 pkg/middleware/scope_test.go+18−4 pkg/middleware/stored_session.go+46−2 pkg/middleware/stored_session_test.go+51 pkg/requests/builder_test.go+15−15 pkg/requests/util/util.go+28−15 pkg/requests/util/util_test.go+74−12 pkg/upstream/http.go+55−16 pkg/upstream/http_test.go+57−3 pkg/upstream/proxy.go+14−14 pkg/upstream/proxy_test.go+33−1 pkg/upstream/upstream_suite_test.go+1−1 pkg/validation/allowlist.go+12 pkg/validation/allowlist_test.go+30 pkg/validation/sessions_test.go+2−2 providers/atlassian-cloud.go+99 providers/atlassian-cloud_test.go+93 providers/azure.go+2−2 providers/dtt_sso.go+202 providers/dtt_sso_test.go+237 providers/google.go+2−2 providers/logingov.go+2−1 providers/ms_entra_id.go+2−1 providers/provider_default.go+2−1 providers/providers.go+7−3 providers/srht.go+2−1 validator.go+4−1 validator_test.go+21
.devcontainer/Dockerfile+1−1
@@ -1,4 +1,4 @@
1−FROM mcr.microsoft.com/vscode/devcontainers/go:1-1.25
1+FROM mcr.microsoft.com/vscode/devcontainers/go:1-1.26
22
33SHELL ["/bin/bash", "-o", "pipefail", "-c"]
44
.github/ISSUE_TEMPLATE/config.yml+2−2
@@ -1,6 +1,6 @@
11blank_issues_enabled: false
22contact_links:
3− - name: OAuth2-Proxy Slack
4− url: https://gophers.slack.com/messages/CM2RSS25N
3+ - name: OAuth2 Proxy Slack
4+ url: https://cloud-native.slack.com/archives/C098Y5URZ2N
55 about: Feel free to ask any questions here.
66
.github/PULL_REQUEST_TEMPLATE.md+3−2
@@ -20,7 +20,8 @@
2020<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
2121<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
2222
23−- [ ] My change requires a change to the documentation or CHANGELOG.
24−- [ ] I have updated the documentation/CHANGELOG accordingly.
23+- [ ] I have added an entry for my changes to the [CHANGELOG.md](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/CHANGELOG.md).
24+- [ ] I have [signed off](https://github.com/apps/dco) all my commits.
2525- [ ] I have created a feature (non-master) branch for my PR.
26+- [ ] I have used [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#examples) for the PR title.
2627- [ ] I have written tests for my code changes.
.github/renovate.json5+9
@@ -46,6 +46,15 @@
4646 depNameTemplate: "alpine",
4747 datasourceTemplate: "docker",
4848 },
49+ {
50+ customType: "regex",
51+ fileMatch: [
52+ "^Makefile",
53+ ],
54+ matchStrings: [
55+ "# renovate: datasource=(?<datasource>.*?) depName=(?<depName>.*?)\\s+?GOLANGCILINT_VERSION \\?= (?<currentValue>.*)\\s",
56+ ],
57+ },
4958 {
5059 customType: "regex",
5160 fileMatch: ["(^|/)\\.github/workflows/[^/]+\\.ya?ml$"],
.github/workflows/ci.yml+8−14
@@ -19,7 +19,7 @@ jobs:
1919 runs-on: ubuntu-latest
2020 steps:
2121 - name: Check out code
22− uses: actions/checkout@v6
22+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2323 with:
2424 fetch-depth: 0
2525
@@ -30,17 +30,11 @@ jobs:
3030 id: go-version
3131
3232 - name: Set up Go
33− uses: actions/setup-go@v6
33+ uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
3434 with:
3535 go-version: ${{ steps.go-version.outputs.version }}
3636 check-latest: true
3737
38− - name: Install golangci-lint
39− uses: golangci/golangci-lint-action@v9
40− with:
41− install-only: true
42− version: v2.11.4 # renovate: datasource=github-tags depName=golangci/golangci-lint
43−
4438 - name: Verify Code Generation
4539 run: |
4640 make verify-generate
@@ -74,14 +68,14 @@ jobs:
7468
7569 - name: Upload Coverage Report
7670 if: github.event_name == 'push'
77− uses: qltysh/qlty-action/coverage@v2
71+ uses: qltysh/qlty-action/coverage@08a0a862c159eae9b9003081da6663d96efef637 # v2.3.0
7872 with:
7973 oidc: true
8074 files: lcov.info
8175
8276 - name: Run Trivy vulnerability scanner
8377 if: (!startsWith(github.head_ref, 'release'))
84− uses: aquasecurity/trivy-action@0.35.0
78+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
8579 with:
8680 scan-type: 'rootfs'
8781 scan-ref: './oauth2-proxy'
@@ -93,7 +87,7 @@ jobs:
9387
9488 - name: Upload Trivy scan results
9589 if: (!startsWith(github.head_ref, 'release'))
96− uses: github/codeql-action/upload-sarif@v4
90+ uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
9791 with:
9892 sarif_file: 'trivy-results.sarif'
9993
@@ -101,16 +95,16 @@ jobs:
10195 runs-on: ubuntu-latest
10296 steps:
10397 - name: Check out code
104− uses: actions/checkout@v6
98+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
10599 with:
106100 fetch-depth: 0
107101
108102 - name: Set up QEMU
109− uses: docker/setup-qemu-action@v3
103+ uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
110104
111105 - name: Set up Docker Buildx
112106 id: buildx
113− uses: docker/setup-buildx-action@v3
107+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
114108
115109 - name: Docker Build
116110 if: (!startsWith(github.head_ref, 'release'))
.github/workflows/codeql.yml+4−4
@@ -32,18 +32,18 @@ jobs:
3232
3333 steps:
3434 - name: Checkout repository
35− uses: actions/checkout@v6
35+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
3636
3737 # Initializes the CodeQL tools for scanning.
3838 - name: Initialize CodeQL
39− uses: github/codeql-action/init@v3
39+ uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
4040 with:
4141 languages: go
4242
4343 # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
4444 # If this step fails, then you should remove it and run the build manually (see below)
4545 - name: Autobuild
46− uses: github/codeql-action/autobuild@v3
46+ uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
4747
4848 # ℹ️ Command-line programs to run using the OS shell.
4949 # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
5757 # make release
5858
5959 - name: Perform CodeQL Analysis
60− uses: github/codeql-action/analyze@v3
60+ uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
.github/workflows/create-release.yml+2−2
@@ -17,7 +17,7 @@ jobs:
1717 runs-on: ubuntu-latest
1818 steps:
1919 - name: Check out code
20− uses: actions/checkout@v6
20+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2121 with:
2222 ref: master
2323 fetch-depth: 0
@@ -54,7 +54,7 @@ jobs:
5454 git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
5555
5656 - name: Setup node
57− uses: actions/setup-node@v6
57+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
5858 with:
5959 node-version-file: docs/package.json
6060
.github/workflows/docs.yml+7−7
@@ -14,13 +14,13 @@ jobs:
1414 if: github.event_name == 'pull_request'
1515 runs-on: ubuntu-latest
1616 steps:
17− - uses: actions/checkout@v6
17+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
1818
1919 - name: Setup Pages
2020 id: pages
21− uses: actions/configure-pages@v5
21+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
2222
23− - uses: actions/setup-node@v6
23+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
2424 with:
2525 # renovate: datasource=node-version depName=node
2626 node-version: 24
@@ -35,9 +35,9 @@ jobs:
3535 if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
3636 runs-on: ubuntu-latest
3737 steps:
38− - uses: actions/checkout@v6
38+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
3939
40− - uses: actions/setup-node@v6
40+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
4141 with:
4242 # renovate: datasource=node-version depName=node
4343 node-version: 24
@@ -49,7 +49,7 @@ jobs:
4949 npm run build
5050
5151 - name: Upload artifact
52− uses: actions/upload-pages-artifact@v3
52+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
5353 with:
5454 path: ./docs/build
5555
@@ -65,4 +65,4 @@ jobs:
6565 steps:
6666 - name: Deploy to GitHub Pages
6767 id: deployment
68− uses: actions/deploy-pages@v4
68+ uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
.github/workflows/labeler.yml+1−1
@@ -9,7 +9,7 @@ jobs:
99 pull-requests: write
1010 runs-on: ubuntu-latest
1111 steps:
12− - uses: actions/labeler@v6
12+ - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
1313 with:
1414 sync-labels: true
1515 dot: true
.github/workflows/nightly.yml+4−4
@@ -12,21 +12,21 @@ jobs:
1212 if: github.repository == 'oauth2-proxy/oauth2-proxy'
1313 steps:
1414 - name: Check out code
15− uses: actions/checkout@v6
15+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
1616 with:
1717 ref: master
1818 fetch-depth: 0
1919 fetch-tags: true
2020
2121 - name: Set up QEMU
22− uses: docker/setup-qemu-action@v3
22+ uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
2323
2424 - name: Set up Docker Buildx
2525 id: buildx
26− uses: docker/setup-buildx-action@v3
26+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
2727
2828 - name: Login to quay.io
29− uses: docker/login-action@v3
29+ uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
3030 with:
3131 registry: quay.io/oauth2-proxy
3232 username: ${{ secrets.REGISTRY_USERNAME_NIGHTLY }}
.github/workflows/publish-release.yml+7−13
@@ -20,7 +20,7 @@ jobs:
2020 tag: ${{ steps.tag.outputs.version }}
2121 steps:
2222 - name: Check out code
23− uses: actions/checkout@v6
23+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2424 with:
2525 ref: ${{ github.event.pull_request.merge_commit_sha }}
2626 fetch-depth: 0
@@ -50,17 +50,11 @@ jobs:
5050 id: go-version
5151
5252 - name: Set up Go
53− uses: actions/setup-go@v6
53+ uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
5454 with:
5555 go-version: ${{ steps.go-version.outputs.version }}
5656 check-latest: true
5757
58− - name: Install golangci-lint
59− uses: golangci/golangci-lint-action@v9
60− with:
61− install-only: true
62− version: v2.11.4 # renovate: datasource=github-tags depName=golangci/golangci-lint
63−
6458 - name: Get go dependencies
6559 run: |
6660 go mod download
@@ -70,7 +64,7 @@ jobs:
7064
7165 # Upload artifacts in case of workflow failure
7266 - name: Upload Artifacts
73− uses: actions/upload-artifact@v7
67+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
7468 with:
7569 name: oauth2-proxy-artifacts
7670 path: |
@@ -106,21 +100,21 @@ jobs:
106100 runs-on: ubuntu-latest
107101 steps:
108102 - name: Check out code
109− uses: actions/checkout@v6
103+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
110104 with:
111105 ref: ${{ needs.publish.outputs.tag }}
112106 fetch-depth: 0
113107 fetch-tags: true
114108
115109 - name: Set up QEMU
116− uses: docker/setup-qemu-action@v3
110+ uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
117111
118112 - name: Set up Docker Buildx
119113 id: buildx
120− uses: docker/setup-buildx-action@v3
114+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
121115
122116 - name: Login to quay.io
123− uses: docker/login-action@v3
117+ uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
124118 with:
125119 registry: quay.io/oauth2-proxy
126120 username: ${{ secrets.REGISTRY_USERNAME }}
.github/workflows/stale.yml+1−1
@@ -10,7 +10,7 @@ jobs:
1010 runs-on: ubuntu-latest
1111
1212 steps:
13− - uses: actions/stale@v10
13+ - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
1414 with:
1515 repo-token: ${{ secrets.GITHUB_TOKEN }}
1616 days-before-stale: 180
CHANGELOG.md+124−4
@@ -2,18 +2,138 @@
22
33## Release Highlights
44
5−- Fixes [CVE-2026-33186](https://nvd.nist.gov/vuln/detail/CVE-2026-33186)
6− - OAuth2 Proxy was not impacted by this vulnerability as it isn't in the path of execution
5+## Important Notes
6+
7+## Breaking Changes
8+
9+## Changes since v7.15.4
10+
11+# V7.15.4
12+
13+## Release Highlights
14+
15+- 🔵 Golang version upgrade to v1.26.7
16+ - Upgrade of all dependencies to their latest versions
17+- 🕵️‍♀️ Vulnerabilities have been addressed
18+ - [CVE-2026-33818](https://nvd.nist.gov/vuln/detail/CVE-2026-33818)
19+ - [CVE-2026-39821](https://nvd.nist.gov/vuln/detail/CVE-2026-39821)
20+ - [CVE-2026-39822](https://nvd.nist.gov/vuln/detail/CVE-2026-39822)
21+ - [CVE-2026-42505](https://nvd.nist.gov/vuln/detail/CVE-2026-42505)
22+ - [CVE-2026-46600](https://nvd.nist.gov/vuln/detail/CVE-2026-46600)
23+ - [CVE-2026-56852](https://nvd.nist.gov/vuln/detail/CVE-2026-56852)
24+ - [CVE-2026-56853](https://nvd.nist.gov/vuln/detail/CVE-2026-56853)
25+ - [CVE-2026-56858](https://nvd.nist.gov/vuln/detail/CVE-2026-56858)
26+ - [CVE-2026-56859](https://nvd.nist.gov/vuln/detail/CVE-2026-56859)
27+ - [CVE-2026-56860](https://nvd.nist.gov/vuln/detail/CVE-2026-56860)
28+ - [CVE-2026-56862](https://nvd.nist.gov/vuln/detail/CVE-2026-56862)
729
830## Important Notes
931
1032## Breaking Changes
1133
12−## Changes since v7.15.0
34+## Changes since v7.15.3
1335
14−# V7.15.0
36+- [#3493](https://github.com/oauth2-proxy/oauth2-proxy/pull/3493) chore(deps): update gomod @tuunit
37+
38+# V7.15.3
39+
40+## Release Highlights
41+
42+- 🔵 Golang version upgrade to v1.26.4
43+ - Upgrade of all dependencies to their latest versions
44+- 🕵️‍♀️ Vulnerabilities have ben addressed
45+ - [CVE-2026-33811](https://nvd.nist.gov/vuln/detail/)
46+ - [CVE-2026-33814](https://nvd.nist.gov/vuln/detail/)
47+ - [CVE-2026-39820](https://nvd.nist.gov/vuln/detail/)
48+ - [CVE-2026-39836](https://nvd.nist.gov/vuln/detail/)
49+ - [CVE-2026-42499](https://nvd.nist.gov/vuln/detail/)
50+ - [CVE-2026-42504](https://nvd.nist.gov/vuln/detail/)
51+ - [CVE-2026-39823](https://nvd.nist.gov/vuln/detail/)
52+ - [CVE-2026-39826](https://nvd.nist.gov/vuln/detail/)
53+ - [CVE-2026-39825](https://nvd.nist.gov/vuln/detail/)
54+ - [CVE-2026-27145](https://nvd.nist.gov/vuln/detail/)
55+ - [CVE-2026-42507](https://nvd.nist.gov/vuln/detail/)
56+
57+## Important Notes
58+
59+## Breaking Changes
60+
61+## Changes since v7.15.2
62+
63+- [#3477](https://github.com/oauth2-proxy/oauth2-proxy/pull/3477) chore(dep): bump go to 1.26 and migrate of reverse proxy handling
64+
65+# V7.15.2
66+
67+## Release Highlights
68+
69+- 🔵 Golang version upgrade to v1.25.9
70+ - Upgrade of all dependencies to their latest versions
71+ - [CVE-2026-34986](https://nvd.nist.gov/vuln/detail/CVE-2026-34986)
72+ - [CVE-2026-32281](https://nvd.nist.gov/vuln/detail/CVE-2026-32281)
73+ - [CVE-2026-32289](https://nvd.nist.gov/vuln/detail/CVE-2026-32289)
74+ - [CVE-2026-32288](https://nvd.nist.gov/vuln/detail/CVE-2026-32288)
75+ - [CVE-2026-32280](https://nvd.nist.gov/vuln/detail/CVE-2026-32280)
76+ - [CVE-2026-32282](https://nvd.nist.gov/vuln/detail/CVE-2026-32282)
77+ - [CVE-2026-32283](https://nvd.nist.gov/vuln/detail/CVE-2026-32283)
78+- 🕵️‍♀️ Vulnerabilities have been addressed
79+
80+## Important Notes
81+
82+We have had security audits performed on OAuth2 Proxy in the past couple of weeks and as a result we have fixed
83+several CRITICAL vulnerabilities.
84+
85+The security vulnerabilities include multiple authentication bypasses and a potential session fixation attack.
86+For more details and to identify if you are effects, we urge all users of OAuth2 Proxy to read the security
87+disclosures.
88+
89+- (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
90+- (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
91+- (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
92+- (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
93+
94+Furthermore, for improving the security of OAuth2 Proxy we introduced a new flag `--trusted-proxy-ip` that allows users
95+to explicitly specify trusted reverse proxy IPs for the `X-Forwarded-*` headers. This is an important step to prevent
96+potential header spoofing attacks and to ensure that OAuth2 Proxy only trusts headers from known and trusted sources.
97+We highly recommend users to review their deployment architecture and consider using this flag to enhance the security
98+of their OAuth2 Proxy instances. Check the docs for more details: https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview#proxy-options
99+
100+Furthermore, we want to thank everyone who contributed to the audits and reported potential issues to make open source
101+software like OAuth2 Proxy more secure for everyone.
102+
103+## Breaking Changes
104+
105+## Changes since v7.15.1
106+
107+- [#3411](https://github.com/oauth2-proxy/oauth2-proxy/pull/3411) chore(deps): update gomod dependencies (@tuunit)
108+- [#3333](https://github.com/oauth2-proxy/oauth2-proxy/pull/3333) fix: invalidate session on fatal OAuth2 refresh errors (@frhack)
109+- [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)
110+- [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)
111+- [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)
112+- [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)
113+- [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)
114+
115+# V7.15.1
116+
117+## Release Highlights
118+
119+- 🐛 Squashed some bugs
120+- 🕵️‍♀️ Vulnerabilities have been addressed
121+ - [CVE-2026-33186](https://nvd.nist.gov/vuln/detail/CVE-2026-33186)
122+ OAuth2 Proxy was not impacted by this vulnerability as it isn't in the path of execution
123+
124+## Important Notes
125+
126+## Breaking Changes
127+
128+## Changes since v7.15.0
15129
16130- [#3382](https://github.com/oauth2-proxy/oauth2-proxy/pull/3382) chore(deps): update gomod and golangci/golangci-lint to v2.11.4 (@tuunit)
131+- [#3374](https://github.com/oauth2-proxy/oauth2-proxy/pull/3374) fix: handle Unix socket RemoteAddr in IP resolution (@H1net)
132+- [#3381](https://github.com/oauth2-proxy/oauth2-proxy/pull/3381) fix: do not log error for backend logout 204 (@artificiosus)
133+- [#3327](https://github.com/oauth2-proxy/oauth2-proxy/pull/3327) fix: improve logging when session refresh token is missing (@yosri-brh)
134+- [#2767](https://github.com/oauth2-proxy/oauth2-proxy/pull/2767) fix: propagate errors during route building (@sybereal)
135+
136+# V7.15.0
17137
18138## Release Highlights
19139
MAINTAINERS.md+1−1
@@ -6,7 +6,7 @@ by our [project governance](GOVERNANCE.md).
66| Name | GitHub Handle | Domains of reponsibility | Email Alias | Affiliation |
77| ---------------- | ------------------------------------------------------ | ------------------------ | -------------------------- | ----------- |
88| Joel Speed | [@JoelSpeed](https://github.com/joelspeed) | Governance, Core | joel@oauth2-proxy.dev | Red Hat |
9−| Jan Larwig | [@tuunit](https://github.com/tuunit) | Governance, Core | jan@oauth2-proxy.dev | IONOS Cloud |
9+| Jan Larwig | [@tuunit](https://github.com/tuunit) | Governance, Core | jan@oauth2-proxy.dev | STACKIT |
1010| JJ Łakis | [@jjlakis](https://github.com/jjlakis) | Provider | jj@oauth2-proxy.dev | - |
1111| Koen van Zuijlen | [@kvanzuijlen](https://github.com/kvanzuijlen) | CI | koen@oauth2-proxy.dev | - |
1212| Pierluigi Lenoci | [@pierluigilenoci](https://github.com/pierluigilenoci) | Helm | pierluigi@oauth2-proxy.dev | SAP |
Makefile+9−4
@@ -25,7 +25,8 @@ help: ## Display this help
2525
2626
2727GO ?= go
28−GOLANGCILINT ?= golangci-lint
28+# renovate: datasource=github-tags depName=golangci/golangci-lint
29+GOLANGCILINT_VERSION ?= v2.11.4
2930
3031BINARY := oauth2-proxy
3132VERSION ?= $(shell git describe --always --dirty --tags 2>/dev/null || echo "undefined")
@@ -66,7 +67,7 @@ DOCKER_BUILDX_PUSH := $(DOCKER_BUILDX) --push
6667DOCKER_BUILDX_PUSH_X_PLATFORM := $(DOCKER_BUILDX_PUSH) --platform ${DOCKER_BUILD_PLATFORM}
6768
6869DOCKER_BUILD_PLATFORM_ALPINE ?= linux/amd64,linux/arm64,linux/ppc64le,linux/arm/v6,linux/arm/v7,linux/s390x
69−DOCKER_BUILD_RUNTIME_IMAGE_ALPINE ?= alpine:3.23.3
70+DOCKER_BUILD_RUNTIME_IMAGE_ALPINE ?= alpine:3.24.1
7071DOCKER_BUILDX_ARGS_ALPINE ?= --build-arg RUNTIME_IMAGE=${DOCKER_BUILD_RUNTIME_IMAGE_ALPINE} ${DOCKER_BUILDX_COMMON_ARGS}
7172DOCKER_BUILDX_X_PLATFORM_ALPINE := docker buildx build ${DOCKER_BUILDX_ARGS_ALPINE} --platform ${DOCKER_BUILD_PLATFORM_ALPINE}
7273DOCKER_BUILDX_PUSH_X_PLATFORM_ALPINE := $(DOCKER_BUILDX_X_PLATFORM_ALPINE) --push
@@ -143,7 +144,7 @@ verify-generate: generate ## Verify command to check if alpha config docs are in
143144##@ Miscellaneous
144145
145146.PHONY: test
146−test: lint ## Run all Go tests
147+test: ## Run all Go tests
147148 GO111MODULE=on $(GO) test $(TESTCOVER) -v -race ./...
148149
149150.PHONY: release
@@ -157,7 +158,11 @@ clean: ## Cleanup release and build files
157158
158159.PHONY: lint
159160lint: validate-go-version ## Lint all files using golangci-lint
160− GO111MODULE=on $(GOLANGCILINT) run
161+ GO111MODULE=on $(GO) run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCILINT_VERSION) run --timeout 10m
162+
163+.PHONY: lint-fix
164+lint-fix: validate-go-version ## Lint all files and apply fixes using golangci-lint
165+ GO111MODULE=on $(GO) run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCILINT_VERSION) run --timeout 10m --fix
161166
162167.PHONY: validate-go-version
163168validate-go-version: ## Validate Go environment requirements
README.md+4−4
@@ -12,7 +12,7 @@
1212
1313OAuth2 Proxy is a flexible, open-source tool that can act as either a standalone reverse proxy or a middleware component integrated into existing reverse proxy or load balancer setups. It provides a simple and secure way to protect your web applications with OAuth2 / OIDC authentication. As a reverse proxy, it intercepts requests to your application and redirects users to an OAuth2 provider for authentication. As a middleware, it can be seamlessly integrated into your existing infrastructure to handle authentication for multiple applications.
1414
15−OAuth2 Proxy supports a lot of OAuth2 as well as OIDC providers. Either through a generic OIDC client or a specific implementation for Google, Microsoft Entra ID, GitHub, login.gov and others. Through specialised provider implementations oauth2-proxy can extract more details about the user like preferred usernames and groups. Those details can then be forwarded as HTTP headers to your upstream applications.
15+OAuth2 Proxy supports a lot of OAuth2 as well as OIDC providers. Either through a generic OIDC client or a specific implementation for Google, Microsoft Entra ID, GitHub, login.gov and others. Through specialised provider implementations OAuth2 Proxy can extract more details about the user like preferred usernames and groups. Those details can then be forwarded as HTTP headers to your upstream applications.
1616
1717![Simplified Architecture](docs/static/img/simplified-architecture.svg)
1818
@@ -23,7 +23,7 @@ OAuth2 Proxy's [Installation Docs](https://oauth2-proxy.github.io/oauth2-proxy/i
2323## Releases
2424
2525### Binaries
26−We publish oauth2-proxy as compiled binaries on GitHub for all major architectures as well as more exotic ones like `ppc64le` as well as `s390x`.
26+We publish OAuth2 Proxy as compiled binaries on GitHub for all major architectures as well as more exotic ones like `ppc64le` as well as `s390x`.
2727
2828Check out the [latest release](https://github.com/oauth2-proxy/oauth2-proxy/releases/latest).
2929
@@ -49,9 +49,9 @@ SAP Open Source Program
4949Microsoft Azure credits for open source projects
5050
5151## Getting Involved
52−[![Slack](https://img.shields.io/badge/slack-Gopher_%23oauth2--proxy-red?logo=slack)](https://gophers.slack.com/archives/CM2RSS25N)
52+[![Slack](https://img.shields.io/badge/slack-CNCF_%23oauth2--proxy-blue?logo=slack)](https://cloud-native.slack.com/archives/C098Y5URZ2N)
5353
54−Join the #oauth2-proxy [Slack channel](https://gophers.slack.com/archives/CM2RSS25N) to chat with other users of oauth2-proxy or reach out to the maintainers directly. Use the [public invite link](https://invite.slack.golangbridge.org/) to get an invite for the Gopher Slack space.
54+Join the #oauth2-proxy [Slack channel](https://cloud-native.slack.com/archives/C098Y5URZ2N) to chat with other users of OAuth2 Proxy or reach out to the maintainers directly. Use the [public invite link](https://slack.cncf.io/) to get an invite for the CNCF space.
5555
5656OAuth2 Proxy is a community-driven project. We rely on the contribut️ions of our users to continually improve it. While review times can vary, we appreciate your patience and understanding. As a volunteer-driven project, we strive to keep this project stable and might take longer to merge changes.
5757
contrib/local-environment/Makefile+2−2
@@ -49,8 +49,8 @@ kubernetes-down:
4949
5050.PHONY: traefik-up
5151traefik-up:
52− docker compose -f docker-compose.yaml -f docker-compose-traefik.yaml up -d
52+ docker compose -f docker-compose-traefik.yaml up -d
5353
5454.PHONY: traefik-%
5555traefik-%:
56− docker compose -f docker-compose.yaml -f docker-compose-traefik.yaml $*
56+ docker compose -f docker-compose-traefik.yaml $*
contrib/local-environment/dex-traefik.yamlnew file+25
@@ -0,0 +1,25 @@
1+# This configuration is intended to be used with the Traefik local environment.
2+# Dex uses in-memory storage because this environment is disposable.
3+issuer: http://dex.localtest.me/dex
4+storage:
5+ type: memory
6+web:
7+ http: 0.0.0.0:5556
8+oauth2:
9+ skipApprovalScreen: true
10+expiry:
11+ signingKeys: "4h"
12+ idTokens: "1h"
13+staticClients:
14+ - id: oauth2-proxy
15+ redirectURIs:
16+ - "http://oauth2-proxy.localtest.me/oauth2/callback"
17+ name: "OAuth2 Proxy"
18+ secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
19+enablePasswordDB: true
20+staticPasswords:
21+ - email: "admin@example.com"
22+ # bcrypt hash of the string "password"
23+ hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
24+ username: "admin"
25+ userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
contrib/local-environment/dex.yaml+13−17
@@ -3,11 +3,7 @@
33# This should configure Dex to run on port 5556 and provides a static login
44issuer: http://dex.localtest.me:5556/dex
55storage:
6− type: etcd
7− config:
8− endpoints:
9− - http://etcd:2379
10− namespace: dex/
6+ type: memory
117web:
128 http: 0.0.0.0:5556
139oauth2:
@@ -16,17 +12,17 @@ expiry:
1612 signingKeys: "4h"
1713 idTokens: "1h"
1814staticClients:
19−- id: oauth2-proxy
20− redirectURIs:
21− # These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
22− - 'http://oauth2-proxy.localtest.me:4180/oauth2/callback' # For basic proxy example.
23− - 'http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback' # For nginx and traefik example.
24− name: 'OAuth2 Proxy'
25− secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
15+ - id: oauth2-proxy
16+ redirectURIs:
17+ # These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
18+ - "http://oauth2-proxy.localtest.me:4180/oauth2/callback" # For basic proxy example.
19+ - "http://oauth2-proxy.localtest.me:8080/oauth2/callback" # For nginx example.
20+ name: "OAuth2 Proxy"
21+ secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
2622enablePasswordDB: true
2723staticPasswords:
28−- email: "admin@example.com"
29− # bcrypt hash of the string "password"
30− hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
31− username: "admin"
32− userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
24+ - email: "admin@example.com"
25+ # bcrypt hash of the string "password"
26+ hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
27+ username: "admin"
28+ userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
contrib/local-environment/docker-compose-alpha-config.yaml+2−15
@@ -1,7 +1,7 @@
11# This docker-compose file can be used to bring up an example instance of oauth2-proxy
22# for manual testing and exploration of features.
33# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
4−# etcd for storage for Dex and HTTPBin as an example upstream.
4+# HTTPBin as an example upstream.
55# This file also uses alpha configuration when configuring OAuth2 Proxy.
66#
77# This file is an extension of the main compose file and must be used with it
@@ -14,7 +14,7 @@ version: "3.0"
1414services:
1515 oauth2-proxy:
1616 container_name: oauth2-proxy
17− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.0
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
1818 command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
1919 hostname: oauth2-proxy
2020 volumes:
@@ -43,25 +43,12 @@ services:
4343 dex:
4444 aliases:
4545 - dex.localtest.me
46− etcd: {}
47− depends_on:
48− - etcd
4946 httpbin:
5047 container_name: httpbin
5148 image: kennethreitz/httpbin
5249 ports: []
5350 networks:
5451 httpbin: {}
55− etcd:
56− container_name: etcd
57− image: gcr.io/etcd-development/etcd:v3.6.8
58− entrypoint: /usr/local/bin/etcd
59− command:
60− - --listen-client-urls=http://0.0.0.0:2379
61− - --advertise-client-urls=http://etcd:2379
62− networks:
63− etcd: {}
6452networks:
6553 dex: {}
66− etcd: {}
6754 httpbin: {}
contrib/local-environment/docker-compose-gitea.yaml+2−2
@@ -14,7 +14,7 @@ version: '3.0'
1414services:
1515 oauth2-proxy:
1616 container_name: oauth2-proxy
17− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.0
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
1818 command: --config /oauth2-proxy.cfg
1919 hostname: oauth2-proxy
2020 volumes:
@@ -39,7 +39,7 @@ services:
3939 httpbin: {}
4040
4141 gitea:
42− image: gitea/gitea:1.25.5
42+ image: gitea/gitea:1.27.2
4343 container_name: gitea
4444 environment:
4545 - USER_UID=1000
contrib/local-environment/docker-compose-keycloak.yaml+6−6
@@ -10,11 +10,11 @@
1010#
1111# Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password
1212# Access http://keycloak.localtest.me:9080 with the same credentials to check out the settings
13−version: '3.0'
13+version: "3.0"
1414services:
1515 oauth2-proxy:
1616 container_name: oauth2-proxy
17− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.0
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
1818 command: --config /oauth2-proxy.cfg
1919 hostname: oauth2-proxy
2020 volumes:
@@ -40,12 +40,12 @@ services:
4040
4141 keycloak:
4242 container_name: keycloak
43− image: keycloak/keycloak:25.0
43+ image: keycloak/keycloak:26.7
4444 hostname: keycloak
4545 command:
46− - 'start-dev'
47− - '--http-port=9080'
48− - '--import-realm'
46+ - "start-dev"
47+ - "--http-port=9080"
48+ - "--import-realm"
4949 volumes:
5050 - ./keycloak:/opt/keycloak/data/import
5151 environment:
contrib/local-environment/docker-compose-nginx.yaml+6−30
@@ -1,38 +1,28 @@
11# This docker-compose file can be used to bring up an example instance of oauth2-proxy
22# for manual testing and exploration of features.
33# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
4−# etcd for storage for Dex, nginx as a reverse proxy and other http services for upstreams
4+# nginx as a reverse proxy and other HTTP services for upstreams.
55#
66# This file is an extension of the main compose file and must be used with it
77# docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml <command>
88# Alternatively:
99# make nginx-<command> (eg make nginx-up, make nginx-down)
1010#
11−# Access one of the following URLs to initiate a login flow:
12−# - http://oauth2-proxy.localhost
13−# - http://httpbin.oauth2-proxy.localhost
14−#
15−# The OAuth2 Proxy itself is hosted at http://oauth2-proxy.oauth2-proxy.localhost
16−#
17−# Note, the above URLs should work with Chrome, but you may need to add hosts
18−# entries for other browsers
19−# 127.0.0.1 oauth2-proxy.localhost
20−# 127.0.0.1 httpbin.oauth2-proxy.localhost
21−# 127.0.0.1 oauth2-proxy.oauth2-proxy.localhost
11+# Access http://oauth2-proxy.localtest.me:8080 to initiate a login flow.
2212version: "3.0"
2313services:
2414 oauth2-proxy:
25− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.0
15+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
2616 ports: []
2717 hostname: oauth2-proxy
2818 container_name: oauth2-proxy
2919 command: --config /oauth2-proxy.cfg
20+ restart: unless-stopped
3021 volumes:
3122 - "./oauth2-proxy-nginx.cfg:/oauth2-proxy.cfg"
3223 networks:
3324 oauth2-proxy: {}
3425 dex: {}
35− etcd: {}
3626 httpbin: {}
3727 depends_on:
3828 - dex
@@ -41,17 +31,16 @@ services:
4131 depends_on:
4232 - oauth2-proxy
4333 container_name: nginx
44− image: nginx:1.29
34+ image: nginx:1.31
4535 restart: unless-stopped
4636 ports:
47− - 80:80/tcp
37+ - 8080:8080/tcp
4838 hostname: nginx
4939 volumes:
5040 - "./nginx.conf:/etc/nginx/conf.d/default.conf"
5141 networks:
5242 oauth2-proxy: {}
5343 dex: {}
54− etcd: {}
5544 httpbin: {}
5645 dex:
5746 container_name: dex
@@ -67,26 +56,13 @@ services:
6756 dex:
6857 aliases:
6958 - dex.localtest.me
70− etcd: {}
71− depends_on:
72− - etcd
7359 httpbin:
7460 container_name: httpbin
7561 image: kennethreitz/httpbin
7662 ports: []
7763 networks:
7864 httpbin: {}
79− etcd:
80− container_name: etcd
81− image: gcr.io/etcd-development/etcd:v3.6.8
82− entrypoint: /usr/local/bin/etcd
83− command:
84− - --listen-client-urls=http://0.0.0.0:2379
85− - --advertise-client-urls=http://etcd:2379
86− networks:
87− etcd: {}
8865networks:
8966 dex: {}
90− etcd: {}
9167 httpbin: {}
9268 oauth2-proxy: {}
contrib/local-environment/docker-compose-traefik.yaml+42−18
@@ -1,7 +1,7 @@
11# This docker-compose file can be used to bring up an example instance of oauth2-proxy
22# for manual testing and exploration of features.
3−# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
4−# HTTPBin as an example upstream.
3+# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider
4+# and HTTPBin as an example upstream.
55#
66# This can either be created using docker-compose
77# docker-compose -f docker-compose-traefik.yaml <command>
@@ -9,32 +9,51 @@
99# make traefik-<command> (eg. make traefik-up, make traefik-down)
1010#
1111# Access one of the following URLs to initiate a login flow:
12−# - http://oauth2-proxy.localhost
13−# - http://httpbin.oauth2-proxy.localhost
12+# - http://oauth2-proxy.localtest.me
13+# - http://httpbin.localtest.me
14+# - http://dex.localtest.me/dex
1415#
15−# The OAuth2 Proxy itself is hosted at http://oauth2-proxy.oauth2-proxy.localhost
16−#
17−# Note, the above URLs should work with Chrome, but you may need to add hosts
18−# entries for other browsers
19−# 127.0.0.1 oauth2-proxy.localhost
20−# 127.0.0.1 httpbin.oauth2-proxy.localhost
21−# 127.0.0.1 oauth2-proxy.oauth2-proxy.localhost
22−version: '3.0'
16+# The OAuth2 Proxy itself is hosted at http://oauth2-proxy.localtest.me
2317services:
2418
2519 oauth2-proxy:
26− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.0
20+ container_name: oauth2-proxy
21+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
22+ command: --config /oauth2-proxy.cfg
2723 ports: []
2824 hostname: oauth2-proxy
2925 volumes:
3026 - "./oauth2-proxy-traefik.cfg:/oauth2-proxy.cfg"
27+ restart: unless-stopped
28+ networks:
29+ oauth2-proxy: {}
30+ dex: {}
31+ depends_on:
32+ - dex
33+ - httpbin
34+
35+ dex:
36+ container_name: dex
37+ image: ghcr.io/dexidp/dex:v2.45.1
38+ command: dex serve /dex.yaml
39+ hostname: dex
40+ volumes:
41+ - "./dex-traefik.yaml:/dex.yaml"
42+ restart: unless-stopped
3143 networks:
32− oauth2-proxy:
44+ dex: {}
45+
46+ httpbin:
47+ container_name: httpbin
48+ image: kennethreitz/httpbin
49+ ports: []
50+ networks:
51+ httpbin: {}
3352
3453 # Reverse proxy
3554 gateway:
3655 container_name: traefik
37− image: traefik:v2.11.40
56+ image: traefik:v3.7.10
3857 volumes:
3958 - "./traefik:/etc/traefik"
4059 ports:
@@ -43,8 +62,13 @@ services:
4362 depends_on:
4463 - oauth2-proxy
4564 networks:
46− oauth2-proxy:
47− httpbin:
65+ oauth2-proxy: {}
66+ httpbin: {}
67+ dex:
68+ aliases:
69+ - dex.localtest.me
4870
4971networks:
50− oauth2-proxy:
72+ dex: {}
73+ httpbin: {}
74+ oauth2-proxy: {}
contrib/local-environment/docker-compose.yaml+2−15
@@ -1,7 +1,7 @@
11# This docker-compose file can be used to bring up an example instance of oauth2-proxy
22# for manual testing and exploration of features.
33# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
4−# etcd for storage for Dex and HTTPBin as an example upstream.
4+# HTTPBin as an example upstream.
55#
66# This can either be created using docker-compose
77# docker-compose -f docker-compose.yaml <command>
@@ -13,7 +13,7 @@ version: "3.0"
1313services:
1414 oauth2-proxy:
1515 container_name: oauth2-proxy
16− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.0
16+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
1717 command: --config /oauth2-proxy.cfg
1818 hostname: oauth2-proxy
1919 volumes:
@@ -41,25 +41,12 @@ services:
4141 dex:
4242 aliases:
4343 - dex.localtest.me
44− etcd: {}
45− depends_on:
46− - etcd
4744 httpbin:
4845 container_name: httpbin
4946 image: kennethreitz/httpbin
5047 ports: []
5148 networks:
5249 httpbin: {}
53− etcd:
54− container_name: etcd
55− image: gcr.io/etcd-development/etcd:v3.6.8
56− entrypoint: /usr/local/bin/etcd
57− command:
58− - --listen-client-urls=http://0.0.0.0:2379
59− - --advertise-client-urls=http://etcd:2379
60− networks:
61− etcd: {}
6250networks:
6351 dex: {}
64− etcd: {}
6552 httpbin: {}
contrib/local-environment/kubernetes/Chart.lock+3−3
@@ -1,7 +1,7 @@
11dependencies:
22- name: dex
33 repository: https://charts.dexidp.io
4− version: 0.24.0
4+ version: 0.24.1
55- name: oauth2-proxy
66 repository: https://oauth2-proxy.github.io/manifests
77 version: 7.18.0
@@ -11,5 +11,5 @@ dependencies:
1111- name: hello-world
1212 repository: https://conservis.github.io/helm-charts
1313 version: 1.1.0
14−digest: sha256:fee913531bfb67e5555e995d8fb040c330e6eb4b5a8c777ceb9841135ea9bb84
15−generated: "2025-10-28T06:46:35.38300324Z"
14+digest: sha256:3fff8bf99cebcd20a73c984423d5f1c2dfa1e5a394f82a5dc60d9aaffe5361b7
15+generated: "2026-08-18T07:33:03.791274914Z"
contrib/local-environment/kubernetes/Chart.yaml+1−1
@@ -3,7 +3,7 @@ description: K8S example based on https://kind.sigs.k8s.io
33name: kubernetes
44dependencies:
55 - name: dex
6− version: 0.24.0
6+ version: 0.24.1
77 repository: https://charts.dexidp.io
88 - name: oauth2-proxy
99 version: &chartVersion 7.18.0
contrib/local-environment/nginx.conf+29−76
@@ -1,91 +1,44 @@
1−# Reverse proxy to oauth2-proxy
2−server {
3− listen 80;
4− server_name oauth2-proxy.oauth2-proxy.localhost;
5−
6− location / {
7− proxy_set_header Host $host;
8− proxy_set_header X-Real-IP $remote_addr;
9−
10− proxy_pass http://oauth2-proxy:4180/;
11− }
12−}
13−
141# Reverse proxy to httpbin
152server {
16− listen 80;
17− server_name httpbin.oauth2-proxy.localhost;
3+ listen 8080;
4+ server_name oauth2-proxy.localtest.me;
185
19− auth_request /internal-auth/oauth2/auth;
20−
21− # On 401, redirect to the sign_in page via a named location
22− # This ensures a proper 302 redirect that browsers will follow
23− error_page 401 = @oauth2_signin;
24−
25− location / {
26− proxy_pass http://httpbin/;
27− }
28−
29− # Named location for OAuth2 sign-in redirect
30− # Returns a proper 302 that works with --skip-provider-button
31− location @oauth2_signin {
32− return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
6+ location /oauth2/ {
7+ proxy_pass http://oauth2-proxy:4180;
8+ proxy_set_header Host $host;
9+ proxy_set_header X-Real-IP $remote_addr;
10+ proxy_set_header X-Forwarded-Uri $request_uri;
11+ proxy_set_header X-Auth-Request-Redirect $request_uri;
3312 }
3413
35− # auth_request must be a URI so this allows an internal path to then proxy to
36− # the real auth_request path.
37− # The trailing /'s are required so that nginx strips the prefix before proxying.
38− location /internal-auth/ {
39− internal; # Ensure external users can't access this path
40−
41− # Make sure the OAuth2 Proxy knows where the original request came from.
42− proxy_set_header Host $host;
43− proxy_set_header X-Real-IP $remote_addr;
44− proxy_set_header X-Forwarded-Uri $request_uri;
45−
46− proxy_pass http://oauth2-proxy:4180/;
14+ location = /oauth2/auth {
15+ proxy_pass http://oauth2-proxy:4180;
16+ proxy_set_header Host $host;
17+ proxy_set_header X-Real-IP $remote_addr;
18+ proxy_set_header X-Forwarded-Uri $request_uri;
19+ # nginx auth_request includes headers but not body
20+ proxy_set_header Content-Length "";
21+ proxy_pass_request_body off;
4722 }
48−}
49−
50−# Statically serve the nginx welcome
51−server {
52− listen 80;
53− server_name oauth2-proxy.localhost;
5423
5524 location / {
56− auth_request /internal-auth/oauth2/auth;
57−
58− # On 401, redirect to the sign_in page via a named location
59− # This ensures a proper 302 redirect that browsers will follow
25+ auth_request /oauth2/auth;
6026 error_page 401 = @oauth2_signin;
6127
62− root /usr/share/nginx/html;
63− index index.html index.htm;
64− }
65−
66− # Named location for OAuth2 sign-in redirect
67− # Returns a proper 302 that works with --skip-provider-button
68− location @oauth2_signin {
69− return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
70− }
28+ # pass information via X-User and X-Email headers to backend,
29+ # requires running with --set-xauthrequest flag
30+ auth_request_set $user $upstream_http_x_auth_request_user;
31+ auth_request_set $email $upstream_http_x_auth_request_email;
32+ proxy_set_header X-User $user;
33+ proxy_set_header X-Email $email;
7134
72− # redirect server error pages to the static page /50x.html
73− error_page 500 502 503 504 /50x.html;
74− location = /50x.html {
75− root /usr/share/nginx/html;
35+ proxy_pass http://httpbin/;
36+ # or "root /path/to/site;" or "fastcgi_pass ..." etc
7637 }
7738
78− # auth_request must be a URI so this allows an internal path to then proxy to
79− # the real auth_request path.
80− # The trailing /'s are required so that nginx strips the prefix before proxying.
81− location /internal-auth/ {
82− internal; # Ensure external users can't access this path
83−
84− # Make sure the OAuth2 Proxy knows where the original request came from.
85− proxy_set_header Host $host;
86− proxy_set_header X-Real-IP $remote_addr;
87− proxy_set_header X-Forwarded-Uri $request_uri;
88−
89− proxy_pass http://oauth2-proxy:4180/;
39+ # Named location for handling OAuth2 sign-in redirects
40+ # This ensures the browser receives a proper 302 redirect that it will follow
41+ location @oauth2_signin {
42+ return 302 /oauth2/sign_in?rd=$scheme://$http_host$request_uri;
9043 }
9144}
contrib/local-environment/oauth2-proxy-nginx.cfg+11−6
@@ -1,14 +1,19 @@
11http_address="0.0.0.0:4180"
22cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3−provider="oidc"
43email_domains="example.com"
5−oidc_issuer_url="http://dex.localtest.me:5556/dex"
4+cookie_secure="false"
5+upstreams="static://200"
6+cookie_domains=[".localtest.me"] # Required so cookie can be read on all subdomains.
7+whitelist_domains=[".localtest.me"] # Required to allow redirection back to original requested target.
8+
9+# dex provider
610client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
711client_id="oauth2-proxy"
8−cookie_secure="false"
12+redirect_url="http://oauth2-proxy.localtest.me:8080/oauth2/callback"
13+
14+oidc_issuer_url="http://dex.localtest.me:5556/dex"
15+provider="oidc"
16+provider_display_name="Dex"
917
10−redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback"
11−cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains.
12−whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target.
1318# Enables the use of `X-Forwarded-*` headers to determine request correctly
1419reverse_proxy="true"
contrib/local-environment/oauth2-proxy-traefik.cfg+4−4
@@ -2,14 +2,14 @@ http_address="0.0.0.0:4180"
22cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
33provider="oidc"
44email_domains=["example.com"]
5−oidc_issuer_url="http://dex.localhost:5556/dex"
5+oidc_issuer_url="http://dex.localtest.me/dex"
66client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
77client_id="oauth2-proxy"
88cookie_secure="false"
99
10−redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback"
11−cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains.
12−whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target.
10+redirect_url="http://oauth2-proxy.localtest.me/oauth2/callback"
11+cookie_domains=".localtest.me" # Required so cookie can be read on all subdomains.
12+whitelist_domains=".localtest.me" # Required to allow redirection back to original requested target.
1313
1414# Mandatory option when using oauth2-proxy with traefik
1515reverse_proxy="true"
contrib/local-environment/traefik/dynamic.yaml+11−4
@@ -1,22 +1,25 @@
11http:
22 routers:
33 oauth2-proxy-route:
4− rule: "Host(`oauth2-proxy.oauth2-proxy.localhost`)"
4+ rule: "Host(`oauth2-proxy.localtest.me`)"
55 middlewares:
66 - auth-headers
77 service: oauth-backend
8+ dex-route:
9+ rule: "Host(`dex.localtest.me`)"
10+ service: dex-service
811 httpbin-route:
9− rule: "Host(`httpbin.oauth2-proxy.localhost`)"
12+ rule: "Host(`httpbin.localtest.me`)"
1013 service: httpbin-service
1114 middlewares:
1215 - oauth-auth-redirect # redirects all unauthenticated to oauth2 signin
1316 httpbin-route-2:
14− rule: "Host(`httpbin.oauth2-proxy.localhost`) && PathPrefix(`/no-auto-redirect`)"
17+ rule: "Host(`httpbin.localtest.me`) && PathPrefix(`/no-auto-redirect`)"
1518 service: httpbin-service
1619 middlewares:
1720 - oauth-auth-wo-redirect # unauthenticated session will return a 401
1821 services-oauth2-route:
19− rule: "Host(`httpbin.oauth2-proxy.localhost`) && PathPrefix(`/oauth2/`)"
22+ rule: "Host(`httpbin.localtest.me`) && PathPrefix(`/oauth2/`)"
2023 middlewares:
2124 - auth-headers
2225 service: oauth-backend
@@ -30,6 +33,10 @@ http:
3033 loadBalancer:
3134 servers:
3235 - url: http://oauth2-proxy:4180
36+ dex-service:
37+ loadBalancer:
38+ servers:
39+ - url: http://dex:5556
3340
3441 middlewares:
3542 auth-headers:
contrib/local-environment/traefik/traefik.yaml+2
@@ -5,3 +5,5 @@ log:
55providers:
66 file:
77 filename: /etc/traefik/dynamic.yaml
8+ # Routes are static for this example; disabling watch avoids host inotify limits.
9+ watch: false
docs/docs/configuration/alpha_config.md+12−1
@@ -167,6 +167,17 @@ injectResponseHeaders:
167167* `claimSource` - `claim` (session claims either from id token or from profile URL)
168168* `secretSource` - `value` (base64), `fromFile` (file path)
169169
170+#### Claim value serialization
171+
172+`claimSource` converts the selected claim into one or more string header values. Scalars
173+produce one value, while arrays produce one value per element. Values that cannot be
174+converted directly to strings, such as objects, are JSON-encoded individually.
175+
176+Each non-empty value is added separately under the configured header name. An array of
177+objects therefore produces multiple header values containing one JSON object each, not a
178+single JSON array. HTTP libraries and intermediaries may expose repeated values as a
179+comma-joined string, so consumers should retrieve and parse each header value separately.
180+
170181**Request option:** `preserveRequestValue: true` retains existing header values
171182
172183**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
@@ -678,7 +689,7 @@ Requests will be proxied to this upstream if the path matches the request path.
678689| Field | Type | Description |
679690| ----- | ---- | ----------- |
680691| `id` | _string_ | ID should be a unique identifier for the upstream.<br/>This value is required for all upstreams. |
681−| `path` | _string_ | Path is used to map requests to the upstream server.<br/>The closest match will take precedence and all Paths must be unique.<br/>Path can also take a pattern when used with RewriteTarget.<br/>Path segments can be captured and matched using regular experessions.<br/>Eg:<br/>- `^/foo$`: Match only the explicit path `/foo`<br/>- `^/bar/$`: Match any path prefixed with `/bar/`<br/>- `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget |
692+| `path` | _string_ | Path is used to map requests to the upstream server.<br/>The closest match will take precedence and all Paths must be unique.<br/>Without RewriteTarget, Path follows gorilla/mux route syntax.<br/>Eg:<br/>- `/foo`: Match only the explicit path `/foo`<br/>- `/bar/`: Match any path prefixed with `/bar/`<br/>- `/baz/{path:.*}`: Match any path prefixed with `/baz/`<br/>When RewriteTarget is set, Path is treated as a regular expression.<br/>Eg: `^/baz/(.*)$` matches any path prefixed with `/baz` and captures the<br/>remaining path for use with RewriteTarget. |
682693| `rewriteTarget` | _string_ | RewriteTarget allows users to rewrite the request path before it is sent to<br/>the upstream server (for an HTTP/HTTPS upstream) or mapped to the filesystem<br/>(for a `file:` upstream).<br/>Use the Path to capture segments for reuse within the rewrite target.<br/>Eg: With a Path of `^/baz/(.*)`, a RewriteTarget of `/foo/$1` would rewrite<br/>the request `/baz/abc/123` to `/foo/abc/123` before proxying to the<br/>upstream server. Or if the upstream were `file:///app`, a request for<br/>`/baz/info.html` would return the contents of the file `/app/foo/info.html`. |
683694| `uri` | _string_ | The URI of the upstream server. This may be an HTTP(S) server of a File<br/>based URL. It may include a path, in which case all requests will be served<br/>under that path.<br/>Eg:<br/>- http://localhost:8080<br/>- https://service.localhost<br/>- https://service.localhost/path<br/>- file://host/path<br/>If the URI's path is "/base" and the incoming request was for "/dir",<br/>the upstream request will be for "/base/dir". |
684695| `insecureSkipTLSVerify` | _bool_ | InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.<br/>This option is insecure and will allow potential Man-In-The-Middle attacks<br/>between OAuth2 Proxy and the upstream server.<br/>Defaults to false. |
docs/docs/configuration/alpha_config.md.tmpl+11
@@ -167,6 +167,17 @@ injectResponseHeaders:
167167* `claimSource` - `claim` (session claims either from id token or from profile URL)
168168* `secretSource` - `value` (base64), `fromFile` (file path)
169169
170+#### Claim value serialization
171+
172+`claimSource` converts the selected claim into one or more string header values. Scalars
173+produce one value, while arrays produce one value per element. Values that cannot be
174+converted directly to strings, such as objects, are JSON-encoded individually.
175+
176+Each non-empty value is added separately under the configured header name. An array of
177+objects therefore produces multiple header values containing one JSON object each, not a
178+single JSON array. HTTP libraries and intermediaries may expose repeated values as a
179+comma-joined string, so consumers should retrieve and parse each header value separately.
180+
170181**Request option:** `preserveRequestValue: true` retains existing header values
171182
172183**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
docs/docs/configuration/integrations/nginx.md+27−11
@@ -60,23 +60,23 @@ server {
6060 auth_request_set $auth_cookie $upstream_http_set_cookie;
6161 add_header Set-Cookie $auth_cookie;
6262
63− # When using the --set-authorization-header flag, some provider's cookies can exceed the 4kb
64− # limit and so the OAuth2 Proxy splits these into multiple parts.
63+ # Large OIDC tokens can make the session cookie exceed the 4KB limit,
64+ # causing OAuth2 Proxy to split the cookie into multiple parts.
6565 # Nginx normally only copies the first `Set-Cookie` header from the auth_request to the response,
66− # so if your cookies are larger than 4kb, you will need to extract additional cookies manually.
67− auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;
66+ # so extract the second part when using the default cookie name, `_oauth2_proxy`.
67+ auth_request_set $auth_cookie_upstream_1 $upstream_cookie__oauth2_proxy_1;
6868
6969 # Extract the Cookie attributes from the first Set-Cookie header and append them
7070 # to the second part ($upstream_cookie_* variables only contain the raw cookie content)
7171 if ($auth_cookie ~* "(; .*)") {
72− set $auth_cookie_name_0 $auth_cookie;
73− set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
72+ set $auth_cookie_0 $auth_cookie;
73+ set $auth_cookie_1 "_oauth2_proxy_1=$auth_cookie_upstream_1$1";
7474 }
7575
7676 # Send both Set-Cookie headers now if there was a second part
77− if ($auth_cookie_name_upstream_1) {
78− add_header Set-Cookie $auth_cookie_name_0;
79− add_header Set-Cookie $auth_cookie_name_1;
77+ if ($auth_cookie_upstream_1) {
78+ add_header Set-Cookie $auth_cookie_0;
79+ add_header Set-Cookie $auth_cookie_1;
8080 }
8181
8282 proxy_pass http://backend/;
@@ -91,6 +91,24 @@ server {
9191}
9292```
9393
94+### Multi-part session cookies
95+
96+The example above is ready to use with the default `--cookie-name=_oauth2_proxy` setting and handles a session split into two cookies. If you configure a custom cookie name, update both references to the second cookie part:
97+
98+| Cookie configuration | Nginx upstream cookie variable | `Set-Cookie` name |
99+| --- | --- | --- |
100+| Default (`_oauth2_proxy`) | `$upstream_cookie__oauth2_proxy_1` | `_oauth2_proxy_1=` |
101+| `--cookie-name=my_cookie` | `$upstream_cookie_my_cookie_1` | `my_cookie_1=` |
102+
103+For example, with `--cookie-name=my_cookie`, change the relevant lines to:
104+
105+```nginx
106+auth_request_set $auth_cookie_upstream_1 $upstream_cookie_my_cookie_1;
107+set $auth_cookie_1 "my_cookie_1=$auth_cookie_upstream_1$1";
108+```
109+
110+If a session is split into more than two cookies, repeat the extraction for each additional numeric suffix. For consistently large sessions, prefer `--session-store-type=redis` so only a small session ticket is stored in the browser cookie.
111+
94112### Understanding the `error_page` redirect pattern
95113
96114The `auth_request` directive expects the authentication endpoint (`/oauth2/auth`) to return:
@@ -167,8 +185,6 @@ It is recommended to use `--session-store-type=redis` when expecting large sessi
167185For a complete example of integrating oauth2-proxy with Kubernetes Dashboard on AKS using Azure Entra ID, including RBAC configuration and troubleshooting, see the [Kubernetes Dashboard on AKS](../providers/ms_entra_id.md#kubernetes-dashboard-on-aks) section in the Microsoft Entra ID provider documentation.
168186:::
169187
170−You have to substitute *name* with the actual cookie name you configured via --cookie-name parameter. If you don't set a custom cookie name the variable should be "$upstream_cookie__oauth2_proxy_1" instead of "$upstream_cookie_name_1" and the new cookie-name should be "_oauth2_proxy_1=" instead of "name_1=".
171−
172188:::note
173189If you set up your OAuth2 provider to rotate your client secret, you can use the `client-secret-file` option to reload the secret when it is updated.
174190:::
docs/docs/configuration/overview.md+44−39
@@ -72,43 +72,6 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
7272| `--config-test` | test configuration and exit (for CI/CD validation) |
7373| `--version` | print version string |
7474
75−## Configuration Validation
76−
77−The `--config-test` flag validates your configuration file without starting the proxy server. This is useful for:
78−- **CI/CD pipelines**: Pre-deployment validation
79−- **Configuration management**: Testing before applying changes
80−- **Debugging**: Verifying syntax and required fields
81−
82−### Usage
83−
84−```bash
85−# Test legacy config
86−oauth2-proxy --config /etc/oauth2-proxy.cfg --config-test
87−
88−# Test alpha config
89−oauth2-proxy --config /etc/core.cfg --alpha-config /etc/alpha.yaml --config-test
90−
91−# CI/CD pre-deployment check
92−# Returns with exit code 1 if any validation errors occur
93−oauth2-proxy --config new-config.cfg --config-test
94−```
95−
96−### Exit Codes
97−
98−- **0**: Configuration is valid ✅
99−- **1**: Configuration is invalid (errors printed to stderr) ❌
100−
101−### Validation Coverage
102−
103−The `--config-test` flag performs the **same comprehensive validation** as normal startup, including:
104−- Required fields (client ID, client secret, cookie secret, etc.)
105−- Syntax validation (TOML/YAML parsing)
106−- Provider configuration
107−- Upstream server definitions
108−- Session store connectivity (e.g., Redis network checks if configured)
109−
110−**Note**: Cannot be combined with `--convert-config-to-alpha`.
111−
11275### General Provider Options
11376
11477Provider specific options can be found on their respective subpages.
@@ -230,6 +193,10 @@ Provider specific options can be found on their respective subpages.
230193
231194### Proxy Options
232195
196+:::warning
197+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.
198+:::
199+
233200| Flag / Config Field | Type | Description | Default |
234201| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
235202| 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` |
@@ -248,10 +215,11 @@ Provider specific options can be found on their respective subpages.
248215| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
249216| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
250217| 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 |
218+| 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"` |
251219| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
252220| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
253−| 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) | |
254−| 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 | |
221+| 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. | |
222+| 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. | |
255223| 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 |
256224| flag: `--skip-provider-button`<br/>toml: `skip_provider_button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
257225| flag: `--ssl-insecure-skip-verify`<br/>toml: `ssl_insecure_skip_verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
@@ -305,6 +273,43 @@ Provider specific options can be found on their respective subpages.
305273| flag: `--upstream-timeout`<br/>toml: `upstream_timeout` | duration | maximum amount of time the server will wait for a response from the upstream | 30s |
306274| flag: `--upstream`<br/>toml: `upstreams` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | |
307275
276+## Configuration Validation
277+
278+The `--config-test` flag validates your configuration file without starting the proxy server. This is useful for:
279+- **CI/CD pipelines**: Pre-deployment validation
280+- **Configuration management**: Testing before applying changes
281+- **Debugging**: Verifying syntax and required fields
282+
283+### Usage
284+
285+```bash
286+# Test legacy config
287+oauth2-proxy --config /etc/oauth2-proxy.cfg --config-test
288+
289+# Test alpha config
290+oauth2-proxy --config /etc/core.cfg --alpha-config /etc/alpha.yaml --config-test
291+
292+# CI/CD pre-deployment check
293+# Returns with exit code 1 if any validation errors occur
294+oauth2-proxy --config new-config.cfg --config-test
295+```
296+
297+### Exit Codes
298+
299+- **0**: Configuration is valid ✅
300+- **1**: Configuration is invalid (errors printed to stderr) ❌
301+
302+### Validation Coverage
303+
304+The `--config-test` flag performs the **same comprehensive validation** as normal startup, including:
305+- Required fields (client ID, client secret, cookie secret, etc.)
306+- Syntax validation (TOML/YAML parsing)
307+- Provider configuration
308+- Upstream server definitions
309+- Session store connectivity (e.g., Redis network checks if configured)
310+
311+**Note**: Cannot be combined with `--convert-config-to-alpha`.
312+
308313## Upstreams Configuration
309314
310315`oauth2-proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers, unix socket or serve static files from the file system.
docs/docs/configuration/providers/atlassian.mdnew file+26
@@ -0,0 +1,26 @@
1+---
2+id: atlassian
3+title: Atlassian Cloud
4+---
5+
6+1. Create an OAuth 2.0 integration in the [Atlassian developer console](https://developer.atlassian.com/console/myapps/).
7+2. Under **Permissions**, add the User identity API with the `read:me` scope.
8+3. Under **Authorization**, set the callback URL to `https://<proxy_host>/oauth2/callback`.
9+4. Copy the client ID and secret from **Settings**.
10+5. Run the proxy with:
11+
12+```shell
13+ --provider=atlassian
14+ --client-id=<client ID>
15+ --client-secret=<secret>
16+ --redirect-url=https://<proxy_host>/oauth2/callback
17+ --cookie-secret=<cookie secret>
18+```
19+
20+A new app works only for the account that created it. Enable sharing under **Distribution** to let other users sign in.
21+Atlassian then admits only accounts with access to the Atlassian site the app is installed on; others are refused at
22+the consent screen.
23+
24+The proxy reads the user's email from `https://api.atlassian.com/me`. Restricting access further is supported by
25+[email](index.md#email-authentication). With `--cookie-refresh` set, the proxy re-validates the token against `/me` on
26+that interval.
docs/docs/configuration/providers/dtt_sso.mdnew file+45
@@ -0,0 +1,45 @@
1+---
2+id: dtt_sso
3+title: DTT SSO
4+---
5+
6+Signs users in with the DTT SSO (auth-server) using the authorization code flow and its HS256 id_token.
7+
8+1. Register a client in the auth-server config, one per protected service, with the proxy's callback as its
9+ only redirect URI:
10+
11+ ```yaml
12+ clients:
13+ redash-gate:
14+ secret: <random secret>
15+ redirect_uri: https://redash.example.com/oauth2/callback
16+ display_name: Redash
17+ ```
18+
19+2. Run the proxy with:
20+
21+ ```shell
22+ --provider=dtt-sso
23+ --client-id=redash-gate
24+ --client-secret-file=/run/secrets/client_secret
25+ --redirect-url=https://redash.example.com/oauth2/callback
26+ --cookie-secret-file=/run/secrets/cookie_secret
27+ --cookie-name=_dtt_sso_redash
28+ --cookie-refresh=5m
29+ ```
30+
31+ Give each instance its own `--cookie-name`: another oauth2-proxy setting its cookie on a parent domain
32+ (the Atlassian proxy uses `_oauth2_proxy` on `.thebestagent.pro`) would otherwise shadow this one.
33+
34+The endpoints default to `https://auth.thebestagent.pro`. For another SSO host set `--login-url`, `--redeem-url`
35+and `--validate-url` to its `/oauth/authorize`, `/oauth/token` and `/oauth/check_token`; the id_token issuer must
36+equal the login URL's origin (the SSO's `site_host`).
37+
38+The scope must stay `openid`, the only scope the SSO accepts.
39+
40+The session carries the SSO `email`, the user UUID as the user, and the SSO `role` as the only group, so
41+`--allowed-group=dtt_admin` restricts access by role. `--authenticated-emails-file` and `--email-domain` restrict
42+by email as usual.
43+
44+With `--cookie-refresh` set, the proxy refreshes the tokens on that interval and re-reads the email and role; a user
45+deleted or signed out in the SSO loses access at the next refresh.
docs/docs/configuration/providers/index.md+2
@@ -9,10 +9,12 @@ with Redirect URI(s) for the domain you intend to run `oauth2-proxy` on.
99Valid providers are :
1010
1111- [ADFS](adfs.md)
12+- [Atlassian Cloud](atlassian.md)
1213- [Bitbucket](bitbucket.md)
1314- [Cidaas](cidaas.md)
1415- [CiscoDuo](cisco_duo.md)
1516- [DigitalOcean](digitalocean.md)
17+- [DTT SSO](dtt_sso.md)
1618- [Facebook](facebook.md)
1719- [Gitea](gitea.md)
1820- [GitHub](github.md)
docs/docs/configuration/systemd_socket.md+8
@@ -40,4 +40,12 @@ the listener it created onto the process, starting with file descriptor 3.
4040 --client-secret=...
4141```
4242
43+## Trusted IPs
44+
45+When listening on a Unix socket, Go sets `http.Request.RemoteAddr` to `"@"` instead of the usual `"host:port"` format. This means there is no client IP available from the connection itself.
46+
47+As a result, `--trusted-ip` entries cannot match against the direct connection address for Unix socket listeners. Requests arriving over a Unix socket will never be considered "trusted" based on their `RemoteAddr`. IP-based trust decisions will still work if a trusted reverse proxy sets `X-Forwarded-For` or `X-Real-IP` headers and `--reverse-proxy=true` is configured.
48+
49+## TLS
50+
4351Currently TLS is not supported (but it's doable).
docs/docs/installation.md+1−1
@@ -5,7 +5,7 @@ title: Installation
55
661. Choose how to deploy:
77
8− a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.0`)
8+ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.4`)
99
1010 b. Using Go to install the latest release
1111 ```bash
docs/docusaurus.config.js+1−1
@@ -105,7 +105,7 @@ const config = {
105105 dropdownActiveClassDisabled: true,
106106 },
107107 {
108− href: 'https://gophers.slack.com/messages/CM2RSS25N',
108+ href: 'https://cloud-native.slack.com/archives/C098Y5URZ2N',
109109 label: 'Slack',
110110 position: 'right',
111111 },
docs/package.json+3
@@ -42,5 +42,8 @@
4242 },
4343 "engines": {
4444 "node": ">=18.0"
45+ },
46+ "overrides" : {
47+ "webpackbar" : "^7.0.0"
4548 }
4649}
docs/sidebars.js+2
@@ -46,11 +46,13 @@ const sidebars = {
4646 },
4747 items: [
4848 "configuration/providers/adfs",
49+ "configuration/providers/atlassian",
4950 "configuration/providers/azure",
5051 "configuration/providers/bitbucket",
5152 "configuration/providers/cidaas",
5253 "configuration/providers/cisco_duo",
5354 "configuration/providers/digitalocean",
55+ "configuration/providers/dtt_sso",
5456 "configuration/providers/facebook",
5557 "configuration/providers/gitea",
5658 "configuration/providers/github",
docs/versioned_docs/version-7.15.x/configuration/overview.md+42−37
@@ -72,43 +72,6 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
7272| `--config-test` | test configuration and exit (for CI/CD validation) |
7373| `--version` | print version string |
7474
75−## Configuration Validation
76−
77−The `--config-test` flag validates your configuration file without starting the proxy server. This is useful for:
78−- **CI/CD pipelines**: Pre-deployment validation
79−- **Configuration management**: Testing before applying changes
80−- **Debugging**: Verifying syntax and required fields
81−
82−### Usage
83−
84−```bash
85−# Test legacy config
86−oauth2-proxy --config /etc/oauth2-proxy.cfg --config-test
87−
88−# Test alpha config
89−oauth2-proxy --config /etc/core.cfg --alpha-config /etc/alpha.yaml --config-test
90−
91−# CI/CD pre-deployment check
92−# Returns with exit code 1 if any validation errors occur
93−oauth2-proxy --config new-config.cfg --config-test
94−```
95−
96−### Exit Codes
97−
98−- **0**: Configuration is valid ✅
99−- **1**: Configuration is invalid (errors printed to stderr) ❌
100−
101−### Validation Coverage
102−
103−The `--config-test` flag performs the **same comprehensive validation** as normal startup, including:
104−- Required fields (client ID, client secret, cookie secret, etc.)
105−- Syntax validation (TOML/YAML parsing)
106−- Provider configuration
107−- Upstream server definitions
108−- Session store connectivity (e.g., Redis network checks if configured)
109−
110−**Note**: Cannot be combined with `--convert-config-to-alpha`.
111−
11275### General Provider Options
11376
11477Provider specific options can be found on their respective subpages.
@@ -230,6 +193,10 @@ Provider specific options can be found on their respective subpages.
230193
231194### Proxy Options
232195
196+:::warning
197+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.
198+:::
199+
233200| Flag / Config Field | Type | Description | Default |
234201| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
235202| 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` |
@@ -248,6 +215,7 @@ Provider specific options can be found on their respective subpages.
248215| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
249216| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
250217| 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 |
218+| 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"` |
251219| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
252220| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
253221| 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) | |
@@ -305,6 +273,43 @@ Provider specific options can be found on their respective subpages.
305273| flag: `--upstream-timeout`<br/>toml: `upstream_timeout` | duration | maximum amount of time the server will wait for a response from the upstream | 30s |
306274| flag: `--upstream`<br/>toml: `upstreams` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | |
307275
276+## Configuration Validation
277+
278+The `--config-test` flag validates your configuration file without starting the proxy server. This is useful for:
279+- **CI/CD pipelines**: Pre-deployment validation
280+- **Configuration management**: Testing before applying changes
281+- **Debugging**: Verifying syntax and required fields
282+
283+### Usage
284+
285+```bash
286+# Test legacy config
287+oauth2-proxy --config /etc/oauth2-proxy.cfg --config-test
288+
289+# Test alpha config
290+oauth2-proxy --config /etc/core.cfg --alpha-config /etc/alpha.yaml --config-test
291+
292+# CI/CD pre-deployment check
293+# Returns with exit code 1 if any validation errors occur
294+oauth2-proxy --config new-config.cfg --config-test
295+```
296+
297+### Exit Codes
298+
299+- **0**: Configuration is valid ✅
300+- **1**: Configuration is invalid (errors printed to stderr) ❌
301+
302+### Validation Coverage
303+
304+The `--config-test` flag performs the **same comprehensive validation** as normal startup, including:
305+- Required fields (client ID, client secret, cookie secret, etc.)
306+- Syntax validation (TOML/YAML parsing)
307+- Provider configuration
308+- Upstream server definitions
309+- Session store connectivity (e.g., Redis network checks if configured)
310+
311+**Note**: Cannot be combined with `--convert-config-to-alpha`.
312+
308313## Upstreams Configuration
309314
310315`oauth2-proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers, unix socket or serve static files from the file system.
docs/versioned_docs/version-7.15.x/configuration/systemd_socket.md+8
@@ -40,4 +40,12 @@ the listener it created onto the process, starting with file descriptor 3.
4040 --client-secret=...
4141```
4242
43+## Trusted IPs
44+
45+When listening on a Unix socket, Go sets `http.Request.RemoteAddr` to `"@"` instead of the usual `"host:port"` format. This means there is no client IP available from the connection itself.
46+
47+As a result, `--trusted-ip` entries cannot match against the direct connection address for Unix socket listeners. Requests arriving over a Unix socket will never be considered "trusted" based on their `RemoteAddr`. IP-based trust decisions will still work if a trusted reverse proxy sets `X-Forwarded-For` or `X-Real-IP` headers and `--reverse-proxy=true` is configured.
48+
49+## TLS
50+
4351Currently TLS is not supported (but it's doable).
docs/versioned_docs/version-7.15.x/installation.md+1−1
@@ -5,7 +5,7 @@ title: Installation
55
661. Choose how to deploy:
77
8− a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.0`)
8+ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.4`)
99
1010 b. Using Go to install the latest release
1111 ```bash
go.mod+42−46
@@ -1,87 +1,83 @@
11module github.com/oauth2-proxy/oauth2-proxy/v7
22
3−go 1.25.0
3+go 1.26.0
44
55require (
66 cloud.google.com/go/compute/metadata v0.9.0
77 github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb
88 github.com/a8m/envsubst v1.4.3
9− github.com/alicebob/miniredis/v2 v2.37.0
9+ github.com/alicebob/miniredis/v2 v2.38.0
1010 github.com/bitly/go-simplejson v0.5.1
11− github.com/bsm/redislock v0.9.4
12− github.com/coreos/go-oidc/v3 v3.17.0
11+ github.com/bsm/redislock v0.10.0
12+ github.com/coreos/go-oidc/v3 v3.20.0
1313 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
14− github.com/fsnotify/fsnotify v1.9.0
15− github.com/go-jose/go-jose/v3 v3.0.4
16− github.com/go-jose/go-jose/v4 v4.1.3
17− github.com/go-viper/mapstructure/v2 v2.4.0
14+ github.com/fsnotify/fsnotify v1.10.1
15+ github.com/go-jose/go-jose/v3 v3.0.5
16+ github.com/go-jose/go-jose/v4 v4.1.4
17+ github.com/go-viper/mapstructure/v2 v2.5.0
1818 github.com/golang-jwt/jwt/v5 v5.3.1
1919 github.com/google/go-cmp v0.7.0
2020 github.com/google/uuid v1.6.0
2121 github.com/gorilla/mux v1.8.1
2222 github.com/justinas/alice v1.2.0
2323 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
24− github.com/onsi/ginkgo/v2 v2.28.1
25− github.com/onsi/gomega v1.39.1
26− github.com/pierrec/lz4/v4 v4.1.26
27− github.com/prometheus/client_golang v1.23.2
28− github.com/redis/go-redis/v9 v9.18.0
24+ github.com/onsi/ginkgo/v2 v2.32.1
25+ github.com/onsi/gomega v1.42.1
26+ github.com/pierrec/lz4/v4 v4.1.28
27+ github.com/prometheus/client_golang v1.24.1
28+ github.com/redis/go-redis/v9 v9.22.0
2929 github.com/spf13/cast v1.10.0
3030 github.com/spf13/pflag v1.0.10
3131 github.com/spf13/viper v1.21.0
32− github.com/stretchr/testify v1.11.1
32+ github.com/stretchr/testify v1.12.0
3333 github.com/vmihailenco/msgpack/v5 v5.4.1
34− go.yaml.in/yaml/v3 v3.0.4
35− golang.org/x/crypto v0.49.0
36− golang.org/x/net v0.52.0
34+ go.yaml.in/yaml/v3 v3.0.5
35+ golang.org/x/crypto v0.55.0
36+ golang.org/x/net v0.58.0
3737 golang.org/x/oauth2 v0.36.0
38− golang.org/x/sync v0.20.0
39− google.golang.org/api v0.272.0
38+ golang.org/x/sync v0.22.0
39+ google.golang.org/api v0.293.0
4040 gopkg.in/natefinch/lumberjack.v2 v2.2.1
41− k8s.io/apimachinery v0.35.3
41+ k8s.io/apimachinery v0.36.3
4242)
4343
4444require (
45− cloud.google.com/go/auth v0.18.2 // indirect
45+ cloud.google.com/go/auth v0.23.1 // indirect
4646 cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
47− github.com/Masterminds/semver/v3 v3.4.0 // indirect
47+ github.com/Masterminds/semver/v3 v3.5.0 // indirect
4848 github.com/beorn7/perks v1.0.1 // indirect
4949 github.com/cespare/xxhash/v2 v2.3.0 // indirect
50− github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
51− github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
52− github.com/felixge/httpsnoop v1.0.4 // indirect
53− github.com/go-logr/logr v1.4.3 // indirect
50+ github.com/felixge/httpsnoop v1.1.0 // indirect
51+ github.com/go-logr/logr v1.4.4 // indirect
5452 github.com/go-logr/stdr v1.2.2 // indirect
5553 github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
56− github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
54+ github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect
5755 github.com/google/s2a-go v0.1.9 // indirect
58− github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
59− github.com/googleapis/gax-go/v2 v2.19.0 // indirect
56+ github.com/googleapis/enterprise-certificate-proxy v0.3.21 // indirect
57+ github.com/googleapis/gax-go/v2 v2.23.0 // indirect
6058 github.com/kylelemons/godebug v1.1.0 // indirect
6159 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
62− github.com/pelletier/go-toml/v2 v2.2.4 // indirect
63− github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
60+ github.com/pelletier/go-toml/v2 v2.4.3 // indirect
6461 github.com/prometheus/client_model v0.6.2 // indirect
65− github.com/prometheus/common v0.67.5 // indirect
66− github.com/prometheus/procfs v0.20.1 // indirect
62+ github.com/prometheus/common v0.70.1 // indirect
63+ github.com/prometheus/procfs v0.21.1 // indirect
6764 github.com/sagikazarmark/locafero v0.12.0 // indirect
6865 github.com/spf13/afero v1.15.0 // indirect
6966 github.com/subosito/gotenv v1.6.0 // indirect
7067 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
71− github.com/yuin/gopher-lua v1.1.1 // indirect
68+ github.com/yuin/gopher-lua v1.1.2 // indirect
7269 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
73− go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
74− go.opentelemetry.io/otel v1.42.0 // indirect
75− go.opentelemetry.io/otel/metric v1.42.0 // indirect
76− go.opentelemetry.io/otel/trace v1.42.0 // indirect
70+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
71+ go.opentelemetry.io/otel v1.45.0 // indirect
72+ go.opentelemetry.io/otel/metric v1.45.0 // indirect
73+ go.opentelemetry.io/otel/trace v1.45.0 // indirect
7774 go.uber.org/atomic v1.11.0 // indirect
78− go.yaml.in/yaml/v2 v2.4.4 // indirect
79− golang.org/x/mod v0.34.0 // indirect
80− golang.org/x/sys v0.42.0 // indirect
81− golang.org/x/text v0.35.0 // indirect
82− golang.org/x/tools v0.43.0 // indirect
83− google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
84− google.golang.org/grpc v1.79.3 // indirect
85− google.golang.org/protobuf v1.36.11 // indirect
75+ golang.org/x/mod v0.40.0 // indirect
76+ golang.org/x/sys v0.47.0 // indirect
77+ golang.org/x/text v0.41.0 // indirect
78+ golang.org/x/tools v0.49.0 // indirect
79+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 // indirect
80+ google.golang.org/grpc v1.83.0 // indirect
81+ google.golang.org/protobuf v1.36.12 // indirect
8682 gopkg.in/yaml.v3 v3.0.1 // indirect
8783)
go.sum+100−106
@@ -1,5 +1,5 @@
1−cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
2−cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
1+cloud.google.com/go/auth v0.23.1 h1:1tPpBPG02lQHmoiAvs9egyCASqXP0xgobptjZzov/Jg=
2+cloud.google.com/go/auth v0.23.1/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
33cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
44cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
55cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@@ -8,14 +8,14 @@ github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWO
88github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
99github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc=
1010github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
11−github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
12−github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
11+github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
12+github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
1313github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
1414github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
1515github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
1616github.com/alicebob/miniredis/v2 v2.11.1/go.mod h1:UA48pmi7aSazcGAvcdKcBB49z521IC9VjTTRz2nIaJE=
17−github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
18−github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
17+github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
18+github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
1919github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
2020github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
2121github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
@@ -24,47 +24,43 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
2424github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
2525github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
2626github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
27−github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
28−github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
27+github.com/bsm/redislock v0.10.0 h1:NAe1OHDnwPmWEzD+LpWBZXAdZrWld/xj/wmqEcQednQ=
28+github.com/bsm/redislock v0.10.0/go.mod h1:M05WZGjPbX/8ne7LmB7AbOsa2kv7Jzx4fqkLpDFZvJs=
2929github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
3030github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
3131github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
3232github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
3333github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
34−github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
35−github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
34+github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE=
35+github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
3636github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
3737github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
3838github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
39−github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
40−github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
41−github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
42−github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
43−github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
44−github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
39+github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
40+github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
4541github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
4642github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
47−github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
48−github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
43+github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
44+github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
4945github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
5046github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
5147github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
5248github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
5349github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
5450github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
55−github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
56−github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
57−github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
58−github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
51+github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
52+github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
53+github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
54+github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
5955github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
60−github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
61−github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
56+github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
57+github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
6258github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
6359github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
6460github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
6561github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
66−github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
67−github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
62+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
63+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
6864github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
6965github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
7066github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -76,27 +72,27 @@ github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85q
7672github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7773github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
7874github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
79−github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
80−github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
75+github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
76+github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
8177github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
8278github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
8379github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
8480github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8581github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
86−github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
87−github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
88−github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
89−github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
82+github.com/googleapis/enterprise-certificate-proxy v0.3.21 h1:OFdQ3tnCX/zaQ0Cedur3D3z7kI6HiLX9g3TiAN4/DFU=
83+github.com/googleapis/enterprise-certificate-proxy v0.3.21/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
84+github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
85+github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
9086github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
9187github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
9288github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
9389github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
9490github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
9591github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
96−github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
97−github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
98−github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
99−github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
92+github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
93+github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
94+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
95+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
10096github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
10197github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
10298github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -113,27 +109,25 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
113109github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
114110github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk=
115111github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw=
116−github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
117−github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
118−github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
119−github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
120−github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
121−github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
122−github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
123−github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
112+github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
113+github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
114+github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
115+github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
116+github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
117+github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
118+github.com/pierrec/lz4/v4 v4.1.28 h1:pPEPwRJ4kybBTfGt28q7lQsRJQHhC08axprdLD5Ppio=
119+github.com/pierrec/lz4/v4 v4.1.28/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
124120github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
125−github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
126−github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
127−github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
128−github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
121+github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
122+github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
129123github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
130124github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
131−github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
132−github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
133−github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
134−github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
135−github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
136−github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
125+github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
126+github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
127+github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
128+github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
129+github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
130+github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
137131github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
138132github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
139133github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
@@ -148,8 +142,8 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
148142github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
149143github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
150144github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
151−github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
152−github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
145+github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
146+github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
153147github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
154148github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
155149github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
@@ -167,57 +161,57 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
167161github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
168162github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
169163github.com/yuin/gopher-lua v0.0.0-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
170−github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
171−github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
172−github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
173−github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
164+github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
165+github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
166+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
167+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
174168go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
175169go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
176−go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
177−go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
178−go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
179−go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
180−go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
181−go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
182−go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
183−go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
184−go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
185−go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
186−go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
187−go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
188−go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
189−go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
170+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
171+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
172+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
173+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
174+go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
175+go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
176+go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
177+go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
178+go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
179+go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
180+go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
181+go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
182+go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
183+go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
190184go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
191185go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
192186go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
193187go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
194188go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
195189go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
196−go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
197−go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
190+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
191+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
198192golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
199193golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
200194golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
201−golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
202−golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
195+golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
196+golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
203197golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
204198golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
205−golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
206−golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
199+golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
200+golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
207201golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
208202golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
209203golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
210204golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
211205golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
212−golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
213−golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
206+golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
207+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
214208golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
215209golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
216210golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
217211golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
218212golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
219−golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
220−golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
213+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
214+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
221215golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
222216golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
223217golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -227,8 +221,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
227221golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
228222golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
229223golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
230−golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
231−golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
224+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
225+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
232226golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
233227golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
234228golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -240,31 +234,31 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
240234golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
241235golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
242236golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
243−golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
244−golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
237+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
238+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
245239golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
246240golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
247241golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
248242golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
249243golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
250244golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
251−golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
252−golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
245+golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
246+golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
253247golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
254−gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
255−gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
256−google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
257−google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
258−google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
259−google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
260−google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js=
261−google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
262−google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
263−google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
264−google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
265−google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
266−google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
267−google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
248+gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
249+gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
250+google.golang.org/api v0.293.0 h1:p9XIWOf63U4OgYx120ZwVU8+vl4XTPmWfgVPnmOAS9w=
251+google.golang.org/api v0.293.0/go.mod h1:6n5tjEB1gzwniZTepZ0g5u+wM7Bof5GeULCx/zh8ZE0=
252+google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
253+google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
254+google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
255+google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
256+google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 h1:utmqiUzZAEINbLmRyAt1QjVkMyLQ+lvIwCpLaB1rZfw=
257+google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
258+google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
259+google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
260+google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
261+google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
268262gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
269263gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
270264gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -273,5 +267,5 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
273267gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
274268gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
275269gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
276−k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
277−k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
270+k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
271+k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
main_test.go+2−2
@@ -278,7 +278,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
278278 Entry("with bad legacy configuration", loadConfigurationTableInput{
279279 configContent: testCoreConfig + "unknown_field=\"something\"",
280280 expectedOptions: func() *options.Options { return nil },
281− 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"),
281+ 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"),
282282 }),
283283 Entry("with bad alpha configuration", loadConfigurationTableInput{
284284 configContent: testCoreConfig,
@@ -290,7 +290,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
290290 configContent: testCoreConfig + "unknown_field=\"something\"",
291291 alphaConfigContent: testAlphaConfig,
292292 expectedOptions: func() *options.Options { return nil },
293− 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"),
293+ 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"),
294294 }),
295295 )
296296
oauthproxy.go+31−20
@@ -59,6 +59,8 @@ const (
5959)
6060
6161var (
62+ defaultTrustedProxyIPs = []string{"0.0.0.0/0", "::/0"}
63+
6264 // ErrNeedsLogin means the user should be redirected to the login page
6365 ErrNeedsLogin = errors.New("redirect to login page")
6466
@@ -183,13 +185,14 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
183185
184186 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)
185187
186− trustedIPs := ip.NewNetSet()
187− for _, ipStr := range opts.TrustedIPs {
188− if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
189− trustedIPs.AddIPNet(*ipNet)
190− } else {
191− return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
192− }
188+ trustedIPs, err := ip.ParseNetSet(opts.TrustedIPs)
189+ if err != nil {
190+ return nil, err
191+ }
192+
193+ trustedProxies, err := buildTrustedProxyNetSet(opts)
194+ if err != nil {
195+ return nil, err
193196 }
194197
195198 allowedRoutes, err := buildRoutesAllowlist(opts)
@@ -202,7 +205,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
202205 return nil, err
203206 }
204207
205− preAuthChain, err := buildPreAuthChain(opts, sessionStore)
208+ preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies)
206209 if err != nil {
207210 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
208211 }
@@ -355,8 +358,8 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
355358// buildPreAuthChain constructs a chain that should process every request before
356359// the OAuth2 Proxy authentication logic kicks in.
357360// For example forcing HTTPS or health checks.
358−func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) {
359− chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
361+func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore, trustedProxies *ip.NetSet) (alice.Chain, error) {
362+ chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader, trustedProxies))
360363
361364 if opts.ForceHTTPS {
362365 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
@@ -395,6 +398,16 @@ func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionSt
395398 return chain, nil
396399}
397400
401+func buildTrustedProxyNetSet(opts *options.Options) (*ip.NetSet, error) {
402+ trustedProxyIPs := opts.TrustedProxyIPs
403+ if opts.ReverseProxy && len(trustedProxyIPs) == 0 {
404+ 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.")
405+ trustedProxyIPs = defaultTrustedProxyIPs
406+ }
407+
408+ return ip.ParseNetSet(trustedProxyIPs)
409+}
410+
398411func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
399412 chain := alice.New()
400413
@@ -574,7 +587,7 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code i
574587
575588// IsAllowedRequest is used to check if auth should be skipped for this request
576589func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
577− isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
590+ isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == http.MethodOptions
578591 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
579592}
580593
@@ -613,9 +626,7 @@ func (p *OAuthProxy) isAPIPath(req *http.Request) bool {
613626
614627// isTrustedIP is used to check if a request comes from a trusted client IP address.
615628func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
616− // RemoteAddr @ means unix socket
617− // https://github.com/golang/go/blob/0fa53e41f122b1661d0678a6d36d71b7b5ad031d/src/syscall/syscall_linux.go#L506-L511
618− if p.trustedIPs == nil && req.RemoteAddr != "@" {
629+ if p.trustedIPs == nil {
619630 return false
620631 }
621632
@@ -636,6 +647,10 @@ func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
636647// SignInPage writes the sign in template to the response
637648func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
638649 prepareNoCache(rw)
650+
651+ if err := p.ClearSessionCookie(rw, req); err != nil {
652+ logger.Printf("Error clearing session cookie: %v", err)
653+ }
639654 rw.WriteHeader(code)
640655
641656 redirectURL, err := p.appDirector.GetRedirect(req)
@@ -649,16 +664,12 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
649664 redirectURL = "/"
650665 }
651666
652− if err := p.ClearSessionCookie(rw, req); err != nil {
653− logger.Printf("Error clearing session cookie: %v", err)
654− }
655−
656667 p.pageWriter.WriteSignInPage(rw, req, redirectURL, code)
657668}
658669
659670// ManualSignIn handles basic auth logins to the proxy
660671func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool, int) {
661− if req.Method != "POST" || p.basicAuthValidator == nil {
672+ if req.Method != http.MethodPost || p.basicAuthValidator == nil {
662673 return "", false, http.StatusOK
663674 }
664675 user := req.FormValue("username")
@@ -800,7 +811,7 @@ func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request) {
800811 }
801812
802813 defer resp.Body.Close()
803− if resp.StatusCode != 200 {
814+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
804815 logger.Errorf("error while calling backend logout url, returned error code %v", resp.StatusCode)
805816 }
806817}
oauthproxy_test.go+243−65
@@ -54,7 +54,7 @@ func TestRobotsTxt(t *testing.T) {
5454 t.Fatal(err)
5555 }
5656 rw := httptest.NewRecorder()
57− req, _ := http.NewRequest("GET", "/robots.txt", nil)
57+ req, _ := http.NewRequest(http.MethodGet, "/robots.txt", nil)
5858 proxy.ServeHTTP(rw, req)
5959 assert.Equal(t, 200, rw.Code)
6060 assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String())
@@ -241,7 +241,7 @@ func TestBasicAuthPassword(t *testing.T) {
241241
242242 // Save the required session
243243 rw := httptest.NewRecorder()
244− req, _ := http.NewRequest("GET", "/", nil)
244+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
245245 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
246246 Email: emailAddress,
247247 })
@@ -250,7 +250,7 @@ func TestBasicAuthPassword(t *testing.T) {
250250 // Extract the cookie value to inject into the test request
251251 cookie := rw.Header().Values("Set-Cookie")[0]
252252
253− req, _ = http.NewRequest("GET", "/", nil)
253+ req, _ = http.NewRequest(http.MethodGet, "/", nil)
254254 req.Header.Set("Cookie", cookie)
255255 rw = httptest.NewRecorder()
256256 proxy.ServeHTTP(rw, req)
@@ -300,14 +300,14 @@ func TestPassGroupsHeadersWithGroups(t *testing.T) {
300300
301301 // Save the required session
302302 rw := httptest.NewRecorder()
303− req, _ := http.NewRequest("GET", "/", nil)
303+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
304304 err = proxy.sessionStore.Save(rw, req, session)
305305 assert.NoError(t, err)
306306
307307 // Extract the cookie value to inject into the test request
308308 cookie := rw.Header().Values("Set-Cookie")[0]
309309
310− req, _ = http.NewRequest("GET", "/", nil)
310+ req, _ = http.NewRequest(http.MethodGet, "/", nil)
311311 req.Header.Set("Cookie", cookie)
312312 rw = httptest.NewRecorder()
313313 proxy.ServeHTTP(rw, req)
@@ -457,7 +457,7 @@ func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoin
457457 return 0, ""
458458 }
459459
460− req, err := http.NewRequest("GET", endpoint, strings.NewReader(""))
460+ req, err := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
461461 if err != nil {
462462 return 0, ""
463463 }
@@ -608,7 +608,7 @@ func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) {
608608
609609func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
610610 rw := httptest.NewRecorder()
611− req, _ := http.NewRequest("GET", endpoint, strings.NewReader(""))
611+ req, _ := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
612612 sipTest.proxy.ServeHTTP(rw, req)
613613 return rw.Code, rw.Body.String()
614614}
@@ -713,6 +713,50 @@ func TestManualSignInCorrectCredentials(t *testing.T) {
713713 assert.Equal(t, http.StatusFound, statusCode)
714714}
715715
716+func TestSignInPageClearsExistingSessionCookie(t *testing.T) {
717+ opts := baseTestOptions()
718+ err := validation.Validate(opts)
719+ require.NoError(t, err)
720+
721+ proxy, err := NewOAuthProxy(opts, func(string) bool {
722+ return true
723+ })
724+ require.NoError(t, err)
725+
726+ // Create a real session cookie using the actual session store.
727+ saveRW := httptest.NewRecorder()
728+ saveReq := httptest.NewRequest(http.MethodGet, "/", nil)
729+ err = proxy.sessionStore.Save(saveRW, saveReq, &sessions.SessionState{
730+ Email: "john.doe@example.com",
731+ })
732+ require.NoError(t, err)
733+
734+ cookies := saveRW.Result().Cookies()
735+ require.NotEmpty(t, cookies)
736+
737+ // Send that cookie to the sign-in page.
738+ req := httptest.NewRequest(http.MethodGet, "/oauth2/sign_in", nil)
739+ for _, c := range cookies {
740+ req.AddCookie(c)
741+ }
742+
743+ rw := httptest.NewRecorder()
744+ proxy.ServeHTTP(rw, req)
745+
746+ assert.Equal(t, http.StatusOK, rw.Code)
747+
748+ cleared := false
749+ for _, c := range rw.Result().Cookies() {
750+ if c.Name == proxy.CookieOptions.Name {
751+ cleared = true
752+ assert.Equal(t, "", c.Value)
753+ assert.Less(t, c.MaxAge, 0)
754+ }
755+ }
756+
757+ assert.True(t, cleared, "expected sign-in page to clear existing session cookie")
758+}
759+
716760func TestSignInPageIncludesTargetRedirect(t *testing.T) {
717761 sipTest, err := NewSignInPageTest(false)
718762 if err != nil {
@@ -850,7 +894,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
850894 // access_token validation.
851895 pcTest.proxy.CookieOptions.Refresh = time.Duration(0)
852896 pcTest.rw = httptest.NewRecorder()
853− pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
897+ pcTest.req, _ = http.NewRequest(http.MethodGet, "/", strings.NewReader(""))
854898 pcTest.validateUser = true
855899 return &pcTest, nil
856900}
@@ -983,7 +1027,7 @@ func NewUserInfoEndpointTest() (*ProcessCookieTest, error) {
9831027 if err != nil {
9841028 return nil, err
9851029 }
986− pcTest.req, _ = http.NewRequest("GET",
1030+ pcTest.req, _ = http.NewRequest(http.MethodGet,
9871031 pcTest.opts.ProxyPrefix+"/userinfo", nil)
9881032 return pcTest, nil
9891033}
@@ -1091,7 +1135,7 @@ func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (
10911135 return nil, err
10921136 }
10931137 pcTest.req, _ = http.NewRequest(
1094− "GET",
1138+ http.MethodGet,
10951139 fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
10961140 nil)
10971141 return pcTest, nil
@@ -1230,7 +1274,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
12301274 pcTest.validateUser = true
12311275
12321276 pcTest.rw = httptest.NewRecorder()
1233− pcTest.req, _ = http.NewRequest("GET",
1277+ pcTest.req, _ = http.NewRequest(http.MethodGet,
12341278 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
12351279
12361280 created := time.Now()
@@ -1323,7 +1367,7 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
13231367 pcTest.validateUser = true
13241368
13251369 pcTest.rw = httptest.NewRecorder()
1326− pcTest.req, _ = http.NewRequest("GET",
1370+ pcTest.req, _ = http.NewRequest(http.MethodGet,
13271371 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
13281372
13291373 created := time.Now()
@@ -1403,7 +1447,7 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
14031447 pcTest.validateUser = true
14041448
14051449 pcTest.rw = httptest.NewRecorder()
1406− pcTest.req, _ = http.NewRequest("GET",
1450+ pcTest.req, _ = http.NewRequest(http.MethodGet,
14071451 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
14081452
14091453 created := time.Now()
@@ -1451,7 +1495,7 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
14511495 }
14521496 proxy.provider = NewTestProvider(upstreamURL, "")
14531497 rw := httptest.NewRecorder()
1454− req, _ := http.NewRequest("OPTIONS", "/preflight-request", nil)
1498+ req, _ := http.NewRequest(http.MethodOptions, "/preflight-request", nil)
14551499 proxy.ServeHTTP(rw, req)
14561500
14571501 assert.Equal(t, 200, rw.Code)
@@ -1608,19 +1652,19 @@ func TestRequestSignature(t *testing.T) {
16081652 resp string
16091653 }{
16101654 "No request signature": {
1611− method: "GET",
1655+ method: http.MethodGet,
16121656 body: "",
16131657 key: "",
16141658 resp: "no signature received",
16151659 },
16161660 "Get request": {
1617− method: "GET",
1661+ method: http.MethodGet,
16181662 body: "",
16191663 key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
16201664 resp: "signatures match",
16211665 },
16221666 "Post request": {
1623− method: "POST",
1667+ method: http.MethodPost,
16241668 body: `{ "hello": "world!" }`,
16251669 key: "d90df39e2d19282840252612dd7c81421a372f61",
16261670 resp: "signatures match",
@@ -2145,7 +2189,33 @@ func TestTrustedIPs(t *testing.T) {
21452189 reverseProxy: false,
21462190 realClientIPHeader: "X-Real-IP", // Default value
21472191 req: func() *http.Request {
2148− req, _ := http.NewRequest("GET", "/", nil)
2192+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2193+ return req
2194+ }(),
2195+ expectTrusted: false,
2196+ },
2197+ // Check Unix socket with no trusted IPs configured does not error.
2198+ {
2199+ name: "UnixSocketWithoutTrustedIPs",
2200+ trustedIPs: nil,
2201+ reverseProxy: false,
2202+ realClientIPHeader: "X-Real-IP",
2203+ req: func() *http.Request {
2204+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2205+ req.RemoteAddr = "@"
2206+ return req
2207+ }(),
2208+ expectTrusted: false,
2209+ },
2210+ // Check Unix socket with trusted IPs configured returns false (no IP to match).
2211+ {
2212+ name: "UnixSocketWithTrustedIPs",
2213+ trustedIPs: []string{"127.0.0.1"},
2214+ reverseProxy: false,
2215+ realClientIPHeader: "X-Real-IP",
2216+ req: func() *http.Request {
2217+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2218+ req.RemoteAddr = "@"
21492219 return req
21502220 }(),
21512221 expectTrusted: false,
@@ -2157,7 +2227,7 @@ func TestTrustedIPs(t *testing.T) {
21572227 reverseProxy: false,
21582228 realClientIPHeader: "X-Real-IP", // Default value
21592229 req: func() *http.Request {
2160− req, _ := http.NewRequest("GET", "/", nil)
2230+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21612231 req.RemoteAddr = "127.0.0.1:43670"
21622232 return req
21632233 }(),
@@ -2170,7 +2240,7 @@ func TestTrustedIPs(t *testing.T) {
21702240 reverseProxy: true,
21712241 realClientIPHeader: "X-Real-IP", // Default value
21722242 req: func() *http.Request {
2173− req, _ := http.NewRequest("GET", "/", nil)
2243+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21742244 req.RemoteAddr = "127.0.0.1:44324"
21752245 return req
21762246 }(),
@@ -2183,7 +2253,7 @@ func TestTrustedIPs(t *testing.T) {
21832253 reverseProxy: true,
21842254 realClientIPHeader: "X-Forwarded-For",
21852255 req: func() *http.Request {
2186− req, _ := http.NewRequest("GET", "/", nil)
2256+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21872257 req.Header.Add("X-Forwarded-For", "127.0.0.1")
21882258 return req
21892259 }(),
@@ -2196,7 +2266,7 @@ func TestTrustedIPs(t *testing.T) {
21962266 reverseProxy: true,
21972267 realClientIPHeader: "X-Forwarded-For",
21982268 req: func() *http.Request {
2199− req, _ := http.NewRequest("GET", "/", nil)
2269+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
22002270 req.Header.Add("X-Forwarded-For", "::1")
22012271 return req
22022272 }(),
@@ -2209,7 +2279,7 @@ func TestTrustedIPs(t *testing.T) {
22092279 reverseProxy: true,
22102280 realClientIPHeader: "X-Forwarded-For",
22112281 req: func() *http.Request {
2212− req, _ := http.NewRequest("GET", "/", nil)
2282+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
22132283 req.Header.Add("X-Forwarded-For", "12.34.56.78")
22142284 return req
22152285 }(),
@@ -2222,7 +2292,7 @@ func TestTrustedIPs(t *testing.T) {
22222292 reverseProxy: true,
22232293 realClientIPHeader: "X-Forwarded-For",
22242294 req: func() *http.Request {
2225− req, _ := http.NewRequest("GET", "/", nil)
2295+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
22262296 req.Header.Add("X-Forwarded-For", "::2")
22272297 return req
22282298 }(),
@@ -2235,7 +2305,7 @@ func TestTrustedIPs(t *testing.T) {
22352305 reverseProxy: true,
22362306 realClientIPHeader: "X-Forwarded-For",
22372307 req: func() *http.Request {
2238− req, _ := http.NewRequest("GET", "/", nil)
2308+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
22392309 req.Header.Add("X-Real-IP", "::1")
22402310 return req
22412311 }(),
@@ -2248,7 +2318,7 @@ func TestTrustedIPs(t *testing.T) {
22482318 reverseProxy: true,
22492319 realClientIPHeader: "X-Forwarded-For",
22502320 req: func() *http.Request {
2251− req, _ := http.NewRequest("GET", "/", nil)
2321+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
22522322 req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
22532323 return req
22542324 }(),
@@ -2261,7 +2331,7 @@ func TestTrustedIPs(t *testing.T) {
22612331 reverseProxy: false,
22622332 realClientIPHeader: "X-Real-IP",
22632333 req: func() *http.Request {
2264− req, _ := http.NewRequest("GET", "/", nil)
2334+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
22652335 req.RemoteAddr = "adsfljk29242as!!"
22662336 return req
22672337 }(),
@@ -2357,12 +2427,12 @@ func Test_buildRoutesAllowlist(t *testing.T) {
23572427 },
23582428 expectedRoutes: []expectedAllowedRoute{
23592429 {
2360− method: "GET",
2430+ method: http.MethodGet,
23612431 negate: false,
23622432 regexString: "^/foo/bar",
23632433 },
23642434 {
2365− method: "POST",
2435+ method: http.MethodPost,
23662436 negate: false,
23672437 regexString: "^/baz/[0-9]+/thing",
23682438 },
@@ -2415,11 +2485,11 @@ func Test_buildRoutesAllowlist(t *testing.T) {
24152485 regexString: "^/baz/[0-9]+/thing/regex",
24162486 },
24172487 {
2418− method: "GET",
2488+ method: http.MethodGet,
24192489 regexString: "^/foo/bar",
24202490 },
24212491 {
2422− method: "POST",
2492+ method: http.MethodPost,
24232493 regexString: "^/baz/[0-9]+/thing",
24242494 },
24252495 {
@@ -2571,7 +2641,7 @@ func TestApiRoutes(t *testing.T) {
25712641
25722642 for _, tc := range testCases {
25732643 t.Run(tc.name, func(t *testing.T) {
2574− req, err := http.NewRequest("GET", tc.url, nil)
2644+ req, err := http.NewRequest(http.MethodGet, tc.url, nil)
25752645 req.Header.Set("Accept", tc.contentType)
25762646 assert.NoError(t, err)
25772647
@@ -2609,9 +2679,11 @@ func TestAllowedRequest(t *testing.T) {
26092679 }
26102680 opts.SkipAuthRegex = []string{
26112681 "^/skip/auth/regex$",
2682+ "^/public/.*/endpoint$",
26122683 }
26132684 opts.SkipAuthRoutes = []string{
26142685 "GET=^/skip/auth/routes/get",
2686+ "^/foo/.*/bar$",
26152687 }
26162688 err := validation.Validate(opts)
26172689 assert.NoError(t, err)
@@ -2628,25 +2700,37 @@ func TestAllowedRequest(t *testing.T) {
26282700 }{
26292701 {
26302702 name: "Regex GET allowed",
2631− method: "GET",
2703+ method: http.MethodGet,
26322704 url: "/skip/auth/regex",
26332705 allowed: true,
26342706 },
26352707 {
26362708 name: "Regex POST allowed ",
2637− method: "POST",
2709+ method: http.MethodPost,
26382710 url: "/skip/auth/regex",
26392711 allowed: true,
26402712 },
26412713 {
26422714 name: "Regex denied",
2643− method: "GET",
2715+ method: http.MethodGet,
26442716 url: "/wrong/denied",
26452717 allowed: false,
26462718 },
2719+ {
2720+ name: "Regex allowed with fragment-free path",
2721+ method: http.MethodGet,
2722+ url: "/public/legit/endpoint",
2723+ allowed: true,
2724+ },
2725+ {
2726+ name: "Regex denied when path contains encoded fragment suffix",
2727+ method: http.MethodGet,
2728+ url: "/public/secret%23/endpoint",
2729+ allowed: false,
2730+ },
26472731 {
26482732 name: "Route allowed",
2649− method: "GET",
2733+ method: http.MethodGet,
26502734 url: "/skip/auth/routes/get",
26512735 allowed: true,
26522736 },
@@ -2658,16 +2742,28 @@ func TestAllowedRequest(t *testing.T) {
26582742 },
26592743 {
26602744 name: "Route denied with wrong path",
2661− method: "GET",
2745+ method: http.MethodGet,
26622746 url: "/skip/auth/routes/wrong/path",
26632747 allowed: false,
26642748 },
26652749 {
26662750 name: "Route denied with wrong path and method",
2667− method: "POST",
2751+ method: http.MethodPost,
26682752 url: "/skip/auth/routes/wrong/path",
26692753 allowed: false,
26702754 },
2755+ {
2756+ name: "Route allowed with fragment-free path",
2757+ method: http.MethodGet,
2758+ url: "/foo/public/bar",
2759+ allowed: true,
2760+ },
2761+ {
2762+ name: "Route denied when path contains encoded fragment suffix",
2763+ method: http.MethodGet,
2764+ url: "/foo/secret%23/bar",
2765+ allowed: false,
2766+ },
26712767 }
26722768
26732769 for _, tc := range testCases {
@@ -2708,9 +2804,11 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
27082804 }
27092805 opts.SkipAuthRegex = []string{
27102806 "^/skip/auth/regex$",
2807+ "^/public/.*/endpoint$",
27112808 }
27122809 opts.SkipAuthRoutes = []string{
27132810 "GET=^/skip/auth/routes/get",
2811+ "^/foo/.*/bar$",
27142812 }
27152813 err := validation.Validate(opts)
27162814 assert.NoError(t, err)
@@ -2727,25 +2825,37 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
27272825 }{
27282826 {
27292827 name: "Regex GET allowed",
2730− method: "GET",
2828+ method: http.MethodGet,
27312829 url: "/skip/auth/regex",
27322830 allowed: true,
27332831 },
27342832 {
27352833 name: "Regex POST allowed ",
2736− method: "POST",
2834+ method: http.MethodPost,
27372835 url: "/skip/auth/regex",
27382836 allowed: true,
27392837 },
27402838 {
27412839 name: "Regex denied",
2742− method: "GET",
2840+ method: http.MethodGet,
27432841 url: "/wrong/denied",
27442842 allowed: false,
27452843 },
2844+ {
2845+ name: "Regex allowed with fragment-free path",
2846+ method: http.MethodGet,
2847+ url: "/public/legit/endpoint",
2848+ allowed: true,
2849+ },
2850+ {
2851+ name: "Regex denied when X-Forwarded-Uri contains an encoded fragment suffix",
2852+ method: http.MethodGet,
2853+ url: "/public/secret%23/endpoint",
2854+ allowed: false,
2855+ },
27462856 {
27472857 name: "Route allowed",
2748− method: "GET",
2858+ method: http.MethodGet,
27492859 url: "/skip/auth/routes/get",
27502860 allowed: true,
27512861 },
@@ -2757,22 +2867,35 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
27572867 },
27582868 {
27592869 name: "Route denied with wrong path",
2760− method: "GET",
2870+ method: http.MethodGet,
27612871 url: "/skip/auth/routes/wrong/path",
27622872 allowed: false,
27632873 },
27642874 {
27652875 name: "Route denied with wrong path and method",
2766− method: "POST",
2876+ method: http.MethodPost,
27672877 url: "/skip/auth/routes/wrong/path",
27682878 allowed: false,
27692879 },
2880+ {
2881+ name: "Route allowed with fragment-free path",
2882+ method: http.MethodGet,
2883+ url: "/foo/public/bar",
2884+ allowed: true,
2885+ },
2886+ {
2887+ name: "Route denied when X-Forwarded-Uri contains an encoded fragment suffix",
2888+ method: http.MethodGet,
2889+ url: "/foo/secret%23/bar",
2890+ allowed: false,
2891+ },
27702892 }
27712893
27722894 for _, tc := range testCases {
27732895 t.Run(tc.name, func(t *testing.T) {
27742896 req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
27752897 req.Header.Set("X-Forwarded-Uri", tc.url)
2898+ req.RemoteAddr = "127.0.0.1:4180"
27762899 assert.NoError(t, err)
27772900
27782901 rw := httptest.NewRecorder()
@@ -2787,6 +2910,43 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
27872910 }
27882911}
27892912
2913+func TestAllowedRequestWithForwardedUriHeaderRequiresTrustedProxy(t *testing.T) {
2914+ upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2915+ w.WriteHeader(200)
2916+ }))
2917+ t.Cleanup(upstreamServer.Close)
2918+
2919+ opts := baseTestOptions()
2920+ opts.ReverseProxy = true
2921+ opts.TrustedProxyIPs = []string{"127.0.0.1/32"}
2922+ opts.UpstreamServers = options.UpstreamConfig{
2923+ Upstreams: []options.Upstream{
2924+ {
2925+ ID: upstreamServer.URL,
2926+ Path: "/",
2927+ URI: upstreamServer.URL,
2928+ },
2929+ },
2930+ }
2931+ opts.SkipAuthRegex = []string{"^/skip/auth/regex$"}
2932+
2933+ err := validation.Validate(opts)
2934+ assert.NoError(t, err)
2935+
2936+ proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2937+ assert.NoError(t, err)
2938+
2939+ req, err := http.NewRequest(http.MethodGet, opts.ProxyPrefix+authOnlyPath, nil)
2940+ assert.NoError(t, err)
2941+ req.RemoteAddr = "192.0.2.10:4180"
2942+ req.Header.Set("X-Forwarded-Uri", "/skip/auth/regex")
2943+
2944+ rw := httptest.NewRecorder()
2945+ proxy.ServeHTTP(rw, req)
2946+
2947+ assert.Equal(t, 401, rw.Code)
2948+}
2949+
27902950func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
27912951 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27922952 w.WriteHeader(200)
@@ -2826,37 +2986,37 @@ func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
28262986 }{
28272987 {
28282988 name: "Some static file allowed",
2829− method: "GET",
2989+ method: http.MethodGet,
28302990 url: "/static/file.txt",
28312991 allowed: true,
28322992 },
28332993 {
28342994 name: "POST to contact form allowed",
2835− method: "POST",
2995+ method: http.MethodPost,
28362996 url: "/contact",
28372997 allowed: true,
28382998 },
28392999 {
28403000 name: "Regex POST allowed",
2841− method: "POST",
3001+ method: http.MethodPost,
28423002 url: "/api/public-entity",
28433003 allowed: true,
28443004 },
28453005 {
28463006 name: "Regex POST with trailing slash allowed",
2847− method: "POST",
3007+ method: http.MethodPost,
28483008 url: "/api/public-entity/",
28493009 allowed: true,
28503010 },
28513011 {
28523012 name: "Regex GET api route denied",
2853− method: "GET",
3013+ method: http.MethodGet,
28543014 url: "/api/users",
28553015 allowed: false,
28563016 },
28573017 {
28583018 name: "Regex POST api route denied",
2859− method: "POST",
3019+ method: http.MethodPost,
28603020 url: "/api/users",
28613021 allowed: false,
28623022 },
@@ -2926,37 +3086,37 @@ func TestAllowedRequestNegateWithMethod(t *testing.T) {
29263086 }{
29273087 {
29283088 name: "Some static file allowed",
2929− method: "GET",
3089+ method: http.MethodGet,
29303090 url: "/static/file.txt",
29313091 allowed: true,
29323092 },
29333093 {
29343094 name: "POST to contact form not allowed",
2935− method: "POST",
3095+ method: http.MethodPost,
29363096 url: "/contact",
29373097 allowed: false,
29383098 },
29393099 {
29403100 name: "Regex POST allowed",
2941− method: "POST",
3101+ method: http.MethodPost,
29423102 url: "/api/public-entity",
29433103 allowed: true,
29443104 },
29453105 {
29463106 name: "Regex POST with trailing slash allowed",
2947− method: "POST",
3107+ method: http.MethodPost,
29483108 url: "/api/public-entity/",
29493109 allowed: true,
29503110 },
29513111 {
29523112 name: "Regex GET api route denied",
2953− method: "GET",
3113+ method: http.MethodGet,
29543114 url: "/api/users",
29553115 allowed: false,
29563116 },
29573117 {
29583118 name: "Regex POST api route denied",
2959− method: "POST",
3119+ method: http.MethodPost,
29603120 url: "/api/users",
29613121 allowed: false,
29623122 },
@@ -3096,7 +3256,7 @@ func TestProxyAllowedGroups(t *testing.T) {
30963256 t.Fatal(err)
30973257 }
30983258
3099− test.req, _ = http.NewRequest("GET", fmt.Sprintf("/%s", tt.querystring), nil)
3259+ test.req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("/%s", tt.querystring), nil)
31003260
31013261 test.req.Header.Add("accept", applicationJSON)
31023262 err = test.SaveSession(session)
@@ -3240,7 +3400,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
32403400 {
32413401 name: "UserWithGroupSkipAuthPreflight",
32423402 groups: []string{"a", "c"},
3243− method: "OPTIONS",
3403+ method: http.MethodOptions,
32443404 ip: "1.2.3.5:43670",
32453405 withSession: true,
32463406 expectedStatusCode: http.StatusAccepted,
@@ -3248,7 +3408,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
32483408 {
32493409 name: "UserWithGroupTrustedIp",
32503410 groups: []string{"a", "c"},
3251− method: "GET",
3411+ method: http.MethodGet,
32523412 ip: "1.2.3.4:43670",
32533413 withSession: true,
32543414 expectedStatusCode: http.StatusAccepted,
@@ -3256,7 +3416,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
32563416 {
32573417 name: "UserWithoutGroupSkipAuthPreflight",
32583418 groups: []string{"c"},
3259− method: "OPTIONS",
3419+ method: http.MethodOptions,
32603420 ip: "1.2.3.5:43670",
32613421 withSession: true,
32623422 expectedStatusCode: http.StatusForbidden,
@@ -3264,21 +3424,21 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
32643424 {
32653425 name: "UserWithoutGroupTrustedIp",
32663426 groups: []string{"c"},
3267− method: "GET",
3427+ method: http.MethodGet,
32683428 ip: "1.2.3.4:43670",
32693429 withSession: true,
32703430 expectedStatusCode: http.StatusForbidden,
32713431 },
32723432 {
32733433 name: "UserWithoutSessionSkipAuthPreflight",
3274− method: "OPTIONS",
3434+ method: http.MethodOptions,
32753435 ip: "1.2.3.5:43670",
32763436 withSession: false,
32773437 expectedStatusCode: http.StatusAccepted,
32783438 },
32793439 {
32803440 name: "UserWithoutSessionTrustedIp",
3281− method: "GET",
3441+ method: http.MethodGet,
32823442 ip: "1.2.3.4:43670",
32833443 withSession: false,
32843444 expectedStatusCode: http.StatusAccepted,
@@ -3378,6 +3538,24 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
33783538 querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
33793539 expectedStatusCode: http.StatusAccepted,
33803540 },
3541+ {
3542+ name: "UserWithMultipleAtSignsExactDomain",
3543+ email: "attacker@evil.com@example.com",
3544+ querystring: "?allowed_email_domains=example.com",
3545+ expectedStatusCode: http.StatusForbidden,
3546+ },
3547+ {
3548+ name: "UserWithMultipleAtSignsWildcardDomain",
3549+ email: "attacker@evil.com@foo.example.com",
3550+ querystring: "?allowed_email_domains=*.example.com",
3551+ expectedStatusCode: http.StatusForbidden,
3552+ },
3553+ {
3554+ name: "UserWithMultipleAtSignsDotPrefixedDomain",
3555+ email: "attacker@evil.com@foo.example.com",
3556+ querystring: "?allowed_email_domains=.example.com",
3557+ expectedStatusCode: http.StatusForbidden,
3558+ },
33813559 }
33823560
33833561 for _, tc := range testCases {
@@ -3612,14 +3790,14 @@ func TestIdTokenPlaceholderInSignOut(t *testing.T) {
36123790
36133791 // Save the required session
36143792 rw := httptest.NewRecorder()
3615− req, _ := http.NewRequest("GET", "/", nil)
3793+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
36163794 err = proxy.sessionStore.Save(rw, req, session)
36173795 assert.NoError(t, err)
36183796
36193797 rw = httptest.NewRecorder()
36203798
36213799 rdUrl := url.QueryEscape("https://my-oidc-provider.example.com/sign_out_page?id_token_hint={id_token}&post_logout_redirect_uri=https://my-app.example.com/")
3622− req, _ = http.NewRequest("GET", "/oauth2/sign_out?rd="+rdUrl, nil)
3800+ req, _ = http.NewRequest(http.MethodGet, "/oauth2/sign_out?rd="+rdUrl, nil)
36233801 req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
36243802 RequestID: "11111111-2222-4333-8444-555555555555",
36253803 Session: session,
pkg/apis/middleware/scope.go+48−1
@@ -2,9 +2,12 @@ package middleware
22
33import (
44 "context"
5+ "net"
56 "net/http"
7+ "strings"
68
79 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
811)
912
1013type scopeKey string
@@ -18,9 +21,13 @@ const RequestScopeKey scopeKey = "request-scope"
1821// within the chain.
1922type RequestScope struct {
2023 // ReverseProxy tracks whether OAuth2-Proxy is operating in reverse proxy
21− // mode and if request `X-Forwarded-*` headers should be trusted
24+ // mode and if request `X-Forwarded-*` headers may be trusted
2225 ReverseProxy bool
2326
27+ // TrustedProxies tracks which direct callers are allowed to supply
28+ // forwarded headers when ReverseProxy mode is enabled.
29+ TrustedProxies *ip.NetSet
30+
2431 // RequestID is set to the request's `X-Request-Id` header if set.
2532 // Otherwise a random UUID is set.
2633 RequestID string
@@ -58,3 +65,43 @@ func AddRequestScope(req *http.Request, scope *RequestScope) *http.Request {
5865 ctx := context.WithValue(req.Context(), RequestScopeKey, scope)
5966 return req.WithContext(ctx)
6067}
68+
69+// CanTrustForwardedHeaders returns whether forwarded headers should be
70+// processed for this request.
71+func (s *RequestScope) CanTrustForwardedHeaders(req *http.Request) bool {
72+ if s == nil || req == nil || !s.ReverseProxy || s.TrustedProxies == nil {
73+ return false
74+ }
75+
76+ if isUnixSocketRemoteAddr(req.RemoteAddr) {
77+ return true
78+ }
79+
80+ remoteIP := parseRemoteAddrIP(req.RemoteAddr)
81+ if remoteIP == nil {
82+ return false
83+ }
84+
85+ return s.TrustedProxies.Has(remoteIP)
86+}
87+
88+func parseRemoteAddrIP(remoteAddr string) net.IP {
89+ if remoteAddr == "" {
90+ return nil
91+ }
92+
93+ if ip := net.ParseIP(remoteAddr); ip != nil {
94+ return ip
95+ }
96+
97+ host, _, err := net.SplitHostPort(remoteAddr)
98+ if err != nil {
99+ return nil
100+ }
101+
102+ return net.ParseIP(host)
103+}
104+
105+func isUnixSocketRemoteAddr(remoteAddr string) bool {
106+ return remoteAddr == "@" || strings.HasPrefix(remoteAddr, "/")
107+}
pkg/apis/middleware/scope_test.go+34
@@ -4,6 +4,7 @@ import (
44 "net/http"
55
66 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
78 . "github.com/onsi/ginkgo/v2"
89 . "github.com/onsi/gomega"
910)
@@ -53,4 +54,37 @@ var _ = Describe("Scope Suite", func() {
5354 })
5455 })
5556 })
57+
58+ Context("CanTrustForwardedHeaders", func() {
59+ var request *http.Request
60+ var scope *middleware.RequestScope
61+
62+ BeforeEach(func() {
63+ var err error
64+ request, err = http.NewRequest("", "http://127.0.0.1/", nil)
65+ Expect(err).ToNot(HaveOccurred())
66+
67+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
68+ Expect(err).ToNot(HaveOccurred())
69+ scope = &middleware.RequestScope{
70+ ReverseProxy: true,
71+ TrustedProxies: trustedProxies,
72+ }
73+ })
74+
75+ It("returns true for a trusted remote address", func() {
76+ request.RemoteAddr = "127.0.0.1:4180"
77+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeTrue())
78+ })
79+
80+ It("returns false for an untrusted remote address", func() {
81+ request.RemoteAddr = "192.0.2.10:4180"
82+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeFalse())
83+ })
84+
85+ It("returns true for unix socket callers", func() {
86+ request.RemoteAddr = "@"
87+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeTrue())
88+ })
89+ })
5690})
pkg/apis/options/load.go+1−1
@@ -145,7 +145,7 @@ func loadAndSubstituteEnvs(configFileName string) ([]byte, error) {
145145func registerFlags(v *viper.Viper, prefix string, flagSet *pflag.FlagSet, options interface{}) error {
146146 val := reflect.ValueOf(options)
147147 var typ reflect.Type
148− if val.Kind() == reflect.Ptr {
148+ if val.Kind() == reflect.Pointer {
149149 typ = val.Elem().Type()
150150 } else {
151151 typ = val.Type()
pkg/apis/options/load_test.go+2−2
@@ -282,7 +282,7 @@ var _ = Describe("Load", func() {
282282 Entry("with an invalid config file", &testOptionsTableInput{
283283 configFile: []byte(`slice_option = foo`),
284284 flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
285− expectedErr: fmt.Errorf("unable to load config file: While parsing config: toml: expected 'false'"),
285+ expectedErr: fmt.Errorf("unable to load config file: While parsing config: toml: expected keyword \"false\""),
286286 expectedOutput: &TestOptions{},
287287 }),
288288 Entry("with an invalid flagset", &testOptionsTableInput{
@@ -329,7 +329,7 @@ var _ = Describe("Load", func() {
329329 Entry("with an unknown option in the config file", &testOptionsTableInput{
330330 configFile: []byte(`unknown_option="foo"`),
331331 flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
332− expectedErr: fmt.Errorf("error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_option"),
332+ expectedErr: fmt.Errorf("error unmarshalling config: decoding failed due to the following error(s):\n\n'options.TestOptions' has invalid keys: unknown_option"),
333333 // Viper will unmarshal before returning the error, so this is the default output
334334 expectedOutput: &TestOptions{
335335 StringOption: "default",
pkg/apis/options/options.go+2
@@ -24,6 +24,7 @@ type Options struct {
2424 ReadyPath string `flag:"ready-path" cfg:"ready_path"`
2525 ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
2626 RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
27+ TrustedProxyIPs []string `flag:"trusted-proxy-ip" cfg:"trusted_proxy_ips"`
2728 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
2829 ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
2930 RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
@@ -119,6 +120,7 @@ func NewFlagSet() *pflag.FlagSet {
119120
120121 flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
121122 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)")
123+ 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.")
122124 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.")
123125 flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
124126 flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
pkg/apis/options/providers.go+6
@@ -151,6 +151,9 @@ const (
151151 // ADFSProvider is the provider type for ADFS
152152 ADFSProvider ProviderType = "adfs"
153153
154+ // AtlassianProvider is the provider type for Atlassian Cloud
155+ AtlassianProvider ProviderType = "atlassian"
156+
154157 // AzureProvider is the provider type for Azure
155158 AzureProvider ProviderType = "azure"
156159
@@ -166,6 +169,9 @@ const (
166169 // DigitalOceanProvider is the provider type for DigitalOcean
167170 DigitalOceanProvider ProviderType = "digitalocean"
168171
172+ // DTTSSOProvider is the provider type for the DTT SSO
173+ DTTSSOProvider ProviderType = "dtt-sso"
174+
169175 // FacebookProvider is the provider type for Facebook
170176 FacebookProvider ProviderType = "facebook"
171177
pkg/apis/options/upstreams.go+7−5
@@ -62,12 +62,14 @@ type Upstream struct {
6262
6363 // Path is used to map requests to the upstream server.
6464 // The closest match will take precedence and all Paths must be unique.
65− // Path can also take a pattern when used with RewriteTarget.
66− // Path segments can be captured and matched using regular experessions.
65+ // Without RewriteTarget, Path follows gorilla/mux route syntax.
6766 // Eg:
68− // - `^/foo$`: Match only the explicit path `/foo`
69− // - `^/bar/$`: Match any path prefixed with `/bar/`
70− // - `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget
67+ // - `/foo`: Match only the explicit path `/foo`
68+ // - `/bar/`: Match any path prefixed with `/bar/`
69+ // - `/baz/{path:.*}`: Match any path prefixed with `/baz/`
70+ // When RewriteTarget is set, Path is treated as a regular expression.
71+ // Eg: `^/baz/(.*)$` matches any path prefixed with `/baz` and captures the
72+ // remaining path for use with RewriteTarget.
7173 Path string `yaml:"path,omitempty"`
7274
7375 // RewriteTarget allows users to rewrite the request path before it is sent to
pkg/apis/sessions/session_state.go+2
@@ -127,6 +127,8 @@ func (s *SessionState) String() string {
127127 }
128128 if s.RefreshToken != "" {
129129 o += " refresh_token:true"
130+ } else {
131+ o += " refresh_token:false"
130132 }
131133 if len(s.Groups) > 0 {
132134 o += fmt.Sprintf(" groups:%v", s.Groups)
pkg/apis/sessions/session_state_test.go+34−5
@@ -4,6 +4,7 @@ import (
44 "crypto/rand"
55 "fmt"
66 "io"
7+ "strings"
78 "testing"
89 "time"
910
@@ -57,7 +58,7 @@ func TestString(t *testing.T) {
5758 User: "some.user",
5859 PreferredUsername: "preferred.user",
5960 },
60− expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user}",
61+ expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user refresh_token:false}",
6162 },
6263 {
6364 name: "Full Session",
@@ -81,7 +82,7 @@ func TestString(t *testing.T) {
8182 PreferredUsername: "preferred.user",
8283 CreatedAt: &created,
8384 },
84− expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user created:2000-01-01 00:00:00 +0000 UTC}",
85+ expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user created:2000-01-01 00:00:00 +0000 UTC refresh_token:false}",
8586 },
8687 {
8788 name: "With an ExpiresOn",
@@ -91,7 +92,7 @@ func TestString(t *testing.T) {
9192 PreferredUsername: "preferred.user",
9293 ExpiresOn: &expires,
9394 },
94− expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user expires:2000-01-01 01:00:00 +0000 UTC}",
95+ expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user expires:2000-01-01 01:00:00 +0000 UTC refresh_token:false}",
9596 },
9697 {
9798 name: "With an AccessToken",
@@ -101,7 +102,7 @@ func TestString(t *testing.T) {
101102 PreferredUsername: "preferred.user",
102103 AccessToken: "access.token",
103104 },
104− expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user token:true}",
105+ expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user token:true refresh_token:false}",
105106 },
106107 {
107108 name: "With an IDToken",
@@ -111,7 +112,7 @@ func TestString(t *testing.T) {
111112 PreferredUsername: "preferred.user",
112113 IDToken: "id.token",
113114 },
114− expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user id_token:true}",
115+ expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user id_token:true refresh_token:false}",
115116 },
116117 {
117118 name: "With a RefreshToken",
@@ -353,3 +354,31 @@ func TestGetClaim(t *testing.T) {
353354 })
354355 }
355356}
357+
358+func TestSessionState_String_RefreshTokenFalse(t *testing.T) {
359+ session := &SessionState{
360+ Email: "test@example.com",
361+ User: "testuser",
362+ // No RefreshToken set
363+ }
364+
365+ result := session.String()
366+
367+ if !strings.Contains(result, "refresh_token:false") {
368+ t.Errorf("Expected 'refresh_token:false' in output, got: %s", result)
369+ }
370+}
371+
372+func TestSessionState_String_RefreshTokenTrue(t *testing.T) {
373+ session := &SessionState{
374+ Email: "test@example.com",
375+ User: "testuser",
376+ RefreshToken: "some-token",
377+ }
378+
379+ result := session.String()
380+
381+ if !strings.Contains(result, "refresh_token:true") {
382+ t.Errorf("Expected 'refresh_token:true' in output, got: %s", result)
383+ }
384+}
pkg/app/redirect/director_test.go+33−2
@@ -4,6 +4,7 @@ import (
44 "net/http"
55
66 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
78 . "github.com/onsi/ginkgo/v2"
89 . "github.com/onsi/gomega"
910)
@@ -33,9 +34,16 @@ var _ = Describe("Director Suite", func() {
3334 req.Header.Add(header, value)
3435 }
3536 }
36− req = middleware.AddRequestScope(req, &middleware.RequestScope{
37+ scope := &middleware.RequestScope{
3738 ReverseProxy: in.reverseProxy,
38− })
39+ }
40+ if in.reverseProxy {
41+ req.RemoteAddr = "127.0.0.1:4180"
42+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
43+ Expect(err).ToNot(HaveOccurred())
44+ scope.TrustedProxies = trustedProxies
45+ }
46+ req = middleware.AddRequestScope(req, scope)
3947
4048 redirect, err := appDirector.GetRedirect(req)
4149 Expect(err).ToNot(HaveOccurred())
@@ -174,4 +182,27 @@ var _ = Describe("Director Suite", func() {
174182 expectedRedirect: "https://a-service.example.com/foo/bar",
175183 }),
176184 )
185+
186+ It("ignores forwarded headers from an untrusted remote address", func() {
187+ appDirector := NewAppDirector(AppDirectorOpts{
188+ ProxyPrefix: testProxyPrefix,
189+ Validator: testValidator(true),
190+ })
191+
192+ req, _ := http.NewRequest("GET", "https://oauth.example.com/foo?bar", nil)
193+ req.RemoteAddr = "192.0.2.10:4180"
194+ req.Header.Add("X-Forwarded-Proto", "https")
195+ req.Header.Add("X-Forwarded-Host", "a-service.example.com")
196+ req.Header.Add("X-Forwarded-Uri", fooBar)
197+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
198+ Expect(err).ToNot(HaveOccurred())
199+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
200+ ReverseProxy: true,
201+ TrustedProxies: trustedProxies,
202+ })
203+
204+ redirect, err := appDirector.GetRedirect(req)
205+ Expect(err).ToNot(HaveOccurred())
206+ Expect(redirect).To(Equal("/foo?bar"))
207+ })
177208})
pkg/authentication/hmacauth/hmacauth_test.go+1−1
@@ -286,7 +286,7 @@ func TestSendAuthenticatedPostRequestToServer(t *testing.T) {
286286 upstream := httptest.NewServer(
287287 http.HandlerFunc(authenticator.Authenticate))
288288
289− req, err := http.NewRequest("POST", upstream.URL+"/foo/bar",
289+ req, err := http.NewRequest(http.MethodPost, upstream.URL+"/foo/bar",
290290 io.NopCloser(&fakeNetConn{reqBody: payload}))
291291 if err != nil {
292292 panic(err)
pkg/cookies/cookies_test.go+7−1
@@ -6,6 +6,7 @@ import (
66 "time"
77
88 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910 . "github.com/onsi/ginkgo/v2"
1011 . "github.com/onsi/gomega"
1112)
@@ -30,8 +31,13 @@ var _ = Describe("Cookie Tests", func() {
3031
3132 if in.xForwardedHost != "" {
3233 req.Header.Add("X-Forwarded-Host", in.xForwardedHost)
34+ req.RemoteAddr = "127.0.0.1:4180"
35+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
36+ Expect(err).ToNot(HaveOccurred())
37+
3338 req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
34− ReverseProxy: true,
39+ ReverseProxy: true,
40+ TrustedProxies: trustedProxies,
3541 })
3642 }
3743
pkg/ip/parse_ip_net.go+16
@@ -1,6 +1,7 @@
11package ip
22
33import (
4+ "fmt"
45 "net"
56 "strings"
67)
@@ -37,3 +38,18 @@ func ParseIPNet(s string) *net.IPNet {
3738 return ipNet
3839 }
3940}
41+
42+func ParseNetSet(ipStrs []string) (*NetSet, error) {
43+ netSet := NewNetSet()
44+
45+ for _, ipStr := range ipStrs {
46+ ipNet := ParseIPNet(ipStr)
47+ if ipNet == nil {
48+ return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
49+ }
50+
51+ netSet.AddIPNet(*ipNet)
52+ }
53+
54+ return netSet, nil
55+}
pkg/ip/parse_ip_net_test.gonew file+118
@@ -0,0 +1,118 @@
1+package ip
2+
3+import (
4+ "net"
5+ "testing"
6+
7+ "github.com/stretchr/testify/assert"
8+)
9+
10+func TestParseIPNet(t *testing.T) {
11+ tests := []struct {
12+ name string
13+ input string
14+ expectedIP net.IP
15+ expectedMask net.IPMask
16+ }{
17+ {
18+ name: "ipv4 address",
19+ input: "127.0.0.1",
20+ expectedIP: net.ParseIP("127.0.0.1"),
21+ expectedMask: net.CIDRMask(32, 32),
22+ },
23+ {
24+ name: "ipv6 address",
25+ input: "::1",
26+ expectedIP: net.ParseIP("::1"),
27+ expectedMask: net.CIDRMask(128, 128),
28+ },
29+ {
30+ name: "ipv4 cidr",
31+ input: "10.0.0.0/24",
32+ expectedIP: net.ParseIP("10.0.0.0"),
33+ expectedMask: net.CIDRMask(24, 32),
34+ },
35+ {
36+ name: "ipv6 cidr",
37+ input: "2001:db8::/64",
38+ expectedIP: net.ParseIP("2001:db8::"),
39+ expectedMask: net.CIDRMask(64, 128),
40+ },
41+ }
42+
43+ for _, test := range tests {
44+ t.Run(test.name, func(t *testing.T) {
45+ ipNet := ParseIPNet(test.input)
46+
47+ assert.NotNil(t, ipNet)
48+ if ipNet == nil {
49+ return
50+ }
51+
52+ assert.True(t, test.expectedIP.Equal(ipNet.IP))
53+ assert.Equal(t, test.expectedMask, ipNet.Mask)
54+ })
55+ }
56+}
57+
58+func TestParseIPNetRejectsInvalidNetworks(t *testing.T) {
59+ tests := []struct {
60+ name string
61+ input string
62+ }{
63+ {
64+ name: "invalid ip",
65+ input: "not-an-ip",
66+ },
67+ {
68+ name: "ipv4 cidr with host bits set",
69+ input: "10.0.0.1/24",
70+ },
71+ {
72+ name: "ipv6 cidr with host bits set",
73+ input: "2001:db8::1/64",
74+ },
75+ {
76+ name: "invalid cidr mask",
77+ input: "10.0.0.0/33",
78+ },
79+ }
80+
81+ for _, test := range tests {
82+ t.Run(test.name, func(t *testing.T) {
83+ assert.Nil(t, ParseIPNet(test.input))
84+ })
85+ }
86+}
87+
88+func TestParseNetSet(t *testing.T) {
89+ netSet, err := ParseNetSet([]string{
90+ "127.0.0.1",
91+ "10.0.0.0/24",
92+ "::1",
93+ "2001:db8::/64",
94+ })
95+
96+ assert.NoError(t, err)
97+ assert.NotNil(t, netSet)
98+ if netSet == nil {
99+ return
100+ }
101+
102+ assert.True(t, netSet.Has(net.ParseIP("127.0.0.1")))
103+ assert.True(t, netSet.Has(net.ParseIP("10.0.0.55")))
104+ assert.True(t, netSet.Has(net.ParseIP("::1")))
105+ assert.True(t, netSet.Has(net.ParseIP("2001:db8::abcd")))
106+
107+ assert.False(t, netSet.Has(net.ParseIP("127.0.0.2")))
108+ assert.False(t, netSet.Has(net.ParseIP("10.0.1.1")))
109+ assert.False(t, netSet.Has(net.ParseIP("::2")))
110+ assert.False(t, netSet.Has(net.ParseIP("2001:db9::1")))
111+}
112+
113+func TestParseNetSetReturnsErrorForInvalidNetwork(t *testing.T) {
114+ netSet, err := ParseNetSet([]string{"127.0.0.1", "10.0.0.1/24"})
115+
116+ assert.Nil(t, netSet)
117+ assert.EqualError(t, err, "could not parse IP network (10.0.0.1/24)")
118+}
pkg/ip/realclientip.go+7−1
@@ -73,6 +73,12 @@ func GetClientIP(p ipapi.RealClientIPParser, req *http.Request) (net.IP, error)
7373
7474// getRemoteIP obtains the IP of the low-level connected network host
7575func getRemoteIP(req *http.Request) (net.IP, error) {
76+ // Unix domain sockets set RemoteAddr to "@" which has no meaningful IP.
77+ // https://github.com/golang/go/blob/0fa53e41f122b1661d0678a6d36d71b7b5ad031d/src/syscall/syscall_linux.go#L506-L511
78+ if req.RemoteAddr == "@" {
79+ return nil, nil
80+ }
81+
7682 //revive:disable:indent-error-flow
7783 if ipStr, _, err := net.SplitHostPort(req.RemoteAddr); err != nil {
7884 return nil, fmt.Errorf("unable to get ip and port from http.RemoteAddr (%s)", req.RemoteAddr)
@@ -94,7 +100,7 @@ func GetClientString(p ipapi.RealClientIPParser, req *http.Request, full bool) (
94100 }
95101
96102 var remoteIPStr string
97− if remoteIP, err := getRemoteIP(req); err == nil {
103+ if remoteIP, err := getRemoteIP(req); err == nil && remoteIP != nil {
98104 remoteIPStr = remoteIP.String()
99105 }
100106
pkg/ip/realclientip_test.go+4
@@ -112,6 +112,8 @@ func TestGetRemoteIP(t *testing.T) {
112112 errString string
113113 expectedIP net.IP
114114 }{
115+ // Unix domain sockets set RemoteAddr to "@"
116+ {"@", "", nil},
115117 {"", "unable to get ip and port from http.RemoteAddr ()", nil},
116118 {"nil", "unable to get ip and port from http.RemoteAddr (nil)", nil},
117119 {"235.28.129.186", "unable to get ip and port from http.RemoteAddr (235.28.129.186)", nil},
@@ -155,6 +157,8 @@ func TestGetClientString(t *testing.T) {
155157 }{
156158 // Should fail quietly, only printing warnings to the log
157159 {nil, "", "", "", ""},
160+ // Unix domain socket — no IP available
161+ {nil, "@", "", "", ""},
158162 {p, "127.0.0.1:11950", "", "127.0.0.1", "127.0.0.1"},
159163 {p, "[::1]:28660", "99.103.56.12", "99.103.56.12", "::1 (99.103.56.12)"},
160164 {nil, "10.254.244.165:62750", "", "10.254.244.165", "10.254.244.165"},
pkg/middleware/healthcheck.go+7−4
@@ -43,10 +43,13 @@ func healthCheck(paths, userAgents []string, next http.Handler) http.Handler {
4343
4444func isHealthCheckRequest(paths, userAgents map[string]struct{}, req *http.Request) bool {
4545 if _, ok := paths[req.URL.EscapedPath()]; ok {
46− return true
47− }
48− if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
49− return true
46+ if len(userAgents) == 0 {
47+ return true
48+ }
49+
50+ if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
51+ return true
52+ }
5053 }
5154 return false
5255}
pkg/middleware/healthcheck_test.go+31−9
@@ -45,6 +45,16 @@ var _ = Describe("HealthCheck suite", func() {
4545 healthCheckPaths: []string{"/ping"},
4646 healthCheckUserAgents: []string{"hc/1.0"},
4747 requestString: "http://example.com/ping",
48+ headers: map[string]string{
49+ "User-Agent": "hc/1.0",
50+ },
51+ expectedStatus: 200,
52+ expectedBody: "OK",
53+ }),
54+ Entry("when requesting the healthcheck path with no health check user agents configured", &requestTableInput{
55+ healthCheckPaths: []string{"/ping"},
56+ healthCheckUserAgents: []string{},
57+ requestString: "http://example.com/ping",
4858 headers: map[string]string{},
4959 expectedStatus: 200,
5060 expectedBody: "OK",
@@ -85,15 +95,25 @@ var _ = Describe("HealthCheck suite", func() {
8595 expectedStatus: 404,
8696 expectedBody: "404 page not found\n",
8797 }),
88− Entry("with a request from the health check user agent", &requestTableInput{
98+ Entry("with a request from the health check user agent on a non-healthcheck path", &requestTableInput{
8999 healthCheckPaths: []string{"/ping"},
90100 healthCheckUserAgents: []string{"hc/1.0"},
91101 requestString: "http://example.com/abc",
92102 headers: map[string]string{
93103 "User-Agent": "hc/1.0",
94104 },
95− expectedStatus: 200,
96− expectedBody: "OK",
105+ expectedStatus: 404,
106+ expectedBody: "404 page not found\n",
107+ }),
108+ Entry("when an auth_request endpoint receives the configured health check user agent", &requestTableInput{
109+ healthCheckPaths: []string{"/ping"},
110+ healthCheckUserAgents: []string{"GoogleHC/1.0"},
111+ requestString: "http://example.com/oauth2/auth",
112+ headers: map[string]string{
113+ "User-Agent": "GoogleHC/1.0",
114+ },
115+ expectedStatus: 404,
116+ expectedBody: "404 page not found\n",
97117 }),
98118 Entry("when a blank string is configured as a health check agent and a request has no user agent", &requestTableInput{
99119 healthCheckPaths: []string{"/ping"},
@@ -107,9 +127,11 @@ var _ = Describe("HealthCheck suite", func() {
107127 healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
108128 healthCheckUserAgents: []string{"hc/1.0"},
109129 requestString: "http://example.com/readiness_check",
110− headers: map[string]string{},
111− expectedStatus: 200,
112− expectedBody: "OK",
130+ headers: map[string]string{
131+ "User-Agent": "hc/1.0",
132+ },
133+ expectedStatus: 200,
134+ expectedBody: "OK",
113135 }),
114136 Entry("with multiple paths, request none of the healthcheck paths", &requestTableInput{
115137 healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
@@ -121,15 +143,15 @@ var _ = Describe("HealthCheck suite", func() {
121143 expectedStatus: 404,
122144 expectedBody: "404 page not found\n",
123145 }),
124− Entry("with multiple user agents, request from a health check user agent", &requestTableInput{
146+ Entry("with multiple user agents, request from a health check user agent on a non-healthcheck path", &requestTableInput{
125147 healthCheckPaths: []string{"/ping"},
126148 healthCheckUserAgents: []string{"hc/1.0", "GoogleHC/1.0"},
127149 requestString: "http://example.com/abc",
128150 headers: map[string]string{
129151 "User-Agent": "GoogleHC/1.0",
130152 },
131− expectedStatus: 200,
132− expectedBody: "OK",
153+ expectedStatus: 404,
154+ expectedBody: "404 page not found\n",
133155 }),
134156 Entry("with multiple user agents, request from none of the health check user agents", &requestTableInput{
135157 healthCheckPaths: []string{"/ping"},
pkg/middleware/redirect_to_https_test.go+15
@@ -6,6 +6,7 @@ import (
66 "net/http/httptest"
77
88 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910 . "github.com/onsi/ginkgo/v2"
1011 . "github.com/onsi/gomega"
1112)
@@ -39,6 +40,10 @@ var _ = Describe("RedirectToHTTPS suite", func() {
3940 scope := &middlewareapi.RequestScope{
4041 ReverseProxy: in.reverseProxy,
4142 }
43+ if in.reverseProxy {
44+ req.RemoteAddr = "127.0.0.1:4180"
45+ scope.TrustedProxies = newRedirectTrustedProxySet("127.0.0.1")
46+ }
4247 req = middlewareapi.AddRequestScope(req, scope)
4348
4449 rw := httptest.NewRecorder()
@@ -207,3 +212,13 @@ var _ = Describe("RedirectToHTTPS suite", func() {
207212 }),
208213 )
209214})
215+
216+func newRedirectTrustedProxySet(cidrs ...string) *ip.NetSet {
217+ set := ip.NewNetSet()
218+ for _, cidr := range cidrs {
219+ ipNet := ip.ParseIPNet(cidr)
220+ Expect(ipNet).ToNot(BeNil())
221+ set.AddIPNet(*ipNet)
222+ }
223+ return set
224+}
pkg/middleware/scope.go+5−3
@@ -6,14 +6,16 @@ import (
66 "github.com/google/uuid"
77 "github.com/justinas/alice"
88 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910)
1011
11−func NewScope(reverseProxy bool, idHeader string) alice.Constructor {
12+func NewScope(reverseProxy bool, idHeader string, trustedProxies *ip.NetSet) alice.Constructor {
1213 return func(next http.Handler) http.Handler {
1314 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1415 scope := &middlewareapi.RequestScope{
15− ReverseProxy: reverseProxy,
16− RequestID: genRequestID(req, idHeader),
16+ ReverseProxy: reverseProxy,
17+ TrustedProxies: trustedProxies,
18+ RequestID: genRequestID(req, idHeader),
1719 }
1820 req = middlewareapi.AddRequestScope(req, scope)
1921 next.ServeHTTP(rw, req)
pkg/middleware/scope_test.go+18−4
@@ -6,6 +6,7 @@ import (
66
77 "github.com/google/uuid"
88 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910 . "github.com/onsi/ginkgo/v2"
1011 . "github.com/onsi/gomega"
1112)
@@ -32,7 +33,7 @@ var _ = Describe("Scope Suite", func() {
3233
3334 Context("ReverseProxy is false", func() {
3435 BeforeEach(func() {
35− handler := NewScope(false, testRequestHeader)(
36+ handler := NewScope(false, testRequestHeader, nil)(
3637 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3738 nextRequest = r
3839 w.WriteHeader(200)
@@ -60,8 +61,15 @@ var _ = Describe("Scope Suite", func() {
6061 })
6162
6263 Context("ReverseProxy is true", func() {
64+ var trustedProxies *ip.NetSet
65+
6366 BeforeEach(func() {
64− handler := NewScope(true, testRequestHeader)(
67+ var err error
68+
69+ trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
70+ Expect(err).ToNot(HaveOccurred())
71+
72+ handler := NewScope(true, testRequestHeader, trustedProxies)(
6573 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6674 nextRequest = r
6775 w.WriteHeader(200)
@@ -74,12 +82,18 @@ var _ = Describe("Scope Suite", func() {
7482 Expect(scope).ToNot(BeNil())
7583 Expect(scope.ReverseProxy).To(BeTrue())
7684 })
85+
86+ It("stores the trusted proxies on the scope", func() {
87+ scope := middlewareapi.GetRequestScope(nextRequest)
88+ Expect(scope).ToNot(BeNil())
89+ Expect(scope.TrustedProxies).To(BeIdenticalTo(trustedProxies))
90+ })
7791 })
7892
7993 Context("Request ID header is present", func() {
8094 BeforeEach(func() {
8195 request.Header.Add(testRequestHeader, testRequestID)
82− handler := NewScope(false, testRequestHeader)(
96+ handler := NewScope(false, testRequestHeader, nil)(
8397 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
8498 nextRequest = r
8599 w.WriteHeader(200)
@@ -97,7 +111,7 @@ var _ = Describe("Scope Suite", func() {
97111 BeforeEach(func() {
98112 uuid.SetRand(mockRand{})
99113
100− handler := NewScope(true, testRequestHeader)(
114+ handler := NewScope(true, testRequestHeader, nil)(
101115 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102116 nextRequest = r
103117 w.WriteHeader(200)
pkg/middleware/stored_session.go+46−2
@@ -5,6 +5,7 @@ import (
55 "errors"
66 "fmt"
77 "net/http"
8+ "strings"
89 "time"
910
1011 "github.com/justinas/alice"
@@ -31,6 +32,33 @@ const (
3132 sessionRefreshRetryPeriod = 10 * time.Millisecond
3233)
3334
35+// isFatalRefreshError checks if a refresh error indicates a revoked or
36+// non-existent session that should be immediately invalidated.
37+// Fatal errors indicate the session is no longer valid at the provider level.
38+// Non-fatal errors (network issues, timeouts) should not invalidate the session.
39+//
40+// Only checks standard OAuth2 error codes (RFC 6749 Section 5.2).
41+// Does NOT check error_description strings as they are optional and provider-specific.
42+func isFatalRefreshError(err error) bool {
43+ if err == nil {
44+ return false
45+ }
46+
47+ // Only check standard OAuth2 error codes (RFC 6749 Section 5.2)
48+ // Do NOT check error_description strings as they are optional and provider-specific
49+ fatalErrors := []string{
50+ "invalid_grant", // refresh token revoked, expired, or session terminated
51+ "invalid_client", // client credentials no longer valid
52+ }
53+
54+ for _, fe := range fatalErrors {
55+ if strings.Contains(err.Error(), fe) {
56+ return true
57+ }
58+ }
59+ return false
60+}
61+
3462// StoredSessionLoaderOptions contains all of the requirements to construct
3563// a stored session loader.
3664// All options must be provided.
@@ -188,9 +216,24 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req
188216 // We are holding the lock and the session needs a refresh
189217 logger.Printf("Refreshing session - User: %s; SessionAge: %s", session.User, session.Age())
190218 if err := s.refreshSession(rw, req, session); err != nil {
191− // If a preemptive refresh fails, we still keep the session
192− // if validateSession succeeds.
193219 logger.Errorf("Unable to refresh session: %v", err)
220+
221+ // Check if this is a fatal error that indicates the session is revoked
222+ // or no longer valid at the provider level
223+ if isFatalRefreshError(err) {
224+ logger.Printf("Fatal refresh error detected (session revoked or invalid), clearing session for user: %s", session.User)
225+
226+ // Clear the session from storage (Redis) and remove the cookie
227+ if err := s.store.Clear(rw, req); err != nil {
228+ logger.Errorf("failed clearing session: %v", err)
229+ }
230+
231+ // Return error immediately to force re-authentication
232+ return fmt.Errorf("session invalidated due to fatal refresh error: %w", err)
233+ }
234+
235+ // For non-fatal errors (network issues, timeouts), keep the session
236+ // and let validateSession determine if it's still usable
194237 }
195238
196239 // Validate all sessions after any Redeem/Refresh operation (fail or success)
@@ -222,6 +265,7 @@ func (s *storedSessionLoader) refreshSession(rw http.ResponseWriter, req *http.R
222265
223266 // Session not refreshed, nothing to persist.
224267 if !refreshed {
268+ logger.Printf("Session not refreshed - User: %s; no refresh token available or provider returned false", session.User)
225269 return nil
226270 }
227271
pkg/middleware/stored_session_test.go+51
@@ -7,6 +7,7 @@ import (
77 "net/http"
88 "net/http/httptest"
99 "sync"
10+ "testing"
1011 "time"
1112
1213 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
801802func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
802803 return nil
803804}
805+
806+// TestIsFatalRefreshError tests the isFatalRefreshError function to ensure
807+// it correctly identifies fatal OAuth2 errors that should invalidate a session.
808+func TestIsFatalRefreshError(t *testing.T) {
809+ tests := []struct {
810+ name string
811+ err error
812+ expected bool
813+ }{
814+ {
815+ name: "nil error",
816+ err: nil,
817+ expected: false,
818+ },
819+ {
820+ name: "invalid_grant error",
821+ err: fmt.Errorf("failed to get token: oauth2: \"invalid_grant\" \"Session not active\""),
822+ expected: true,
823+ },
824+ {
825+ name: "invalid_client error",
826+ err: fmt.Errorf("invalid_client: client not found"),
827+ expected: true,
828+ },
829+ {
830+ name: "network timeout - not fatal",
831+ err: fmt.Errorf("Post \"https://keycloak/token\": dial tcp: connect: connection refused"),
832+ expected: false,
833+ },
834+ {
835+ name: "server error - not fatal",
836+ err: fmt.Errorf("unexpected status code 500"),
837+ expected: false,
838+ },
839+ {
840+ name: "generic refresh error - not fatal",
841+ err: fmt.Errorf("error refreshing tokens: context deadline exceeded"),
842+ expected: false,
843+ },
844+ }
845+
846+ for _, tt := range tests {
847+ t.Run(tt.name, func(t *testing.T) {
848+ result := isFatalRefreshError(tt.err)
849+ if result != tt.expected {
850+ t.Errorf("isFatalRefreshError(%v) = %v, want %v", tt.err, result, tt.expected)
851+ }
852+ })
853+ }
854+}
pkg/requests/builder_test.go+15−15
@@ -31,7 +31,7 @@ var _ = Describe("Builder suite", func() {
3131
3232 Context("with a basic request", func() {
3333 assertSuccessfulRequest(getBuilder, testHTTPRequest{
34− Method: "GET",
34+ Method: http.MethodGet,
3535 Header: baseHeaders,
3636 Body: []byte{},
3737 RequestURI: "/json/path",
@@ -52,7 +52,7 @@ var _ = Describe("Builder suite", func() {
5252 })
5353
5454 assertSuccessfulRequest(getBuilder, testHTTPRequest{
55− Method: "GET",
55+ Method: http.MethodGet,
5656 Header: baseHeaders,
5757 Body: []byte{},
5858 RequestURI: "/json/path",
@@ -78,7 +78,7 @@ var _ = Describe("Builder suite", func() {
7878 })
7979
8080 assertSuccessfulRequest(getBuilder, testHTTPRequest{
81− Method: "GET",
81+ Method: http.MethodGet,
8282 Header: header,
8383 Body: []byte(body),
8484 RequestURI: "/json/path",
@@ -93,11 +93,11 @@ var _ = Describe("Builder suite", func() {
9393
9494 BeforeEach(func() {
9595 buf := bytes.NewBuffer([]byte(body))
96− b = b.WithMethod("POST").WithBody(buf)
96+ b = b.WithMethod(http.MethodPost).WithBody(buf)
9797 })
9898
9999 assertSuccessfulRequest(getBuilder, testHTTPRequest{
100− Method: "POST",
100+ Method: http.MethodPost,
101101 Header: header,
102102 Body: []byte(body),
103103 RequestURI: "/json/path",
@@ -109,11 +109,11 @@ var _ = Describe("Builder suite", func() {
109109 header.Set("Content-Length", "0")
110110
111111 BeforeEach(func() {
112− b = b.WithMethod("POST")
112+ b = b.WithMethod(http.MethodPost)
113113 })
114114
115115 assertSuccessfulRequest(getBuilder, testHTTPRequest{
116− Method: "POST",
116+ Method: http.MethodPost,
117117 Header: header,
118118 Body: []byte{},
119119 RequestURI: "/json/path",
@@ -122,11 +122,11 @@ var _ = Describe("Builder suite", func() {
122122
123123 Context("OPTIONS", func() {
124124 BeforeEach(func() {
125− b = b.WithMethod("OPTIONS")
125+ b = b.WithMethod(http.MethodOptions)
126126 })
127127
128128 assertSuccessfulRequest(getBuilder, testHTTPRequest{
129− Method: "OPTIONS",
129+ Method: http.MethodOptions,
130130 Header: baseHeaders,
131131 Body: []byte{},
132132 RequestURI: "/json/path",
@@ -152,7 +152,7 @@ var _ = Describe("Builder suite", func() {
152152 })
153153
154154 assertSuccessfulRequest(getBuilder, testHTTPRequest{
155− Method: "GET",
155+ Method: http.MethodGet,
156156 Header: header,
157157 Body: []byte{},
158158 RequestURI: "/json/path",
@@ -170,7 +170,7 @@ var _ = Describe("Builder suite", func() {
170170 })
171171
172172 assertSuccessfulRequest(getBuilder, testHTTPRequest{
173− Method: "GET",
173+ Method: http.MethodGet,
174174 Header: replacementHeaders,
175175 Body: []byte{},
176176 RequestURI: "/json/path",
@@ -190,7 +190,7 @@ var _ = Describe("Builder suite", func() {
190190 })
191191
192192 assertSuccessfulRequest(getBuilder, testHTTPRequest{
193− Method: "GET",
193+ Method: http.MethodGet,
194194 Header: replacementHeaders,
195195 Body: []byte{},
196196 RequestURI: "/json/path",
@@ -205,7 +205,7 @@ var _ = Describe("Builder suite", func() {
205205 })
206206
207207 assertSuccessfulRequest(getBuilder, testHTTPRequest{
208− Method: "GET",
208+ Method: http.MethodGet,
209209 Header: header,
210210 Body: []byte{},
211211 RequestURI: "/json/path",
@@ -219,12 +219,12 @@ var _ = Describe("Builder suite", func() {
219219 result := b.Do()
220220 Expect(result.Error()).ToNot(HaveOccurred())
221221
222− b.WithMethod("POST")
222+ b.WithMethod(http.MethodPost)
223223 })
224224
225225 Context("should not redo the request", func() {
226226 assertSuccessfulRequest(getBuilder, testHTTPRequest{
227− Method: "GET",
227+ Method: http.MethodGet,
228228 Header: baseHeaders,
229229 Body: []byte{},
230230 RequestURI: "/json/path",
pkg/requests/util/util.go+28−15
@@ -15,30 +15,30 @@ const (
1515)
1616
1717// GetRequestProto returns the request scheme or X-Forwarded-Proto if present
18−// and the request is proxied.
18+// and the request came from a trusted reverse proxy.
1919func GetRequestProto(req *http.Request) string {
2020 proto := req.Header.Get(XForwardedProto)
21− if !IsProxied(req) || proto == "" {
21+ if !CanTrustForwardedHeaders(req) || proto == "" {
2222 proto = req.URL.Scheme
2323 }
2424 return proto
2525}
2626
2727// GetRequestHost returns the request host header or X-Forwarded-Host if
28−// present and the request is proxied.
28+// present and the request came from a trusted reverse proxy.
2929func GetRequestHost(req *http.Request) string {
3030 host := req.Header.Get(XForwardedHost)
31− if !IsProxied(req) || host == "" {
31+ if !CanTrustForwardedHeaders(req) || host == "" {
3232 host = req.Host
3333 }
3434 return host
3535}
3636
3737// GetRequestURI return the request URI or X-Forwarded-Uri if present and the
38−// request is proxied.
38+// request came from a trusted reverse proxy.
3939func GetRequestURI(req *http.Request) string {
4040 uri := req.Header.Get(XForwardedURI)
41− if !IsProxied(req) || uri == "" {
41+ if !CanTrustForwardedHeaders(req) || uri == "" {
4242 // Use RequestURI to preserve ?query
4343 uri = req.URL.RequestURI()
4444 }
@@ -46,17 +46,29 @@ func GetRequestURI(req *http.Request) string {
4646}
4747
4848// GetRequestPath returns the request URI or X-Forwarded-Uri if present and the
49−// request is proxied but always strips the query parameters and only returns
50−// the pure path
49+// request came from a trusted reverse proxy but always strips the query
50+// parameters and fragment suffixes and only returns the pure path.
5151func GetRequestPath(req *http.Request) string {
52− uri := GetRequestURI(req)
52+ uri := stripRequestFragment(GetRequestURI(req))
5353
5454 // Parse URI and return only the path component
5555 if parsedURL, err := url.Parse(uri); err == nil {
56− return parsedURL.Path
56+ return stripRequestFragment(parsedURL.Path)
5757 }
5858
5959 // Fallback: strip query parameters manually
60+ return stripRequestQuery(uri)
61+}
62+
63+func stripRequestFragment(uri string) string {
64+ if idx := strings.Index(uri, "#"); idx != -1 {
65+ return uri[:idx]
66+ }
67+
68+ return uri
69+}
70+
71+func stripRequestQuery(uri string) string {
6072 if idx := strings.Index(uri, "?"); idx != -1 {
6173 return uri[:idx]
6274 }
@@ -64,17 +76,18 @@ func GetRequestPath(req *http.Request) string {
6476 return uri
6577}
6678
67−// IsProxied determines if a request was from a proxy based on the RequestScope
68−// ReverseProxy tracker.
69−func IsProxied(req *http.Request) bool {
79+// CanTrustForwardedHeaders determines if forwarded headers should be processed
80+// based on the RequestScope and the direct caller's address.
81+func CanTrustForwardedHeaders(req *http.Request) bool {
7082 scope := middlewareapi.GetRequestScope(req)
7183 if scope == nil {
7284 return false
7385 }
74− return scope.ReverseProxy
86+
87+ return scope.CanTrustForwardedHeaders(req)
7588}
7689
7790func IsForwardedRequest(req *http.Request) bool {
78− return IsProxied(req) &&
91+ return CanTrustForwardedHeaders(req) &&
7992 req.Host != GetRequestHost(req)
8093}
pkg/requests/util/util_test.go+74−12
@@ -6,6 +6,7 @@ import (
66 "net/http/httptest"
77
88 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1011 . "github.com/onsi/ginkgo/v2"
1112 . "github.com/onsi/gomega"
@@ -19,8 +20,13 @@ var _ = Describe("Util Suite", func() {
1920 uriNoQueryParams = "/test/endpoint"
2021 )
2122 var req *http.Request
23+ var trustedProxies *ip.NetSet
2224
2325 BeforeEach(func() {
26+ var err error
27+ trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
28+ Expect(err).ToNot(HaveOccurred())
29+
2430 req = httptest.NewRequest(
2531 http.MethodGet,
2632 fmt.Sprintf("%s://%s%s", proto, host, uriWithQueryParams),
@@ -29,7 +35,7 @@ var _ = Describe("Util Suite", func() {
2935 })
3036
3137 Context("GetRequestHost", func() {
32− Context("IsProxied is false", func() {
38+ Context("trusted forwarded headers are disabled", func() {
3339 BeforeEach(func() {
3440 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
3541 })
@@ -44,10 +50,12 @@ var _ = Describe("Util Suite", func() {
4450 })
4551 })
4652
47− Context("IsProxied is true", func() {
53+ Context("trusted forwarded headers are enabled", func() {
4854 BeforeEach(func() {
55+ req.RemoteAddr = "127.0.0.1:4180"
4956 req = middleware.AddRequestScope(req, &middleware.RequestScope{
50− ReverseProxy: true,
57+ ReverseProxy: true,
58+ TrustedProxies: trustedProxies,
5159 })
5260 })
5361
@@ -63,7 +71,7 @@ var _ = Describe("Util Suite", func() {
6371 })
6472
6573 Context("GetRequestProto", func() {
66− Context("IsProxied is false", func() {
74+ Context("trusted forwarded headers are disabled", func() {
6775 BeforeEach(func() {
6876 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
6977 })
@@ -78,10 +86,12 @@ var _ = Describe("Util Suite", func() {
7886 })
7987 })
8088
81− Context("IsProxied is true", func() {
89+ Context("trusted forwarded headers are enabled", func() {
8290 BeforeEach(func() {
91+ req.RemoteAddr = "127.0.0.1:4180"
8392 req = middleware.AddRequestScope(req, &middleware.RequestScope{
84− ReverseProxy: true,
93+ ReverseProxy: true,
94+ TrustedProxies: trustedProxies,
8595 })
8696 })
8797
@@ -97,7 +107,7 @@ var _ = Describe("Util Suite", func() {
97107 })
98108
99109 Context("GetRequestURI", func() {
100− Context("IsProxied is false", func() {
110+ Context("trusted forwarded headers are disabled", func() {
101111 BeforeEach(func() {
102112 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
103113 })
@@ -112,10 +122,12 @@ var _ = Describe("Util Suite", func() {
112122 })
113123 })
114124
115− Context("IsProxied is true", func() {
125+ Context("trusted forwarded headers are enabled", func() {
116126 BeforeEach(func() {
127+ req.RemoteAddr = "127.0.0.1:4180"
117128 req = middleware.AddRequestScope(req, &middleware.RequestScope{
118− ReverseProxy: true,
129+ ReverseProxy: true,
130+ TrustedProxies: trustedProxies,
119131 })
120132 })
121133
@@ -131,7 +143,7 @@ var _ = Describe("Util Suite", func() {
131143 })
132144
133145 Context("GetRequestPath", func() {
134− Context("IsProxied is false", func() {
146+ Context("trusted forwarded headers are disabled", func() {
135147 BeforeEach(func() {
136148 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
137149 })
@@ -140,16 +152,35 @@ var _ = Describe("Util Suite", func() {
140152 Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
141153 })
142154
155+ It("drops fragment content from a parsed request path", func() {
156+ // Simulate net/http ParseRequestURI preserving '#' in URL.Path.
157+ req.URL.Path = "/foo/secret#/bar"
158+ req.URL.RawPath = "/foo/secret%23/bar"
159+ Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
160+ })
161+
162+ It("drops fragment-like suffixes from encoded number signs", func() {
163+ req = httptest.NewRequest(
164+ http.MethodGet,
165+ fmt.Sprintf("%s://%s/foo/secret%%23/bar?query=param", proto, host),
166+ nil,
167+ )
168+ req = middleware.AddRequestScope(req, &middleware.RequestScope{})
169+ Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
170+ })
171+
143172 It("ignores X-Forwarded-Uri and returns the URI (without query params)", func() {
144173 req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
145174 Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
146175 })
147176 })
148177
149− Context("IsProxied is true", func() {
178+ Context("trusted forwarded headers are enabled", func() {
150179 BeforeEach(func() {
180+ req.RemoteAddr = "127.0.0.1:4180"
151181 req = middleware.AddRequestScope(req, &middleware.RequestScope{
152− ReverseProxy: true,
182+ ReverseProxy: true,
183+ TrustedProxies: trustedProxies,
153184 })
154185 })
155186
@@ -161,6 +192,37 @@ var _ = Describe("Util Suite", func() {
161192 req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
162193 Expect(util.GetRequestPath(req)).To(Equal("/some/other/path"))
163194 })
195+
196+ It("drops fragment-like suffixes from the X-Forwarded-Uri", func() {
197+ req.Header.Add("X-Forwarded-Uri", "/foo/secret%23/bar?query=param")
198+ Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
199+ })
200+ })
201+ })
202+
203+ Context("CanTrustForwardedHeaders", func() {
204+ It("returns false when no scope is present", func() {
205+ Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
206+ })
207+
208+ It("returns true when the remote address is trusted", func() {
209+ req.RemoteAddr = "127.0.0.1:4180"
210+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
211+ ReverseProxy: true,
212+ TrustedProxies: trustedProxies,
213+ })
214+
215+ Expect(util.CanTrustForwardedHeaders(req)).To(BeTrue())
216+ })
217+
218+ It("returns false when the remote address is untrusted", func() {
219+ req.RemoteAddr = "192.0.2.10:4180"
220+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
221+ ReverseProxy: true,
222+ TrustedProxies: trustedProxies,
223+ })
224+
225+ Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
164226 })
165227 })
166228})
pkg/upstream/http.go+55−16
@@ -123,7 +123,7 @@ func (t *unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
123123// The proxy should render an error page if there are failures connecting to the
124124// upstream server.
125125func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler ProxyErrorHandler) http.Handler {
126− proxy := httputil.NewSingleHostReverseProxy(target)
126+ proxy := newSingleHostReverseProxy(target)
127127
128128 // Inherit default transport options from Go's stdlib
129129 transport := http.DefaultTransport.(*http.Transport).Clone()
@@ -155,7 +155,7 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr
155155 }
156156
157157 // Ensure we always pass the original request path
158− setProxyDirector(proxy)
158+ setProxyRewrite(proxy)
159159
160160 // TODO (@tuunit) - this should be inverted or get a better name in the future to set the upstream host header
161161 // only if PassHostHeader is explicitly set to true. Currently this would be a breaking change.
@@ -179,32 +179,71 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr
179179 return proxy
180180}
181181
182−// setProxyUpstreamHostHeader sets the proxy.Director so that upstream requests
182+func newSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy {
183+ return &httputil.ReverseProxy{
184+ Rewrite: func(proxyReq *httputil.ProxyRequest) {
185+ proxyReq.SetURL(target)
186+ proxyReq.Out.Host = proxyReq.In.Host
187+ setProxyForwardingHeaders(proxyReq)
188+ },
189+ }
190+}
191+
192+// setProxyUpstreamHostHeader sets the proxy.Rewrite so that upstream requests
183193// receive a host header matching the target URL.
184194func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
185− director := proxy.Director
186− proxy.Director = func(req *http.Request) {
187− director(req)
188− req.Host = target.Host
195+ rewrite := proxy.Rewrite
196+ proxy.Rewrite = func(proxyReq *httputil.ProxyRequest) {
197+ rewrite(proxyReq)
198+ proxyReq.Out.Host = target.Host
189199 }
190200}
191201
192−// setProxyDirector sets the proxy.Director so that request URIs are escaped
202+// setProxyRewrite sets the proxy.Rewrite so that request URIs are escaped
193203// when proxying to usptream servers.
194−func setProxyDirector(proxy *httputil.ReverseProxy) {
195− director := proxy.Director
196− proxy.Director = func(req *http.Request) {
197− director(req)
204+func setProxyRewrite(proxy *httputil.ReverseProxy) {
205+ rewrite := proxy.Rewrite
206+ proxy.Rewrite = func(proxyReq *httputil.ProxyRequest) {
207+ rewrite(proxyReq)
198208 // use RequestURI so that we aren't unescaping encoded slashes in the request path
199− req.URL.Opaque = req.RequestURI
200− req.URL.RawQuery = ""
201− req.URL.ForceQuery = false
209+ proxyReq.Out.URL.Opaque = proxyReq.In.RequestURI
210+ proxyReq.Out.URL.RawQuery = ""
211+ proxyReq.Out.URL.ForceQuery = false
212+ }
213+}
214+
215+func setProxyForwardingHeaders(proxyReq *httputil.ProxyRequest) {
216+ // TODO (@tuunit): Preserve the legacy Director-based forwarding header behavior
217+ // for backwards compatibility. Harden this with saner defaults and/or
218+ // explicit flags in the future.
219+ for _, header := range []string{"Forwarded", "X-Forwarded-Host", "X-Forwarded-Proto"} {
220+ if values, ok := proxyReq.In.Header[header]; ok {
221+ proxyReq.Out.Header[header] = append([]string(nil), values...)
222+ }
223+ }
224+
225+ prior, ok := proxyReq.In.Header["X-Forwarded-For"]
226+ if ok {
227+ proxyReq.Out.Header["X-Forwarded-For"] = append([]string(nil), prior...)
228+ }
229+
230+ clientIP, _, err := net.SplitHostPort(proxyReq.In.RemoteAddr)
231+ if err != nil {
232+ return
233+ }
234+
235+ omit := ok && prior == nil
236+ if len(prior) > 0 {
237+ clientIP = strings.Join(prior, ", ") + ", " + clientIP
238+ }
239+ if !omit {
240+ proxyReq.Out.Header.Set("X-Forwarded-For", clientIP)
202241 }
203242}
204243
205244// newWebSocketReverseProxy creates a new reverse proxy for proxying websocket connections.
206245func newWebSocketReverseProxy(u *url.URL, skipTLSVerify *bool, passHostHeader *bool) http.Handler {
207− wsProxy := httputil.NewSingleHostReverseProxy(u)
246+ wsProxy := newSingleHostReverseProxy(u)
208247
209248 // Inherit default transport options from Go's stdlib
210249 transport := http.DefaultTransport.(*http.Transport).Clone()
pkg/upstream/http_test.go+57−3
@@ -361,7 +361,12 @@ var _ = Describe("HTTP Upstream Suite", func() {
361361 return http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
362362 proxy, ok := h.(*httputil.ReverseProxy)
363363 Expect(ok).To(BeTrue())
364− proxy.Director(req)
364+ outReq := req.Clone(req.Context())
365+ proxy.Rewrite(&httputil.ProxyRequest{
366+ In: req,
367+ Out: outReq,
368+ })
369+ req.Host = outReq.Host
365370 })
366371 }
367372 httpUpstream.handler = requestInterceptor(httpUpstream.handler)
@@ -370,6 +375,54 @@ var _ = Describe("HTTP Upstream Suite", func() {
370375 Expect(req.Host).To(Equal(strings.TrimPrefix(serverAddr, "http://")))
371376 })
372377
378+ It("ServeHTTP preserves forwarding headers when using Rewrite", func() {
379+ req := httptest.NewRequest("", "http://example.localhost/foo", nil)
380+ req.RemoteAddr = "192.0.2.10:1234"
381+ req.Header.Set("Forwarded", "for=192.0.2.1;proto=https;host=example.localhost")
382+ req.Header.Set("X-Forwarded-Host", "forwarded.example.localhost")
383+ req.Header.Set("X-Forwarded-Proto", "https")
384+ req.Header.Set("X-Forwarded-For", "192.0.2.1")
385+ req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{})
386+ rw := httptest.NewRecorder()
387+
388+ upstream := options.Upstream{
389+ ID: "preserveForwardedHeaders",
390+ PassHostHeader: ptr.To(true),
391+ ProxyWebSockets: ptr.To(false),
392+ InsecureSkipTLSVerify: ptr.To(false),
393+ FlushInterval: &defaultFlushInterval,
394+ Timeout: &defaultTimeout,
395+ }
396+
397+ u, err := url.Parse(serverAddr)
398+ Expect(err).ToNot(HaveOccurred())
399+
400+ handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
401+ httpUpstream, ok := handler.(*httpUpstreamProxy)
402+ Expect(ok).To(BeTrue())
403+
404+ requestInterceptor := func(h http.Handler) http.Handler {
405+ return http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
406+ proxy, ok := h.(*httputil.ReverseProxy)
407+ Expect(ok).To(BeTrue())
408+
409+ outReq := req.Clone(req.Context())
410+ proxy.Rewrite(&httputil.ProxyRequest{
411+ In: req,
412+ Out: outReq,
413+ })
414+
415+ Expect(outReq.Header.Values("Forwarded")).To(Equal([]string{"for=192.0.2.1;proto=https;host=example.localhost"}))
416+ Expect(outReq.Header.Values("X-Forwarded-Host")).To(Equal([]string{"forwarded.example.localhost"}))
417+ Expect(outReq.Header.Values("X-Forwarded-Proto")).To(Equal([]string{"https"}))
418+ Expect(outReq.Header.Values("X-Forwarded-For")).To(Equal([]string{"192.0.2.1, 192.0.2.10"}))
419+ })
420+ }
421+ httpUpstream.handler = requestInterceptor(httpUpstream.handler)
422+
423+ httpUpstream.ServeHTTP(rw, req)
424+ })
425+
373426 type newUpstreamTableInput struct {
374427 proxyWebSockets bool
375428 flushInterval time.Duration
@@ -405,6 +458,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
405458
406459 proxy, ok := upstreamProxy.handler.(*httputil.ReverseProxy)
407460 Expect(ok).To(BeTrue())
461+ Expect(proxy.Rewrite).ToNot(BeNil())
408462 Expect(proxy.FlushInterval).To(Equal(in.flushInterval))
409463 transport, ok := proxy.Transport.(*http.Transport)
410464 Expect(ok).To(BeTrue())
@@ -498,7 +552,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
498552
499553 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
500554
501− proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
555+ proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
502556 })
503557
504558 AfterEach(func() {
@@ -549,7 +603,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
549603 Expect(err).ToNot(HaveOccurred())
550604
551605 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
552− noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
606+ noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
553607 defer noPassHostServer.Close()
554608
555609 origin := "http://example.localhost"
pkg/upstream/proxy.go+14−14
@@ -58,7 +58,9 @@ func NewProxy(upstreams options.UpstreamConfig, sigData *options.SignatureData,
5858 }
5959 }
6060
61− registerTrailingSlashHandler(m.serveMux)
61+ if err := registerTrailingSlashHandler(m.serveMux); err != nil {
62+ return nil, fmt.Errorf("could not register trailing slash handler: %w", err)
63+ }
6264 return m, nil
6365}
6466
@@ -94,8 +96,7 @@ func (m *multiUpstreamProxy) registerHTTPUpstreamProxy(upstream options.Upstream
9496// registerHandler ensures the given handler is regiestered with the serveMux.
9597func (m *multiUpstreamProxy) registerHandler(upstream options.Upstream, handler http.Handler, writer pagewriter.Writer) error {
9698 if upstream.RewriteTarget == "" {
97− m.registerSimpleHandler(upstream.Path, handler)
98− return nil
99+ return m.registerSimpleHandler(upstream.Path, handler)
99100 }
100101
101102 return m.registerRewriteHandler(upstream, handler, writer)
@@ -103,12 +104,12 @@ func (m *multiUpstreamProxy) registerHandler(upstream options.Upstream, handler
103104
104105// registerSimpleHandler maintains the behaviour of the go standard serveMux
105106// by ensuring any path with a trailing `/` matches all paths under that prefix.
106−func (m *multiUpstreamProxy) registerSimpleHandler(path string, handler http.Handler) {
107+func (m *multiUpstreamProxy) registerSimpleHandler(path string, handler http.Handler) error {
107108 if strings.HasSuffix(path, "/") {
108− m.serveMux.PathPrefix(path).Handler(handler)
109− } else {
110− m.serveMux.Path(path).Handler(handler)
109+ return m.serveMux.PathPrefix(path).Handler(handler).GetError()
111110 }
111+
112+ return m.serveMux.Path(path).Handler(handler).GetError()
112113}
113114
114115// registerRewriteHandler ensures the handler is registered for all paths
@@ -123,19 +124,18 @@ func (m *multiUpstreamProxy) registerRewriteHandler(upstream options.Upstream, h
123124
124125 rewrite := newRewritePath(rewriteRegExp, upstream.RewriteTarget, writer)
125126 h := alice.New(rewrite).Then(handler)
126− m.serveMux.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
127− return rewriteRegExp.MatchString(req.URL.Path)
128− }).Handler(h)
129127
130− return nil
128+ return m.serveMux.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
129+ return rewriteRegExp.MatchString(req.URL.Path)
130+ }).Handler(h).GetError()
131131}
132132
133133// registerTrailingSlashHandler creates a new matcher that will check if the
134134// requested path would match if it had a trailing slash appended.
135135// If the path matches with a trailing slash, we send back a redirect.
136136// This allows us to be consistent with the built in go servemux implementation.
137−func registerTrailingSlashHandler(serveMux *mux.Router) {
138− serveMux.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
137+func registerTrailingSlashHandler(serveMux *mux.Router) error {
138+ return serveMux.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
139139 if strings.HasSuffix(req.URL.Path, "/") {
140140 return false
141141 }
@@ -149,7 +149,7 @@ func registerTrailingSlashHandler(serveMux *mux.Router) {
149149 return serveMux.Match(slashReq, m)
150150 }).Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
151151 http.Redirect(rw, req, req.URL.String()+"/", http.StatusMovedPermanently)
152− }))
152+ })).GetError()
153153}
154154
155155// sortByPathLongest ensures that the upstreams are sorted by longest path.
pkg/upstream/proxy_test.go+33−1
@@ -79,7 +79,7 @@ var _ = Describe("Proxy Suite", func() {
7979 {
8080 ID: "bad-http-backend",
8181 Path: "/bad-http/",
82− URI: "http://::1",
82+ URI: invalidServer,
8383 },
8484 {
8585 ID: "single-path-backend",
@@ -383,6 +383,38 @@ var _ = Describe("Proxy Suite", func() {
383383 )
384384 })
385385
386+ Context("multiUpstreamProxy errors", func() {
387+ type proxyErrorTableInput struct {
388+ upstreams options.UpstreamConfig
389+ expectedError string
390+ }
391+
392+ DescribeTable("NewProxy", func(in *proxyErrorTableInput) {
393+ sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"}
394+
395+ writer := &pagewriter.WriterFuncs{
396+ ProxyErrorFunc: func(rw http.ResponseWriter, _ *http.Request, _ error) {
397+ rw.WriteHeader(502)
398+ rw.Write([]byte("Proxy Error"))
399+ },
400+ }
401+
402+ _, err := NewProxy(in.upstreams, sigData, writer)
403+ Expect(err).To(MatchError(in.expectedError))
404+ },
405+ Entry("regex matcher without rewrite target", &proxyErrorTableInput{
406+ upstreams: options.UpstreamConfig{
407+ Upstreams: []options.Upstream{{
408+ ID: "api",
409+ Path: "^/api/$",
410+ URI: "http://example.com",
411+ }},
412+ },
413+ expectedError: `could not register http upstream "api": mux: path must start with a slash, got "^/api/$"`,
414+ }),
415+ )
416+ })
417+
386418 Context("sortByPathLongest", func() {
387419 type sortByPathLongestTableInput struct {
388420 input []options.Upstream
pkg/upstream/upstream_suite_test.go+1−1
@@ -24,7 +24,7 @@ var (
2424 serverAddr string
2525 unixServer *httptest.Server
2626 unixServerAddr string
27− invalidServer = "http://::1"
27+ invalidServer = "http://127.0.0.1:1"
2828)
2929
3030func TestUpstreamSuite(t *testing.T) {
pkg/validation/allowlist.go+12
@@ -16,6 +16,7 @@ func validateAllowlists(o *options.Options) []string {
1616
1717 msgs = append(msgs, validateAuthRoutes(o)...)
1818 msgs = append(msgs, validateAuthRegexes(o)...)
19+ msgs = append(msgs, validateTrustedProxyIPs(o)...)
1920 msgs = append(msgs, validateTrustedIPs(o)...)
2021
2122 if len(o.TrustedIPs) > 0 && o.ReverseProxy {
@@ -28,6 +29,17 @@ func validateAllowlists(o *options.Options) []string {
2829 return msgs
2930}
3031
32+// validateTrustedProxyIPs validates IP/CIDRs for trusted reverse proxies.
33+func validateTrustedProxyIPs(o *options.Options) []string {
34+ msgs := []string{}
35+ for i, ipStr := range o.TrustedProxyIPs {
36+ if ip.ParseIPNet(ipStr) == nil {
37+ msgs = append(msgs, fmt.Sprintf("trusted_proxy_ips[%d] (%s) could not be recognized", i, ipStr))
38+ }
39+ }
40+ return msgs
41+}
42+
3143// validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes
3244func validateAuthRoutes(o *options.Options) []string {
3345 msgs := []string{}
pkg/validation/allowlist_test.go+30
@@ -23,6 +23,11 @@ var _ = Describe("Allowlist", func() {
2323 errStrings []string
2424 }
2525
26+ type validateTrustedProxyIPsTableInput struct {
27+ trustedProxyIPs []string
28+ errStrings []string
29+ }
30+
2631 DescribeTable("validateRoutes",
2732 func(r *validateRoutesTableInput) {
2833 opts := &options.Options{
@@ -121,4 +126,29 @@ var _ = Describe("Allowlist", func() {
121126 },
122127 }),
123128 )
129+
130+ DescribeTable("validateTrustedProxyIPs",
131+ func(t *validateTrustedProxyIPsTableInput) {
132+ opts := &options.Options{
133+ TrustedProxyIPs: t.trustedProxyIPs,
134+ }
135+ Expect(validateTrustedProxyIPs(opts)).To(ConsistOf(t.errStrings))
136+ },
137+ Entry("Valid trusted proxy IPs", &validateTrustedProxyIPsTableInput{
138+ trustedProxyIPs: []string{
139+ "127.0.0.1",
140+ "10.32.0.1/32",
141+ "::1",
142+ "2a12:105:ee7:9234:0:0:0:0/64",
143+ },
144+ errStrings: []string{},
145+ }),
146+ Entry("Invalid trusted proxy IPs", &validateTrustedProxyIPsTableInput{
147+ trustedProxyIPs: []string{"[::1]", "alkwlkbn/32"},
148+ errStrings: []string{
149+ "trusted_proxy_ips[0] ([::1]) could not be recognized",
150+ "trusted_proxy_ips[1] (alkwlkbn/32) could not be recognized",
151+ },
152+ }),
153+ )
124154})
pkg/validation/sessions_test.go+2−2
@@ -193,8 +193,8 @@ var _ = Describe("Sessions", func() {
193193 unreachableRedisDelMsg = "unable to delete the redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused"
194194 unreachableSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels specified in configuration are unreachable: redis: nil"
195195 unrechableSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels specified in configuration are unreachable: redis: nil"
196− refusedSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels specified in configuration are unreachable: context deadline exceeded"
197− refusedSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels specified in configuration are unreachable: context deadline exceeded"
196+ refusedSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels specified in configuration are unreachable: dial tcp 127.0.0.1:65535: connect: connection refused"
197+ refusedSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels specified in configuration are unreachable: dial tcp 127.0.0.1:65535: connect: connection refused"
198198 )
199199
200200 type redisStoreTableInput struct {
providers/atlassian-cloud.gonew file+99
@@ -0,0 +1,99 @@
1+package providers
2+
3+import (
4+ "context"
5+ "errors"
6+ "net/url"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
10+)
11+
12+// AtlassianProvider represents an Atlassian Cloud based Identity Provider
13+type AtlassianProvider struct {
14+ *ProviderData
15+}
16+
17+var _ Provider = (*AtlassianProvider)(nil)
18+
19+const (
20+ atlassianProviderName = "Atlassian"
21+ atlassianDefaultScope = "read:me"
22+ atlassianPrompt = "consent"
23+ atlassianAudience = "api.atlassian.com"
24+)
25+
26+var (
27+ atlassianDefaultLoginURL = &url.URL{
28+ Scheme: "https",
29+ Host: "auth.atlassian.com",
30+ Path: "/authorize",
31+ }
32+
33+ atlassianDefaultRedeemURL = &url.URL{
34+ Scheme: "https",
35+ Host: "auth.atlassian.com",
36+ Path: "/oauth/token",
37+ }
38+
39+ // atlassianDefaultProfileURL returns the signed-in user's profile.
40+ atlassianDefaultProfileURL = &url.URL{
41+ Scheme: "https",
42+ Host: "api.atlassian.com",
43+ Path: "/me",
44+ }
45+)
46+
47+// NewAtlassianProvider initiates a new AtlassianProvider
48+func NewAtlassianProvider(p *ProviderData) *AtlassianProvider {
49+ p.setProviderDefaults(providerDefaults{
50+ name: atlassianProviderName,
51+ loginURL: atlassianDefaultLoginURL,
52+ redeemURL: atlassianDefaultRedeemURL,
53+ profileURL: atlassianDefaultProfileURL,
54+ validateURL: atlassianDefaultProfileURL,
55+ scope: atlassianDefaultScope,
56+ })
57+ return &AtlassianProvider{ProviderData: p}
58+}
59+
60+// GetLoginURL adds the audience and prompt parameters Atlassian requires.
61+func (p *AtlassianProvider) GetLoginURL(redirectURI, state, _ string, extraParams url.Values) string {
62+ params := url.Values{}
63+ for k, v := range extraParams {
64+ params[k] = v
65+ }
66+ params.Del("approval_prompt")
67+ if params.Get("prompt") == "" {
68+ params.Set("prompt", atlassianPrompt)
69+ }
70+ params.Set("audience", atlassianAudience)
71+
72+ loginURL := makeLoginURL(p.ProviderData, redirectURI, state, params)
73+ return loginURL.String()
74+}
75+
76+// EnrichSession sets the session's email from the Atlassian profile.
77+func (p *AtlassianProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
78+ var profile struct {
79+ Email string `json:"email"`
80+ }
81+ err := requests.New(p.ProfileURL.String()).
82+ WithContext(ctx).
83+ WithHeaders(makeOIDCHeader(s.AccessToken)).
84+ Do().
85+ UnmarshalInto(&profile)
86+ if err != nil {
87+ return err
88+ }
89+ if profile.Email == "" {
90+ return errors.New("no email in Atlassian profile")
91+ }
92+ s.Email = profile.Email
93+ return nil
94+}
95+
96+// ValidateSession validates the AccessToken
97+func (p *AtlassianProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
98+ return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
99+}
providers/atlassian-cloud_test.gonew file+93
@@ -0,0 +1,93 @@
1+package providers
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "testing"
9+
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11+ . "github.com/onsi/gomega"
12+)
13+
14+func testAtlassianBackend(status int, body string) *httptest.Server {
15+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16+ if r.URL.Path != "/me" || r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
17+ w.WriteHeader(http.StatusUnauthorized)
18+ return
19+ }
20+ w.WriteHeader(status)
21+ _, _ = w.Write([]byte(body))
22+ }))
23+}
24+
25+func testAtlassianProvider(backend *httptest.Server) *AtlassianProvider {
26+ p := NewAtlassianProvider(&ProviderData{})
27+ u, _ := url.Parse(backend.URL)
28+ updateURL(p.ProfileURL, u.Host)
29+ updateURL(p.ValidateURL, u.Host)
30+ return p
31+}
32+
33+func TestNewAtlassianProvider(t *testing.T) {
34+ g := NewWithT(t)
35+
36+ providerData := NewAtlassianProvider(&ProviderData{}).Data()
37+ g.Expect(providerData.ProviderName).To(Equal("Atlassian"))
38+ g.Expect(providerData.LoginURL.String()).To(Equal("https://auth.atlassian.com/authorize"))
39+ g.Expect(providerData.RedeemURL.String()).To(Equal("https://auth.atlassian.com/oauth/token"))
40+ g.Expect(providerData.ProfileURL.String()).To(Equal("https://api.atlassian.com/me"))
41+ g.Expect(providerData.ValidateURL.String()).To(Equal("https://api.atlassian.com/me"))
42+ g.Expect(providerData.Scope).To(Equal("read:me"))
43+}
44+
45+func TestAtlassianProviderGetLoginURL(t *testing.T) {
46+ g := NewWithT(t)
47+ p := NewAtlassianProvider(&ProviderData{ClientID: "client"})
48+
49+ loginURL, err := url.Parse(p.GetLoginURL("https://proxy/oauth2/callback", "state", "",
50+ url.Values{"approval_prompt": {"force"}}))
51+ g.Expect(err).ToNot(HaveOccurred())
52+
53+ q := loginURL.Query()
54+ g.Expect(q.Get("audience")).To(Equal("api.atlassian.com"))
55+ g.Expect(q.Get("prompt")).To(Equal("consent"))
56+ g.Expect(q.Has("approval_prompt")).To(BeFalse())
57+ g.Expect(q.Get("scope")).To(Equal("read:me"))
58+
59+ loginURL, err = url.Parse(p.GetLoginURL("https://proxy/oauth2/callback", "state", "",
60+ url.Values{"prompt": {"login"}}))
61+ g.Expect(err).ToNot(HaveOccurred())
62+ g.Expect(loginURL.Query().Get("prompt")).To(Equal("login"))
63+}
64+
65+func TestAtlassianProviderEnrichSession(t *testing.T) {
66+ g := NewWithT(t)
67+ b := testAtlassianBackend(http.StatusOK, `{"account_id":"abc","email":"user@example.com"}`)
68+ defer b.Close()
69+ p := testAtlassianProvider(b)
70+
71+ session := CreateAuthorizedSession()
72+ g.Expect(p.EnrichSession(context.Background(), session)).To(Succeed())
73+ g.Expect(session.Email).To(Equal("user@example.com"))
74+}
75+
76+func TestAtlassianProviderEnrichSessionWithoutEmail(t *testing.T) {
77+ g := NewWithT(t)
78+ b := testAtlassianBackend(http.StatusOK, `{"account_id":"abc"}`)
79+ defer b.Close()
80+ p := testAtlassianProvider(b)
81+
82+ g.Expect(p.EnrichSession(context.Background(), CreateAuthorizedSession())).ToNot(Succeed())
83+}
84+
85+func TestAtlassianProviderValidateSession(t *testing.T) {
86+ g := NewWithT(t)
87+ b := testAtlassianBackend(http.StatusOK, `{"email":"user@example.com"}`)
88+ defer b.Close()
89+ p := testAtlassianProvider(b)
90+
91+ g.Expect(p.ValidateSession(context.Background(), CreateAuthorizedSession())).To(BeTrue())
92+ g.Expect(p.ValidateSession(context.Background(), &sessions.SessionState{AccessToken: "wrong"})).To(BeFalse())
93+}
providers/azure.go+2−2
@@ -166,7 +166,7 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code, codeVerif
166166
167167 err = requests.New(p.RedeemURL.String()).
168168 WithContext(ctx).
169− WithMethod("POST").
169+ WithMethod(http.MethodPost).
170170 WithBody(bytes.NewBufferString(params.Encode())).
171171 SetHeader("Content-Type", "application/x-www-form-urlencoded").
172172 Do().
@@ -334,7 +334,7 @@ func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sess
334334
335335 err = requests.New(p.RedeemURL.String()).
336336 WithContext(ctx).
337− WithMethod("POST").
337+ WithMethod(http.MethodPost).
338338 WithBody(bytes.NewBufferString(params.Encode())).
339339 SetHeader("Content-Type", "application/x-www-form-urlencoded").
340340 Do().
providers/dtt_sso.gonew file+202
@@ -0,0 +1,202 @@
1+package providers
2+
3+import (
4+ "bytes"
5+ "context"
6+ "encoding/base64"
7+ "errors"
8+ "fmt"
9+ "net/http"
10+ "net/url"
11+ "time"
12+
13+ "github.com/golang-jwt/jwt/v5"
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
16+)
17+
18+// DTTSSOProvider signs users in with the DTT SSO (auth-server).
19+type DTTSSOProvider struct {
20+ *ProviderData
21+}
22+
23+var _ Provider = (*DTTSSOProvider)(nil)
24+
25+const (
26+ dttSSOProviderName = "DTT SSO"
27+ dttSSODefaultScope = "openid"
28+)
29+
30+var (
31+ dttSSODefaultLoginURL = &url.URL{
32+ Scheme: "https",
33+ Host: "auth.thebestagent.pro",
34+ Path: "/oauth/authorize",
35+ }
36+
37+ dttSSODefaultRedeemURL = &url.URL{
38+ Scheme: "https",
39+ Host: "auth.thebestagent.pro",
40+ Path: "/oauth/token",
41+ }
42+
43+ dttSSODefaultValidateURL = &url.URL{
44+ Scheme: "https",
45+ Host: "auth.thebestagent.pro",
46+ Path: "/oauth/check_token",
47+ }
48+)
49+
50+// NewDTTSSOProvider initiates a new DTTSSOProvider
51+func NewDTTSSOProvider(p *ProviderData) *DTTSSOProvider {
52+ p.setProviderDefaults(providerDefaults{
53+ name: dttSSOProviderName,
54+ loginURL: dttSSODefaultLoginURL,
55+ redeemURL: dttSSODefaultRedeemURL,
56+ profileURL: nil,
57+ validateURL: dttSSODefaultValidateURL,
58+ scope: dttSSODefaultScope,
59+ })
60+ return &DTTSSOProvider{ProviderData: p}
61+}
62+
63+// dttSSOTokenResponse is the SSO's /oauth/token response.
64+type dttSSOTokenResponse struct {
65+ AccessToken string `json:"access_token"`
66+ RefreshToken string `json:"refresh_token"`
67+ ExpiresIn int64 `json:"expires_in"`
68+ IDToken string `json:"id_token"`
69+}
70+
71+// dttSSOClaims holds the id_token claims the provider uses.
72+type dttSSOClaims struct {
73+ jwt.RegisteredClaims
74+ Email string `json:"email"`
75+ Role string `json:"role"`
76+ UserID string `json:"user_id"`
77+}
78+
79+// Redeem exchanges the authorization code for a session.
80+func (p *DTTSSOProvider) Redeem(ctx context.Context, redirectURL, code, _ string) (*sessions.SessionState, error) {
81+ if code == "" {
82+ return nil, ErrMissingCode
83+ }
84+ return p.requestTokens(ctx, url.Values{
85+ "grant_type": {"authorization_code"},
86+ "code": {code},
87+ "redirect_uri": {redirectURL},
88+ })
89+}
90+
91+// RefreshSession renews the tokens and re-reads the email and role.
92+func (p *DTTSSOProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
93+ if s == nil || s.RefreshToken == "" {
94+ return false, nil
95+ }
96+ refreshed, err := p.requestTokens(ctx, url.Values{
97+ "grant_type": {"refresh_token"},
98+ "refresh_token": {s.RefreshToken},
99+ })
100+ if err != nil {
101+ return false, fmt.Errorf("unable to redeem refresh token: %v", err)
102+ }
103+ s.AccessToken = refreshed.AccessToken
104+ s.RefreshToken = refreshed.RefreshToken
105+ s.IDToken = refreshed.IDToken
106+ s.Email = refreshed.Email
107+ s.User = refreshed.User
108+ s.Groups = refreshed.Groups
109+ s.CreatedAt = refreshed.CreatedAt
110+ s.ExpiresOn = refreshed.ExpiresOn
111+ return true, nil
112+}
113+
114+// ValidateSession checks the access token with the SSO's check_token endpoint.
115+func (p *DTTSSOProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
116+ return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
117+}
118+
119+// requestTokens calls the token endpoint and builds a session from the verified id_token.
120+//
121+// The SSO compares the Basic credentials byte for byte, so they are sent unescaped
122+// rather than through golang.org/x/oauth2, which URL-escapes them.
123+func (p *DTTSSOProvider) requestTokens(ctx context.Context, params url.Values) (*sessions.SessionState, error) {
124+ clientSecret, err := p.GetClientSecret()
125+ if err != nil {
126+ return nil, err
127+ }
128+ credentials := base64.StdEncoding.EncodeToString([]byte(p.ClientID + ":" + clientSecret))
129+
130+ result := requests.New(p.RedeemURL.String()).
131+ WithContext(ctx).
132+ WithMethod(http.MethodPost).
133+ WithBody(bytes.NewBufferString(params.Encode())).
134+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
135+ SetHeader("Authorization", "Basic "+credentials).
136+ Do()
137+ if result.Error() != nil {
138+ return nil, result.Error()
139+ }
140+ if result.StatusCode() != http.StatusOK {
141+ return nil, fmt.Errorf("token request failed: status %d - %s", result.StatusCode(), result.Body())
142+ }
143+
144+ var token dttSSOTokenResponse
145+ if err := result.UnmarshalInto(&token); err != nil {
146+ return nil, err
147+ }
148+ if token.AccessToken == "" {
149+ return nil, errors.New("token response carried no access_token")
150+ }
151+ claims, err := p.verifyIDToken(token.IDToken, clientSecret)
152+ if err != nil {
153+ return nil, err
154+ }
155+
156+ s := &sessions.SessionState{
157+ AccessToken: token.AccessToken,
158+ RefreshToken: token.RefreshToken,
159+ IDToken: token.IDToken,
160+ Email: claims.Email,
161+ User: claims.UserID,
162+ }
163+ if claims.Role != "" {
164+ s.Groups = []string{claims.Role}
165+ }
166+ s.CreatedAtNow()
167+ if token.ExpiresIn > 0 {
168+ s.ExpiresIn(time.Duration(token.ExpiresIn) * time.Second)
169+ }
170+ return s, nil
171+}
172+
173+// verifyIDToken checks the id_token's HS256 signature (keyed on the client secret),
174+// issuer, audience and expiry.
175+//
176+// go-jose is not used: it rejects HMAC keys under 32 bytes, and SSO client secrets
177+// can be shorter.
178+func (p *DTTSSOProvider) verifyIDToken(raw, clientSecret string) (*dttSSOClaims, error) {
179+ if raw == "" {
180+ return nil, errors.New("token response carried no id_token; the scope must be exactly \"openid\"")
181+ }
182+ var claims dttSSOClaims
183+ _, err := jwt.ParseWithClaims(raw, &claims,
184+ func(*jwt.Token) (any, error) { return []byte(clientSecret), nil },
185+ jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
186+ jwt.WithIssuer(p.issuer()),
187+ jwt.WithAudience(p.ClientID),
188+ jwt.WithExpirationRequired(),
189+ )
190+ if err != nil {
191+ return nil, fmt.Errorf("verifying id_token: %v", err)
192+ }
193+ if claims.Email == "" {
194+ return nil, errors.New("id_token carries no email")
195+ }
196+ return &claims, nil
197+}
198+
199+// issuer is the SSO's site host, the origin of the login URL.
200+func (p *DTTSSOProvider) issuer() string {
201+ return p.LoginURL.Scheme + "://" + p.LoginURL.Host
202+}
providers/dtt_sso_test.gonew file+237
@@ -0,0 +1,237 @@
1+package providers
2+
3+import (
4+ "context"
5+ "encoding/base64"
6+ "encoding/json"
7+ "net/http"
8+ "net/http/httptest"
9+ "net/url"
10+ "testing"
11+ "time"
12+
13+ "github.com/golang-jwt/jwt/v5"
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15+ . "github.com/onsi/gomega"
16+)
17+
18+const (
19+ dttSSOTestClientID = "redash-gate"
20+ // Shorter than 32 bytes, and contains characters golang.org/x/oauth2 would
21+ // URL-escape in Basic auth.
22+ dttSSOTestSecret = "s3cr+t/abc=="
23+)
24+
25+// dttSSOTestServer mimics the SSO's token and check_token endpoints.
26+type dttSSOTestServer struct {
27+ *httptest.Server
28+ claims map[string]interface{}
29+ signingKey []byte
30+ lastForm url.Values
31+ validTokens map[string]bool
32+ tokenCounter int
33+}
34+
35+func newDTTSSOTestServer() *dttSSOTestServer {
36+ s := &dttSSOTestServer{
37+ signingKey: []byte(dttSSOTestSecret),
38+ validTokens: map[string]bool{},
39+ }
40+ s.Server = httptest.NewServer(http.HandlerFunc(s.handle))
41+ u, _ := url.Parse(s.URL)
42+ s.claims = map[string]interface{}{
43+ "iss": "http://" + u.Host,
44+ "aud": dttSSOTestClientID,
45+ "sub": "0b5c1e1a-1111-2222-3333-444455556666",
46+ "user_id": "0b5c1e1a-1111-2222-3333-444455556666",
47+ "email": "admin@example.com",
48+ "role": "dtt_admin",
49+ "exp": time.Now().Add(time.Hour).Unix(),
50+ }
51+ return s
52+}
53+
54+func (s *dttSSOTestServer) handle(w http.ResponseWriter, r *http.Request) {
55+ switch r.URL.Path {
56+ case "/oauth/token":
57+ want := "Basic " + base64.StdEncoding.EncodeToString([]byte(dttSSOTestClientID+":"+dttSSOTestSecret))
58+ if r.Header.Get("Authorization") != want {
59+ w.WriteHeader(http.StatusUnauthorized)
60+ return
61+ }
62+ _ = r.ParseForm()
63+ s.lastForm = r.PostForm
64+ if r.PostForm.Get("grant_type") == "refresh_token" && !s.validTokens["refresh-"+r.PostForm.Get("refresh_token")] {
65+ w.WriteHeader(http.StatusBadRequest)
66+ _, _ = w.Write([]byte(`{"error":"invalid_grant","error_description":"Invalid refresh token"}`))
67+ return
68+ }
69+ s.tokenCounter++
70+ access := "access-" + string(rune('0'+s.tokenCounter))
71+ refresh := "refresh-" + string(rune('0'+s.tokenCounter))
72+ s.validTokens[access] = true
73+ s.validTokens["refresh-"+refresh] = true
74+ w.Header().Set("Content-Type", "application/json")
75+ _ = json.NewEncoder(w).Encode(map[string]interface{}{
76+ "access_token": access,
77+ "token_type": "bearer",
78+ "expires_in": 3600,
79+ "refresh_token": refresh,
80+ "id_token": s.idToken(),
81+ "scope": "openid",
82+ })
83+ case "/oauth/check_token":
84+ token := r.Header.Get("Authorization")
85+ if len(token) > len("Bearer ") && s.validTokens[token[len("Bearer "):]] {
86+ _, _ = w.Write([]byte(`{"user_id":"0b5c1e1a-1111-2222-3333-444455556666","real_user_id":null,"issued_by":"redash-gate"}`))
87+ return
88+ }
89+ w.WriteHeader(http.StatusUnauthorized)
90+ _, _ = w.Write([]byte("Invalid access token"))
91+ default:
92+ w.WriteHeader(http.StatusNotFound)
93+ }
94+}
95+
96+func (s *dttSSOTestServer) idToken() string {
97+ if s.claims == nil {
98+ return ""
99+ }
100+ raw, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(s.claims)).SignedString(s.signingKey)
101+ if err != nil {
102+ panic(err)
103+ }
104+ return raw
105+}
106+
107+func testDTTSSOProvider(server *dttSSOTestServer) *DTTSSOProvider {
108+ p := NewDTTSSOProvider(&ProviderData{ClientID: dttSSOTestClientID, ClientSecret: dttSSOTestSecret})
109+ u, _ := url.Parse(server.URL)
110+ updateURL(p.LoginURL, u.Host)
111+ updateURL(p.RedeemURL, u.Host)
112+ updateURL(p.ValidateURL, u.Host)
113+ return p
114+}
115+
116+func TestNewDTTSSOProvider(t *testing.T) {
117+ g := NewWithT(t)
118+
119+ providerData := NewDTTSSOProvider(&ProviderData{}).Data()
120+ g.Expect(providerData.ProviderName).To(Equal("DTT SSO"))
121+ g.Expect(providerData.LoginURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/authorize"))
122+ g.Expect(providerData.RedeemURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/token"))
123+ g.Expect(providerData.ValidateURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/check_token"))
124+ g.Expect(providerData.Scope).To(Equal("openid"))
125+}
126+
127+func TestDTTSSOProviderRedeem(t *testing.T) {
128+ g := NewWithT(t)
129+ server := newDTTSSOTestServer()
130+ defer server.Close()
131+ p := testDTTSSOProvider(server)
132+
133+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
134+ g.Expect(err).ToNot(HaveOccurred())
135+ g.Expect(s.Email).To(Equal("admin@example.com"))
136+ g.Expect(s.User).To(Equal("0b5c1e1a-1111-2222-3333-444455556666"))
137+ g.Expect(s.Groups).To(Equal([]string{"dtt_admin"}))
138+ g.Expect(s.AccessToken).To(Equal("access-1"))
139+ g.Expect(s.RefreshToken).To(Equal("refresh-1"))
140+ g.Expect(s.IDToken).ToNot(BeEmpty())
141+ g.Expect(s.ExpiresOn).ToNot(BeNil())
142+ g.Expect(*s.ExpiresOn).To(BeTemporally("~", time.Now().Add(time.Hour), time.Minute))
143+
144+ g.Expect(server.lastForm.Get("grant_type")).To(Equal("authorization_code"))
145+ g.Expect(server.lastForm.Get("code")).To(Equal("the-code"))
146+ g.Expect(server.lastForm.Get("redirect_uri")).To(Equal("https://redash.example.com/oauth2/callback"))
147+}
148+
149+func TestDTTSSOProviderRedeemRejectsBadIDTokens(t *testing.T) {
150+ testCases := map[string]func(s *dttSSOTestServer){
151+ "signed with another secret": func(s *dttSSOTestServer) { s.signingKey = []byte("some-other-client-secret") },
152+ "other audience": func(s *dttSSOTestServer) { s.claims["aud"] = "frappe-gate" },
153+ "other issuer": func(s *dttSSOTestServer) { s.claims["iss"] = "https://evil.example.com" },
154+ "expired": func(s *dttSSOTestServer) { s.claims["exp"] = time.Now().Add(-time.Minute).Unix() },
155+ "no email": func(s *dttSSOTestServer) { delete(s.claims, "email") },
156+ "no id_token": func(s *dttSSOTestServer) { s.claims = nil },
157+ }
158+ for name, tamper := range testCases {
159+ t.Run(name, func(t *testing.T) {
160+ g := NewWithT(t)
161+ server := newDTTSSOTestServer()
162+ defer server.Close()
163+ tamper(server)
164+
165+ _, err := testDTTSSOProvider(server).Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
166+ g.Expect(err).To(HaveOccurred())
167+ })
168+ }
169+}
170+
171+func TestDTTSSOProviderRedeemRejectsUnsignedIDToken(t *testing.T) {
172+ g := NewWithT(t)
173+ server := newDTTSSOTestServer()
174+ defer server.Close()
175+ p := testDTTSSOProvider(server)
176+
177+ header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`))
178+ payload, _ := json.Marshal(server.claims)
179+ unsigned := header + "." + base64.RawURLEncoding.EncodeToString(payload) + "."
180+
181+ _, err := p.verifyIDToken(unsigned, dttSSOTestSecret)
182+ g.Expect(err).To(HaveOccurred())
183+}
184+
185+func TestDTTSSOProviderRefreshSession(t *testing.T) {
186+ g := NewWithT(t)
187+ server := newDTTSSOTestServer()
188+ defer server.Close()
189+ p := testDTTSSOProvider(server)
190+
191+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
192+ g.Expect(err).ToNot(HaveOccurred())
193+ nonce := []byte("keep-me")
194+ s.Nonce = nonce
195+
196+ server.claims["role"] = "agent"
197+ server.claims["email"] = "renamed@example.com"
198+ refreshed, err := p.RefreshSession(context.Background(), s)
199+ g.Expect(err).ToNot(HaveOccurred())
200+ g.Expect(refreshed).To(BeTrue())
201+ g.Expect(s.AccessToken).To(Equal("access-2"))
202+ g.Expect(s.RefreshToken).To(Equal("refresh-2"))
203+ g.Expect(s.Groups).To(Equal([]string{"agent"}))
204+ g.Expect(s.Email).To(Equal("renamed@example.com"))
205+ g.Expect(s.Nonce).To(Equal(nonce))
206+ g.Expect(server.lastForm.Get("grant_type")).To(Equal("refresh_token"))
207+ g.Expect(server.lastForm.Get("refresh_token")).To(Equal("refresh-1"))
208+}
209+
210+func TestDTTSSOProviderRefreshSessionFailures(t *testing.T) {
211+ g := NewWithT(t)
212+ server := newDTTSSOTestServer()
213+ defer server.Close()
214+ p := testDTTSSOProvider(server)
215+
216+ refreshed, err := p.RefreshSession(context.Background(), &sessions.SessionState{})
217+ g.Expect(err).ToNot(HaveOccurred())
218+ g.Expect(refreshed).To(BeFalse())
219+
220+ refreshed, err = p.RefreshSession(context.Background(), &sessions.SessionState{RefreshToken: "revoked"})
221+ g.Expect(err).To(HaveOccurred())
222+ g.Expect(refreshed).To(BeFalse())
223+}
224+
225+func TestDTTSSOProviderValidateSession(t *testing.T) {
226+ g := NewWithT(t)
227+ server := newDTTSSOTestServer()
228+ defer server.Close()
229+ p := testDTTSSOProvider(server)
230+
231+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
232+ g.Expect(err).ToNot(HaveOccurred())
233+ g.Expect(p.ValidateSession(context.Background(), s)).To(BeTrue())
234+
235+ delete(server.validTokens, s.AccessToken)
236+ g.Expect(p.ValidateSession(context.Background(), s)).To(BeFalse())
237+}
providers/google.go+2−2
@@ -219,7 +219,7 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code, codeVeri
219219
220220 err = requests.New(p.RedeemURL.String()).
221221 WithContext(ctx).
222− WithMethod("POST").
222+ WithMethod(http.MethodPost).
223223 WithBody(bytes.NewBufferString(params.Encode())).
224224 SetHeader("Content-Type", "application/x-www-form-urlencoded").
225225 Do().
@@ -543,7 +543,7 @@ func (p *GoogleProvider) redeemRefreshToken(ctx context.Context, s *sessions.Ses
543543
544544 err = requests.New(p.RedeemURL.String()).
545545 WithContext(ctx).
546− WithMethod("POST").
546+ WithMethod(http.MethodPost).
547547 WithBody(bytes.NewBufferString(params.Encode())).
548548 SetHeader("Content-Type", "application/x-www-form-urlencoded").
549549 Do().
providers/logingov.go+2−1
@@ -8,6 +8,7 @@ import (
88 "errors"
99 "fmt"
1010 "math/big"
11+ "net/http"
1112 "net/url"
1213 "os"
1314 "time"
@@ -237,7 +238,7 @@ func (p *LoginGovProvider) Redeem(ctx context.Context, _, code, codeVerifier str
237238 }
238239 err = requests.New(p.RedeemURL.String()).
239240 WithContext(ctx).
240− WithMethod("POST").
241+ WithMethod(http.MethodPost).
241242 WithBody(bytes.NewBufferString(params.Encode())).
242243 SetHeader("Content-Type", "application/x-www-form-urlencoded").
243244 Do().
providers/ms_entra_id.go+2−1
@@ -5,6 +5,7 @@ import (
55 "context"
66 "encoding/json"
77 "fmt"
8+ "net/http"
89 "net/url"
910 "os"
1011 "regexp"
@@ -304,7 +305,7 @@ func (p *MicrosoftEntraIDProvider) checkTenantMatchesTenantList(tenant string, a
304305func (p *MicrosoftEntraIDProvider) fetchToken(ctx context.Context, params url.Values) (*oauth2.Token, error) {
305306 resp := requests.New(p.RedeemURL.String()).
306307 WithContext(ctx).
307− WithMethod("POST").
308+ WithMethod(http.MethodPost).
308309 WithBody(bytes.NewBufferString(params.Encode())).
309310 SetHeader("Content-Type", "application/x-www-form-urlencoded").
310311 Do()
providers/provider_default.go+2−1
@@ -5,6 +5,7 @@ import (
55 "context"
66 "errors"
77 "fmt"
8+ "net/http"
89 "net/url"
910
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
@@ -71,7 +72,7 @@ func (p *ProviderData) Redeem(ctx context.Context, redirectURL, code, codeVerifi
7172
7273 result := requests.New(p.RedeemURL.String()).
7374 WithContext(ctx).
74− WithMethod("POST").
75+ WithMethod(http.MethodPost).
7576 WithBody(bytes.NewBufferString(params.Encode())).
7677 SetHeader("Content-Type", "application/x-www-form-urlencoded").
7778 Do()
providers/providers.go+7−3
@@ -40,6 +40,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
4040 switch providerConfig.Type {
4141 case options.ADFSProvider:
4242 return NewADFSProvider(providerData, providerConfig), nil
43+ case options.AtlassianProvider:
44+ return NewAtlassianProvider(providerData), nil
4345 case options.AzureProvider:
4446 return NewAzureProvider(providerData, providerConfig.AzureConfig), nil
4547 case options.MicrosoftEntraIDProvider:
@@ -50,6 +52,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
5052 return NewCIDAASProvider(providerData, providerConfig), nil
5153 case options.DigitalOceanProvider:
5254 return NewDigitalOceanProvider(providerData), nil
55+ case options.DTTSSOProvider:
56+ return NewDTTSSOProvider(providerData), nil
5357 case options.FacebookProvider:
5458 return NewFacebookProvider(providerData), nil
5559 case options.GitHubProvider:
@@ -189,9 +193,9 @@ func parseCodeChallengeMethod(providerConfig options.Provider) string {
189193
190194func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
191195 switch providerType {
192− case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
193− options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider,
194− options.NextCloudProvider, options.SourceHutProvider:
196+ case options.AtlassianProvider, options.BitbucketProvider, options.DigitalOceanProvider, options.DTTSSOProvider,
197+ options.FacebookProvider, options.GitHubProvider, options.GoogleProvider, options.KeycloakProvider,
198+ options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider, options.SourceHutProvider:
195199 return false, nil
196200 case options.OIDCProvider, options.ADFSProvider, options.AzureProvider, options.CidaasProvider,
197201 options.GitLabProvider, options.KeycloakOIDCProvider, options.MicrosoftEntraIDProvider:
providers/srht.go+2−1
@@ -4,6 +4,7 @@ import (
44 "bytes"
55 "context"
66 "fmt"
7+ "net/http"
78 "net/url"
89
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@@ -75,7 +76,7 @@ func NewSourceHutProvider(p *ProviderData) *SourceHutProvider {
7576func (p *SourceHutProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
7677 json, err := requests.New(p.ProfileURL.String()).
7778 WithContext(ctx).
78− WithMethod("POST").
79+ WithMethod(http.MethodPost).
7980 SetHeader("Content-Type", "application/json").
8081 SetHeader("Authorization", "Bearer "+s.AccessToken).
8182 WithBody(bytes.NewBufferString(`{"query": "{ me { username, email } }"}`)).
validator.go+4−1
@@ -110,6 +110,10 @@ func NewValidator(domains []string, usersFile string) func(string) bool {
110110
111111// isEmailValidWithDomains checks if the authenticated email is validated against the provided domain
112112func isEmailValidWithDomains(email string, allowedDomains []string) bool {
113+ if strings.Count(email, "@") != 1 {
114+ return false
115+ }
116+
113117 for _, domain := range allowedDomains {
114118 // allow if the domain is perfect suffix match with the email
115119 if strings.HasSuffix(email, "@"+domain) {
@@ -119,7 +123,6 @@ func isEmailValidWithDomains(email string, allowedDomains []string) bool {
119123 // allow if the domain is prefixed with . or *. and
120124 // the last element (split on @) has the suffix as the domain
121125 atoms := strings.Split(email, "@")
122−
123126 if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
124127 (strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
125128 return true
validator_test.go+21
@@ -404,6 +404,27 @@ func TestValidatorCases(t *testing.T) {
404404 allowedDomains: []string{"*.company.com"},
405405 expectedAuthZ: false,
406406 },
407+ {
408+ name: "CheckThatTwoAtSignsIsInvalid",
409+ email: "attacker@evil.com@company.com",
410+ allowedEmails: []string(nil),
411+ allowedDomains: []string{"company.com"},
412+ expectedAuthZ: false,
413+ },
414+ {
415+ name: "CheckThatTwoAtSignsIsInvalidEvenWithDotPrefix",
416+ email: "attacker@evil.com@company.com",
417+ allowedEmails: []string(nil),
418+ allowedDomains: []string{".company.com"},
419+ expectedAuthZ: false,
420+ },
421+ {
422+ name: "CheckThatTwoAtSignsIsInvalidEvenWithWildcardPrefix",
423+ email: "attacker@evil.com@foo.company.com",
424+ allowedEmails: []string(nil),
425+ allowedDomains: []string{"*.company.com"},
426+ expectedAuthZ: false,
427+ },
407428 }
408429
409430 for _, tc := range testCases {