git.iko.soy/typeable/oauth2-proxy
on master help sign in
.devcontainer/Dockerfile+1−1
since splitsnapshots
unifiedsplit
@@ -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
2
2
3SHELL ["/bin/bash", "-o", "pipefail", "-c"]
3SHELL ["/bin/bash", "-o", "pipefail", "-c"]
4
4
.github/ISSUE_TEMPLATE/config.yml+2−2
@@ -1,6 +1,6 @@
1blank_issues_enabled: false
1blank_issues_enabled: false
2contact_links:
2contact_links:
3− - name: OAuth2-Proxy Slack
3+ - name: OAuth2 Proxy Slack
4− url: https://gophers.slack.com/messages/CM2RSS25N
4+ url: https://cloud-native.slack.com/archives/C098Y5URZ2N
5 about: Feel free to ask any questions here.
5 about: Feel free to ask any questions here.
6
6
.github/renovate.json5+9
@@ -46,6 +46,15 @@
46 depNameTemplate: "alpine",
46 depNameTemplate: "alpine",
47 datasourceTemplate: "docker",
47 datasourceTemplate: "docker",
48 },
48 },
49+ {
50+ customType: "regex",
51+ fileMatch: [
52+ "^Makefile",
53+ ],
54+ matchStrings: [
55+ "# renovate: datasource=(?<datasource>.*?) depName=(?<depName>.*?)\\s+?GOLANGCILINT_VERSION \\?= (?<currentValue>.*)\\s",
56+ ],
57+ },
49 {
58 {
50 customType: "regex",
59 customType: "regex",
51 fileMatch: ["(^|/)\\.github/workflows/[^/]+\\.ya?ml$"],
60 fileMatch: ["(^|/)\\.github/workflows/[^/]+\\.ya?ml$"],
.github/workflows/ci.yml+8−14
@@ -19,7 +19,7 @@ jobs:
19 runs-on: ubuntu-latest
19 runs-on: ubuntu-latest
20 steps:
20 steps:
21 - name: Check out code
21 - name: Check out code
22− uses: actions/checkout@v6
22+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23 with:
23 with:
24 fetch-depth: 0
24 fetch-depth: 0
25
25
@@ -30,17 +30,11 @@ jobs:
30 id: go-version
30 id: go-version
31
31
32 - name: Set up Go
32 - name: Set up Go
33− uses: actions/setup-go@v6
33+ uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
34 with:
34 with:
35 go-version: ${{ steps.go-version.outputs.version }}
35 go-version: ${{ steps.go-version.outputs.version }}
36 check-latest: true
36 check-latest: true
37
37
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−
44 - name: Verify Code Generation
38 - name: Verify Code Generation
45 run: |
39 run: |
46 make verify-generate
40 make verify-generate
@@ -74,14 +68,14 @@ jobs:
74
68
75 - name: Upload Coverage Report
69 - name: Upload Coverage Report
76 if: github.event_name == 'push'
70 if: github.event_name == 'push'
77− uses: qltysh/qlty-action/coverage@v2
71+ uses: qltysh/qlty-action/coverage@08a0a862c159eae9b9003081da6663d96efef637 # v2.3.0
78 with:
72 with:
79 oidc: true
73 oidc: true
80 files: lcov.info
74 files: lcov.info
81
75
82 - name: Run Trivy vulnerability scanner
76 - name: Run Trivy vulnerability scanner
83 if: (!startsWith(github.head_ref, 'release'))
77 if: (!startsWith(github.head_ref, 'release'))
84− uses: aquasecurity/trivy-action@0.35.0
78+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
85 with:
79 with:
86 scan-type: 'rootfs'
80 scan-type: 'rootfs'
87 scan-ref: './oauth2-proxy'
81 scan-ref: './oauth2-proxy'
@@ -93,7 +87,7 @@ jobs:
93
87
94 - name: Upload Trivy scan results
88 - name: Upload Trivy scan results
95 if: (!startsWith(github.head_ref, 'release'))
89 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
97 with:
91 with:
98 sarif_file: 'trivy-results.sarif'
92 sarif_file: 'trivy-results.sarif'
99
93
@@ -101,16 +95,16 @@ jobs:
101 runs-on: ubuntu-latest
95 runs-on: ubuntu-latest
102 steps:
96 steps:
103 - name: Check out code
97 - name: Check out code
104− uses: actions/checkout@v6
98+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
105 with:
99 with:
106 fetch-depth: 0
100 fetch-depth: 0
107
101
108 - name: Set up QEMU
102 - name: Set up QEMU
109− uses: docker/setup-qemu-action@v3
103+ uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
110
104
111 - name: Set up Docker Buildx
105 - name: Set up Docker Buildx
112 id: buildx
106 id: buildx
113− uses: docker/setup-buildx-action@v3
107+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
114
108
115 - name: Docker Build
109 - name: Docker Build
116 if: (!startsWith(github.head_ref, 'release'))
110 if: (!startsWith(github.head_ref, 'release'))
.github/workflows/codeql.yml+4−4
@@ -32,18 +32,18 @@ jobs:
32
32
33 steps:
33 steps:
34 - name: Checkout repository
34 - name: Checkout repository
35− uses: actions/checkout@v6
35+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36
36
37 # Initializes the CodeQL tools for scanning.
37 # Initializes the CodeQL tools for scanning.
38 - name: Initialize CodeQL
38 - name: Initialize CodeQL
39− uses: github/codeql-action/init@v3
39+ uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
40 with:
40 with:
41 languages: go
41 languages: go
42
42
43 # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
43 # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
44 # If this step fails, then you should remove it and run the build manually (see below)
44 # If this step fails, then you should remove it and run the build manually (see below)
45 - name: Autobuild
45 - name: Autobuild
46− uses: github/codeql-action/autobuild@v3
46+ uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
47
47
48 # ℹ️ Command-line programs to run using the OS shell.
48 # ℹ️ Command-line programs to run using the OS shell.
49 # 📚 https://git.io/JvXDl
49 # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
57 # make release
57 # make release
58
58
59 - name: Perform CodeQL Analysis
59 - 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:
17 runs-on: ubuntu-latest
17 runs-on: ubuntu-latest
18 steps:
18 steps:
19 - name: Check out code
19 - name: Check out code
20− uses: actions/checkout@v6
20+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21 with:
21 with:
22 ref: master
22 ref: master
23 fetch-depth: 0
23 fetch-depth: 0
@@ -54,7 +54,7 @@ jobs:
54 git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
54 git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
55
55
56 - name: Setup node
56 - name: Setup node
57− uses: actions/setup-node@v6
57+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
58 with:
58 with:
59 node-version-file: docs/package.json
59 node-version-file: docs/package.json
60
60
.github/workflows/docs.yml+7−7
@@ -14,13 +14,13 @@ jobs:
14 if: github.event_name == 'pull_request'
14 if: github.event_name == 'pull_request'
15 runs-on: ubuntu-latest
15 runs-on: ubuntu-latest
16 steps:
16 steps:
17− - uses: actions/checkout@v6
17+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
18
18
19 - name: Setup Pages
19 - name: Setup Pages
20 id: pages
20 id: pages
21− uses: actions/configure-pages@v5
21+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
22
22
23− - uses: actions/setup-node@v6
23+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
24 with:
24 with:
25 # renovate: datasource=node-version depName=node
25 # renovate: datasource=node-version depName=node
26 node-version: 24
26 node-version: 24
@@ -35,9 +35,9 @@ jobs:
35 if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
35 if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
36 runs-on: ubuntu-latest
36 runs-on: ubuntu-latest
37 steps:
37 steps:
38− - uses: actions/checkout@v6
38+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39
39
40− - uses: actions/setup-node@v6
40+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
41 with:
41 with:
42 # renovate: datasource=node-version depName=node
42 # renovate: datasource=node-version depName=node
43 node-version: 24
43 node-version: 24
@@ -49,7 +49,7 @@ jobs:
49 npm run build
49 npm run build
50
50
51 - name: Upload artifact
51 - name: Upload artifact
52− uses: actions/upload-pages-artifact@v3
52+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
53 with:
53 with:
54 path: ./docs/build
54 path: ./docs/build
55
55
@@ -65,4 +65,4 @@ jobs:
65 steps:
65 steps:
66 - name: Deploy to GitHub Pages
66 - name: Deploy to GitHub Pages
67 id: deployment
67 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:
9 pull-requests: write
9 pull-requests: write
10 runs-on: ubuntu-latest
10 runs-on: ubuntu-latest
11 steps:
11 steps:
12− - uses: actions/labeler@v6
12+ - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
13 with:
13 with:
14 sync-labels: true
14 sync-labels: true
15 dot: true
15 dot: true
.github/workflows/nightly.yml+4−4
@@ -12,21 +12,21 @@ jobs:
12 if: github.repository == 'oauth2-proxy/oauth2-proxy'
12 if: github.repository == 'oauth2-proxy/oauth2-proxy'
13 steps:
13 steps:
14 - name: Check out code
14 - name: Check out code
15− uses: actions/checkout@v6
15+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
16 with:
16 with:
17 ref: master
17 ref: master
18 fetch-depth: 0
18 fetch-depth: 0
19 fetch-tags: true
19 fetch-tags: true
20
20
21 - name: Set up QEMU
21 - name: Set up QEMU
22− uses: docker/setup-qemu-action@v3
22+ uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
23
23
24 - name: Set up Docker Buildx
24 - name: Set up Docker Buildx
25 id: buildx
25 id: buildx
26− uses: docker/setup-buildx-action@v3
26+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
27
27
28 - name: Login to quay.io
28 - name: Login to quay.io
29− uses: docker/login-action@v3
29+ uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
30 with:
30 with:
31 registry: quay.io/oauth2-proxy
31 registry: quay.io/oauth2-proxy
32 username: ${{ secrets.REGISTRY_USERNAME_NIGHTLY }}
32 username: ${{ secrets.REGISTRY_USERNAME_NIGHTLY }}
.github/workflows/publish-release.yml+7−13
@@ -20,7 +20,7 @@ jobs:
20 tag: ${{ steps.tag.outputs.version }}
20 tag: ${{ steps.tag.outputs.version }}
21 steps:
21 steps:
22 - name: Check out code
22 - name: Check out code
23− uses: actions/checkout@v6
23+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24 with:
24 with:
25 ref: ${{ github.event.pull_request.merge_commit_sha }}
25 ref: ${{ github.event.pull_request.merge_commit_sha }}
26 fetch-depth: 0
26 fetch-depth: 0
@@ -50,17 +50,11 @@ jobs:
50 id: go-version
50 id: go-version
51
51
52 - name: Set up Go
52 - name: Set up Go
53− uses: actions/setup-go@v6
53+ uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
54 with:
54 with:
55 go-version: ${{ steps.go-version.outputs.version }}
55 go-version: ${{ steps.go-version.outputs.version }}
56 check-latest: true
56 check-latest: true
57
57
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−
64 - name: Get go dependencies
58 - name: Get go dependencies
65 run: |
59 run: |
66 go mod download
60 go mod download
@@ -70,7 +64,7 @@ jobs:
70
64
71 # Upload artifacts in case of workflow failure
65 # Upload artifacts in case of workflow failure
72 - name: Upload Artifacts
66 - name: Upload Artifacts
73− uses: actions/upload-artifact@v7
67+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
74 with:
68 with:
75 name: oauth2-proxy-artifacts
69 name: oauth2-proxy-artifacts
76 path: |
70 path: |
@@ -106,21 +100,21 @@ jobs:
106 runs-on: ubuntu-latest
100 runs-on: ubuntu-latest
107 steps:
101 steps:
108 - name: Check out code
102 - name: Check out code
109− uses: actions/checkout@v6
103+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
110 with:
104 with:
111 ref: ${{ needs.publish.outputs.tag }}
105 ref: ${{ needs.publish.outputs.tag }}
112 fetch-depth: 0
106 fetch-depth: 0
113 fetch-tags: true
107 fetch-tags: true
114
108
115 - name: Set up QEMU
109 - name: Set up QEMU
116− uses: docker/setup-qemu-action@v3
110+ uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
117
111
118 - name: Set up Docker Buildx
112 - name: Set up Docker Buildx
119 id: buildx
113 id: buildx
120− uses: docker/setup-buildx-action@v3
114+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
121
115
122 - name: Login to quay.io
116 - name: Login to quay.io
123− uses: docker/login-action@v3
117+ uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
124 with:
118 with:
125 registry: quay.io/oauth2-proxy
119 registry: quay.io/oauth2-proxy
126 username: ${{ secrets.REGISTRY_USERNAME }}
120 username: ${{ secrets.REGISTRY_USERNAME }}
.github/workflows/stale.yml+1−1
@@ -10,7 +10,7 @@ jobs:
10 runs-on: ubuntu-latest
10 runs-on: ubuntu-latest
11
11
12 steps:
12 steps:
13− - uses: actions/stale@v10
13+ - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
14 with:
14 with:
15 repo-token: ${{ secrets.GITHUB_TOKEN }}
15 repo-token: ${{ secrets.GITHUB_TOKEN }}
16 days-before-stale: 180
16 days-before-stale: 180
CHANGELOG.md+104
@@ -6,8 +6,112 @@
6
6
7## Breaking Changes
7## Breaking Changes
8
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)
29+
30+## Important Notes
31+
32+## Breaking Changes
33+
34+## Changes since v7.15.3
35+
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+
9## Changes since v7.15.1
105## Changes since v7.15.1
10
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+
11# V7.15.1
115# V7.15.1
12
116
13## Release Highlights
117## Release Highlights
MAINTAINERS.md+1−1
@@ -6,7 +6,7 @@ by our [project governance](GOVERNANCE.md).
6| Name | GitHub Handle | Domains of reponsibility | Email Alias | Affiliation |
6| Name | GitHub Handle | Domains of reponsibility | Email Alias | Affiliation |
7| ---------------- | ------------------------------------------------------ | ------------------------ | -------------------------- | ----------- |
7| ---------------- | ------------------------------------------------------ | ------------------------ | -------------------------- | ----------- |
8| Joel Speed | [@JoelSpeed](https://github.com/joelspeed) | Governance, Core | joel@oauth2-proxy.dev | Red Hat |
8| 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 |
10| JJ Łakis | [@jjlakis](https://github.com/jjlakis) | Provider | jj@oauth2-proxy.dev | - |
10| JJ Łakis | [@jjlakis](https://github.com/jjlakis) | Provider | jj@oauth2-proxy.dev | - |
11| Koen van Zuijlen | [@kvanzuijlen](https://github.com/kvanzuijlen) | CI | koen@oauth2-proxy.dev | - |
11| Koen van Zuijlen | [@kvanzuijlen](https://github.com/kvanzuijlen) | CI | koen@oauth2-proxy.dev | - |
12| Pierluigi Lenoci | [@pierluigilenoci](https://github.com/pierluigilenoci) | Helm | pierluigi@oauth2-proxy.dev | SAP |
12| Pierluigi Lenoci | [@pierluigilenoci](https://github.com/pierluigilenoci) | Helm | pierluigi@oauth2-proxy.dev | SAP |
Makefile+9−4
@@ -25,7 +25,8 @@ help: ## Display this help
25
25
26
26
27GO ?= go
27GO ?= go
28−GOLANGCILINT ?= golangci-lint
28+# renovate: datasource=github-tags depName=golangci/golangci-lint
29+GOLANGCILINT_VERSION ?= v2.11.4
29
30
30BINARY := oauth2-proxy
31BINARY := oauth2-proxy
31VERSION ?= $(shell git describe --always --dirty --tags 2>/dev/null || echo "undefined")
32VERSION ?= $(shell git describe --always --dirty --tags 2>/dev/null || echo "undefined")
@@ -66,7 +67,7 @@ DOCKER_BUILDX_PUSH := $(DOCKER_BUILDX) --push
66DOCKER_BUILDX_PUSH_X_PLATFORM := $(DOCKER_BUILDX_PUSH) --platform ${DOCKER_BUILD_PLATFORM}
67DOCKER_BUILDX_PUSH_X_PLATFORM := $(DOCKER_BUILDX_PUSH) --platform ${DOCKER_BUILD_PLATFORM}
67
68
68DOCKER_BUILD_PLATFORM_ALPINE ?= linux/amd64,linux/arm64,linux/ppc64le,linux/arm/v6,linux/arm/v7,linux/s390x
69DOCKER_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
70DOCKER_BUILDX_ARGS_ALPINE ?= --build-arg RUNTIME_IMAGE=${DOCKER_BUILD_RUNTIME_IMAGE_ALPINE} ${DOCKER_BUILDX_COMMON_ARGS}
71DOCKER_BUILDX_ARGS_ALPINE ?= --build-arg RUNTIME_IMAGE=${DOCKER_BUILD_RUNTIME_IMAGE_ALPINE} ${DOCKER_BUILDX_COMMON_ARGS}
71DOCKER_BUILDX_X_PLATFORM_ALPINE := docker buildx build ${DOCKER_BUILDX_ARGS_ALPINE} --platform ${DOCKER_BUILD_PLATFORM_ALPINE}
72DOCKER_BUILDX_X_PLATFORM_ALPINE := docker buildx build ${DOCKER_BUILDX_ARGS_ALPINE} --platform ${DOCKER_BUILD_PLATFORM_ALPINE}
72DOCKER_BUILDX_PUSH_X_PLATFORM_ALPINE := $(DOCKER_BUILDX_X_PLATFORM_ALPINE) --push
73DOCKER_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
143##@ Miscellaneous
144##@ Miscellaneous
144
145
145.PHONY: test
146.PHONY: test
146−test: lint ## Run all Go tests
147+test: ## Run all Go tests
147 GO111MODULE=on $(GO) test $(TESTCOVER) -v -race ./...
148 GO111MODULE=on $(GO) test $(TESTCOVER) -v -race ./...
148
149
149.PHONY: release
150.PHONY: release
@@ -157,7 +158,11 @@ clean: ## Cleanup release and build files
157
158
158.PHONY: lint
159.PHONY: lint
159lint: validate-go-version ## Lint all files using golangci-lint
160lint: 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
161
166
162.PHONY: validate-go-version
167.PHONY: validate-go-version
163validate-go-version: ## Validate Go environment requirements
168validate-go-version: ## Validate Go environment requirements
README.md+4−4
@@ -12,7 +12,7 @@
12
12
13OAuth2 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.
13OAuth2 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.
14
14
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.
16
16
17![Simplified Architecture](docs/static/img/simplified-architecture.svg)
17![Simplified Architecture](docs/static/img/simplified-architecture.svg)
18
18
@@ -23,7 +23,7 @@ OAuth2 Proxy's [Installation Docs](https://oauth2-proxy.github.io/oauth2-proxy/i
23## Releases
23## Releases
24
24
25### Binaries
25### 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`.
27
27
28Check out the [latest release](https://github.com/oauth2-proxy/oauth2-proxy/releases/latest).
28Check out the [latest release](https://github.com/oauth2-proxy/oauth2-proxy/releases/latest).
29
29
@@ -49,9 +49,9 @@ SAP Open Source Program
49Microsoft Azure credits for open source projects
49Microsoft Azure credits for open source projects
50
50
51## Getting Involved
51## 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)
53
53
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.
55
55
56OAuth2 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.
56OAuth2 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.
57
57
contrib/local-environment/Makefile+2−2
@@ -49,8 +49,8 @@ kubernetes-down:
49
49
50.PHONY: traefik-up
50.PHONY: traefik-up
51traefik-up:
51traefik-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
53
53
54.PHONY: traefik-%
54.PHONY: traefik-%
55traefik-%:
55traefik-%:
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 @@
3# This should configure Dex to run on port 5556 and provides a static login
3# This should configure Dex to run on port 5556 and provides a static login
4issuer: http://dex.localtest.me:5556/dex
4issuer: http://dex.localtest.me:5556/dex
5storage:
5storage:
6− type: etcd
6+ type: memory
7− config:
8− endpoints:
9− - http://etcd:2379
10− namespace: dex/
11web:
7web:
12 http: 0.0.0.0:5556
8 http: 0.0.0.0:5556
13oauth2:
9oauth2:
@@ -16,17 +12,17 @@ expiry:
16 signingKeys: "4h"
12 signingKeys: "4h"
17 idTokens: "1h"
13 idTokens: "1h"
18staticClients:
14staticClients:
19−- id: oauth2-proxy
15+ - id: oauth2-proxy
20− redirectURIs:
16+ redirectURIs:
21− # These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
17+ # These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
22− - 'http://oauth2-proxy.localtest.me:4180/oauth2/callback' # For basic proxy example.
18+ - "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.
19+ - "http://oauth2-proxy.localtest.me:8080/oauth2/callback" # For nginx example.
24− name: 'OAuth2 Proxy'
20+ name: "OAuth2 Proxy"
25− secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
21+ secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
26enablePasswordDB: true
22enablePasswordDB: true
27staticPasswords:
23staticPasswords:
28−- email: "admin@example.com"
24+ - email: "admin@example.com"
29− # bcrypt hash of the string "password"
25+ # bcrypt hash of the string "password"
30− hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
26+ hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
31− username: "admin"
27+ username: "admin"
32− userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
28+ userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
contrib/local-environment/docker-compose-alpha-config.yaml+2−15
@@ -1,7 +1,7 @@
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
2# for manual testing and exploration of features.
2# for manual testing and exploration of features.
3# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
3# 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.
5# This file also uses alpha configuration when configuring OAuth2 Proxy.
5# This file also uses alpha configuration when configuring OAuth2 Proxy.
6#
6#
7# This file is an extension of the main compose file and must be used with it
7# This file is an extension of the main compose file and must be used with it
@@ -14,7 +14,7 @@ version: "3.0"
14services:
14services:
15 oauth2-proxy:
15 oauth2-proxy:
16 container_name: oauth2-proxy
16 container_name: oauth2-proxy
17− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
18 command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
18 command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
19 hostname: oauth2-proxy
19 hostname: oauth2-proxy
20 volumes:
20 volumes:
@@ -43,25 +43,12 @@ services:
43 dex:
43 dex:
44 aliases:
44 aliases:
45 - dex.localtest.me
45 - dex.localtest.me
46− etcd: {}
47− depends_on:
48− - etcd
49 httpbin:
46 httpbin:
50 container_name: httpbin
47 container_name: httpbin
51 image: kennethreitz/httpbin
48 image: kennethreitz/httpbin
52 ports: []
49 ports: []
53 networks:
50 networks:
54 httpbin: {}
51 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: {}
64networks:
52networks:
65 dex: {}
53 dex: {}
66− etcd: {}
67 httpbin: {}
54 httpbin: {}
contrib/local-environment/docker-compose-gitea.yaml+2−2
@@ -14,7 +14,7 @@ version: '3.0'
14services:
14services:
15 oauth2-proxy:
15 oauth2-proxy:
16 container_name: oauth2-proxy
16 container_name: oauth2-proxy
17− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
18 command: --config /oauth2-proxy.cfg
18 command: --config /oauth2-proxy.cfg
19 hostname: oauth2-proxy
19 hostname: oauth2-proxy
20 volumes:
20 volumes:
@@ -39,7 +39,7 @@ services:
39 httpbin: {}
39 httpbin: {}
40
40
41 gitea:
41 gitea:
42− image: gitea/gitea:1.25.5
42+ image: gitea/gitea:1.27.2
43 container_name: gitea
43 container_name: gitea
44 environment:
44 environment:
45 - USER_UID=1000
45 - USER_UID=1000
contrib/local-environment/docker-compose-keycloak.yaml+6−6
@@ -10,11 +10,11 @@
10#
10#
11# Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password
11# Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password
12# Access http://keycloak.localtest.me:9080 with the same credentials to check out the settings
12# Access http://keycloak.localtest.me:9080 with the same credentials to check out the settings
13−version: '3.0'
13+version: "3.0"
14services:
14services:
15 oauth2-proxy:
15 oauth2-proxy:
16 container_name: oauth2-proxy
16 container_name: oauth2-proxy
17− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
18 command: --config /oauth2-proxy.cfg
18 command: --config /oauth2-proxy.cfg
19 hostname: oauth2-proxy
19 hostname: oauth2-proxy
20 volumes:
20 volumes:
@@ -40,12 +40,12 @@ services:
40
40
41 keycloak:
41 keycloak:
42 container_name: keycloak
42 container_name: keycloak
43− image: keycloak/keycloak:25.0
43+ image: keycloak/keycloak:26.7
44 hostname: keycloak
44 hostname: keycloak
45 command:
45 command:
46− - 'start-dev'
46+ - "start-dev"
47− - '--http-port=9080'
47+ - "--http-port=9080"
48− - '--import-realm'
48+ - "--import-realm"
49 volumes:
49 volumes:
50 - ./keycloak:/opt/keycloak/data/import
50 - ./keycloak:/opt/keycloak/data/import
51 environment:
51 environment:
contrib/local-environment/docker-compose-nginx.yaml+6−30
@@ -1,38 +1,28 @@
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
2# for manual testing and exploration of features.
2# for manual testing and exploration of features.
3# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
3# 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.
5#
5#
6# This file is an extension of the main compose file and must be used with it
6# This file is an extension of the main compose file and must be used with it
7# docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml <command>
7# docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml <command>
8# Alternatively:
8# Alternatively:
9# make nginx-<command> (eg make nginx-up, make nginx-down)
9# make nginx-<command> (eg make nginx-up, make nginx-down)
10#
10#
11−# Access one of the following URLs to initiate a login flow:
11+# Access http://oauth2-proxy.localtest.me:8080 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
22version: "3.0"
12version: "3.0"
23services:
13services:
24 oauth2-proxy:
14 oauth2-proxy:
25− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
15+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
26 ports: []
16 ports: []
27 hostname: oauth2-proxy
17 hostname: oauth2-proxy
28 container_name: oauth2-proxy
18 container_name: oauth2-proxy
29 command: --config /oauth2-proxy.cfg
19 command: --config /oauth2-proxy.cfg
20+ restart: unless-stopped
30 volumes:
21 volumes:
31 - "./oauth2-proxy-nginx.cfg:/oauth2-proxy.cfg"
22 - "./oauth2-proxy-nginx.cfg:/oauth2-proxy.cfg"
32 networks:
23 networks:
33 oauth2-proxy: {}
24 oauth2-proxy: {}
34 dex: {}
25 dex: {}
35− etcd: {}
36 httpbin: {}
26 httpbin: {}
37 depends_on:
27 depends_on:
38 - dex
28 - dex
@@ -41,17 +31,16 @@ services:
41 depends_on:
31 depends_on:
42 - oauth2-proxy
32 - oauth2-proxy
43 container_name: nginx
33 container_name: nginx
44− image: nginx:1.29
34+ image: nginx:1.31
45 restart: unless-stopped
35 restart: unless-stopped
46 ports:
36 ports:
47− - 80:80/tcp
37+ - 8080:8080/tcp
48 hostname: nginx
38 hostname: nginx
49 volumes:
39 volumes:
50 - "./nginx.conf:/etc/nginx/conf.d/default.conf"
40 - "./nginx.conf:/etc/nginx/conf.d/default.conf"
51 networks:
41 networks:
52 oauth2-proxy: {}
42 oauth2-proxy: {}
53 dex: {}
43 dex: {}
54− etcd: {}
55 httpbin: {}
44 httpbin: {}
56 dex:
45 dex:
57 container_name: dex
46 container_name: dex
@@ -67,26 +56,13 @@ services:
67 dex:
56 dex:
68 aliases:
57 aliases:
69 - dex.localtest.me
58 - dex.localtest.me
70− etcd: {}
71− depends_on:
72− - etcd
73 httpbin:
59 httpbin:
74 container_name: httpbin
60 container_name: httpbin
75 image: kennethreitz/httpbin
61 image: kennethreitz/httpbin
76 ports: []
62 ports: []
77 networks:
63 networks:
78 httpbin: {}
64 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: {}
88networks:
65networks:
89 dex: {}
66 dex: {}
90− etcd: {}
91 httpbin: {}
67 httpbin: {}
92 oauth2-proxy: {}
68 oauth2-proxy: {}
contrib/local-environment/docker-compose-traefik.yaml+42−18
@@ -1,7 +1,7 @@
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
2# for manual testing and exploration of features.
2# for manual testing and exploration of features.
3−# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
3+# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider
4−# HTTPBin as an example upstream.
4+# and HTTPBin as an example upstream.
5#
5#
6# This can either be created using docker-compose
6# This can either be created using docker-compose
7# docker-compose -f docker-compose-traefik.yaml <command>
7# docker-compose -f docker-compose-traefik.yaml <command>
@@ -9,32 +9,51 @@
9# make traefik-<command> (eg. make traefik-up, make traefik-down)
9# make traefik-<command> (eg. make traefik-up, make traefik-down)
10#
10#
11# Access one of the following URLs to initiate a login flow:
11# Access one of the following URLs to initiate a login flow:
12−# - http://oauth2-proxy.localhost
12+# - http://oauth2-proxy.localtest.me
13−# - http://httpbin.oauth2-proxy.localhost
13+# - http://httpbin.localtest.me
14+# - http://dex.localtest.me/dex
14#
15#
15−# The OAuth2 Proxy itself is hosted at http://oauth2-proxy.oauth2-proxy.localhost
16+# The OAuth2 Proxy itself is hosted at http://oauth2-proxy.localtest.me
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'
23services:
17services:
24
18
25 oauth2-proxy:
19 oauth2-proxy:
20+ container_name: oauth2-proxy
26− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
21+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
22+ command: --config /oauth2-proxy.cfg
27 ports: []
23 ports: []
28 hostname: oauth2-proxy
24 hostname: oauth2-proxy
29 volumes:
25 volumes:
30 - "./oauth2-proxy-traefik.cfg:/oauth2-proxy.cfg"
26 - "./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
31 networks:
43 networks:
32− oauth2-proxy:
44+ dex: {}
45+
46+ httpbin:
47+ container_name: httpbin
48+ image: kennethreitz/httpbin
49+ ports: []
50+ networks:
51+ httpbin: {}
33
52
34 # Reverse proxy
53 # Reverse proxy
35 gateway:
54 gateway:
36 container_name: traefik
55 container_name: traefik
37− image: traefik:v2.11.40
56+ image: traefik:v3.7.10
38 volumes:
57 volumes:
39 - "./traefik:/etc/traefik"
58 - "./traefik:/etc/traefik"
40 ports:
59 ports:
@@ -43,8 +62,13 @@ services:
43 depends_on:
62 depends_on:
44 - oauth2-proxy
63 - oauth2-proxy
45 networks:
64 networks:
46− oauth2-proxy:
65+ oauth2-proxy: {}
47− httpbin:
66+ httpbin: {}
67+ dex:
68+ aliases:
69+ - dex.localtest.me
48
70
49networks:
71networks:
72+ dex: {}
73+ httpbin: {}
50− oauth2-proxy:
74+ oauth2-proxy: {}
contrib/local-environment/docker-compose.yaml+2−15
@@ -1,7 +1,7 @@
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
2# for manual testing and exploration of features.
2# for manual testing and exploration of features.
3# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
3# 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.
5#
5#
6# This can either be created using docker-compose
6# This can either be created using docker-compose
7# docker-compose -f docker-compose.yaml <command>
7# docker-compose -f docker-compose.yaml <command>
@@ -13,7 +13,7 @@ version: "3.0"
13services:
13services:
14 oauth2-proxy:
14 oauth2-proxy:
15 container_name: oauth2-proxy
15 container_name: oauth2-proxy
16− image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
16+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
17 command: --config /oauth2-proxy.cfg
17 command: --config /oauth2-proxy.cfg
18 hostname: oauth2-proxy
18 hostname: oauth2-proxy
19 volumes:
19 volumes:
@@ -41,25 +41,12 @@ services:
41 dex:
41 dex:
42 aliases:
42 aliases:
43 - dex.localtest.me
43 - dex.localtest.me
44− etcd: {}
45− depends_on:
46− - etcd
47 httpbin:
44 httpbin:
48 container_name: httpbin
45 container_name: httpbin
49 image: kennethreitz/httpbin
46 image: kennethreitz/httpbin
50 ports: []
47 ports: []
51 networks:
48 networks:
52 httpbin: {}
49 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: {}
62networks:
50networks:
63 dex: {}
51 dex: {}
64− etcd: {}
65 httpbin: {}
52 httpbin: {}
contrib/local-environment/kubernetes/Chart.lock+3−3
@@ -1,7 +1,7 @@
1dependencies:
1dependencies:
2- name: dex
2- name: dex
3 repository: https://charts.dexidp.io
3 repository: https://charts.dexidp.io
4− version: 0.24.0
4+ version: 0.24.1
5- name: oauth2-proxy
5- name: oauth2-proxy
6 repository: https://oauth2-proxy.github.io/manifests
6 repository: https://oauth2-proxy.github.io/manifests
7 version: 7.18.0
7 version: 7.18.0
@@ -11,5 +11,5 @@ dependencies:
11- name: hello-world
11- name: hello-world
12 repository: https://conservis.github.io/helm-charts
12 repository: https://conservis.github.io/helm-charts
13 version: 1.1.0
13 version: 1.1.0
14−digest: sha256:fee913531bfb67e5555e995d8fb040c330e6eb4b5a8c777ceb9841135ea9bb84
14+digest: sha256:3fff8bf99cebcd20a73c984423d5f1c2dfa1e5a394f82a5dc60d9aaffe5361b7
15−generated: "2025-10-28T06:46:35.38300324Z"
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
3name: kubernetes
3name: kubernetes
4dependencies:
4dependencies:
5 - name: dex
5 - name: dex
6− version: 0.24.0
6+ version: 0.24.1
7 repository: https://charts.dexidp.io
7 repository: https://charts.dexidp.io
8 - name: oauth2-proxy
8 - name: oauth2-proxy
9 version: &chartVersion 7.18.0
9 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−
14# Reverse proxy to httpbin
1# Reverse proxy to httpbin
15server {
2server {
16− listen 80;
3+ listen 8080;
17− server_name httpbin.oauth2-proxy.localhost;
4+ server_name oauth2-proxy.localtest.me;
18
5
19− auth_request /internal-auth/oauth2/auth;
6+ location /oauth2/ {
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/;
7+ proxy_pass http://oauth2-proxy:4180;
27− }
8+ proxy_set_header Host $host;
28−
9+ proxy_set_header X-Real-IP $remote_addr;
29− # Named location for OAuth2 sign-in redirect
10+ proxy_set_header X-Forwarded-Uri $request_uri;
30− # Returns a proper 302 that works with --skip-provider-button
11+ proxy_set_header X-Auth-Request-Redirect $request_uri;
31− location @oauth2_signin {
32− return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
33 }
12 }
34
13
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/ {
14+ location = /oauth2/auth {
39− internal; # Ensure external users can't access this path
15+ proxy_pass http://oauth2-proxy:4180;
40−
41− # Make sure the OAuth2 Proxy knows where the original request came from.
42− proxy_set_header Host $host;
16+ proxy_set_header Host $host;
43− proxy_set_header X-Real-IP $remote_addr;
17+ proxy_set_header X-Real-IP $remote_addr;
44− proxy_set_header X-Forwarded-Uri $request_uri;
18+ proxy_set_header X-Forwarded-Uri $request_uri;
45−
19+ # nginx auth_request includes headers but not body
46− proxy_pass http://oauth2-proxy:4180/;
20+ proxy_set_header Content-Length "";
21+ proxy_pass_request_body off;
47 }
22 }
48−}
49−
50−# Statically serve the nginx welcome
51−server {
52− listen 80;
53− server_name oauth2-proxy.localhost;
54
23
55 location / {
24 location / {
56− auth_request /internal-auth/oauth2/auth;
25+ auth_request /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
60 error_page 401 = @oauth2_signin;
26 error_page 401 = @oauth2_signin;
61
27
62− root /usr/share/nginx/html;
28+ # pass information via X-User and X-Email headers to backend,
63− index index.html index.htm;
29+ # requires running with --set-xauthrequest flag
64− }
30+ auth_request_set $user $upstream_http_x_auth_request_user;
65−
31+ auth_request_set $email $upstream_http_x_auth_request_email;
66− # Named location for OAuth2 sign-in redirect
32+ proxy_set_header X-User $user;
67− # Returns a proper 302 that works with --skip-provider-button
33+ proxy_set_header X-Email $email;
68− location @oauth2_signin {
69− return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
70− }
71
34
72− # redirect server error pages to the static page /50x.html
35+ proxy_pass http://httpbin/;
73− error_page 500 502 503 504 /50x.html;
36+ # or "root /path/to/site;" or "fastcgi_pass ..." etc
74− location = /50x.html {
75− root /usr/share/nginx/html;
76 }
37 }
77
38
78− # auth_request must be a URI so this allows an internal path to then proxy to
39+ # Named location for handling OAuth2 sign-in redirects
79− # the real auth_request path.
40+ # This ensures the browser receives a proper 302 redirect that it will follow
80− # The trailing /'s are required so that nginx strips the prefix before proxying.
41+ location @oauth2_signin {
81− location /internal-auth/ {
42+ return 302 /oauth2/sign_in?rd=$scheme://$http_host$request_uri;
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/;
90 }
43 }
91}
44}
contrib/local-environment/oauth2-proxy-nginx.cfg+11−6
@@ -1,14 +1,19 @@
1http_address="0.0.0.0:4180"
1http_address="0.0.0.0:4180"
2cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
2cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3−provider="oidc"
4email_domains="example.com"
3email_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
6client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
10client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
7client_id="oauth2-proxy"
11client_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"
9
17
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.
13# Enables the use of `X-Forwarded-*` headers to determine request correctly
18# Enables the use of `X-Forwarded-*` headers to determine request correctly
14reverse_proxy="true"
19reverse_proxy="true"
contrib/local-environment/oauth2-proxy-traefik.cfg+4−4
@@ -2,14 +2,14 @@ http_address="0.0.0.0:4180"
2cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
2cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3provider="oidc"
3provider="oidc"
4email_domains=["example.com"]
4email_domains=["example.com"]
5−oidc_issuer_url="http://dex.localhost:5556/dex"
5+oidc_issuer_url="http://dex.localtest.me/dex"
6client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
6client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
7client_id="oauth2-proxy"
7client_id="oauth2-proxy"
8cookie_secure="false"
8cookie_secure="false"
9
9
10−redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback"
10+redirect_url="http://oauth2-proxy.localtest.me/oauth2/callback"
11−cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains.
11+cookie_domains=".localtest.me" # Required so cookie can be read on all subdomains.
12−whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target.
12+whitelist_domains=".localtest.me" # Required to allow redirection back to original requested target.
13
13
14# Mandatory option when using oauth2-proxy with traefik
14# Mandatory option when using oauth2-proxy with traefik
15reverse_proxy="true"
15reverse_proxy="true"
contrib/local-environment/traefik/dynamic.yaml+11−4
@@ -1,22 +1,25 @@
1http:
1http:
2 routers:
2 routers:
3 oauth2-proxy-route:
3 oauth2-proxy-route:
4− rule: "Host(`oauth2-proxy.oauth2-proxy.localhost`)"
4+ rule: "Host(`oauth2-proxy.localtest.me`)"
5 middlewares:
5 middlewares:
6 - auth-headers
6 - auth-headers
7 service: oauth-backend
7 service: oauth-backend
8+ dex-route:
9+ rule: "Host(`dex.localtest.me`)"
10+ service: dex-service
8 httpbin-route:
11 httpbin-route:
9− rule: "Host(`httpbin.oauth2-proxy.localhost`)"
12+ rule: "Host(`httpbin.localtest.me`)"
10 service: httpbin-service
13 service: httpbin-service
11 middlewares:
14 middlewares:
12 - oauth-auth-redirect # redirects all unauthenticated to oauth2 signin
15 - oauth-auth-redirect # redirects all unauthenticated to oauth2 signin
13 httpbin-route-2:
16 httpbin-route-2:
14− rule: "Host(`httpbin.oauth2-proxy.localhost`) && PathPrefix(`/no-auto-redirect`)"
17+ rule: "Host(`httpbin.localtest.me`) && PathPrefix(`/no-auto-redirect`)"
15 service: httpbin-service
18 service: httpbin-service
16 middlewares:
19 middlewares:
17 - oauth-auth-wo-redirect # unauthenticated session will return a 401
20 - oauth-auth-wo-redirect # unauthenticated session will return a 401
18 services-oauth2-route:
21 services-oauth2-route:
19− rule: "Host(`httpbin.oauth2-proxy.localhost`) && PathPrefix(`/oauth2/`)"
22+ rule: "Host(`httpbin.localtest.me`) && PathPrefix(`/oauth2/`)"
20 middlewares:
23 middlewares:
21 - auth-headers
24 - auth-headers
22 service: oauth-backend
25 service: oauth-backend
@@ -30,6 +33,10 @@ http:
30 loadBalancer:
33 loadBalancer:
31 servers:
34 servers:
32 - url: http://oauth2-proxy:4180
35 - url: http://oauth2-proxy:4180
36+ dex-service:
37+ loadBalancer:
38+ servers:
39+ - url: http://dex:5556
33
40
34 middlewares:
41 middlewares:
35 auth-headers:
42 auth-headers:
contrib/local-environment/traefik/traefik.yaml+2
@@ -5,3 +5,5 @@ log:
5providers:
5providers:
6 file:
6 file:
7 filename: /etc/traefik/dynamic.yaml
7 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:
167* `claimSource` - `claim` (session claims either from id token or from profile URL)
167* `claimSource` - `claim` (session claims either from id token or from profile URL)
168* `secretSource` - `value` (base64), `fromFile` (file path)
168* `secretSource` - `value` (base64), `fromFile` (file path)
169
169
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+
170**Request option:** `preserveRequestValue: true` retains existing header values
181**Request option:** `preserveRequestValue: true` retains existing header values
171
182
172**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
183**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.
678| Field | Type | Description |
689| Field | Type | Description |
679| ----- | ---- | ----------- |
690| ----- | ---- | ----------- |
680| `id` | _string_ | ID should be a unique identifier for the upstream.<br/>This value is required for all upstreams. |
691| `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. |
682| `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`. |
693| `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`. |
683| `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". |
694| `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". |
684| `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. |
695| `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:
167* `claimSource` - `claim` (session claims either from id token or from profile URL)
167* `claimSource` - `claim` (session claims either from id token or from profile URL)
168* `secretSource` - `value` (base64), `fromFile` (file path)
168* `secretSource` - `value` (base64), `fromFile` (file path)
169
169
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+
170**Request option:** `preserveRequestValue: true` retains existing header values
181**Request option:** `preserveRequestValue: true` retains existing header values
171
182
172**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
183**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
docs/docs/configuration/integrations/nginx.md+27−11
@@ -60,23 +60,23 @@ server {
60 auth_request_set $auth_cookie $upstream_http_set_cookie;
60 auth_request_set $auth_cookie $upstream_http_set_cookie;
61 add_header Set-Cookie $auth_cookie;
61 add_header Set-Cookie $auth_cookie;
62
62
63− # When using the --set-authorization-header flag, some provider's cookies can exceed the 4kb
63+ # Large OIDC tokens can make the session cookie exceed the 4KB limit,
64− # limit and so the OAuth2 Proxy splits these into multiple parts.
64+ # causing OAuth2 Proxy to split the cookie into multiple parts.
65 # Nginx normally only copies the first `Set-Cookie` header from the auth_request to the response,
65 # 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.
66+ # so extract the second part when using the default cookie name, `_oauth2_proxy`.
67− auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;
67+ auth_request_set $auth_cookie_upstream_1 $upstream_cookie__oauth2_proxy_1;
68
68
69 # Extract the Cookie attributes from the first Set-Cookie header and append them
69 # Extract the Cookie attributes from the first Set-Cookie header and append them
70 # to the second part ($upstream_cookie_* variables only contain the raw cookie content)
70 # to the second part ($upstream_cookie_* variables only contain the raw cookie content)
71 if ($auth_cookie ~* "(; .*)") {
71 if ($auth_cookie ~* "(; .*)") {
72− set $auth_cookie_name_0 $auth_cookie;
72+ set $auth_cookie_0 $auth_cookie;
73− set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
73+ set $auth_cookie_1 "_oauth2_proxy_1=$auth_cookie_upstream_1$1";
74 }
74 }
75
75
76 # Send both Set-Cookie headers now if there was a second part
76 # Send both Set-Cookie headers now if there was a second part
77− if ($auth_cookie_name_upstream_1) {
77+ if ($auth_cookie_upstream_1) {
78− add_header Set-Cookie $auth_cookie_name_0;
78+ add_header Set-Cookie $auth_cookie_0;
79− add_header Set-Cookie $auth_cookie_name_1;
79+ add_header Set-Cookie $auth_cookie_1;
80 }
80 }
81
81
82 proxy_pass http://backend/;
82 proxy_pass http://backend/;
@@ -91,6 +91,24 @@ server {
91}
91}
92```
92```
93
93
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+
94### Understanding the `error_page` redirect pattern
112### Understanding the `error_page` redirect pattern
95
113
96The `auth_request` directive expects the authentication endpoint (`/oauth2/auth`) to return:
114The `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
167For 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.
185For 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.
168:::
186:::
169
187
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−
172:::note
188:::note
173If 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.
189If 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.
174:::
190:::
docs/docs/configuration/overview.md+7−2
@@ -193,6 +193,10 @@ Provider specific options can be found on their respective subpages.
193
193
194### Proxy Options
194### Proxy Options
195
195
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+
196| Flag / Config Field | Type | Description | Default |
200| Flag / Config Field | Type | Description | Default |
197| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
201| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
198| 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` |
202| flag: `--allow-query-semicolons`<br/>toml: `allow_query_semicolons` | bool | allow the use of semicolons in query args ([required for some legacy applications](https://github.com/golang/go/issues/25192)) | `false` |
@@ -211,10 +215,11 @@ Provider specific options can be found on their respective subpages.
211| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
215| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
212| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
216| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
213| 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 |
217| 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"` |
214| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
219| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
215| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
220| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
216−| 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) | |
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. | |
217−| 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 | |
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. | |
218| 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 |
223| 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 |
219| flag: `--skip-provider-button`<br/>toml: `skip_provider_button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
224| flag: `--skip-provider-button`<br/>toml: `skip_provider_button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
220| flag: `--ssl-insecure-skip-verify`<br/>toml: `ssl_insecure_skip_verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
225| flag: `--ssl-insecure-skip-verify`<br/>toml: `ssl_insecure_skip_verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
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.
9Valid providers are :
9Valid providers are :
10
10
11- [ADFS](adfs.md)
11- [ADFS](adfs.md)
12+- [Atlassian Cloud](atlassian.md)
12- [Bitbucket](bitbucket.md)
13- [Bitbucket](bitbucket.md)
13- [Cidaas](cidaas.md)
14- [Cidaas](cidaas.md)
14- [CiscoDuo](cisco_duo.md)
15- [CiscoDuo](cisco_duo.md)
15- [DigitalOcean](digitalocean.md)
16- [DigitalOcean](digitalocean.md)
17+- [DTT SSO](dtt_sso.md)
16- [Facebook](facebook.md)
18- [Facebook](facebook.md)
17- [Gitea](gitea.md)
19- [Gitea](gitea.md)
18- [GitHub](github.md)
20- [GitHub](github.md)
docs/docs/installation.md+1−1
@@ -5,7 +5,7 @@ title: Installation
5
5
61. Choose how to deploy:
61. Choose how to deploy:
7
7
8− a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.1`)
8+ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.4`)
9
9
10 b. Using Go to install the latest release
10 b. Using Go to install the latest release
11 ```bash
11 ```bash
docs/docusaurus.config.js+1−1
@@ -105,7 +105,7 @@ const config = {
105 dropdownActiveClassDisabled: true,
105 dropdownActiveClassDisabled: true,
106 },
106 },
107 {
107 {
108− href: 'https://gophers.slack.com/messages/CM2RSS25N',
108+ href: 'https://cloud-native.slack.com/archives/C098Y5URZ2N',
109 label: 'Slack',
109 label: 'Slack',
110 position: 'right',
110 position: 'right',
111 },
111 },
docs/package.json+3
@@ -42,5 +42,8 @@
42 },
42 },
43 "engines": {
43 "engines": {
44 "node": ">=18.0"
44 "node": ">=18.0"
45+ },
46+ "overrides" : {
47+ "webpackbar" : "^7.0.0"
45 }
48 }
46}
49}
docs/sidebars.js+2
@@ -46,11 +46,13 @@ const sidebars = {
46 },
46 },
47 items: [
47 items: [
48 "configuration/providers/adfs",
48 "configuration/providers/adfs",
49+ "configuration/providers/atlassian",
49 "configuration/providers/azure",
50 "configuration/providers/azure",
50 "configuration/providers/bitbucket",
51 "configuration/providers/bitbucket",
51 "configuration/providers/cidaas",
52 "configuration/providers/cidaas",
52 "configuration/providers/cisco_duo",
53 "configuration/providers/cisco_duo",
53 "configuration/providers/digitalocean",
54 "configuration/providers/digitalocean",
55+ "configuration/providers/dtt_sso",
54 "configuration/providers/facebook",
56 "configuration/providers/facebook",
55 "configuration/providers/gitea",
57 "configuration/providers/gitea",
56 "configuration/providers/github",
58 "configuration/providers/github",
docs/versioned_docs/version-7.15.x/configuration/overview.md+5
@@ -193,6 +193,10 @@ Provider specific options can be found on their respective subpages.
193
193
194### Proxy Options
194### Proxy Options
195
195
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+
196| Flag / Config Field | Type | Description | Default |
200| Flag / Config Field | Type | Description | Default |
197| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
201| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
198| 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` |
202| flag: `--allow-query-semicolons`<br/>toml: `allow_query_semicolons` | bool | allow the use of semicolons in query args ([required for some legacy applications](https://github.com/golang/go/issues/25192)) | `false` |
@@ -211,6 +215,7 @@ Provider specific options can be found on their respective subpages.
211| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
215| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
212| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
216| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
213| 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 |
217| 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"` |
214| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
219| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
215| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
220| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
216| 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) | |
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) | |
docs/versioned_docs/version-7.15.x/installation.md+1−1
@@ -5,7 +5,7 @@ title: Installation
5
5
61. Choose how to deploy:
61. Choose how to deploy:
7
7
8− a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.1`)
8+ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.4`)
9
9
10 b. Using Go to install the latest release
10 b. Using Go to install the latest release
11 ```bash
11 ```bash
go.mod+42−46
@@ -1,87 +1,83 @@
1module github.com/oauth2-proxy/oauth2-proxy/v7
1module github.com/oauth2-proxy/oauth2-proxy/v7
2
2
3−go 1.25.0
3+go 1.26.0
4
4
5require (
5require (
6 cloud.google.com/go/compute/metadata v0.9.0
6 cloud.google.com/go/compute/metadata v0.9.0
7 github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb
7 github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb
8 github.com/a8m/envsubst v1.4.3
8 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
10 github.com/bitly/go-simplejson v0.5.1
10 github.com/bitly/go-simplejson v0.5.1
11− github.com/bsm/redislock v0.9.4
11+ github.com/bsm/redislock v0.10.0
12− github.com/coreos/go-oidc/v3 v3.17.0
12+ github.com/coreos/go-oidc/v3 v3.20.0
13 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
13 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
14− github.com/fsnotify/fsnotify v1.9.0
14+ github.com/fsnotify/fsnotify v1.10.1
15− github.com/go-jose/go-jose/v3 v3.0.4
15+ github.com/go-jose/go-jose/v3 v3.0.5
16− github.com/go-jose/go-jose/v4 v4.1.3
16+ github.com/go-jose/go-jose/v4 v4.1.4
17− github.com/go-viper/mapstructure/v2 v2.4.0
17+ github.com/go-viper/mapstructure/v2 v2.5.0
18 github.com/golang-jwt/jwt/v5 v5.3.1
18 github.com/golang-jwt/jwt/v5 v5.3.1
19 github.com/google/go-cmp v0.7.0
19 github.com/google/go-cmp v0.7.0
20 github.com/google/uuid v1.6.0
20 github.com/google/uuid v1.6.0
21 github.com/gorilla/mux v1.8.1
21 github.com/gorilla/mux v1.8.1
22 github.com/justinas/alice v1.2.0
22 github.com/justinas/alice v1.2.0
23 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
23 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
24− github.com/onsi/ginkgo/v2 v2.28.1
24+ github.com/onsi/ginkgo/v2 v2.32.1
25− github.com/onsi/gomega v1.39.1
25+ github.com/onsi/gomega v1.42.1
26− github.com/pierrec/lz4/v4 v4.1.26
26+ github.com/pierrec/lz4/v4 v4.1.28
27− github.com/prometheus/client_golang v1.23.2
27+ github.com/prometheus/client_golang v1.24.1
28− github.com/redis/go-redis/v9 v9.18.0
28+ github.com/redis/go-redis/v9 v9.22.0
29 github.com/spf13/cast v1.10.0
29 github.com/spf13/cast v1.10.0
30 github.com/spf13/pflag v1.0.10
30 github.com/spf13/pflag v1.0.10
31 github.com/spf13/viper v1.21.0
31 github.com/spf13/viper v1.21.0
32− github.com/stretchr/testify v1.11.1
32+ github.com/stretchr/testify v1.12.0
33 github.com/vmihailenco/msgpack/v5 v5.4.1
33 github.com/vmihailenco/msgpack/v5 v5.4.1
34− go.yaml.in/yaml/v3 v3.0.4
34+ go.yaml.in/yaml/v3 v3.0.5
35− golang.org/x/crypto v0.49.0
35+ golang.org/x/crypto v0.55.0
36− golang.org/x/net v0.52.0
36+ golang.org/x/net v0.58.0
37 golang.org/x/oauth2 v0.36.0
37 golang.org/x/oauth2 v0.36.0
38− golang.org/x/sync v0.20.0
38+ golang.org/x/sync v0.22.0
39− google.golang.org/api v0.272.0
39+ google.golang.org/api v0.293.0
40 gopkg.in/natefinch/lumberjack.v2 v2.2.1
40 gopkg.in/natefinch/lumberjack.v2 v2.2.1
41− k8s.io/apimachinery v0.35.3
41+ k8s.io/apimachinery v0.36.3
42)
42)
43
43
44require (
44require (
45− cloud.google.com/go/auth v0.18.2 // indirect
45+ cloud.google.com/go/auth v0.23.1 // indirect
46 cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
46 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
48 github.com/beorn7/perks v1.0.1 // indirect
48 github.com/beorn7/perks v1.0.1 // indirect
49 github.com/cespare/xxhash/v2 v2.3.0 // indirect
49 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
50+ github.com/felixge/httpsnoop v1.1.0 // indirect
53− github.com/go-logr/logr v1.4.3 // indirect
51+ github.com/go-logr/logr v1.4.4 // indirect
54 github.com/go-logr/stdr v1.2.2 // indirect
52 github.com/go-logr/stdr v1.2.2 // indirect
55 github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
53 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
57 github.com/google/s2a-go v0.1.9 // indirect
55 github.com/google/s2a-go v0.1.9 // indirect
58− github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
56+ github.com/googleapis/enterprise-certificate-proxy v0.3.21 // indirect
59− github.com/googleapis/gax-go/v2 v2.19.0 // indirect
57+ github.com/googleapis/gax-go/v2 v2.23.0 // indirect
60 github.com/kylelemons/godebug v1.1.0 // indirect
58 github.com/kylelemons/godebug v1.1.0 // indirect
61 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
59 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
62− github.com/pelletier/go-toml/v2 v2.2.4 // indirect
60+ github.com/pelletier/go-toml/v2 v2.4.3 // indirect
63− github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
64 github.com/prometheus/client_model v0.6.2 // indirect
61 github.com/prometheus/client_model v0.6.2 // indirect
65− github.com/prometheus/common v0.67.5 // indirect
62+ github.com/prometheus/common v0.70.1 // indirect
66− github.com/prometheus/procfs v0.20.1 // indirect
63+ github.com/prometheus/procfs v0.21.1 // indirect
67 github.com/sagikazarmark/locafero v0.12.0 // indirect
64 github.com/sagikazarmark/locafero v0.12.0 // indirect
68 github.com/spf13/afero v1.15.0 // indirect
65 github.com/spf13/afero v1.15.0 // indirect
69 github.com/subosito/gotenv v1.6.0 // indirect
66 github.com/subosito/gotenv v1.6.0 // indirect
70 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
67 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
72 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
69 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
73− go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
70+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
74− go.opentelemetry.io/otel v1.42.0 // indirect
71+ go.opentelemetry.io/otel v1.45.0 // indirect
75− go.opentelemetry.io/otel/metric v1.42.0 // indirect
72+ go.opentelemetry.io/otel/metric v1.45.0 // indirect
76− go.opentelemetry.io/otel/trace v1.42.0 // indirect
73+ go.opentelemetry.io/otel/trace v1.45.0 // indirect
77 go.uber.org/atomic v1.11.0 // indirect
74 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
75+ golang.org/x/mod v0.40.0 // indirect
80− golang.org/x/sys v0.42.0 // indirect
76+ golang.org/x/sys v0.47.0 // indirect
81− golang.org/x/text v0.35.0 // indirect
77+ golang.org/x/text v0.41.0 // indirect
82− golang.org/x/tools v0.43.0 // indirect
78+ golang.org/x/tools v0.49.0 // indirect
83− google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
79+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 // indirect
84− google.golang.org/grpc v1.79.3 // indirect
80+ google.golang.org/grpc v1.83.0 // indirect
85− google.golang.org/protobuf v1.36.11 // indirect
81+ google.golang.org/protobuf v1.36.12 // indirect
86 gopkg.in/yaml.v3 v3.0.1 // indirect
82 gopkg.in/yaml.v3 v3.0.1 // indirect
87)
83)
go.sum+100−106
@@ -1,5 +1,5 @@
1−cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
1+cloud.google.com/go/auth v0.23.1 h1:1tPpBPG02lQHmoiAvs9egyCASqXP0xgobptjZzov/Jg=
2−cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
2+cloud.google.com/go/auth v0.23.1/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
3cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
3cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
4cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
4cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
5cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
5cloud.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
8github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
8github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
9github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc=
9github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc=
10github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
10github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
11−github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
11+github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
12−github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
12+github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
13github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
13github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
14github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
14github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
15github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
15github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
16github.com/alicebob/miniredis/v2 v2.11.1/go.mod h1:UA48pmi7aSazcGAvcdKcBB49z521IC9VjTTRz2nIaJE=
16github.com/alicebob/miniredis/v2 v2.11.1/go.mod h1:UA48pmi7aSazcGAvcdKcBB49z521IC9VjTTRz2nIaJE=
17−github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
17+github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
18−github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
18+github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
19github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
19github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
20github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
20github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
21github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
21github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
@@ -24,47 +24,43 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
24github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
24github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
25github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
25github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
26github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
26github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
27−github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
27+github.com/bsm/redislock v0.10.0 h1:NAe1OHDnwPmWEzD+LpWBZXAdZrWld/xj/wmqEcQednQ=
28−github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
28+github.com/bsm/redislock v0.10.0/go.mod h1:M05WZGjPbX/8ne7LmB7AbOsa2kv7Jzx4fqkLpDFZvJs=
29github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
29github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
30github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
30github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
31github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
31github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
32github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
32github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
33github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
33github.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=
34+github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE=
35−github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
35+github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
36github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
36github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
37github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
37github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
38github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
38github.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=
39+github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
40−github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
40+github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
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=
45github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
41github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
46github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
42github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
47−github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
43+github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
48−github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
44+github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
49github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
45github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
50github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
46github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
51github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
47github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
52github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
48github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
53github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
49github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
54github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
50github.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=
51+github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
56−github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
52+github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
57−github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
53+github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
58−github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
54+github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
59github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
55github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
60−github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
56+github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
61−github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
57+github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
62github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
58github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
63github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
59github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
64github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
60github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
65github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
61github.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=
62+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
67−github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
63+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
68github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
64github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
69github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
65github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
70github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
66github.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
76github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
72github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
77github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
73github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
78github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
74github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
79−github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
75+github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
80−github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
76+github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
81github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
77github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
82github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
78github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
83github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
79github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
84github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
80github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
85github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
81github.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=
82+github.com/googleapis/enterprise-certificate-proxy v0.3.21 h1:OFdQ3tnCX/zaQ0Cedur3D3z7kI6HiLX9g3TiAN4/DFU=
87−github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
83+github.com/googleapis/enterprise-certificate-proxy v0.3.21/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
88−github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
84+github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
89−github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
85+github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
90github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
86github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
91github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
87github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
92github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
88github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
93github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
89github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
94github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
90github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
95github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
91github.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=
92+github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
97−github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
93+github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
98−github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
94+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
99−github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
95+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
100github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
96github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
101github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
97github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
102github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
98github.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
113github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
109github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
114github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk=
110github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk=
115github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw=
111github.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=
112+github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
117−github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
113+github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
118−github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
114+github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
119−github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
115+github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
120−github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
116+github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
121−github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
117+github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
122−github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
118+github.com/pierrec/lz4/v4 v4.1.28 h1:pPEPwRJ4kybBTfGt28q7lQsRJQHhC08axprdLD5Ppio=
123−github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
119+github.com/pierrec/lz4/v4 v4.1.28/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
124github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
120github.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=
121+github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
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=
122+github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
129github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
123github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
130github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
124github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
131−github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
125+github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
132−github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
126+github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
133−github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
127+github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
134−github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
128+github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
135−github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
129+github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
136−github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
130+github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
137github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
131github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
138github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
132github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
139github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
133github.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=
148github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
142github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
149github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
143github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
150github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
144github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
151−github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
145+github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
152−github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
146+github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
153github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
147github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
154github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
148github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
155github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
149github.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
167github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
161github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
168github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
162github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
169github.com/yuin/gopher-lua v0.0.0-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
163github.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=
164+github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
171−github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
165+github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
172−github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
166+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
173−github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
167+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
174go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
168go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
175go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
169go.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=
170+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
177−go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
171+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
178−go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
172+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
179−go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
173+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
180−go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
174+go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
181−go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
175+go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
182−go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
176+go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
183−go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
177+go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
184−go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
178+go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
185−go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
179+go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
186−go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
180+go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
187−go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
181+go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
188−go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
182+go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
189−go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
183+go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
190go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
184go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
191go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
185go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
192go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
186go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
193go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
187go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
194go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
188go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
195go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
189go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
196−go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
190+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
197−go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
191+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
198golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
192golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
199golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
193golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
200golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
194golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
201−golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
195+golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
202−golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
196+golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
203golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
197golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
204golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
198golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
205−golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
199+golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
206−golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
200+golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
207golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
201golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
208golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
202golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
209golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
203golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
210golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
204golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
211golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
205golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
212−golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
206+golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
213−golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
207+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
214golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
208golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
215golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
209golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
216golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
210golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
217golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
211golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
218golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
212golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
219−golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
213+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
220−golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
214+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
221golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
215golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
222golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
216golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
223golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
217golang.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
227golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
221golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
228golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
222golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
229golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
223golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
230−golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
224+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
231−golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
225+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
232golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
226golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
233golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
227golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
234golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
228golang.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=
240golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
234golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
241golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
235golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
242golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
236golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
243−golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
237+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
244−golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
238+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
245golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
239golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
246golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
240golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
247golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
241golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
248golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
242golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
249golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
243golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
250golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
244golang.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=
245+golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
252−golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
246+golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
253golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
247golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
254−gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
248+gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
255−gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
249+gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
256−google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
250+google.golang.org/api v0.293.0 h1:p9XIWOf63U4OgYx120ZwVU8+vl4XTPmWfgVPnmOAS9w=
257−google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
251+google.golang.org/api v0.293.0/go.mod h1:6n5tjEB1gzwniZTepZ0g5u+wM7Bof5GeULCx/zh8ZE0=
258−google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
252+google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
259−google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
253+google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
260−google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js=
254+google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
261−google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
255+google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
262−google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
256+google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 h1:utmqiUzZAEINbLmRyAt1QjVkMyLQ+lvIwCpLaB1rZfw=
263−google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
257+google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
264−google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
258+google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
265−google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
259+google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
266−google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
260+google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
267−google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
261+google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
268gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
262gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
269gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
263gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
270gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
264gopkg.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
273gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
267gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
274gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
268gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
275gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
269gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
276−k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
270+k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
277−k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
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"
278 Entry("with bad legacy configuration", loadConfigurationTableInput{
278 Entry("with bad legacy configuration", loadConfigurationTableInput{
279 configContent: testCoreConfig + "unknown_field=\"something\"",
279 configContent: testCoreConfig + "unknown_field=\"something\"",
280 expectedOptions: func() *options.Options { return nil },
280 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"),
282 }),
282 }),
283 Entry("with bad alpha configuration", loadConfigurationTableInput{
283 Entry("with bad alpha configuration", loadConfigurationTableInput{
284 configContent: testCoreConfig,
284 configContent: testCoreConfig,
@@ -290,7 +290,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
290 configContent: testCoreConfig + "unknown_field=\"something\"",
290 configContent: testCoreConfig + "unknown_field=\"something\"",
291 alphaConfigContent: testAlphaConfig,
291 alphaConfigContent: testAlphaConfig,
292 expectedOptions: func() *options.Options { return nil },
292 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"),
294 }),
294 }),
295 )
295 )
296
296
oauthproxy.go+29−16
@@ -59,6 +59,8 @@ const (
59)
59)
60
60
61var (
61var (
62+ defaultTrustedProxyIPs = []string{"0.0.0.0/0", "::/0"}
63+
62 // ErrNeedsLogin means the user should be redirected to the login page
64 // ErrNeedsLogin means the user should be redirected to the login page
63 ErrNeedsLogin = errors.New("redirect to login page")
65 ErrNeedsLogin = errors.New("redirect to login page")
64
66
@@ -183,13 +185,14 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
183
185
184 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)
186 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)
185
187
186− trustedIPs := ip.NewNetSet()
187− for _, ipStr := range opts.TrustedIPs {
188+ trustedIPs, err := ip.ParseNetSet(opts.TrustedIPs)
188− if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
189+ if err != nil {
189− trustedIPs.AddIPNet(*ipNet)
190+ return nil, err
190− } else {
191+ }
191− return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
192+
192− }
193+ trustedProxies, err := buildTrustedProxyNetSet(opts)
194+ if err != nil {
195+ return nil, err
193 }
196 }
194
197
195 allowedRoutes, err := buildRoutesAllowlist(opts)
198 allowedRoutes, err := buildRoutesAllowlist(opts)
@@ -202,7 +205,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
202 return nil, err
205 return nil, err
203 }
206 }
204
207
205− preAuthChain, err := buildPreAuthChain(opts, sessionStore)
208+ preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies)
206 if err != nil {
209 if err != nil {
207 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
210 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
208 }
211 }
@@ -355,8 +358,8 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
355// buildPreAuthChain constructs a chain that should process every request before
358// buildPreAuthChain constructs a chain that should process every request before
356// the OAuth2 Proxy authentication logic kicks in.
359// the OAuth2 Proxy authentication logic kicks in.
357// For example forcing HTTPS or health checks.
360// For example forcing HTTPS or health checks.
358−func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) {
361+func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore, trustedProxies *ip.NetSet) (alice.Chain, error) {
359− chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
362+ chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader, trustedProxies))
360
363
361 if opts.ForceHTTPS {
364 if opts.ForceHTTPS {
362 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
365 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
@@ -395,6 +398,16 @@ func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionSt
395 return chain, nil
398 return chain, nil
396}
399}
397
400
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+
398func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
411func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
399 chain := alice.New()
412 chain := alice.New()
400
413
@@ -574,7 +587,7 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code i
574
587
575// IsAllowedRequest is used to check if auth should be skipped for this request
588// IsAllowedRequest is used to check if auth should be skipped for this request
576func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
589func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
577− isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
590+ isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == http.MethodOptions
578 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
591 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
579}
592}
580
593
@@ -634,6 +647,10 @@ func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
634// SignInPage writes the sign in template to the response
647// SignInPage writes the sign in template to the response
635func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
648func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
636 prepareNoCache(rw)
649 prepareNoCache(rw)
650+
651+ if err := p.ClearSessionCookie(rw, req); err != nil {
652+ logger.Printf("Error clearing session cookie: %v", err)
653+ }
637 rw.WriteHeader(code)
654 rw.WriteHeader(code)
638
655
639 redirectURL, err := p.appDirector.GetRedirect(req)
656 redirectURL, err := p.appDirector.GetRedirect(req)
@@ -647,16 +664,12 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
647 redirectURL = "/"
664 redirectURL = "/"
648 }
665 }
649
666
650− if err := p.ClearSessionCookie(rw, req); err != nil {
651− logger.Printf("Error clearing session cookie: %v", err)
652− }
653−
654 p.pageWriter.WriteSignInPage(rw, req, redirectURL, code)
667 p.pageWriter.WriteSignInPage(rw, req, redirectURL, code)
655}
668}
656
669
657// ManualSignIn handles basic auth logins to the proxy
670// ManualSignIn handles basic auth logins to the proxy
658func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool, int) {
671func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool, int) {
659− if req.Method != "POST" || p.basicAuthValidator == nil {
672+ if req.Method != http.MethodPost || p.basicAuthValidator == nil {
660 return "", false, http.StatusOK
673 return "", false, http.StatusOK
661 }
674 }
662 user := req.FormValue("username")
675 user := req.FormValue("username")
oauthproxy_test.go+219−67
@@ -54,7 +54,7 @@ func TestRobotsTxt(t *testing.T) {
54 t.Fatal(err)
54 t.Fatal(err)
55 }
55 }
56 rw := httptest.NewRecorder()
56 rw := httptest.NewRecorder()
57− req, _ := http.NewRequest("GET", "/robots.txt", nil)
57+ req, _ := http.NewRequest(http.MethodGet, "/robots.txt", nil)
58 proxy.ServeHTTP(rw, req)
58 proxy.ServeHTTP(rw, req)
59 assert.Equal(t, 200, rw.Code)
59 assert.Equal(t, 200, rw.Code)
60 assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String())
60 assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String())
@@ -241,7 +241,7 @@ func TestBasicAuthPassword(t *testing.T) {
241
241
242 // Save the required session
242 // Save the required session
243 rw := httptest.NewRecorder()
243 rw := httptest.NewRecorder()
244− req, _ := http.NewRequest("GET", "/", nil)
244+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
245 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
245 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
246 Email: emailAddress,
246 Email: emailAddress,
247 })
247 })
@@ -250,7 +250,7 @@ func TestBasicAuthPassword(t *testing.T) {
250 // Extract the cookie value to inject into the test request
250 // Extract the cookie value to inject into the test request
251 cookie := rw.Header().Values("Set-Cookie")[0]
251 cookie := rw.Header().Values("Set-Cookie")[0]
252
252
253− req, _ = http.NewRequest("GET", "/", nil)
253+ req, _ = http.NewRequest(http.MethodGet, "/", nil)
254 req.Header.Set("Cookie", cookie)
254 req.Header.Set("Cookie", cookie)
255 rw = httptest.NewRecorder()
255 rw = httptest.NewRecorder()
256 proxy.ServeHTTP(rw, req)
256 proxy.ServeHTTP(rw, req)
@@ -300,14 +300,14 @@ func TestPassGroupsHeadersWithGroups(t *testing.T) {
300
300
301 // Save the required session
301 // Save the required session
302 rw := httptest.NewRecorder()
302 rw := httptest.NewRecorder()
303− req, _ := http.NewRequest("GET", "/", nil)
303+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
304 err = proxy.sessionStore.Save(rw, req, session)
304 err = proxy.sessionStore.Save(rw, req, session)
305 assert.NoError(t, err)
305 assert.NoError(t, err)
306
306
307 // Extract the cookie value to inject into the test request
307 // Extract the cookie value to inject into the test request
308 cookie := rw.Header().Values("Set-Cookie")[0]
308 cookie := rw.Header().Values("Set-Cookie")[0]
309
309
310− req, _ = http.NewRequest("GET", "/", nil)
310+ req, _ = http.NewRequest(http.MethodGet, "/", nil)
311 req.Header.Set("Cookie", cookie)
311 req.Header.Set("Cookie", cookie)
312 rw = httptest.NewRecorder()
312 rw = httptest.NewRecorder()
313 proxy.ServeHTTP(rw, req)
313 proxy.ServeHTTP(rw, req)
@@ -457,7 +457,7 @@ func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoin
457 return 0, ""
457 return 0, ""
458 }
458 }
459
459
460− req, err := http.NewRequest("GET", endpoint, strings.NewReader(""))
460+ req, err := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
461 if err != nil {
461 if err != nil {
462 return 0, ""
462 return 0, ""
463 }
463 }
@@ -608,7 +608,7 @@ func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) {
608
608
609func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
609func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
610 rw := httptest.NewRecorder()
610 rw := httptest.NewRecorder()
611− req, _ := http.NewRequest("GET", endpoint, strings.NewReader(""))
611+ req, _ := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
612 sipTest.proxy.ServeHTTP(rw, req)
612 sipTest.proxy.ServeHTTP(rw, req)
613 return rw.Code, rw.Body.String()
613 return rw.Code, rw.Body.String()
614}
614}
@@ -713,6 +713,50 @@ func TestManualSignInCorrectCredentials(t *testing.T) {
713 assert.Equal(t, http.StatusFound, statusCode)
713 assert.Equal(t, http.StatusFound, statusCode)
714}
714}
715
715
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+
716func TestSignInPageIncludesTargetRedirect(t *testing.T) {
760func TestSignInPageIncludesTargetRedirect(t *testing.T) {
717 sipTest, err := NewSignInPageTest(false)
761 sipTest, err := NewSignInPageTest(false)
718 if err != nil {
762 if err != nil {
@@ -850,7 +894,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
850 // access_token validation.
894 // access_token validation.
851 pcTest.proxy.CookieOptions.Refresh = time.Duration(0)
895 pcTest.proxy.CookieOptions.Refresh = time.Duration(0)
852 pcTest.rw = httptest.NewRecorder()
896 pcTest.rw = httptest.NewRecorder()
853− pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
897+ pcTest.req, _ = http.NewRequest(http.MethodGet, "/", strings.NewReader(""))
854 pcTest.validateUser = true
898 pcTest.validateUser = true
855 return &pcTest, nil
899 return &pcTest, nil
856}
900}
@@ -983,7 +1027,7 @@ func NewUserInfoEndpointTest() (*ProcessCookieTest, error) {
983 if err != nil {
1027 if err != nil {
984 return nil, err
1028 return nil, err
985 }
1029 }
986− pcTest.req, _ = http.NewRequest("GET",
1030+ pcTest.req, _ = http.NewRequest(http.MethodGet,
987 pcTest.opts.ProxyPrefix+"/userinfo", nil)
1031 pcTest.opts.ProxyPrefix+"/userinfo", nil)
988 return pcTest, nil
1032 return pcTest, nil
989}
1033}
@@ -1091,7 +1135,7 @@ func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (
1091 return nil, err
1135 return nil, err
1092 }
1136 }
1093 pcTest.req, _ = http.NewRequest(
1137 pcTest.req, _ = http.NewRequest(
1094− "GET",
1138+ http.MethodGet,
1095 fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
1139 fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
1096 nil)
1140 nil)
1097 return pcTest, nil
1141 return pcTest, nil
@@ -1230,7 +1274,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
1230 pcTest.validateUser = true
1274 pcTest.validateUser = true
1231
1275
1232 pcTest.rw = httptest.NewRecorder()
1276 pcTest.rw = httptest.NewRecorder()
1233− pcTest.req, _ = http.NewRequest("GET",
1277+ pcTest.req, _ = http.NewRequest(http.MethodGet,
1234 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1278 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1235
1279
1236 created := time.Now()
1280 created := time.Now()
@@ -1323,7 +1367,7 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
1323 pcTest.validateUser = true
1367 pcTest.validateUser = true
1324
1368
1325 pcTest.rw = httptest.NewRecorder()
1369 pcTest.rw = httptest.NewRecorder()
1326− pcTest.req, _ = http.NewRequest("GET",
1370+ pcTest.req, _ = http.NewRequest(http.MethodGet,
1327 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1371 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1328
1372
1329 created := time.Now()
1373 created := time.Now()
@@ -1403,7 +1447,7 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
1403 pcTest.validateUser = true
1447 pcTest.validateUser = true
1404
1448
1405 pcTest.rw = httptest.NewRecorder()
1449 pcTest.rw = httptest.NewRecorder()
1406− pcTest.req, _ = http.NewRequest("GET",
1450+ pcTest.req, _ = http.NewRequest(http.MethodGet,
1407 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1451 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1408
1452
1409 created := time.Now()
1453 created := time.Now()
@@ -1451,7 +1495,7 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
1451 }
1495 }
1452 proxy.provider = NewTestProvider(upstreamURL, "")
1496 proxy.provider = NewTestProvider(upstreamURL, "")
1453 rw := httptest.NewRecorder()
1497 rw := httptest.NewRecorder()
1454− req, _ := http.NewRequest("OPTIONS", "/preflight-request", nil)
1498+ req, _ := http.NewRequest(http.MethodOptions, "/preflight-request", nil)
1455 proxy.ServeHTTP(rw, req)
1499 proxy.ServeHTTP(rw, req)
1456
1500
1457 assert.Equal(t, 200, rw.Code)
1501 assert.Equal(t, 200, rw.Code)
@@ -1608,19 +1652,19 @@ func TestRequestSignature(t *testing.T) {
1608 resp string
1652 resp string
1609 }{
1653 }{
1610 "No request signature": {
1654 "No request signature": {
1611− method: "GET",
1655+ method: http.MethodGet,
1612 body: "",
1656 body: "",
1613 key: "",
1657 key: "",
1614 resp: "no signature received",
1658 resp: "no signature received",
1615 },
1659 },
1616 "Get request": {
1660 "Get request": {
1617− method: "GET",
1661+ method: http.MethodGet,
1618 body: "",
1662 body: "",
1619 key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
1663 key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
1620 resp: "signatures match",
1664 resp: "signatures match",
1621 },
1665 },
1622 "Post request": {
1666 "Post request": {
1623− method: "POST",
1667+ method: http.MethodPost,
1624 body: `{ "hello": "world!" }`,
1668 body: `{ "hello": "world!" }`,
1625 key: "d90df39e2d19282840252612dd7c81421a372f61",
1669 key: "d90df39e2d19282840252612dd7c81421a372f61",
1626 resp: "signatures match",
1670 resp: "signatures match",
@@ -2145,7 +2189,7 @@ func TestTrustedIPs(t *testing.T) {
2145 reverseProxy: false,
2189 reverseProxy: false,
2146 realClientIPHeader: "X-Real-IP", // Default value
2190 realClientIPHeader: "X-Real-IP", // Default value
2147 req: func() *http.Request {
2191 req: func() *http.Request {
2148− req, _ := http.NewRequest("GET", "/", nil)
2192+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2149 return req
2193 return req
2150 }(),
2194 }(),
2151 expectTrusted: false,
2195 expectTrusted: false,
@@ -2157,7 +2201,7 @@ func TestTrustedIPs(t *testing.T) {
2157 reverseProxy: false,
2201 reverseProxy: false,
2158 realClientIPHeader: "X-Real-IP",
2202 realClientIPHeader: "X-Real-IP",
2159 req: func() *http.Request {
2203 req: func() *http.Request {
2160− req, _ := http.NewRequest("GET", "/", nil)
2204+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2161 req.RemoteAddr = "@"
2205 req.RemoteAddr = "@"
2162 return req
2206 return req
2163 }(),
2207 }(),
@@ -2170,7 +2214,7 @@ func TestTrustedIPs(t *testing.T) {
2170 reverseProxy: false,
2214 reverseProxy: false,
2171 realClientIPHeader: "X-Real-IP",
2215 realClientIPHeader: "X-Real-IP",
2172 req: func() *http.Request {
2216 req: func() *http.Request {
2173− req, _ := http.NewRequest("GET", "/", nil)
2217+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2174 req.RemoteAddr = "@"
2218 req.RemoteAddr = "@"
2175 return req
2219 return req
2176 }(),
2220 }(),
@@ -2183,7 +2227,7 @@ func TestTrustedIPs(t *testing.T) {
2183 reverseProxy: false,
2227 reverseProxy: false,
2184 realClientIPHeader: "X-Real-IP", // Default value
2228 realClientIPHeader: "X-Real-IP", // Default value
2185 req: func() *http.Request {
2229 req: func() *http.Request {
2186− req, _ := http.NewRequest("GET", "/", nil)
2230+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2187 req.RemoteAddr = "127.0.0.1:43670"
2231 req.RemoteAddr = "127.0.0.1:43670"
2188 return req
2232 return req
2189 }(),
2233 }(),
@@ -2196,7 +2240,7 @@ func TestTrustedIPs(t *testing.T) {
2196 reverseProxy: true,
2240 reverseProxy: true,
2197 realClientIPHeader: "X-Real-IP", // Default value
2241 realClientIPHeader: "X-Real-IP", // Default value
2198 req: func() *http.Request {
2242 req: func() *http.Request {
2199− req, _ := http.NewRequest("GET", "/", nil)
2243+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2200 req.RemoteAddr = "127.0.0.1:44324"
2244 req.RemoteAddr = "127.0.0.1:44324"
2201 return req
2245 return req
2202 }(),
2246 }(),
@@ -2209,7 +2253,7 @@ func TestTrustedIPs(t *testing.T) {
2209 reverseProxy: true,
2253 reverseProxy: true,
2210 realClientIPHeader: "X-Forwarded-For",
2254 realClientIPHeader: "X-Forwarded-For",
2211 req: func() *http.Request {
2255 req: func() *http.Request {
2212− req, _ := http.NewRequest("GET", "/", nil)
2256+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2213 req.Header.Add("X-Forwarded-For", "127.0.0.1")
2257 req.Header.Add("X-Forwarded-For", "127.0.0.1")
2214 return req
2258 return req
2215 }(),
2259 }(),
@@ -2222,7 +2266,7 @@ func TestTrustedIPs(t *testing.T) {
2222 reverseProxy: true,
2266 reverseProxy: true,
2223 realClientIPHeader: "X-Forwarded-For",
2267 realClientIPHeader: "X-Forwarded-For",
2224 req: func() *http.Request {
2268 req: func() *http.Request {
2225− req, _ := http.NewRequest("GET", "/", nil)
2269+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2226 req.Header.Add("X-Forwarded-For", "::1")
2270 req.Header.Add("X-Forwarded-For", "::1")
2227 return req
2271 return req
2228 }(),
2272 }(),
@@ -2235,7 +2279,7 @@ func TestTrustedIPs(t *testing.T) {
2235 reverseProxy: true,
2279 reverseProxy: true,
2236 realClientIPHeader: "X-Forwarded-For",
2280 realClientIPHeader: "X-Forwarded-For",
2237 req: func() *http.Request {
2281 req: func() *http.Request {
2238− req, _ := http.NewRequest("GET", "/", nil)
2282+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2239 req.Header.Add("X-Forwarded-For", "12.34.56.78")
2283 req.Header.Add("X-Forwarded-For", "12.34.56.78")
2240 return req
2284 return req
2241 }(),
2285 }(),
@@ -2248,7 +2292,7 @@ func TestTrustedIPs(t *testing.T) {
2248 reverseProxy: true,
2292 reverseProxy: true,
2249 realClientIPHeader: "X-Forwarded-For",
2293 realClientIPHeader: "X-Forwarded-For",
2250 req: func() *http.Request {
2294 req: func() *http.Request {
2251− req, _ := http.NewRequest("GET", "/", nil)
2295+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2252 req.Header.Add("X-Forwarded-For", "::2")
2296 req.Header.Add("X-Forwarded-For", "::2")
2253 return req
2297 return req
2254 }(),
2298 }(),
@@ -2261,7 +2305,7 @@ func TestTrustedIPs(t *testing.T) {
2261 reverseProxy: true,
2305 reverseProxy: true,
2262 realClientIPHeader: "X-Forwarded-For",
2306 realClientIPHeader: "X-Forwarded-For",
2263 req: func() *http.Request {
2307 req: func() *http.Request {
2264− req, _ := http.NewRequest("GET", "/", nil)
2308+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2265 req.Header.Add("X-Real-IP", "::1")
2309 req.Header.Add("X-Real-IP", "::1")
2266 return req
2310 return req
2267 }(),
2311 }(),
@@ -2274,7 +2318,7 @@ func TestTrustedIPs(t *testing.T) {
2274 reverseProxy: true,
2318 reverseProxy: true,
2275 realClientIPHeader: "X-Forwarded-For",
2319 realClientIPHeader: "X-Forwarded-For",
2276 req: func() *http.Request {
2320 req: func() *http.Request {
2277− req, _ := http.NewRequest("GET", "/", nil)
2321+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2278 req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
2322 req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
2279 return req
2323 return req
2280 }(),
2324 }(),
@@ -2287,7 +2331,7 @@ func TestTrustedIPs(t *testing.T) {
2287 reverseProxy: false,
2331 reverseProxy: false,
2288 realClientIPHeader: "X-Real-IP",
2332 realClientIPHeader: "X-Real-IP",
2289 req: func() *http.Request {
2333 req: func() *http.Request {
2290− req, _ := http.NewRequest("GET", "/", nil)
2334+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2291 req.RemoteAddr = "adsfljk29242as!!"
2335 req.RemoteAddr = "adsfljk29242as!!"
2292 return req
2336 return req
2293 }(),
2337 }(),
@@ -2383,12 +2427,12 @@ func Test_buildRoutesAllowlist(t *testing.T) {
2383 },
2427 },
2384 expectedRoutes: []expectedAllowedRoute{
2428 expectedRoutes: []expectedAllowedRoute{
2385 {
2429 {
2386− method: "GET",
2430+ method: http.MethodGet,
2387 negate: false,
2431 negate: false,
2388 regexString: "^/foo/bar",
2432 regexString: "^/foo/bar",
2389 },
2433 },
2390 {
2434 {
2391− method: "POST",
2435+ method: http.MethodPost,
2392 negate: false,
2436 negate: false,
2393 regexString: "^/baz/[0-9]+/thing",
2437 regexString: "^/baz/[0-9]+/thing",
2394 },
2438 },
@@ -2441,11 +2485,11 @@ func Test_buildRoutesAllowlist(t *testing.T) {
2441 regexString: "^/baz/[0-9]+/thing/regex",
2485 regexString: "^/baz/[0-9]+/thing/regex",
2442 },
2486 },
2443 {
2487 {
2444− method: "GET",
2488+ method: http.MethodGet,
2445 regexString: "^/foo/bar",
2489 regexString: "^/foo/bar",
2446 },
2490 },
2447 {
2491 {
2448− method: "POST",
2492+ method: http.MethodPost,
2449 regexString: "^/baz/[0-9]+/thing",
2493 regexString: "^/baz/[0-9]+/thing",
2450 },
2494 },
2451 {
2495 {
@@ -2597,7 +2641,7 @@ func TestApiRoutes(t *testing.T) {
2597
2641
2598 for _, tc := range testCases {
2642 for _, tc := range testCases {
2599 t.Run(tc.name, func(t *testing.T) {
2643 t.Run(tc.name, func(t *testing.T) {
2600− req, err := http.NewRequest("GET", tc.url, nil)
2644+ req, err := http.NewRequest(http.MethodGet, tc.url, nil)
2601 req.Header.Set("Accept", tc.contentType)
2645 req.Header.Set("Accept", tc.contentType)
2602 assert.NoError(t, err)
2646 assert.NoError(t, err)
2603
2647
@@ -2635,9 +2679,11 @@ func TestAllowedRequest(t *testing.T) {
2635 }
2679 }
2636 opts.SkipAuthRegex = []string{
2680 opts.SkipAuthRegex = []string{
2637 "^/skip/auth/regex$",
2681 "^/skip/auth/regex$",
2682+ "^/public/.*/endpoint$",
2638 }
2683 }
2639 opts.SkipAuthRoutes = []string{
2684 opts.SkipAuthRoutes = []string{
2640 "GET=^/skip/auth/routes/get",
2685 "GET=^/skip/auth/routes/get",
2686+ "^/foo/.*/bar$",
2641 }
2687 }
2642 err := validation.Validate(opts)
2688 err := validation.Validate(opts)
2643 assert.NoError(t, err)
2689 assert.NoError(t, err)
@@ -2654,25 +2700,37 @@ func TestAllowedRequest(t *testing.T) {
2654 }{
2700 }{
2655 {
2701 {
2656 name: "Regex GET allowed",
2702 name: "Regex GET allowed",
2657− method: "GET",
2703+ method: http.MethodGet,
2658 url: "/skip/auth/regex",
2704 url: "/skip/auth/regex",
2659 allowed: true,
2705 allowed: true,
2660 },
2706 },
2661 {
2707 {
2662 name: "Regex POST allowed ",
2708 name: "Regex POST allowed ",
2663− method: "POST",
2709+ method: http.MethodPost,
2664 url: "/skip/auth/regex",
2710 url: "/skip/auth/regex",
2665 allowed: true,
2711 allowed: true,
2666 },
2712 },
2667 {
2713 {
2668 name: "Regex denied",
2714 name: "Regex denied",
2669− method: "GET",
2715+ method: http.MethodGet,
2670 url: "/wrong/denied",
2716 url: "/wrong/denied",
2671 allowed: false,
2717 allowed: false,
2672 },
2718 },
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+ },
2673 {
2731 {
2674 name: "Route allowed",
2732 name: "Route allowed",
2675− method: "GET",
2733+ method: http.MethodGet,
2676 url: "/skip/auth/routes/get",
2734 url: "/skip/auth/routes/get",
2677 allowed: true,
2735 allowed: true,
2678 },
2736 },
@@ -2684,16 +2742,28 @@ func TestAllowedRequest(t *testing.T) {
2684 },
2742 },
2685 {
2743 {
2686 name: "Route denied with wrong path",
2744 name: "Route denied with wrong path",
2687− method: "GET",
2745+ method: http.MethodGet,
2688 url: "/skip/auth/routes/wrong/path",
2746 url: "/skip/auth/routes/wrong/path",
2689 allowed: false,
2747 allowed: false,
2690 },
2748 },
2691 {
2749 {
2692 name: "Route denied with wrong path and method",
2750 name: "Route denied with wrong path and method",
2693− method: "POST",
2751+ method: http.MethodPost,
2694 url: "/skip/auth/routes/wrong/path",
2752 url: "/skip/auth/routes/wrong/path",
2695 allowed: false,
2753 allowed: false,
2696 },
2754 },
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+ },
2697 }
2767 }
2698
2768
2699 for _, tc := range testCases {
2769 for _, tc := range testCases {
@@ -2734,9 +2804,11 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2734 }
2804 }
2735 opts.SkipAuthRegex = []string{
2805 opts.SkipAuthRegex = []string{
2736 "^/skip/auth/regex$",
2806 "^/skip/auth/regex$",
2807+ "^/public/.*/endpoint$",
2737 }
2808 }
2738 opts.SkipAuthRoutes = []string{
2809 opts.SkipAuthRoutes = []string{
2739 "GET=^/skip/auth/routes/get",
2810 "GET=^/skip/auth/routes/get",
2811+ "^/foo/.*/bar$",
2740 }
2812 }
2741 err := validation.Validate(opts)
2813 err := validation.Validate(opts)
2742 assert.NoError(t, err)
2814 assert.NoError(t, err)
@@ -2753,25 +2825,37 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2753 }{
2825 }{
2754 {
2826 {
2755 name: "Regex GET allowed",
2827 name: "Regex GET allowed",
2756− method: "GET",
2828+ method: http.MethodGet,
2757 url: "/skip/auth/regex",
2829 url: "/skip/auth/regex",
2758 allowed: true,
2830 allowed: true,
2759 },
2831 },
2760 {
2832 {
2761 name: "Regex POST allowed ",
2833 name: "Regex POST allowed ",
2762− method: "POST",
2834+ method: http.MethodPost,
2763 url: "/skip/auth/regex",
2835 url: "/skip/auth/regex",
2764 allowed: true,
2836 allowed: true,
2765 },
2837 },
2766 {
2838 {
2767 name: "Regex denied",
2839 name: "Regex denied",
2768− method: "GET",
2840+ method: http.MethodGet,
2769 url: "/wrong/denied",
2841 url: "/wrong/denied",
2770 allowed: false,
2842 allowed: false,
2771 },
2843 },
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+ },
2772 {
2856 {
2773 name: "Route allowed",
2857 name: "Route allowed",
2774− method: "GET",
2858+ method: http.MethodGet,
2775 url: "/skip/auth/routes/get",
2859 url: "/skip/auth/routes/get",
2776 allowed: true,
2860 allowed: true,
2777 },
2861 },
@@ -2783,22 +2867,35 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2783 },
2867 },
2784 {
2868 {
2785 name: "Route denied with wrong path",
2869 name: "Route denied with wrong path",
2786− method: "GET",
2870+ method: http.MethodGet,
2787 url: "/skip/auth/routes/wrong/path",
2871 url: "/skip/auth/routes/wrong/path",
2788 allowed: false,
2872 allowed: false,
2789 },
2873 },
2790 {
2874 {
2791 name: "Route denied with wrong path and method",
2875 name: "Route denied with wrong path and method",
2792− method: "POST",
2876+ method: http.MethodPost,
2793 url: "/skip/auth/routes/wrong/path",
2877 url: "/skip/auth/routes/wrong/path",
2794 allowed: false,
2878 allowed: false,
2795 },
2879 },
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+ },
2796 }
2892 }
2797
2893
2798 for _, tc := range testCases {
2894 for _, tc := range testCases {
2799 t.Run(tc.name, func(t *testing.T) {
2895 t.Run(tc.name, func(t *testing.T) {
2800 req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
2896 req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
2801 req.Header.Set("X-Forwarded-Uri", tc.url)
2897 req.Header.Set("X-Forwarded-Uri", tc.url)
2898+ req.RemoteAddr = "127.0.0.1:4180"
2802 assert.NoError(t, err)
2899 assert.NoError(t, err)
2803
2900
2804 rw := httptest.NewRecorder()
2901 rw := httptest.NewRecorder()
@@ -2813,6 +2910,43 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2813 }
2910 }
2814}
2911}
2815
2912
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+
2816func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
2950func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
2817 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2951 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2818 w.WriteHeader(200)
2952 w.WriteHeader(200)
@@ -2852,37 +2986,37 @@ func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
2852 }{
2986 }{
2853 {
2987 {
2854 name: "Some static file allowed",
2988 name: "Some static file allowed",
2855− method: "GET",
2989+ method: http.MethodGet,
2856 url: "/static/file.txt",
2990 url: "/static/file.txt",
2857 allowed: true,
2991 allowed: true,
2858 },
2992 },
2859 {
2993 {
2860 name: "POST to contact form allowed",
2994 name: "POST to contact form allowed",
2861− method: "POST",
2995+ method: http.MethodPost,
2862 url: "/contact",
2996 url: "/contact",
2863 allowed: true,
2997 allowed: true,
2864 },
2998 },
2865 {
2999 {
2866 name: "Regex POST allowed",
3000 name: "Regex POST allowed",
2867− method: "POST",
3001+ method: http.MethodPost,
2868 url: "/api/public-entity",
3002 url: "/api/public-entity",
2869 allowed: true,
3003 allowed: true,
2870 },
3004 },
2871 {
3005 {
2872 name: "Regex POST with trailing slash allowed",
3006 name: "Regex POST with trailing slash allowed",
2873− method: "POST",
3007+ method: http.MethodPost,
2874 url: "/api/public-entity/",
3008 url: "/api/public-entity/",
2875 allowed: true,
3009 allowed: true,
2876 },
3010 },
2877 {
3011 {
2878 name: "Regex GET api route denied",
3012 name: "Regex GET api route denied",
2879− method: "GET",
3013+ method: http.MethodGet,
2880 url: "/api/users",
3014 url: "/api/users",
2881 allowed: false,
3015 allowed: false,
2882 },
3016 },
2883 {
3017 {
2884 name: "Regex POST api route denied",
3018 name: "Regex POST api route denied",
2885− method: "POST",
3019+ method: http.MethodPost,
2886 url: "/api/users",
3020 url: "/api/users",
2887 allowed: false,
3021 allowed: false,
2888 },
3022 },
@@ -2952,37 +3086,37 @@ func TestAllowedRequestNegateWithMethod(t *testing.T) {
2952 }{
3086 }{
2953 {
3087 {
2954 name: "Some static file allowed",
3088 name: "Some static file allowed",
2955− method: "GET",
3089+ method: http.MethodGet,
2956 url: "/static/file.txt",
3090 url: "/static/file.txt",
2957 allowed: true,
3091 allowed: true,
2958 },
3092 },
2959 {
3093 {
2960 name: "POST to contact form not allowed",
3094 name: "POST to contact form not allowed",
2961− method: "POST",
3095+ method: http.MethodPost,
2962 url: "/contact",
3096 url: "/contact",
2963 allowed: false,
3097 allowed: false,
2964 },
3098 },
2965 {
3099 {
2966 name: "Regex POST allowed",
3100 name: "Regex POST allowed",
2967− method: "POST",
3101+ method: http.MethodPost,
2968 url: "/api/public-entity",
3102 url: "/api/public-entity",
2969 allowed: true,
3103 allowed: true,
2970 },
3104 },
2971 {
3105 {
2972 name: "Regex POST with trailing slash allowed",
3106 name: "Regex POST with trailing slash allowed",
2973− method: "POST",
3107+ method: http.MethodPost,
2974 url: "/api/public-entity/",
3108 url: "/api/public-entity/",
2975 allowed: true,
3109 allowed: true,
2976 },
3110 },
2977 {
3111 {
2978 name: "Regex GET api route denied",
3112 name: "Regex GET api route denied",
2979− method: "GET",
3113+ method: http.MethodGet,
2980 url: "/api/users",
3114 url: "/api/users",
2981 allowed: false,
3115 allowed: false,
2982 },
3116 },
2983 {
3117 {
2984 name: "Regex POST api route denied",
3118 name: "Regex POST api route denied",
2985− method: "POST",
3119+ method: http.MethodPost,
2986 url: "/api/users",
3120 url: "/api/users",
2987 allowed: false,
3121 allowed: false,
2988 },
3122 },
@@ -3122,7 +3256,7 @@ func TestProxyAllowedGroups(t *testing.T) {
3122 t.Fatal(err)
3256 t.Fatal(err)
3123 }
3257 }
3124
3258
3125− test.req, _ = http.NewRequest("GET", fmt.Sprintf("/%s", tt.querystring), nil)
3259+ test.req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("/%s", tt.querystring), nil)
3126
3260
3127 test.req.Header.Add("accept", applicationJSON)
3261 test.req.Header.Add("accept", applicationJSON)
3128 err = test.SaveSession(session)
3262 err = test.SaveSession(session)
@@ -3266,7 +3400,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3266 {
3400 {
3267 name: "UserWithGroupSkipAuthPreflight",
3401 name: "UserWithGroupSkipAuthPreflight",
3268 groups: []string{"a", "c"},
3402 groups: []string{"a", "c"},
3269− method: "OPTIONS",
3403+ method: http.MethodOptions,
3270 ip: "1.2.3.5:43670",
3404 ip: "1.2.3.5:43670",
3271 withSession: true,
3405 withSession: true,
3272 expectedStatusCode: http.StatusAccepted,
3406 expectedStatusCode: http.StatusAccepted,
@@ -3274,7 +3408,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3274 {
3408 {
3275 name: "UserWithGroupTrustedIp",
3409 name: "UserWithGroupTrustedIp",
3276 groups: []string{"a", "c"},
3410 groups: []string{"a", "c"},
3277− method: "GET",
3411+ method: http.MethodGet,
3278 ip: "1.2.3.4:43670",
3412 ip: "1.2.3.4:43670",
3279 withSession: true,
3413 withSession: true,
3280 expectedStatusCode: http.StatusAccepted,
3414 expectedStatusCode: http.StatusAccepted,
@@ -3282,7 +3416,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3282 {
3416 {
3283 name: "UserWithoutGroupSkipAuthPreflight",
3417 name: "UserWithoutGroupSkipAuthPreflight",
3284 groups: []string{"c"},
3418 groups: []string{"c"},
3285− method: "OPTIONS",
3419+ method: http.MethodOptions,
3286 ip: "1.2.3.5:43670",
3420 ip: "1.2.3.5:43670",
3287 withSession: true,
3421 withSession: true,
3288 expectedStatusCode: http.StatusForbidden,
3422 expectedStatusCode: http.StatusForbidden,
@@ -3290,21 +3424,21 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3290 {
3424 {
3291 name: "UserWithoutGroupTrustedIp",
3425 name: "UserWithoutGroupTrustedIp",
3292 groups: []string{"c"},
3426 groups: []string{"c"},
3293− method: "GET",
3427+ method: http.MethodGet,
3294 ip: "1.2.3.4:43670",
3428 ip: "1.2.3.4:43670",
3295 withSession: true,
3429 withSession: true,
3296 expectedStatusCode: http.StatusForbidden,
3430 expectedStatusCode: http.StatusForbidden,
3297 },
3431 },
3298 {
3432 {
3299 name: "UserWithoutSessionSkipAuthPreflight",
3433 name: "UserWithoutSessionSkipAuthPreflight",
3300− method: "OPTIONS",
3434+ method: http.MethodOptions,
3301 ip: "1.2.3.5:43670",
3435 ip: "1.2.3.5:43670",
3302 withSession: false,
3436 withSession: false,
3303 expectedStatusCode: http.StatusAccepted,
3437 expectedStatusCode: http.StatusAccepted,
3304 },
3438 },
3305 {
3439 {
3306 name: "UserWithoutSessionTrustedIp",
3440 name: "UserWithoutSessionTrustedIp",
3307− method: "GET",
3441+ method: http.MethodGet,
3308 ip: "1.2.3.4:43670",
3442 ip: "1.2.3.4:43670",
3309 withSession: false,
3443 withSession: false,
3310 expectedStatusCode: http.StatusAccepted,
3444 expectedStatusCode: http.StatusAccepted,
@@ -3404,6 +3538,24 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
3404 querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
3538 querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
3405 expectedStatusCode: http.StatusAccepted,
3539 expectedStatusCode: http.StatusAccepted,
3406 },
3540 },
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+ },
3407 }
3559 }
3408
3560
3409 for _, tc := range testCases {
3561 for _, tc := range testCases {
@@ -3638,14 +3790,14 @@ func TestIdTokenPlaceholderInSignOut(t *testing.T) {
3638
3790
3639 // Save the required session
3791 // Save the required session
3640 rw := httptest.NewRecorder()
3792 rw := httptest.NewRecorder()
3641− req, _ := http.NewRequest("GET", "/", nil)
3793+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
3642 err = proxy.sessionStore.Save(rw, req, session)
3794 err = proxy.sessionStore.Save(rw, req, session)
3643 assert.NoError(t, err)
3795 assert.NoError(t, err)
3644
3796
3645 rw = httptest.NewRecorder()
3797 rw = httptest.NewRecorder()
3646
3798
3647 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/")
3799 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/")
3648− req, _ = http.NewRequest("GET", "/oauth2/sign_out?rd="+rdUrl, nil)
3800+ req, _ = http.NewRequest(http.MethodGet, "/oauth2/sign_out?rd="+rdUrl, nil)
3649 req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
3801 req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
3650 RequestID: "11111111-2222-4333-8444-555555555555",
3802 RequestID: "11111111-2222-4333-8444-555555555555",
3651 Session: session,
3803 Session: session,
pkg/apis/middleware/scope.go+48−1
@@ -2,9 +2,12 @@ package middleware
2
2
3import (
3import (
4 "context"
4 "context"
5+ "net"
5 "net/http"
6 "net/http"
7+ "strings"
6
8
7 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
8)
11)
9
12
10type scopeKey string
13type scopeKey string
@@ -18,9 +21,13 @@ const RequestScopeKey scopeKey = "request-scope"
18// within the chain.
21// within the chain.
19type RequestScope struct {
22type RequestScope struct {
20 // ReverseProxy tracks whether OAuth2-Proxy is operating in reverse proxy
23 // 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
22 ReverseProxy bool
25 ReverseProxy bool
23
26
27+ // TrustedProxies tracks which direct callers are allowed to supply
28+ // forwarded headers when ReverseProxy mode is enabled.
29+ TrustedProxies *ip.NetSet
30+
24 // RequestID is set to the request's `X-Request-Id` header if set.
31 // RequestID is set to the request's `X-Request-Id` header if set.
25 // Otherwise a random UUID is set.
32 // Otherwise a random UUID is set.
26 RequestID string
33 RequestID string
@@ -58,3 +65,43 @@ func AddRequestScope(req *http.Request, scope *RequestScope) *http.Request {
58 ctx := context.WithValue(req.Context(), RequestScopeKey, scope)
65 ctx := context.WithValue(req.Context(), RequestScopeKey, scope)
59 return req.WithContext(ctx)
66 return req.WithContext(ctx)
60}
67}
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 (
4 "net/http"
4 "net/http"
5
5
6 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
6 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
7 . "github.com/onsi/ginkgo/v2"
8 . "github.com/onsi/ginkgo/v2"
8 . "github.com/onsi/gomega"
9 . "github.com/onsi/gomega"
9)
10)
@@ -53,4 +54,37 @@ var _ = Describe("Scope Suite", func() {
53 })
54 })
54 })
55 })
55 })
56 })
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+ })
56})
90})
pkg/apis/options/load.go+1−1
@@ -145,7 +145,7 @@ func loadAndSubstituteEnvs(configFileName string) ([]byte, error) {
145func registerFlags(v *viper.Viper, prefix string, flagSet *pflag.FlagSet, options interface{}) error {
145func registerFlags(v *viper.Viper, prefix string, flagSet *pflag.FlagSet, options interface{}) error {
146 val := reflect.ValueOf(options)
146 val := reflect.ValueOf(options)
147 var typ reflect.Type
147 var typ reflect.Type
148− if val.Kind() == reflect.Ptr {
148+ if val.Kind() == reflect.Pointer {
149 typ = val.Elem().Type()
149 typ = val.Elem().Type()
150 } else {
150 } else {
151 typ = val.Type()
151 typ = val.Type()
pkg/apis/options/load_test.go+2−2
@@ -282,7 +282,7 @@ var _ = Describe("Load", func() {
282 Entry("with an invalid config file", &testOptionsTableInput{
282 Entry("with an invalid config file", &testOptionsTableInput{
283 configFile: []byte(`slice_option = foo`),
283 configFile: []byte(`slice_option = foo`),
284 flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
284 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\""),
286 expectedOutput: &TestOptions{},
286 expectedOutput: &TestOptions{},
287 }),
287 }),
288 Entry("with an invalid flagset", &testOptionsTableInput{
288 Entry("with an invalid flagset", &testOptionsTableInput{
@@ -329,7 +329,7 @@ var _ = Describe("Load", func() {
329 Entry("with an unknown option in the config file", &testOptionsTableInput{
329 Entry("with an unknown option in the config file", &testOptionsTableInput{
330 configFile: []byte(`unknown_option="foo"`),
330 configFile: []byte(`unknown_option="foo"`),
331 flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
331 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"),
333 // Viper will unmarshal before returning the error, so this is the default output
333 // Viper will unmarshal before returning the error, so this is the default output
334 expectedOutput: &TestOptions{
334 expectedOutput: &TestOptions{
335 StringOption: "default",
335 StringOption: "default",
pkg/apis/options/options.go+2
@@ -24,6 +24,7 @@ type Options struct {
24 ReadyPath string `flag:"ready-path" cfg:"ready_path"`
24 ReadyPath string `flag:"ready-path" cfg:"ready_path"`
25 ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
25 ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
26 RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
26 RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
27+ TrustedProxyIPs []string `flag:"trusted-proxy-ip" cfg:"trusted_proxy_ips"`
27 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
28 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
28 ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
29 ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
29 RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
30 RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
@@ -119,6 +120,7 @@ func NewFlagSet() *pflag.FlagSet {
119
120
120 flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
121 flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
121 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)")
122 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.")
122 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.")
124 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.")
123 flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
125 flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
124 flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
126 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 (
151 // ADFSProvider is the provider type for ADFS
151 // ADFSProvider is the provider type for ADFS
152 ADFSProvider ProviderType = "adfs"
152 ADFSProvider ProviderType = "adfs"
153
153
154+ // AtlassianProvider is the provider type for Atlassian Cloud
155+ AtlassianProvider ProviderType = "atlassian"
156+
154 // AzureProvider is the provider type for Azure
157 // AzureProvider is the provider type for Azure
155 AzureProvider ProviderType = "azure"
158 AzureProvider ProviderType = "azure"
156
159
@@ -166,6 +169,9 @@ const (
166 // DigitalOceanProvider is the provider type for DigitalOcean
169 // DigitalOceanProvider is the provider type for DigitalOcean
167 DigitalOceanProvider ProviderType = "digitalocean"
170 DigitalOceanProvider ProviderType = "digitalocean"
168
171
172+ // DTTSSOProvider is the provider type for the DTT SSO
173+ DTTSSOProvider ProviderType = "dtt-sso"
174+
169 // FacebookProvider is the provider type for Facebook
175 // FacebookProvider is the provider type for Facebook
170 FacebookProvider ProviderType = "facebook"
176 FacebookProvider ProviderType = "facebook"
171
177
pkg/apis/options/upstreams.go+7−5
@@ -62,12 +62,14 @@ type Upstream struct {
62
62
63 // Path is used to map requests to the upstream server.
63 // Path is used to map requests to the upstream server.
64 // The closest match will take precedence and all Paths must be unique.
64 // The closest match will take precedence and all Paths must be unique.
65− // Path can also take a pattern when used with RewriteTarget.
65+ // Without RewriteTarget, Path follows gorilla/mux route syntax.
66− // Path segments can be captured and matched using regular experessions.
67 // Eg:
66 // Eg:
68− // - `^/foo$`: Match only the explicit path `/foo`
67+ // - `/foo`: Match only the explicit path `/foo`
69− // - `^/bar/$`: Match any path prefixed with `/bar/`
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
70− // - `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget
72+ // remaining path for use with RewriteTarget.
71 Path string `yaml:"path,omitempty"`
73 Path string `yaml:"path,omitempty"`
72
74
73 // RewriteTarget allows users to rewrite the request path before it is sent to
75 // RewriteTarget allows users to rewrite the request path before it is sent to
pkg/app/redirect/director_test.go+33−2
@@ -4,6 +4,7 @@ import (
4 "net/http"
4 "net/http"
5
5
6 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
6 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
7 . "github.com/onsi/ginkgo/v2"
8 . "github.com/onsi/ginkgo/v2"
8 . "github.com/onsi/gomega"
9 . "github.com/onsi/gomega"
9)
10)
@@ -33,9 +34,16 @@ var _ = Describe("Director Suite", func() {
33 req.Header.Add(header, value)
34 req.Header.Add(header, value)
34 }
35 }
35 }
36 }
36− req = middleware.AddRequestScope(req, &middleware.RequestScope{
37+ scope := &middleware.RequestScope{
37 ReverseProxy: in.reverseProxy,
38 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)
39
47
40 redirect, err := appDirector.GetRedirect(req)
48 redirect, err := appDirector.GetRedirect(req)
41 Expect(err).ToNot(HaveOccurred())
49 Expect(err).ToNot(HaveOccurred())
@@ -174,4 +182,27 @@ var _ = Describe("Director Suite", func() {
174 expectedRedirect: "https://a-service.example.com/foo/bar",
182 expectedRedirect: "https://a-service.example.com/foo/bar",
175 }),
183 }),
176 )
184 )
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+ })
177})
208})
pkg/authentication/hmacauth/hmacauth_test.go+1−1
@@ -286,7 +286,7 @@ func TestSendAuthenticatedPostRequestToServer(t *testing.T) {
286 upstream := httptest.NewServer(
286 upstream := httptest.NewServer(
287 http.HandlerFunc(authenticator.Authenticate))
287 http.HandlerFunc(authenticator.Authenticate))
288
288
289− req, err := http.NewRequest("POST", upstream.URL+"/foo/bar",
289+ req, err := http.NewRequest(http.MethodPost, upstream.URL+"/foo/bar",
290 io.NopCloser(&fakeNetConn{reqBody: payload}))
290 io.NopCloser(&fakeNetConn{reqBody: payload}))
291 if err != nil {
291 if err != nil {
292 panic(err)
292 panic(err)
pkg/cookies/cookies_test.go+7−1
@@ -6,6 +6,7 @@ import (
6 "time"
6 "time"
7
7
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
9 . "github.com/onsi/ginkgo/v2"
10 . "github.com/onsi/ginkgo/v2"
10 . "github.com/onsi/gomega"
11 . "github.com/onsi/gomega"
11)
12)
@@ -30,8 +31,13 @@ var _ = Describe("Cookie Tests", func() {
30
31
31 if in.xForwardedHost != "" {
32 if in.xForwardedHost != "" {
32 req.Header.Add("X-Forwarded-Host", in.xForwardedHost)
33 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+
33 req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
38 req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
34− ReverseProxy: true,
39+ ReverseProxy: true,
40+ TrustedProxies: trustedProxies,
35 })
41 })
36 }
42 }
37
43
pkg/ip/parse_ip_net.go+16
@@ -1,6 +1,7 @@
1package ip
1package ip
2
2
3import (
3import (
4+ "fmt"
4 "net"
5 "net"
5 "strings"
6 "strings"
6)
7)
@@ -37,3 +38,18 @@ func ParseIPNet(s string) *net.IPNet {
37 return ipNet
38 return ipNet
38 }
39 }
39}
40}
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/middleware/healthcheck.go+7−4
@@ -43,10 +43,13 @@ func healthCheck(paths, userAgents []string, next http.Handler) http.Handler {
43
43
44func isHealthCheckRequest(paths, userAgents map[string]struct{}, req *http.Request) bool {
44func isHealthCheckRequest(paths, userAgents map[string]struct{}, req *http.Request) bool {
45 if _, ok := paths[req.URL.EscapedPath()]; ok {
45 if _, ok := paths[req.URL.EscapedPath()]; ok {
46+ if len(userAgents) == 0 {
46− return true
47+ return true
47− }
48+ }
49+
48− if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
50+ if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
49− return true
51+ return true
52+ }
50 }
53 }
51 return false
54 return false
52}
55}
pkg/middleware/healthcheck_test.go+31−9
@@ -45,6 +45,16 @@ var _ = Describe("HealthCheck suite", func() {
45 healthCheckPaths: []string{"/ping"},
45 healthCheckPaths: []string{"/ping"},
46 healthCheckUserAgents: []string{"hc/1.0"},
46 healthCheckUserAgents: []string{"hc/1.0"},
47 requestString: "http://example.com/ping",
47 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",
48 headers: map[string]string{},
58 headers: map[string]string{},
49 expectedStatus: 200,
59 expectedStatus: 200,
50 expectedBody: "OK",
60 expectedBody: "OK",
@@ -85,15 +95,25 @@ var _ = Describe("HealthCheck suite", func() {
85 expectedStatus: 404,
95 expectedStatus: 404,
86 expectedBody: "404 page not found\n",
96 expectedBody: "404 page not found\n",
87 }),
97 }),
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{
89 healthCheckPaths: []string{"/ping"},
99 healthCheckPaths: []string{"/ping"},
90 healthCheckUserAgents: []string{"hc/1.0"},
100 healthCheckUserAgents: []string{"hc/1.0"},
91 requestString: "http://example.com/abc",
101 requestString: "http://example.com/abc",
92 headers: map[string]string{
102 headers: map[string]string{
93 "User-Agent": "hc/1.0",
103 "User-Agent": "hc/1.0",
94 },
104 },
95− expectedStatus: 200,
105+ expectedStatus: 404,
96− expectedBody: "OK",
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",
97 }),
117 }),
98 Entry("when a blank string is configured as a health check agent and a request has no user agent", &requestTableInput{
118 Entry("when a blank string is configured as a health check agent and a request has no user agent", &requestTableInput{
99 healthCheckPaths: []string{"/ping"},
119 healthCheckPaths: []string{"/ping"},
@@ -107,9 +127,11 @@ var _ = Describe("HealthCheck suite", func() {
107 healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
127 healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
108 healthCheckUserAgents: []string{"hc/1.0"},
128 healthCheckUserAgents: []string{"hc/1.0"},
109 requestString: "http://example.com/readiness_check",
129 requestString: "http://example.com/readiness_check",
110− headers: map[string]string{},
130+ headers: map[string]string{
131+ "User-Agent": "hc/1.0",
132+ },
111− expectedStatus: 200,
133+ expectedStatus: 200,
112− expectedBody: "OK",
134+ expectedBody: "OK",
113 }),
135 }),
114 Entry("with multiple paths, request none of the healthcheck paths", &requestTableInput{
136 Entry("with multiple paths, request none of the healthcheck paths", &requestTableInput{
115 healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
137 healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
@@ -121,15 +143,15 @@ var _ = Describe("HealthCheck suite", func() {
121 expectedStatus: 404,
143 expectedStatus: 404,
122 expectedBody: "404 page not found\n",
144 expectedBody: "404 page not found\n",
123 }),
145 }),
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{
125 healthCheckPaths: []string{"/ping"},
147 healthCheckPaths: []string{"/ping"},
126 healthCheckUserAgents: []string{"hc/1.0", "GoogleHC/1.0"},
148 healthCheckUserAgents: []string{"hc/1.0", "GoogleHC/1.0"},
127 requestString: "http://example.com/abc",
149 requestString: "http://example.com/abc",
128 headers: map[string]string{
150 headers: map[string]string{
129 "User-Agent": "GoogleHC/1.0",
151 "User-Agent": "GoogleHC/1.0",
130 },
152 },
131− expectedStatus: 200,
153+ expectedStatus: 404,
132− expectedBody: "OK",
154+ expectedBody: "404 page not found\n",
133 }),
155 }),
134 Entry("with multiple user agents, request from none of the health check user agents", &requestTableInput{
156 Entry("with multiple user agents, request from none of the health check user agents", &requestTableInput{
135 healthCheckPaths: []string{"/ping"},
157 healthCheckPaths: []string{"/ping"},
pkg/middleware/redirect_to_https_test.go+15
@@ -6,6 +6,7 @@ import (
6 "net/http/httptest"
6 "net/http/httptest"
7
7
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
9 . "github.com/onsi/ginkgo/v2"
10 . "github.com/onsi/ginkgo/v2"
10 . "github.com/onsi/gomega"
11 . "github.com/onsi/gomega"
11)
12)
@@ -39,6 +40,10 @@ var _ = Describe("RedirectToHTTPS suite", func() {
39 scope := &middlewareapi.RequestScope{
40 scope := &middlewareapi.RequestScope{
40 ReverseProxy: in.reverseProxy,
41 ReverseProxy: in.reverseProxy,
41 }
42 }
43+ if in.reverseProxy {
44+ req.RemoteAddr = "127.0.0.1:4180"
45+ scope.TrustedProxies = newRedirectTrustedProxySet("127.0.0.1")
46+ }
42 req = middlewareapi.AddRequestScope(req, scope)
47 req = middlewareapi.AddRequestScope(req, scope)
43
48
44 rw := httptest.NewRecorder()
49 rw := httptest.NewRecorder()
@@ -207,3 +212,13 @@ var _ = Describe("RedirectToHTTPS suite", func() {
207 }),
212 }),
208 )
213 )
209})
214})
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 (
6 "github.com/google/uuid"
6 "github.com/google/uuid"
7 "github.com/justinas/alice"
7 "github.com/justinas/alice"
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
9)
10)
10
11
11−func NewScope(reverseProxy bool, idHeader string) alice.Constructor {
12+func NewScope(reverseProxy bool, idHeader string, trustedProxies *ip.NetSet) alice.Constructor {
12 return func(next http.Handler) http.Handler {
13 return func(next http.Handler) http.Handler {
13 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
14 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
14 scope := &middlewareapi.RequestScope{
15 scope := &middlewareapi.RequestScope{
15− ReverseProxy: reverseProxy,
16+ ReverseProxy: reverseProxy,
17+ TrustedProxies: trustedProxies,
16− RequestID: genRequestID(req, idHeader),
18+ RequestID: genRequestID(req, idHeader),
17 }
19 }
18 req = middlewareapi.AddRequestScope(req, scope)
20 req = middlewareapi.AddRequestScope(req, scope)
19 next.ServeHTTP(rw, req)
21 next.ServeHTTP(rw, req)
pkg/middleware/scope_test.go+18−4
@@ -6,6 +6,7 @@ import (
6
6
7 "github.com/google/uuid"
7 "github.com/google/uuid"
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
8 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
9 . "github.com/onsi/ginkgo/v2"
10 . "github.com/onsi/ginkgo/v2"
10 . "github.com/onsi/gomega"
11 . "github.com/onsi/gomega"
11)
12)
@@ -32,7 +33,7 @@ var _ = Describe("Scope Suite", func() {
32
33
33 Context("ReverseProxy is false", func() {
34 Context("ReverseProxy is false", func() {
34 BeforeEach(func() {
35 BeforeEach(func() {
35− handler := NewScope(false, testRequestHeader)(
36+ handler := NewScope(false, testRequestHeader, nil)(
36 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37 nextRequest = r
38 nextRequest = r
38 w.WriteHeader(200)
39 w.WriteHeader(200)
@@ -60,8 +61,15 @@ var _ = Describe("Scope Suite", func() {
60 })
61 })
61
62
62 Context("ReverseProxy is true", func() {
63 Context("ReverseProxy is true", func() {
64+ var trustedProxies *ip.NetSet
65+
63 BeforeEach(func() {
66 BeforeEach(func() {
67+ var err error
68+
69+ trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
70+ Expect(err).ToNot(HaveOccurred())
71+
64− handler := NewScope(true, testRequestHeader)(
72+ handler := NewScope(true, testRequestHeader, trustedProxies)(
65 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
73 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66 nextRequest = r
74 nextRequest = r
67 w.WriteHeader(200)
75 w.WriteHeader(200)
@@ -74,12 +82,18 @@ var _ = Describe("Scope Suite", func() {
74 Expect(scope).ToNot(BeNil())
82 Expect(scope).ToNot(BeNil())
75 Expect(scope.ReverseProxy).To(BeTrue())
83 Expect(scope.ReverseProxy).To(BeTrue())
76 })
84 })
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+ })
77 })
91 })
78
92
79 Context("Request ID header is present", func() {
93 Context("Request ID header is present", func() {
80 BeforeEach(func() {
94 BeforeEach(func() {
81 request.Header.Add(testRequestHeader, testRequestID)
95 request.Header.Add(testRequestHeader, testRequestID)
82− handler := NewScope(false, testRequestHeader)(
96+ handler := NewScope(false, testRequestHeader, nil)(
83 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
97 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84 nextRequest = r
98 nextRequest = r
85 w.WriteHeader(200)
99 w.WriteHeader(200)
@@ -97,7 +111,7 @@ var _ = Describe("Scope Suite", func() {
97 BeforeEach(func() {
111 BeforeEach(func() {
98 uuid.SetRand(mockRand{})
112 uuid.SetRand(mockRand{})
99
113
100− handler := NewScope(true, testRequestHeader)(
114+ handler := NewScope(true, testRequestHeader, nil)(
101 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
115 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102 nextRequest = r
116 nextRequest = r
103 w.WriteHeader(200)
117 w.WriteHeader(200)
pkg/middleware/stored_session.go+45−2
@@ -5,6 +5,7 @@ import (
5 "errors"
5 "errors"
6 "fmt"
6 "fmt"
7 "net/http"
7 "net/http"
8+ "strings"
8 "time"
9 "time"
9
10
10 "github.com/justinas/alice"
11 "github.com/justinas/alice"
@@ -31,6 +32,33 @@ const (
31 sessionRefreshRetryPeriod = 10 * time.Millisecond
32 sessionRefreshRetryPeriod = 10 * time.Millisecond
32)
33)
33
34
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+
34// StoredSessionLoaderOptions contains all of the requirements to construct
62// StoredSessionLoaderOptions contains all of the requirements to construct
35// a stored session loader.
63// a stored session loader.
36// All options must be provided.
64// All options must be provided.
@@ -188,9 +216,24 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req
188 // We are holding the lock and the session needs a refresh
216 // We are holding the lock and the session needs a refresh
189 logger.Printf("Refreshing session - User: %s; SessionAge: %s", session.User, session.Age())
217 logger.Printf("Refreshing session - User: %s; SessionAge: %s", session.User, session.Age())
190 if err := s.refreshSession(rw, req, session); err != nil {
218 if err := s.refreshSession(rw, req, session); err != nil {
191− // If a preemptive refresh fails, we still keep the session
192− // if validateSession succeeds.
193 logger.Errorf("Unable to refresh session: %v", err)
219 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
194 }
237 }
195
238
196 // Validate all sessions after any Redeem/Refresh operation (fail or success)
239 // Validate all sessions after any Redeem/Refresh operation (fail or success)
pkg/middleware/stored_session_test.go+51
@@ -7,6 +7,7 @@ import (
7 "net/http"
7 "net/http"
8 "net/http/httptest"
8 "net/http/httptest"
9 "sync"
9 "sync"
10+ "testing"
10 "time"
11 "time"
11
12
12 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
13 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
801func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
802func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
802 return nil
803 return nil
803}
804}
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() {
31
31
32 Context("with a basic request", func() {
32 Context("with a basic request", func() {
33 assertSuccessfulRequest(getBuilder, testHTTPRequest{
33 assertSuccessfulRequest(getBuilder, testHTTPRequest{
34− Method: "GET",
34+ Method: http.MethodGet,
35 Header: baseHeaders,
35 Header: baseHeaders,
36 Body: []byte{},
36 Body: []byte{},
37 RequestURI: "/json/path",
37 RequestURI: "/json/path",
@@ -52,7 +52,7 @@ var _ = Describe("Builder suite", func() {
52 })
52 })
53
53
54 assertSuccessfulRequest(getBuilder, testHTTPRequest{
54 assertSuccessfulRequest(getBuilder, testHTTPRequest{
55− Method: "GET",
55+ Method: http.MethodGet,
56 Header: baseHeaders,
56 Header: baseHeaders,
57 Body: []byte{},
57 Body: []byte{},
58 RequestURI: "/json/path",
58 RequestURI: "/json/path",
@@ -78,7 +78,7 @@ var _ = Describe("Builder suite", func() {
78 })
78 })
79
79
80 assertSuccessfulRequest(getBuilder, testHTTPRequest{
80 assertSuccessfulRequest(getBuilder, testHTTPRequest{
81− Method: "GET",
81+ Method: http.MethodGet,
82 Header: header,
82 Header: header,
83 Body: []byte(body),
83 Body: []byte(body),
84 RequestURI: "/json/path",
84 RequestURI: "/json/path",
@@ -93,11 +93,11 @@ var _ = Describe("Builder suite", func() {
93
93
94 BeforeEach(func() {
94 BeforeEach(func() {
95 buf := bytes.NewBuffer([]byte(body))
95 buf := bytes.NewBuffer([]byte(body))
96− b = b.WithMethod("POST").WithBody(buf)
96+ b = b.WithMethod(http.MethodPost).WithBody(buf)
97 })
97 })
98
98
99 assertSuccessfulRequest(getBuilder, testHTTPRequest{
99 assertSuccessfulRequest(getBuilder, testHTTPRequest{
100− Method: "POST",
100+ Method: http.MethodPost,
101 Header: header,
101 Header: header,
102 Body: []byte(body),
102 Body: []byte(body),
103 RequestURI: "/json/path",
103 RequestURI: "/json/path",
@@ -109,11 +109,11 @@ var _ = Describe("Builder suite", func() {
109 header.Set("Content-Length", "0")
109 header.Set("Content-Length", "0")
110
110
111 BeforeEach(func() {
111 BeforeEach(func() {
112− b = b.WithMethod("POST")
112+ b = b.WithMethod(http.MethodPost)
113 })
113 })
114
114
115 assertSuccessfulRequest(getBuilder, testHTTPRequest{
115 assertSuccessfulRequest(getBuilder, testHTTPRequest{
116− Method: "POST",
116+ Method: http.MethodPost,
117 Header: header,
117 Header: header,
118 Body: []byte{},
118 Body: []byte{},
119 RequestURI: "/json/path",
119 RequestURI: "/json/path",
@@ -122,11 +122,11 @@ var _ = Describe("Builder suite", func() {
122
122
123 Context("OPTIONS", func() {
123 Context("OPTIONS", func() {
124 BeforeEach(func() {
124 BeforeEach(func() {
125− b = b.WithMethod("OPTIONS")
125+ b = b.WithMethod(http.MethodOptions)
126 })
126 })
127
127
128 assertSuccessfulRequest(getBuilder, testHTTPRequest{
128 assertSuccessfulRequest(getBuilder, testHTTPRequest{
129− Method: "OPTIONS",
129+ Method: http.MethodOptions,
130 Header: baseHeaders,
130 Header: baseHeaders,
131 Body: []byte{},
131 Body: []byte{},
132 RequestURI: "/json/path",
132 RequestURI: "/json/path",
@@ -152,7 +152,7 @@ var _ = Describe("Builder suite", func() {
152 })
152 })
153
153
154 assertSuccessfulRequest(getBuilder, testHTTPRequest{
154 assertSuccessfulRequest(getBuilder, testHTTPRequest{
155− Method: "GET",
155+ Method: http.MethodGet,
156 Header: header,
156 Header: header,
157 Body: []byte{},
157 Body: []byte{},
158 RequestURI: "/json/path",
158 RequestURI: "/json/path",
@@ -170,7 +170,7 @@ var _ = Describe("Builder suite", func() {
170 })
170 })
171
171
172 assertSuccessfulRequest(getBuilder, testHTTPRequest{
172 assertSuccessfulRequest(getBuilder, testHTTPRequest{
173− Method: "GET",
173+ Method: http.MethodGet,
174 Header: replacementHeaders,
174 Header: replacementHeaders,
175 Body: []byte{},
175 Body: []byte{},
176 RequestURI: "/json/path",
176 RequestURI: "/json/path",
@@ -190,7 +190,7 @@ var _ = Describe("Builder suite", func() {
190 })
190 })
191
191
192 assertSuccessfulRequest(getBuilder, testHTTPRequest{
192 assertSuccessfulRequest(getBuilder, testHTTPRequest{
193− Method: "GET",
193+ Method: http.MethodGet,
194 Header: replacementHeaders,
194 Header: replacementHeaders,
195 Body: []byte{},
195 Body: []byte{},
196 RequestURI: "/json/path",
196 RequestURI: "/json/path",
@@ -205,7 +205,7 @@ var _ = Describe("Builder suite", func() {
205 })
205 })
206
206
207 assertSuccessfulRequest(getBuilder, testHTTPRequest{
207 assertSuccessfulRequest(getBuilder, testHTTPRequest{
208− Method: "GET",
208+ Method: http.MethodGet,
209 Header: header,
209 Header: header,
210 Body: []byte{},
210 Body: []byte{},
211 RequestURI: "/json/path",
211 RequestURI: "/json/path",
@@ -219,12 +219,12 @@ var _ = Describe("Builder suite", func() {
219 result := b.Do()
219 result := b.Do()
220 Expect(result.Error()).ToNot(HaveOccurred())
220 Expect(result.Error()).ToNot(HaveOccurred())
221
221
222− b.WithMethod("POST")
222+ b.WithMethod(http.MethodPost)
223 })
223 })
224
224
225 Context("should not redo the request", func() {
225 Context("should not redo the request", func() {
226 assertSuccessfulRequest(getBuilder, testHTTPRequest{
226 assertSuccessfulRequest(getBuilder, testHTTPRequest{
227− Method: "GET",
227+ Method: http.MethodGet,
228 Header: baseHeaders,
228 Header: baseHeaders,
229 Body: []byte{},
229 Body: []byte{},
230 RequestURI: "/json/path",
230 RequestURI: "/json/path",
pkg/requests/util/util.go+28−15
@@ -15,30 +15,30 @@ const (
15)
15)
16
16
17// GetRequestProto returns the request scheme or X-Forwarded-Proto if present
17// 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.
19func GetRequestProto(req *http.Request) string {
19func GetRequestProto(req *http.Request) string {
20 proto := req.Header.Get(XForwardedProto)
20 proto := req.Header.Get(XForwardedProto)
21− if !IsProxied(req) || proto == "" {
21+ if !CanTrustForwardedHeaders(req) || proto == "" {
22 proto = req.URL.Scheme
22 proto = req.URL.Scheme
23 }
23 }
24 return proto
24 return proto
25}
25}
26
26
27// GetRequestHost returns the request host header or X-Forwarded-Host if
27// 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.
29func GetRequestHost(req *http.Request) string {
29func GetRequestHost(req *http.Request) string {
30 host := req.Header.Get(XForwardedHost)
30 host := req.Header.Get(XForwardedHost)
31− if !IsProxied(req) || host == "" {
31+ if !CanTrustForwardedHeaders(req) || host == "" {
32 host = req.Host
32 host = req.Host
33 }
33 }
34 return host
34 return host
35}
35}
36
36
37// GetRequestURI return the request URI or X-Forwarded-Uri if present and the
37// 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.
39func GetRequestURI(req *http.Request) string {
39func GetRequestURI(req *http.Request) string {
40 uri := req.Header.Get(XForwardedURI)
40 uri := req.Header.Get(XForwardedURI)
41− if !IsProxied(req) || uri == "" {
41+ if !CanTrustForwardedHeaders(req) || uri == "" {
42 // Use RequestURI to preserve ?query
42 // Use RequestURI to preserve ?query
43 uri = req.URL.RequestURI()
43 uri = req.URL.RequestURI()
44 }
44 }
@@ -46,17 +46,29 @@ func GetRequestURI(req *http.Request) string {
46}
46}
47
47
48// GetRequestPath returns the request URI or X-Forwarded-Uri if present and the
48// 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
49+// request came from a trusted reverse proxy but always strips the query
50−// the pure path
50+// parameters and fragment suffixes and only returns the pure path.
51func GetRequestPath(req *http.Request) string {
51func GetRequestPath(req *http.Request) string {
52− uri := GetRequestURI(req)
52+ uri := stripRequestFragment(GetRequestURI(req))
53
53
54 // Parse URI and return only the path component
54 // Parse URI and return only the path component
55 if parsedURL, err := url.Parse(uri); err == nil {
55 if parsedURL, err := url.Parse(uri); err == nil {
56− return parsedURL.Path
56+ return stripRequestFragment(parsedURL.Path)
57 }
57 }
58
58
59 // Fallback: strip query parameters manually
59 // 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 {
60 if idx := strings.Index(uri, "?"); idx != -1 {
72 if idx := strings.Index(uri, "?"); idx != -1 {
61 return uri[:idx]
73 return uri[:idx]
62 }
74 }
@@ -64,17 +76,18 @@ func GetRequestPath(req *http.Request) string {
64 return uri
76 return uri
65}
77}
66
78
67−// IsProxied determines if a request was from a proxy based on the RequestScope
79+// CanTrustForwardedHeaders determines if forwarded headers should be processed
68−// ReverseProxy tracker.
80+// based on the RequestScope and the direct caller's address.
69−func IsProxied(req *http.Request) bool {
81+func CanTrustForwardedHeaders(req *http.Request) bool {
70 scope := middlewareapi.GetRequestScope(req)
82 scope := middlewareapi.GetRequestScope(req)
71 if scope == nil {
83 if scope == nil {
72 return false
84 return false
73 }
85 }
74− return scope.ReverseProxy
86+
87+ return scope.CanTrustForwardedHeaders(req)
75}
88}
76
89
77func IsForwardedRequest(req *http.Request) bool {
90func IsForwardedRequest(req *http.Request) bool {
78− return IsProxied(req) &&
91+ return CanTrustForwardedHeaders(req) &&
79 req.Host != GetRequestHost(req)
92 req.Host != GetRequestHost(req)
80}
93}
pkg/requests/util/util_test.go+74−12
@@ -6,6 +6,7 @@ import (
6 "net/http/httptest"
6 "net/http/httptest"
7
7
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
10 . "github.com/onsi/ginkgo/v2"
11 . "github.com/onsi/ginkgo/v2"
11 . "github.com/onsi/gomega"
12 . "github.com/onsi/gomega"
@@ -19,8 +20,13 @@ var _ = Describe("Util Suite", func() {
19 uriNoQueryParams = "/test/endpoint"
20 uriNoQueryParams = "/test/endpoint"
20 )
21 )
21 var req *http.Request
22 var req *http.Request
23+ var trustedProxies *ip.NetSet
22
24
23 BeforeEach(func() {
25 BeforeEach(func() {
26+ var err error
27+ trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
28+ Expect(err).ToNot(HaveOccurred())
29+
24 req = httptest.NewRequest(
30 req = httptest.NewRequest(
25 http.MethodGet,
31 http.MethodGet,
26 fmt.Sprintf("%s://%s%s", proto, host, uriWithQueryParams),
32 fmt.Sprintf("%s://%s%s", proto, host, uriWithQueryParams),
@@ -29,7 +35,7 @@ var _ = Describe("Util Suite", func() {
29 })
35 })
30
36
31 Context("GetRequestHost", func() {
37 Context("GetRequestHost", func() {
32− Context("IsProxied is false", func() {
38+ Context("trusted forwarded headers are disabled", func() {
33 BeforeEach(func() {
39 BeforeEach(func() {
34 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
40 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
35 })
41 })
@@ -44,10 +50,12 @@ var _ = Describe("Util Suite", func() {
44 })
50 })
45 })
51 })
46
52
47− Context("IsProxied is true", func() {
53+ Context("trusted forwarded headers are enabled", func() {
48 BeforeEach(func() {
54 BeforeEach(func() {
55+ req.RemoteAddr = "127.0.0.1:4180"
49 req = middleware.AddRequestScope(req, &middleware.RequestScope{
56 req = middleware.AddRequestScope(req, &middleware.RequestScope{
50− ReverseProxy: true,
57+ ReverseProxy: true,
58+ TrustedProxies: trustedProxies,
51 })
59 })
52 })
60 })
53
61
@@ -63,7 +71,7 @@ var _ = Describe("Util Suite", func() {
63 })
71 })
64
72
65 Context("GetRequestProto", func() {
73 Context("GetRequestProto", func() {
66− Context("IsProxied is false", func() {
74+ Context("trusted forwarded headers are disabled", func() {
67 BeforeEach(func() {
75 BeforeEach(func() {
68 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
76 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
69 })
77 })
@@ -78,10 +86,12 @@ var _ = Describe("Util Suite", func() {
78 })
86 })
79 })
87 })
80
88
81− Context("IsProxied is true", func() {
89+ Context("trusted forwarded headers are enabled", func() {
82 BeforeEach(func() {
90 BeforeEach(func() {
91+ req.RemoteAddr = "127.0.0.1:4180"
83 req = middleware.AddRequestScope(req, &middleware.RequestScope{
92 req = middleware.AddRequestScope(req, &middleware.RequestScope{
84− ReverseProxy: true,
93+ ReverseProxy: true,
94+ TrustedProxies: trustedProxies,
85 })
95 })
86 })
96 })
87
97
@@ -97,7 +107,7 @@ var _ = Describe("Util Suite", func() {
97 })
107 })
98
108
99 Context("GetRequestURI", func() {
109 Context("GetRequestURI", func() {
100− Context("IsProxied is false", func() {
110+ Context("trusted forwarded headers are disabled", func() {
101 BeforeEach(func() {
111 BeforeEach(func() {
102 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
112 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
103 })
113 })
@@ -112,10 +122,12 @@ var _ = Describe("Util Suite", func() {
112 })
122 })
113 })
123 })
114
124
115− Context("IsProxied is true", func() {
125+ Context("trusted forwarded headers are enabled", func() {
116 BeforeEach(func() {
126 BeforeEach(func() {
127+ req.RemoteAddr = "127.0.0.1:4180"
117 req = middleware.AddRequestScope(req, &middleware.RequestScope{
128 req = middleware.AddRequestScope(req, &middleware.RequestScope{
118− ReverseProxy: true,
129+ ReverseProxy: true,
130+ TrustedProxies: trustedProxies,
119 })
131 })
120 })
132 })
121
133
@@ -131,7 +143,7 @@ var _ = Describe("Util Suite", func() {
131 })
143 })
132
144
133 Context("GetRequestPath", func() {
145 Context("GetRequestPath", func() {
134− Context("IsProxied is false", func() {
146+ Context("trusted forwarded headers are disabled", func() {
135 BeforeEach(func() {
147 BeforeEach(func() {
136 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
148 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
137 })
149 })
@@ -140,16 +152,35 @@ var _ = Describe("Util Suite", func() {
140 Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
152 Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
141 })
153 })
142
154
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+
143 It("ignores X-Forwarded-Uri and returns the URI (without query params)", func() {
172 It("ignores X-Forwarded-Uri and returns the URI (without query params)", func() {
144 req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
173 req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
145 Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
174 Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
146 })
175 })
147 })
176 })
148
177
149− Context("IsProxied is true", func() {
178+ Context("trusted forwarded headers are enabled", func() {
150 BeforeEach(func() {
179 BeforeEach(func() {
180+ req.RemoteAddr = "127.0.0.1:4180"
151 req = middleware.AddRequestScope(req, &middleware.RequestScope{
181 req = middleware.AddRequestScope(req, &middleware.RequestScope{
152− ReverseProxy: true,
182+ ReverseProxy: true,
183+ TrustedProxies: trustedProxies,
153 })
184 })
154 })
185 })
155
186
@@ -161,6 +192,37 @@ var _ = Describe("Util Suite", func() {
161 req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
192 req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
162 Expect(util.GetRequestPath(req)).To(Equal("/some/other/path"))
193 Expect(util.GetRequestPath(req)).To(Equal("/some/other/path"))
163 })
194 })
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())
164 })
226 })
165 })
227 })
166})
228})
pkg/upstream/http.go+55−16
@@ -123,7 +123,7 @@ func (t *unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
123// The proxy should render an error page if there are failures connecting to the
123// The proxy should render an error page if there are failures connecting to the
124// upstream server.
124// upstream server.
125func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler ProxyErrorHandler) http.Handler {
125func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler ProxyErrorHandler) http.Handler {
126− proxy := httputil.NewSingleHostReverseProxy(target)
126+ proxy := newSingleHostReverseProxy(target)
127
127
128 // Inherit default transport options from Go's stdlib
128 // Inherit default transport options from Go's stdlib
129 transport := http.DefaultTransport.(*http.Transport).Clone()
129 transport := http.DefaultTransport.(*http.Transport).Clone()
@@ -155,7 +155,7 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr
155 }
155 }
156
156
157 // Ensure we always pass the original request path
157 // Ensure we always pass the original request path
158− setProxyDirector(proxy)
158+ setProxyRewrite(proxy)
159
159
160 // TODO (@tuunit) - this should be inverted or get a better name in the future to set the upstream host header
160 // TODO (@tuunit) - this should be inverted or get a better name in the future to set the upstream host header
161 // only if PassHostHeader is explicitly set to true. Currently this would be a breaking change.
161 // 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
179 return proxy
179 return proxy
180}
180}
181
181
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+
182−// setProxyUpstreamHostHeader sets the proxy.Director so that upstream requests
192+// setProxyUpstreamHostHeader sets the proxy.Rewrite so that upstream requests
183// receive a host header matching the target URL.
193// receive a host header matching the target URL.
184func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
194func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
185− director := proxy.Director
195+ rewrite := proxy.Rewrite
186− proxy.Director = func(req *http.Request) {
196+ proxy.Rewrite = func(proxyReq *httputil.ProxyRequest) {
187− director(req)
197+ rewrite(proxyReq)
188− req.Host = target.Host
198+ proxyReq.Out.Host = target.Host
189 }
199 }
190}
200}
191
201
192−// setProxyDirector sets the proxy.Director so that request URIs are escaped
202+// setProxyRewrite sets the proxy.Rewrite so that request URIs are escaped
193// when proxying to usptream servers.
203// when proxying to usptream servers.
194−func setProxyDirector(proxy *httputil.ReverseProxy) {
204+func setProxyRewrite(proxy *httputil.ReverseProxy) {
195− director := proxy.Director
205+ rewrite := proxy.Rewrite
196− proxy.Director = func(req *http.Request) {
206+ proxy.Rewrite = func(proxyReq *httputil.ProxyRequest) {
197− director(req)
207+ rewrite(proxyReq)
198 // use RequestURI so that we aren't unescaping encoded slashes in the request path
208 // use RequestURI so that we aren't unescaping encoded slashes in the request path
199− req.URL.Opaque = req.RequestURI
209+ proxyReq.Out.URL.Opaque = proxyReq.In.RequestURI
200− req.URL.RawQuery = ""
210+ proxyReq.Out.URL.RawQuery = ""
201− req.URL.ForceQuery = false
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)
202 }
241 }
203}
242}
204
243
205// newWebSocketReverseProxy creates a new reverse proxy for proxying websocket connections.
244// newWebSocketReverseProxy creates a new reverse proxy for proxying websocket connections.
206func newWebSocketReverseProxy(u *url.URL, skipTLSVerify *bool, passHostHeader *bool) http.Handler {
245func newWebSocketReverseProxy(u *url.URL, skipTLSVerify *bool, passHostHeader *bool) http.Handler {
207− wsProxy := httputil.NewSingleHostReverseProxy(u)
246+ wsProxy := newSingleHostReverseProxy(u)
208
247
209 // Inherit default transport options from Go's stdlib
248 // Inherit default transport options from Go's stdlib
210 transport := http.DefaultTransport.(*http.Transport).Clone()
249 transport := http.DefaultTransport.(*http.Transport).Clone()
pkg/upstream/http_test.go+57−3
@@ -361,7 +361,12 @@ var _ = Describe("HTTP Upstream Suite", func() {
361 return http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
361 return http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
362 proxy, ok := h.(*httputil.ReverseProxy)
362 proxy, ok := h.(*httputil.ReverseProxy)
363 Expect(ok).To(BeTrue())
363 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
365 })
370 })
366 }
371 }
367 httpUpstream.handler = requestInterceptor(httpUpstream.handler)
372 httpUpstream.handler = requestInterceptor(httpUpstream.handler)
@@ -370,6 +375,54 @@ var _ = Describe("HTTP Upstream Suite", func() {
370 Expect(req.Host).To(Equal(strings.TrimPrefix(serverAddr, "http://")))
375 Expect(req.Host).To(Equal(strings.TrimPrefix(serverAddr, "http://")))
371 })
376 })
372
377
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+
373 type newUpstreamTableInput struct {
426 type newUpstreamTableInput struct {
374 proxyWebSockets bool
427 proxyWebSockets bool
375 flushInterval time.Duration
428 flushInterval time.Duration
@@ -405,6 +458,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
405
458
406 proxy, ok := upstreamProxy.handler.(*httputil.ReverseProxy)
459 proxy, ok := upstreamProxy.handler.(*httputil.ReverseProxy)
407 Expect(ok).To(BeTrue())
460 Expect(ok).To(BeTrue())
461+ Expect(proxy.Rewrite).ToNot(BeNil())
408 Expect(proxy.FlushInterval).To(Equal(in.flushInterval))
462 Expect(proxy.FlushInterval).To(Equal(in.flushInterval))
409 transport, ok := proxy.Transport.(*http.Transport)
463 transport, ok := proxy.Transport.(*http.Transport)
410 Expect(ok).To(BeTrue())
464 Expect(ok).To(BeTrue())
@@ -498,7 +552,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
498
552
499 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
553 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
500
554
501− proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
555+ proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
502 })
556 })
503
557
504 AfterEach(func() {
558 AfterEach(func() {
@@ -549,7 +603,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
549 Expect(err).ToNot(HaveOccurred())
603 Expect(err).ToNot(HaveOccurred())
550
604
551 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
605 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))
553 defer noPassHostServer.Close()
607 defer noPassHostServer.Close()
554
608
555 origin := "http://example.localhost"
609 origin := "http://example.localhost"
pkg/upstream/proxy_test.go+1−1
@@ -79,7 +79,7 @@ var _ = Describe("Proxy Suite", func() {
79 {
79 {
80 ID: "bad-http-backend",
80 ID: "bad-http-backend",
81 Path: "/bad-http/",
81 Path: "/bad-http/",
82− URI: "http://::1",
82+ URI: invalidServer,
83 },
83 },
84 {
84 {
85 ID: "single-path-backend",
85 ID: "single-path-backend",
pkg/upstream/upstream_suite_test.go+1−1
@@ -24,7 +24,7 @@ var (
24 serverAddr string
24 serverAddr string
25 unixServer *httptest.Server
25 unixServer *httptest.Server
26 unixServerAddr string
26 unixServerAddr string
27− invalidServer = "http://::1"
27+ invalidServer = "http://127.0.0.1:1"
28)
28)
29
29
30func TestUpstreamSuite(t *testing.T) {
30func TestUpstreamSuite(t *testing.T) {
pkg/validation/allowlist.go+12
@@ -16,6 +16,7 @@ func validateAllowlists(o *options.Options) []string {
16
16
17 msgs = append(msgs, validateAuthRoutes(o)...)
17 msgs = append(msgs, validateAuthRoutes(o)...)
18 msgs = append(msgs, validateAuthRegexes(o)...)
18 msgs = append(msgs, validateAuthRegexes(o)...)
19+ msgs = append(msgs, validateTrustedProxyIPs(o)...)
19 msgs = append(msgs, validateTrustedIPs(o)...)
20 msgs = append(msgs, validateTrustedIPs(o)...)
20
21
21 if len(o.TrustedIPs) > 0 && o.ReverseProxy {
22 if len(o.TrustedIPs) > 0 && o.ReverseProxy {
@@ -28,6 +29,17 @@ func validateAllowlists(o *options.Options) []string {
28 return msgs
29 return msgs
29}
30}
30
31
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+
31// validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes
43// validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes
32func validateAuthRoutes(o *options.Options) []string {
44func validateAuthRoutes(o *options.Options) []string {
33 msgs := []string{}
45 msgs := []string{}
pkg/validation/allowlist_test.go+30
@@ -23,6 +23,11 @@ var _ = Describe("Allowlist", func() {
23 errStrings []string
23 errStrings []string
24 }
24 }
25
25
26+ type validateTrustedProxyIPsTableInput struct {
27+ trustedProxyIPs []string
28+ errStrings []string
29+ }
30+
26 DescribeTable("validateRoutes",
31 DescribeTable("validateRoutes",
27 func(r *validateRoutesTableInput) {
32 func(r *validateRoutesTableInput) {
28 opts := &options.Options{
33 opts := &options.Options{
@@ -121,4 +126,29 @@ var _ = Describe("Allowlist", func() {
121 },
126 },
122 }),
127 }),
123 )
128 )
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+ )
124})
154})
pkg/validation/sessions_test.go+2−2
@@ -193,8 +193,8 @@ var _ = Describe("Sessions", func() {
193 unreachableRedisDelMsg = "unable to delete the redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused"
193 unreachableRedisDelMsg = "unable to delete the redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused"
194 unreachableSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels specified in configuration are unreachable: redis: nil"
194 unreachableSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels specified in configuration are unreachable: redis: nil"
195 unrechableSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels specified in configuration are unreachable: redis: nil"
195 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"
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: context deadline exceeded"
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"
198 )
198 )
199
199
200 type redisStoreTableInput struct {
200 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
166
166
167 err = requests.New(p.RedeemURL.String()).
167 err = requests.New(p.RedeemURL.String()).
168 WithContext(ctx).
168 WithContext(ctx).
169− WithMethod("POST").
169+ WithMethod(http.MethodPost).
170 WithBody(bytes.NewBufferString(params.Encode())).
170 WithBody(bytes.NewBufferString(params.Encode())).
171 SetHeader("Content-Type", "application/x-www-form-urlencoded").
171 SetHeader("Content-Type", "application/x-www-form-urlencoded").
172 Do().
172 Do().
@@ -334,7 +334,7 @@ func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sess
334
334
335 err = requests.New(p.RedeemURL.String()).
335 err = requests.New(p.RedeemURL.String()).
336 WithContext(ctx).
336 WithContext(ctx).
337− WithMethod("POST").
337+ WithMethod(http.MethodPost).
338 WithBody(bytes.NewBufferString(params.Encode())).
338 WithBody(bytes.NewBufferString(params.Encode())).
339 SetHeader("Content-Type", "application/x-www-form-urlencoded").
339 SetHeader("Content-Type", "application/x-www-form-urlencoded").
340 Do().
340 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
219
219
220 err = requests.New(p.RedeemURL.String()).
220 err = requests.New(p.RedeemURL.String()).
221 WithContext(ctx).
221 WithContext(ctx).
222− WithMethod("POST").
222+ WithMethod(http.MethodPost).
223 WithBody(bytes.NewBufferString(params.Encode())).
223 WithBody(bytes.NewBufferString(params.Encode())).
224 SetHeader("Content-Type", "application/x-www-form-urlencoded").
224 SetHeader("Content-Type", "application/x-www-form-urlencoded").
225 Do().
225 Do().
@@ -543,7 +543,7 @@ func (p *GoogleProvider) redeemRefreshToken(ctx context.Context, s *sessions.Ses
543
543
544 err = requests.New(p.RedeemURL.String()).
544 err = requests.New(p.RedeemURL.String()).
545 WithContext(ctx).
545 WithContext(ctx).
546− WithMethod("POST").
546+ WithMethod(http.MethodPost).
547 WithBody(bytes.NewBufferString(params.Encode())).
547 WithBody(bytes.NewBufferString(params.Encode())).
548 SetHeader("Content-Type", "application/x-www-form-urlencoded").
548 SetHeader("Content-Type", "application/x-www-form-urlencoded").
549 Do().
549 Do().
providers/logingov.go+2−1
@@ -8,6 +8,7 @@ import (
8 "errors"
8 "errors"
9 "fmt"
9 "fmt"
10 "math/big"
10 "math/big"
11+ "net/http"
11 "net/url"
12 "net/url"
12 "os"
13 "os"
13 "time"
14 "time"
@@ -237,7 +238,7 @@ func (p *LoginGovProvider) Redeem(ctx context.Context, _, code, codeVerifier str
237 }
238 }
238 err = requests.New(p.RedeemURL.String()).
239 err = requests.New(p.RedeemURL.String()).
239 WithContext(ctx).
240 WithContext(ctx).
240− WithMethod("POST").
241+ WithMethod(http.MethodPost).
241 WithBody(bytes.NewBufferString(params.Encode())).
242 WithBody(bytes.NewBufferString(params.Encode())).
242 SetHeader("Content-Type", "application/x-www-form-urlencoded").
243 SetHeader("Content-Type", "application/x-www-form-urlencoded").
243 Do().
244 Do().
providers/ms_entra_id.go+2−1
@@ -5,6 +5,7 @@ import (
5 "context"
5 "context"
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8+ "net/http"
8 "net/url"
9 "net/url"
9 "os"
10 "os"
10 "regexp"
11 "regexp"
@@ -304,7 +305,7 @@ func (p *MicrosoftEntraIDProvider) checkTenantMatchesTenantList(tenant string, a
304func (p *MicrosoftEntraIDProvider) fetchToken(ctx context.Context, params url.Values) (*oauth2.Token, error) {
305func (p *MicrosoftEntraIDProvider) fetchToken(ctx context.Context, params url.Values) (*oauth2.Token, error) {
305 resp := requests.New(p.RedeemURL.String()).
306 resp := requests.New(p.RedeemURL.String()).
306 WithContext(ctx).
307 WithContext(ctx).
307− WithMethod("POST").
308+ WithMethod(http.MethodPost).
308 WithBody(bytes.NewBufferString(params.Encode())).
309 WithBody(bytes.NewBufferString(params.Encode())).
309 SetHeader("Content-Type", "application/x-www-form-urlencoded").
310 SetHeader("Content-Type", "application/x-www-form-urlencoded").
310 Do()
311 Do()
providers/provider_default.go+2−1
@@ -5,6 +5,7 @@ import (
5 "context"
5 "context"
6 "errors"
6 "errors"
7 "fmt"
7 "fmt"
8+ "net/http"
8 "net/url"
9 "net/url"
9
10
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
@@ -71,7 +72,7 @@ func (p *ProviderData) Redeem(ctx context.Context, redirectURL, code, codeVerifi
71
72
72 result := requests.New(p.RedeemURL.String()).
73 result := requests.New(p.RedeemURL.String()).
73 WithContext(ctx).
74 WithContext(ctx).
74− WithMethod("POST").
75+ WithMethod(http.MethodPost).
75 WithBody(bytes.NewBufferString(params.Encode())).
76 WithBody(bytes.NewBufferString(params.Encode())).
76 SetHeader("Content-Type", "application/x-www-form-urlencoded").
77 SetHeader("Content-Type", "application/x-www-form-urlencoded").
77 Do()
78 Do()
providers/providers.go+7−3
@@ -40,6 +40,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
40 switch providerConfig.Type {
40 switch providerConfig.Type {
41 case options.ADFSProvider:
41 case options.ADFSProvider:
42 return NewADFSProvider(providerData, providerConfig), nil
42 return NewADFSProvider(providerData, providerConfig), nil
43+ case options.AtlassianProvider:
44+ return NewAtlassianProvider(providerData), nil
43 case options.AzureProvider:
45 case options.AzureProvider:
44 return NewAzureProvider(providerData, providerConfig.AzureConfig), nil
46 return NewAzureProvider(providerData, providerConfig.AzureConfig), nil
45 case options.MicrosoftEntraIDProvider:
47 case options.MicrosoftEntraIDProvider:
@@ -50,6 +52,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
50 return NewCIDAASProvider(providerData, providerConfig), nil
52 return NewCIDAASProvider(providerData, providerConfig), nil
51 case options.DigitalOceanProvider:
53 case options.DigitalOceanProvider:
52 return NewDigitalOceanProvider(providerData), nil
54 return NewDigitalOceanProvider(providerData), nil
55+ case options.DTTSSOProvider:
56+ return NewDTTSSOProvider(providerData), nil
53 case options.FacebookProvider:
57 case options.FacebookProvider:
54 return NewFacebookProvider(providerData), nil
58 return NewFacebookProvider(providerData), nil
55 case options.GitHubProvider:
59 case options.GitHubProvider:
@@ -189,9 +193,9 @@ func parseCodeChallengeMethod(providerConfig options.Provider) string {
189
193
190func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
194func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
191 switch providerType {
195 switch providerType {
192− case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
196+ case options.AtlassianProvider, options.BitbucketProvider, options.DigitalOceanProvider, options.DTTSSOProvider,
193− options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider,
197+ options.FacebookProvider, options.GitHubProvider, options.GoogleProvider, options.KeycloakProvider,
194− options.NextCloudProvider, options.SourceHutProvider:
198+ options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider, options.SourceHutProvider:
195 return false, nil
199 return false, nil
196 case options.OIDCProvider, options.ADFSProvider, options.AzureProvider, options.CidaasProvider,
200 case options.OIDCProvider, options.ADFSProvider, options.AzureProvider, options.CidaasProvider,
197 options.GitLabProvider, options.KeycloakOIDCProvider, options.MicrosoftEntraIDProvider:
201 options.GitLabProvider, options.KeycloakOIDCProvider, options.MicrosoftEntraIDProvider:
providers/srht.go+2−1
@@ -4,6 +4,7 @@ import (
4 "bytes"
4 "bytes"
5 "context"
5 "context"
6 "fmt"
6 "fmt"
7+ "net/http"
7 "net/url"
8 "net/url"
8
9
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@@ -75,7 +76,7 @@ func NewSourceHutProvider(p *ProviderData) *SourceHutProvider {
75func (p *SourceHutProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
76func (p *SourceHutProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
76 json, err := requests.New(p.ProfileURL.String()).
77 json, err := requests.New(p.ProfileURL.String()).
77 WithContext(ctx).
78 WithContext(ctx).
78− WithMethod("POST").
79+ WithMethod(http.MethodPost).
79 SetHeader("Content-Type", "application/json").
80 SetHeader("Content-Type", "application/json").
80 SetHeader("Authorization", "Bearer "+s.AccessToken).
81 SetHeader("Authorization", "Bearer "+s.AccessToken).
81 WithBody(bytes.NewBufferString(`{"query": "{ me { username, email } }"}`)).
82 WithBody(bytes.NewBufferString(`{"query": "{ me { username, email } }"}`)).
validator.go+4−1
@@ -110,6 +110,10 @@ func NewValidator(domains []string, usersFile string) func(string) bool {
110
110
111// isEmailValidWithDomains checks if the authenticated email is validated against the provided domain
111// isEmailValidWithDomains checks if the authenticated email is validated against the provided domain
112func isEmailValidWithDomains(email string, allowedDomains []string) bool {
112func isEmailValidWithDomains(email string, allowedDomains []string) bool {
113+ if strings.Count(email, "@") != 1 {
114+ return false
115+ }
116+
113 for _, domain := range allowedDomains {
117 for _, domain := range allowedDomains {
114 // allow if the domain is perfect suffix match with the email
118 // allow if the domain is perfect suffix match with the email
115 if strings.HasSuffix(email, "@"+domain) {
119 if strings.HasSuffix(email, "@"+domain) {
@@ -119,7 +123,6 @@ func isEmailValidWithDomains(email string, allowedDomains []string) bool {
119 // allow if the domain is prefixed with . or *. and
123 // allow if the domain is prefixed with . or *. and
120 // the last element (split on @) has the suffix as the domain
124 // the last element (split on @) has the suffix as the domain
121 atoms := strings.Split(email, "@")
125 atoms := strings.Split(email, "@")
122−
123 if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
126 if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
124 (strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
127 (strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
125 return true
128 return true
validator_test.go+21
@@ -404,6 +404,27 @@ func TestValidatorCases(t *testing.T) {
404 allowedDomains: []string{"*.company.com"},
404 allowedDomains: []string{"*.company.com"},
405 expectedAuthZ: false,
405 expectedAuthZ: false,
406 },
406 },
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+ },
407 }
428 }
408
429
409 for _, tc := range testCases {
430 for _, tc := range testCases {