git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge remote-tracking branch 'origin/master' into atlassian-provider
commit0b318688161d5c498064f2468546d81e97079205 authorAndrey Orlov <aaorlov1@gmail.com> dateFri 19 Feb 2021, 14:51 (6 years ago) parentd430b8c322308a
137 files changed+7875−1773
.github/workflows/ci.yaml+7−3 CHANGELOG.md+124−24 Dockerfile+1−1 Dockerfile.arm64+1−1 Dockerfile.armv6+1−1 MAINTAINERS+1 Makefile+9−1 README.md+2−2 SECURITY.md+3 contrib/local-environment/Makefile+8 contrib/local-environment/README.md+3 contrib/local-environment/docker-compose-alpha-config.yaml+19 contrib/local-environment/docker-compose-keycloak.yaml+1−1 contrib/local-environment/docker-compose.yaml+1−1 contrib/local-environment/oauth2-proxy-alpha-config.cfg+10 contrib/local-environment/oauth2-proxy-alpha-config.yaml+17 dist.sh+2−9 docs/docs/community/security.md+49 docs/docs/configuration/alpha_config.md+212 docs/docs/configuration/alpha_config.md.tmpl+101 docs/docs/configuration/auth.md+113−59 docs/docs/configuration/overview.md+175−3 docs/docs/features/endpoints.md+1 docs/docs/installation.md+1−1 docs/sidebars.js+7−1 docs/versioned_docs/version-6.1.x/community/security.md+49 docs/versioned_docs/version-6.1.x/configuration/auth.md+1−1 docs/versioned_docs/version-6.1.x/configuration/overview.md+2 docs/versioned_docs/version-7.0.x/behaviour.md+11 docs/versioned_docs/version-7.0.x/community/security.md+49 docs/versioned_docs/version-7.0.x/configuration/alpha_config.md+212 docs/versioned_docs/version-7.0.x/configuration/alpha_config.md.tmpl+101 docs/versioned_docs/version-7.0.x/configuration/auth.md+510 docs/versioned_docs/version-7.0.x/configuration/overview.md+528 docs/versioned_docs/version-7.0.x/configuration/sessions.md+67 docs/versioned_docs/version-7.0.x/configuration/tls.md+70 docs/versioned_docs/version-7.0.x/features/endpoints.md+35 docs/versioned_docs/version-7.0.x/features/request_signatures.md+20 docs/versioned_docs/version-7.0.x/installation.md+24 docs/versioned_sidebars/version-6.1.x-sidebars.json+11 docs/versioned_sidebars/version-7.0.x-sidebars.json+65 docs/versions.json+1 go.mod+3 go.sum+259−7 main.go+154−24 main_suite_test.go+17 main_test.go+215 oauthproxy.go+506−365 oauthproxy_test.go+238−36 pkg/apis/middleware/middleware_suite_test.go+20 pkg/apis/middleware/scope.go+29 pkg/apis/middleware/scope_test.go+56 pkg/apis/options/alpha_options.go+16 pkg/apis/options/app.go+50 pkg/apis/options/common.go+1 pkg/apis/options/doc.go+2 pkg/apis/options/legacy_options.go+10−2 pkg/apis/options/legacy_options_test.go+2−3 pkg/apis/options/load.go+28 pkg/apis/options/load_test.go+192−1 pkg/apis/options/options.go+20−20 pkg/apis/options/options_suite_test.go+1 pkg/apis/options/util/util.go+1−4 pkg/apis/options/util/util_suite_test.go+1 pkg/apis/options/util/util_test.go+3−12 pkg/app/pagewriter/error_page.go+97 pkg/app/pagewriter/error_page_test.go+101 pkg/app/pagewriter/pagewriter.go+85 pkg/app/pagewriter/pagewriter_suite_test.go+17 pkg/app/pagewriter/pagewriter_test.go+126 pkg/app/pagewriter/sign_in_page.go+66 pkg/app/pagewriter/sign_in_page_test.go+61 pkg/app/pagewriter/templates.go+259 pkg/app/pagewriter/templates_test.go+199 pkg/authentication/basic/basic_suite_test.go+1 pkg/cookies/cookies.go+4−4 pkg/header/header_suite_test.go+1 pkg/header/injector_test.go+7−7 pkg/logger/logger.go+3−3 pkg/middleware/basic_session.go+2−1 pkg/middleware/basic_session_test.go+2−4 pkg/middleware/headers.go+14−2 pkg/middleware/headers_test.go+14−17 pkg/middleware/jwt_session.go+1−1 pkg/middleware/jwt_session_test.go+2−3 pkg/middleware/metrics.go+122 pkg/middleware/metrics_test.go+67 pkg/middleware/middleware_suite_test.go+1 pkg/middleware/redirect_to_https.go+7−6 pkg/middleware/redirect_to_https_test.go+31 pkg/middleware/scope.go+9−28 pkg/middleware/scope_test.go+32−57 pkg/middleware/stored_session.go+2−1 pkg/middleware/stored_session_test.go+2−3 pkg/middleware/testdata/metrics/notfoundrequest.txt+3 pkg/middleware/testdata/metrics/successfulrequest.txt+3 pkg/requests/requests_suite_test.go+1 pkg/requests/util/util.go+48 pkg/requests/util/util_suite_test.go+20 pkg/requests/util/util_test.go+131 pkg/sessions/cookie/session_store.go+3−4 pkg/sessions/cookie/session_store_test.go+52−1 pkg/sessions/persistence/persistence_suite_test.go+2 pkg/sessions/redis/redis_store_test.go+1 pkg/sessions/session_store_test.go+1 pkg/upstream/proxy.go−22 pkg/upstream/proxy_test.go+6−5 pkg/upstream/upstream_suite_test.go+1 pkg/util/util.go−10 pkg/util/util_test.go−15 pkg/validation/common.go+1−10 pkg/validation/common_test.go+1−10 pkg/validation/header_test.go+2−2 pkg/validation/options.go+26−11 pkg/validation/validation_suite_test.go+1 providers/azure.go+3−3 providers/digitalocean.go+1−1 providers/github.go+2−2 providers/gitlab.go+177−38 providers/gitlab_test.go+210−67 providers/google.go+2−2 providers/internal_util_test.go+20−20 providers/keycloak.go+24−25 providers/keycloak_test.go+180−133 providers/logingov.go+9−2 providers/oidc.go+108−200 providers/oidc_test.go+358−215 providers/provider_data.go+130−1 providers/provider_data_test.go+437 providers/provider_default.go+11−3 providers/providers.go+1−1 providers/providers_suite_test.go+17 providers/util.go+43 providers/util_test.go+48−1 templates.go−187 templates_test.go−62 tools/tools.go+7
.github/workflows/ci.yaml+7−3
@@ -14,7 +14,7 @@ jobs:
1414 build:
1515 env:
1616 COVER: true
17− runs-on: ubuntu-18.04
17+ runs-on: ubuntu-20.04
1818 steps:
1919
2020 - name: Check out code
@@ -28,11 +28,15 @@ jobs:
2828
2929 - name: Get dependencies
3030 run: |
31− curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
31+ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.36.0
3232 go mod download
3333 curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
3434 chmod +x ./cc-test-reporter
3535
36+ - name: Verify Code Generation
37+ run: |
38+ make verify-generate
39+
3640 - name: Lint
3741 run: |
3842 make lint
@@ -48,7 +52,7 @@ jobs:
4852 ./.github/workflows/test.sh
4953
5054 docker:
51− runs-on: ubuntu-18.04
55+ runs-on: ubuntu-20.04
5256 steps:
5357
5458 - name: Check out code
CHANGELOG.md+124−24
@@ -4,24 +4,95 @@
44
55## Important Notes
66
7+## Breaking Changes
8+
9+## Changes since v7.0.1
10+
11+- [#1052](https://github.com/oauth2-proxy/oauth2-proxy/pull/1052) Update golangci-lint to latest version (v1.36.0) (@JoelSpeed)
12+- [#1043](https://github.com/oauth2-proxy/oauth2-proxy/pull/1043) Refactor Sign In Page rendering and capture all page rendering code in pagewriter package (@JoelSpeed)
13+- [#1029](https://github.com/oauth2-proxy/oauth2-proxy/pull/1029) Refactor error page rendering and allow debug messages on error (@JoelSpeed)
14+- [#1028](https://github.com/oauth2-proxy/oauth2-proxy/pull/1028) Refactor templates, update theme and provide styled error pages (@JoelSpeed)
15+- [#1039](https://github.com/oauth2-proxy/oauth2-proxy/pull/1039) Ensure errors in tests are logged to the GinkgoWriter (@JoelSpeed)
16+- [#980](https://github.com/oauth2-proxy/oauth2-proxy/pull/980) Add Prometheus metrics endpoint
17+- [#1023](https://github.com/oauth2-proxy/oauth2-proxy/pull/1023) Update docs on Traefik ForwardAuth support without the use of Traefik 'errors' middleware
18+
19+# V7.0.1
20+
21+## Release Highlights
22+
23+- Fixed a bug that meant that flag ordering mattered
24+- Fixed a bug where response headers for groups were not being flattened
25+
26+## Important Notes
27+
28+N/A
29+
30+## Breaking Changes
31+
32+N/A
33+
34+## Changes since v7.0.0
35+
36+- [#1020](https://github.com/oauth2-proxy/oauth2-proxy/pull/1020) Flatten array-based response headers (@NickMeves)
37+- [#1026](https://github.com/oauth2-proxy/oauth2-proxy/pull/1026) Ensure config flags get parsed correctly when other flags precede them (@JoelSpeed)
38+
39+# V7.0.0
40+
41+## Release Highlights
42+
43+- Major internal improvements to provider interfaces
44+- Added group authorization support
45+- Improved support for external auth for Traefik
46+- Introduced alpha configuration format to allow users to trial new configuration format and alpha features
47+- GitLab provider now supports restricting to members of a project
48+- Keycloak provider now supports restricting users to members of a set of groups
49+- (Alpha) Flexible header configuration allowing user defined mapping of session claims to header values
50+
51+
52+## Important Notes
53+
54+- [GHSA-4mf2-f3wh-gvf2](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-4mf2-f3wh-gvf2) The whitelist domain feature has been updated to fix a vulnerability that was identified, please see the linked advisory for details
55+- [#964](https://github.com/oauth2-proxy/oauth2-proxy/pull/964) Redirect URL generation will attempt secondary strategies
56+ in the priority chain if any fail the `IsValidRedirect` security check. Previously any failures fell back to `/`.
57+- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) Keycloak will now use `--profile-url` if set for the userinfo endpoint
58+ instead of `--validate-url`. `--validate-url` will still work for backwards compatibility.
59+- [#957](https://github.com/oauth2-proxy/oauth2-proxy/pull/957) To use X-Forwarded-{Proto,Host,Uri} on redirect detection, `--reverse-proxy` must be `true`.
60+- [#936](https://github.com/oauth2-proxy/oauth2-proxy/pull/936) `--user-id-claim` option is deprecated and replaced by `--oidc-email-claim`
61+- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Gitlab projects needs a Gitlab application with the extra `read_api` enabled
62+- [#849](https://github.com/oauth2-proxy/oauth2-proxy/pull/849) `/oauth2/auth` `allowed_groups` querystring parameter can be paired with the `allowed-groups` configuration option.
63+ - The `allowed_groups` querystring parameter can specify multiple comma delimited groups.
64+ - In this scenario, the user must have a group (from their multiple groups) present in both lists to not get a 401 or 403 response code.
65+ - Example:
66+ - OAuth2-Proxy globally sets the `allowed_groups` as `engineering`.
67+ - An application using Kubernetes ingress uses the `/oauth2/auth` endpoint with `allowed_groups` querystring set to `backend`.
68+ - A user must have a session with the groups `["engineering", "backend"]` to pass authorization.
69+ - Another user with the groups `["engineering", "frontend"]` would fail the querystring authorization portion.
770- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Existing sessions from v6.0.0 or earlier are no longer valid. They will trigger a reauthentication.
871- [#826](https://github.com/oauth2-proxy/oauth2-proxy/pull/826) `skip-auth-strip-headers` now applies to all requests, not just those where authentication would be skipped.
972- [#797](https://github.com/oauth2-proxy/oauth2-proxy/pull/797) The behavior of the Google provider Groups restriction changes with this
1073 - Either `--google-group` or the new `--allowed-group` will work for Google now (`--google-group` will be used if both are set)
1174 - Group membership lists will be passed to the backend with the `X-Forwarded-Groups` header
1275 - If you change the list of allowed groups, existing sessions that now don't have a valid group will be logged out immediately.
13− - Previously, group membership was only checked on session creation and refresh.
76+ - Previously, group membership was only checked on session creation and refresh.
1477- [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) `--skip-auth-route` is (almost) backwards compatible with `--skip-auth-regex`
1578 - We are marking `--skip-auth-regex` as DEPRECATED and will remove it in the next major version.
1679 - If your regex contains an `=` and you want it for all methods, you will need to add a leading `=` (this is the area where `--skip-auth-regex` doesn't port perfectly)
1780- [#575](https://github.com/oauth2-proxy/oauth2-proxy/pull/575) Sessions from v5.1.1 or earlier will no longer validate since they were not signed with SHA1.
1881 - Sessions from v6.0.0 or later had a graceful conversion to SHA256 that resulted in no reauthentication
1982 - Upgrading from v5.1.1 or earlier will result in a reauthentication
20−- [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Ensure you have configured oauth2-proxy to use the `groups` scope. The user may be logged out initially as they may not currently have the `groups` claim however after going back through login process wil be authenticated.
83+- [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Ensure you have configured oauth2-proxy to use the `groups` scope.
84+ - The user may be logged out initially as they may not currently have the `groups` claim however after going back through login process wil be authenticated.
2185- [#839](https://github.com/oauth2-proxy/oauth2-proxy/pull/839) Enables complex data structures for group claim entries, which are output as Json by default.
2286
2387## Breaking Changes
2488
89+- [#964](https://github.com/oauth2-proxy/oauth2-proxy/pull/964) `--reverse-proxy` must be true to trust `X-Forwarded-*` headers as canonical.
90+ These are used throughout the application in redirect URLs, cookie domains and host logging logic. These are the headers:
91+ - `X-Forwarded-Proto` instead of `req.URL.Scheme`
92+ - `X-Forwarded-Host` instead of `req.Host`
93+ - `X-Forwarded-Uri` instead of `req.URL.RequestURI()`
94+- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) In config files & envvar configs, `keycloak_group` is now the plural `keycloak_groups`.
95+ Flag configs are still `--keycloak-group` but it can be passed multiple times.
2596- [#911](https://github.com/oauth2-proxy/oauth2-proxy/pull/911) Specifying a non-existent provider will cause OAuth2-Proxy to fail on startup instead of defaulting to "google".
2697- [#797](https://github.com/oauth2-proxy/oauth2-proxy/pull/797) Security changes to Google provider group authorization flow
2798 - If you change the list of allowed groups, existing sessions that now don't have a valid group will be logged out immediately.
@@ -38,17 +109,32 @@
38109 be any redirects in the browser anymore when tokens expire, but instead a token refresh is initiated
39110 in the background, which leads to new tokens being returned in the cookies.
40111 - Please note that `--cookie-refresh` must be 0 (the default) or equal to the token lifespan configured in Azure AD to make
41− Azure token refresh reliable. Setting this value to 0 means that it relies on the provider implementation
42− to decide if a refresh is required.
112+ Azure token refresh reliable. Setting this value to 0 means that it relies on the provider implementation
113+ to decide if a refresh is required.
43114
44115## Changes since v6.1.1
45116
117+- [GHSA-4mf2-f3wh-gvf2](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-4mf2-f3wh-gvf2) Subdomain checking of whitelisted domains could allow unintended redirects (@NickMeves)
118+- [#1002](https://github.com/oauth2-proxy/oauth2-proxy/pull/1002) Use logger for logging refreshed session in azure and gitlab provider (@Bibob7)
119+- [#799](https://github.com/oauth2-proxy/oauth2-proxy/pull/799) Use comma separated multiple values for header (@lilida)
120+- [#903](https://github.com/oauth2-proxy/oauth2-proxy/pull/903) Add docs and generated reference for Alpha configuration (@JoelSpeed)
121+- [#995](https://github.com/oauth2-proxy/oauth2-proxy/pull/995) Add Security Policy (@JoelSpeed)
122+- [#964](https://github.com/oauth2-proxy/oauth2-proxy/pull/964) Require `--reverse-proxy` true to trust `X-Forwareded-*` type headers (@NickMeves)
123+- [#970](https://github.com/oauth2-proxy/oauth2-proxy/pull/970) Fix joined cookie name for those containing underline in the suffix (@peppered)
124+- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) Migrate Keycloak to EnrichSession & support multiple groups for authorization (@NickMeves)
125+- [#957](https://github.com/oauth2-proxy/oauth2-proxy/pull/957) Use X-Forwarded-{Proto,Host,Uri} on redirect as last resort (@linuxgemini)
126+- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Add support for Gitlab project based authentication (@factorysh)
127+- [#907](https://github.com/oauth2-proxy/oauth2-proxy/pull/907) Introduce alpha configuration option to enable testing of structured configuration (@JoelSpeed)
128+- [#938](https://github.com/oauth2-proxy/oauth2-proxy/pull/938) Cleanup missed provider renaming refactor methods (@NickMeves)
129+- [#816](https://github.com/oauth2-proxy/oauth2-proxy/pull/816) (via [#936](https://github.com/oauth2-proxy/oauth2-proxy/pull/936)) Support non-list group claims (@loafoe)
130+- [#936](https://github.com/oauth2-proxy/oauth2-proxy/pull/936) Refactor OIDC Provider and support groups from Profile URL (@NickMeves)
131+- [#869](https://github.com/oauth2-proxy/oauth2-proxy/pull/869) Streamline provider interface method names and signatures (@NickMeves)
132+- [#849](https://github.com/oauth2-proxy/oauth2-proxy/pull/849) Support group authorization on `oauth2/auth` endpoint via `allowed_groups` querystring (@NickMeves)
46133- [#925](https://github.com/oauth2-proxy/oauth2-proxy/pull/925) Fix basic auth legacy header conversion (@JoelSpeed)
47134- [#916](https://github.com/oauth2-proxy/oauth2-proxy/pull/916) Add AlphaOptions struct to prepare for alpha config loading (@JoelSpeed)
48135- [#923](https://github.com/oauth2-proxy/oauth2-proxy/pull/923) Support TLS 1.3 (@aajisaka)
49136- [#918](https://github.com/oauth2-proxy/oauth2-proxy/pull/918) Fix log header output (@JoelSpeed)
50−- [#911](https://github.com/oauth2-proxy/oauth2-proxy/pull/911) Validate provider type on startup.
51−- [#869](https://github.com/oauth2-proxy/oauth2-proxy/pull/869) Streamline provider interface method names and signatures (@NickMeves)
137+- [#911](https://github.com/oauth2-proxy/oauth2-proxy/pull/911) Validate provider type on startup. (@arcivanov)
52138- [#906](https://github.com/oauth2-proxy/oauth2-proxy/pull/906) Set up v6.1.x versioned documentation as default documentation (@JoelSpeed)
53139- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Remove v5 legacy sessions support (@NickMeves)
54140- [#904](https://github.com/oauth2-proxy/oauth2-proxy/pull/904) Set `skip-auth-strip-headers` to `true` by default (@NickMeves)
@@ -75,7 +161,8 @@
75161- [#750](https://github.com/oauth2-proxy/oauth2-proxy/pull/750) ci: Migrate to Github Actions (@shinebayar-g)
76162- [#829](https://github.com/oauth2-proxy/oauth2-proxy/pull/820) Rename test directory to testdata (@johejo)
77163- [#819](https://github.com/oauth2-proxy/oauth2-proxy/pull/819) Improve CI (@johejo)
78−
164+- [#989](https://github.com/oauth2-proxy/oauth2-proxy/pull/989) Adapt isAjax to support mimetype lists (@rassie)
165+- [#1013](https://github.com/oauth2-proxy/oauth2-proxy/pull/1013) Update alpine version to 3.13 (@nishanth-pinnapareddy)
79166
80167# v6.1.1
81168
@@ -178,7 +265,7 @@ N/A
178265- [#440](https://github.com/oauth2-proxy/oauth2-proxy/pull/440) Switch Azure AD Graph API to Microsoft Graph API
179266 - The Azure AD Graph API has been [deprecated](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api) and is being replaced by the Microsoft Graph API.
180267 If your application relies on the access token being passed to it to access the Azure AD Graph API, you should migrate your application to use the Microsoft Graph API.
181− Existing behaviour can be retained by setting `-resource=https://graph.windows.net`.
268+ Existing behaviour can be retained by setting `-resource=https://graph.windows.net`.
182269- [#484](https://github.com/oauth2-proxy/oauth2-proxy/pull/484) Configuration loading has been replaced with Viper and PFlag
183270 - Flags now require a `--` prefix before the option
184271 - Previously flags allowed either `-` or `--` to prefix the option name
@@ -199,7 +286,7 @@ N/A
199286- [#556](https://github.com/oauth2-proxy/oauth2-proxy/pull/556) Remove unintentional auto-padding of secrets that were too short
200287 - Previously, after cookie-secrets were opportunistically base64 decoded to raw bytes,
201288 they were padded to have a length divisible by 4.
202− - This led to wrong sized secrets being valid AES lengths of 16, 24, or 32 bytes. Or it led to confusing errors
289+ - This led to wrong sized secrets being valid AES lengths of 16, 24, or 32 bytes. Or it led to confusing errors
203290 reporting an invalid length of 20 or 28 when the user input cookie-secret was not that length.
204291 - Now we will only base64 decode a cookie-secret to raw bytes if it is 16, 24, or 32 bytes long. Otherwise, we will convert
205292 the direct cookie-secret to bytes without silent padding added.
@@ -304,15 +391,18 @@ N/A
304391# v5.1.0
305392
306393## Release Highlights
394+
307395- Bump to Go 1.14
308396- Reduced number of Google API requests for group validation
309397- Support for Redis Cluster
310398- Support for overriding hosts in hosts file
311399
312400## Important Notes
401+
313402- [#335] The session expiry for the OIDC provider is now taken from the Token Response (expires_in) rather than from the id_token (exp)
314403
315404## Breaking Changes
405+
316406N/A
317407
318408## Changes since v5.0.0
@@ -336,13 +426,15 @@ N/A
336426# v5.0.0
337427
338428## Release Highlights
429+
339430- Disabled CGO (binaries will work regardless og glibc/musl)
340431- Allow whitelisted redirect ports
341432- Nextcloud provider support added
342433- DigitalOcean provider support added
343434
344435## Important Notes
345−- (Security) Fix for [open redirect vulnerability](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-qqxw-m5fj-f7gv).. a bad actor using `/\` in redirect URIs can redirect a session to another domain
436+
437+- (Security) Fix for [open redirect vulnerability](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-qqxw-m5fj-f7gv).. a bad actor using `/\` in redirect URIs can redirect a session to another domain
346438
347439## Breaking Changes
348440
@@ -363,6 +455,7 @@ N/A
363455# v4.1.0
364456
365457## Release Highlights
458+
366459- Added Keycloak provider
367460- Build on Go 1.13
368461- Upgrade Docker image to use Debian Buster
@@ -371,12 +464,15 @@ N/A
371464- Added support for GitHub teams
372465
373466## Important Notes
467+
374468N/A
375469
376470## Breaking Changes
471+
377472N/A
378473
379474## Changes since v4.0.0
475+
380476- [#292](https://github.com/oauth2-proxy/oauth2-proxy/pull/292) Added bash >= 4.0 dependency to configure script (@jmfrank63)
381477- [#227](https://github.com/oauth2-proxy/oauth2-proxy/pull/227) Add Keycloak provider (@Ofinka)
382478- [#259](https://github.com/oauth2-proxy/oauth2-proxy/pull/259) Redirect to HTTPS (@jmickey)
@@ -399,6 +495,7 @@ N/A
399495# v4.0.0
400496
401497## Release Highlights
498+
402499- Documentation is now on a [microsite](https://oauth2-proxy.github.io/oauth2-proxy/)
403500- Health check logging can now be disabled for quieter logs
404501- Authorization Header JWTs can now be verified by the proxy to skip authentication for machine users
@@ -406,29 +503,30 @@ N/A
406503- Logging overhaul allows customisable logging formats
407504
408505## Important Notes
506+
409507- This release includes a number of breaking changes that will require users to
410−reconfigure their proxies. Please read the Breaking Changes below thoroughly.
508+ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
411509
412510## Breaking Changes
413511
414512- [#231](https://github.com/oauth2-proxy/oauth2-proxy/pull/231) Rework GitLab provider
415513 - This PR changes the configuration options for the GitLab provider to use
416− a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than
417− explicit `-login-url`, `-redeem-url` and `-validate-url` parameters.
514+ a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than
515+ explicit `-login-url`, `-redeem-url` and `-validate-url` parameters.
418516- [#186](https://github.com/oauth2-proxy/oauth2-proxy/pull/186) Make config consistent
419517 - This PR changes configuration options so that all flags have a config counterpart
420− of the same name but with underscores (`_`) in place of hyphens (`-`).
421− This change affects the following flags:
518+ of the same name but with underscores (`_`) in place of hyphens (`-`).
519+ This change affects the following flags:
422520 - The `--tls-key` flag is now `--tls-key-file` to be consistent with existing
423− file flags and the existing config and environment settings
521+ file flags and the existing config and environment settings
424522 - The `--tls-cert` flag is now `--tls-cert-file` to be consistent with existing
425− file flags and the existing config and environment settings
426− This change affects the following existing configuration options:
523+ file flags and the existing config and environment settings
524+ This change affects the following existing configuration options:
427525 - The `proxy-prefix` option is now `proxy_prefix`.
428− This PR changes environment variables so that all flags have an environment
429− counterpart of the same name but capitalised, with underscores (`_`) in place
430− of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`.
431− This change affects the following existing environment variables:
526+ This PR changes environment variables so that all flags have an environment
527+ counterpart of the same name but capitalised, with underscores (`_`) in place
528+ of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`.
529+ This change affects the following existing environment variables:
432530 - The `OAUTH2_SKIP_OIDC_DISCOVERY` environment variable is now `OAUTH2_PROXY_SKIP_OIDC_DISCOVERY`.
433531 - The `OAUTH2_OIDC_JWKS_URL` environment variable is now `OAUTH2_PROXY_OIDC_JWKS_URL`.
434532- [#146](https://github.com/oauth2-proxy/oauth2-proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field
@@ -454,7 +552,7 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
454552- [#65](https://github.com/oauth2-proxy/oauth2-proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
455553 the `-skip-jwt-bearer-token` options. (@brianv0)
456554 - Additional verifiers can be configured via the `-extra-jwt-issuers` flag if the JWT issuers is either an OpenID provider or has a JWKS URL
457− (e.g. `https://example.com/.well-known/jwks.json`).
555+ (e.g. `https://example.com/.well-known/jwks.json`).
458556- [#180](https://github.com/oauth2-proxy/oauth2-proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
459557- [#175](https://github.com/oauth2-proxy/oauth2-proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
460558 - Includes fix for potential signature checking issue when OIDC discovery is skipped.
@@ -512,6 +610,7 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
512610# v3.2.0
513611
514612## Release highlights
613+
515614- Internal restructure of session state storage to use JSON rather than proprietary scheme
516615- Added health check options for running on GCP behind a load balancer
517616- Improved support for protecting websockets
@@ -519,9 +618,10 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
519618- Allow manual configuration of OIDC providers
520619
521620## Important notes
621+
522622- Dockerfile user is now non-root, this may break your existing deployment
523623- In the OIDC provider, when no email is returned, the ID Token subject will be used
524−instead of returning an error
624+ instead of returning an error
525625- GitHub user emails must now be primary and verified before authenticating
526626
527627## Changes since v3.1.0
Dockerfile+1−1
@@ -20,7 +20,7 @@ COPY . .
2020RUN VERSION=${VERSION} make build && touch jwt_signing_key.pem
2121
2222# Copy binary to alpine
23−FROM alpine:3.12
23+FROM alpine:3.13
2424COPY nsswitch.conf /etc/nsswitch.conf
2525COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
2626COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy
Dockerfile.arm64+1−1
@@ -20,7 +20,7 @@ COPY . .
2020RUN VERSION=${VERSION} GOARCH=arm64 make build && touch jwt_signing_key.pem
2121
2222# Copy binary to alpine
23−FROM arm64v8/alpine:3.12
23+FROM arm64v8/alpine:3.13
2424COPY nsswitch.conf /etc/nsswitch.conf
2525COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
2626COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy
Dockerfile.armv6+1−1
@@ -20,7 +20,7 @@ COPY . .
2020RUN VERSION=${VERSION} GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem
2121
2222# Copy binary to alpine
23−FROM arm32v6/alpine:3.12
23+FROM arm32v6/alpine:3.13
2424COPY nsswitch.conf /etc/nsswitch.conf
2525COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
2626COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy
MAINTAINERS+1
@@ -1,2 +1,3 @@
11Joel Speed <joel.speed@hotmail.co.uk> (@JoelSpeed)
22Henry Jenkins <henry@henryjenkins.name> (@steakunderscore)
3+Nick Meves <nick.meves@greenhouse.io> (@NickMeves)
Makefile+9−1
@@ -69,12 +69,20 @@ docker-push-all: docker-push
6969 docker push $(REGISTRY)/oauth2-proxy:latest-armv6
7070 docker push $(REGISTRY)/oauth2-proxy:${VERSION}-armv6
7171
72+.PHONY: generate
73+generate:
74+ go generate ./pkg/...
75+
76+.PHONY: verify-generate
77+verify-generate: generate
78+ git diff --exit-code
79+
7280.PHONY: test
7381test: lint
7482 GO111MODULE=on $(GO) test $(TESTCOVER) -v -race ./...
7583
7684.PHONY: release
77−release: lint test
85+release: validate-go-version lint test
7886 BINARY=${BINARY} VERSION=${VERSION} ./dist.sh
7987
8088.PHONY: validate-go-version
README.md+2−2
@@ -23,7 +23,7 @@ Going forward, all images shall be available at `quay.io/oauth2-proxy/oauth2-pro
2323
24241. Choose how to deploy:
2525
26− a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v6.1.1`)
26+ a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.0.1`)
2727
2828 b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy` which will put the binary in `$GOROOT/bin`
2929
@@ -54,7 +54,7 @@ Read the docs on our [Docs site](https://oauth2-proxy.github.io/oauth2-proxy/doc
5454
5555## Getting Involved
5656
57−If you would like to reach out to the maintainers, come talk to us in the `#oauth2_proxy` channel in the [Gophers slack](http://gophers.slack.com/).
57+If you would like to reach out to the maintainers, come talk to us in the `#oauth2-proxy` channel in the [Gophers slack](http://gophers.slack.com/).
5858
5959## Contributing
6060
SECURITY.mdnew file+3
@@ -0,0 +1,3 @@
1+# Security Disclosures
2+
3+Please see [our community docs](https://oauth2-proxy.github.io/oauth2-proxy/docs/community/security) for our security policy.
contrib/local-environment/Makefile+8
@@ -6,6 +6,14 @@ up:
66%:
77 docker-compose $*
88
9+.PHONY: alpha-config-up
10+alpha-config-up:
11+ docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml up -d
12+
13+.PHONY: alpha-config-%
14+alpha-config-%:
15+ docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml $*
16+
917.PHONY: nginx-up
1018nginx-up:
1119 docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml up -d
contrib/local-environment/README.mdnew file+3
@@ -0,0 +1,3 @@
1+# oauth2-proxy: local-environment
2+
3+Run `make up` to deploy local dex, etcd and oauth2-proxy instances in Docker containers. Review the [`Makefile`](Makefile) for additional deployment options.
contrib/local-environment/docker-compose-alpha-config.yamlnew file+19
@@ -0,0 +1,19 @@
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.
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.
5+# This file also uses alpha configuration when configuring OAuth2 Proxy.
6+#
7+# This file is an extension of the main compose file and must be used with it
8+# docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml <command>
9+# Alternatively:
10+# make alpha-config-<command> (eg make nginx-up, make nginx-down)
11+#
12+# Access http://localhost:4180 to initiate a login cycle
13+version: '3.0'
14+services:
15+ oauth2-proxy:
16+ command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
17+ volumes:
18+ - "./oauth2-proxy-alpha-config.cfg:/oauth2-proxy.cfg"
19+ - "./oauth2-proxy-alpha-config.yaml:/oauth2-proxy-alpha-config.yaml"
contrib/local-environment/docker-compose-keycloak.yaml+1−1
@@ -15,7 +15,7 @@ services:
1515
1616 oauth2-proxy:
1717 container_name: oauth2-proxy
18− image: quay.io/oauth2-proxy/oauth2-proxy:v6.1.1
18+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.0.1
1919 command: --config /oauth2-proxy.cfg
2020 hostname: oauth2-proxy
2121 volumes:
contrib/local-environment/docker-compose.yaml+1−1
@@ -13,7 +13,7 @@ version: '3.0'
1313services:
1414 oauth2-proxy:
1515 container_name: oauth2-proxy
16− image: quay.io/oauth2-proxy/oauth2-proxy:v6.1.1
16+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.0.1
1717 command: --config /oauth2-proxy.cfg
1818 ports:
1919 - 4180:4180/tcp
contrib/local-environment/oauth2-proxy-alpha-config.cfgnew file+10
@@ -0,0 +1,10 @@
1+http_address="0.0.0.0:4180"
2+cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3+provider="oidc"
4+email_domains="example.com"
5+oidc_issuer_url="http://dex.localhost:4190/dex"
6+client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
7+client_id="oauth2-proxy"
8+cookie_secure="false"
9+
10+redirect_url="http://localhost:4180/oauth2/callback"
contrib/local-environment/oauth2-proxy-alpha-config.yamlnew file+17
@@ -0,0 +1,17 @@
1+upstreams:
2+ - id: httpbin
3+ path: /
4+ uri: http://httpbin
5+injectRequestHeaders:
6+- name: X-Forwarded-Groups
7+ values:
8+ - claim: groups
9+- name: X-Forwarded-User
10+ values:
11+ - claim: user
12+- name: X-Forwarded-Email
13+ values:
14+ - claim: email
15+- name: X-Forwarded-Preferred-Username
16+ values:
17+ - claim: preferred_username
dist.sh+2−9
@@ -7,13 +7,6 @@ if [[ -z ${BINARY} ]] || [[ -z ${VERSION} ]]; then
77 exit 1
88fi
99
10−# Check for Go version 1.14.*
11−GO_VERSION=$(go version | awk '{print $3}')
12−if [[ ! "${GO_VERSION}" =~ ^go1.14.* ]]; then
13− echo "Go version must be >= go1.14"
14− exit 1
15−fi
16−
1710ARCHS=(darwin-amd64 linux-amd64 linux-arm64 linux-armv6 freebsd-amd64 windows-amd64)
1811
1912mkdir -p release
@@ -28,10 +21,10 @@ for ARCH in "${ARCHS[@]}"; do
2821 # Create architecture specific binaries
2922 if [[ ${GO_ARCH} == "armv6" ]]; then
3023 GO111MODULE=on GOOS=${GO_OS} GOARCH=arm GOARM=6 CGO_ENABLED=0 go build -ldflags="-X main.VERSION=${VERSION}" \
31− -o release/${BINARY}-${VERSION}.${ARCH}/${BINARY} github.com/oauth2-proxy/oauth2-proxy
24+ -o release/${BINARY}-${VERSION}.${ARCH}/${BINARY} .
3225 else
3326 GO111MODULE=on GOOS=${GO_OS} GOARCH=${GO_ARCH} CGO_ENABLED=0 go build -ldflags="-X main.VERSION=${VERSION}" \
34− -o release/${BINARY}-${VERSION}.${ARCH}/${BINARY} github.com/oauth2-proxy/oauth2-proxy
27+ -o release/${BINARY}-${VERSION}.${ARCH}/${BINARY} .
3528 fi
3629
3730 cd release
docs/docs/community/security.mdnew file+49
@@ -0,0 +1,49 @@
1+---
2+id: security
3+title: Security
4+---
5+
6+:::note
7+OAuth2 Proxy is a community project.
8+Maintainers do not work on this project full time, and as such,
9+while we endeavour to respond to disclosures as quickly as possible,
10+this may take longer than in projects with corporate sponsorship.
11+:::
12+
13+## Security Disclosures
14+
15+:::important
16+If you believe you have found a vulnerability within OAuth2 Proxy or any of its
17+dependencies, please do NOT open an issue or PR on GitHub, please do NOT post
18+any details publicly.
19+:::
20+
21+Security disclosures MUST be done in private.
22+If you have found an issue that you would like to bring to the attention of the
23+maintenance team for OAuth2 Proxy, please compose an email and send it to the
24+list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file.
25+
26+Please include as much detail as possible.
27+Ideally, your disclosure should include:
28+- A reproducible case that can be used to demonstrate the exploit
29+- How you discovered this vulnerability
30+- A potential fix for the issue (if you have thought of one)
31+- Versions affected (if not present in master)
32+- Your GitHub ID
33+
34+### How will we respond to disclosures?
35+
36+We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories)
37+to privately discuss fixes for disclosed vulnerabilities.
38+If you include a GitHub ID with your disclosure we will add you as a collaborator
39+for the advisory so that you can join the discussion and validate any fixes
40+we may propose.
41+
42+For minor issues and previously disclosed vulnerabilities (typically for
43+dependencies), we may use regular PRs for fixes and forego the security advisory.
44+
45+Once a fix has been agreed upon, we will merge the fix and create a new release.
46+If we have multiple security issues in flight simultaneously, we may delay
47+merging fixes until all patches are ready.
48+We may also backport the fix to previous releases,
49+but this will be at the discretion of the maintainers.
docs/docs/configuration/alpha_config.mdnew file+212
@@ -0,0 +1,212 @@
1+---
2+id: alpha-config
3+title: Alpha Configuration
4+---
5+
6+:::warning
7+This page contains documentation for alpha features.
8+We reserve the right to make breaking changes to the features detailed within this page with no notice.
9+
10+Options described in this page may be changed, removed, renamed or moved without prior warning.
11+Please beware of this before you use alpha configuration options.
12+:::
13+
14+This page details a set of **alpha** configuration options in a new format.
15+Going forward we are intending to add structured configuration in YAML format to
16+replace the existing TOML based configuration file and flags.
17+
18+Below is a reference for the structure of the configuration, with
19+[AlphaOptions](#alphaoptions) as the root of the configuration.
20+
21+When using alpha configuration, your config file will look something like below:
22+
23+```yaml
24+upstreams:
25+ - id: ...
26+ ...
27+injectRequestHeaders:
28+ - name: ...
29+ ...
30+injectResponseHeaders:
31+ - name: ...
32+ ...
33+```
34+
35+Please browse the [reference](#configuration-reference) below for the structure
36+of the new configuration format.
37+
38+## Using Alpha Configuration
39+
40+To use the new **alpha** configuration, generate a YAML file based on the format
41+described in the [reference](#configuration-reference) below.
42+
43+Provide the path to this file using the `--alpha-config` flag.
44+
45+:::note
46+When using the `--alpha-config` flag, some options are no longer available.
47+See [removed options](#removed-options) below for more information.
48+:::
49+
50+### Converting configuration to the new structure
51+
52+Before adding the new `--alpha-config` option, start OAuth2 Proxy using the
53+`convert-config-to-alpha` flag to convert existing configuration to the new format.
54+
55+```bash
56+oauth2-proxy --convert-config-to-alpha --config ./path/to/existing/config.cfg
57+```
58+
59+This will convert any options supported by the new format to YAML and print the
60+new configuration to `STDOUT`.
61+
62+Copy this to a new file, remove any options from your existing configuration
63+noted in [removed options](#removed-options) and then start OAuth2 Proxy using
64+the new config.
65+
66+```bash
67+oauth2-proxy --alpha-config ./path/to/new/config.yaml --config ./path/to/existing/config.cfg
68+```
69+
70+## Removed options
71+
72+The following flags/options and their respective environment variables are no
73+longer available when using alpha configuration:
74+
75+<!-- Legacy Upstream FlagSet -->
76+- `flush-interval`/`flush_interval`
77+- `pass-host-header`/`pass_host_header`
78+- `proxy-websockets`/`proxy_websockets`
79+- `ssl-upstream-insecure-skip-verify`/`ssl_upstream_insecure_skip_verify`
80+- `upstream`/`upstreams`
81+
82+<!-- Legacy Headers FlagSet -->
83+- `pass-basic-auth`/`pass_basic_auth`
84+- `pass-access-token`/`pass_access_token`
85+- `pass-user-headers`/`pass_user_headers`
86+- `pass-authorization-header`/`pass_authorization_header`
87+- `set-basic-auth`/`set_basic_auth`
88+- `set-xauthrequest`/`set_xauthrequest`
89+- `set-authorization-header`/`set_authorization_header`
90+- `prefer-email-to-user`/`prefer_email_to_user`
91+- `basic-auth-password`/`basic_auth_password`
92+- `skip-auth-strip-headers`/`skip_auth_strip_headers`
93+
94+Attempting to use these options via flags or via config when `--alpha-config`
95+set will result in an error.
96+
97+:::important
98+You must remove these options before starting OAuth2 Proxy with `--alpha-config`
99+:::
100+
101+## Configuration Reference
102+<!--- THIS FILE IS AUTOGENERATED!!! DO NOT EDIT!!! -->
103+
104+### AlphaOptions
105+
106+AlphaOptions contains alpha structured configuration options.
107+Usage of these options allows users to access alpha features that are not
108+available as part of the primary configuration structure for OAuth2 Proxy.
109+
110+:::warning
111+The options within this structure are considered alpha.
112+They may change between releases without notice.
113+:::
114+
115+| Field | Type | Description |
116+| ----- | ---- | ----------- |
117+| `upstreams` | _[Upstreams](#upstreams)_ | Upstreams is used to configure upstream servers.<br/>Once a user is authenticated, requests to the server will be proxied to<br/>these upstream servers based on the path mappings defined in this list. |
118+| `injectRequestHeaders` | _[[]Header](#header)_ | InjectRequestHeaders is used to configure headers that should be added<br/>to requests to upstream servers.<br/>Headers may source values from either the authenticated user's session<br/>or from a static secret value. |
119+| `injectResponseHeaders` | _[[]Header](#header)_ | InjectResponseHeaders is used to configure headers that should be added<br/>to responses from the proxy.<br/>This is typically used when using the proxy as an external authentication<br/>provider in conjunction with another proxy such as NGINX and its<br/>auth_request module.<br/>Headers may source values from either the authenticated user's session<br/>or from a static secret value. |
120+
121+### ClaimSource
122+
123+(**Appears on:** [HeaderValue](#headervalue))
124+
125+ClaimSource allows loading a header value from a claim within the session
126+
127+| Field | Type | Description |
128+| ----- | ---- | ----------- |
129+| `claim` | _string_ | Claim is the name of the claim in the session that the value should be<br/>loaded from. |
130+| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
131+| `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.<br/>Note the value of claim will become the basic auth username and the<br/>basicAuthPassword will be used as the password value. |
132+
133+### Duration
134+#### (`string` alias)
135+
136+(**Appears on:** [Upstream](#upstream))
137+
138+Duration is as string representation of a period of time.
139+A duration string is a is a possibly signed sequence of decimal numbers,
140+each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".
141+Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
142+
143+
144+### Header
145+
146+(**Appears on:** [AlphaOptions](#alphaoptions))
147+
148+Header represents an individual header that will be added to a request or
149+response header.
150+
151+| Field | Type | Description |
152+| ----- | ---- | ----------- |
153+| `name` | _string_ | Name is the header name to be used for this set of values.<br/>Names should be unique within a list of Headers. |
154+| `preserveRequestValue` | _bool_ | PreserveRequestValue determines whether any values for this header<br/>should be preserved for the request to the upstream server.<br/>This option only takes effet on injected request headers.<br/>Defaults to false (headers that match this header will be stripped). |
155+| `values` | _[[]HeaderValue](#headervalue)_ | Values contains the desired values for this header |
156+
157+### HeaderValue
158+
159+(**Appears on:** [Header](#header))
160+
161+HeaderValue represents a single header value and the sources that can
162+make up the header value
163+
164+| Field | Type | Description |
165+| ----- | ---- | ----------- |
166+| `value` | _[]byte_ | Value expects a base64 encoded string value. |
167+| `fromEnv` | _string_ | FromEnv expects the name of an environment variable. |
168+| `fromFile` | _string_ | FromFile expects a path to a file containing the secret value. |
169+| `claim` | _string_ | Claim is the name of the claim in the session that the value should be<br/>loaded from. |
170+| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
171+| `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.<br/>Note the value of claim will become the basic auth username and the<br/>basicAuthPassword will be used as the password value. |
172+
173+### SecretSource
174+
175+(**Appears on:** [ClaimSource](#claimsource), [HeaderValue](#headervalue))
176+
177+SecretSource references an individual secret value.
178+Only one source within the struct should be defined at any time.
179+
180+| Field | Type | Description |
181+| ----- | ---- | ----------- |
182+| `value` | _[]byte_ | Value expects a base64 encoded string value. |
183+| `fromEnv` | _string_ | FromEnv expects the name of an environment variable. |
184+| `fromFile` | _string_ | FromFile expects a path to a file containing the secret value. |
185+
186+### Upstream
187+
188+(**Appears on:** [Upstreams](#upstreams))
189+
190+Upstream represents the configuration for an upstream server.
191+Requests will be proxied to this upstream if the path matches the request path.
192+
193+| Field | Type | Description |
194+| ----- | ---- | ----------- |
195+| `id` | _string_ | ID should be a unique identifier for the upstream.<br/>This value is required for all upstreams. |
196+| `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. |
197+| `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". |
198+| `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/>betweem OAuth2 Proxy and the usptream server.<br/>Defaults to false. |
199+| `static` | _bool_ | Static will make all requests to this upstream have a static response.<br/>The response will have a body of "Authenticated" and a response code<br/>matching StaticCode.<br/>If StaticCode is not set, the response will return a 200 response. |
200+| `staticCode` | _int_ | StaticCode determines the response code for the Static response.<br/>This option can only be used with Static enabled. |
201+| `flushInterval` | _[Duration](#duration)_ | FlushInterval is the period between flushing the response buffer when<br/>streaming response from the upstream.<br/>Defaults to 1 second. |
202+| `passHostHeader` | _bool_ | PassHostHeader determines whether the request host header should be proxied<br/>to the upstream server.<br/>Defaults to true. |
203+| `proxyWebSockets` | _bool_ | ProxyWebSockets enables proxying of websockets to upstream servers<br/>Defaults to true. |
204+
205+### Upstreams
206+
207+#### ([[]Upstream](#upstream) alias)
208+
209+(**Appears on:** [AlphaOptions](#alphaoptions))
210+
211+Upstreams is a collection of definitions for upstream servers.
212+
docs/docs/configuration/alpha_config.md.tmplnew file+101
@@ -0,0 +1,101 @@
1+---
2+id: alpha-config
3+title: Alpha Configuration
4+---
5+
6+:::warning
7+This page contains documentation for alpha features.
8+We reserve the right to make breaking changes to the features detailed within this page with no notice.
9+
10+Options described in this page may be changed, removed, renamed or moved without prior warning.
11+Please beware of this before you use alpha configuration options.
12+:::
13+
14+This page details a set of **alpha** configuration options in a new format.
15+Going forward we are intending to add structured configuration in YAML format to
16+replace the existing TOML based configuration file and flags.
17+
18+Below is a reference for the structure of the configuration, with
19+[AlphaOptions](#alphaoptions) as the root of the configuration.
20+
21+When using alpha configuration, your config file will look something like below:
22+
23+```yaml
24+upstreams:
25+ - id: ...
26+ ...
27+injectRequestHeaders:
28+ - name: ...
29+ ...
30+injectResponseHeaders:
31+ - name: ...
32+ ...
33+```
34+
35+Please browse the [reference](#configuration-reference) below for the structure
36+of the new configuration format.
37+
38+## Using Alpha Configuration
39+
40+To use the new **alpha** configuration, generate a YAML file based on the format
41+described in the [reference](#configuration-reference) below.
42+
43+Provide the path to this file using the `--alpha-config` flag.
44+
45+:::note
46+When using the `--alpha-config` flag, some options are no longer available.
47+See [removed options](#removed-options) below for more information.
48+:::
49+
50+### Converting configuration to the new structure
51+
52+Before adding the new `--alpha-config` option, start OAuth2 Proxy using the
53+`convert-config-to-alpha` flag to convert existing configuration to the new format.
54+
55+```bash
56+oauth2-proxy --convert-config-to-alpha --config ./path/to/existing/config.cfg
57+```
58+
59+This will convert any options supported by the new format to YAML and print the
60+new configuration to `STDOUT`.
61+
62+Copy this to a new file, remove any options from your existing configuration
63+noted in [removed options](#removed-options) and then start OAuth2 Proxy using
64+the new config.
65+
66+```bash
67+oauth2-proxy --alpha-config ./path/to/new/config.yaml --config ./path/to/existing/config.cfg
68+```
69+
70+## Removed options
71+
72+The following flags/options and their respective environment variables are no
73+longer available when using alpha configuration:
74+
75+<!-- Legacy Upstream FlagSet -->
76+- `flush-interval`/`flush_interval`
77+- `pass-host-header`/`pass_host_header`
78+- `proxy-websockets`/`proxy_websockets`
79+- `ssl-upstream-insecure-skip-verify`/`ssl_upstream_insecure_skip_verify`
80+- `upstream`/`upstreams`
81+
82+<!-- Legacy Headers FlagSet -->
83+- `pass-basic-auth`/`pass_basic_auth`
84+- `pass-access-token`/`pass_access_token`
85+- `pass-user-headers`/`pass_user_headers`
86+- `pass-authorization-header`/`pass_authorization_header`
87+- `set-basic-auth`/`set_basic_auth`
88+- `set-xauthrequest`/`set_xauthrequest`
89+- `set-authorization-header`/`set_authorization_header`
90+- `prefer-email-to-user`/`prefer_email_to_user`
91+- `basic-auth-password`/`basic_auth_password`
92+- `skip-auth-strip-headers`/`skip_auth_strip_headers`
93+
94+Attempting to use these options via flags or via config when `--alpha-config`
95+set will result in an error.
96+
97+:::important
98+You must remove these options before starting OAuth2 Proxy with `--alpha-config`
99+:::
100+
101+## Configuration Reference
docs/docs/configuration/auth.md+113−59
@@ -135,21 +135,35 @@ If you are using GitHub enterprise, make sure you set the following to the appro
135135
136136Make sure you set the following to the appropriate url:
137137
138− -provider=keycloak
139− -client-id=<client you have created>
140− -client-secret=<your client's secret>
141− -login-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/auth"
142− -redeem-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/token"
143− -validate-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo"
144− -keycloak-group=<user_group>
145−
146−The group management in keycloak is using a tree. If you create a group named admin in keycloak you should define the 'keycloak-group' value to /admin.
138+ --provider=keycloak
139+ --client-id=<client you have created>
140+ --client-secret=<your client's secret>
141+ --login-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/auth"
142+ --redeem-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/token"
143+ --profile-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo"
144+ --validate-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo"
145+ --keycloak-group=<first_allowed_user_group>
146+ --keycloak-group=<second_allowed_user_group>
147+
148+For group based authorization, the optional `--keycloak-group` (legacy) or `--allowed-group` (global standard)
149+flags can be used to specify which groups to limit access to.
150+
151+If these are unset but a `groups` mapper is set up above in step (3), the provider will still
152+populate the `X-Forwarded-Groups` header to your upstream server with the `groups` data in the
153+Keycloak userinfo endpoint response.
154+
155+The group management in keycloak is using a tree. If you create a group named admin in keycloak
156+you should define the 'keycloak-group' value to /admin.
147157
148158### GitLab Auth Provider
149159
160+This auth provider has been tested against Gitlab version 12.X. Due to Gitlab API changes, it may not work for version prior to 12.X (see [994](https://github.com/oauth2-proxy/oauth2-proxy/issues/994)).
161+
150162Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](https://docs.gitlab.com/ce/integration/oauth_provider.html). Make sure to enable at least the `openid`, `profile` and `email` scopes, and set the redirect url to your application url e.g. https://myapp.com/oauth2/callback.
151163
152−The following config should be set to ensure that the oauth will work properly. To get a cookie secret follow [these steps](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/docs/configuration/configuration.md#configuration)
164+If you need projects filtering, add the extra `read_api` scope to your application.
165+
166+The following config should be set to ensure that the oauth will work properly. To get a cookie secret follow [these steps](./overview.md#generating-a-cookie-secret)
153167
154168```
155169 --provider="gitlab"
@@ -186,27 +200,66 @@ Take note of your `TenantId` if applicable for your situation. The `TenantId` ca
186200
187201### OpenID Connect Provider
188202
189−OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects. This provider was originally built against CoreOS Dex and we will use it as an example.
203+OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects.
190204
191−1. Launch a Dex instance using the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md).
192−2. Setup oauth2-proxy with the correct provider and using the default ports and callbacks.
193−3. Login with the fixture use in the dex guide and run the oauth2-proxy with the following args:
205+This provider was originally built against CoreOS Dex and we will use it as an example.
206+The OpenID Connect Provider (OIDC) can also be used to connect to other Identity Providers such as Okta, an example can be found below.
194207
195−```
208+#### Dex
209+
210+To configure the OIDC provider for Dex, perform the following steps:
211+
212+1. Download Dex:
213+
214+ ```
215+ go get github.com/dexidp/dex
216+ ```
217+
218+ See the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md) for more details.
219+
220+2. Setup oauth2-proxy with the correct provider and using the default ports and callbacks. Add a configuration block to the `staticClients` section of `examples/config-dev.yaml`:
221+
222+ ```
223+ - id: oauth2-proxy
224+ redirectURIs:
225+ - 'http://127.0.0.1:4180/oauth2/callback'
226+ name: 'oauth2-proxy'
227+ secret: proxy
228+ ```
229+
230+3. Launch Dex: from `$GOPATH/github.com/dexidp/dex`, run:
231+
232+ ```
233+ bin/dex serve examples/config-dev.yaml
234+ ```
235+
236+4. In a second terminal, run the oauth2-proxy with the following args:
237+
238+ ```
196239 -provider oidc
197240 -provider-display-name "My OIDC Provider"
198241 -client-id oauth2-proxy
199242 -client-secret proxy
200243 -redirect-url http://127.0.0.1:4180/oauth2/callback
201− -oidc-issuer-url http://127.0.0.1:5556
244+ -oidc-issuer-url http://127.0.0.1:5556/dex
202245 -cookie-secure=false
203− -email-domain example.com
204−```
246+ -cookie-secret=secret
247+ -email-domain kilgore.trout
248+ ```
249+
250+ To serve the current working directory as a web site under the `/static` endpoint, add:
251+
252+ ```
253+ -upstream file://$PWD/#/static/
254+ ```
205255
206−The OpenID Connect Provider (OIDC) can also be used to connect to other Identity Providers such as Okta. To configure the OIDC provider for Okta, perform
207−the following steps:
256+5. Test the setup by visiting http://127.0.0.1:4180 or http://127.0.0.1:4180/static .
208257
209−#### Configuring the OIDC Provider with Okta
258+See also [our local testing environment](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/contrib/local-environment) for a self-contained example using Docker and etcd as storage for Dex.
259+
260+#### Okta
261+
262+To configure the OIDC provider for Okta, perform the following steps:
210263
2112641. Log in to Okta using an administrative account. It is suggested you try this in preview first, `example.oktapreview.com`
2122652. (OPTIONAL) If you want to configure authorization scopes and claims to be passed on to multiple applications,
@@ -229,30 +282,31 @@ you may wish to configure an authorization server for each application. Otherwis
229282* Under **Assignments** select the users or groups you wish to access your application.
2302834. Create a configuration file like the following:
231284
232−```
233−provider = "oidc"
234−redirect_url = "https://example.corp.com/oauth2/callback"
235−oidc_issuer_url = "https://corp.okta.com/oauth2/abCd1234"
236−upstreams = [
237− "https://example.corp.com"
238−]
239−email_domains = [
240− "corp.com"
241−]
242−client_id = "XXXXX"
243−client_secret = "YYYYY"
244−pass_access_token = true
245−cookie_secret = "ZZZZZ"
246−skip_provider_button = true
247−```
248−
249−The `oidc_issuer_url` is based on URL from your **Authorization Server**'s **Issuer** field in step 2, or simply https://corp.okta.com
285+ ```
286+ provider = "oidc"
287+ redirect_url = "https://example.corp.com/oauth2/callback"
288+ oidc_issuer_url = "https://corp.okta.com/oauth2/abCd1234"
289+ upstreams = [
290+ "https://example.corp.com"
291+ ]
292+ email_domains = [
293+ "corp.com"
294+ ]
295+ client_id = "XXXXX"
296+ client_secret = "YYYYY"
297+ pass_access_token = true
298+ cookie_secret = "ZZZZZ"
299+ skip_provider_button = true
300+ ```
301+
302+The `oidc_issuer_url` is based on URL from your **Authorization Server**'s **Issuer** field in step 2, or simply https://corp.okta.com .
250303The `client_id` and `client_secret` are configured in the application settings.
251304Generate a unique `client_secret` to encrypt the cookie.
252305
253306Then you can start the oauth2-proxy with `./oauth2-proxy --config /etc/example.cfg`
254307
255−#### Configuring the OIDC Provider with Okta - localhost
308+#### Okta - localhost
309+
2563101. Signup for developer account: https://developer.okta.com/signup/
2573112. Create New `Web` Application: https://${your-okta-domain}/dev/console/apps/new
2583123. Example Application Settings for localhost:
@@ -265,25 +319,25 @@ Then you can start the oauth2-proxy with `./oauth2-proxy --config /etc/example.c
2653194. Make note of the `Client ID` and `Client secret`, they are needed in a future step
2663205. Make note of the **default** Authorization Server Issuer URI from: https://${your-okta-domain}/admin/oauth2/as
2673216. Example config file `/etc/localhost.cfg`
268− ```
269− provider = "oidc"
270− redirect_url = "http://localhost:4180/oauth2/callback"
271− oidc_issuer_url = "https://${your-okta-domain}/oauth2/default"
272− upstreams = [
273− "http://0.0.0.0:8080"
274− ]
275− email_domains = [
276− "*"
277− ]
278− client_id = "XXX"
279− client_secret = "YYY"
280− pass_access_token = true
281− cookie_secret = "ZZZ"
282− cookie_secure = false
283− skip_provider_button = true
284− # Note: use the following for testing within a container
285− # http_address = "0.0.0.0:4180"
286− ```
322+ ```
323+ provider = "oidc"
324+ redirect_url = "http://localhost:4180/oauth2/callback"
325+ oidc_issuer_url = "https://${your-okta-domain}/oauth2/default"
326+ upstreams = [
327+ "http://0.0.0.0:8080"
328+ ]
329+ email_domains = [
330+ "*"
331+ ]
332+ client_id = "XXX"
333+ client_secret = "YYY"
334+ pass_access_token = true
335+ cookie_secret = "ZZZ"
336+ cookie_secure = false
337+ skip_provider_button = true
338+ # Note: use the following for testing within a container
339+ # http_address = "0.0.0.0:4180"
340+ ```
2873417. Then you can start the oauth2-proxy with `./oauth2-proxy --config /etc/localhost.cfg`
288342
289343### login.gov Provider
docs/docs/configuration/overview.md+175−3
@@ -5,6 +5,8 @@ title: Overview
55
66`oauth2-proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables).
77
8+### Generating a Cookie Secret
9+
810To generate a strong cookie secret use `python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(16)).decode())'`
911
1012### Config File
@@ -54,6 +56,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
5456| `--github-token` | string | the token to use when verifying repository collaborators (must have push access to the repository) | |
5557| `--github-user` | string \| list | To allow users to login by username even if they do not belong to the specified org and team or collaborators | |
5658| `--gitlab-group` | string \| list | restrict logins to members of any of these groups (slug), separated by a comma | |
59+| `--gitlab-projects` | string \| list | restrict logins to members of any of these projects (may be given multiple times) formatted as `orgname/repo=accesslevel`. Access level should be a value matching [Gitlab access levels](https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent | |
5760| `--google-admin-email` | string | the google admin to impersonate for api calls | |
5861| `--google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
5962| `--google-service-account-json` | string | the path to the service account json credentials | |
@@ -73,7 +76,8 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
7376| `--insecure-oidc-skip-issuer-verification` | bool | allow the OIDC issuer URL to differ from the expected (currently required for Azure multi-tenant compatibility) | false |
7477| `--oidc-issuer-url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | |
7578| `--oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
76−| `--oidc-groups-claim` | string | which claim contains the user groups | `"groups"` |
79+| `--oidc-email-claim` | string | which OIDC claim contains the user's email | `"email"` |
80+| `--oidc-groups-claim` | string | which OIDC claim contains the user groups | `"groups"` |
7781| `--pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header. When used with `--set-xauthrequest` this adds the X-Auth-Request-Access-Token header to the response | false |
7882| `--pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
7983| `--pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
@@ -87,6 +91,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
8791| `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
8892| `--ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
8993| `--ping-user-agent` | string | a User-Agent that can be used for basic health checks | `""` (don't check user agent) |
94+| `--metrics-address` | string | the address prometheus metrics will be scraped from | `""` |
9095| `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
9196| `--proxy-websockets` | bool | enables WebSocket proxying | true |
9297| `--pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | |
@@ -104,13 +109,14 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
104109| `--request-logging` | bool | Log requests | true |
105110| `--request-logging-format` | string | Template for request log lines | see [Logging Configuration](#logging-configuration) |
106111| `--resource` | string | The resource that is protected (Azure AD only) | |
107−| `--reverse-proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-IP are accepted | false |
112+| `--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 |
108113| `--scope` | string | OAuth scope specification | |
109114| `--session-cookie-minimal` | bool | strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only) | false |
110115| `--session-store-type` | string | [Session data storage backend](sessions.md); redis or cookie | cookie |
111116| `--set-xauthrequest` | bool | set X-Auth-Request-User, X-Auth-Request-Groups, X-Auth-Request-Email and X-Auth-Request-Preferred-Username response headers (useful in Nginx auth_request mode). When used with `--pass-access-token`, X-Auth-Request-Access-Token is added to response headers. | false |
112117| `--set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
113118| `--set-basic-auth` | bool | set HTTP Basic Auth information in response (useful in Nginx auth_request mode) | false |
119+| `--show-debug-on-error` | bool | show detailed error information on error pages (WARNING: this may contain sensitive information - do not use in production) | false |
114120| `--signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
115121| `--silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
116122| `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
@@ -127,7 +133,6 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
127133| `--tls-cert-file` | string | path to certificate file | |
128134| `--tls-key-file` | string | path to private key file | |
129135| `--upstream` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | |
130−| `--user-id-claim` | string | which claim contains the user ID | \["email"\] |
131136| `--allowed-group` | string \| list | restrict logins to members of this group (may be given multiple times) | |
132137| `--validate-url` | string | Access token validation endpoint | |
133138| `--version` | n/a | print version string | |
@@ -353,6 +358,173 @@ It is recommended to use `--session-store-type=redis` when expecting large sessi
353358
354359You 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=".
355360
361+## Configuring for use with the Traefik (v2) `ForwardAuth` middleware
362+
363+**This option requires `--reverse-proxy` option to be set.**
364+
365+### ForwardAuth with 401 errors middleware
366+
367+The [Traefik v2 `ForwardAuth` middleware](https://doc.traefik.io/traefik/middlewares/forwardauth/) allows Traefik to authenticate requests via the oauth2-proxy's `/oauth2/auth` endpoint on every request, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the whole request through. For example, on Dynamic File (YAML) Configuration:
368+
369+```yaml
370+http:
371+ routers:
372+ a-service:
373+ rule: "Host(`a-service.example.com`)"
374+ service: a-service-backend
375+ middlewares:
376+ - oauth-errors
377+ - oauth-auth
378+ tls:
379+ certResolver: default
380+ domains:
381+ - main: "example.com"
382+ sans:
383+ - "*.example.com"
384+ oauth:
385+ rule: "Host(`a-service.example.com`, `oauth.example.com`) && PathPrefix(`/oauth2/`)"
386+ middlewares:
387+ - auth-headers
388+ service: oauth-backend
389+ tls:
390+ certResolver: default
391+ domains:
392+ - main: "example.com"
393+ sans:
394+ - "*.example.com"
395+
396+ services:
397+ a-service-backend:
398+ loadBalancer:
399+ servers:
400+ - url: http://172.16.0.2:7555
401+ oauth-backend:
402+ loadBalancer:
403+ servers:
404+ - url: http://172.16.0.1:4180
405+
406+ middlewares:
407+ auth-headers:
408+ headers:
409+ sslRedirect: true
410+ stsSeconds: 315360000
411+ browserXssFilter: true
412+ contentTypeNosniff: true
413+ forceSTSHeader: true
414+ sslHost: example.com
415+ stsIncludeSubdomains: true
416+ stsPreload: true
417+ frameDeny: true
418+ oauth-auth:
419+ forwardAuth:
420+ address: https://oauth.example.com/oauth2/auth
421+ trustForwardHeader: true
422+ oauth-errors:
423+ errors:
424+ status:
425+ - "401-403"
426+ service: oauth-backend
427+ query: "/oauth2/sign_in"
428+```
429+
430+### ForwardAuth with static upstreams configuration
431+
432+Redirect to sign_in functionality provided without the use of `errors` middleware with [Traefik v2 `ForwardAuth` middleware](https://doc.traefik.io/traefik/middlewares/forwardauth/) pointing to oauth2-proxy service's `/` endpoint
433+
434+**Following options need to be set on `oauth2-proxy`:**
435+- `--upstream=static://202`: Configures a static response for authenticated sessions
436+- `--reverseproxy=true`: Enables the use of `X-Forwarded-*` headers to determine redirects correctly
437+
438+```yaml
439+http:
440+ routers:
441+ a-service-route-1:
442+ rule: "Host(`a-service.example.com`, `b-service.example.com`) && PathPrefix(`/`)"
443+ service: a-service-backend
444+ middlewares:
445+ - oauth-auth-redirect # redirects all unauthenticated to oauth2 signin
446+ tls:
447+ certResolver: default
448+ domains:
449+ - main: "example.com"
450+ sans:
451+ - "*.example.com"
452+ a-service-route-2:
453+ rule: "Host(`a-service.example.com`) && PathPrefix(`/no-auto-redirect`)"
454+ service: a-service-backend
455+ middlewares:
456+ - oauth-auth-wo-redirect # unauthenticated session will return a 401
457+ tls:
458+ certResolver: default
459+ domains:
460+ - main: "example.com"
461+ sans:
462+ - "*.example.com"
463+ services-oauth2-route:
464+ rule: "Host(`a-service.example.com`, `b-service.example.com`) && PathPrefix(`/oauth2/`)"
465+ middlewares:
466+ - auth-headers
467+ service: oauth-backend
468+ tls:
469+ certResolver: default
470+ domains:
471+ - main: "example.com"
472+ sans:
473+ - "*.example.com"
474+ oauth2-proxy-route:
475+ rule: "Host(`oauth.example.com`) && PathPrefix(`/`)"
476+ middlewares:
477+ - auth-headers
478+ service: oauth-backend
479+ tls:
480+ certResolver: default
481+ domains:
482+ - main: "example.com"
483+ sans:
484+ - "*.example.com"
485+
486+ services:
487+ a-service-backend:
488+ loadBalancer:
489+ servers:
490+ - url: http://172.16.0.2:7555
491+ b-service-backend:
492+ loadBalancer:
493+ servers:
494+ - url: http://172.16.0.3:7555
495+ oauth-backend:
496+ loadBalancer:
497+ servers:
498+ - url: http://172.16.0.1:4180
499+
500+ middlewares:
501+ auth-headers:
502+ headers:
503+ sslRedirect: true
504+ stsSeconds: 315360000
505+ browserXssFilter: true
506+ contentTypeNosniff: true
507+ forceSTSHeader: true
508+ sslHost: example.com
509+ stsIncludeSubdomains: true
510+ stsPreload: true
511+ frameDeny: true
512+ oauth-auth-redirect:
513+ forwardAuth:
514+ address: https://oauth.example.com/
515+ trustForwardHeader: true
516+ authResponseHeaders:
517+ - X-Auth-Request-Access-Token
518+ - Authorization
519+ oauth-auth-wo-redirect:
520+ forwardAuth:
521+ address: https://oauth.example.com/oauth2/auth
522+ trustForwardHeader: true
523+ authResponseHeaders:
524+ - X-Auth-Request-Access-Token
525+ - Authorization
526+```
527+
356528:::note
357529If 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.
358530:::
docs/docs/features/endpoints.md+1
@@ -7,6 +7,7 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w
77
88- /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info
99- /ping - returns a 200 OK response, which is intended for use with health checks
10+- /metrics - Metrics endpoint for Prometheus to scrape, serve on the address specified by `--metrics-address`, disabled by default
1011- /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
1112- /oauth2/sign_out - this URL is used to clear the session cookie
1213- /oauth2/start - a URL that will redirect to start the OAuth cycle
docs/docs/installation.md+1−1
@@ -6,7 +6,7 @@ slug: /
66
771. Choose how to deploy:
88
9− a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v6.1.1`)
9+ a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.0.1`)
1010
1111 b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy/v7` which will put the binary in `$GOPATH/bin`
1212
docs/sidebars.js+7−1
@@ -12,7 +12,7 @@ module.exports = {
1212 type: 'category',
1313 label: 'Configuration',
1414 collapsed: false,
15− items: ['configuration/overview', 'configuration/oauth_provider', 'configuration/session_storage', 'configuration/tls'],
15+ items: ['configuration/overview', 'configuration/oauth_provider', 'configuration/session_storage', 'configuration/tls', 'configuration/alpha-config'],
1616 },
1717 {
1818 type: 'category',
@@ -20,5 +20,11 @@ module.exports = {
2020 collapsed: false,
2121 items: ['features/endpoints', 'features/request_signatures'],
2222 },
23+ {
24+ type: 'category',
25+ label: 'Community',
26+ collapsed: false,
27+ items: ['community/security'],
28+ },
2329 ],
2430};
docs/versioned_docs/version-6.1.x/community/security.mdnew file+49
@@ -0,0 +1,49 @@
1+---
2+id: security
3+title: Security
4+---
5+
6+:::note
7+OAuth2 Proxy is a community project.
8+Maintainers do not work on this project full time, and as such,
9+while we endeavour to respond to disclosures as quickly as possible,
10+this may take longer than in projects with corporate sponsorship.
11+:::
12+
13+## Security Disclosures
14+
15+:::important
16+If you believe you have found a vulnerability within OAuth2 Proxy or any of its
17+dependencies, please do NOT open an issue or PR on GitHub, please do NOT post any
18+details publicly.
19+:::
20+
21+Security disclosures MUST be done in private.
22+If you have found an issue that you would like to bring to the attention of the
23+maintenance team for OAuth2 Proxy, please compose an email and send it to the
24+list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file.
25+
26+Please include as much detail as possible.
27+Ideally, your disclosure should include:
28+- A reproducible case that can be used to demonstrate the exploit
29+- How you discovered this vulnerability
30+- A potential fix for the issue (if you have thought of one)
31+- Versions affected (if not present in master)
32+- Your GitHub ID
33+
34+### How will we respond to disclosures?
35+
36+We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories)
37+to privately discuss fixes for disclosed vulnerabilities.
38+If you include a GitHub ID with your disclosure we will add you as a collaborator
39+for the advisory so that you can join the discussion and validate any fixes
40+we may propose.
41+
42+For minor issues and previously disclosed vulnerabilities (typically for
43+dependencies), we may use regular PRs for fixes and forego the security advisory.
44+
45+Once a fix has been agreed upon, we will merge the fix and create a new release.
46+If we have multiple security issues in flight simultaneously, we may delay
47+merging fixes until all patches are ready.
48+We may also backport the fix to previous releases,
49+but this will be at the discretion of the maintainers.
docs/versioned_docs/version-6.1.x/configuration/auth.md+1−1
@@ -149,7 +149,7 @@ The group management in keycloak is using a tree. If you create a group named ad
149149
150150Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](https://docs.gitlab.com/ce/integration/oauth_provider.html). Make sure to enable at least the `openid`, `profile` and `email` scopes, and set the redirect url to your application url e.g. https://myapp.com/oauth2/callback.
151151
152−The following config should be set to ensure that the oauth will work properly. To get a cookie secret follow [these steps](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/docs/configuration/configuration.md#configuration)
152+The following config should be set to ensure that the oauth will work properly. To get a cookie secret follow [these steps](./overview.md#generating-a-cookie-secret)
153153
154154```
155155 --provider="gitlab"
docs/versioned_docs/version-6.1.x/configuration/overview.md+2
@@ -5,6 +5,8 @@ title: Overview
55
66`oauth2-proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables).
77
8+### Generating a Cookie Secret
9+
810To generate a strong cookie secret use `python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(16)).decode())'`
911
1012### Config File
docs/versioned_docs/version-7.0.x/behaviour.mdnew file+11
@@ -0,0 +1,11 @@
1+---
2+id: behaviour
3+title: Behaviour
4+---
5+
6+1. Any request passing through the proxy (and not matched by `--skip-auth-regex`) is checked for the proxy's session cookie (`--cookie-name`) (or, if allowed, a JWT token - see `--skip-jwt-bearer-tokens`).
7+2. If authentication is required but missing then the user is asked to log in and redirected to the authentication provider (unless it is an Ajax request, i.e. one with `Accept: application/json`, in which case 401 Unauthorized is returned)
8+3. After returning from the authentication provider, the oauth tokens are stored in the configured session store (cookie, redis, ...) and a cookie is set
9+4. The request is forwarded to the upstream server with added user info and authentication headers (depending on the configuration)
10+
11+Notice that the proxy also provides a number of useful [endpoints](features/endpoints.md).
docs/versioned_docs/version-7.0.x/community/security.mdnew file+49
@@ -0,0 +1,49 @@
1+---
2+id: security
3+title: Security
4+---
5+
6+:::note
7+OAuth2 Proxy is a community project.
8+Maintainers do not work on this project full time, and as such,
9+while we endeavour to respond to disclosures as quickly as possible,
10+this may take longer than in projects with corporate sponsorship.
11+:::
12+
13+## Security Disclosures
14+
15+:::important
16+If you believe you have found a vulnerability within OAuth2 Proxy or any of its
17+dependencies, please do NOT open an issue or PR on GitHub, please do NOT post
18+any details publicly.
19+:::
20+
21+Security disclosures MUST be done in private.
22+If you have found an issue that you would like to bring to the attention of the
23+maintenance team for OAuth2 Proxy, please compose an email and send it to the
24+list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file.
25+
26+Please include as much detail as possible.
27+Ideally, your disclosure should include:
28+- A reproducible case that can be used to demonstrate the exploit
29+- How you discovered this vulnerability
30+- A potential fix for the issue (if you have thought of one)
31+- Versions affected (if not present in master)
32+- Your GitHub ID
33+
34+### How will we respond to disclosures?
35+
36+We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories)
37+to privately discuss fixes for disclosed vulnerabilities.
38+If you include a GitHub ID with your disclosure we will add you as a collaborator
39+for the advisory so that you can join the discussion and validate any fixes
40+we may propose.
41+
42+For minor issues and previously disclosed vulnerabilities (typically for
43+dependencies), we may use regular PRs for fixes and forego the security advisory.
44+
45+Once a fix has been agreed upon, we will merge the fix and create a new release.
46+If we have multiple security issues in flight simultaneously, we may delay
47+merging fixes until all patches are ready.
48+We may also backport the fix to previous releases,
49+but this will be at the discretion of the maintainers.
docs/versioned_docs/version-7.0.x/configuration/alpha_config.mdnew file+212
@@ -0,0 +1,212 @@
1+---
2+id: alpha-config
3+title: Alpha Configuration
4+---
5+
6+:::warning
7+This page contains documentation for alpha features.
8+We reserve the right to make breaking changes to the features detailed within this page with no notice.
9+
10+Options described in this page may be changed, removed, renamed or moved without prior warning.
11+Please beware of this before you use alpha configuration options.
12+:::
13+
14+This page details a set of **alpha** configuration options in a new format.
15+Going forward we are intending to add structured configuration in YAML format to
16+replace the existing TOML based configuration file and flags.
17+
18+Below is a reference for the structure of the configuration, with
19+[AlphaOptions](#alphaoptions) as the root of the configuration.
20+
21+When using alpha configuration, your config file will look something like below:
22+
23+```yaml
24+upstreams:
25+ - id: ...
26+ ...
27+injectRequestHeaders:
28+ - name: ...
29+ ...
30+injectResponseHeaders:
31+ - name: ...
32+ ...
33+```
34+
35+Please browse the [reference](#configuration-reference) below for the structure
36+of the new configuration format.
37+
38+## Using Alpha Configuration
39+
40+To use the new **alpha** configuration, generate a YAML file based on the format
41+described in the [reference](#configuration-reference) below.
42+
43+Provide the path to this file using the `--alpha-config` flag.
44+
45+:::note
46+When using the `--alpha-config` flag, some options are no longer available.
47+See [removed options](#removed-options) below for more information.
48+:::
49+
50+### Converting configuration to the new structure
51+
52+Before adding the new `--alpha-config` option, start OAuth2 Proxy using the
53+`convert-config-to-alpha` flag to convert existing configuration to the new format.
54+
55+```bash
56+oauth2-proxy --convert-config-to-alpha --config ./path/to/existing/config.cfg
57+```
58+
59+This will convert any options supported by the new format to YAML and print the
60+new configuration to `STDOUT`.
61+
62+Copy this to a new file, remove any options from your existing configuration
63+noted in [removed options](#removed-options) and then start OAuth2 Proxy using
64+the new config.
65+
66+```bash
67+oauth2-proxy --alpha-config ./path/to/new/config.yaml --config ./path/to/existing/config.cfg
68+```
69+
70+## Removed options
71+
72+The following flags/options and their respective environment variables are no
73+longer available when using alpha configuration:
74+
75+<!-- Legacy Upstream FlagSet -->
76+- `flush-interval`/`flush_interval`
77+- `pass-host-header`/`pass_host_header`
78+- `proxy-websockets`/`proxy_websockets`
79+- `ssl-upstream-insecure-skip-verify`/`ssl_upstream_insecure_skip_verify`
80+- `upstream`/`upstreams`
81+
82+<!-- Legacy Headers FlagSet -->
83+- `pass-basic-auth`/`pass_basic_auth`
84+- `pass-access-token`/`pass_access_token`
85+- `pass-user-headers`/`pass_user_headers`
86+- `pass-authorization-header`/`pass_authorization_header`
87+- `set-basic-auth`/`set_basic_auth`
88+- `set-xauthrequest`/`set_xauthrequest`
89+- `set-authorization-header`/`set_authorization_header`
90+- `prefer-email-to-user`/`prefer_email_to_user`
91+- `basic-auth-password`/`basic_auth_password`
92+- `skip-auth-strip-headers`/`skip_auth_strip_headers`
93+
94+Attempting to use these options via flags or via config when `--alpha-config`
95+set will result in an error.
96+
97+:::important
98+You must remove these options before starting OAuth2 Proxy with `--alpha-config`
99+:::
100+
101+## Configuration Reference
102+<!--- THIS FILE IS AUTOGENERATED!!! DO NOT EDIT!!! -->
103+
104+### AlphaOptions
105+
106+AlphaOptions contains alpha structured configuration options.
107+Usage of these options allows users to access alpha features that are not
108+available as part of the primary configuration structure for OAuth2 Proxy.
109+
110+:::warning
111+The options within this structure are considered alpha.
112+They may change between releases without notice.
113+:::
114+
115+| Field | Type | Description |
116+| ----- | ---- | ----------- |
117+| `upstreams` | _[Upstreams](#upstreams)_ | Upstreams is used to configure upstream servers.<br/>Once a user is authenticated, requests to the server will be proxied to<br/>these upstream servers based on the path mappings defined in this list. |
118+| `injectRequestHeaders` | _[[]Header](#header)_ | InjectRequestHeaders is used to configure headers that should be added<br/>to requests to upstream servers.<br/>Headers may source values from either the authenticated user's session<br/>or from a static secret value. |
119+| `injectResponseHeaders` | _[[]Header](#header)_ | InjectResponseHeaders is used to configure headers that should be added<br/>to responses from the proxy.<br/>This is typically used when using the proxy as an external authentication<br/>provider in conjunction with another proxy such as NGINX and its<br/>auth_request module.<br/>Headers may source values from either the authenticated user's session<br/>or from a static secret value. |
120+
121+### ClaimSource
122+
123+(**Appears on:** [HeaderValue](#headervalue))
124+
125+ClaimSource allows loading a header value from a claim within the session
126+
127+| Field | Type | Description |
128+| ----- | ---- | ----------- |
129+| `claim` | _string_ | Claim is the name of the claim in the session that the value should be<br/>loaded from. |
130+| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
131+| `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.<br/>Note the value of claim will become the basic auth username and the<br/>basicAuthPassword will be used as the password value. |
132+
133+### Duration
134+#### (`string` alias)
135+
136+(**Appears on:** [Upstream](#upstream))
137+
138+Duration is as string representation of a period of time.
139+A duration string is a is a possibly signed sequence of decimal numbers,
140+each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".
141+Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
142+
143+
144+### Header
145+
146+(**Appears on:** [AlphaOptions](#alphaoptions))
147+
148+Header represents an individual header that will be added to a request or
149+response header.
150+
151+| Field | Type | Description |
152+| ----- | ---- | ----------- |
153+| `name` | _string_ | Name is the header name to be used for this set of values.<br/>Names should be unique within a list of Headers. |
154+| `preserveRequestValue` | _bool_ | PreserveRequestValue determines whether any values for this header<br/>should be preserved for the request to the upstream server.<br/>This option only takes effet on injected request headers.<br/>Defaults to false (headers that match this header will be stripped). |
155+| `values` | _[[]HeaderValue](#headervalue)_ | Values contains the desired values for this header |
156+
157+### HeaderValue
158+
159+(**Appears on:** [Header](#header))
160+
161+HeaderValue represents a single header value and the sources that can
162+make up the header value
163+
164+| Field | Type | Description |
165+| ----- | ---- | ----------- |
166+| `value` | _[]byte_ | Value expects a base64 encoded string value. |
167+| `fromEnv` | _string_ | FromEnv expects the name of an environment variable. |
168+| `fromFile` | _string_ | FromFile expects a path to a file containing the secret value. |
169+| `claim` | _string_ | Claim is the name of the claim in the session that the value should be<br/>loaded from. |
170+| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
171+| `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.<br/>Note the value of claim will become the basic auth username and the<br/>basicAuthPassword will be used as the password value. |
172+
173+### SecretSource
174+
175+(**Appears on:** [ClaimSource](#claimsource), [HeaderValue](#headervalue))
176+
177+SecretSource references an individual secret value.
178+Only one source within the struct should be defined at any time.
179+
180+| Field | Type | Description |
181+| ----- | ---- | ----------- |
182+| `value` | _[]byte_ | Value expects a base64 encoded string value. |
183+| `fromEnv` | _string_ | FromEnv expects the name of an environment variable. |
184+| `fromFile` | _string_ | FromFile expects a path to a file containing the secret value. |
185+
186+### Upstream
187+
188+(**Appears on:** [Upstreams](#upstreams))
189+
190+Upstream represents the configuration for an upstream server.
191+Requests will be proxied to this upstream if the path matches the request path.
192+
193+| Field | Type | Description |
194+| ----- | ---- | ----------- |
195+| `id` | _string_ | ID should be a unique identifier for the upstream.<br/>This value is required for all upstreams. |
196+| `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. |
197+| `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". |
198+| `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/>betweem OAuth2 Proxy and the usptream server.<br/>Defaults to false. |
199+| `static` | _bool_ | Static will make all requests to this upstream have a static response.<br/>The response will have a body of "Authenticated" and a response code<br/>matching StaticCode.<br/>If StaticCode is not set, the response will return a 200 response. |
200+| `staticCode` | _int_ | StaticCode determines the response code for the Static response.<br/>This option can only be used with Static enabled. |
201+| `flushInterval` | _[Duration](#duration)_ | FlushInterval is the period between flushing the response buffer when<br/>streaming response from the upstream.<br/>Defaults to 1 second. |
202+| `passHostHeader` | _bool_ | PassHostHeader determines whether the request host header should be proxied<br/>to the upstream server.<br/>Defaults to true. |
203+| `proxyWebSockets` | _bool_ | ProxyWebSockets enables proxying of websockets to upstream servers<br/>Defaults to true. |
204+
205+### Upstreams
206+
207+#### ([[]Upstream](#upstream) alias)
208+
209+(**Appears on:** [AlphaOptions](#alphaoptions))
210+
211+Upstreams is a collection of definitions for upstream servers.
212+
docs/versioned_docs/version-7.0.x/configuration/alpha_config.md.tmplnew file+101
@@ -0,0 +1,101 @@
1+---
2+id: alpha-config
3+title: Alpha Configuration
4+---
5+
6+:::warning
7+This page contains documentation for alpha features.
8+We reserve the right to make breaking changes to the features detailed within this page with no notice.
9+
10+Options described in this page may be changed, removed, renamed or moved without prior warning.
11+Please beware of this before you use alpha configuration options.
12+:::
13+
14+This page details a set of **alpha** configuration options in a new format.
15+Going forward we are intending to add structured configuration in YAML format to
16+replace the existing TOML based configuration file and flags.
17+
18+Below is a reference for the structure of the configuration, with
19+[AlphaOptions](#alphaoptions) as the root of the configuration.
20+
21+When using alpha configuration, your config file will look something like below:
22+
23+```yaml
24+upstreams:
25+ - id: ...
26+ ...
27+injectRequestHeaders:
28+ - name: ...
29+ ...
30+injectResponseHeaders:
31+ - name: ...
32+ ...
33+```
34+
35+Please browse the [reference](#configuration-reference) below for the structure
36+of the new configuration format.
37+
38+## Using Alpha Configuration
39+
40+To use the new **alpha** configuration, generate a YAML file based on the format
41+described in the [reference](#configuration-reference) below.
42+
43+Provide the path to this file using the `--alpha-config` flag.
44+
45+:::note
46+When using the `--alpha-config` flag, some options are no longer available.
47+See [removed options](#removed-options) below for more information.
48+:::
49+
50+### Converting configuration to the new structure
51+
52+Before adding the new `--alpha-config` option, start OAuth2 Proxy using the
53+`convert-config-to-alpha` flag to convert existing configuration to the new format.
54+
55+```bash
56+oauth2-proxy --convert-config-to-alpha --config ./path/to/existing/config.cfg
57+```
58+
59+This will convert any options supported by the new format to YAML and print the
60+new configuration to `STDOUT`.
61+
62+Copy this to a new file, remove any options from your existing configuration
63+noted in [removed options](#removed-options) and then start OAuth2 Proxy using
64+the new config.
65+
66+```bash
67+oauth2-proxy --alpha-config ./path/to/new/config.yaml --config ./path/to/existing/config.cfg
68+```
69+
70+## Removed options
71+
72+The following flags/options and their respective environment variables are no
73+longer available when using alpha configuration:
74+
75+<!-- Legacy Upstream FlagSet -->
76+- `flush-interval`/`flush_interval`
77+- `pass-host-header`/`pass_host_header`
78+- `proxy-websockets`/`proxy_websockets`
79+- `ssl-upstream-insecure-skip-verify`/`ssl_upstream_insecure_skip_verify`
80+- `upstream`/`upstreams`
81+
82+<!-- Legacy Headers FlagSet -->
83+- `pass-basic-auth`/`pass_basic_auth`
84+- `pass-access-token`/`pass_access_token`
85+- `pass-user-headers`/`pass_user_headers`
86+- `pass-authorization-header`/`pass_authorization_header`
87+- `set-basic-auth`/`set_basic_auth`
88+- `set-xauthrequest`/`set_xauthrequest`
89+- `set-authorization-header`/`set_authorization_header`
90+- `prefer-email-to-user`/`prefer_email_to_user`
91+- `basic-auth-password`/`basic_auth_password`
92+- `skip-auth-strip-headers`/`skip_auth_strip_headers`
93+
94+Attempting to use these options via flags or via config when `--alpha-config`
95+set will result in an error.
96+
97+:::important
98+You must remove these options before starting OAuth2 Proxy with `--alpha-config`
99+:::
100+
101+## Configuration Reference
docs/versioned_docs/version-7.0.x/configuration/auth.mdnew file+510
@@ -0,0 +1,510 @@
1+---
2+id: oauth_provider
3+title: OAuth Provider Configuration
4+---
5+
6+You will need to register an OAuth application with a Provider (Google, GitHub or another provider), and configure it with Redirect URI(s) for the domain you intend to run `oauth2-proxy` on.
7+
8+Valid providers are :
9+
10+- [Google](#google-auth-provider) _default_
11+- [Azure](#azure-auth-provider)
12+- [Facebook](#facebook-auth-provider)
13+- [GitHub](#github-auth-provider)
14+- [Keycloak](#keycloak-auth-provider)
15+- [GitLab](#gitlab-auth-provider)
16+- [LinkedIn](#linkedin-auth-provider)
17+- [Microsoft Azure AD](#microsoft-azure-ad-provider)
18+- [OpenID Connect](#openid-connect-provider)
19+- [login.gov](#logingov-provider)
20+- [Nextcloud](#nextcloud-provider)
21+- [DigitalOcean](#digitalocean-auth-provider)
22+- [Bitbucket](#bitbucket-auth-provider)
23+- [Gitea](#gitea-auth-provider)
24+
25+The provider can be selected using the `provider` configuration value.
26+
27+Please note that not all providers support all claims. The `preferred_username` claim is currently only supported by the OpenID Connect provider.
28+
29+### Google Auth Provider
30+
31+For Google, the registration steps are:
32+
33+1. Create a new project: https://console.developers.google.com/project
34+2. Choose the new project from the top right project dropdown (only if another project is selected)
35+3. In the project Dashboard center pane, choose **"API Manager"**
36+4. In the left Nav pane, choose **"Credentials"**
37+5. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save.
38+6. In the center pane, choose **"Credentials"** tab.
39+ - Open the **"New credentials"** drop down
40+ - Choose **"OAuth client ID"**
41+ - Choose **"Web application"**
42+ - Application name is freeform, choose something appropriate
43+ - Authorized JavaScript origins is your domain ex: `https://internal.yourcompany.com`
44+ - Authorized redirect URIs is the location of oauth2/callback ex: `https://internal.yourcompany.com/oauth2/callback`
45+ - Choose **"Create"**
46+7. Take note of the **Client ID** and **Client Secret**
47+
48+It's recommended to refresh sessions on a short interval (1h) with `cookie-refresh` setting which validates that the account is still authorized.
49+
50+#### Restrict auth to specific Google groups on your domain. (optional)
51+
52+1. Create a service account: https://developers.google.com/identity/protocols/OAuth2ServiceAccount and make sure to download the json file.
53+2. Make note of the Client ID for a future step.
54+3. Under "APIs & Auth", choose APIs.
55+4. Click on Admin SDK and then Enable API.
56+5. Follow the steps on https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account and give the client id from step 2 the following oauth scopes:
57+
58+```
59+https://www.googleapis.com/auth/admin.directory.group.readonly
60+https://www.googleapis.com/auth/admin.directory.user.readonly
61+```
62+
63+6. Follow the steps on https://support.google.com/a/answer/60757 to enable Admin API access.
64+7. Create or choose an existing administrative email address on the Gmail domain to assign to the `google-admin-email` flag. This email will be impersonated by this client to make calls to the Admin SDK. See the note on the link from step 5 for the reason why.
65+8. Create or choose an existing email group and set that email to the `google-group` flag. You can pass multiple instances of this flag with different groups
66+ and the user will be checked against all the provided groups.
67+9. Lock down the permissions on the json file downloaded from step 1 so only oauth2-proxy is able to read the file and set the path to the file in the `google-service-account-json` flag.
68+10. Restart oauth2-proxy.
69+
70+Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ).
71+
72+### Azure Auth Provider
73+
74+1. Add an application: go to [https://portal.azure.com](https://portal.azure.com), choose **"Azure Active Directory"** in the left menu, select **"App registrations"** and then click on **"New app registration"**.
75+2. Pick a name and choose **"Webapp / API"** as application type. Use `https://internal.yourcompany.com` as Sign-on URL. Click **"Create"**.
76+3. On the **"Settings"** / **"Properties"** page of the app, pick a logo and select **"Multi-tenanted"** if you want to allow users from multiple organizations to access your app. Note down the application ID. Click **"Save"**.
77+4. On the **"Settings"** / **"Required Permissions"** page of the app, click on **"Windows Azure Active Directory"** and then on **"Access the directory as the signed in user"**. Hit **"Save"** and then then on **"Grant permissions"** (you might need another admin to do this).
78+5. On the **"Settings"** / **"Reply URLs"** page of the app, add `https://internal.yourcompanycom/oauth2/callback` for each host that you want to protect by the oauth2 proxy. Click **"Save"**.
79+6. On the **"Settings"** / **"Keys"** page of the app, add a new key and note down the value after hitting **"Save"**.
80+7. Configure the proxy with
81+
82+```
83+ --provider=azure
84+ --client-id=<application ID from step 3>
85+ --client-secret=<value from step 6>
86+```
87+
88+Note: When using the Azure Auth provider with nginx and the cookie session store you may find the cookie is too large and doesn't get passed through correctly. Increasing the proxy_buffer_size in nginx or implementing the [redis session storage](sessions.md#redis-storage) should resolve this.
89+
90+### Facebook Auth Provider
91+
92+1. Create a new FB App from <https://developers.facebook.com/>
93+2. Under FB Login, set your Valid OAuth redirect URIs to `https://internal.yourcompany.com/oauth2/callback`
94+
95+### GitHub Auth Provider
96+
97+1. Create a new project: https://github.com/settings/developers
98+2. Under `Authorization callback URL` enter the correct url ie `https://internal.yourcompany.com/oauth2/callback`
99+
100+The GitHub auth provider supports two additional ways to restrict authentication to either organization and optional team level access, or to collaborators of a repository. Restricting by these options is normally accompanied with `--email-domain=*`
101+
102+NOTE: When `--github-user` is set, the specified users are allowed to login even if they do not belong to the specified org and team or collaborators.
103+
104+To restrict by organization only, include the following flag:
105+
106+ -github-org="": restrict logins to members of this organisation
107+
108+To restrict within an organization to specific teams, include the following flag in addition to `-github-org`:
109+
110+ -github-team="": restrict logins to members of any of these teams (slug), separated by a comma
111+
112+If you would rather restrict access to collaborators of a repository, those users must either have push access to a public repository or any access to a private repository:
113+
114+ -github-repo="": restrict logins to collaborators of this repository formatted as orgname/repo
115+
116+If you'd like to allow access to users with **read only** access to a **public** repository you will need to provide a [token](https://github.com/settings/tokens) for a user that has write access to the repository. The token must be created with at least the `public_repo` scope:
117+
118+ -github-token="": the token to use when verifying repository collaborators
119+
120+To allow a user to login with their username even if they do not belong to the specified org and team or collaborators, separated by a comma
121+
122+ -github-user="": allow logins by username, separated by a comma
123+
124+If you are using GitHub enterprise, make sure you set the following to the appropriate url:
125+
126+ -login-url="http(s)://<enterprise github host>/login/oauth/authorize"
127+ -redeem-url="http(s)://<enterprise github host>/login/oauth/access_token"
128+ -validate-url="http(s)://<enterprise github host>/api/v3"
129+
130+### Keycloak Auth Provider
131+
132+1. Create new client in your Keycloak with **Access Type** 'confidental' and **Valid Redirect URIs** 'https://internal.yourcompany.com/oauth2/callback'
133+2. Take note of the Secret in the credential tab of the client
134+3. Create a mapper with **Mapper Type** 'Group Membership' and **Token Claim Name** 'groups'.
135+
136+Make sure you set the following to the appropriate url:
137+
138+ --provider=keycloak
139+ --client-id=<client you have created>
140+ --client-secret=<your client's secret>
141+ --login-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/auth"
142+ --redeem-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/token"
143+ --profile-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo"
144+ --validate-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo"
145+ --keycloak-group=<first_allowed_user_group>
146+ --keycloak-group=<second_allowed_user_group>
147+
148+For group based authorization, the optional `--keycloak-group` (legacy) or `--allowed-group` (global standard)
149+flags can be used to specify which groups to limit access to.
150+
151+If these are unset but a `groups` mapper is set up above in step (3), the provider will still
152+populate the `X-Forwarded-Groups` header to your upstream server with the `groups` data in the
153+Keycloak userinfo endpoint response.
154+
155+The group management in keycloak is using a tree. If you create a group named admin in keycloak
156+you should define the 'keycloak-group' value to /admin.
157+
158+### GitLab Auth Provider
159+
160+This auth provider has been tested against Gitlab version 12.X. Due to Gitlab API changes, it may not work for version prior to 12.X (see [994](https://github.com/oauth2-proxy/oauth2-proxy/issues/994)).
161+
162+Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](https://docs.gitlab.com/ce/integration/oauth_provider.html). Make sure to enable at least the `openid`, `profile` and `email` scopes, and set the redirect url to your application url e.g. https://myapp.com/oauth2/callback.
163+
164+If you need projects filtering, add the extra `read_api` scope to your application.
165+
166+The following config should be set to ensure that the oauth will work properly. To get a cookie secret follow [these steps](./overview.md#generating-a-cookie-secret)
167+
168+```
169+ --provider="gitlab"
170+ --redirect-url="https://myapp.com/oauth2/callback" // Should be the same as the redirect url for the application in gitlab
171+ --client-id=GITLAB_CLIENT_ID
172+ --client-secret=GITLAB_CLIENT_SECRET
173+ --cookie-secret=COOKIE_SECRET
174+```
175+
176+Restricting by group membership is possible with the following option:
177+
178+ --gitlab-group="mygroup,myothergroup": restrict logins to members of any of these groups (slug), separated by a comma
179+
180+If you are using self-hosted GitLab, make sure you set the following to the appropriate URL:
181+
182+ --oidc-issuer-url="<your gitlab url>"
183+
184+### LinkedIn Auth Provider
185+
186+For LinkedIn, the registration steps are:
187+
188+1. Create a new project: https://www.linkedin.com/secure/developer
189+2. In the OAuth User Agreement section:
190+ - In default scope, select r_basicprofile and r_emailaddress.
191+ - In "OAuth 2.0 Redirect URLs", enter `https://internal.yourcompany.com/oauth2/callback`
192+3. Fill in the remaining required fields and Save.
193+4. Take note of the **Consumer Key / API Key** and **Consumer Secret / Secret Key**
194+
195+### Microsoft Azure AD Provider
196+
197+For adding an application to the Microsoft Azure AD follow [these steps to add an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
198+
199+Take note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server.
200+
201+### OpenID Connect Provider
202+
203+OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects.
204+
205+This provider was originally built against CoreOS Dex and we will use it as an example.
206+The OpenID Connect Provider (OIDC) can also be used to connect to other Identity Providers such as Okta, an example can be found below.
207+
208+#### Dex
209+
210+To configure the OIDC provider for Dex, perform the following steps:
211+
212+1. Download Dex:
213+
214+ ```
215+ go get github.com/dexidp/dex
216+ ```
217+
218+ See the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md) for more details.
219+
220+2. Setup oauth2-proxy with the correct provider and using the default ports and callbacks. Add a configuration block to the `staticClients` section of `examples/config-dev.yaml`:
221+
222+ ```
223+ - id: oauth2-proxy
224+ redirectURIs:
225+ - 'http://127.0.0.1:4180/oauth2/callback'
226+ name: 'oauth2-proxy'
227+ secret: proxy
228+ ```
229+
230+3. Launch Dex: from `$GOPATH/github.com/dexidp/dex`, run:
231+
232+ ```
233+ bin/dex serve examples/config-dev.yaml
234+ ```
235+
236+4. In a second terminal, run the oauth2-proxy with the following args:
237+
238+ ```
239+ -provider oidc
240+ -provider-display-name "My OIDC Provider"
241+ -client-id oauth2-proxy
242+ -client-secret proxy
243+ -redirect-url http://127.0.0.1:4180/oauth2/callback
244+ -oidc-issuer-url http://127.0.0.1:5556/dex
245+ -cookie-secure=false
246+ -cookie-secret=secret
247+ -email-domain kilgore.trout
248+ ```
249+
250+ To serve the current working directory as a web site under the `/static` endpoint, add:
251+
252+ ```
253+ -upstream file://$PWD/#/static/
254+ ```
255+
256+5. Test the setup by visiting http://127.0.0.1:4180 or http://127.0.0.1:4180/static .
257+
258+See also [our local testing environment](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/contrib/local-environment) for a self-contained example using Docker and etcd as storage for Dex.
259+
260+#### Okta
261+
262+To configure the OIDC provider for Okta, perform the following steps:
263+
264+1. Log in to Okta using an administrative account. It is suggested you try this in preview first, `example.oktapreview.com`
265+2. (OPTIONAL) If you want to configure authorization scopes and claims to be passed on to multiple applications,
266+you may wish to configure an authorization server for each application. Otherwise, the provided `default` will work.
267+* Navigate to **Security** then select **API**
268+* Click **Add Authorization Server**, if this option is not available you may require an additional license for a custom authorization server.
269+* Fill out the **Name** with something to describe the application you are protecting. e.g. 'Example App'.
270+* For **Audience**, pick the URL of the application you wish to protect: https://example.corp.com
271+* Fill out a **Description**
272+* Add any **Access Policies** you wish to configure to limit application access.
273+* The default settings will work for other options.
274+[See Okta documentation for more information on Authorization Servers](https://developer.okta.com/docs/guides/customize-authz-server/overview/)
275+3. Navigate to **Applications** then select **Add Application**.
276+* Select **Web** for the **Platform** setting.
277+* Select **OpenID Connect** and click **Create**
278+* Pick an **Application Name** such as `Example App`.
279+* Set the **Login redirect URI** to `https://example.corp.com`.
280+* Under **General** set the **Allowed grant types** to `Authorization Code` and `Refresh Token`.
281+* Leave the rest as default, taking note of the `Client ID` and `Client Secret`.
282+* Under **Assignments** select the users or groups you wish to access your application.
283+4. Create a configuration file like the following:
284+
285+ ```
286+ provider = "oidc"
287+ redirect_url = "https://example.corp.com/oauth2/callback"
288+ oidc_issuer_url = "https://corp.okta.com/oauth2/abCd1234"
289+ upstreams = [
290+ "https://example.corp.com"
291+ ]
292+ email_domains = [
293+ "corp.com"
294+ ]
295+ client_id = "XXXXX"
296+ client_secret = "YYYYY"
297+ pass_access_token = true
298+ cookie_secret = "ZZZZZ"
299+ skip_provider_button = true
300+ ```
301+
302+The `oidc_issuer_url` is based on URL from your **Authorization Server**'s **Issuer** field in step 2, or simply https://corp.okta.com .
303+The `client_id` and `client_secret` are configured in the application settings.
304+Generate a unique `client_secret` to encrypt the cookie.
305+
306+Then you can start the oauth2-proxy with `./oauth2-proxy --config /etc/example.cfg`
307+
308+#### Okta - localhost
309+
310+1. Signup for developer account: https://developer.okta.com/signup/
311+2. Create New `Web` Application: https://${your-okta-domain}/dev/console/apps/new
312+3. Example Application Settings for localhost:
313+ * **Name:** My Web App
314+ * **Base URIs:** http://localhost:4180/
315+ * **Login redirect URIs:** http://localhost:4180/oauth2/callback
316+ * **Logout redirect URIs:** http://localhost:4180/
317+ * **Group assignments:** `Everyone`
318+ * **Grant type allowed:** `Authorization Code` and `Refresh Token`
319+4. Make note of the `Client ID` and `Client secret`, they are needed in a future step
320+5. Make note of the **default** Authorization Server Issuer URI from: https://${your-okta-domain}/admin/oauth2/as
321+6. Example config file `/etc/localhost.cfg`
322+ ```
323+ provider = "oidc"
324+ redirect_url = "http://localhost:4180/oauth2/callback"
325+ oidc_issuer_url = "https://${your-okta-domain}/oauth2/default"
326+ upstreams = [
327+ "http://0.0.0.0:8080"
328+ ]
329+ email_domains = [
330+ "*"
331+ ]
332+ client_id = "XXX"
333+ client_secret = "YYY"
334+ pass_access_token = true
335+ cookie_secret = "ZZZ"
336+ cookie_secure = false
337+ skip_provider_button = true
338+ # Note: use the following for testing within a container
339+ # http_address = "0.0.0.0:4180"
340+ ```
341+7. Then you can start the oauth2-proxy with `./oauth2-proxy --config /etc/localhost.cfg`
342+
343+### login.gov Provider
344+
345+login.gov is an OIDC provider for the US Government.
346+If you are a US Government agency, you can contact the login.gov team through the contact information
347+that you can find on https://login.gov/developers/ and work with them to understand how to get login.gov
348+accounts for integration/test and production access.
349+
350+A developer guide is available here: https://developers.login.gov/, though this proxy handles everything
351+but the data you need to create to register your application in the login.gov dashboard.
352+
353+As a demo, we will assume that you are running your application that you want to secure locally on
354+http://localhost:3000/, that you will be starting your proxy up on http://localhost:4180/, and that
355+you have an agency integration account for testing.
356+
357+First, register your application in the dashboard. The important bits are:
358+ * Identity protocol: make this `Openid connect`
359+ * Issuer: do what they say for OpenID Connect. We will refer to this string as `${LOGINGOV_ISSUER}`.
360+ * Public key: This is a self-signed certificate in .pem format generated from a 2048 bit RSA private key.
361+ A quick way to do this is `openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes -subj '/C=US/ST=Washington/L=DC/O=GSA/OU=18F/CN=localhost'`,
362+ The contents of the `key.pem` shall be referred to as `${OAUTH2_PROXY_JWT_KEY}`.
363+ * Return to App URL: Make this be `http://localhost:4180/`
364+ * Redirect URIs: Make this be `http://localhost:4180/oauth2/callback`.
365+ * Attribute Bundle: Make sure that email is selected.
366+
367+Now start the proxy up with the following options:
368+```
369+./oauth2-proxy -provider login.gov \
370+ -client-id=${LOGINGOV_ISSUER} \
371+ -redirect-url=http://localhost:4180/oauth2/callback \
372+ -oidc-issuer-url=https://idp.int.identitysandbox.gov/ \
373+ -cookie-secure=false \
374+ -email-domain=gsa.gov \
375+ -upstream=http://localhost:3000/ \
376+ -cookie-secret=somerandomstring12341234567890AB \
377+ -cookie-domain=localhost \
378+ -skip-provider-button=true \
379+ -pubjwk-url=https://idp.int.identitysandbox.gov/api/openid_connect/certs \
380+ -profile-url=https://idp.int.identitysandbox.gov/api/openid_connect/userinfo \
381+ -jwt-key="${OAUTH2_PROXY_JWT_KEY}"
382+```
383+You can also set all these options with environment variables, for use in cloud/docker environments.
384+One tricky thing that you may encounter is that some cloud environments will pass in environment
385+variables in a docker env-file, which does not allow multiline variables like a PEM file.
386+If you encounter this, then you can create a `jwt_signing_key.pem` file in the top level
387+directory of the repo which contains the key in PEM format and then do your docker build.
388+The docker build process will copy that file into your image which you can then access by
389+setting the `OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem`
390+environment variable, or by setting `--jwt-key-file=/etc/ssl/private/jwt_signing_key.pem` on the commandline.
391+
392+Once it is running, you should be able to go to `http://localhost:4180/` in your browser,
393+get authenticated by the login.gov integration server, and then get proxied on to your
394+application running on `http://localhost:3000/`. In a real deployment, you would secure
395+your application with a firewall or something so that it was only accessible from the
396+proxy, and you would use real hostnames everywhere.
397+
398+#### Skip OIDC discovery
399+
400+Some providers do not support OIDC discovery via their issuer URL, so oauth2-proxy cannot simply grab the authorization, token and jwks URI endpoints from the provider's metadata.
401+
402+In this case, you can set the `--skip-oidc-discovery` option, and supply those required endpoints manually:
403+
404+```
405+ -provider oidc
406+ -client-id oauth2-proxy
407+ -client-secret proxy
408+ -redirect-url http://127.0.0.1:4180/oauth2/callback
409+ -oidc-issuer-url http://127.0.0.1:5556
410+ -skip-oidc-discovery
411+ -login-url http://127.0.0.1:5556/authorize
412+ -redeem-url http://127.0.0.1:5556/token
413+ -oidc-jwks-url http://127.0.0.1:5556/keys
414+ -cookie-secure=false
415+ -email-domain example.com
416+```
417+
418+### Nextcloud Provider
419+
420+The Nextcloud provider allows you to authenticate against users in your
421+Nextcloud instance.
422+
423+When you are using the Nextcloud provider, you must specify the urls via
424+configuration, environment variable, or command line argument. Depending
425+on whether your Nextcloud instance is using pretty urls your urls may be of the
426+form `/index.php/apps/oauth2/*` or `/apps/oauth2/*`.
427+
428+Refer to the [OAuth2
429+documentation](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/oauth2.html)
430+to setup the client id and client secret. Your "Redirection URI" will be
431+`https://internalapp.yourcompany.com/oauth2/callback`.
432+
433+```
434+ -provider nextcloud
435+ -client-id <from nextcloud admin>
436+ -client-secret <from nextcloud admin>
437+ -login-url="<your nextcloud url>/index.php/apps/oauth2/authorize"
438+ -redeem-url="<your nextcloud url>/index.php/apps/oauth2/api/v1/token"
439+ -validate-url="<your nextcloud url>/ocs/v2.php/cloud/user?format=json"
440+```
441+
442+Note: in *all* cases the validate-url will *not* have the `index.php`.
443+
444+### DigitalOcean Auth Provider
445+
446+1. [Create a new OAuth application](https://cloud.digitalocean.com/account/api/applications)
447+ * You can fill in the name, homepage, and description however you wish.
448+ * In the "Application callback URL" field, enter: `https://oauth-proxy/oauth2/callback`, substituting `oauth2-proxy` with the actual hostname that oauth2-proxy is running on. The URL must match oauth2-proxy's configured redirect URL.
449+2. Note the Client ID and Client Secret.
450+
451+To use the provider, pass the following options:
452+
453+```
454+ --provider=digitalocean
455+ --client-id=<Client ID>
456+ --client-secret=<Client Secret>
457+```
458+
459+ Alternatively, set the equivalent options in the config file. The redirect URL defaults to `https://<requested host header>/oauth2/callback`. If you need to change it, you can use the `--redirect-url` command-line option.
460+
461+### Bitbucket Auth Provider
462+
463+1. [Add a new OAuth consumer](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html)
464+ * In "Callback URL" use `https://<oauth2-proxy>/oauth2/callback`, substituting `<oauth2-proxy>` with the actual hostname that oauth2-proxy is running on.
465+ * In Permissions section select:
466+ * Account -> Email
467+ * Team membership -> Read
468+ * Repositories -> Read
469+2. Note the Client ID and Client Secret.
470+
471+To use the provider, pass the following options:
472+
473+```
474+ --provider=bitbucket
475+ --client-id=<Client ID>
476+ --client-secret=<Client Secret>
477+```
478+
479+The default configuration allows everyone with Bitbucket account to authenticate. To restrict the access to the team members use additional configuration option: `--bitbucket-team=<Team name>`. To restrict the access to only these users who has access to one selected repository use `--bitbucket-repository=<Repository name>`.
480+
481+
482+### Gitea Auth Provider
483+
484+1. Create a new application: `https://< your gitea host >/user/settings/applications`
485+2. Under `Redirect URI` enter the correct URL i.e. `https://<proxied host>/oauth2/callback`
486+3. Note the Client ID and Client Secret.
487+4. Pass the following options to the proxy:
488+
489+```
490+ --provider="github"
491+ --redirect-url="https://<proxied host>/oauth2/callback"
492+ --provider-display-name="Gitea"
493+ --client-id="< client_id as generated by Gitea >"
494+ --client-secret="< client_secret as generated by Gitea >"
495+ --login-url="https://< your gitea host >/login/oauth/authorize"
496+ --redeem-url="https://< your gitea host >/login/oauth/access_token"
497+ --validate-url="https://< your gitea host >/api/v1"
498+```
499+
500+
501+## Email Authentication
502+
503+To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`.
504+
505+## Adding a new Provider
506+
507+Follow the examples in the [`providers` package](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/) to define a new
508+`Provider` instance. Add a new `case` to
509+[`providers.New()`](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/providers.go) to allow `oauth2-proxy` to use the
510+new `Provider`.
docs/versioned_docs/version-7.0.x/configuration/overview.mdnew file+528
@@ -0,0 +1,528 @@
1+---
2+id: overview
3+title: Overview
4+---
5+
6+`oauth2-proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables).
7+
8+### Generating a Cookie Secret
9+
10+To generate a strong cookie secret use `python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(16)).decode())'`
11+
12+### Config File
13+
14+Every command line argument can be specified in a config file by replacing hyphens (-) with underscores (\_). If the argument can be specified multiple times, the config option should be plural (trailing s).
15+
16+An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/contrib/oauth2-proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `--config=/etc/oauth2-proxy.cfg`
17+
18+### Command Line Options
19+
20+| Option | Type | Description | Default |
21+| ------ | ---- | ----------- | ------- |
22+| `--acr-values` | string | optional, see [docs](https://openid.net/specs/openid-connect-eap-acr-values-1_0.html#acrValues) | `""` |
23+| `--approval-prompt` | string | OAuth approval_prompt | `"force"` |
24+| `--auth-logging` | bool | Log authentication attempts | true |
25+| `--auth-logging-format` | string | Template for authentication log lines | see [Logging Configuration](#logging-configuration) |
26+| `--authenticated-emails-file` | string | authenticate against emails via file (one per line) | |
27+| `--azure-tenant` | string | go to a tenant-specific or common (tenant-independent) endpoint. | `"common"` |
28+| `--basic-auth-password` | string | the password to set when passing the HTTP Basic Auth header | |
29+| `--client-id` | string | the OAuth Client ID, e.g. `"123456.apps.googleusercontent.com"` | |
30+| `--client-secret` | string | the OAuth Client Secret | |
31+| `--client-secret-file` | string | the file with OAuth Client Secret | |
32+| `--config` | string | path to config file | |
33+| `--cookie-domain` | string \| list | Optional cookie domains to force cookies to (e.g. `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match). | |
34+| `--cookie-expire` | duration | expire timeframe for cookie | 168h0m0s |
35+| `--cookie-httponly` | bool | set HttpOnly cookie flag | true |
36+| `--cookie-name` | string | the name of the cookie that the oauth_proxy creates | `"_oauth2_proxy"` |
37+| `--cookie-path` | string | an optional cookie path to force cookies to (e.g. `/poc/`) | `"/"` |
38+| `--cookie-refresh` | duration | refresh the cookie after this duration; `0` to disable; not supported by all providers&nbsp;\[[1](#footnote1)\] | |
39+| `--cookie-secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
40+| `--cookie-secure` | bool | set [secure (HTTPS only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true |
41+| `--cookie-samesite` | string | set SameSite cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). | `""` |
42+| `--custom-templates-dir` | string | path to custom html templates | |
43+| `--display-htpasswd-form` | bool | display username / password login form if an htpasswd file is provided | true |
44+| `--email-domain` | string \| list | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | |
45+| `--errors-to-info-log` | bool | redirects error-level logging to default log channel instead of stderr | |
46+| `--extra-jwt-issuers` | string | if `--skip-jwt-bearer-tokens` is set, a list of extra JWT `issuer=audience` (see a token's `iss`, `aud` fields) pairs (where the issuer URL has a `.well-known/openid-configuration` or a `.well-known/jwks.json`) | |
47+| `--exclude-logging-paths` | string | comma separated list of paths to exclude from logging, e.g. `"/ping,/path2"` |`""` (no paths excluded) |
48+| `--flush-interval` | duration | period between flushing response buffers when streaming responses | `"1s"` |
49+| `--force-https` | bool | enforce https redirect | `false` |
50+| `--banner` | string | custom (html) banner string. Use `"-"` to disable default banner. | |
51+| `--footer` | string | custom (html) footer string. Use `"-"` to disable default footer. | |
52+| `--gcp-healthchecks` | bool | will enable `/liveness_check`, `/readiness_check`, and `/` (with the proper user-agent) endpoints that will make it work well with GCP App Engine and GKE Ingresses | false |
53+| `--github-org` | string | restrict logins to members of this organisation | |
54+| `--github-team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
55+| `--github-repo` | string | restrict logins to collaborators of this repository formatted as `orgname/repo` | |
56+| `--github-token` | string | the token to use when verifying repository collaborators (must have push access to the repository) | |
57+| `--github-user` | string \| list | To allow users to login by username even if they do not belong to the specified org and team or collaborators | |
58+| `--gitlab-group` | string \| list | restrict logins to members of any of these groups (slug), separated by a comma | |
59+| `--gitlab-projects` | string \| list | restrict logins to members of any of these projects (may be given multiple times) formatted as `orgname/repo=accesslevel`. Access level should be a value matching [Gitlab access levels](https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent | |
60+| `--google-admin-email` | string | the google admin to impersonate for api calls | |
61+| `--google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
62+| `--google-service-account-json` | string | the path to the service account json credentials | |
63+| `--htpasswd-file` | string | additionally authenticate against a htpasswd file. Entries must be created with `htpasswd -B` for bcrypt encryption | |
64+| `--http-address` | string | `[http://]<addr>:<port>` or `unix://<path>` to listen on for HTTP clients | `"127.0.0.1:4180"` |
65+| `--https-address` | string | `<addr>:<port>` to listen on for HTTPS clients | `":443"` |
66+| `--logging-compress` | bool | Should rotated log files be compressed using gzip | false |
67+| `--logging-filename` | string | File to log requests to, empty for `stdout` | `""` (stdout) |
68+| `--logging-local-time` | bool | Use local time in log files and backup filenames instead of UTC | true (local time) |
69+| `--logging-max-age` | int | Maximum number of days to retain old log files | 7 |
70+| `--logging-max-backups` | int | Maximum number of old log files to retain; 0 to disable | 0 |
71+| `--logging-max-size` | int | Maximum size in megabytes of the log file before rotation | 100 |
72+| `--jwt-key` | string | private key in PEM format used to sign JWT, so that you can say something like `--jwt-key="${OAUTH2_PROXY_JWT_KEY}"`: required by login.gov | |
73+| `--jwt-key-file` | string | path to the private key file in PEM format used to sign the JWT so that you can say something like `--jwt-key-file=/etc/ssl/private/jwt_signing_key.pem`: required by login.gov | |
74+| `--login-url` | string | Authentication endpoint | |
75+| `--insecure-oidc-allow-unverified-email` | bool | don't fail if an email address in an id_token is not verified | false |
76+| `--insecure-oidc-skip-issuer-verification` | bool | allow the OIDC issuer URL to differ from the expected (currently required for Azure multi-tenant compatibility) | false |
77+| `--oidc-issuer-url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | |
78+| `--oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
79+| `--oidc-email-claim` | string | which OIDC claim contains the user's email | `"email"` |
80+| `--oidc-groups-claim` | string | which OIDC claim contains the user groups | `"groups"` |
81+| `--pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header. When used with `--set-xauthrequest` this adds the X-Auth-Request-Access-Token header to the response | false |
82+| `--pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
83+| `--pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
84+| `--prefer-email-to-user` | bool | Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, e.g. htaccess authentication. Used in conjunction with `--pass-basic-auth` and `--pass-user-headers` | false |
85+| `--pass-host-header` | bool | pass the request Host Header to upstream | true |
86+| `--pass-user-headers` | bool | pass X-Forwarded-User, X-Forwarded-Groups, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
87+| `--profile-url` | string | Profile access endpoint | |
88+| `--prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` |
89+| `--provider` | string | OAuth provider | google |
90+| `--provider-ca-file` | string \| list | Paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead. |
91+| `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
92+| `--ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
93+| `--ping-user-agent` | string | a User-Agent that can be used for basic health checks | `""` (don't check user agent) |
94+| `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
95+| `--proxy-websockets` | bool | enables WebSocket proxying | true |
96+| `--pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | |
97+| `--real-client-ip-header` | string | Header used to determine the real IP of the client, requires `--reverse-proxy` to be set (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP) | X-Real-IP |
98+| `--redeem-url` | string | Token redemption endpoint | |
99+| `--redirect-url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
100+| `--redis-cluster-connection-urls` | string \| list | List of Redis cluster connection URLs (e.g. `redis://HOST[:PORT]`). Used in conjunction with `--redis-use-cluster` | |
101+| `--redis-connection-url` | string | URL of redis server for redis session storage (e.g. `redis://HOST[:PORT]`) | |
102+| `--redis-password` | string | Redis password. Applicable for all Redis configurations. Will override any password set in `--redis-connection-url` | |
103+| `--redis-sentinel-password` | string | Redis sentinel password. Used only for sentinel connection; any redis node passwords need to use `--redis-password` | |
104+| `--redis-sentinel-master-name` | string | Redis sentinel master name. Used in conjunction with `--redis-use-sentinel` | |
105+| `--redis-sentinel-connection-urls` | string \| list | List of Redis sentinel connection URLs (e.g. `redis://HOST[:PORT]`). Used in conjunction with `--redis-use-sentinel` | |
106+| `--redis-use-cluster` | bool | Connect to redis cluster. Must set `--redis-cluster-connection-urls` to use this feature | false |
107+| `--redis-use-sentinel` | bool | Connect to redis via sentinels. Must set `--redis-sentinel-master-name` and `--redis-sentinel-connection-urls` to use this feature | false |
108+| `--request-logging` | bool | Log requests | true |
109+| `--request-logging-format` | string | Template for request log lines | see [Logging Configuration](#logging-configuration) |
110+| `--resource` | string | The resource that is protected (Azure AD only) | |
111+| `--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 |
112+| `--scope` | string | OAuth scope specification | |
113+| `--session-cookie-minimal` | bool | strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only) | false |
114+| `--session-store-type` | string | [Session data storage backend](sessions.md); redis or cookie | cookie |
115+| `--set-xauthrequest` | bool | set X-Auth-Request-User, X-Auth-Request-Groups, X-Auth-Request-Email and X-Auth-Request-Preferred-Username response headers (useful in Nginx auth_request mode). When used with `--pass-access-token`, X-Auth-Request-Access-Token is added to response headers. | false |
116+| `--set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
117+| `--set-basic-auth` | bool | set HTTP Basic Auth information in response (useful in Nginx auth_request mode) | false |
118+| `--signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
119+| `--silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
120+| `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
121+| `--skip-auth-regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
122+| `--skip-auth-route` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR path_regex alone for all methods | |
123+| `--skip-auth-strip-headers` | bool | strips `X-Forwarded-*` style authentication headers & `Authorization` header if they would be set by oauth2-proxy | true |
124+| `--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 |
125+| `--skip-oidc-discovery` | bool | bypass OIDC endpoint discovery. `--login-url`, `--redeem-url` and `--oidc-jwks-url` must be configured in this case | false |
126+| `--skip-provider-button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
127+| `--ssl-insecure-skip-verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
128+| `--ssl-upstream-insecure-skip-verify` | bool | skip validation of certificates presented when using HTTPS upstreams | false |
129+| `--standard-logging` | bool | Log standard runtime information | true |
130+| `--standard-logging-format` | string | Template for standard log lines | see [Logging Configuration](#logging-configuration) |
131+| `--tls-cert-file` | string | path to certificate file | |
132+| `--tls-key-file` | string | path to private key file | |
133+| `--upstream` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | |
134+| `--allowed-group` | string \| list | restrict logins to members of this group (may be given multiple times) | |
135+| `--validate-url` | string | Access token validation endpoint | |
136+| `--version` | n/a | print version string | |
137+| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (e.g. `.example.com`)&nbsp;\[[2](#footnote2)\] | |
138+| `--trusted-ip` | string \| list | list of IPs or CIDR ranges to allow to bypass authentication (may be given multiple times). When combined with `--reverse-proxy` and optionally `--real-client-ip-header` this will evaluate the trust of the IP stored in an HTTP header by a reverse proxy rather than the layer-3/4 remote address. WARNING: trusting IPs has inherent security flaws, especially when obtaining the IP address from an HTTP header (reverse-proxy mode). Use this option only if you understand the risks and how to manage them. | |
139+
140+\[<a name="footnote1">1</a>\]: Only these providers support `--cookie-refresh`: GitLab, Google and OIDC
141+
142+\[<a name="footnote2">2</a>\]: When using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URL's protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`.
143+
144+See below for provider specific options
145+
146+### Upstreams Configuration
147+
148+`oauth2-proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter. This will forward all authenticated requests to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream.
149+
150+Static file paths are configured as a file:// URL. `file:///var/www/static/` will serve the files from that directory at `http://[oauth2-proxy url]/var/www/static/`, which may not be what you want. You can provide the path to where the files should be available by adding a fragment to the configured URL. The value of the fragment will then be used to specify which path the files are available at, e.g. `file:///var/www/static/#/static/` will make `/var/www/static/` available at `http://[oauth2-proxy url]/static/`.
151+
152+Multiple upstreams can either be configured by supplying a comma separated list to the `--upstream` parameter, supplying the parameter multiple times or providing a list in the [config file](#config-file). When multiple upstreams are used routing to them will be based on the path they are set up with.
153+
154+### Environment variables
155+
156+Every command line argument can be specified as an environment variable by
157+prefixing it with `OAUTH2_PROXY_`, capitalising it, and replacing hyphens (`-`)
158+with underscores (`_`). If the argument can be specified multiple times, the
159+environment variable should be plural (trailing `S`).
160+
161+This is particularly useful for storing secrets outside of a configuration file
162+or the command line.
163+
164+For example, the `--cookie-secret` flag becomes `OAUTH2_PROXY_COOKIE_SECRET`,
165+and the `--email-domain` flag becomes `OAUTH2_PROXY_EMAIL_DOMAINS`.
166+
167+## Logging Configuration
168+
169+By default, OAuth2 Proxy logs all output to stdout. Logging can be configured to output to a rotating log file using the `--logging-filename` command.
170+
171+If logging to a file you can also configure the maximum file size (`--logging-max-size`), age (`--logging-max-age`), max backup logs (`--logging-max-backups`), and if backup logs should be compressed (`--logging-compress`).
172+
173+There are three different types of logging: standard, authentication, and HTTP requests. These can each be enabled or disabled with `--standard-logging`, `--auth-logging`, and `--request-logging`.
174+
175+Each type of logging has its own configurable format and variables. By default these formats are similar to the Apache Combined Log.
176+
177+Logging of requests to the `/ping` endpoint (or using `--ping-user-agent`) can be disabled with `--silence-ping-logging` reducing log volume. This flag appends the `--ping-path` to `--exclude-logging-paths`.
178+
179+### Auth Log Format
180+Authentication logs are logs which are guaranteed to contain a username or email address of a user attempting to authenticate. These logs are output by default in the below format:
181+
182+```
183+<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] [<STATUS>] <MESSAGE>
184+```
185+
186+The status block will contain one of the below strings:
187+
188+- `AuthSuccess` If a user has authenticated successfully by any method
189+- `AuthFailure` If the user failed to authenticate explicitly
190+- `AuthError` If there was an unexpected error during authentication
191+
192+If you require a different format than that, you can configure it with the `--auth-logging-format` flag.
193+The default format is configured as follows:
194+
195+```
196+{{.Client}} - {{.Username}} [{{.Timestamp}}] [{{.Status}}] {{.Message}}
197+```
198+
199+Available variables for auth logging:
200+
201+| Variable | Example | Description |
202+| --- | --- | --- |
203+| Client | 74.125.224.72 | The client/remote IP address. Will use the X-Real-IP header it if exists & reverse-proxy is set to true. |
204+| Host | domain.com | The value of the Host header. |
205+| Protocol | HTTP/1.0 | The request protocol. |
206+| RequestMethod | GET | The request method. |
207+| Timestamp | 19/Mar/2015:17:20:19 -0400 | The date and time of the logging event. |
208+| UserAgent | - | The full user agent as reported by the requesting client. |
209+| Username | username@email.com | The email or username of the auth request. |
210+| Status | AuthSuccess | The status of the auth request. See above for details. |
211+| Message | Authenticated via OAuth2 | The details of the auth attempt. |
212+
213+### Request Log Format
214+HTTP request logs will output by default in the below format:
215+
216+```
217+<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>
218+```
219+
220+If you require a different format than that, you can configure it with the `--request-logging-format` flag.
221+The default format is configured as follows:
222+
223+```
224+{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}
225+```
226+
227+Available variables for request logging:
228+
229+| Variable | Example | Description |
230+| --- | --- | --- |
231+| Client | 74.125.224.72 | The client/remote IP address. Will use the X-Real-IP header it if exists & reverse-proxy is set to true. |
232+| Host | domain.com | The value of the Host header. |
233+| Protocol | HTTP/1.0 | The request protocol. |
234+| RequestDuration | 0.001 | The time in seconds that a request took to process. |
235+| RequestMethod | GET | The request method. |
236+| RequestURI | "/oauth2/auth" | The URI path of the request. |
237+| ResponseSize | 12 | The size in bytes of the response. |
238+| StatusCode | 200 | The HTTP status code of the response. |
239+| Timestamp | 19/Mar/2015:17:20:19 -0400 | The date and time of the logging event. |
240+| Upstream | - | The upstream data of the HTTP request. |
241+| UserAgent | - | The full user agent as reported by the requesting client. |
242+| Username | username@email.com | The email or username of the auth request. |
243+
244+### Standard Log Format
245+All other logging that is not covered by the above two types of logging will be output in this standard logging format. This includes configuration information at startup and errors that occur outside of a session. The default format is below:
246+
247+```
248+[19/Mar/2015:17:20:19 -0400] [main.go:40] <MESSAGE>
249+```
250+
251+If you require a different format than that, you can configure it with the `--standard-logging-format` flag. The default format is configured as follows:
252+
253+```
254+[{{.Timestamp}}] [{{.File}}] {{.Message}}
255+```
256+
257+Available variables for standard logging:
258+
259+| Variable | Example | Description |
260+| --- | --- | --- |
261+| Timestamp | 19/Mar/2015:17:20:19 -0400 | The date and time of the logging event. |
262+| File | main.go:40 | The file and line number of the logging statement. |
263+| Message | HTTP: listening on 127.0.0.1:4180 | The details of the log statement. |
264+
265+## Configuring for use with the Nginx `auth_request` directive
266+
267+The [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) allows Nginx to authenticate requests via the oauth2-proxy's `/auth` endpoint, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the request through. For example:
268+
269+```nginx
270+server {
271+ listen 443 ssl;
272+ server_name ...;
273+ include ssl/ssl.conf;
274+
275+ location /oauth2/ {
276+ proxy_pass http://127.0.0.1:4180;
277+ proxy_set_header Host $host;
278+ proxy_set_header X-Real-IP $remote_addr;
279+ proxy_set_header X-Scheme $scheme;
280+ proxy_set_header X-Auth-Request-Redirect $request_uri;
281+ # or, if you are handling multiple domains:
282+ # proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
283+ }
284+ location = /oauth2/auth {
285+ proxy_pass http://127.0.0.1:4180;
286+ proxy_set_header Host $host;
287+ proxy_set_header X-Real-IP $remote_addr;
288+ proxy_set_header X-Scheme $scheme;
289+ # nginx auth_request includes headers but not body
290+ proxy_set_header Content-Length "";
291+ proxy_pass_request_body off;
292+ }
293+
294+ location / {
295+ auth_request /oauth2/auth;
296+ error_page 401 = /oauth2/sign_in;
297+
298+ # pass information via X-User and X-Email headers to backend,
299+ # requires running with --set-xauthrequest flag
300+ auth_request_set $user $upstream_http_x_auth_request_user;
301+ auth_request_set $email $upstream_http_x_auth_request_email;
302+ proxy_set_header X-User $user;
303+ proxy_set_header X-Email $email;
304+
305+ # if you enabled --pass-access-token, this will pass the token to the backend
306+ auth_request_set $token $upstream_http_x_auth_request_access_token;
307+ proxy_set_header X-Access-Token $token;
308+
309+ # if you enabled --cookie-refresh, this is needed for it to work with auth_request
310+ auth_request_set $auth_cookie $upstream_http_set_cookie;
311+ add_header Set-Cookie $auth_cookie;
312+
313+ # When using the --set-authorization-header flag, some provider's cookies can exceed the 4kb
314+ # limit and so the OAuth2 Proxy splits these into multiple parts.
315+ # Nginx normally only copies the first `Set-Cookie` header from the auth_request to the response,
316+ # so if your cookies are larger than 4kb, you will need to extract additional cookies manually.
317+ auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;
318+
319+ # Extract the Cookie attributes from the first Set-Cookie header and append them
320+ # to the second part ($upstream_cookie_* variables only contain the raw cookie content)
321+ if ($auth_cookie ~* "(; .*)") {
322+ set $auth_cookie_name_0 $auth_cookie;
323+ set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
324+ }
325+
326+ # Send both Set-Cookie headers now if there was a second part
327+ if ($auth_cookie_name_upstream_1) {
328+ add_header Set-Cookie $auth_cookie_name_0;
329+ add_header Set-Cookie $auth_cookie_name_1;
330+ }
331+
332+ proxy_pass http://backend/;
333+ # or "root /path/to/site;" or "fastcgi_pass ..." etc
334+ }
335+}
336+```
337+
338+When you use ingress-nginx in Kubernetes, you MUST use `kubernetes/ingress-nginx` (which includes the Lua module) and the following configuration snippet for your `Ingress`.
339+Variables set with `auth_request_set` are not `set`-able in plain nginx config when the location is processed via `proxy_pass` and then may only be processed by Lua.
340+Note that `nginxinc/kubernetes-ingress` does not include the Lua module.
341+
342+```yaml
343+nginx.ingress.kubernetes.io/auth-response-headers: Authorization
344+nginx.ingress.kubernetes.io/auth-signin: https://$host/oauth2/start?rd=$escaped_request_uri
345+nginx.ingress.kubernetes.io/auth-url: https://$host/oauth2/auth
346+nginx.ingress.kubernetes.io/configuration-snippet: |
347+ auth_request_set $name_upstream_1 $upstream_cookie_name_1;
348+
349+ access_by_lua_block {
350+ if ngx.var.name_upstream_1 ~= "" then
351+ ngx.header["Set-Cookie"] = "name_1=" .. ngx.var.name_upstream_1 .. ngx.var.auth_cookie:match("(; .*)")
352+ end
353+ }
354+```
355+It is recommended to use `--session-store-type=redis` when expecting large sessions/OIDC tokens (_e.g._ with MS Azure).
356+
357+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=".
358+
359+## Configuring for use with the Traefik (v2) `ForwardAuth` middleware
360+
361+**This option requires `--reverse-proxy` option to be set.**
362+
363+### ForwardAuth with 401 errors middleware
364+
365+The [Traefik v2 `ForwardAuth` middleware](https://doc.traefik.io/traefik/middlewares/forwardauth/) allows Traefik to authenticate requests via the oauth2-proxy's `/oauth2/auth` endpoint on every request, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the whole request through. For example, on Dynamic File (YAML) Configuration:
366+
367+```yaml
368+http:
369+ routers:
370+ a-service:
371+ rule: "Host(`a-service.example.com`)"
372+ service: a-service-backend
373+ middlewares:
374+ - oauth-errors
375+ - oauth-auth
376+ tls:
377+ certResolver: default
378+ domains:
379+ - main: "example.com"
380+ sans:
381+ - "*.example.com"
382+ oauth:
383+ rule: "Host(`a-service.example.com`, `oauth.example.com`) && PathPrefix(`/oauth2/`)"
384+ middlewares:
385+ - auth-headers
386+ service: oauth-backend
387+ tls:
388+ certResolver: default
389+ domains:
390+ - main: "example.com"
391+ sans:
392+ - "*.example.com"
393+
394+ services:
395+ a-service-backend:
396+ loadBalancer:
397+ servers:
398+ - url: http://172.16.0.2:7555
399+ oauth-backend:
400+ loadBalancer:
401+ servers:
402+ - url: http://172.16.0.1:4180
403+
404+ middlewares:
405+ auth-headers:
406+ headers:
407+ sslRedirect: true
408+ stsSeconds: 315360000
409+ browserXssFilter: true
410+ contentTypeNosniff: true
411+ forceSTSHeader: true
412+ sslHost: example.com
413+ stsIncludeSubdomains: true
414+ stsPreload: true
415+ frameDeny: true
416+ oauth-auth:
417+ forwardAuth:
418+ address: https://oauth.example.com/oauth2/auth
419+ trustForwardHeader: true
420+ oauth-errors:
421+ errors:
422+ status:
423+ - "401-403"
424+ service: oauth-backend
425+ query: "/oauth2/sign_in"
426+```
427+
428+### ForwardAuth with static upstreams configuration
429+
430+Redirect to sign_in functionality provided without the use of `errors` middleware with [Traefik v2 `ForwardAuth` middleware](https://doc.traefik.io/traefik/middlewares/forwardauth/) pointing to oauth2-proxy service's `/` endpoint
431+
432+**Following options need to be set on `oauth2-proxy`:**
433+- `--upstream=static://202`: Configures a static response for authenticated sessions
434+- `--reverseproxy=true`: Enables the use of `X-Forwarded-*` headers to determine redirects correctly
435+
436+```yaml
437+http:
438+ routers:
439+ a-service-route-1:
440+ rule: "Host(`a-service.example.com`, `b-service.example.com`) && PathPrefix(`/`)"
441+ service: a-service-backend
442+ middlewares:
443+ - oauth-auth-redirect # redirects all unauthenticated to oauth2 signin
444+ tls:
445+ certResolver: default
446+ domains:
447+ - main: "example.com"
448+ sans:
449+ - "*.example.com"
450+ a-service-route-2:
451+ rule: "Host(`a-service.example.com`) && PathPrefix(`/no-auto-redirect`)"
452+ service: a-service-backend
453+ middlewares:
454+ - oauth-auth-wo-redirect # unauthenticated session will return a 401
455+ tls:
456+ certResolver: default
457+ domains:
458+ - main: "example.com"
459+ sans:
460+ - "*.example.com"
461+ services-oauth2-route:
462+ rule: "Host(`a-service.example.com`, `b-service.example.com`) && PathPrefix(`/oauth2/`)"
463+ middlewares:
464+ - auth-headers
465+ service: oauth-backend
466+ tls:
467+ certResolver: default
468+ domains:
469+ - main: "example.com"
470+ sans:
471+ - "*.example.com"
472+ oauth2-proxy-route:
473+ rule: "Host(`oauth.example.com`) && PathPrefix(`/`)"
474+ middlewares:
475+ - auth-headers
476+ service: oauth-backend
477+ tls:
478+ certResolver: default
479+ domains:
480+ - main: "example.com"
481+ sans:
482+ - "*.example.com"
483+
484+ services:
485+ a-service-backend:
486+ loadBalancer:
487+ servers:
488+ - url: http://172.16.0.2:7555
489+ b-service-backend:
490+ loadBalancer:
491+ servers:
492+ - url: http://172.16.0.3:7555
493+ oauth-backend:
494+ loadBalancer:
495+ servers:
496+ - url: http://172.16.0.1:4180
497+
498+ middlewares:
499+ auth-headers:
500+ headers:
501+ sslRedirect: true
502+ stsSeconds: 315360000
503+ browserXssFilter: true
504+ contentTypeNosniff: true
505+ forceSTSHeader: true
506+ sslHost: example.com
507+ stsIncludeSubdomains: true
508+ stsPreload: true
509+ frameDeny: true
510+ oauth-auth-redirect:
511+ forwardAuth:
512+ address: https://oauth.example.com/
513+ trustForwardHeader: true
514+ authResponseHeaders:
515+ - X-Auth-Request-Access-Token
516+ - Authorization
517+ oauth-auth-wo-redirect:
518+ forwardAuth:
519+ address: https://oauth.example.com/oauth2/auth
520+ trustForwardHeader: true
521+ authResponseHeaders:
522+ - X-Auth-Request-Access-Token
523+ - Authorization
524+```
525+
526+:::note
527+If 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.
528+:::
docs/versioned_docs/version-7.0.x/configuration/sessions.mdnew file+67
@@ -0,0 +1,67 @@
1+---
2+id: session_storage
3+title: Session Storage
4+---
5+
6+Sessions allow a user's authentication to be tracked between multiple HTTP
7+requests to a service.
8+
9+The OAuth2 Proxy uses a Cookie to track user sessions and will store the session
10+data in one of the available session storage backends.
11+
12+At present the available backends are (as passed to `--session-store-type`):
13+- [cookie](#cookie-storage) (default)
14+- [redis](#redis-storage)
15+
16+### Cookie Storage
17+
18+The Cookie storage backend is the default backend implementation and has
19+been used in the OAuth2 Proxy historically.
20+
21+With the Cookie storage backend, all session information is stored in client
22+side cookies and transferred with each and every request.
23+
24+The following should be known when using this implementation:
25+- Since all state is stored client side, this storage backend means that the OAuth2 Proxy is completely stateless
26+- Cookies are signed server side to prevent modification client-side
27+- It is mandatory to set a `cookie-secret` which will ensure data is encrypted within the cookie data.
28+- Since multiple requests can be made concurrently to the OAuth2 Proxy, this session implementation
29+cannot lock sessions and while updating and refreshing sessions, there can be conflicts which force
30+users to re-authenticate
31+
32+
33+### Redis Storage
34+
35+The Redis Storage backend stores sessions, encrypted, in redis. Instead sending all the information
36+back the the client for storage, as in the [Cookie storage](#cookie-storage), a ticket is sent back
37+to the user as the cookie value instead.
38+
39+A ticket is composed as the following:
40+
41+`{CookieName}-{ticketID}.{secret}`
42+
43+Where:
44+
45+- The `CookieName` is the OAuth2 cookie name (_oauth2_proxy by default)
46+- The `ticketID` is a 128 bit random number, hex-encoded
47+- The `secret` is a 128 bit random number, base64url encoded (no padding). The secret is unique for every session.
48+- The pair of `{CookieName}-{ticketID}` comprises a ticket handle, and thus, the redis key
49+to which the session is stored. The encoded session is encrypted with the secret and stored
50+in redis via the `SETEX` command.
51+
52+Encrypting every session uniquely protects the refresh/access/id tokens stored in the session from
53+disclosure.
54+
55+#### Usage
56+
57+When using the redis store, specify `--session-store-type=redis` as well as the Redis connection URL, via
58+`--redis-connection-url=redis://host[:port][/db-number]`.
59+
60+You may also configure the store for Redis Sentinel. In this case, you will want to use the
61+`--redis-use-sentinel=true` flag, as well as configure the flags `--redis-sentinel-master-name`
62+and `--redis-sentinel-connection-urls` appropriately.
63+
64+Redis Cluster is available to be the backend store as well. To leverage it, you will need to set the
65+`--redis-use-cluster=true` flag, and configure the flags `--redis-cluster-connection-urls` appropriately.
66+
67+Note that flags `--redis-use-sentinel=true` and `--redis-use-cluster=true` are mutually exclusive.
docs/versioned_docs/version-7.0.x/configuration/tls.mdnew file+70
@@ -0,0 +1,70 @@
1+---
2+id: tls
3+title: TLS Configuration
4+---
5+
6+There are two recommended configurations.
7+
8+1. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert-file=/path/to/cert.pem` and `--tls-key-file=/path/to/cert.key`.
9+
10+ The command line to run `oauth2-proxy` in this configuration would look like this:
11+
12+ ```bash
13+ ./oauth2-proxy \
14+ --email-domain="yourcompany.com" \
15+ --upstream=http://127.0.0.1:8080/ \
16+ --tls-cert-file=/path/to/cert.pem \
17+ --tls-key-file=/path/to/cert.key \
18+ --cookie-secret=... \
19+ --cookie-secure=true \
20+ --provider=... \
21+ --client-id=... \
22+ --client-secret=...
23+ ```
24+
25+2. Configure SSL Termination with [Nginx](http://nginx.org/) (example config below), Amazon ELB, Google Cloud Platform Load Balancing, or ....
26+
27+ Because `oauth2-proxy` listens on `127.0.0.1:4180` by default, to listen on all interfaces (needed when using an
28+ external load balancer like Amazon ELB or Google Platform Load Balancing) use `--http-address="0.0.0.0:4180"` or
29+ `--http-address="http://:4180"`.
30+
31+ Nginx will listen on port `443` and handle SSL connections while proxying to `oauth2-proxy` on port `4180`.
32+ `oauth2-proxy` will then authenticate requests for an upstream application. The external endpoint for this example
33+ would be `https://internal.yourcompany.com/`.
34+
35+ An example Nginx config follows. Note the use of `Strict-Transport-Security` header to pin requests to SSL
36+ via [HSTS](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security):
37+
38+ ```
39+ server {
40+ listen 443 default ssl;
41+ server_name internal.yourcompany.com;
42+ ssl_certificate /path/to/cert.pem;
43+ ssl_certificate_key /path/to/cert.key;
44+ add_header Strict-Transport-Security max-age=2592000;
45+
46+ location / {
47+ proxy_pass http://127.0.0.1:4180;
48+ proxy_set_header Host $host;
49+ proxy_set_header X-Real-IP $remote_addr;
50+ proxy_set_header X-Scheme $scheme;
51+ proxy_connect_timeout 1;
52+ proxy_send_timeout 30;
53+ proxy_read_timeout 30;
54+ }
55+ }
56+ ```
57+
58+ The command line to run `oauth2-proxy` in this configuration would look like this:
59+
60+ ```bash
61+ ./oauth2-proxy \
62+ --email-domain="yourcompany.com" \
63+ --upstream=http://127.0.0.1:8080/ \
64+ --cookie-secret=... \
65+ --cookie-secure=true \
66+ --provider=... \
67+ --reverse-proxy=true \
68+ --client-id=... \
69+ --client-secret=...
70+ ```
docs/versioned_docs/version-7.0.x/features/endpoints.mdnew file+35
@@ -0,0 +1,35 @@
1+---
2+id: endpoints
3+title: Endpoints
4+---
5+
6+OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable.
7+
8+- /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info
9+- /ping - returns a 200 OK response, which is intended for use with health checks
10+- /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
11+- /oauth2/sign_out - this URL is used to clear the session cookie
12+- /oauth2/start - a URL that will redirect to start the OAuth cycle
13+- /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url.
14+- /oauth2/userinfo - the URL is used to return user's email from the session in JSON format.
15+- /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](../configuration/overview.md#configuring-for-use-with-the-nginx-auth_request-directive)
16+
17+### Sign out
18+
19+To sign the user out, redirect them to `/oauth2/sign_out`. This endpoint only removes oauth2-proxy's own cookies, i.e. the user is still logged in with the authentication provider and may automatically re-login when accessing the application again. You will also need to redirect the user to the authentication provider's sign out page afterwards using the `rd` query parameter, i.e. redirect the user to something like (notice the url-encoding!):
20+
21+```
22+/oauth2/sign_out?rd=https%3A%2F%2Fmy-oidc-provider.example.com%2Fsign_out_page
23+```
24+
25+Alternatively, include the redirect URL in the `X-Auth-Request-Redirect` header:
26+
27+```
28+GET /oauth2/sign_out HTTP/1.1
29+X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page
30+...
31+```
32+
33+(The "sign_out_page" should be the [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0.html#rfc.section.2.1) from [the metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) if your OIDC provider supports Session Management and Discovery.)
34+
35+BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](../configuration/overview) configuration option otherwise the redirect will be ignored.
docs/versioned_docs/version-7.0.x/features/request_signatures.mdnew file+20
@@ -0,0 +1,20 @@
1+---
2+id: request_signatures
3+title: Request Signatures
4+---
5+
6+If `signature_key` is defined, proxied requests will be signed with the
7+`GAP-Signature` header, which is a [Hash-based Message Authentication Code
8+(HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code)
9+of selected request information and the request body [see `SIGNATURE_HEADERS`
10+in `oauthproxy.go`](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/oauthproxy.go).
11+
12+`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`)
13+
14+For more information about HMAC request signature validation, read the
15+following:
16+
17+- [Amazon Web Services: Signing and Authenticating REST
18+ Requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html)
19+- [rc3.org: Using HMAC to authenticate Web service
20+ requests](http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/)
docs/versioned_docs/version-7.0.x/installation.mdnew file+24
@@ -0,0 +1,24 @@
1+---
2+id: installation
3+title: Installation
4+slug: /
5+---
6+
7+1. Choose how to deploy:
8+
9+ a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.0.1`)
10+
11+ b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy/v7` which will put the binary in `$GOPATH/bin`
12+
13+ c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available)
14+
15+Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`.
16+
17+```
18+$ sha256sum -c sha256sum.txt 2>&1 | grep OK
19+oauth2-proxy-x.y.z.linux-amd64: OK
20+```
21+
22+2. [Select a Provider and Register an OAuth Application with a Provider](configuration/auth.md)
23+3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration/overview.md)
24+4. [Configure SSL or Deploy behind a SSL endpoint](configuration/tls.md) (example provided for Nginx)
docs/versioned_sidebars/version-6.1.x-sidebars.json+11
@@ -45,6 +45,17 @@
4545 "id": "version-6.1.x/features/request_signatures"
4646 }
4747 ]
48+ },
49+ {
50+ "collapsed": false,
51+ "type": "category",
52+ "label": "Community",
53+ "items": [
54+ {
55+ "type": "doc",
56+ "id": "version-6.1.x/community/security"
57+ }
58+ ]
4859 }
4960 ]
5061}
docs/versioned_sidebars/version-7.0.x-sidebars.jsonnew file+65
@@ -0,0 +1,65 @@
1+{
2+ "version-7.0.x/docs": [
3+ {
4+ "type": "doc",
5+ "id": "version-7.0.x/installation"
6+ },
7+ {
8+ "type": "doc",
9+ "id": "version-7.0.x/behaviour"
10+ },
11+ {
12+ "collapsed": false,
13+ "type": "category",
14+ "label": "Configuration",
15+ "items": [
16+ {
17+ "type": "doc",
18+ "id": "version-7.0.x/configuration/overview"
19+ },
20+ {
21+ "type": "doc",
22+ "id": "version-7.0.x/configuration/oauth_provider"
23+ },
24+ {
25+ "type": "doc",
26+ "id": "version-7.0.x/configuration/session_storage"
27+ },
28+ {
29+ "type": "doc",
30+ "id": "version-7.0.x/configuration/tls"
31+ },
32+ {
33+ "type": "doc",
34+ "id": "version-7.0.x/configuration/alpha-config"
35+ }
36+ ]
37+ },
38+ {
39+ "collapsed": false,
40+ "type": "category",
41+ "label": "Features",
42+ "items": [
43+ {
44+ "type": "doc",
45+ "id": "version-7.0.x/features/endpoints"
46+ },
47+ {
48+ "type": "doc",
49+ "id": "version-7.0.x/features/request_signatures"
50+ }
51+ ]
52+ },
53+ {
54+ "collapsed": false,
55+ "type": "category",
56+ "label": "Community",
57+ "items": [
58+ {
59+ "type": "doc",
60+ "id": "version-7.0.x/community/security"
61+ }
62+ ]
63+ }
64+ ]
65+}
docs/versions.json+1
@@ -1,3 +1,4 @@
11[
2+ "7.0.x",
23 "6.1.x"
34]
go.mod+3
@@ -11,14 +11,17 @@ require (
1111 github.com/dgrijalva/jwt-go v3.2.0+incompatible
1212 github.com/frankban/quicktest v1.10.0 // indirect
1313 github.com/fsnotify/fsnotify v1.4.9
14+ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
1415 github.com/go-redis/redis/v8 v8.2.3
1516 github.com/justinas/alice v1.2.0
1617 github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa
1718 github.com/mitchellh/mapstructure v1.1.2
19+ github.com/oauth2-proxy/tools/reference-gen v0.0.0-20210118095127-56ffd7384404
1820 github.com/onsi/ginkgo v1.14.1
1921 github.com/onsi/gomega v1.10.2
2022 github.com/pierrec/lz4 v2.5.2+incompatible
2123 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
24+ github.com/prometheus/client_golang v1.9.0
2225 github.com/spf13/pflag v1.0.5
2326 github.com/spf13/viper v1.6.3
2427 github.com/stretchr/testify v1.6.1
go.sum+259−7
@@ -8,25 +8,48 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
88github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
99github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc=
1010github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
11+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
1112github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
1213github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
1314github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
1415github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
16+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
17+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
18+github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
19+github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
1520github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
21+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
1622github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
23+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
24+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
1725github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
1826github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
1927github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
2028github.com/alicebob/miniredis/v2 v2.11.1/go.mod h1:UA48pmi7aSazcGAvcdKcBB49z521IC9VjTTRz2nIaJE=
2129github.com/alicebob/miniredis/v2 v2.13.0 h1:QPosMaxm+r6Qs+YcCtL2Z2a2RSdC9VfXJLpd80l8ICU=
2230github.com/alicebob/miniredis/v2 v2.13.0/go.mod h1:0UIBNuf97uxrWhdVBpJvPtafKyGpL2NS2pYe0tYM97k=
31+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
32+github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
33+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
2334github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
35+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
36+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
37+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
38+github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
39+github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
40+github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
2441github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
42+github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
2543github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
44+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
45+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
46+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
2647github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
2748github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
2849github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
2950github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
51+github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
52+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
3053github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
3154github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
3255github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -35,14 +58,21 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
3558github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
3659github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
3760github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
61+github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
3862github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
63+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
64+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
3965github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
4066github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
4167github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
4268github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
4369github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
70+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
4471github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
72+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
4573github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
74+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
75+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
4676github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4777github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4878github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -53,22 +83,38 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
5383github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
5484github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
5585github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
86+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
87+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
88+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
89+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
90+github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
5691github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
5792github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
93+github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
5894github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
5995github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
6096github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
97+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
98+github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
99+github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
61100github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE=
62101github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
63102github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
64103github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
65104github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
66105github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
106+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
67107github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
108+github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
109+github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
68110github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
111+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
112+github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
69113github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
70114github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
115+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
71116github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
117+github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
72118github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
73119github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
74120github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
@@ -76,13 +122,19 @@ github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nA
76122github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
77123github.com/go-redis/redis/v8 v8.2.3 h1:eNesND+DWt/sjQOtPFxAbQkTIXaXX00qNLxjVWkZ70k=
78124github.com/go-redis/redis/v8 v8.2.3/go.mod h1:ysgGY09J/QeDYbu3HikWEIPCwaeOkuNoTgKayTEaEOw=
125+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
79126github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
127+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
80128github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
129+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
81130github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
82131github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
83132github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
84133github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
134+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
85135github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
136+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
137+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA=
86138github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
87139github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
88140github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -99,6 +151,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
99151github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
100152github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
101153github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
154+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
155+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
156+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
102157github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
103158github.com/gomodule/redigo v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8=
104159github.com/gomodule/redigo v1.8.1/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
@@ -114,6 +169,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
114169github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
115170github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
116171github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
172+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
173+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
117174github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
118175github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
119176github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -122,30 +179,63 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
122179github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
123180github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
124181github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
182+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
183+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
184+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
185+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
125186github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
126187github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
188+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
127189github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
128190github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
191+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
192+github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
193+github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
194+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
195+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
196+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
197+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
198+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
199+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
200+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
201+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
202+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
203+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
204+github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
205+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
129206github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
130207github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
131208github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
132209github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
133210github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
211+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
212+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
213+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
214+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
134215github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
216+github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
217+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
218+github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
219+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
135220github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
221+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
136222github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
223+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
224+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
137225github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
138226github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
139227github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
140228github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
141229github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
142230github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
231+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
143232github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
144233github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
145234github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
146235github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
147236github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
148237github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
238+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
149239github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
150240github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
151241github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
@@ -153,14 +243,29 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
153243github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
154244github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
155245github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
246+github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
247+github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
248+github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
156249github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
157250github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
158251github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
159252github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
160253github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
254+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
255+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
256+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
257+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
258+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
161259github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
162260github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa h1:hI1uC2A3vJFjwvBn0G0a7QBRdBUp6Y048BtLAHRTKPo=
163261github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s=
262+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
263+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
264+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
265+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
266+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
267+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
268+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
164269github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
165270github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
166271github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -169,63 +274,137 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
169274github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
170275github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
171276github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
277+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
172278github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
279+github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
280+github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
281+github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
282+github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
283+github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
284+github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
285+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
173286github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
174287github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
288+github.com/oauth2-proxy/tools/reference-gen v0.0.0-20210118095127-56ffd7384404 h1:ZpzR4Ou1nhldBG/vEzauoqyaUlofaUcLkv1C/gBK8ls=
289+github.com/oauth2-proxy/tools/reference-gen v0.0.0-20210118095127-56ffd7384404/go.mod h1:YpORG8zs14vNlpXvuHYnnDvWazIRaDk02MaY8lafqdI=
290+github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
291+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
175292github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
293+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
176294github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
177295github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
296+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
178297github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
179298github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
180299github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4=
181300github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
182301github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
302+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
183303github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
184304github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
185305github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
186306github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
187307github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
308+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
309+github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
310+github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
311+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
312+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
313+github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
314+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
315+github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
316+github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
317+github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
318+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
319+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
188320github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
189321github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
322+github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
323+github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
324+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
190325github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
191326github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
192327github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
328+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
193329github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
330+github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
194331github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
195332github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
333+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
196334github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
197335github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
198336github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
337+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
338+github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
199339github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
340+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
341+github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
342+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
343+github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
344+github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
200345github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
346+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
201347github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
348+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
202349github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
350+github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
351+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
352+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
203353github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
354+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
355+github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
204356github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
357+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
358+github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
359+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
360+github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
361+github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
205362github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
363+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
364+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
206365github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
366+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
367+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
368+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
369+github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
370+github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
207371github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
372+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
208373github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
374+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
375+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
376+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
377+github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
378+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
379+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
209380github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
381+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
382+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
210383github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
211384github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
212385github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
213386github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
214387github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
388+github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
215389github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
216390github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
217391github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
218392github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
219393github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
394+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
220395github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
221396github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
222397github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
398+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
223399github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
224400github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
225401github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
226402github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
227403github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
228404github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
405+github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
406+github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
407+github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
229408github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
230409github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
231410github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -236,7 +415,10 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
236415github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
237416github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
238417github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
418+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
239419github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
420+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
421+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
240422github.com/vmihailenco/msgpack/v4 v4.3.11 h1:Q47CePddpNGNhk4GCnAx9DDtASi2rasatE0cd26cZoE=
241423github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
242424github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY=
@@ -245,21 +427,37 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
245427github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
246428github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 h1:1+FQ4Ns+UZtUiQ4lP0sTCyKSQ0EXoiwAdHZB0Pd5t9Q=
247429github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997/go.mod h1:DIGbh/f5XMAessMV/uaIik81gkDVjUeQ9ApdaU7wRKE=
430+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
248431github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
249432github.com/yuin/gopher-lua v0.0.0-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
250433github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
251434github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
252435go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
436+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
437+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
438+go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
439+go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
253440go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
254441go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
442+go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
443+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
255444go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E=
256445go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0=
446+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
257447go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
448+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
258449go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
450+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
451+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
259452go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
453+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
260454golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
455+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
261456golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
262457golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
458+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
459+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
460+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
263461golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
264462golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
265463golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -268,22 +466,34 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
268466golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
269467golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
270468golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
469+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
470+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
471+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
472+golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
473+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
271474golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
272475golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
273476golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
477+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
274478golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
479+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
275480golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
276481golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
482+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
277483golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
278484golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
279485golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
280486golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
281487golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
282488golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
489+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
490+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
491+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
492+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
283493golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
284494golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
285−golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
286495golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
496+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
287497golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
288498golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
289499golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -296,32 +506,46 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
296506golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
297507golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
298508golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
509+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
510+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
299511golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
300512golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
301513golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
514+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
302515golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
303516golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
517+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
304518golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
305519golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
306520golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
521+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
522+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
307523golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
524+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
525+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
308526golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
309527golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
310528golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
529+golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
530+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
311531golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
312−golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
313532golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
314−golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
533+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
315534golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
535+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
536+golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
537+golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
316538golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
317539golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
318−golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
319540golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
320541golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
321542golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
543+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
322544golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
323545golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
546+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
324547golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
548+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
325549golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
326550golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
327551golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -331,12 +555,23 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
331555golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
332556golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
333557golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
558+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
559+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
560+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
561+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
562+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
563+golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 h1:BMFHd4OFnFtWX46Xj4DN6vvT1btiBxyq+s0orYBqcQY=
564+golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
565+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
566+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
334567golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
335568golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
569+google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
336570google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
337571google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
338572google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
339573google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
574+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
340575google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
341576google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
342577google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
@@ -344,13 +579,20 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
344579google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
345580google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
346581google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
347−google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
582+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
583+google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
348584google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
349585google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
350586google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
587+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
351588google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
589+google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
590+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
352591google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
592+google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
353593google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
594+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
595+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
354596google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
355597google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
356598google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -359,18 +601,19 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
359601google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
360602google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
361603google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
362−google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
363604google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
364605google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
365606google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
366607google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
367608gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
368609gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
369−gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
370610gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
371611gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
372612gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
613+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
614+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
373615gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
616+gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
374617gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
375618gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
376619gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -381,24 +624,33 @@ gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y
381624gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
382625gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
383626gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
627+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
384628gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
385629gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
386630gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
387631gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
632+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
388633gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
389634gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
390635gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
391636gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
392637gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
638+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
393639honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
394640honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
395641honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
642+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
396643k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc=
397644k8s.io/apimachinery v0.19.3/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
398645k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
646+k8s.io/gengo v0.0.0-20201113003025-83324d819ded h1:JApXBKYyB7l9xx+DK7/+mFjC7A9Bt5A93FPvFD0HIFE=
647+k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
399648k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
400649k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
650+k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
651+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
401652k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
402653sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
403654sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
404655sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
656+sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
main.go+154−24
@@ -1,76 +1,206 @@
11package main
22
33import (
4+ "context"
45 "fmt"
56 "math/rand"
7+ "net/http"
68 "os"
79 "os/signal"
810 "runtime"
911 "syscall"
1012 "time"
1113
14+ "github.com/ghodss/yaml"
1215 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1316 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
17+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
1418 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation"
19+ "github.com/spf13/pflag"
1520)
1621
1722func main() {
1823 logger.SetFlags(logger.Lshortfile)
19− flagSet := options.NewFlagSet()
2024
21− config := flagSet.String("config", "", "path to config file")
22− showVersion := flagSet.Bool("version", false, "print version string")
25+ configFlagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ContinueOnError)
2326
24− err := flagSet.Parse(os.Args[1:])
25− if err != nil {
26− logger.Printf("ERROR: Failed to parse flags: %v", err)
27− os.Exit(1)
28− }
27+ // Because we parse early to determine alpha vs legacy config, we have to
28+ // ignore any unknown flags for now
29+ configFlagSet.ParseErrorsWhitelist.UnknownFlags = true
30+
31+ config := configFlagSet.String("config", "", "path to config file")
32+ alphaConfig := configFlagSet.String("alpha-config", "", "path to alpha config file (use at your own risk - the structure in this config file may change between minor releases)")
33+ convertConfig := configFlagSet.Bool("convert-config-to-alpha", false, "if true, the proxy will load configuration as normal and convert existing configuration to the alpha config structure, and print it to stdout")
34+ showVersion := configFlagSet.Bool("version", false, "print version string")
35+ configFlagSet.Parse(os.Args[1:])
2936
3037 if *showVersion {
3138 fmt.Printf("oauth2-proxy %s (built with %s)\n", VERSION, runtime.Version())
3239 return
3340 }
3441
35− legacyOpts := options.NewLegacyOptions()
36− err = options.Load(*config, flagSet, legacyOpts)
37− if err != nil {
38− logger.Errorf("ERROR: Failed to load config: %v", err)
39− os.Exit(1)
42+ if *convertConfig && *alphaConfig != "" {
43+ logger.Fatal("cannot use alpha-config and conver-config-to-alpha together")
4044 }
4145
42− opts, err := legacyOpts.ToOptions()
46+ opts, err := loadConfiguration(*config, *alphaConfig, configFlagSet, os.Args[1:])
4347 if err != nil {
44− logger.Errorf("ERROR: Failed to convert config: %v", err)
45− os.Exit(1)
48+ logger.Fatalf("ERROR: %v", err)
4649 }
4750
48− err = validation.Validate(opts)
49− if err != nil {
50− logger.Printf("%s", err)
51− os.Exit(1)
51+ if *convertConfig {
52+ if err := printConvertedConfig(opts); err != nil {
53+ logger.Fatalf("ERROR: could not convert config: %v", err)
54+ }
55+ return
56+ }
57+
58+ if err = validation.Validate(opts); err != nil {
59+ logger.Fatalf("%s", err)
5260 }
5361
5462 validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile)
5563 oauthproxy, err := NewOAuthProxy(opts, validator)
5664 if err != nil {
57− logger.Errorf("ERROR: Failed to initialise OAuth2 Proxy: %v", err)
58− os.Exit(1)
65+ logger.Fatalf("ERROR: Failed to initialise OAuth2 Proxy: %v", err)
5966 }
6067
6168 rand.Seed(time.Now().UnixNano())
6269
70+ oauthProxyStop := make(chan struct{}, 1)
71+ metricsStop := startMetricsServer(opts.MetricsAddress, oauthProxyStop)
72+
6373 s := &Server{
6474 Handler: oauthproxy,
6575 Opts: opts,
66− stop: make(chan struct{}, 1),
76+ stop: oauthProxyStop,
6777 }
6878 // Observe signals in background goroutine.
6979 go func() {
7080 sigint := make(chan os.Signal, 1)
7181 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
7282 <-sigint
73− s.stop <- struct{}{} // notify having caught signal
83+ s.stop <- struct{}{} // notify having caught signal stop oauthproxy
84+ close(metricsStop) // and the metrics endpoint
7485 }()
7586 s.ListenAndServe()
7687}
88+
89+// startMetricsServer will start the metrics server on the specified address.
90+// It always return a channel to signal stop even when it does not run.
91+func startMetricsServer(address string, oauthProxyStop chan struct{}) chan struct{} {
92+ stop := make(chan struct{}, 1)
93+
94+ // Attempt to setup the metrics endpoint if we have an address
95+ if address != "" {
96+ s := &http.Server{Addr: address, Handler: middleware.DefaultMetricsHandler}
97+ go func() {
98+ // ListenAndServe always returns a non-nil error. After Shutdown or
99+ // Close, the returned error is ErrServerClosed
100+ if err := s.ListenAndServe(); err != http.ErrServerClosed {
101+ logger.Println(err)
102+ // Stop the metrics shutdown go routine
103+ close(stop)
104+ // Stop the oauthproxy server, we have encounter an unexpected error
105+ close(oauthProxyStop)
106+ }
107+ }()
108+
109+ go func() {
110+ <-stop
111+ if err := s.Shutdown(context.Background()); err != nil {
112+ logger.Print(err)
113+ }
114+ }()
115+ }
116+
117+ return stop
118+}
119+
120+// loadConfiguration will load in the user's configuration.
121+// It will either load the alpha configuration (if alphaConfig is given)
122+// or the legacy configuration.
123+func loadConfiguration(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
124+ if alphaConfig != "" {
125+ logger.Printf("WARNING: You are using alpha configuration. The structure in this configuration file may change without notice. You MUST remove conflicting options from your existing configuration.")
126+ return loadAlphaOptions(config, alphaConfig, extraFlags, args)
127+ }
128+ return loadLegacyOptions(config, extraFlags, args)
129+}
130+
131+// loadLegacyOptions loads the old toml options using the legacy flagset
132+// and legacy options struct.
133+func loadLegacyOptions(config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
134+ optionsFlagSet := options.NewLegacyFlagSet()
135+ optionsFlagSet.AddFlagSet(extraFlags)
136+ if err := optionsFlagSet.Parse(args); err != nil {
137+ return nil, fmt.Errorf("failed to parse flags: %v", err)
138+ }
139+
140+ legacyOpts := options.NewLegacyOptions()
141+ if err := options.Load(config, optionsFlagSet, legacyOpts); err != nil {
142+ return nil, fmt.Errorf("failed to load config: %v", err)
143+ }
144+
145+ opts, err := legacyOpts.ToOptions()
146+ if err != nil {
147+ return nil, fmt.Errorf("failed to convert config: %v", err)
148+ }
149+
150+ return opts, nil
151+}
152+
153+// loadAlphaOptions loads the old style config excluding options converted to
154+// the new alpha format, then merges the alpha options, loaded from YAML,
155+// into the core configuration.
156+func loadAlphaOptions(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
157+ opts, err := loadOptions(config, extraFlags, args)
158+ if err != nil {
159+ return nil, fmt.Errorf("failed to load core options: %v", err)
160+ }
161+
162+ alphaOpts := &options.AlphaOptions{}
163+ if err := options.LoadYAML(alphaConfig, alphaOpts); err != nil {
164+ return nil, fmt.Errorf("failed to load alpha options: %v", err)
165+ }
166+
167+ alphaOpts.MergeInto(opts)
168+ return opts, nil
169+}
170+
171+// loadOptions loads the configuration using the old style format into the
172+// core options.Options struct.
173+// This means that none of the options that have been converted to alpha config
174+// will be loaded using this method.
175+func loadOptions(config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
176+ optionsFlagSet := options.NewFlagSet()
177+ optionsFlagSet.AddFlagSet(extraFlags)
178+ if err := optionsFlagSet.Parse(args); err != nil {
179+ return nil, fmt.Errorf("failed to parse flags: %v", err)
180+ }
181+
182+ opts := options.NewOptions()
183+ if err := options.Load(config, optionsFlagSet, opts); err != nil {
184+ return nil, fmt.Errorf("failed to load config: %v", err)
185+ }
186+
187+ return opts, nil
188+}
189+
190+// printConvertedConfig extracts alpha options from the loaded configuration
191+// and renders these to stdout in YAML format.
192+func printConvertedConfig(opts *options.Options) error {
193+ alphaConfig := &options.AlphaOptions{}
194+ alphaConfig.ExtractFrom(opts)
195+
196+ data, err := yaml.Marshal(alphaConfig)
197+ if err != nil {
198+ return fmt.Errorf("unable to marshal config: %v", err)
199+ }
200+
201+ if _, err := os.Stdout.Write(data); err != nil {
202+ return fmt.Errorf("unable to write output: %v", err)
203+ }
204+
205+ return nil
206+}
main_suite_test.gonew file+17
@@ -0,0 +1,17 @@
1+package main
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestMainSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
14+
15+ RegisterFailHandler(Fail)
16+ RunSpecs(t, "Main Suite")
17+}
main_test.gonew file+215
@@ -0,0 +1,215 @@
1+package main
2+
3+import (
4+ "errors"
5+ "io/ioutil"
6+ "os"
7+ "time"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/ginkgo/extensions/table"
12+ . "github.com/onsi/gomega"
13+ "github.com/spf13/pflag"
14+)
15+
16+var _ = Describe("Configuration Loading Suite", func() {
17+ const testLegacyConfig = `
18+upstreams="http://httpbin"
19+set_basic_auth="true"
20+basic_auth_password="super-secret-password"
21+`
22+
23+ const testAlphaConfig = `
24+upstreams:
25+ - id: /
26+ path: /
27+ uri: http://httpbin
28+ flushInterval: 1s
29+ passHostHeader: true
30+ proxyWebSockets: true
31+injectRequestHeaders:
32+- name: Authorization
33+ values:
34+ - claim: user
35+ prefix: "Basic "
36+ basicAuthPassword:
37+ value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
38+- name: X-Forwarded-Groups
39+ values:
40+ - claim: groups
41+- name: X-Forwarded-User
42+ values:
43+ - claim: user
44+- name: X-Forwarded-Email
45+ values:
46+ - claim: email
47+- name: X-Forwarded-Preferred-Username
48+ values:
49+ - claim: preferred_username
50+injectResponseHeaders:
51+- name: Authorization
52+ values:
53+ - claim: user
54+ prefix: "Basic "
55+ basicAuthPassword:
56+ value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
57+`
58+
59+ const testCoreConfig = `
60+http_address="0.0.0.0:4180"
61+cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
62+provider="oidc"
63+email_domains="example.com"
64+oidc_issuer_url="http://dex.localhost:4190/dex"
65+client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
66+client_id="oauth2-proxy"
67+cookie_secure="false"
68+
69+redirect_url="http://localhost:4180/oauth2/callback"
70+`
71+
72+ boolPtr := func(b bool) *bool {
73+ return &b
74+ }
75+
76+ durationPtr := func(d time.Duration) *options.Duration {
77+ du := options.Duration(d)
78+ return &du
79+ }
80+
81+ testExpectedOptions := func() *options.Options {
82+ opts, err := options.NewLegacyOptions().ToOptions()
83+ Expect(err).ToNot(HaveOccurred())
84+
85+ opts.HTTPAddress = "0.0.0.0:4180"
86+ opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
87+ opts.ProviderType = "oidc"
88+ opts.EmailDomains = []string{"example.com"}
89+ opts.OIDCIssuerURL = "http://dex.localhost:4190/dex"
90+ opts.ClientSecret = "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
91+ opts.ClientID = "oauth2-proxy"
92+ opts.Cookie.Secure = false
93+ opts.RawRedirectURL = "http://localhost:4180/oauth2/callback"
94+
95+ opts.UpstreamServers = options.Upstreams{
96+ {
97+ ID: "/",
98+ Path: "/",
99+ URI: "http://httpbin",
100+ FlushInterval: durationPtr(options.DefaultUpstreamFlushInterval),
101+ PassHostHeader: boolPtr(true),
102+ ProxyWebSockets: boolPtr(true),
103+ },
104+ }
105+
106+ authHeader := options.Header{
107+ Name: "Authorization",
108+ Values: []options.HeaderValue{
109+ {
110+ ClaimSource: &options.ClaimSource{
111+ Claim: "user",
112+ Prefix: "Basic ",
113+ BasicAuthPassword: &options.SecretSource{
114+ Value: []byte("super-secret-password"),
115+ },
116+ },
117+ },
118+ },
119+ }
120+
121+ opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...)
122+ opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader)
123+ return opts
124+ }
125+
126+ type loadConfigurationTableInput struct {
127+ configContent string
128+ alphaConfigContent string
129+ args []string
130+ extraFlags func() *pflag.FlagSet
131+ expectedOptions func() *options.Options
132+ expectedErr error
133+ }
134+
135+ DescribeTable("LoadConfiguration",
136+ func(in loadConfigurationTableInput) {
137+ var configFileName, alphaConfigFileName string
138+
139+ defer func() {
140+ if configFileName != "" {
141+ Expect(os.Remove(configFileName)).To(Succeed())
142+ }
143+ if alphaConfigFileName != "" {
144+ Expect(os.Remove(alphaConfigFileName)).To(Succeed())
145+ }
146+ }()
147+
148+ if in.configContent != "" {
149+ By("Writing the config to a temporary file", func() {
150+ file, err := ioutil.TempFile("", "oauth2-proxy-test-config-XXXX.cfg")
151+ Expect(err).ToNot(HaveOccurred())
152+ defer file.Close()
153+
154+ configFileName = file.Name()
155+
156+ _, err = file.WriteString(in.configContent)
157+ Expect(err).ToNot(HaveOccurred())
158+ })
159+ }
160+
161+ if in.alphaConfigContent != "" {
162+ By("Writing the config to a temporary file", func() {
163+ file, err := ioutil.TempFile("", "oauth2-proxy-test-alpha-config-XXXX.yaml")
164+ Expect(err).ToNot(HaveOccurred())
165+ defer file.Close()
166+
167+ alphaConfigFileName = file.Name()
168+
169+ _, err = file.WriteString(in.alphaConfigContent)
170+ Expect(err).ToNot(HaveOccurred())
171+ })
172+ }
173+
174+ extraFlags := pflag.NewFlagSet("test-flagset", pflag.ExitOnError)
175+ if in.extraFlags != nil {
176+ extraFlags = in.extraFlags()
177+ }
178+
179+ opts, err := loadConfiguration(configFileName, alphaConfigFileName, extraFlags, in.args)
180+ if in.expectedErr != nil {
181+ Expect(err).To(MatchError(in.expectedErr.Error()))
182+ } else {
183+ Expect(err).ToNot(HaveOccurred())
184+ }
185+ Expect(in.expectedOptions).ToNot(BeNil())
186+ Expect(opts).To(Equal(in.expectedOptions()))
187+ },
188+ Entry("with legacy configuration", loadConfigurationTableInput{
189+ configContent: testCoreConfig + testLegacyConfig,
190+ expectedOptions: testExpectedOptions,
191+ }),
192+ Entry("with alpha configuration", loadConfigurationTableInput{
193+ configContent: testCoreConfig,
194+ alphaConfigContent: testAlphaConfig,
195+ expectedOptions: testExpectedOptions,
196+ }),
197+ Entry("with bad legacy configuration", loadConfigurationTableInput{
198+ configContent: testCoreConfig + "unknown_field=\"something\"",
199+ expectedOptions: func() *options.Options { return nil },
200+ expectedErr: errors.New("failed to load config: error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_field"),
201+ }),
202+ Entry("with bad alpha configuration", loadConfigurationTableInput{
203+ configContent: testCoreConfig,
204+ alphaConfigContent: testAlphaConfig + ":",
205+ expectedOptions: func() *options.Options { return nil },
206+ expectedErr: errors.New("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line 34: did not find expected key"),
207+ }),
208+ Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{
209+ configContent: testCoreConfig + "unknown_field=\"something\"",
210+ alphaConfigContent: testAlphaConfig,
211+ expectedOptions: func() *options.Options { return nil },
212+ expectedErr: errors.New("failed to load core options: failed to load config: error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_field"),
213+ }),
214+ )
215+})
oauthproxy.go+506−365
@@ -5,7 +5,6 @@ import (
55 "encoding/json"
66 "errors"
77 "fmt"
8− "html/template"
98 "net"
109 "net/http"
1110 "net/url"
@@ -18,22 +17,21 @@ import (
1817 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
1918 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
2019 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
20+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
2121 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
2222 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
2323 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
2424 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
2525 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
2626 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
27+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
2728 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
2829 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
29− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
3030 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
3131)
3232
3333const (
34− httpScheme = "http"
35− httpsScheme = "https"
36−
34+ schemeHTTPS = "https"
3735 applicationJSON = "application/json"
3836)
3937
@@ -77,38 +75,33 @@ type OAuthProxy struct {
7775 AuthOnlyPath string
7876 UserInfoPath string
7977
80− allowedRoutes []allowedRoute
81− redirectURL *url.URL // the url to receive requests at
82− whitelistDomains []string
83− provider providers.Provider
84− providerNameOverride string
85− sessionStore sessionsapi.SessionStore
86− ProxyPrefix string
87− SignInMessage string
88− basicAuthValidator basic.Validator
89− displayHtpasswdForm bool
90− serveMux http.Handler
91− SetXAuthRequest bool
92− PassBasicAuth bool
93− SetBasicAuth bool
94− SkipProviderButton bool
95− PassUserHeaders bool
96− BasicAuthPassword string
97− PassAccessToken bool
98− SetAuthorization bool
99− PassAuthorization bool
100− PreferEmailToUser bool
101− skipAuthPreflight bool
102− skipJwtBearerTokens bool
103− templates *template.Template
104− realClientIPParser ipapi.RealClientIPParser
105− trustedIPs *ip.NetSet
106− Banner string
107− Footer string
78+ allowedRoutes []allowedRoute
79+ redirectURL *url.URL // the url to receive requests at
80+ whitelistDomains []string
81+ provider providers.Provider
82+ sessionStore sessionsapi.SessionStore
83+ ProxyPrefix string
84+ basicAuthValidator basic.Validator
85+ serveMux http.Handler
86+ SetXAuthRequest bool
87+ PassBasicAuth bool
88+ SetBasicAuth bool
89+ SkipProviderButton bool
90+ PassUserHeaders bool
91+ BasicAuthPassword string
92+ PassAccessToken bool
93+ SetAuthorization bool
94+ PassAuthorization bool
95+ PreferEmailToUser bool
96+ skipAuthPreflight bool
97+ skipJwtBearerTokens bool
98+ realClientIPParser ipapi.RealClientIPParser
99+ trustedIPs *ip.NetSet
108100
109101 sessionChain alice.Chain
110102 headersChain alice.Chain
111103 preAuthChain alice.Chain
104+ pageWriter pagewriter.Writer
112105}
113106
114107// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
@@ -118,9 +111,31 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
118111 return nil, fmt.Errorf("error initialising session store: %v", err)
119112 }
120113
121− templates := loadTemplates(opts.CustomTemplatesDir)
122− proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
123− upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
114+ var basicAuthValidator basic.Validator
115+ if opts.HtpasswdFile != "" {
116+ logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
117+ var err error
118+ basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
119+ if err != nil {
120+ return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
121+ }
122+ }
123+
124+ pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
125+ TemplatesPath: opts.Templates.Path,
126+ ProxyPrefix: opts.ProxyPrefix,
127+ Footer: opts.Templates.Footer,
128+ Version: VERSION,
129+ Debug: opts.Templates.Debug,
130+ ProviderName: buildProviderName(opts.GetProvider(), opts.ProviderName),
131+ SignInMessage: buildSignInMessage(opts),
132+ DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
133+ })
134+ if err != nil {
135+ return nil, fmt.Errorf("error initialising page writer: %v", err)
136+ }
137+
138+ upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter.ProxyErrorHandler)
124139 if err != nil {
125140 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
126141 }
@@ -153,16 +168,6 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
153168 }
154169 }
155170
156− var basicAuthValidator basic.Validator
157− if opts.HtpasswdFile != "" {
158− logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
159− var err error
160− basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
161− if err != nil {
162− return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
163− }
164− }
165−
166171 allowedRoutes, err := buildRoutesAllowlist(opts)
167172 if err != nil {
168173 return nil, err
@@ -199,29 +204,24 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
199204 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
200205 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
201206
202− ProxyPrefix: opts.ProxyPrefix,
203− provider: opts.GetProvider(),
204− providerNameOverride: opts.ProviderName,
205− sessionStore: sessionStore,
206− serveMux: upstreamProxy,
207− redirectURL: redirectURL,
208− allowedRoutes: allowedRoutes,
209− whitelistDomains: opts.WhitelistDomains,
210− skipAuthPreflight: opts.SkipAuthPreflight,
211− skipJwtBearerTokens: opts.SkipJwtBearerTokens,
212− realClientIPParser: opts.GetRealClientIPParser(),
213− SkipProviderButton: opts.SkipProviderButton,
214− templates: templates,
215− trustedIPs: trustedIPs,
216− Banner: opts.Banner,
217− Footer: opts.Footer,
218− SignInMessage: buildSignInMessage(opts),
219−
220− basicAuthValidator: basicAuthValidator,
221− displayHtpasswdForm: basicAuthValidator != nil && opts.DisplayHtpasswdForm,
222− sessionChain: sessionChain,
223− headersChain: headersChain,
224− preAuthChain: preAuthChain,
207+ ProxyPrefix: opts.ProxyPrefix,
208+ provider: opts.GetProvider(),
209+ sessionStore: sessionStore,
210+ serveMux: upstreamProxy,
211+ redirectURL: redirectURL,
212+ allowedRoutes: allowedRoutes,
213+ whitelistDomains: opts.WhitelistDomains,
214+ skipAuthPreflight: opts.SkipAuthPreflight,
215+ skipJwtBearerTokens: opts.SkipJwtBearerTokens,
216+ realClientIPParser: opts.GetRealClientIPParser(),
217+ SkipProviderButton: opts.SkipProviderButton,
218+ trustedIPs: trustedIPs,
219+
220+ basicAuthValidator: basicAuthValidator,
221+ sessionChain: sessionChain,
222+ headersChain: headersChain,
223+ preAuthChain: preAuthChain,
224+ pageWriter: pageWriter,
225225 }, nil
226226}
227227
@@ -229,7 +229,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
229229// the OAuth2 Proxy authentication logic kicks in.
230230// For example forcing HTTPS or health checks.
231231func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
232− chain := alice.New(middleware.NewScope())
232+ chain := alice.New(middleware.NewScope(opts.ReverseProxy))
233233
234234 if opts.ForceHTTPS {
235235 _, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress)
@@ -254,6 +254,8 @@ func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
254254 chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents))
255255 }
256256
257+ chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
258+
257259 return chain, nil
258260}
259261
@@ -303,11 +305,11 @@ func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
303305
304306func buildSignInMessage(opts *options.Options) string {
305307 var msg string
306− if len(opts.Banner) >= 1 {
307− if opts.Banner == "-" {
308+ if len(opts.Templates.Banner) >= 1 {
309+ if opts.Templates.Banner == "-" {
308310 msg = ""
309311 } else {
310− msg = opts.Banner
312+ msg = opts.Templates.Banner
311313 }
312314 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
313315 if len(opts.EmailDomains) > 1 {
@@ -319,6 +321,13 @@ func buildSignInMessage(opts *options.Options) string {
319321 return msg
320322}
321323
324+func buildProviderName(p providers.Provider, override string) string {
325+ if override != "" {
326+ return override
327+ }
328+ return p.Data().ProviderName
329+}
330+
322331// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
323332// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
324333// (method=path support)
@@ -366,49 +375,6 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
366375 return routes, nil
367376}
368377
369−// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
370−// redirect clients to once authenticated
371−func (p *OAuthProxy) GetRedirectURI(host string) string {
372− // default to the request Host if not set
373− if p.redirectURL.Host != "" {
374− return p.redirectURL.String()
375− }
376− u := *p.redirectURL
377− if u.Scheme == "" {
378− if p.CookieSecure {
379− u.Scheme = httpsScheme
380− } else {
381− u.Scheme = httpScheme
382− }
383− }
384− u.Host = host
385− return u.String()
386−}
387−
388−func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
389− if code == "" {
390− return nil, providers.ErrMissingCode
391− }
392− redirectURI := p.GetRedirectURI(host)
393− s, err := p.provider.Redeem(ctx, redirectURI, code)
394− if err != nil {
395− return nil, err
396− }
397− return s, nil
398−}
399−
400−func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
401− var err error
402− if s.Email == "" {
403− s.Email, err = p.provider.GetEmailAddress(ctx, s)
404− if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
405− return err
406− }
407− }
408−
409− return p.provider.EnrichSession(ctx, s)
410−}
411−
412378// MakeCSRFCookie creates a cookie for CSRF
413379func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
414380 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
@@ -418,7 +384,7 @@ func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, ex
418384 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
419385
420386 if cookieDomain != "" {
421− domain := util.GetRequestHost(req)
387+ domain := requestutil.GetRequestHost(req)
422388 if h, _, err := net.SplitHostPort(domain); err == nil {
423389 domain = h
424390 }
@@ -466,166 +432,6 @@ func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *s
466432 return p.sessionStore.Save(rw, req, s)
467433}
468434
469−// RobotsTxt disallows scraping pages from the OAuthProxy
470−func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
471− _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
472− if err != nil {
473− logger.Printf("Error writing robots.txt: %v", err)
474− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
475− return
476− }
477− rw.WriteHeader(http.StatusOK)
478−}
479−
480−// ErrorPage writes an error response
481−func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
482− rw.WriteHeader(code)
483− t := struct {
484− Title string
485− Message string
486− ProxyPrefix string
487− }{
488− Title: fmt.Sprintf("%d %s", code, title),
489− Message: message,
490− ProxyPrefix: p.ProxyPrefix,
491− }
492− err := p.templates.ExecuteTemplate(rw, "error.html", t)
493− if err != nil {
494− logger.Printf("Error rendering error.html template: %v", err)
495− http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
496− }
497−}
498−
499−// SignInPage writes the sing in template to the response
500−func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
501− prepareNoCache(rw)
502− err := p.ClearSessionCookie(rw, req)
503− if err != nil {
504− logger.Printf("Error clearing session cookie: %v", err)
505− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
506− return
507− }
508− rw.WriteHeader(code)
509−
510− redirectURL, err := p.GetRedirect(req)
511− if err != nil {
512− logger.Errorf("Error obtaining redirect: %v", err)
513− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
514− return
515− }
516−
517− if redirectURL == p.SignInPath {
518− redirectURL = "/"
519− }
520−
521− // We allow unescaped template.HTML since it is user configured options
522− /* #nosec G203 */
523− t := struct {
524− ProviderName string
525− SignInMessage template.HTML
526− CustomLogin bool
527− Redirect string
528− Version string
529− ProxyPrefix string
530− Footer template.HTML
531− }{
532− ProviderName: p.provider.Data().ProviderName,
533− SignInMessage: template.HTML(p.SignInMessage),
534− CustomLogin: p.displayHtpasswdForm,
535− Redirect: redirectURL,
536− Version: VERSION,
537− ProxyPrefix: p.ProxyPrefix,
538− Footer: template.HTML(p.Footer),
539− }
540− if p.providerNameOverride != "" {
541− t.ProviderName = p.providerNameOverride
542− }
543− err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
544− if err != nil {
545− logger.Printf("Error rendering sign_in.html template: %v", err)
546− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
547− }
548−}
549−
550−// ManualSignIn handles basic auth logins to the proxy
551−func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
552− if req.Method != "POST" || p.basicAuthValidator == nil {
553− return "", false
554− }
555− user := req.FormValue("username")
556− passwd := req.FormValue("password")
557− if user == "" {
558− return "", false
559− }
560− // check auth
561− if p.basicAuthValidator.Validate(user, passwd) {
562− logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
563− return user, true
564− }
565− logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
566− return "", false
567−}
568−
569−// GetRedirect reads the query parameter to get the URL to redirect clients to
570−// once authenticated with the OAuthProxy
571−func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
572− err = req.ParseForm()
573− if err != nil {
574− return
575− }
576−
577− redirect = req.Header.Get("X-Auth-Request-Redirect")
578− if req.Form.Get("rd") != "" {
579− redirect = req.Form.Get("rd")
580− }
581− if !p.IsValidRedirect(redirect) {
582− // Use RequestURI to preserve ?query
583− redirect = req.URL.RequestURI()
584− if strings.HasPrefix(redirect, p.ProxyPrefix) {
585− redirect = "/"
586− }
587− }
588−
589− return
590−}
591−
592−// splitHostPort separates host and port. If the port is not valid, it returns
593−// the entire input as host, and it doesn't check the validity of the host.
594−// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
595−// *** taken from net/url, modified validOptionalPort() to accept ":*"
596−func splitHostPort(hostport string) (host, port string) {
597− host = hostport
598−
599− colon := strings.LastIndexByte(host, ':')
600− if colon != -1 && validOptionalPort(host[colon:]) {
601− host, port = host[:colon], host[colon+1:]
602− }
603−
604− if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
605− host = host[1 : len(host)-1]
606− }
607−
608− return
609−}
610−
611−// validOptionalPort reports whether port is either an empty string
612−// or matches /^:\d*$/
613−// *** taken from net/url, modified to accept ":*"
614−func validOptionalPort(port string) bool {
615− if port == "" || port == ":*" {
616− return true
617− }
618− if port[0] != ':' {
619− return false
620− }
621− for _, b := range port[1:] {
622− if b < '0' || b > '9' {
623− return false
624− }
625− }
626− return true
627−}
628−
629435// IsValidRedirect checks whether the redirect URL is whitelisted
630436func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
631437 switch {
@@ -642,21 +448,23 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
642448 }
643449 redirectHostname := redirectURL.Hostname()
644450
645− for _, domain := range p.whitelistDomains {
646− domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
647− if domainHostname == "" {
451+ for _, allowedDomain := range p.whitelistDomains {
452+ allowedHost, allowedPort := splitHostPort(allowedDomain)
453+ if allowedHost == "" {
648454 continue
649455 }
650456
651− if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
457+ if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
458+ (strings.HasPrefix(allowedHost, ".") &&
459+ strings.HasSuffix(redirectHostname, allowedHost)) {
652460 // the domain names match, now validate the ports
653461 // if the whitelisted domain's port is '*', allow all ports
654462 // if the whitelisted domain contains a specific port, only allow that port
655463 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
656464 redirectPort := redirectURL.Port()
657− if (domainPort == "*") ||
658− (domainPort == redirectPort) ||
659− (domainPort == "" && redirectPort == "") {
465+ if allowedPort == "*" ||
466+ allowedPort == redirectPort ||
467+ (allowedPort == "" && redirectPort == "") {
660468 return true
661469 }
662470 }
@@ -670,10 +478,65 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
670478 }
671479}
672480
481+func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
482+ p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
483+}
484+
485+func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
486+ if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
487+ prepareNoCache(rw)
488+ }
489+
490+ switch path := req.URL.Path; {
491+ case path == p.RobotsPath:
492+ p.RobotsTxt(rw, req)
493+ case p.IsAllowedRequest(req):
494+ p.SkipAuthProxy(rw, req)
495+ case path == p.SignInPath:
496+ p.SignIn(rw, req)
497+ case path == p.SignOutPath:
498+ p.SignOut(rw, req)
499+ case path == p.OAuthStartPath:
500+ p.OAuthStart(rw, req)
501+ case path == p.OAuthCallbackPath:
502+ p.OAuthCallback(rw, req)
503+ case path == p.AuthOnlyPath:
504+ p.AuthOnly(rw, req)
505+ case path == p.UserInfoPath:
506+ p.UserInfo(rw, req)
507+ default:
508+ p.Proxy(rw, req)
509+ }
510+}
511+
512+// RobotsTxt disallows scraping pages from the OAuthProxy
513+func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter, req *http.Request) {
514+ _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
515+ if err != nil {
516+ logger.Printf("Error writing robots.txt: %v", err)
517+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
518+ return
519+ }
520+ rw.WriteHeader(http.StatusOK)
521+}
522+
523+// ErrorPage writes an error response
524+func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
525+ redirectURL, err := p.getAppRedirect(req)
526+ if err != nil {
527+ logger.Errorf("Error obtaining redirect: %v", err)
528+ }
529+ if redirectURL == p.SignInPath || redirectURL == "" {
530+ redirectURL = "/"
531+ }
532+
533+ p.pageWriter.WriteErrorPage(rw, code, redirectURL, appError, messages...)
534+}
535+
673536// IsAllowedRequest is used to check if auth should be skipped for this request
674537func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
675538 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
676− return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
539+ return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
677540}
678541
679542// IsAllowedRoute is used to check if the request method & path is allowed without auth
@@ -686,23 +549,8 @@ func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
686549 return false
687550}
688551
689−// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
690−var noCacheHeaders = map[string]string{
691− "Expires": time.Unix(0, 0).Format(time.RFC1123),
692− "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
693− "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
694−}
695−
696−// prepareNoCache prepares headers for preventing browser caching.
697−func prepareNoCache(w http.ResponseWriter) {
698− // Set NoCache headers
699− for k, v := range noCacheHeaders {
700− w.Header().Set(k, v)
701− }
702−}
703−
704−// IsTrustedIP is used to check if a request comes from a trusted client IP address.
705−func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
552+// isTrustedIP is used to check if a request comes from a trusted client IP address.
553+func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
706554 if p.trustedIPs == nil {
707555 return false
708556 }
@@ -721,43 +569,56 @@ func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
721569 return p.trustedIPs.Has(remoteAddr)
722570}
723571
724−func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
725− p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
726−}
572+// SignInPage writes the sing in template to the response
573+func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
574+ prepareNoCache(rw)
575+ err := p.ClearSessionCookie(rw, req)
576+ if err != nil {
577+ logger.Printf("Error clearing session cookie: %v", err)
578+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
579+ return
580+ }
581+ rw.WriteHeader(code)
727582
728−func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
729− if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
730− prepareNoCache(rw)
583+ redirectURL, err := p.getAppRedirect(req)
584+ if err != nil {
585+ logger.Errorf("Error obtaining redirect: %v", err)
586+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
587+ return
731588 }
732589
733− switch path := req.URL.Path; {
734− case path == p.RobotsPath:
735− p.RobotsTxt(rw)
736− case p.IsAllowedRequest(req):
737− p.SkipAuthProxy(rw, req)
738− case path == p.SignInPath:
739− p.SignIn(rw, req)
740− case path == p.SignOutPath:
741− p.SignOut(rw, req)
742− case path == p.OAuthStartPath:
743− p.OAuthStart(rw, req)
744− case path == p.OAuthCallbackPath:
745− p.OAuthCallback(rw, req)
746− case path == p.AuthOnlyPath:
747− p.AuthenticateOnly(rw, req)
748− case path == p.UserInfoPath:
749− p.UserInfo(rw, req)
750− default:
751− p.Proxy(rw, req)
590+ if redirectURL == p.SignInPath {
591+ redirectURL = "/"
592+ }
593+
594+ p.pageWriter.WriteSignInPage(rw, redirectURL)
595+}
596+
597+// ManualSignIn handles basic auth logins to the proxy
598+func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
599+ if req.Method != "POST" || p.basicAuthValidator == nil {
600+ return "", false
601+ }
602+ user := req.FormValue("username")
603+ passwd := req.FormValue("password")
604+ if user == "" {
605+ return "", false
752606 }
607+ // check auth
608+ if p.basicAuthValidator.Validate(user, passwd) {
609+ logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
610+ return user, true
611+ }
612+ logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
613+ return "", false
753614}
754615
755616// SignIn serves a page prompting users to sign in
756617func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
757− redirect, err := p.GetRedirect(req)
618+ redirect, err := p.getAppRedirect(req)
758619 if err != nil {
759620 logger.Errorf("Error obtaining redirect: %v", err)
760− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
621+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
761622 return
762623 }
763624
@@ -767,7 +628,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
767628 err = p.SaveSession(rw, req, session)
768629 if err != nil {
769630 logger.Printf("Error saving session: %v", err)
770− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
631+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
771632 return
772633 }
773634 http.Redirect(rw, req, redirect, http.StatusFound)
@@ -780,7 +641,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
780641 }
781642}
782643
783−//UserInfo endpoint outputs session email and preferred username in JSON format
644+// UserInfo endpoint outputs session email and preferred username in JSON format
784645func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
785646
786647 session, err := p.getAuthenticatedSession(rw, req)
@@ -806,22 +667,22 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
806667 err = json.NewEncoder(rw).Encode(userInfo)
807668 if err != nil {
808669 logger.Printf("Error encoding user info: %v", err)
809− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
670+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
810671 }
811672}
812673
813674// SignOut sends a response to clear the authentication cookie
814675func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
815− redirect, err := p.GetRedirect(req)
676+ redirect, err := p.getAppRedirect(req)
816677 if err != nil {
817678 logger.Errorf("Error obtaining redirect: %v", err)
818− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
679+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
819680 return
820681 }
821682 err = p.ClearSessionCookie(rw, req)
822683 if err != nil {
823684 logger.Errorf("Error clearing session cookie: %v", err)
824− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
685+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
825686 return
826687 }
827688 http.Redirect(rw, req, redirect, http.StatusFound)
@@ -833,17 +694,17 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
833694 nonce, err := encryption.Nonce()
834695 if err != nil {
835696 logger.Errorf("Error obtaining nonce: %v", err)
836− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
697+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
837698 return
838699 }
839700 p.SetCSRFCookie(rw, req, nonce)
840− redirect, err := p.GetRedirect(req)
701+ redirect, err := p.getAppRedirect(req)
841702 if err != nil {
842703 logger.Errorf("Error obtaining redirect: %v", err)
843− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
704+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
844705 return
845706 }
846− redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
707+ redirectURI := p.getOAuthRedirectURI(req)
847708 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
848709}
849710
@@ -856,34 +717,36 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
856717 err := req.ParseForm()
857718 if err != nil {
858719 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
859− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
720+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
860721 return
861722 }
862723 errorString := req.Form.Get("error")
863724 if errorString != "" {
864725 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
865− p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
726+ message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
727+ // Set the debug message and override the non debug message to be the same for this case
728+ p.ErrorPage(rw, req, http.StatusForbidden, message, message)
866729 return
867730 }
868731
869− session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
732+ session, err := p.redeemCode(req)
870733 if err != nil {
871734 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
872− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
735+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
873736 return
874737 }
875738
876739 err = p.enrichSessionState(req.Context(), session)
877740 if err != nil {
878741 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
879− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
742+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
880743 return
881744 }
882745
883746 state := strings.SplitN(req.Form.Get("state"), ":", 2)
884747 if len(state) != 2 {
885748 logger.Error("Error while parsing OAuth2 state: invalid length")
886− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
749+ p.ErrorPage(rw, req, http.StatusInternalServerError, "State paremeter did not have expected length", "Login Failed: Invalid State after login.")
887750 return
888751 }
889752 nonce := state[0]
@@ -891,13 +754,13 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
891754 c, err := req.Cookie(p.CSRFCookieName)
892755 if err != nil {
893756 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
894− p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
757+ p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
895758 return
896759 }
897760 p.ClearCSRFCookie(rw, req)
898761 if c.Value != nonce {
899762 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
900− p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
763+ p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
901764 return
902765 }
903766
@@ -915,26 +778,62 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
915778 err := p.SaveSession(rw, req, session)
916779 if err != nil {
917780 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
918− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
781+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
919782 return
920783 }
921784 http.Redirect(rw, req, redirect, http.StatusFound)
922785 } else {
923786 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
924− p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
787+ p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
788+ }
789+}
790+
791+func (p *OAuthProxy) redeemCode(req *http.Request) (*sessionsapi.SessionState, error) {
792+ code := req.Form.Get("code")
793+ if code == "" {
794+ return nil, providers.ErrMissingCode
795+ }
796+
797+ redirectURI := p.getOAuthRedirectURI(req)
798+ s, err := p.provider.Redeem(req.Context(), redirectURI, code)
799+ if err != nil {
800+ return nil, err
925801 }
802+ return s, nil
803+}
804+
805+func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
806+ var err error
807+ if s.Email == "" {
808+ // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
809+ // nolint:staticcheck
810+ s.Email, err = p.provider.GetEmailAddress(ctx, s)
811+ if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
812+ return err
813+ }
814+ }
815+
816+ return p.provider.EnrichSession(ctx, s)
926817}
927818
928−// AuthenticateOnly checks whether the user is currently logged in
929−func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
819+// AuthOnly checks whether the user is currently logged in (both authentication
820+// and optional authorization).
821+func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
930822 session, err := p.getAuthenticatedSession(rw, req)
931823 if err != nil {
932− http.Error(rw, "unauthorized request", http.StatusUnauthorized)
824+ http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
825+ return
826+ }
827+
828+ // Unauthorized cases need to return 403 to prevent infinite redirects with
829+ // subrequest architectures
830+ if !authOnlyAuthorize(req, session) {
831+ http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
933832 return
934833 }
935834
936835 // we are authenticated
937− p.addHeadersForProxying(rw, req, session)
836+ p.addHeadersForProxying(rw, session)
938837 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
939838 rw.WriteHeader(http.StatusAccepted)
940839 })).ServeHTTP(rw, req)
@@ -952,13 +851,13 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
952851 switch err {
953852 case nil:
954853 // we are authenticated
955− p.addHeadersForProxying(rw, req, session)
854+ p.addHeadersForProxying(rw, session)
956855 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
957856 case ErrNeedsLogin:
958857 // we need to send the user to a login screen
959858 if isAjax(req) {
960859 // no point redirecting an AJAX request
961− p.ErrorJSON(rw, http.StatusUnauthorized)
860+ p.errorJSON(rw, http.StatusUnauthorized)
962861 return
963862 }
964863
@@ -969,15 +868,202 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
969868 }
970869
971870 case ErrAccessDenied:
972− p.ErrorPage(rw, http.StatusUnauthorized, "Permission Denied", "Unauthorized")
871+ p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
973872
974873 default:
975874 // unknown error
976875 logger.Errorf("Unexpected internal error: %v", err)
977− p.ErrorPage(rw, http.StatusInternalServerError,
978− "Internal Error", "Internal Error")
876+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
979877 }
878+}
980879
880+// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
881+var noCacheHeaders = map[string]string{
882+ "Expires": time.Unix(0, 0).Format(time.RFC1123),
883+ "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
884+ "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
885+}
886+
887+// prepareNoCache prepares headers for preventing browser caching.
888+func prepareNoCache(w http.ResponseWriter) {
889+ // Set NoCache headers
890+ for k, v := range noCacheHeaders {
891+ w.Header().Set(k, v)
892+ }
893+}
894+
895+// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
896+// redirect clients to once authenticated.
897+// This is usually the OAuthProxy callback URL.
898+func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
899+ // if `p.redirectURL` already has a host, return it
900+ if p.redirectURL.Host != "" {
901+ return p.redirectURL.String()
902+ }
903+
904+ // Otherwise figure out the scheme + host from the request
905+ rd := *p.redirectURL
906+ rd.Host = requestutil.GetRequestHost(req)
907+ rd.Scheme = requestutil.GetRequestProto(req)
908+
909+ // If CookieSecure is true, return `https` no matter what
910+ // Not all reverse proxies set X-Forwarded-Proto
911+ if p.CookieSecure {
912+ rd.Scheme = schemeHTTPS
913+ }
914+ return rd.String()
915+}
916+
917+// getAppRedirect determines the full URL or URI path to redirect clients to
918+// once authenticated with the OAuthProxy
919+// Strategy priority (first legal result is used):
920+// - `rd` querysting parameter
921+// - `X-Auth-Request-Redirect` header
922+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
923+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
924+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
925+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
926+// - `/`
927+func (p *OAuthProxy) getAppRedirect(req *http.Request) (string, error) {
928+ err := req.ParseForm()
929+ if err != nil {
930+ return "", err
931+ }
932+
933+ // These redirect getter functions are strategies ordered by priority
934+ // for figuring out the redirect URL.
935+ type redirectGetter func(req *http.Request) string
936+ for _, rdGetter := range []redirectGetter{
937+ p.getRdQuerystringRedirect,
938+ p.getXAuthRequestRedirect,
939+ p.getXForwardedHeadersRedirect,
940+ p.getURIRedirect,
941+ } {
942+ redirect := rdGetter(req)
943+ // Call `p.IsValidRedirect` again here a final time to be safe
944+ if redirect != "" && p.IsValidRedirect(redirect) {
945+ return redirect, nil
946+ }
947+ }
948+
949+ return "/", nil
950+}
951+
952+func isForwardedRequest(req *http.Request) bool {
953+ return requestutil.IsProxied(req) &&
954+ req.Host != requestutil.GetRequestHost(req)
955+}
956+
957+func (p *OAuthProxy) hasProxyPrefix(path string) bool {
958+ return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
959+}
960+
961+func (p *OAuthProxy) validateRedirect(redirect string, errorFormat string) string {
962+ if p.IsValidRedirect(redirect) {
963+ return redirect
964+ }
965+ if redirect != "" {
966+ logger.Errorf(errorFormat, redirect)
967+ }
968+ return ""
969+}
970+
971+// getRdQuerystringRedirect handles this getAppRedirect strategy:
972+// - `rd` querysting parameter
973+func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
974+ return p.validateRedirect(
975+ req.Form.Get("rd"),
976+ "Invalid redirect provided in rd querystring parameter: %s",
977+ )
978+}
979+
980+// getXAuthRequestRedirect handles this getAppRedirect strategy:
981+// - `X-Auth-Request-Redirect` Header
982+func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
983+ return p.validateRedirect(
984+ req.Header.Get("X-Auth-Request-Redirect"),
985+ "Invalid redirect provided in X-Auth-Request-Redirect header: %s",
986+ )
987+}
988+
989+// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
990+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
991+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
992+func (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
993+ if !isForwardedRequest(req) {
994+ return ""
995+ }
996+
997+ uri := requestutil.GetRequestURI(req)
998+ if p.hasProxyPrefix(uri) {
999+ uri = "/"
1000+ }
1001+
1002+ redirect := fmt.Sprintf(
1003+ "%s://%s%s",
1004+ requestutil.GetRequestProto(req),
1005+ requestutil.GetRequestHost(req),
1006+ uri,
1007+ )
1008+
1009+ return p.validateRedirect(redirect,
1010+ "Invalid redirect generated from X-Forwarded-* headers: %s")
1011+}
1012+
1013+// getURIRedirect handles these getAppRedirect strategies:
1014+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
1015+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
1016+// - `/`
1017+func (p *OAuthProxy) getURIRedirect(req *http.Request) string {
1018+ redirect := p.validateRedirect(
1019+ requestutil.GetRequestURI(req),
1020+ "Invalid redirect generated from X-Forwarded-Uri header: %s",
1021+ )
1022+ if redirect == "" {
1023+ redirect = req.URL.RequestURI()
1024+ }
1025+
1026+ if p.hasProxyPrefix(redirect) {
1027+ return "/"
1028+ }
1029+ return redirect
1030+}
1031+
1032+// splitHostPort separates host and port. If the port is not valid, it returns
1033+// the entire input as host, and it doesn't check the validity of the host.
1034+// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
1035+// *** taken from net/url, modified validOptionalPort() to accept ":*"
1036+func splitHostPort(hostport string) (host, port string) {
1037+ host = hostport
1038+
1039+ colon := strings.LastIndexByte(host, ':')
1040+ if colon != -1 && validOptionalPort(host[colon:]) {
1041+ host, port = host[:colon], host[colon+1:]
1042+ }
1043+
1044+ if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
1045+ host = host[1 : len(host)-1]
1046+ }
1047+
1048+ return
1049+}
1050+
1051+// validOptionalPort reports whether port is either an empty string
1052+// or matches /^:\d*$/
1053+// *** taken from net/url, modified to accept ":*"
1054+func validOptionalPort(port string) bool {
1055+ if port == "" || port == ":*" {
1056+ return true
1057+ }
1058+ if port[0] != ':' {
1059+ return false
1060+ }
1061+ for _, b := range port[1:] {
1062+ if b < '0' || b > '9' {
1063+ return false
1064+ }
1065+ }
1066+ return true
9811067}
9821068
9831069// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
@@ -989,7 +1075,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
9891075 var session *sessionsapi.SessionState
9901076
9911077 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
992− session = middleware.GetRequestScope(req).Session
1078+ session = middlewareapi.GetRequestScope(req).Session
9931079 }))
9941080 getSession.ServeHTTP(rw, req)
9951081
@@ -1016,8 +1102,55 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
10161102 return session, nil
10171103}
10181104
1105+// authOnlyAuthorize handles special authorization logic that is only done
1106+// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1107+//
1108+// TODO (@NickMeves): This method is a placeholder to be extended but currently
1109+// fails the linter. Remove the nolint when functionality expands.
1110+//
1111+//nolint:gosimple
1112+func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1113+ // Allow secondary group restrictions based on the `allowed_groups`
1114+ // querystring parameter
1115+ if !checkAllowedGroups(req, s) {
1116+ return false
1117+ }
1118+
1119+ return true
1120+}
1121+
1122+func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1123+ allowedGroups := extractAllowedGroups(req)
1124+ if len(allowedGroups) == 0 {
1125+ return true
1126+ }
1127+
1128+ for _, group := range s.Groups {
1129+ if _, ok := allowedGroups[group]; ok {
1130+ return true
1131+ }
1132+ }
1133+
1134+ return false
1135+}
1136+
1137+func extractAllowedGroups(req *http.Request) map[string]struct{} {
1138+ groups := map[string]struct{}{}
1139+
1140+ query := req.URL.Query()
1141+ for _, allowedGroups := range query["allowed_groups"] {
1142+ for _, group := range strings.Split(allowedGroups, ",") {
1143+ if group != "" {
1144+ groups[group] = struct{}{}
1145+ }
1146+ }
1147+ }
1148+
1149+ return groups
1150+}
1151+
10191152// addHeadersForProxying adds the appropriate headers the request / response for proxying
1020−func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1153+func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
10211154 if session.Email == "" {
10221155 rw.Header().Set("GAP-Auth", session.User)
10231156 } else {
@@ -1029,16 +1162,24 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
10291162func isAjax(req *http.Request) bool {
10301163 acceptValues := req.Header.Values("Accept")
10311164 const ajaxReq = applicationJSON
1032− for _, v := range acceptValues {
1033− if v == ajaxReq {
1034− return true
1165+ // Iterate over multiple Accept headers, i.e.
1166+ // Accept: application/json
1167+ // Accept: text/plain
1168+ for _, mimeTypes := range acceptValues {
1169+ // Iterate over multiple mimetypes in a single header, i.e.
1170+ // Accept: application/json, text/plain, */*
1171+ for _, mimeType := range strings.Split(mimeTypes, ",") {
1172+ mimeType = strings.TrimSpace(mimeType)
1173+ if mimeType == ajaxReq {
1174+ return true
1175+ }
10351176 }
10361177 }
10371178 return false
10381179}
10391180
1040−// ErrorJSON returns the error code with an application/json mime type
1041−func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1181+// errorJSON returns the error code with an application/json mime type
1182+func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
10421183 rw.Header().Set("Content-Type", applicationJSON)
10431184 rw.WriteHeader(code)
10441185}
oauthproxy_test.go+238−36
@@ -19,6 +19,7 @@ import (
1919
2020 "github.com/coreos/go-oidc"
2121 "github.com/mbland/hmacauth"
22+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
2223 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
2324 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
2425 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@@ -297,6 +298,11 @@ func TestIsValidRedirect(t *testing.T) {
297298 Redirect: "/\t/\t\\evil.com",
298299 ExpectedResult: false,
299300 },
301+ {
302+ Desc: "openRedirectPartialSubdomain",
303+ Redirect: "http://evilbar.foo",
304+ ExpectedResult: false,
305+ },
300306 }
301307
302308 for _, tc := range testCases {
@@ -414,8 +420,9 @@ func Test_redeemCode(t *testing.T) {
414420 t.Fatal(err)
415421 }
416422
417− _, err = proxy.redeemCode(context.Background(), "www.example.com", "")
418− assert.Error(t, err)
423+ req := httptest.NewRequest(http.MethodGet, "/", nil)
424+ _, err = proxy.redeemCode(req)
425+ assert.Equal(t, providers.ErrMissingCode, err)
419426}
420427
421428func Test_enrichSession(t *testing.T) {
@@ -515,7 +522,7 @@ func TestBasicAuthPassword(t *testing.T) {
515522 ClaimSource: &options.ClaimSource{
516523 Claim: "email",
517524 BasicAuthPassword: &options.SecretSource{
518− Value: []byte(base64.StdEncoding.EncodeToString([]byte(basicAuthPassword))),
525+ Value: []byte(basicAuthPassword),
519526 },
520527 },
521528 },
@@ -610,7 +617,7 @@ func TestPassGroupsHeadersWithGroups(t *testing.T) {
610617 rw = httptest.NewRecorder()
611618 proxy.ServeHTTP(rw, req)
612619
613− assert.Equal(t, groups, req.Header["X-Forwarded-Groups"])
620+ assert.Equal(t, []string{"a,b"}, req.Header["X-Forwarded-Groups"])
614621}
615622
616623type PassAccessTokenTest struct {
@@ -1197,18 +1204,20 @@ func TestUserInfoEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
11971204 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
11981205}
11991206
1200−func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
1207+func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
12011208 pcTest, err := NewProcessCookieTestWithOptionsModifiers(modifiers...)
12021209 if err != nil {
12031210 return nil, err
12041211 }
1205− pcTest.req, _ = http.NewRequest("GET",
1206− pcTest.opts.ProxyPrefix+"/auth", nil)
1212+ pcTest.req, _ = http.NewRequest(
1213+ "GET",
1214+ fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
1215+ nil)
12071216 return pcTest, nil
12081217}
12091218
12101219func TestAuthOnlyEndpointAccepted(t *testing.T) {
1211− test, err := NewAuthOnlyEndpointTest()
1220+ test, err := NewAuthOnlyEndpointTest("")
12121221 if err != nil {
12131222 t.Fatal(err)
12141223 }
@@ -1226,7 +1235,7 @@ func TestAuthOnlyEndpointAccepted(t *testing.T) {
12261235}
12271236
12281237func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1229− test, err := NewAuthOnlyEndpointTest()
1238+ test, err := NewAuthOnlyEndpointTest("")
12301239 if err != nil {
12311240 t.Fatal(err)
12321241 }
@@ -1234,11 +1243,11 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
12341243 test.proxy.ServeHTTP(test.rw, test.req)
12351244 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
12361245 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
1237− assert.Equal(t, "unauthorized request\n", string(bodyBytes))
1246+ assert.Equal(t, "Unauthorized\n", string(bodyBytes))
12381247}
12391248
12401249func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1241− test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
1250+ test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
12421251 opts.Cookie.Expire = time.Duration(24) * time.Hour
12431252 })
12441253 if err != nil {
@@ -1254,11 +1263,11 @@ func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
12541263 test.proxy.ServeHTTP(test.rw, test.req)
12551264 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
12561265 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
1257− assert.Equal(t, "unauthorized request\n", string(bodyBytes))
1266+ assert.Equal(t, "Unauthorized\n", string(bodyBytes))
12581267}
12591268
12601269func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
1261− test, err := NewAuthOnlyEndpointTest()
1270+ test, err := NewAuthOnlyEndpointTest("")
12621271 if err != nil {
12631272 t.Fatal(err)
12641273 }
@@ -1273,7 +1282,7 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
12731282 test.proxy.ServeHTTP(test.rw, test.req)
12741283 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
12751284 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
1276− assert.Equal(t, "unauthorized request\n", string(bodyBytes))
1285+ assert.Equal(t, "Unauthorized\n", string(bodyBytes))
12771286}
12781287
12791288func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
@@ -1408,7 +1417,7 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
14081417 ClaimSource: &options.ClaimSource{
14091418 Claim: "user",
14101419 BasicAuthPassword: &options.SecretSource{
1411− Value: []byte(base64.StdEncoding.EncodeToString([]byte("This is a secure password"))),
1420+ Value: []byte("This is a secure password"),
14121421 },
14131422 },
14141423 },
@@ -1746,8 +1755,9 @@ func TestRequestSignature(t *testing.T) {
17461755 }
17471756}
17481757
1749−func TestGetRedirect(t *testing.T) {
1758+func Test_getAppRedirect(t *testing.T) {
17501759 opts := baseTestOptions()
1760+ opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com", ".example.com:8443")
17511761 err := validation.Validate(opts)
17521762 assert.NoError(t, err)
17531763 require.NotEmpty(t, opts.ProxyPrefix)
@@ -1759,28 +1769,144 @@ func TestGetRedirect(t *testing.T) {
17591769 tests := []struct {
17601770 name string
17611771 url string
1772+ headers map[string]string
1773+ reverseProxy bool
17621774 expectedRedirect string
17631775 }{
17641776 {
17651777 name: "request outside of ProxyPrefix redirects to original URL",
17661778 url: "/foo/bar",
1779+ headers: nil,
1780+ reverseProxy: false,
17671781 expectedRedirect: "/foo/bar",
17681782 },
17691783 {
17701784 name: "request with query preserves query",
17711785 url: "/foo?bar",
1786+ headers: nil,
1787+ reverseProxy: false,
17721788 expectedRedirect: "/foo?bar",
17731789 },
17741790 {
17751791 name: "request under ProxyPrefix redirects to root",
17761792 url: proxy.ProxyPrefix + "/foo/bar",
1793+ headers: nil,
1794+ reverseProxy: false,
17771795 expectedRedirect: "/",
17781796 },
1797+ {
1798+ name: "proxied request outside of ProxyPrefix redirects to proxied URL",
1799+ url: "https://oauth.example.com/foo/bar",
1800+ headers: map[string]string{
1801+ "X-Forwarded-Proto": "https",
1802+ "X-Forwarded-Host": "a-service.example.com",
1803+ "X-Forwarded-Uri": "/foo/bar",
1804+ },
1805+ reverseProxy: true,
1806+ expectedRedirect: "https://a-service.example.com/foo/bar",
1807+ },
1808+ {
1809+ name: "non-proxied request with spoofed proxy headers wouldn't redirect",
1810+ url: "https://oauth.example.com/foo?bar",
1811+ headers: map[string]string{
1812+ "X-Forwarded-Proto": "https",
1813+ "X-Forwarded-Host": "a-service.example.com",
1814+ "X-Forwarded-Uri": "/foo/bar",
1815+ },
1816+ reverseProxy: false,
1817+ expectedRedirect: "/foo?bar",
1818+ },
1819+ {
1820+ name: "proxied request under ProxyPrefix redirects to root",
1821+ url: "https://oauth.example.com" + proxy.ProxyPrefix + "/foo/bar",
1822+ headers: map[string]string{
1823+ "X-Forwarded-Proto": "https",
1824+ "X-Forwarded-Host": "a-service.example.com",
1825+ "X-Forwarded-Uri": proxy.ProxyPrefix + "/foo/bar",
1826+ },
1827+ reverseProxy: true,
1828+ expectedRedirect: "https://a-service.example.com/",
1829+ },
1830+ {
1831+ name: "proxied request with port under ProxyPrefix redirects to root",
1832+ url: "https://oauth.example.com" + proxy.ProxyPrefix + "/foo/bar",
1833+ headers: map[string]string{
1834+ "X-Forwarded-Proto": "https",
1835+ "X-Forwarded-Host": "a-service.example.com:8443",
1836+ "X-Forwarded-Uri": proxy.ProxyPrefix + "/foo/bar",
1837+ },
1838+ reverseProxy: true,
1839+ expectedRedirect: "https://a-service.example.com:8443/",
1840+ },
1841+ {
1842+ name: "proxied request with missing uri header would still redirect to desired redirect",
1843+ url: "https://oauth.example.com/foo?bar",
1844+ headers: map[string]string{
1845+ "X-Forwarded-Proto": "https",
1846+ "X-Forwarded-Host": "a-service.example.com",
1847+ },
1848+ reverseProxy: true,
1849+ expectedRedirect: "https://a-service.example.com/foo?bar",
1850+ },
1851+ {
1852+ name: "request with headers proxy not being set (and reverse proxy enabled) would still redirect to desired redirect",
1853+ url: "https://oauth.example.com/foo?bar",
1854+ headers: nil,
1855+ reverseProxy: true,
1856+ expectedRedirect: "/foo?bar",
1857+ },
1858+ {
1859+ name: "proxied request with X-Auth-Request-Redirect being set outside of ProxyPrefix redirects to proxied URL",
1860+ url: "https://oauth.example.com/foo/bar",
1861+ headers: map[string]string{
1862+ "X-Auth-Request-Redirect": "https://a-service.example.com/foo/bar",
1863+ },
1864+ reverseProxy: true,
1865+ expectedRedirect: "https://a-service.example.com/foo/bar",
1866+ },
1867+ {
1868+ name: "proxied request with rd query string redirects to proxied URL",
1869+ url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbar",
1870+ headers: nil,
1871+ reverseProxy: false,
1872+ expectedRedirect: "https://a-service.example.com/foo/bar",
1873+ },
1874+ {
1875+ name: "proxied request with rd query string and all headers set (and reverse proxy not enabled) redirects to proxied URL on rd query string",
1876+ url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fjazz",
1877+ headers: map[string]string{
1878+ "X-Auth-Request-Redirect": "https://a-service.example.com/foo/baz",
1879+ "X-Forwarded-Proto": "http",
1880+ "X-Forwarded-Host": "another-service.example.com",
1881+ "X-Forwarded-Uri": "/seasons/greetings",
1882+ },
1883+ reverseProxy: false,
1884+ expectedRedirect: "https://a-service.example.com/foo/jazz",
1885+ },
1886+ {
1887+ name: "proxied request with rd query string and some headers set redirects to proxied URL on rd query string",
1888+ url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbaz",
1889+ headers: map[string]string{
1890+ "X-Forwarded-Proto": "https",
1891+ "X-Forwarded-Host": "another-service.example.com",
1892+ "X-Forwarded-Uri": "/seasons/greetings",
1893+ },
1894+ reverseProxy: true,
1895+ expectedRedirect: "https://a-service.example.com/foo/baz",
1896+ },
17791897 }
17801898 for _, tt := range tests {
17811899 t.Run(tt.name, func(t *testing.T) {
17821900 req, _ := http.NewRequest("GET", tt.url, nil)
1783− redirect, err := proxy.GetRedirect(req)
1901+ for header, value := range tt.headers {
1902+ if value != "" {
1903+ req.Header.Add(header, value)
1904+ }
1905+ }
1906+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
1907+ ReverseProxy: tt.reverseProxy,
1908+ })
1909+ redirect, err := proxy.getAppRedirect(req)
17841910
17851911 assert.NoError(t, err)
17861912 assert.Equal(t, tt.expectedRedirect, redirect)
@@ -1848,6 +1974,13 @@ func TestAjaxUnauthorizedRequest2(t *testing.T) {
18481974 testAjaxUnauthorizedRequest(t, header)
18491975}
18501976
1977+func TestAjaxUnauthorizedRequestAccept1(t *testing.T) {
1978+ header := make(http.Header)
1979+ header.Add("Accept", "application/json, text/plain, */*")
1980+
1981+ testAjaxUnauthorizedRequest(t, header)
1982+}
1983+
18511984func TestAjaxForbiddendRequest(t *testing.T) {
18521985 test, err := newAjaxRequestTest()
18531986 if err != nil {
@@ -1960,7 +2093,7 @@ func TestGetJwtSession(t *testing.T) {
19602093 verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
19612094 &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
19622095
1963− test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
2096+ test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
19642097 opts.InjectRequestHeaders = []options.Header{
19652098 {
19662099 Name: "Authorization",
@@ -2028,7 +2161,6 @@ func TestGetJwtSession(t *testing.T) {
20282161 },
20292162 },
20302163 }
2031−
20322164 opts.SkipJwtBearerTokens = true
20332165 opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), verifier))
20342166 })
@@ -2683,7 +2815,7 @@ func TestProxyAllowedGroups(t *testing.T) {
26832815 test.proxy.ServeHTTP(test.rw, test.req)
26842816
26852817 if tt.expectUnauthorized {
2686− assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
2818+ assert.Equal(t, http.StatusForbidden, test.rw.Code)
26872819 } else {
26882820 assert.Equal(t, http.StatusOK, test.rw.Code)
26892821 }
@@ -2692,32 +2824,106 @@ func TestProxyAllowedGroups(t *testing.T) {
26922824}
26932825
26942826func TestAuthOnlyAllowedGroups(t *testing.T) {
2695− tests := []struct {
2827+ testCases := []struct {
26962828 name string
26972829 allowedGroups []string
26982830 groups []string
2699− expectUnauthorized bool
2831+ querystring string
2832+ expectedStatusCode int
27002833 }{
2701− {"NoAllowedGroups", []string{}, []string{}, false},
2702− {"NoAllowedGroupsUserHasGroups", []string{}, []string{"a", "b"}, false},
2703− {"UserInAllowedGroup", []string{"a"}, []string{"a", "b"}, false},
2704− {"UserNotInAllowedGroup", []string{"a"}, []string{"c"}, true},
2834+ {
2835+ name: "NoAllowedGroups",
2836+ allowedGroups: []string{},
2837+ groups: []string{},
2838+ querystring: "",
2839+ expectedStatusCode: http.StatusAccepted,
2840+ },
2841+ {
2842+ name: "NoAllowedGroupsUserHasGroups",
2843+ allowedGroups: []string{},
2844+ groups: []string{"a", "b"},
2845+ querystring: "",
2846+ expectedStatusCode: http.StatusAccepted,
2847+ },
2848+ {
2849+ name: "UserInAllowedGroup",
2850+ allowedGroups: []string{"a"},
2851+ groups: []string{"a", "b"},
2852+ querystring: "",
2853+ expectedStatusCode: http.StatusAccepted,
2854+ },
2855+ {
2856+ name: "UserNotInAllowedGroup",
2857+ allowedGroups: []string{"a"},
2858+ groups: []string{"c"},
2859+ querystring: "",
2860+ expectedStatusCode: http.StatusUnauthorized,
2861+ },
2862+ {
2863+ name: "UserInQuerystringGroup",
2864+ allowedGroups: []string{"a", "b"},
2865+ groups: []string{"a", "c"},
2866+ querystring: "?allowed_groups=a",
2867+ expectedStatusCode: http.StatusAccepted,
2868+ },
2869+ {
2870+ name: "UserInMultiParamQuerystringGroup",
2871+ allowedGroups: []string{"a", "b"},
2872+ groups: []string{"b"},
2873+ querystring: "?allowed_groups=a&allowed_groups=b,d",
2874+ expectedStatusCode: http.StatusAccepted,
2875+ },
2876+ {
2877+ name: "UserInOnlyQuerystringGroup",
2878+ allowedGroups: []string{},
2879+ groups: []string{"a", "c"},
2880+ querystring: "?allowed_groups=a,b",
2881+ expectedStatusCode: http.StatusAccepted,
2882+ },
2883+ {
2884+ name: "UserInDelimitedQuerystringGroup",
2885+ allowedGroups: []string{"a", "b", "c"},
2886+ groups: []string{"c"},
2887+ querystring: "?allowed_groups=a,c",
2888+ expectedStatusCode: http.StatusAccepted,
2889+ },
2890+ {
2891+ name: "UserNotInQuerystringGroup",
2892+ allowedGroups: []string{},
2893+ groups: []string{"c"},
2894+ querystring: "?allowed_groups=a,b",
2895+ expectedStatusCode: http.StatusForbidden,
2896+ },
2897+ {
2898+ name: "UserInConfigGroupNotInQuerystringGroup",
2899+ allowedGroups: []string{"a", "b", "c"},
2900+ groups: []string{"c"},
2901+ querystring: "?allowed_groups=a,b",
2902+ expectedStatusCode: http.StatusForbidden,
2903+ },
2904+ {
2905+ name: "UserInQuerystringGroupNotInConfigGroup",
2906+ allowedGroups: []string{"a", "b"},
2907+ groups: []string{"c"},
2908+ querystring: "?allowed_groups=b,c",
2909+ expectedStatusCode: http.StatusUnauthorized,
2910+ },
27052911 }
27062912
2707− for _, tt := range tests {
2708− t.Run(tt.name, func(t *testing.T) {
2913+ for _, tc := range testCases {
2914+ t.Run(tc.name, func(t *testing.T) {
27092915 emailAddress := "test"
27102916 created := time.Now()
27112917
27122918 session := &sessions.SessionState{
2713− Groups: tt.groups,
2919+ Groups: tc.groups,
27142920 Email: emailAddress,
27152921 AccessToken: "oauth_token",
27162922 CreatedAt: &created,
27172923 }
27182924
2719− test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
2720− opts.AllowedGroups = tt.allowedGroups
2925+ test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {
2926+ opts.AllowedGroups = tc.allowedGroups
27212927 })
27222928 if err != nil {
27232929 t.Fatal(err)
@@ -2728,11 +2934,7 @@ func TestAuthOnlyAllowedGroups(t *testing.T) {
27282934
27292935 test.proxy.ServeHTTP(test.rw, test.req)
27302936
2731− if tt.expectUnauthorized {
2732− assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
2733− } else {
2734− assert.Equal(t, http.StatusAccepted, test.rw.Code)
2735− }
2937+ assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
27362938 })
27372939 }
27382940}
pkg/apis/middleware/middleware_suite_test.gonew file+20
@@ -0,0 +1,20 @@
1+package middleware_test
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+// TestMiddlewareSuite and related tests are in a *_test package
12+// to prevent circular imports with the `logger` package which uses
13+// this functionality
14+func TestMiddlewareSuite(t *testing.T) {
15+ logger.SetOutput(GinkgoWriter)
16+ logger.SetErrOutput(GinkgoWriter)
17+
18+ RegisterFailHandler(Fail)
19+ RunSpecs(t, "Middleware API")
20+}
pkg/apis/middleware/scope.go+29
@@ -1,13 +1,26 @@
11package middleware
22
33import (
4+ "context"
5+ "net/http"
6+
47 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
58)
69
10+type scopeKey string
11+
12+// RequestScopeKey uses a typed string to reduce likelihood of clashing
13+// with other context keys
14+const RequestScopeKey scopeKey = "request-scope"
15+
716// RequestScope contains information regarding the request that is being made.
817// The RequestScope is used to pass information between different middlewares
918// within the chain.
1019type RequestScope struct {
20+ // ReverseProxy tracks whether OAuth2-Proxy is operating in reverse proxy
21+ // mode and if request `X-Forwarded-*` headers should be trusted
22+ ReverseProxy bool
23+
1124 // Session details the authenticated users information (if it exists).
1225 Session *sessions.SessionState
1326
@@ -22,3 +35,19 @@ type RequestScope struct {
2235 // it was loaded or not.
2336 SessionRevalidated bool
2437}
38+
39+// GetRequestScope returns the current request scope from the given request
40+func GetRequestScope(req *http.Request) *RequestScope {
41+ scope := req.Context().Value(RequestScopeKey)
42+ if scope == nil {
43+ return nil
44+ }
45+
46+ return scope.(*RequestScope)
47+}
48+
49+// AddRequestScope adds a RequestScope to a request
50+func AddRequestScope(req *http.Request, scope *RequestScope) *http.Request {
51+ ctx := context.WithValue(req.Context(), RequestScopeKey, scope)
52+ return req.WithContext(ctx)
53+}
pkg/apis/middleware/scope_test.gonew file+56
@@ -0,0 +1,56 @@
1+package middleware_test
2+
3+import (
4+ "net/http"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+var _ = Describe("Scope Suite", func() {
12+ Context("GetRequestScope", func() {
13+ var request *http.Request
14+
15+ BeforeEach(func() {
16+ var err error
17+ request, err = http.NewRequest("", "http://127.0.0.1/", nil)
18+ Expect(err).ToNot(HaveOccurred())
19+ })
20+
21+ Context("with a scope", func() {
22+ var scope *middleware.RequestScope
23+
24+ BeforeEach(func() {
25+ scope = &middleware.RequestScope{}
26+ request = middleware.AddRequestScope(request, scope)
27+ })
28+
29+ It("returns the scope", func() {
30+ s := middleware.GetRequestScope(request)
31+ Expect(s).ToNot(BeNil())
32+ Expect(s).To(Equal(scope))
33+ })
34+
35+ Context("if the scope is then modified", func() {
36+ BeforeEach(func() {
37+ Expect(scope.SaveSession).To(BeFalse())
38+ scope.SaveSession = true
39+ })
40+
41+ It("returns the updated session", func() {
42+ s := middleware.GetRequestScope(request)
43+ Expect(s).ToNot(BeNil())
44+ Expect(s).To(Equal(scope))
45+ Expect(s.SaveSession).To(BeTrue())
46+ })
47+ })
48+ })
49+
50+ Context("without a scope", func() {
51+ It("returns nil", func() {
52+ Expect(middleware.GetRequestScope(request)).To(BeNil())
53+ })
54+ })
55+ })
56+})
pkg/apis/options/alpha_options.go+16
@@ -29,3 +29,19 @@ type AlphaOptions struct {
2929 // or from a static secret value.
3030 InjectResponseHeaders []Header `json:"injectResponseHeaders,omitempty"`
3131}
32+
33+// MergeInto replaces alpha options in the Options struct with the values
34+// from the AlphaOptions
35+func (a *AlphaOptions) MergeInto(opts *Options) {
36+ opts.UpstreamServers = a.Upstreams
37+ opts.InjectRequestHeaders = a.InjectRequestHeaders
38+ opts.InjectResponseHeaders = a.InjectResponseHeaders
39+}
40+
41+// ExtractFrom populates the fields in the AlphaOptions with the values from
42+// the Options
43+func (a *AlphaOptions) ExtractFrom(opts *Options) {
44+ a.Upstreams = opts.UpstreamServers
45+ a.InjectRequestHeaders = opts.InjectRequestHeaders
46+ a.InjectResponseHeaders = opts.InjectResponseHeaders
47+}
pkg/apis/options/app.gonew file+50
@@ -0,0 +1,50 @@
1+package options
2+
3+import "github.com/spf13/pflag"
4+
5+// Templates includes options for configuring the sign in and error pages
6+// appearance.
7+type Templates struct {
8+ // Path is the path to a folder containing a sign_in.html and an error.html
9+ // template.
10+ // These files will be used instead of the default templates if present.
11+ // If either file is missing, the default will be used instead.
12+ Path string `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
13+
14+ // Banner overides the default sign_in page banner text. If unspecified,
15+ // the message will give users a list of allowed email domains.
16+ Banner string `flag:"banner" cfg:"banner"`
17+
18+ // Footer overrides the default sign_in page footer text.
19+ Footer string `flag:"footer" cfg:"footer"`
20+
21+ // DisplayLoginForm determines whether the sign_in page should render a
22+ // password form if a static passwords file (htpasswd file) has been
23+ // configured.
24+ DisplayLoginForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
25+
26+ // Debug renders detailed errors when an error page is shown.
27+ // It is not advised to use this in production as errors may contain sensitive
28+ // information.
29+ // Use only for diagnosing backend errors.
30+ Debug bool `flag:"show-debug-on-error" cfg:"show-debug-on-error"`
31+}
32+
33+func templatesFlagSet() *pflag.FlagSet {
34+ flagSet := pflag.NewFlagSet("templates", pflag.ExitOnError)
35+
36+ flagSet.String("custom-templates-dir", "", "path to custom html templates")
37+ flagSet.String("banner", "", "custom banner string. Use \"-\" to disable default banner.")
38+ flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
39+ flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided")
40+ flagSet.Bool("show-debug-on-error", false, "show detailed error information on error pages (WARNING: this may contain sensitive information - do not use in production)")
41+
42+ return flagSet
43+}
44+
45+// templatesDefaults creates a Templates and populates it with any default values
46+func templatesDefaults() Templates {
47+ return Templates{
48+ DisplayLoginForm: true,
49+ }
50+}
pkg/apis/options/common.go+1
@@ -28,6 +28,7 @@ type SecretSource struct {
2828// A duration string is a is a possibly signed sequence of decimal numbers,
2929// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".
3030// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
31+// +reference-gen:alias-name=string
3132type Duration time.Duration
3233
3334// UnmarshalJSON parses the duration string and sets the value of duration
pkg/apis/options/doc.gonew file+2
@@ -0,0 +1,2 @@
1+//go:generate go run github.com/oauth2-proxy/tools/reference-gen/cmd/reference-gen --package github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options --types AlphaOptions --header-file ../../../docs/docs/configuration/alpha_config.md.tmpl --out-file ../../../docs/docs/configuration/alpha_config.md
2+package options
pkg/apis/options/legacy_options.go+10−2
@@ -1,7 +1,6 @@
11package options
22
33import (
4− "encoding/base64"
54 "fmt"
65 "net/url"
76 "strconv"
@@ -40,6 +39,15 @@ func NewLegacyOptions() *LegacyOptions {
4039 }
4140}
4241
42+func NewLegacyFlagSet() *pflag.FlagSet {
43+ flagSet := NewFlagSet()
44+
45+ flagSet.AddFlagSet(legacyUpstreamsFlagSet())
46+ flagSet.AddFlagSet(legacyHeadersFlagSet())
47+
48+ return flagSet
49+}
50+
4351func (l *LegacyOptions) ToOptions() (*Options, error) {
4452 upstreams, err := l.LegacyUpstreams.convert()
4553 if err != nil {
@@ -235,7 +243,7 @@ func getBasicAuthHeader(preferEmailToUser bool, basicAuthPassword string) Header
235243 Claim: claim,
236244 Prefix: "Basic ",
237245 BasicAuthPassword: &SecretSource{
238− Value: []byte(base64.StdEncoding.EncodeToString([]byte(basicAuthPassword))),
246+ Value: []byte(basicAuthPassword),
239247 },
240248 },
241249 },
pkg/apis/options/legacy_options_test.go+2−3
@@ -1,7 +1,6 @@
11package options
22
33import (
4− "encoding/base64"
54 "time"
65
76 . "github.com/onsi/ginkgo"
@@ -332,7 +331,7 @@ var _ = Describe("Legacy Options", func() {
332331 Claim: "user",
333332 Prefix: "Basic ",
334333 BasicAuthPassword: &SecretSource{
335− Value: []byte(base64.StdEncoding.EncodeToString([]byte(basicAuthSecret))),
334+ Value: []byte(basicAuthSecret),
336335 },
337336 },
338337 },
@@ -372,7 +371,7 @@ var _ = Describe("Legacy Options", func() {
372371 Claim: "email",
373372 Prefix: "Basic ",
374373 BasicAuthPassword: &SecretSource{
375− Value: []byte(base64.StdEncoding.EncodeToString([]byte(basicAuthSecret))),
374+ Value: []byte(basicAuthSecret),
376375 },
377376 },
378377 },
pkg/apis/options/load.go+28
@@ -1,10 +1,13 @@
11package options
22
33import (
4+ "errors"
45 "fmt"
6+ "io/ioutil"
57 "reflect"
68 "strings"
79
10+ "github.com/ghodss/yaml"
811 "github.com/mitchellh/mapstructure"
912 "github.com/spf13/pflag"
1013 "github.com/spf13/viper"
@@ -132,3 +135,28 @@ func isUnexported(name string) bool {
132135 first := string(name[0])
133136 return first == strings.ToLower(first)
134137}
138+
139+// LoadYAML will load a YAML based configuration file into the options interface provided.
140+func LoadYAML(configFileName string, into interface{}) error {
141+ v := viper.New()
142+ v.SetConfigFile(configFileName)
143+ v.SetConfigType("yaml")
144+ v.SetTypeByDefaultValue(true)
145+
146+ if configFileName == "" {
147+ return errors.New("no configuration file provided")
148+ }
149+
150+ data, err := ioutil.ReadFile(configFileName)
151+ if err != nil {
152+ return fmt.Errorf("unable to load config file: %w", err)
153+ }
154+
155+ // UnmarshalStrict will return an error if the config includes options that are
156+ // not mapped to felds of the into struct
157+ if err := yaml.UnmarshalStrict(data, into, yaml.DisallowUnknownFields); err != nil {
158+ return fmt.Errorf("error unmarshalling config: %w", err)
159+ }
160+
161+ return nil
162+}
pkg/apis/options/load_test.go+192−1
@@ -1,9 +1,11 @@
11package options
22
33import (
4+ "errors"
45 "fmt"
56 "io/ioutil"
67 "os"
8+ "time"
79
810 . "github.com/onsi/ginkgo"
911 . "github.com/onsi/ginkgo/extensions/table"
@@ -295,10 +297,199 @@ var _ = Describe("Load", func() {
295297 expectedOutput: NewOptions(),
296298 }),
297299 Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{
298− flagSet: NewFlagSet,
300+ flagSet: NewLegacyFlagSet,
299301 input: &LegacyOptions{},
300302 expectedOutput: NewLegacyOptions(),
301303 }),
302304 )
303305 })
304306})
307+
308+var _ = Describe("LoadYAML", func() {
309+ Context("with a testOptions structure", func() {
310+ type TestOptionSubStruct struct {
311+ StringSliceOption []string `yaml:"stringSliceOption,omitempty"`
312+ }
313+
314+ type TestOptions struct {
315+ StringOption string `yaml:"stringOption,omitempty"`
316+ Sub TestOptionSubStruct `yaml:"sub,omitempty"`
317+
318+ // Check that embedded fields can be unmarshalled
319+ TestOptionSubStruct `yaml:",inline,squash"`
320+ }
321+
322+ var testOptionsConfigBytesFull = []byte(`
323+stringOption: foo
324+stringSliceOption:
325+- a
326+- b
327+- c
328+sub:
329+ stringSliceOption:
330+ - d
331+ - e
332+`)
333+
334+ type loadYAMLTableInput struct {
335+ configFile []byte
336+ input interface{}
337+ expectedErr error
338+ expectedOutput interface{}
339+ }
340+
341+ DescribeTable("LoadYAML",
342+ func(in loadYAMLTableInput) {
343+ var configFileName string
344+
345+ if in.configFile != nil {
346+ By("Creating a config file")
347+ configFile, err := ioutil.TempFile("", "oauth2-proxy-test-config-file")
348+ Expect(err).ToNot(HaveOccurred())
349+ defer configFile.Close()
350+
351+ _, err = configFile.Write(in.configFile)
352+ Expect(err).ToNot(HaveOccurred())
353+ defer os.Remove(configFile.Name())
354+
355+ configFileName = configFile.Name()
356+ }
357+
358+ var input interface{}
359+ if in.input != nil {
360+ input = in.input
361+ } else {
362+ input = &TestOptions{}
363+ }
364+ err := LoadYAML(configFileName, input)
365+ if in.expectedErr != nil {
366+ Expect(err).To(MatchError(in.expectedErr.Error()))
367+ } else {
368+ Expect(err).ToNot(HaveOccurred())
369+ }
370+ Expect(input).To(Equal(in.expectedOutput))
371+ },
372+ Entry("with a valid input", loadYAMLTableInput{
373+ configFile: testOptionsConfigBytesFull,
374+ input: &TestOptions{},
375+ expectedOutput: &TestOptions{
376+ StringOption: "foo",
377+ Sub: TestOptionSubStruct{
378+ StringSliceOption: []string{"d", "e"},
379+ },
380+ TestOptionSubStruct: TestOptionSubStruct{
381+ StringSliceOption: []string{"a", "b", "c"},
382+ },
383+ },
384+ }),
385+ Entry("with no config file", loadYAMLTableInput{
386+ configFile: nil,
387+ input: &TestOptions{},
388+ expectedOutput: &TestOptions{},
389+ expectedErr: errors.New("no configuration file provided"),
390+ }),
391+ Entry("with invalid YAML", loadYAMLTableInput{
392+ configFile: []byte("\tfoo: bar"),
393+ input: &TestOptions{},
394+ expectedOutput: &TestOptions{},
395+ expectedErr: errors.New("error unmarshalling config: error converting YAML to JSON: yaml: found character that cannot start any token"),
396+ }),
397+ Entry("with extra fields in the YAML", loadYAMLTableInput{
398+ configFile: append(testOptionsConfigBytesFull, []byte("foo: bar\n")...),
399+ input: &TestOptions{},
400+ expectedOutput: &TestOptions{
401+ StringOption: "foo",
402+ Sub: TestOptionSubStruct{
403+ StringSliceOption: []string{"d", "e"},
404+ },
405+ TestOptionSubStruct: TestOptionSubStruct{
406+ StringSliceOption: []string{"a", "b", "c"},
407+ },
408+ },
409+ expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""),
410+ }),
411+ Entry("with an incorrect type for a string field", loadYAMLTableInput{
412+ configFile: []byte(`stringOption: ["a", "b"]`),
413+ input: &TestOptions{},
414+ expectedOutput: &TestOptions{},
415+ expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go struct field TestOptions.StringOption of type string"),
416+ }),
417+ Entry("with an incorrect type for an array field", loadYAMLTableInput{
418+ configFile: []byte(`stringSliceOption: "a"`),
419+ input: &TestOptions{},
420+ expectedOutput: &TestOptions{},
421+ expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field TestOptions.StringSliceOption of type []string"),
422+ }),
423+ )
424+ })
425+
426+ It("should load a full example AlphaOptions", func() {
427+ config := []byte(`
428+upstreams:
429+- id: httpbin
430+ path: /
431+ uri: http://httpbin
432+ flushInterval: 500ms
433+injectRequestHeaders:
434+- name: X-Forwarded-User
435+ values:
436+ - claim: user
437+injectResponseHeaders:
438+- name: X-Secret
439+ values:
440+ - value: c2VjcmV0
441+`)
442+
443+ By("Creating a config file")
444+ configFile, err := ioutil.TempFile("", "oauth2-proxy-test-alpha-config-file")
445+ Expect(err).ToNot(HaveOccurred())
446+ defer configFile.Close()
447+
448+ _, err = configFile.Write(config)
449+ Expect(err).ToNot(HaveOccurred())
450+ defer os.Remove(configFile.Name())
451+
452+ configFileName := configFile.Name()
453+
454+ By("Loading the example config")
455+ into := &AlphaOptions{}
456+ Expect(LoadYAML(configFileName, into)).To(Succeed())
457+
458+ flushInterval := Duration(500 * time.Millisecond)
459+
460+ Expect(into).To(Equal(&AlphaOptions{
461+ Upstreams: []Upstream{
462+ {
463+ ID: "httpbin",
464+ Path: "/",
465+ URI: "http://httpbin",
466+ FlushInterval: &flushInterval,
467+ },
468+ },
469+ InjectRequestHeaders: []Header{
470+ {
471+ Name: "X-Forwarded-User",
472+ Values: []HeaderValue{
473+ {
474+ ClaimSource: &ClaimSource{
475+ Claim: "user",
476+ },
477+ },
478+ },
479+ },
480+ },
481+ InjectResponseHeaders: []Header{
482+ {
483+ Name: "X-Secret",
484+ Values: []HeaderValue{
485+ {
486+ SecretSource: &SecretSource{
487+ Value: []byte("secret"),
488+ },
489+ },
490+ },
491+ },
492+ },
493+ }))
494+ })
495+})
pkg/apis/options/options.go+20−20
@@ -22,6 +22,7 @@ type Options struct {
2222 ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
2323 PingPath string `flag:"ping-path" cfg:"ping_path"`
2424 PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
25+ MetricsAddress string `flag:"metrics-address" cfg:"metrics_address"`
2526 HTTPAddress string `flag:"http-address" cfg:"http_address"`
2627 HTTPSAddress string `flag:"https-address" cfg:"https_address"`
2728 ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
@@ -36,7 +37,7 @@ type Options struct {
3637 TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
3738
3839 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
39− KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"`
40+ KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
4041 AtlassianGroups []string `flag:"atlassian-group" cfg:"atlassian_groups"`
4142 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
4243 BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
@@ -49,18 +50,16 @@ type Options struct {
4950 GitHubToken string `flag:"github-token" cfg:"github_token"`
5051 GitHubUsers []string `flag:"github-user" cfg:"github_users"`
5152 GitLabGroup []string `flag:"gitlab-group" cfg:"gitlab_groups"`
53+ GitlabProjects []string `flag:"gitlab-project" cfg:"gitlab_projects"`
5254 GoogleGroups []string `flag:"google-group" cfg:"google_group"`
5355 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
5456 GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
5557 HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
56− DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
57− CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
58− Banner string `flag:"banner" cfg:"banner"`
59− Footer string `flag:"footer" cfg:"footer"`
6058
61− Cookie Cookie `cfg:",squash"`
62− Session SessionOptions `cfg:",squash"`
63− Logging Logging `cfg:",squash"`
59+ Cookie Cookie `cfg:",squash"`
60+ Session SessionOptions `cfg:",squash"`
61+ Logging Logging `cfg:",squash"`
62+ Templates Templates `cfg:",squash"`
6463
6564 // Not used in the legacy config, name not allowed to match an external key (upstreams)
6665 // TODO(JoelSpeed): Rename when legacy config is removed
@@ -87,6 +86,7 @@ type Options struct {
8786 InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"`
8887 SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"`
8988 OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"`
89+ OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"`
9090 OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"`
9191 LoginURL string `flag:"login-url" cfg:"login_url"`
9292 RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
@@ -136,23 +136,25 @@ func NewOptions() *Options {
136136 return &Options{
137137 ProxyPrefix: "/oauth2",
138138 ProviderType: "google",
139+ MetricsAddress: "",
139140 PingPath: "/ping",
140141 HTTPAddress: "127.0.0.1:4180",
141142 HTTPSAddress: ":443",
142143 RealClientIPHeader: "X-Real-IP",
143144 ForceHTTPS: false,
144− DisplayHtpasswdForm: true,
145145 Cookie: cookieDefaults(),
146146 Session: sessionOptionsDefaults(),
147+ Templates: templatesDefaults(),
147148 AzureTenant: "common",
148149 SkipAuthPreflight: false,
149150 Prompt: "", // Change to "login" when ApprovalPrompt officially deprecated
150151 ApprovalPrompt: "force",
151− UserIDClaim: "email",
152152 InsecureOIDCAllowUnverifiedEmail: false,
153153 SkipOIDCDiscovery: false,
154154 Logging: loggingDefaults(),
155− OIDCGroupsClaim: "groups",
155+ UserIDClaim: providers.OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
156+ OIDCEmailClaim: providers.OIDCEmailClaim,
157+ OIDCGroupsClaim: providers.OIDCGroupsClaim,
156158 }
157159}
158160
@@ -179,7 +181,7 @@ func NewFlagSet() *pflag.FlagSet {
179181
180182 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
181183 flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
182− flagSet.String("keycloak-group", "", "restrict login to members of this group.")
184+ flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)")
183185 flagSet.StringSlice("atlassian-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
184186 flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
185187 flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
@@ -190,6 +192,7 @@ func NewFlagSet() *pflag.FlagSet {
190192 flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)")
191193 flagSet.StringSlice("github-user", []string{}, "allow users with these usernames to login even if they do not belong to the specified org and team or collaborators (may be given multiple times)")
192194 flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
195+ flagSet.StringSlice("gitlab-project", []string{}, "restrict logins to members of this project (may be given multiple times) (eg `group/project=accesslevel`). Access level should be a value matching Gitlab access levels (see https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent")
193196 flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
194197 flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
195198 flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
@@ -198,13 +201,10 @@ func NewFlagSet() *pflag.FlagSet {
198201 flagSet.String("client-secret-file", "", "the file with OAuth Client Secret")
199202 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
200203 flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
201− flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided")
202− flagSet.String("custom-templates-dir", "", "path to custom html templates")
203− flagSet.String("banner", "", "custom banner string. Use \"-\" to disable default banner.")
204− flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
205204 flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
206205 flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks")
207206 flagSet.String("ping-user-agent", "", "special User-Agent that will be used for basic health checks")
207+ flagSet.String("metrics-address", "", "the address /metrics will be served on (e.g. \":9100\")")
208208 flagSet.String("session-store-type", "cookie", "the session storage provider to use")
209209 flagSet.Bool("session-cookie-minimal", false, "strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only)")
210210 flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
@@ -226,7 +226,8 @@ func NewFlagSet() *pflag.FlagSet {
226226 flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL")
227227 flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints")
228228 flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
229− flagSet.String("oidc-groups-claim", "groups", "which claim contains the user groups")
229+ flagSet.String("oidc-groups-claim", providers.OIDCGroupsClaim, "which OIDC claim contains the user groups")
230+ flagSet.String("oidc-email-claim", providers.OIDCEmailClaim, "which OIDC claim contains the user's email")
230231 flagSet.String("login-url", "", "Authentication endpoint")
231232 flagSet.String("redeem-url", "", "Token redemption endpoint")
232233 flagSet.String("profile-url", "", "Profile access endpoint")
@@ -243,13 +244,12 @@ func NewFlagSet() *pflag.FlagSet {
243244 flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov")
244245 flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")
245246
246− flagSet.String("user-id-claim", "email", "which claim contains the user ID")
247+ flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID")
247248 flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
248249
249250 flagSet.AddFlagSet(cookieFlagSet())
250251 flagSet.AddFlagSet(loggingFlagSet())
251− flagSet.AddFlagSet(legacyUpstreamsFlagSet())
252− flagSet.AddFlagSet(legacyHeadersFlagSet())
252+ flagSet.AddFlagSet(templatesFlagSet())
253253
254254 return flagSet
255255}
pkg/apis/options/options_suite_test.go+1
@@ -10,6 +10,7 @@ import (
1010
1111func TestOptionsSuite(t *testing.T) {
1212 logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
1314
1415 RegisterFailHandler(Fail)
1516 RunSpecs(t, "Options Suite")
pkg/apis/options/util/util.go+1−4
@@ -1,7 +1,6 @@
11package util
22
33import (
4− "encoding/base64"
54 "errors"
65 "io/ioutil"
76 "os"
@@ -13,9 +12,7 @@ import (
1312func GetSecretValue(source *options.SecretSource) ([]byte, error) {
1413 switch {
1514 case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "":
16− value := make([]byte, base64.StdEncoding.DecodedLen(len(source.Value)))
17− decoded, err := base64.StdEncoding.Decode(value, source.Value)
18− return value[:decoded], err
15+ return source.Value, nil
1916 case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "":
2017 return []byte(os.Getenv(source.FromEnv)), nil
2118 case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "":
pkg/apis/options/util/util_suite_test.go+1
@@ -10,6 +10,7 @@ import (
1010
1111func TestUtilSuite(t *testing.T) {
1212 logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
1314
1415 RegisterFailHandler(Fail)
1516 RunSpecs(t, "Options Util Suite")
pkg/apis/options/util/util_test.go+3−12
@@ -1,7 +1,6 @@
11package util
22
33import (
4− "encoding/base64"
54 "io/ioutil"
65 "os"
76 "path"
@@ -31,20 +30,12 @@ var _ = Describe("GetSecretValue", func() {
3130 os.RemoveAll(fileDir)
3231 })
3332
34− It("returns the correct value from base64", func() {
35− originalValue := []byte("secret-value-1")
36− b64Value := base64.StdEncoding.EncodeToString((originalValue))
37−
38− // Once encoded, the originalValue could have a decoded length longer than
39− // its actual length, ensure we trim this.
40− // This assertion ensures we are testing the triming
41− Expect(len(originalValue)).To(BeNumerically("<", base64.StdEncoding.DecodedLen(len(b64Value))))
42−
33+ It("returns the correct value from the string value", func() {
4334 value, err := GetSecretValue(&options.SecretSource{
44− Value: []byte(b64Value),
35+ Value: []byte("secret-value-1"),
4536 })
4637 Expect(err).ToNot(HaveOccurred())
47− Expect(value).To(Equal(originalValue))
38+ Expect(string(value)).To(Equal("secret-value-1"))
4839 })
4940
5041 It("returns the correct value from the environment", func() {
pkg/app/pagewriter/error_page.gonew file+97
@@ -0,0 +1,97 @@
1+package pagewriter
2+
3+import (
4+ "fmt"
5+ "html/template"
6+ "net/http"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
9+)
10+
11+// errorMessages are default error messages for each of the the different
12+// http status codes expected to be rendered in the error page.
13+var errorMessages = map[int]string{
14+ http.StatusInternalServerError: "Oops! Something went wrong. For more information contact your server administrator.",
15+ http.StatusNotFound: "We could not find the resource you were looking for.",
16+ http.StatusForbidden: "You do not have permission to access this resource.",
17+ http.StatusUnauthorized: "You need to be logged in to access this resource.",
18+}
19+
20+// errorPageWriter is used to render error pages.
21+type errorPageWriter struct {
22+ // template is the error page HTML template.
23+ template *template.Template
24+
25+ // proxyPrefix is the prefix under which OAuth2 Proxy pages are served.
26+ proxyPrefix string
27+
28+ // footer is the footer to be displayed at the bottom of the page.
29+ // If not set, a default footer will be used.
30+ footer string
31+
32+ // version is the OAuth2 Proxy version to be used in the default footer.
33+ version string
34+
35+ // debug determines whether errors pages should be rendered with detailed
36+ // errors.
37+ debug bool
38+}
39+
40+// WriteErrorPage writes an error page to the given response writer.
41+// It uses the passed redirectURL to give users the option to go back to where
42+// they originally came from or try signing in again.
43+func (e *errorPageWriter) WriteErrorPage(rw http.ResponseWriter, status int, redirectURL string, appError string, messages ...interface{}) {
44+ rw.WriteHeader(status)
45+
46+ // We allow unescaped template.HTML since it is user configured options
47+ /* #nosec G203 */
48+ data := struct {
49+ Title string
50+ Message string
51+ ProxyPrefix string
52+ StatusCode int
53+ Redirect string
54+ Footer template.HTML
55+ Version string
56+ }{
57+ Title: http.StatusText(status),
58+ Message: e.getMessage(status, appError, messages...),
59+ ProxyPrefix: e.proxyPrefix,
60+ StatusCode: status,
61+ Redirect: redirectURL,
62+ Footer: template.HTML(e.footer),
63+ Version: e.version,
64+ }
65+
66+ if err := e.template.Execute(rw, data); err != nil {
67+ logger.Printf("Error rendering error template: %v", err)
68+ http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
69+ }
70+}
71+
72+// ProxyErrorHandler is used by the upstream ReverseProxy to render error pages
73+// when there are issues with upstream servers.
74+// It is expected to always render a bad gateway error.
75+func (e *errorPageWriter) ProxyErrorHandler(rw http.ResponseWriter, req *http.Request, proxyErr error) {
76+ logger.Errorf("Error proxying to upstream server: %v", proxyErr)
77+ e.WriteErrorPage(rw, http.StatusBadGateway, "", proxyErr.Error(), "There was a problem connecting to the upstream server.")
78+}
79+
80+// getMessage creates the message for the template parameters.
81+// If the errorPagewriter.Debug is enabled, the application error takes precedence.
82+// Otherwise, any messages will be used.
83+// The first message is expected to be a format string.
84+// If no messages are supplied, a default error message will be used.
85+func (e *errorPageWriter) getMessage(status int, appError string, messages ...interface{}) string {
86+ if e.debug {
87+ return appError
88+ }
89+ if len(messages) > 0 {
90+ format := fmt.Sprintf("%v", messages[0])
91+ return fmt.Sprintf(format, messages[1:]...)
92+ }
93+ if msg, ok := errorMessages[status]; ok {
94+ return msg
95+ }
96+ return "Unknown error"
97+}
pkg/app/pagewriter/error_page_test.gonew file+101
@@ -0,0 +1,101 @@
1+package pagewriter
2+
3+import (
4+ "errors"
5+ "html/template"
6+ "io/ioutil"
7+ "net/http/httptest"
8+
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/gomega"
11+)
12+
13+var _ = Describe("Error Page Writer", func() {
14+ var errorPage *errorPageWriter
15+
16+ BeforeEach(func() {
17+ tmpl, err := template.New("").Parse("{{.Title}} {{.Message}} {{.ProxyPrefix}} {{.StatusCode}} {{.Redirect}} {{.Footer}} {{.Version}}")
18+ Expect(err).ToNot(HaveOccurred())
19+
20+ errorPage = &errorPageWriter{
21+ template: tmpl,
22+ proxyPrefix: "/prefix/",
23+ footer: "Custom Footer Text",
24+ version: "v0.0.0-test",
25+ }
26+ })
27+
28+ Context("WriteErrorPage", func() {
29+ It("Writes the template to the response writer", func() {
30+ recorder := httptest.NewRecorder()
31+ errorPage.WriteErrorPage(recorder, 403, "/redirect", "Access Denied")
32+
33+ body, err := ioutil.ReadAll(recorder.Result().Body)
34+ Expect(err).ToNot(HaveOccurred())
35+ Expect(string(body)).To(Equal("Forbidden You do not have permission to access this resource. /prefix/ 403 /redirect Custom Footer Text v0.0.0-test"))
36+ })
37+
38+ It("With a different code, uses the stock message for the correct code", func() {
39+ recorder := httptest.NewRecorder()
40+ errorPage.WriteErrorPage(recorder, 500, "/redirect", "Access Denied")
41+
42+ body, err := ioutil.ReadAll(recorder.Result().Body)
43+ Expect(err).ToNot(HaveOccurred())
44+ Expect(string(body)).To(Equal("Internal Server Error Oops! Something went wrong. For more information contact your server administrator. /prefix/ 500 /redirect Custom Footer Text v0.0.0-test"))
45+ })
46+
47+ It("With a message override, uses the message", func() {
48+ recorder := httptest.NewRecorder()
49+ errorPage.WriteErrorPage(recorder, 403, "/redirect", "Access Denied", "An extra message: %s", "with more context.")
50+
51+ body, err := ioutil.ReadAll(recorder.Result().Body)
52+ Expect(err).ToNot(HaveOccurred())
53+ Expect(string(body)).To(Equal("Forbidden An extra message: with more context. /prefix/ 403 /redirect Custom Footer Text v0.0.0-test"))
54+ })
55+ })
56+
57+ Context("ProxyErrorHandler", func() {
58+ It("Writes a bad gateway error the response writer", func() {
59+ req := httptest.NewRequest("", "/bad-gateway", nil)
60+ recorder := httptest.NewRecorder()
61+ errorPage.ProxyErrorHandler(recorder, req, errors.New("some upstream error"))
62+
63+ body, err := ioutil.ReadAll(recorder.Result().Body)
64+ Expect(err).ToNot(HaveOccurred())
65+ Expect(string(body)).To(Equal("Bad Gateway There was a problem connecting to the upstream server. /prefix/ 502 Custom Footer Text v0.0.0-test"))
66+ })
67+ })
68+
69+ Context("With Debug enabled", func() {
70+ BeforeEach(func() {
71+ tmpl, err := template.New("").Parse("{{.Message}}")
72+ Expect(err).ToNot(HaveOccurred())
73+
74+ errorPage.template = tmpl
75+ errorPage.debug = true
76+ })
77+
78+ Context("WriteErrorPage", func() {
79+ It("Writes the detailed error in place of the message", func() {
80+ recorder := httptest.NewRecorder()
81+ errorPage.WriteErrorPage(recorder, 403, "/redirect", "Debug error")
82+
83+ body, err := ioutil.ReadAll(recorder.Result().Body)
84+ Expect(err).ToNot(HaveOccurred())
85+ Expect(string(body)).To(Equal("Debug error"))
86+ })
87+ })
88+
89+ Context("ProxyErrorHandler", func() {
90+ It("Writes a bad gateway error the response writer", func() {
91+ req := httptest.NewRequest("", "/bad-gateway", nil)
92+ recorder := httptest.NewRecorder()
93+ errorPage.ProxyErrorHandler(recorder, req, errors.New("some upstream error"))
94+
95+ body, err := ioutil.ReadAll(recorder.Result().Body)
96+ Expect(err).ToNot(HaveOccurred())
97+ Expect(string(body)).To(Equal("some upstream error"))
98+ })
99+ })
100+ })
101+})
pkg/app/pagewriter/pagewriter.gonew file+85
@@ -0,0 +1,85 @@
1+package pagewriter
2+
3+import (
4+ "fmt"
5+ "net/http"
6+)
7+
8+// Writer is an interface for rendering html templates for both sign-in and
9+// error pages.
10+// It can also be used to write errors for the http.ReverseProxy used in the
11+// upstream package.
12+type Writer interface {
13+ WriteSignInPage(rw http.ResponseWriter, redirectURL string)
14+ WriteErrorPage(rw http.ResponseWriter, status int, redirectURL string, appError string, messages ...interface{})
15+ ProxyErrorHandler(rw http.ResponseWriter, req *http.Request, proxyErr error)
16+}
17+
18+// pageWriter implements the Writer interface
19+type pageWriter struct {
20+ *errorPageWriter
21+ *signInPageWriter
22+}
23+
24+// Opts contains all options required to configure the template
25+// rendering within OAuth2 Proxy.
26+type Opts struct {
27+ // TemplatesPath is the path from which to load custom templates for the sign-in and error pages.
28+ TemplatesPath string
29+
30+ // ProxyPrefix is the prefix under which OAuth2 Proxy pages are served.
31+ ProxyPrefix string
32+
33+ // Footer is the footer to be displayed at the bottom of the page.
34+ // If not set, a default footer will be used.
35+ Footer string
36+
37+ // Version is the OAuth2 Proxy version to be used in the default footer.
38+ Version string
39+
40+ // Debug determines whether errors pages should be rendered with detailed
41+ // errors.
42+ Debug bool
43+
44+ // DisplayLoginForm determines whether or not the basic auth password form is displayed on the sign-in page.
45+ DisplayLoginForm bool
46+
47+ // ProviderName is the name of the provider that should be displayed on the login button.
48+ ProviderName string
49+
50+ // SignInMessage is the messge displayed above the login button.
51+ SignInMessage string
52+}
53+
54+// NewWriter constructs a Writer from the options given to allow
55+// rendering of sign-in and error pages.
56+func NewWriter(opts Opts) (Writer, error) {
57+ templates, err := loadTemplates(opts.TemplatesPath)
58+ if err != nil {
59+ return nil, fmt.Errorf("error loading templates: %v", err)
60+ }
61+
62+ errorPage := &errorPageWriter{
63+ template: templates.Lookup("error.html"),
64+ proxyPrefix: opts.ProxyPrefix,
65+ footer: opts.Footer,
66+ version: opts.Version,
67+ debug: opts.Debug,
68+ }
69+
70+ signInPage := &signInPageWriter{
71+ template: templates.Lookup("sign_in.html"),
72+ errorPageWriter: errorPage,
73+ proxyPrefix: opts.ProxyPrefix,
74+ providerName: opts.ProviderName,
75+ signInMessage: opts.SignInMessage,
76+ footer: opts.Footer,
77+ version: opts.Version,
78+ displayLoginForm: opts.DisplayLoginForm,
79+ }
80+
81+ return &pageWriter{
82+ errorPageWriter: errorPage,
83+ signInPageWriter: signInPage,
84+ }, nil
85+}
pkg/app/pagewriter/pagewriter_suite_test.gonew file+17
@@ -0,0 +1,17 @@
1+package pagewriter
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestOptionsSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
14+
15+ RegisterFailHandler(Fail)
16+ RunSpecs(t, "App Suite")
17+}
pkg/app/pagewriter/pagewriter_test.gonew file+126
@@ -0,0 +1,126 @@
1+package pagewriter
2+
3+import (
4+ "io/ioutil"
5+ "net/http/httptest"
6+ "os"
7+ "path/filepath"
8+
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/gomega"
11+)
12+
13+var _ = Describe("Writer", func() {
14+ Context("NewWriter", func() {
15+ var writer Writer
16+ var opts Opts
17+
18+ BeforeEach(func() {
19+ opts = Opts{
20+ TemplatesPath: "",
21+ ProxyPrefix: "/prefix",
22+ Footer: "<Footer>",
23+ Version: "<Version>",
24+ Debug: false,
25+ DisplayLoginForm: false,
26+ ProviderName: "<ProviderName>",
27+ SignInMessage: "<SignInMessage>",
28+ }
29+ })
30+
31+ Context("With no custom templates", func() {
32+ BeforeEach(func() {
33+ var err error
34+ writer, err = NewWriter(opts)
35+ Expect(err).ToNot(HaveOccurred())
36+ })
37+
38+ It("Writes the default error template", func() {
39+ recorder := httptest.NewRecorder()
40+ writer.WriteErrorPage(recorder, 500, "/redirect", "Some debug error")
41+
42+ body, err := ioutil.ReadAll(recorder.Result().Body)
43+ Expect(err).ToNot(HaveOccurred())
44+ Expect(string(body)).To(HavePrefix("\n<!DOCTYPE html>"))
45+ })
46+
47+ It("Writes the default sign in template", func() {
48+ recorder := httptest.NewRecorder()
49+ writer.WriteSignInPage(recorder, "/redirect")
50+
51+ body, err := ioutil.ReadAll(recorder.Result().Body)
52+ Expect(err).ToNot(HaveOccurred())
53+ Expect(string(body)).To(HavePrefix("\n<!DOCTYPE html>"))
54+ })
55+ })
56+
57+ Context("With custom templates", func() {
58+ var customDir string
59+
60+ BeforeEach(func() {
61+ var err error
62+ customDir, err = ioutil.TempDir("", "oauth2-proxy-pagewriter-test")
63+ Expect(err).ToNot(HaveOccurred())
64+
65+ templateHTML := `Custom Template`
66+ signInFile := filepath.Join(customDir, signInTemplateName)
67+ Expect(ioutil.WriteFile(signInFile, []byte(templateHTML), 0600)).To(Succeed())
68+ errorFile := filepath.Join(customDir, errorTemplateName)
69+ Expect(ioutil.WriteFile(errorFile, []byte(templateHTML), 0600)).To(Succeed())
70+
71+ opts.TemplatesPath = customDir
72+
73+ writer, err = NewWriter(opts)
74+ Expect(err).ToNot(HaveOccurred())
75+ })
76+
77+ AfterEach(func() {
78+ Expect(os.RemoveAll(customDir)).To(Succeed())
79+ })
80+
81+ It("Writes the custom error template", func() {
82+ recorder := httptest.NewRecorder()
83+ writer.WriteErrorPage(recorder, 500, "/redirect", "Some debug error")
84+
85+ body, err := ioutil.ReadAll(recorder.Result().Body)
86+ Expect(err).ToNot(HaveOccurred())
87+ Expect(string(body)).To(Equal("Custom Template"))
88+ })
89+
90+ It("Writes the custom sign in template", func() {
91+ recorder := httptest.NewRecorder()
92+ writer.WriteSignInPage(recorder, "/redirect")
93+
94+ body, err := ioutil.ReadAll(recorder.Result().Body)
95+ Expect(err).ToNot(HaveOccurred())
96+ Expect(string(body)).To(Equal("Custom Template"))
97+ })
98+ })
99+
100+ Context("With an invalid custom template", func() {
101+ var customDir string
102+
103+ BeforeEach(func() {
104+ var err error
105+ customDir, err = ioutil.TempDir("", "oauth2-proxy-pagewriter-test")
106+ Expect(err).ToNot(HaveOccurred())
107+
108+ templateHTML := `{{ Custom Broken Template`
109+ signInFile := filepath.Join(customDir, signInTemplateName)
110+ Expect(ioutil.WriteFile(signInFile, []byte(templateHTML), 0600)).To(Succeed())
111+
112+ opts.TemplatesPath = customDir
113+ })
114+
115+ AfterEach(func() {
116+ Expect(os.RemoveAll(customDir)).To(Succeed())
117+ })
118+
119+ It("Should return an error", func() {
120+ writer, err := NewWriter(opts)
121+ Expect(err).To(MatchError(ContainSubstring("template: sign_in.html:1: function \"Custom\" not defined")))
122+ Expect(writer).To(BeNil())
123+ })
124+ })
125+ })
126+})
pkg/app/pagewriter/sign_in_page.gonew file+66
@@ -0,0 +1,66 @@
1+package pagewriter
2+
3+import (
4+ "html/template"
5+ "net/http"
6+
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
8+)
9+
10+// signInPageWriter is used to render sign-in pages.
11+type signInPageWriter struct {
12+ // Template is the sign-in page HTML template.
13+ template *template.Template
14+
15+ // errorPageWriter is used to render an error if there are problems with rendering the sign-in page.
16+ errorPageWriter *errorPageWriter
17+
18+ // ProxyPrefix is the prefix under which OAuth2 Proxy pages are served.
19+ proxyPrefix string
20+
21+ // ProviderName is the name of the provider that should be displayed on the login button.
22+ providerName string
23+
24+ // SignInMessage is the messge displayed above the login button.
25+ signInMessage string
26+
27+ // Footer is the footer to be displayed at the bottom of the page.
28+ // If not set, a default footer will be used.
29+ footer string
30+
31+ // Version is the OAuth2 Proxy version to be used in the default footer.
32+ version string
33+
34+ // DisplayLoginForm determines whether or not the basic auth password form is displayed on the sign-in page.
35+ displayLoginForm bool
36+}
37+
38+// WriteSignInPage writes the sign-in page to the given response writer.
39+// It uses the redirectURL to be able to set the final destination for the user post login.
40+func (s *signInPageWriter) WriteSignInPage(rw http.ResponseWriter, redirectURL string) {
41+ // We allow unescaped template.HTML since it is user configured options
42+ /* #nosec G203 */
43+ t := struct {
44+ ProviderName string
45+ SignInMessage template.HTML
46+ CustomLogin bool
47+ Redirect string
48+ Version string
49+ ProxyPrefix string
50+ Footer template.HTML
51+ }{
52+ ProviderName: s.providerName,
53+ SignInMessage: template.HTML(s.signInMessage),
54+ CustomLogin: s.displayLoginForm,
55+ Redirect: redirectURL,
56+ Version: s.version,
57+ ProxyPrefix: s.proxyPrefix,
58+ Footer: template.HTML(s.footer),
59+ }
60+
61+ err := s.template.Execute(rw, t)
62+ if err != nil {
63+ logger.Printf("Error rendering sign-in template: %v", err)
64+ s.errorPageWriter.WriteErrorPage(rw, http.StatusInternalServerError, redirectURL, err.Error())
65+ }
66+}
pkg/app/pagewriter/sign_in_page_test.gonew file+61
@@ -0,0 +1,61 @@
1+package pagewriter
2+
3+import (
4+ "html/template"
5+ "io/ioutil"
6+ "net/http/httptest"
7+
8+ . "github.com/onsi/ginkgo"
9+ . "github.com/onsi/gomega"
10+)
11+
12+var _ = Describe("SignIn Page Writer", func() {
13+ var signInPage *signInPageWriter
14+
15+ BeforeEach(func() {
16+ errorTmpl, err := template.New("").Parse("{{.Title}}")
17+ Expect(err).ToNot(HaveOccurred())
18+ errorPage := &errorPageWriter{
19+ template: errorTmpl,
20+ }
21+
22+ tmpl, err := template.New("").Parse("{{.ProxyPrefix}} {{.ProviderName}} {{.SignInMessage}} {{.Footer}} {{.Version}} {{.Redirect}} {{.CustomLogin}}")
23+ Expect(err).ToNot(HaveOccurred())
24+
25+ signInPage = &signInPageWriter{
26+ template: tmpl,
27+ errorPageWriter: errorPage,
28+ proxyPrefix: "/prefix/",
29+ providerName: "My Provider",
30+ signInMessage: "Sign In Here",
31+ footer: "Custom Footer Text",
32+ version: "v0.0.0-test",
33+ displayLoginForm: true,
34+ }
35+ })
36+
37+ Context("WriteSignInPage", func() {
38+ It("Writes the template to the response writer", func() {
39+ recorder := httptest.NewRecorder()
40+ signInPage.WriteSignInPage(recorder, "/redirect")
41+
42+ body, err := ioutil.ReadAll(recorder.Result().Body)
43+ Expect(err).ToNot(HaveOccurred())
44+ Expect(string(body)).To(Equal("/prefix/ My Provider Sign In Here Custom Footer Text v0.0.0-test /redirect true"))
45+ })
46+
47+ It("Writes an error if the template can't be rendered", func() {
48+ // Overwrite the template with something bad
49+ tmpl, err := template.New("").Parse("{{.Unknown}}")
50+ Expect(err).ToNot(HaveOccurred())
51+ signInPage.template = tmpl
52+
53+ recorder := httptest.NewRecorder()
54+ signInPage.WriteSignInPage(recorder, "/redirect")
55+
56+ body, err := ioutil.ReadAll(recorder.Result().Body)
57+ Expect(err).ToNot(HaveOccurred())
58+ Expect(string(body)).To(Equal("Internal Server Error"))
59+ })
60+ })
61+})
pkg/app/pagewriter/templates.gonew file+259
@@ -0,0 +1,259 @@
1+package pagewriter
2+
3+import (
4+ "fmt"
5+ "html/template"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
11+)
12+
13+const (
14+ errorTemplateName = "error.html"
15+ signInTemplateName = "sign_in.html"
16+
17+ defaultErrorTemplate = `{{define "error.html"}}
18+<!DOCTYPE html>
19+<html lang="en" charset="utf-8">
20+<head>
21+ <meta charset="utf-8">
22+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
23+ <title>{{.StatusCode}} {{.Title}}</title>
24+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
25+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css">
26+
27+ <script type="text/javascript">
28+ document.addEventListener('DOMContentLoaded', function() {
29+ let cardToggles = document.getElementsByClassName('card-toggle');
30+ for (let i = 0; i < cardToggles.length; i++) {
31+ cardToggles[i].addEventListener('click', e => {
32+ e.currentTarget.parentElement.parentElement.childNodes[3].classList.toggle('is-hidden');
33+ });
34+ }
35+ });
36+ </script>
37+
38+ <style>
39+ body {
40+ height: 100vh;
41+ }
42+ .error-box {
43+ margin: 1.25rem auto;
44+ max-width: 600px;
45+ }
46+ .status-code {
47+ font-size: 12rem;
48+ font-weight: 600;
49+ }
50+ #more-info.card {
51+ border: 1px solid #f0f0f0;
52+ }
53+ footer a {
54+ text-decoration: underline;
55+ }
56+ </style>
57+</head>
58+<body class="has-background-light">
59+ <section class="section">
60+ <div class="box block error-box has-text-centered">
61+ <div class="status-code">{{.StatusCode}}</div>
62+ <div class="block">
63+ <h1 class="subtitle is-1">{{.Title}}</h1>
64+ </div>
65+
66+ {{ if .Message }}
67+ <div id="more-info" class="block card is-fullwidth is-shadowless">
68+ <header class="card-header is-shadowless">
69+ <p class="card-header-title">More Info</p>
70+ <a class="card-header-icon card-toggle">
71+ <i class="fa fa-angle-down"></i>
72+ </a>
73+ </header>
74+ <div class="card-content has-text-left is-hidden">
75+ <div class="content">
76+ {{.Message}}
77+ </div>
78+ </div>
79+ </div>
80+ {{ end }}
81+
82+ {{ if .Redirect }}
83+ <hr>
84+
85+ <div class="columns">
86+ <div class="column">
87+ <form method="GET" action="{{.Redirect}}">
88+ <button type="submit" class="button is-danger is-fullwidth">Go back</button>
89+ </form>
90+ </div>
91+ <div class="column">
92+ <form method="GET" action="{{.ProxyPrefix}}/sign_in">
93+ <input type="hidden" name="rd" value="{{.Redirect}}">
94+ <button type="submit" class="button is-primary is-fullwidth">Sign in</button>
95+ </form>
96+ </div>
97+ </div>
98+ {{ end }}
99+
100+ </div>
101+ </section>
102+
103+ <footer class="footer has-text-grey has-background-light is-size-7">
104+ <div class="content has-text-centered">
105+ {{ if eq .Footer "-" }}
106+ {{ else if eq .Footer ""}}
107+ <p>Secured with <a href="https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy" class="has-text-grey">OAuth2 Proxy</a> version {{.Version}}</p>
108+ {{ else }}
109+ <p>{{.Footer}}</p>
110+ {{ end }}
111+ </div>
112+ </footer>
113+
114+ </body>
115+</html>
116+{{end}}`
117+
118+ defaultSignInTemplate = `{{define "sign_in.html"}}
119+<!DOCTYPE html>
120+<html lang="en" charset="utf-8">
121+ <head>
122+ <meta charset="utf-8">
123+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
124+ <title>Sign In</title>
125+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
126+
127+ <style>
128+ body {
129+ height: 100vh;
130+ }
131+ .sign-in-box {
132+ max-width: 400px;
133+ margin: 1.25rem auto;
134+ }
135+ footer a {
136+ text-decoration: underline;
137+ }
138+ </style>
139+
140+ <script>
141+ if (window.location.hash) {
142+ (function() {
143+ var inputs = document.getElementsByName('rd');
144+ for (var i = 0; i < inputs.length; i++) {
145+ // Add hash, but make sure it is only added once
146+ var idx = inputs[i].value.indexOf('#');
147+ if (idx >= 0) {
148+ // Remove existing hash from URL
149+ inputs[i].value = inputs[i].value.substr(0, idx);
150+ }
151+ inputs[i].value += window.location.hash;
152+ }
153+ })();
154+ }
155+ </script>
156+ </head>
157+ <body class="has-background-light">
158+ <section class="section">
159+ <div class="box block sign-in-box has-text-centered">
160+ <form method="GET" action="{{.ProxyPrefix}}/start">
161+ <input type="hidden" name="rd" value="{{.Redirect}}">
162+ {{ if .SignInMessage }}
163+ <p class="block">{{.SignInMessage}}</p>
164+ {{ end}}
165+ <button type="submit" class="button block is-primary">Sign in with {{.ProviderName}}</button>
166+ </form>
167+
168+ {{ if .CustomLogin }}
169+ <hr>
170+
171+ <form method="POST" action="{{.ProxyPrefix}}/sign_in" class="block">
172+ <input type="hidden" name="rd" value="{{.Redirect}}">
173+
174+ <div class="field">
175+ <label class="label" for="username">Username</label>
176+ <div class="control">
177+ <input class="input" type="email" placeholder="e.g. userx@example.com" name="username" id="username">
178+ </div>
179+ </div>
180+
181+ <div class="field">
182+ <label class="label" for="password">Password</label>
183+ <div class="control">
184+ <input class="input" type="password" placeholder="********" name="password" id="password">
185+ </div>
186+ </div>
187+ <button class="button is-primary">Sign in</button>
188+ {{ end }}
189+ </form>
190+ </div>
191+ </section>
192+
193+ <footer class="footer has-text-grey has-background-light is-size-7">
194+ <div class="content has-text-centered">
195+ {{ if eq .Footer "-" }}
196+ {{ else if eq .Footer ""}}
197+ <p>Secured with <a href="https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy" class="has-text-grey">OAuth2 Proxy</a> version {{.Version}}</p>
198+ {{ else }}
199+ <p>{{.Footer}}</p>
200+ {{ end }}
201+ </div>
202+ </footer>
203+
204+ </body>
205+</html>
206+{{end}}`
207+)
208+
209+// loadTemplates adds the Sign In and Error templates from the custom template
210+// directory, or uses the defaults if they do not exist or the custom directory
211+// is not provided.
212+func loadTemplates(customDir string) (*template.Template, error) {
213+ t := template.New("").Funcs(template.FuncMap{
214+ "ToUpper": strings.ToUpper,
215+ "ToLower": strings.ToLower,
216+ })
217+ var err error
218+ t, err = addTemplate(t, customDir, signInTemplateName, defaultSignInTemplate)
219+ if err != nil {
220+ return nil, fmt.Errorf("could not add Sign In template: %v", err)
221+ }
222+ t, err = addTemplate(t, customDir, errorTemplateName, defaultErrorTemplate)
223+ if err != nil {
224+ return nil, fmt.Errorf("could not add Error template: %v", err)
225+ }
226+
227+ return t, nil
228+}
229+
230+// addTemplate will add the template from the custom directory if provided,
231+// else it will add the default template.
232+func addTemplate(t *template.Template, customDir, fileName, defaultTemplate string) (*template.Template, error) {
233+ filePath := filepath.Join(customDir, fileName)
234+ if customDir != "" && isFile(filePath) {
235+ t, err := t.ParseFiles(filePath)
236+ if err != nil {
237+ return nil, fmt.Errorf("failed to parse template %s: %v", filePath, err)
238+ }
239+ return t, nil
240+ }
241+ t, err := t.Parse(defaultTemplate)
242+ if err != nil {
243+ // This should not happen.
244+ // Default templates should be tested and so should never fail to parse.
245+ logger.Panic("Could not parse defaultTemplate: ", err)
246+ }
247+ return t, nil
248+}
249+
250+// isFile checks if the file exists and checks whether it is a regular file.
251+// If either of these fail then it cannot be used as a template file.
252+func isFile(fileName string) bool {
253+ info, err := os.Stat(fileName)
254+ if err != nil {
255+ logger.Errorf("Could not load file %s: %v, will use default template", fileName, err)
256+ return false
257+ }
258+ return info.Mode().IsRegular()
259+}
pkg/app/pagewriter/templates_test.gonew file+199
@@ -0,0 +1,199 @@
1+package pagewriter
2+
3+import (
4+ "bytes"
5+ "html/template"
6+ "io/ioutil"
7+ "os"
8+ "path/filepath"
9+
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var _ = Describe("Templates", func() {
15+ var customDir string
16+
17+ BeforeEach(func() {
18+ var err error
19+ customDir, err = ioutil.TempDir("", "oauth2-proxy-templates-test")
20+ Expect(err).ToNot(HaveOccurred())
21+
22+ templateHTML := `{{.TestString}} {{.TestString | ToLower}} {{.TestString | ToUpper}}`
23+ signInFile := filepath.Join(customDir, signInTemplateName)
24+ Expect(ioutil.WriteFile(signInFile, []byte(templateHTML), 0600)).To(Succeed())
25+ errorFile := filepath.Join(customDir, errorTemplateName)
26+ Expect(ioutil.WriteFile(errorFile, []byte(templateHTML), 0600)).To(Succeed())
27+ })
28+
29+ AfterEach(func() {
30+ Expect(os.RemoveAll(customDir)).To(Succeed())
31+ })
32+
33+ Context("loadTemplates", func() {
34+ var data interface{}
35+ var t *template.Template
36+
37+ BeforeEach(func() {
38+ data = struct {
39+ // For default templates
40+ ProxyPrefix string
41+ Redirect string
42+ Footer string
43+
44+ // For default sign_in template
45+ SignInMessage string
46+ ProviderName string
47+ CustomLogin bool
48+
49+ // For default error template
50+ StatusCode int
51+ Title string
52+ Message string
53+
54+ // For custom templates
55+ TestString string
56+ }{
57+ ProxyPrefix: "<proxy-prefix>",
58+ Redirect: "<redirect>",
59+ Footer: "<footer>",
60+
61+ SignInMessage: "<sign-in-message>",
62+ ProviderName: "<provider-name>",
63+ CustomLogin: false,
64+
65+ StatusCode: 404,
66+ Title: "<title>",
67+ Message: "<message>",
68+
69+ TestString: "Testing",
70+ }
71+ })
72+
73+ Context("With no custom directory", func() {
74+ BeforeEach(func() {
75+ var err error
76+ t, err = loadTemplates("")
77+ Expect(err).ToNot(HaveOccurred())
78+ })
79+
80+ It("Use the default sign_in page", func() {
81+ buf := bytes.NewBuffer([]byte{})
82+ Expect(t.ExecuteTemplate(buf, signInTemplateName, data)).To(Succeed())
83+ Expect(buf.String()).To(HavePrefix("\n<!DOCTYPE html>"))
84+ })
85+
86+ It("Use the default error page", func() {
87+ buf := bytes.NewBuffer([]byte{})
88+ Expect(t.ExecuteTemplate(buf, errorTemplateName, data)).To(Succeed())
89+ Expect(buf.String()).To(HavePrefix("\n<!DOCTYPE html>"))
90+ })
91+ })
92+
93+ Context("With a custom directory", func() {
94+ Context("With both templates", func() {
95+ BeforeEach(func() {
96+ var err error
97+ t, err = loadTemplates(customDir)
98+ Expect(err).ToNot(HaveOccurred())
99+ })
100+
101+ It("Use the custom sign_in page", func() {
102+ buf := bytes.NewBuffer([]byte{})
103+ Expect(t.ExecuteTemplate(buf, signInTemplateName, data)).To(Succeed())
104+ Expect(buf.String()).To(Equal("Testing testing TESTING"))
105+ })
106+
107+ It("Use the custom error page", func() {
108+ buf := bytes.NewBuffer([]byte{})
109+ Expect(t.ExecuteTemplate(buf, errorTemplateName, data)).To(Succeed())
110+ Expect(buf.String()).To(Equal("Testing testing TESTING"))
111+ })
112+ })
113+
114+ Context("With no error template", func() {
115+ BeforeEach(func() {
116+ Expect(os.Remove(filepath.Join(customDir, errorTemplateName))).To(Succeed())
117+
118+ var err error
119+ t, err = loadTemplates(customDir)
120+ Expect(err).ToNot(HaveOccurred())
121+ })
122+
123+ It("Use the custom sign_in page", func() {
124+ buf := bytes.NewBuffer([]byte{})
125+ Expect(t.ExecuteTemplate(buf, signInTemplateName, data)).To(Succeed())
126+ Expect(buf.String()).To(Equal("Testing testing TESTING"))
127+ })
128+
129+ It("Use the default error page", func() {
130+ buf := bytes.NewBuffer([]byte{})
131+ Expect(t.ExecuteTemplate(buf, errorTemplateName, data)).To(Succeed())
132+ Expect(buf.String()).To(HavePrefix("\n<!DOCTYPE html>"))
133+ })
134+ })
135+
136+ Context("With no sign_in template", func() {
137+ BeforeEach(func() {
138+ Expect(os.Remove(filepath.Join(customDir, signInTemplateName))).To(Succeed())
139+
140+ var err error
141+ t, err = loadTemplates(customDir)
142+ Expect(err).ToNot(HaveOccurred())
143+ })
144+
145+ It("Use the default sign_in page", func() {
146+ buf := bytes.NewBuffer([]byte{})
147+ Expect(t.ExecuteTemplate(buf, signInTemplateName, data)).To(Succeed())
148+ Expect(buf.String()).To(HavePrefix("\n<!DOCTYPE html>"))
149+ })
150+
151+ It("Use the custom error page", func() {
152+ buf := bytes.NewBuffer([]byte{})
153+ Expect(t.ExecuteTemplate(buf, errorTemplateName, data)).To(Succeed())
154+ Expect(buf.String()).To(Equal("Testing testing TESTING"))
155+ })
156+ })
157+
158+ Context("With an invalid sign_in template", func() {
159+ BeforeEach(func() {
160+ signInFile := filepath.Join(customDir, signInTemplateName)
161+ Expect(ioutil.WriteFile(signInFile, []byte("{{"), 0600))
162+ })
163+
164+ It("Should return an error when loading templates", func() {
165+ t, err := loadTemplates(customDir)
166+ Expect(err).To(MatchError(HavePrefix("could not add Sign In template:")))
167+ Expect(t).To(BeNil())
168+ })
169+ })
170+
171+ Context("With an invalid error template", func() {
172+ BeforeEach(func() {
173+ errorFile := filepath.Join(customDir, errorTemplateName)
174+ Expect(ioutil.WriteFile(errorFile, []byte("{{"), 0600))
175+ })
176+
177+ It("Should return an error when loading templates", func() {
178+ t, err := loadTemplates(customDir)
179+ Expect(err).To(MatchError(HavePrefix("could not add Error template:")))
180+ Expect(t).To(BeNil())
181+ })
182+ })
183+ })
184+ })
185+
186+ Context("isFile", func() {
187+ It("with a valid file", func() {
188+ Expect(isFile(filepath.Join(customDir, signInTemplateName))).To(BeTrue())
189+ })
190+
191+ It("with a directory", func() {
192+ Expect(isFile(customDir)).To(BeFalse())
193+ })
194+
195+ It("with an invalid file", func() {
196+ Expect(isFile(filepath.Join(customDir, "does_not_exist.html"))).To(BeFalse())
197+ })
198+ })
199+})
pkg/authentication/basic/basic_suite_test.go+1
@@ -10,6 +10,7 @@ import (
1010
1111func TestBasicSuite(t *testing.T) {
1212 logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
1314
1415 RegisterFailHandler(Fail)
1516 RunSpecs(t, "Basic")
pkg/cookies/cookies.go+4−4
@@ -9,14 +9,14 @@ import (
99
1010 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1111 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
12− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
12+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1313)
1414
1515// MakeCookie constructs a cookie from the given parameters,
1616// discovering the domain from the request if not specified.
1717func MakeCookie(req *http.Request, name string, value string, path string, domain string, httpOnly bool, secure bool, expiration time.Duration, now time.Time, sameSite http.SameSite) *http.Cookie {
1818 if domain != "" {
19− host := util.GetRequestHost(req)
19+ host := requestutil.GetRequestHost(req)
2020 if h, _, err := net.SplitHostPort(host); err == nil {
2121 host = h
2222 }
@@ -48,7 +48,7 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, cookieO
4848 // If nothing matches, create the cookie with the shortest domain
4949 defaultDomain := ""
5050 if len(cookieOpts.Domains) > 0 {
51− logger.Errorf("Warning: request host %q did not match any of the specific cookie domains of %q", util.GetRequestHost(req), strings.Join(cookieOpts.Domains, ","))
51+ logger.Errorf("Warning: request host %q did not match any of the specific cookie domains of %q", requestutil.GetRequestHost(req), strings.Join(cookieOpts.Domains, ","))
5252 defaultDomain = cookieOpts.Domains[len(cookieOpts.Domains)-1]
5353 }
5454 return MakeCookie(req, name, value, cookieOpts.Path, defaultDomain, cookieOpts.HTTPOnly, cookieOpts.Secure, expiration, now, ParseSameSite(cookieOpts.SameSite))
@@ -57,7 +57,7 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, cookieO
5757// GetCookieDomain returns the correct cookie domain given a list of domains
5858// by checking the X-Fowarded-Host and host header of an an http request
5959func GetCookieDomain(req *http.Request, cookieDomains []string) string {
60− host := util.GetRequestHost(req)
60+ host := requestutil.GetRequestHost(req)
6161 for _, domain := range cookieDomains {
6262 if strings.HasSuffix(host, domain) {
6363 return domain
pkg/header/header_suite_test.go+1
@@ -17,6 +17,7 @@ var (
1717
1818func TestHeaderSuite(t *testing.T) {
1919 logger.SetOutput(GinkgoWriter)
20+ logger.SetErrOutput(GinkgoWriter)
2021
2122 RegisterFailHandler(Fail)
2223 RunSpecs(t, "Header")
pkg/header/injector_test.go+7−7
@@ -49,14 +49,14 @@ var _ = Describe("Injector Suite", func() {
4949 },
5050 expectedErr: nil,
5151 }),
52− Entry("with a static valued header from base64", newInjectorTableInput{
52+ Entry("with a static valued header from string", newInjectorTableInput{
5353 headers: []options.Header{
5454 {
5555 Name: "Secret",
5656 Values: []options.HeaderValue{
5757 {
5858 SecretSource: &options.SecretSource{
59− Value: []byte(base64.StdEncoding.EncodeToString([]byte("super-secret"))),
59+ Value: []byte("super-secret"),
6060 },
6161 },
6262 },
@@ -200,7 +200,7 @@ var _ = Describe("Injector Suite", func() {
200200 ClaimSource: &options.ClaimSource{
201201 Claim: "user",
202202 BasicAuthPassword: &options.SecretSource{
203− Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
203+ Value: []byte("basic-password"),
204204 },
205205 },
206206 },
@@ -349,7 +349,7 @@ var _ = Describe("Injector Suite", func() {
349349 ClaimSource: &options.ClaimSource{
350350 Claim: "user",
351351 BasicAuthPassword: &options.SecretSource{
352− Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
352+ Value: []byte("basic-password"),
353353 },
354354 },
355355 },
@@ -380,17 +380,17 @@ var _ = Describe("Injector Suite", func() {
380380 Values: []options.HeaderValue{
381381 {
382382 SecretSource: &options.SecretSource{
383− Value: []byte(base64.StdEncoding.EncodeToString([]byte("major=1"))),
383+ Value: []byte("major=1"),
384384 },
385385 },
386386 {
387387 SecretSource: &options.SecretSource{
388− Value: []byte(base64.StdEncoding.EncodeToString([]byte("minor=2"))),
388+ Value: []byte("minor=2"),
389389 },
390390 },
391391 {
392392 SecretSource: &options.SecretSource{
393− Value: []byte(base64.StdEncoding.EncodeToString([]byte("patch=3"))),
393+ Value: []byte("patch=3"),
394394 },
395395 },
396396 },
pkg/logger/logger.go+3−3
@@ -12,7 +12,7 @@ import (
1212 "text/template"
1313 "time"
1414
15− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
15+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1616)
1717
1818// AuthStatus defines the different types of auth logging that occur
@@ -197,7 +197,7 @@ func (l *Logger) PrintAuthf(username string, req *http.Request, status AuthStatu
197197
198198 err := l.authTemplate.Execute(l.writer, authLogMessageData{
199199 Client: client,
200− Host: util.GetRequestHost(req),
200+ Host: requestutil.GetRequestHost(req),
201201 Protocol: req.Proto,
202202 RequestMethod: req.Method,
203203 Timestamp: FormatTimestamp(now),
@@ -251,7 +251,7 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url.
251251
252252 err := l.reqTemplate.Execute(l.writer, reqLogMessageData{
253253 Client: client,
254− Host: util.GetRequestHost(req),
254+ Host: requestutil.GetRequestHost(req),
255255 Protocol: req.Proto,
256256 RequestDuration: fmt.Sprintf("%0.3f", duration),
257257 RequestMethod: req.Method,
pkg/middleware/basic_session.go+2−1
@@ -5,6 +5,7 @@ import (
55 "net/http"
66
77 "github.com/justinas/alice"
8+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
89 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@@ -23,7 +24,7 @@ func NewBasicAuthSessionLoader(validator basic.Validator) alice.Constructor {
2324// If a session was loaded by a previous handler, it will not be replaced.
2425func loadBasicAuthSession(validator basic.Validator, next http.Handler) http.Handler {
2526 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
26− scope := GetRequestScope(req)
27+ scope := middlewareapi.GetRequestScope(req)
2728 // If scope is nil, this will panic.
2829 // A scope should always be injected before this handler is called.
2930 if scope.Session != nil {
pkg/middleware/basic_session_test.go+2−4
@@ -1,7 +1,6 @@
11package middleware
22
33import (
4− "context"
54 "fmt"
65 "net/http"
76 "net/http/httptest"
@@ -40,8 +39,7 @@ var _ = Describe("Basic Auth Session Suite", func() {
4039 // Set up the request with the authorization header and a request scope
4140 req := httptest.NewRequest("", "/", nil)
4241 req.Header.Set("Authorization", in.authorizationHeader)
43− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
44− req = req.WithContext(contextWithScope)
42+ req = middlewareapi.AddRequestScope(req, scope)
4543
4644 rw := httptest.NewRecorder()
4745
@@ -57,7 +55,7 @@ var _ = Describe("Basic Auth Session Suite", func() {
5755 // from the scope
5856 var gotSession *sessionsapi.SessionState
5957 handler := NewBasicAuthSessionLoader(validator)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60− gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
58+ gotSession = middlewareapi.GetRequestScope(r).Session
6159 }))
6260 handler.ServeHTTP(rw, req)
6361
pkg/middleware/headers.go+14−2
@@ -3,8 +3,10 @@ package middleware
33import (
44 "fmt"
55 "net/http"
6+ "strings"
67
78 "github.com/justinas/alice"
9+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
810 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
911 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/header"
1012)
@@ -39,6 +41,14 @@ func newStripHeaders(headers []options.Header) alice.Constructor {
3941 }
4042}
4143
44+func flattenHeaders(headers http.Header) {
45+ for name, values := range headers {
46+ if len(values) > 1 {
47+ headers.Set(name, strings.Join(values, ","))
48+ }
49+ }
50+}
51+
4252func stripHeaders(headers []string, next http.Handler) http.Handler {
4353 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
4454 for _, header := range headers {
@@ -61,11 +71,12 @@ func newRequestHeaderInjector(headers []options.Header) (alice.Constructor, erro
6171
6272func injectRequestHeaders(injector header.Injector, next http.Handler) http.Handler {
6373 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
64− scope := GetRequestScope(req)
74+ scope := middlewareapi.GetRequestScope(req)
6575
6676 // If scope is nil, this will panic.
6777 // A scope should always be injected before this handler is called.
6878 injector.Inject(req.Header, scope.Session)
79+ flattenHeaders(req.Header)
6980 next.ServeHTTP(rw, req)
7081 })
7182}
@@ -92,11 +103,12 @@ func newResponseHeaderInjector(headers []options.Header) (alice.Constructor, err
92103
93104func injectResponseHeaders(injector header.Injector, next http.Handler) http.Handler {
94105 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
95− scope := GetRequestScope(req)
106+ scope := middlewareapi.GetRequestScope(req)
96107
97108 // If scope is nil, this will panic.
98109 // A scope should always be injected before this handler is called.
99110 injector.Inject(rw.Header(), scope.Session)
111+ flattenHeaders(rw.Header())
100112 next.ServeHTTP(rw, req)
101113 })
102114}
pkg/middleware/headers_test.go+14−17
@@ -1,7 +1,6 @@
11package middleware
22
33import (
4− "context"
54 "encoding/base64"
65 "net/http"
76 "net/http/httptest"
@@ -31,8 +30,7 @@ var _ = Describe("Headers Suite", func() {
3130
3231 // Set up the request with a request scope
3332 req := httptest.NewRequest("", "/", nil)
34− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
35− req = req.WithContext(contextWithScope)
33+ req = middlewareapi.AddRequestScope(req, scope)
3634 req.Header = in.initialHeaders.Clone()
3735
3836 rw := httptest.NewRecorder()
@@ -57,11 +55,11 @@ var _ = Describe("Headers Suite", func() {
5755 Entry("with no configured headers", headersTableInput{
5856 headers: []options.Header{},
5957 initialHeaders: http.Header{
60− "foo": []string{"bar", "baz"},
58+ "Foo": []string{"bar", "baz"},
6159 },
6260 session: &sessionsapi.SessionState{},
6361 expectedHeaders: http.Header{
64− "foo": []string{"bar", "baz"},
62+ "Foo": []string{"bar,baz"},
6563 },
6664 expectedErr: "",
6765 }),
@@ -79,13 +77,13 @@ var _ = Describe("Headers Suite", func() {
7977 },
8078 },
8179 initialHeaders: http.Header{
82− "foo": []string{"bar", "baz"},
80+ "Foo": []string{"bar", "baz"},
8381 },
8482 session: &sessionsapi.SessionState{
8583 IDToken: "IDToken-1234",
8684 },
8785 expectedHeaders: http.Header{
88− "foo": []string{"bar", "baz"},
86+ "Foo": []string{"bar,baz"},
8987 "Claim": []string{"IDToken-1234"},
9088 },
9189 expectedErr: "",
@@ -135,7 +133,7 @@ var _ = Describe("Headers Suite", func() {
135133 IDToken: "IDToken-1234",
136134 },
137135 expectedHeaders: http.Header{
138− "Claim": []string{"bar", "baz", "IDToken-1234"},
136+ "Claim": []string{"bar,baz,IDToken-1234"},
139137 },
140138 expectedErr: "",
141139 }),
@@ -178,7 +176,7 @@ var _ = Describe("Headers Suite", func() {
178176 },
179177 session: nil,
180178 expectedHeaders: http.Header{
181− "Claim": []string{"bar", "baz"},
179+ "Claim": []string{"bar,baz"},
182180 },
183181 expectedErr: "",
184182 }),
@@ -218,8 +216,7 @@ var _ = Describe("Headers Suite", func() {
218216
219217 // Set up the request with a request scope
220218 req := httptest.NewRequest("", "/", nil)
221− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
222− req = req.WithContext(contextWithScope)
219+ req = middlewareapi.AddRequestScope(req, scope)
223220
224221 rw := httptest.NewRecorder()
225222 for key, values := range in.initialHeaders {
@@ -252,7 +249,7 @@ var _ = Describe("Headers Suite", func() {
252249 },
253250 session: &sessionsapi.SessionState{},
254251 expectedHeaders: http.Header{
255− "Foo": []string{"bar", "baz"},
252+ "Foo": []string{"bar,baz"},
256253 },
257254 expectedErr: "",
258255 }),
@@ -276,7 +273,7 @@ var _ = Describe("Headers Suite", func() {
276273 IDToken: "IDToken-1234",
277274 },
278275 expectedHeaders: http.Header{
279− "Foo": []string{"bar", "baz"},
276+ "Foo": []string{"bar,baz"},
280277 "Claim": []string{"IDToken-1234"},
281278 },
282279 expectedErr: "",
@@ -301,7 +298,7 @@ var _ = Describe("Headers Suite", func() {
301298 IDToken: "IDToken-1234",
302299 },
303300 expectedHeaders: http.Header{
304− "Claim": []string{"bar", "baz", "IDToken-1234"},
301+ "Claim": []string{"bar,baz,IDToken-1234"},
305302 },
306303 expectedErr: "",
307304 }),
@@ -326,7 +323,7 @@ var _ = Describe("Headers Suite", func() {
326323 IDToken: "IDToken-1234",
327324 },
328325 expectedHeaders: http.Header{
329− "Claim": []string{"bar", "baz", "IDToken-1234"},
326+ "Claim": []string{"bar,baz,IDToken-1234"},
330327 },
331328 expectedErr: "",
332329 }),
@@ -348,7 +345,7 @@ var _ = Describe("Headers Suite", func() {
348345 },
349346 session: nil,
350347 expectedHeaders: http.Header{
351− "Claim": []string{"bar", "baz"},
348+ "Claim": []string{"bar,baz"},
352349 },
353350 expectedErr: "",
354351 }),
@@ -371,7 +368,7 @@ var _ = Describe("Headers Suite", func() {
371368 },
372369 session: nil,
373370 expectedHeaders: http.Header{
374− "Claim": []string{"bar", "baz"},
371+ "Claim": []string{"bar,baz"},
375372 },
376373 expectedErr: "",
377374 }),
pkg/middleware/jwt_session.go+1−1
@@ -37,7 +37,7 @@ type jwtSessionLoader struct {
3737// If a session was loaded by a previous handler, it will not be replaced.
3838func (j *jwtSessionLoader) loadSession(next http.Handler) http.Handler {
3939 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
40− scope := GetRequestScope(req)
40+ scope := middlewareapi.GetRequestScope(req)
4141 // If scope is nil, this will panic.
4242 // A scope should always be injected before this handler is called.
4343 if scope.Session != nil {
pkg/middleware/jwt_session_test.go+2−3
@@ -103,8 +103,7 @@ Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
103103 // Set up the request with the authorization header and a request scope
104104 req := httptest.NewRequest("", "/", nil)
105105 req.Header.Set("Authorization", in.authorizationHeader)
106− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
107− req = req.WithContext(contextWithScope)
106+ req = middlewareapi.AddRequestScope(req, scope)
108107
109108 rw := httptest.NewRecorder()
110109
@@ -116,7 +115,7 @@ Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
116115 // from the scope
117116 var gotSession *sessionsapi.SessionState
118117 handler := NewJwtSessionLoader(sessionLoaders)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
119− gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
118+ gotSession = middlewareapi.GetRequestScope(r).Session
120119 }))
121120 handler.ServeHTTP(rw, req)
122121
pkg/middleware/metrics.gonew file+122
@@ -0,0 +1,122 @@
1+package middleware
2+
3+import (
4+ "net/http"
5+
6+ "github.com/justinas/alice"
7+ "github.com/prometheus/client_golang/prometheus"
8+ "github.com/prometheus/client_golang/prometheus/promhttp"
9+)
10+
11+// DefaultMetricsHandler is the default http.Handler for serving metrics from
12+// the default prometheus.Registry
13+var DefaultMetricsHandler = NewMetricsHandlerWithDefaultRegistry()
14+
15+// NewMetricsHandlerWithDefaultRegistry creates a new http.Handler for serving
16+// metrics from the default prometheus.Registry.
17+func NewMetricsHandlerWithDefaultRegistry() http.Handler {
18+ return NewMetricsHandler(prometheus.DefaultRegisterer, prometheus.DefaultGatherer)
19+}
20+
21+// NewMetricsHandler creates a new http.Handler for serving metrics from the
22+// provided prometheus.Registerer and prometheus.Gatherer
23+func NewMetricsHandler(registerer prometheus.Registerer, gatherer prometheus.Gatherer) http.Handler {
24+ return promhttp.InstrumentMetricHandler(
25+ registerer, promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{}),
26+ )
27+}
28+
29+// NewRequestMetricsWithDefaultRegistry returns a middleware that will record
30+// metrics for HTTP requests to the default prometheus.Registry
31+func NewRequestMetricsWithDefaultRegistry() alice.Constructor {
32+ return NewRequestMetrics(prometheus.DefaultRegisterer)
33+}
34+
35+// NewRequestMetrics returns a middleware that will record metrics for HTTP
36+// requests to the provided prometheus.Registerer
37+func NewRequestMetrics(registerer prometheus.Registerer) alice.Constructor {
38+ return func(next http.Handler) http.Handler {
39+ // Counter for all requests
40+ // This is bucketed based on the response code we set
41+ counterHandler := func(next http.Handler) http.Handler {
42+ return promhttp.InstrumentHandlerCounter(registerRequestsCounter(registerer), next)
43+ }
44+
45+ // Gauge to all requests currently being handled
46+ inFlightHandler := func(next http.Handler) http.Handler {
47+ return promhttp.InstrumentHandlerInFlight(registerInflightRequestsGauge(registerer), next)
48+ }
49+
50+ // The latency of all requests bucketed by HTTP method
51+ durationHandler := func(next http.Handler) http.Handler {
52+ return promhttp.InstrumentHandlerDuration(registerRequestsLatencyHistogram(registerer), next)
53+ }
54+
55+ return alice.New(counterHandler, inFlightHandler, durationHandler).Then(next)
56+ }
57+}
58+
59+// registerRequestsCounter registers the 'oauth2_proxy_requests_total' metric
60+// This keeps a tally of all received requests bucket by their HTTP response
61+// status code
62+func registerRequestsCounter(registerer prometheus.Registerer) *prometheus.CounterVec {
63+ counter := prometheus.NewCounterVec(
64+ prometheus.CounterOpts{
65+ Name: "oauth2_proxy_requests_total",
66+ Help: "Total number of requests by HTTP status code.",
67+ },
68+ []string{"code"},
69+ )
70+
71+ if err := registerer.Register(counter); err != nil {
72+ if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
73+ counter = are.ExistingCollector.(*prometheus.CounterVec)
74+ } else {
75+ panic(err)
76+ }
77+ }
78+
79+ return counter
80+}
81+
82+// registerInflightRequestsGauge registers 'oauth2_proxy_requests_in_flight'
83+// This only keeps the count of currently in progress HTTP requests
84+func registerInflightRequestsGauge(registerer prometheus.Registerer) prometheus.Gauge {
85+ gauge := prometheus.NewGauge(prometheus.GaugeOpts{
86+ Name: "oauth2_proxy_requests_in_flight",
87+ Help: "Current number of requests being served.",
88+ })
89+
90+ if err := registerer.Register(gauge); err != nil {
91+ if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
92+ gauge = are.ExistingCollector.(prometheus.Gauge)
93+ } else {
94+ panic(err)
95+ }
96+ }
97+
98+ return gauge
99+}
100+
101+// registerRequestsLatencyHistogram registers 'oauth2_proxy_response_duration_seconds'
102+// This keeps tally of the requests bucketed by the time taken to process the request
103+func registerRequestsLatencyHistogram(registerer prometheus.Registerer) *prometheus.HistogramVec {
104+ histogram := prometheus.NewHistogramVec(
105+ prometheus.HistogramOpts{
106+ Name: "oauth2_proxy_response_duration_seconds",
107+ Help: "A histogram of request latencies.",
108+ Buckets: prometheus.DefBuckets,
109+ },
110+ []string{"method"},
111+ )
112+
113+ if err := registerer.Register(histogram); err != nil {
114+ if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
115+ histogram = are.ExistingCollector.(*prometheus.HistogramVec)
116+ } else {
117+ panic(err)
118+ }
119+ }
120+
121+ return histogram
122+}
pkg/middleware/metrics_test.gonew file+67
@@ -0,0 +1,67 @@
1+package middleware
2+
3+import (
4+ "net/http"
5+ "net/http/httptest"
6+ "os"
7+
8+ . "github.com/onsi/ginkgo"
9+ . "github.com/onsi/ginkgo/extensions/table"
10+ . "github.com/onsi/gomega"
11+
12+ "github.com/prometheus/client_golang/prometheus"
13+ "github.com/prometheus/client_golang/prometheus/testutil"
14+)
15+
16+var _ = Describe("Instrumentation suite", func() {
17+ type requestTableInput struct {
18+ registry *prometheus.Registry
19+ requestString string
20+ expectedHandler http.Handler
21+ expectedMetrics []string
22+ expectedStatus int
23+ // Prometheus output is large so is stored in testdata
24+ expectedResultsFile string
25+ }
26+
27+ DescribeTable("when serving a request",
28+ func(in *requestTableInput) {
29+ req := httptest.NewRequest("", in.requestString, nil)
30+
31+ rw := httptest.NewRecorder()
32+
33+ handler := NewRequestMetrics(in.registry)(in.expectedHandler)
34+ handler.ServeHTTP(rw, req)
35+
36+ Expect(rw.Code).To(Equal(in.expectedStatus))
37+
38+ expectedPrometheusText, err := os.Open(in.expectedResultsFile)
39+ Expect(err).NotTo(HaveOccurred())
40+
41+ err = testutil.GatherAndCompare(in.registry, expectedPrometheusText, in.expectedMetrics...)
42+ Expect(err).NotTo(HaveOccurred())
43+ },
44+ Entry("successfully", func() *requestTableInput {
45+ in := &requestTableInput{
46+ registry: prometheus.NewRegistry(),
47+ requestString: "http://example.com/metrics",
48+ expectedMetrics: []string{
49+ "oauth2_proxy_requests_total",
50+ },
51+ expectedStatus: 200,
52+ expectedResultsFile: "testdata/metrics/successfulrequest.txt",
53+ }
54+ in.expectedHandler = NewMetricsHandler(in.registry, in.registry)
55+
56+ return in
57+ }()),
58+ Entry("with not found", &requestTableInput{
59+ registry: prometheus.NewRegistry(),
60+ requestString: "http://example.com/",
61+ expectedHandler: http.NotFoundHandler(),
62+ expectedMetrics: []string{"oauth2_proxy_requests_total"},
63+ expectedStatus: 404,
64+ expectedResultsFile: "testdata/metrics/notfoundrequest.txt",
65+ }),
66+ )
67+})
pkg/middleware/middleware_suite_test.go+1
@@ -11,6 +11,7 @@ import (
1111
1212func TestMiddlewareSuite(t *testing.T) {
1313 logger.SetOutput(GinkgoWriter)
14+ logger.SetErrOutput(GinkgoWriter)
1415
1516 RegisterFailHandler(Fail)
1617 RunSpecs(t, "Middleware")
pkg/middleware/redirect_to_https.go+7−6
@@ -7,7 +7,7 @@ import (
77 "strings"
88
99 "github.com/justinas/alice"
10− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
10+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1111)
1212
1313const httpsScheme = "https"
@@ -26,10 +26,11 @@ func NewRedirectToHTTPS(httpsPort string) alice.Constructor {
2626// to the port from the httpsAddress given.
2727func redirectToHTTPS(httpsPort string, next http.Handler) http.Handler {
2828 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
29− proto := req.Header.Get("X-Forwarded-Proto")
30− if strings.EqualFold(proto, httpsScheme) || (req.TLS != nil && proto == "") {
31− // Only care about the connection to us being HTTPS if the proto is empty,
32− // otherwise the proto is source of truth
29+ proto := requestutil.GetRequestProto(req)
30+ if strings.EqualFold(proto, httpsScheme) || (req.TLS != nil && proto == req.URL.Scheme) {
31+ // Only care about the connection to us being HTTPS if the proto wasn't
32+ // from a trusted `X-Forwarded-Proto` (proto == req.URL.Scheme).
33+ // Otherwise the proto is source of truth
3334 next.ServeHTTP(rw, req)
3435 return
3536 }
@@ -41,7 +42,7 @@ func redirectToHTTPS(httpsPort string, next http.Handler) http.Handler {
4142
4243 // Set the Host in case the targetURL still does not have one
4344 // or it isn't X-Forwarded-Host aware
44− targetURL.Host = util.GetRequestHost(req)
45+ targetURL.Host = requestutil.GetRequestHost(req)
4546
4647 // Overwrite the port if the original request was to a non-standard port
4748 if targetURL.Port() != "" {
pkg/middleware/redirect_to_https_test.go+31
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "net/http/httptest"
77
8+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
89 . "github.com/onsi/ginkgo"
910 . "github.com/onsi/ginkgo/extensions/table"
1011 . "github.com/onsi/gomega"
@@ -21,6 +22,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
2122 requestString string
2223 useTLS bool
2324 headers map[string]string
25+ reverseProxy bool
2426 expectedStatus int
2527 expectedBody string
2628 expectedLocation string
@@ -35,6 +37,10 @@ var _ = Describe("RedirectToHTTPS suite", func() {
3537 if in.useTLS {
3638 req.TLS = &tls.ConnectionState{}
3739 }
40+ scope := &middlewareapi.RequestScope{
41+ ReverseProxy: in.reverseProxy,
42+ }
43+ req = middlewareapi.AddRequestScope(req, scope)
3844
3945 rw := httptest.NewRecorder()
4046
@@ -52,6 +58,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
5258 requestString: "http://example.com",
5359 useTLS: false,
5460 headers: map[string]string{},
61+ reverseProxy: false,
5562 expectedStatus: 308,
5663 expectedBody: permanentRedirectBody("https://example.com"),
5764 expectedLocation: "https://example.com",
@@ -60,6 +67,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
6067 requestString: "https://example.com",
6168 useTLS: true,
6269 headers: map[string]string{},
70+ reverseProxy: false,
6371 expectedStatus: 200,
6472 expectedBody: "test",
6573 }),
@@ -69,15 +77,28 @@ var _ = Describe("RedirectToHTTPS suite", func() {
6977 headers: map[string]string{
7078 "X-Forwarded-Proto": "HTTPS",
7179 },
80+ reverseProxy: true,
7281 expectedStatus: 200,
7382 expectedBody: "test",
7483 }),
84+ Entry("without TLS and X-Forwarded-Proto=HTTPS but ReverseProxy not set", &requestTableInput{
85+ requestString: "http://example.com",
86+ useTLS: false,
87+ headers: map[string]string{
88+ "X-Forwarded-Proto": "HTTPS",
89+ },
90+ reverseProxy: false,
91+ expectedStatus: 308,
92+ expectedBody: permanentRedirectBody("https://example.com"),
93+ expectedLocation: "https://example.com",
94+ }),
7595 Entry("with TLS and X-Forwarded-Proto=HTTPS", &requestTableInput{
7696 requestString: "https://example.com",
7797 useTLS: true,
7898 headers: map[string]string{
7999 "X-Forwarded-Proto": "HTTPS",
80100 },
101+ reverseProxy: true,
81102 expectedStatus: 200,
82103 expectedBody: "test",
83104 }),
@@ -87,6 +108,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
87108 headers: map[string]string{
88109 "X-Forwarded-Proto": "https",
89110 },
111+ reverseProxy: true,
90112 expectedStatus: 200,
91113 expectedBody: "test",
92114 }),
@@ -96,6 +118,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
96118 headers: map[string]string{
97119 "X-Forwarded-Proto": "https",
98120 },
121+ reverseProxy: true,
99122 expectedStatus: 200,
100123 expectedBody: "test",
101124 }),
@@ -105,6 +128,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
105128 headers: map[string]string{
106129 "X-Forwarded-Proto": "HTTP",
107130 },
131+ reverseProxy: true,
108132 expectedStatus: 308,
109133 expectedBody: permanentRedirectBody("https://example.com"),
110134 expectedLocation: "https://example.com",
@@ -115,6 +139,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
115139 headers: map[string]string{
116140 "X-Forwarded-Proto": "HTTP",
117141 },
142+ reverseProxy: true,
118143 expectedStatus: 308,
119144 expectedBody: permanentRedirectBody("https://example.com"),
120145 expectedLocation: "https://example.com",
@@ -125,6 +150,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
125150 headers: map[string]string{
126151 "X-Forwarded-Proto": "http",
127152 },
153+ reverseProxy: true,
128154 expectedStatus: 308,
129155 expectedBody: permanentRedirectBody("https://example.com"),
130156 expectedLocation: "https://example.com",
@@ -135,6 +161,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
135161 headers: map[string]string{
136162 "X-Forwarded-Proto": "http",
137163 },
164+ reverseProxy: true,
138165 expectedStatus: 308,
139166 expectedBody: permanentRedirectBody("https://example.com"),
140167 expectedLocation: "https://example.com",
@@ -143,6 +170,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
143170 requestString: "http://example.com:8080",
144171 useTLS: false,
145172 headers: map[string]string{},
173+ reverseProxy: false,
146174 expectedStatus: 308,
147175 expectedBody: permanentRedirectBody("https://example.com:8443"),
148176 expectedLocation: "https://example.com:8443",
@@ -151,6 +179,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
151179 requestString: "https://example.com:8443",
152180 useTLS: true,
153181 headers: map[string]string{},
182+ reverseProxy: false,
154183 expectedStatus: 200,
155184 expectedBody: "test",
156185 }),
@@ -161,6 +190,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
161190 requestString: "/",
162191 useTLS: false,
163192 expectedStatus: 308,
193+ reverseProxy: false,
164194 expectedBody: permanentRedirectBody("https://example.com/"),
165195 expectedLocation: "https://example.com/",
166196 }),
@@ -171,6 +201,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
171201 "X-Forwarded-Proto": "HTTP",
172202 "X-Forwarded-Host": "external.example.com",
173203 },
204+ reverseProxy: true,
174205 expectedStatus: 308,
175206 expectedBody: permanentRedirectBody("https://external.example.com"),
176207 expectedLocation: "https://external.example.com",
pkg/middleware/scope.go+9−28
@@ -1,39 +1,20 @@
11package middleware
22
33import (
4− "context"
54 "net/http"
65
76 "github.com/justinas/alice"
87 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
98)
109
11−type scopeKey string
12−
13−// requestScopeKey uses a typed string to reduce likelihood of clasing
14−// with other context keys
15−const requestScopeKey scopeKey = "request-scope"
16−
17−func NewScope() alice.Constructor {
18− return addScope
19−}
20−
21−// addScope injects a new request scope into the request context.
22−func addScope(next http.Handler) http.Handler {
23− return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
24− scope := &middlewareapi.RequestScope{}
25− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
26− requestWithScope := req.WithContext(contextWithScope)
27− next.ServeHTTP(rw, requestWithScope)
28− })
29−}
30−
31−// GetRequestScope returns the current request scope from the given request
32−func GetRequestScope(req *http.Request) *middlewareapi.RequestScope {
33− scope := req.Context().Value(requestScopeKey)
34− if scope == nil {
35− return nil
10+func NewScope(reverseProxy bool) alice.Constructor {
11+ return func(next http.Handler) http.Handler {
12+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
13+ scope := &middlewareapi.RequestScope{
14+ ReverseProxy: reverseProxy,
15+ }
16+ req = middlewareapi.AddRequestScope(req, scope)
17+ next.ServeHTTP(rw, req)
18+ })
3619 }
37−
38− return scope.(*middlewareapi.RequestScope)
3920}
pkg/middleware/scope_test.go+32−57
@@ -1,7 +1,6 @@
11package middleware
22
33import (
4− "context"
54 "net/http"
65 "net/http/httptest"
76
@@ -21,73 +20,49 @@ var _ = Describe("Scope Suite", func() {
2120 Expect(err).ToNot(HaveOccurred())
2221
2322 rw = httptest.NewRecorder()
24−
25− handler := NewScope()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26− nextRequest = r
27− w.WriteHeader(200)
28− }))
29− handler.ServeHTTP(rw, request)
30− })
31−
32− It("does not add a scope to the original request", func() {
33− Expect(request.Context().Value(requestScopeKey)).To(BeNil())
34− })
35−
36− It("cannot load a scope from the original request using GetRequestScope", func() {
37− Expect(GetRequestScope(request)).To(BeNil())
38− })
39−
40− It("adds a scope to the request for the next handler", func() {
41− Expect(nextRequest.Context().Value(requestScopeKey)).ToNot(BeNil())
42− })
43−
44− It("can load a scope from the next handler's request using GetRequestScope", func() {
45− Expect(GetRequestScope(nextRequest)).ToNot(BeNil())
46− })
47− })
48−
49− Context("GetRequestScope", func() {
50− var request *http.Request
51−
52− BeforeEach(func() {
53− var err error
54− request, err = http.NewRequest("", "http://127.0.0.1/", nil)
55− Expect(err).ToNot(HaveOccurred())
5623 })
5724
58− Context("with a scope", func() {
59− var scope *middlewareapi.RequestScope
60−
25+ Context("ReverseProxy is false", func() {
6126 BeforeEach(func() {
62− scope = &middlewareapi.RequestScope{}
63− contextWithScope := context.WithValue(request.Context(), requestScopeKey, scope)
64− request = request.WithContext(contextWithScope)
27+ handler := NewScope(false)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+ nextRequest = r
29+ w.WriteHeader(200)
30+ }))
31+ handler.ServeHTTP(rw, request)
6532 })
6633
67− It("returns the scope", func() {
68− s := GetRequestScope(request)
69− Expect(s).ToNot(BeNil())
70− Expect(s).To(Equal(scope))
34+ It("does not add a scope to the original request", func() {
35+ Expect(request.Context().Value(middlewareapi.RequestScopeKey)).To(BeNil())
7136 })
7237
73− Context("if the scope is then modified", func() {
74− BeforeEach(func() {
75− Expect(scope.SaveSession).To(BeFalse())
76− scope.SaveSession = true
77− })
38+ It("cannot load a scope from the original request using GetRequestScope", func() {
39+ Expect(middlewareapi.GetRequestScope(request)).To(BeNil())
40+ })
41+
42+ It("adds a scope to the request for the next handler", func() {
43+ Expect(nextRequest.Context().Value(middlewareapi.RequestScopeKey)).ToNot(BeNil())
44+ })
7845
79− It("returns the updated session", func() {
80− s := GetRequestScope(request)
81− Expect(s).ToNot(BeNil())
82− Expect(s).To(Equal(scope))
83− Expect(s.SaveSession).To(BeTrue())
84− })
46+ It("can load a scope from the next handler's request using GetRequestScope", func() {
47+ scope := middlewareapi.GetRequestScope(nextRequest)
48+ Expect(scope).ToNot(BeNil())
49+ Expect(scope.ReverseProxy).To(BeFalse())
8550 })
8651 })
8752
88− Context("without a scope", func() {
89− It("returns nil", func() {
90− Expect(GetRequestScope(request)).To(BeNil())
53+ Context("ReverseProxy is true", func() {
54+ BeforeEach(func() {
55+ handler := NewScope(true)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+ nextRequest = r
57+ w.WriteHeader(200)
58+ }))
59+ handler.ServeHTTP(rw, request)
60+ })
61+
62+ It("return a scope where the ReverseProxy field is true", func() {
63+ scope := middlewareapi.GetRequestScope(nextRequest)
64+ Expect(scope).ToNot(BeNil())
65+ Expect(scope.ReverseProxy).To(BeTrue())
9166 })
9267 })
9368 })
pkg/middleware/stored_session.go+2−1
@@ -8,6 +8,7 @@ import (
88 "time"
99
1010 "github.com/justinas/alice"
11+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
1112 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1213 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1314)
@@ -59,7 +60,7 @@ type storedSessionLoader struct {
5960// If a session was loader by a previous handler, it will not be replaced.
6061func (s *storedSessionLoader) loadSession(next http.Handler) http.Handler {
6162 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
62− scope := GetRequestScope(req)
63+ scope := middlewareapi.GetRequestScope(req)
6364 // If scope is nil, this will panic.
6465 // A scope should always be injected before this handler is called.
6566 if scope.Session != nil {
pkg/middleware/stored_session_test.go+2−3
@@ -104,8 +104,7 @@ var _ = Describe("Stored Session Suite", func() {
104104 // Set up the request with the request headesr and a request scope
105105 req := httptest.NewRequest("", "/", nil)
106106 req.Header = in.requestHeaders
107− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
108− req = req.WithContext(contextWithScope)
107+ req = middlewareapi.AddRequestScope(req, scope)
109108
110109 rw := httptest.NewRecorder()
111110
@@ -120,7 +119,7 @@ var _ = Describe("Stored Session Suite", func() {
120119 // from the scope
121120 var gotSession *sessionsapi.SessionState
122121 handler := NewStoredSessionLoader(opts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
123− gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
122+ gotSession = middlewareapi.GetRequestScope(r).Session
124123 }))
125124 handler.ServeHTTP(rw, req)
126125
pkg/middleware/testdata/metrics/notfoundrequest.txtnew file+3
@@ -0,0 +1,3 @@
1+# HELP oauth2_proxy_requests_total Total number of requests by HTTP status code.
2+# TYPE oauth2_proxy_requests_total counter
3+oauth2_proxy_requests_total{code="404"} 1
pkg/middleware/testdata/metrics/successfulrequest.txtnew file+3
@@ -0,0 +1,3 @@
1+# HELP oauth2_proxy_requests_total Total number of requests by HTTP status code.
2+# TYPE oauth2_proxy_requests_total counter
3+oauth2_proxy_requests_total{code="200"} 1
pkg/requests/requests_suite_test.go+1
@@ -21,6 +21,7 @@ var (
2121
2222func TestRequetsSuite(t *testing.T) {
2323 logger.SetOutput(GinkgoWriter)
24+ logger.SetErrOutput(GinkgoWriter)
2425 log.SetOutput(GinkgoWriter)
2526
2627 RegisterFailHandler(Fail)
pkg/requests/util/util.gonew file+48
@@ -0,0 +1,48 @@
1+package util
2+
3+import (
4+ "net/http"
5+
6+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+)
8+
9+// GetRequestProto returns the request scheme or X-Forwarded-Proto if present
10+// and the request is proxied.
11+func GetRequestProto(req *http.Request) string {
12+ proto := req.Header.Get("X-Forwarded-Proto")
13+ if !IsProxied(req) || proto == "" {
14+ proto = req.URL.Scheme
15+ }
16+ return proto
17+}
18+
19+// GetRequestHost returns the request host header or X-Forwarded-Host if
20+// present and the request is proxied.
21+func GetRequestHost(req *http.Request) string {
22+ host := req.Header.Get("X-Forwarded-Host")
23+ if !IsProxied(req) || host == "" {
24+ host = req.Host
25+ }
26+ return host
27+}
28+
29+// GetRequestURI return the request URI or X-Forwarded-Uri if present and the
30+// request is proxied.
31+func GetRequestURI(req *http.Request) string {
32+ uri := req.Header.Get("X-Forwarded-Uri")
33+ if !IsProxied(req) || uri == "" {
34+ // Use RequestURI to preserve ?query
35+ uri = req.URL.RequestURI()
36+ }
37+ return uri
38+}
39+
40+// IsProxied determines if a request was from a proxy based on the RequestScope
41+// ReverseProxy tracker.
42+func IsProxied(req *http.Request) bool {
43+ scope := middlewareapi.GetRequestScope(req)
44+ if scope == nil {
45+ return false
46+ }
47+ return scope.ReverseProxy
48+}
pkg/requests/util/util_suite_test.gonew file+20
@@ -0,0 +1,20 @@
1+package util_test
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+// TestRequestUtilSuite and related tests are in a *_test package
12+// to prevent circular imports with the `logger` package which uses
13+// this functionality
14+func TestRequestUtilSuite(t *testing.T) {
15+ logger.SetOutput(GinkgoWriter)
16+ logger.SetErrOutput(GinkgoWriter)
17+
18+ RegisterFailHandler(Fail)
19+ RunSpecs(t, "Request Utils")
20+}
pkg/requests/util/util_test.gonew file+131
@@ -0,0 +1,131 @@
1+package util_test
2+
3+import (
4+ "fmt"
5+ "net/http"
6+ "net/http/httptest"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var _ = Describe("Util Suite", func() {
15+ const (
16+ proto = "http"
17+ host = "www.oauth2proxy.test"
18+ uri = "/test/endpoint"
19+ )
20+ var req *http.Request
21+
22+ BeforeEach(func() {
23+ req = httptest.NewRequest(
24+ http.MethodGet,
25+ fmt.Sprintf("%s://%s%s", proto, host, uri),
26+ nil,
27+ )
28+ })
29+
30+ Context("GetRequestHost", func() {
31+ Context("IsProxied is false", func() {
32+ BeforeEach(func() {
33+ req = middleware.AddRequestScope(req, &middleware.RequestScope{})
34+ })
35+
36+ It("returns the host", func() {
37+ Expect(util.GetRequestHost(req)).To(Equal(host))
38+ })
39+
40+ It("ignores X-Forwarded-Host and returns the host", func() {
41+ req.Header.Add("X-Forwarded-Host", "external.oauth2proxy.text")
42+ Expect(util.GetRequestHost(req)).To(Equal(host))
43+ })
44+ })
45+
46+ Context("IsProxied is true", func() {
47+ BeforeEach(func() {
48+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
49+ ReverseProxy: true,
50+ })
51+ })
52+
53+ It("returns the host if X-Forwarded-Host is not present", func() {
54+ Expect(util.GetRequestHost(req)).To(Equal(host))
55+ })
56+
57+ It("returns the X-Forwarded-Host when present", func() {
58+ req.Header.Add("X-Forwarded-Host", "external.oauth2proxy.text")
59+ Expect(util.GetRequestHost(req)).To(Equal("external.oauth2proxy.text"))
60+ })
61+ })
62+ })
63+
64+ Context("GetRequestProto", func() {
65+ Context("IsProxied is false", func() {
66+ BeforeEach(func() {
67+ req = middleware.AddRequestScope(req, &middleware.RequestScope{})
68+ })
69+
70+ It("returns the scheme", func() {
71+ Expect(util.GetRequestProto(req)).To(Equal(proto))
72+ })
73+
74+ It("ignores X-Forwarded-Proto and returns the scheme", func() {
75+ req.Header.Add("X-Forwarded-Proto", "https")
76+ Expect(util.GetRequestProto(req)).To(Equal(proto))
77+ })
78+ })
79+
80+ Context("IsProxied is true", func() {
81+ BeforeEach(func() {
82+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
83+ ReverseProxy: true,
84+ })
85+ })
86+
87+ It("returns the scheme if X-Forwarded-Proto is not present", func() {
88+ Expect(util.GetRequestProto(req)).To(Equal(proto))
89+ })
90+
91+ It("returns the X-Forwarded-Proto when present", func() {
92+ req.Header.Add("X-Forwarded-Proto", "https")
93+ Expect(util.GetRequestProto(req)).To(Equal("https"))
94+ })
95+ })
96+ })
97+
98+ Context("GetRequestURI", func() {
99+ Context("IsProxied is false", func() {
100+ BeforeEach(func() {
101+ req = middleware.AddRequestScope(req, &middleware.RequestScope{})
102+ })
103+
104+ It("returns the URI", func() {
105+ Expect(util.GetRequestURI(req)).To(Equal(uri))
106+ })
107+
108+ It("ignores X-Forwarded-Uri and returns the URI", func() {
109+ req.Header.Add("X-Forwarded-Uri", "/some/other/path")
110+ Expect(util.GetRequestURI(req)).To(Equal(uri))
111+ })
112+ })
113+
114+ Context("IsProxied is true", func() {
115+ BeforeEach(func() {
116+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
117+ ReverseProxy: true,
118+ })
119+ })
120+
121+ It("returns the URI if X-Forwarded-Uri is not present", func() {
122+ Expect(util.GetRequestURI(req)).To(Equal(uri))
123+ })
124+
125+ It("returns the X-Forwarded-Uri when present", func() {
126+ req.Header.Add("X-Forwarded-Uri", "/some/other/path")
127+ Expect(util.GetRequestURI(req)).To(Equal("/some/other/path"))
128+ })
129+ })
130+ })
131+})
pkg/sessions/cookie/session_store.go+3−4
@@ -5,7 +5,6 @@ import (
55 "fmt"
66 "net/http"
77 "regexp"
8− "strings"
98 "time"
109
1110 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
@@ -220,12 +219,12 @@ func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) {
220219 if len(cookies) == 0 {
221220 return nil, fmt.Errorf("could not find cookie %s", cookieName)
222221 }
223− return joinCookies(cookies)
222+ return joinCookies(cookies, cookieName)
224223}
225224
226225// joinCookies takes a slice of cookies from the request and reconstructs the
227226// full session cookie
228−func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) {
227+func joinCookies(cookies []*http.Cookie, cookieName string) (*http.Cookie, error) {
229228 if len(cookies) == 0 {
230229 return nil, fmt.Errorf("list of cookies must be > 0")
231230 }
@@ -236,7 +235,7 @@ func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) {
236235 for i := 1; i < len(cookies); i++ {
237236 c.Value += cookies[i].Value
238237 }
239− c.Name = strings.TrimRight(c.Name, "_0")
238+ c.Name = cookieName
240239 return c, nil
241240}
242241
pkg/sessions/cookie/session_store_test.go+52−1
@@ -19,6 +19,8 @@ import (
1919
2020func TestSessionStore(t *testing.T) {
2121 logger.SetOutput(GinkgoWriter)
22+ logger.SetErrOutput(GinkgoWriter)
23+
2224 RegisterFailHandler(Fail)
2325 RunSpecs(t, "Cookie SessionStore")
2426}
@@ -154,9 +156,58 @@ func Test_splitCookie_joinCookies(t *testing.T) {
154156 Value: value,
155157 }
156158 splitCookies := splitCookie(cookie)
157− joinedCookie, err := joinCookies(splitCookies)
159+ joinedCookie, err := joinCookies(splitCookies, cookie.Name)
158160 assert.NoError(t, err)
159161 assert.Equal(t, *cookie, *joinedCookie)
160162 })
161163 }
162164}
165+
166+func Test_joinCookies_withUnderlineSuffix(t *testing.T) {
167+ testCases := map[string]struct {
168+ CookieName string
169+ SplitOrder []int
170+ }{
171+ "Ascending order split with \"_\" suffix": {
172+ CookieName: "_cookie_name_",
173+ SplitOrder: []int{0, 1, 2, 3, 4},
174+ },
175+ "Descending order split with \"_\" suffix": {
176+ CookieName: "_cookie_name_",
177+ SplitOrder: []int{4, 3, 2, 1, 0},
178+ },
179+ "Arbitrary order split with \"_\" suffix": {
180+ CookieName: "_cookie_name_",
181+ SplitOrder: []int{3, 1, 2, 0, 4},
182+ },
183+ "Arbitrary order split with \"_0\" suffix": {
184+ CookieName: "_cookie_name_0",
185+ SplitOrder: []int{1, 3, 0, 2, 4},
186+ },
187+ "Arbitrary order split with \"_1\" suffix": {
188+ CookieName: "_cookie_name_1",
189+ SplitOrder: []int{4, 1, 3, 0, 2},
190+ },
191+ "Arbitrary order split with \"__\" suffix": {
192+ CookieName: "_cookie_name__",
193+ SplitOrder: []int{1, 0, 4, 3, 2},
194+ },
195+ }
196+
197+ for testName, testCase := range testCases {
198+ t.Run(testName, func(t *testing.T) {
199+ cookieName := testCase.CookieName
200+ var splitCookies []*http.Cookie
201+ for _, splitSuffix := range testCase.SplitOrder {
202+ cookie := &http.Cookie{
203+ Name: splitCookieName(cookieName, splitSuffix),
204+ Value: strings.Repeat("v", 1000),
205+ }
206+ splitCookies = append(splitCookies, cookie)
207+ }
208+ joinedCookie, err := joinCookies(splitCookies, cookieName)
209+ assert.NoError(t, err)
210+ assert.Equal(t, cookieName, joinedCookie.Name)
211+ })
212+ }
213+}
pkg/sessions/persistence/persistence_suite_test.go+2
@@ -10,6 +10,8 @@ import (
1010
1111func TestPersistenceSuite(t *testing.T) {
1212 logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
14+
1315 RegisterFailHandler(Fail)
1416 RunSpecs(t, "Persistence")
1517}
pkg/sessions/redis/redis_store_test.go+1
@@ -33,6 +33,7 @@ func (l *wrappedRedisLogger) Printf(_ context.Context, format string, v ...inter
3333
3434func TestSessionStore(t *testing.T) {
3535 logger.SetOutput(GinkgoWriter)
36+ logger.SetErrOutput(GinkgoWriter)
3637
3738 redisLogger := &wrappedRedisLogger{Logger: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile)}
3839 redisLogger.SetOutput(GinkgoWriter)
pkg/sessions/session_store_test.go+1
@@ -18,6 +18,7 @@ import (
1818
1919func TestSessionStore(t *testing.T) {
2020 logger.SetOutput(GinkgoWriter)
21+ logger.SetErrOutput(GinkgoWriter)
2122
2223 RegisterFailHandler(Fail)
2324 RunSpecs(t, "SessionStore")
pkg/upstream/proxy.go−22
@@ -2,7 +2,6 @@ package upstream
22
33import (
44 "fmt"
5− "html/template"
65 "net/http"
76 "net/url"
87
@@ -71,24 +70,3 @@ func (m *multiUpstreamProxy) registerHTTPUpstreamProxy(upstream options.Upstream
7170 logger.Printf("mapping path %q => upstream %q", upstream.Path, upstream.URI)
7271 m.serveMux.Handle(upstream.Path, newHTTPUpstreamProxy(upstream, u, sigData, errorHandler))
7372}
74−
75−// NewProxyErrorHandler creates a ProxyErrorHandler using the template given.
76−func NewProxyErrorHandler(errorTemplate *template.Template, proxyPrefix string) ProxyErrorHandler {
77− return func(rw http.ResponseWriter, req *http.Request, proxyErr error) {
78− logger.Errorf("Error proxying to upstream server: %v", proxyErr)
79− rw.WriteHeader(http.StatusBadGateway)
80− data := struct {
81− Title string
82− Message string
83− ProxyPrefix string
84− }{
85− Title: "Bad Gateway",
86− Message: "Error proxying to upstream server",
87− ProxyPrefix: proxyPrefix,
88− }
89− err := errorTemplate.Execute(rw, data)
90− if err != nil {
91− http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
92− }
93− }
94−}
pkg/upstream/proxy_test.go+6−5
@@ -4,7 +4,6 @@ import (
44 "crypto"
55 "encoding/json"
66 "fmt"
7− "html/template"
87 "net/http"
98 "net/http/httptest"
109
@@ -20,9 +19,10 @@ var _ = Describe("Proxy Suite", func() {
2019 BeforeEach(func() {
2120 sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"}
2221
23− tmpl, err := template.New("").Parse("{{ .Title }}\n{{ .Message }}\n{{ .ProxyPrefix }}")
24− Expect(err).ToNot(HaveOccurred())
25− errorHandler := NewProxyErrorHandler(tmpl, "prefix")
22+ errorHandler := func(rw http.ResponseWriter, _ *http.Request, _ error) {
23+ rw.WriteHeader(502)
24+ rw.Write([]byte("Proxy Error"))
25+ }
2626
2727 ok := http.StatusOK
2828
@@ -56,6 +56,7 @@ var _ = Describe("Proxy Suite", func() {
5656 },
5757 }
5858
59+ var err error
5960 upstreamServer, err = NewProxy(upstreams, sigData, errorHandler)
6061 Expect(err).ToNot(HaveOccurred())
6162 })
@@ -143,7 +144,7 @@ var _ = Describe("Proxy Suite", func() {
143144 gapUpstream: {"bad-http-backend"},
144145 },
145146 // This tests the error handler
146− raw: "Bad Gateway\nError proxying to upstream server\nprefix",
147+ raw: "Proxy Error",
147148 },
148149 }),
149150 Entry("with a request to the to an unregistered path", &proxyTableInput{
pkg/upstream/upstream_suite_test.go+1
@@ -26,6 +26,7 @@ var (
2626
2727func TestUpstreamSuite(t *testing.T) {
2828 logger.SetOutput(GinkgoWriter)
29+ logger.SetErrOutput(GinkgoWriter)
2930 log.SetOutput(GinkgoWriter)
3031
3132 RegisterFailHandler(Fail)
pkg/util/util.go−10
@@ -4,7 +4,6 @@ import (
44 "crypto/x509"
55 "fmt"
66 "io/ioutil"
7− "net/http"
87)
98
109func GetCertPool(paths []string) (*x509.CertPool, error) {
@@ -24,12 +23,3 @@ func GetCertPool(paths []string) (*x509.CertPool, error) {
2423 }
2524 return pool, nil
2625}
27−
28−// GetRequestHost return the request host header or X-Forwarded-Host if present
29−func GetRequestHost(req *http.Request) string {
30− host := req.Header.Get("X-Forwarded-Host")
31− if host == "" {
32− host = req.Host
33− }
34− return host
35−}
pkg/util/util_test.go−15
@@ -4,11 +4,9 @@ import (
44 "crypto/x509/pkix"
55 "encoding/asn1"
66 "io/ioutil"
7− "net/http/httptest"
87 "os"
98 "testing"
109
11− . "github.com/onsi/gomega"
1210 "github.com/stretchr/testify/assert"
1311)
1412
@@ -97,16 +95,3 @@ func TestGetCertPool(t *testing.T) {
9795 expectedSubjects := []string{testCA1Subj, testCA2Subj}
9896 assert.Equal(t, expectedSubjects, got)
9997}
100−
101−func TestGetRequestHost(t *testing.T) {
102− g := NewWithT(t)
103−
104− req := httptest.NewRequest("GET", "https://example.com", nil)
105− host := GetRequestHost(req)
106− g.Expect(host).To(Equal("example.com"))
107−
108− proxyReq := httptest.NewRequest("GET", "http://internal.example.com", nil)
109− proxyReq.Header.Add("X-Forwarded-Host", "external.example.com")
110− extHost := GetRequestHost(proxyReq)
111− g.Expect(extHost).To(Equal("external.example.com"))
112−}
pkg/validation/common.go+1−10
@@ -1,7 +1,6 @@
11package validation
22
33import (
4− "encoding/base64"
54 "fmt"
65 "os"
76
@@ -13,7 +12,7 @@ const multipleValuesForSecretSource = "multiple values specified for secret sour
1312func validateSecretSource(source options.SecretSource) string {
1413 switch {
1514 case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "":
16− return validateSecretSourceValue(source.Value)
15+ return ""
1716 case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "":
1817 return validateSecretSourceEnv(source.FromEnv)
1918 case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "":
@@ -23,14 +22,6 @@ func validateSecretSource(source options.SecretSource) string {
2322 }
2423}
2524
26−func validateSecretSourceValue(value []byte) string {
27− dst := make([]byte, len(value))
28− if _, err := base64.StdEncoding.Decode(dst, value); err != nil {
29− return fmt.Sprintf("error decoding secret value: %v", err)
30− }
31− return ""
32−}
33−
3425func validateSecretSourceEnv(key string) string {
3526 if value := os.Getenv(key); value == "" {
3627 return fmt.Sprintf("error loading secret from environent: no value for for key %q", key)
pkg/validation/common_test.go+1−10
@@ -1,7 +1,6 @@
11package validation
22
33import (
4− "encoding/base64"
54 "io/ioutil"
65 "os"
76
@@ -17,7 +16,7 @@ var _ = Describe("Common", func() {
1716 var validSecretSourceFile string
1817
1918 BeforeEach(func() {
20− validSecretSourceValue = []byte(base64.StdEncoding.EncodeToString([]byte("This is a secret source value")))
19+ validSecretSourceValue = []byte("This is a secret source value")
2120 Expect(os.Setenv(validSecretSourceEnv, "This is a secret source env")).To(Succeed())
2221 tmp, err := ioutil.TempFile("", "oauth2-proxy-secret-source-test")
2322 Expect(err).ToNot(HaveOccurred())
@@ -110,14 +109,6 @@ var _ = Describe("Common", func() {
110109 },
111110 expectedMsg: "",
112111 }),
113− Entry("with an invalid Value", validateSecretSourceTableInput{
114− source: func() options.SecretSource {
115− return options.SecretSource{
116− Value: []byte("Invalid Base64 Value"),
117− }
118− },
119− expectedMsg: "error decoding secret value: illegal base64 data at input byte 7",
120− }),
121112 Entry("with an invalid FromEnv", validateSecretSourceTableInput{
122113 source: func() options.SecretSource {
123114 return options.SecretSource{
pkg/validation/header_test.go+2−2
@@ -148,7 +148,7 @@ var _ = Describe("Headers", func() {
148148 ClaimSource: &options.ClaimSource{
149149 Claim: "user",
150150 BasicAuthPassword: &options.SecretSource{
151− Value: []byte("secret"),
151+ FromEnv: "UNKNOWN_ENV",
152152 },
153153 },
154154 },
@@ -157,7 +157,7 @@ var _ = Describe("Headers", func() {
157157 validHeader1,
158158 },
159159 expectedMsgs: []string{
160− "invalid header \"With-Invalid-Basic-Auth\": invalid values: invalid basicAuthPassword: error decoding secret value: illegal base64 data at input byte 4",
160+ "invalid header \"With-Invalid-Basic-Auth\": invalid values: invalid basicAuthPassword: error loading secret from environent: no value for for key \"UNKNOWN_ENV\"",
161161 },
162162 }),
163163 )
pkg/validation/options.go+26−11
@@ -29,7 +29,7 @@ func Validate(o *options.Options) error {
2929 msgs = append(msgs, validateSessionCookieMinimal(o)...)
3030 msgs = append(msgs, validateRedisSessionStore(o)...)
3131 msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
32− msgs = append(msgs, prefixValues("injectRespeonseHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
32+ msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
3333
3434 if o.SSLInsecureSkipVerify {
3535 // InsecureSkipVerify is a configurable option we allow
@@ -41,10 +41,10 @@ func Validate(o *options.Options) error {
4141 } else if len(o.ProviderCAFiles) > 0 {
4242 pool, err := util.GetCertPool(o.ProviderCAFiles)
4343 if err == nil {
44− transport := &http.Transport{
45− TLSClientConfig: &tls.Config{
46− RootCAs: pool,
47− },
44+ transport := http.DefaultTransport.(*http.Transport).Clone()
45+ transport.TLSClientConfig = &tls.Config{
46+ RootCAs: pool,
47+ MinVersion: tls.VersionTLS12,
4848 }
4949
5050 http.DefaultClient = &http.Client{Transport: transport}
@@ -233,9 +233,19 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
233233 p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
234234 p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
235235
236− // Make the OIDC Verifier accessible to all providers that can support it
236+ // Make the OIDC options available to all providers that support it
237+ p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
238+ p.EmailClaim = o.OIDCEmailClaim
239+ p.GroupsClaim = o.OIDCGroupsClaim
237240 p.Verifier = o.GetOIDCVerifier()
238241
242+ // TODO (@NickMeves) - Remove This
243+ // Backwards Compatibility for Deprecated UserIDClaim option
244+ if o.OIDCEmailClaim == providers.OIDCEmailClaim &&
245+ o.UserIDClaim != providers.OIDCEmailClaim {
246+ p.EmailClaim = o.UserIDClaim
247+ }
248+
239249 p.SetAllowedGroups(o.AllowedGroups)
240250
241251 provider := providers.New(o.ProviderType, p)
@@ -255,7 +265,10 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
255265 p.SetRepo(o.GitHubRepo, o.GitHubToken)
256266 p.SetUsers(o.GitHubUsers)
257267 case *providers.KeycloakProvider:
258− p.SetGroup(o.KeycloakGroup)
268+ // Backwards compatibility with `--keycloak-group` option
269+ if len(o.KeycloakGroups) > 0 {
270+ p.SetAllowedGroups(o.KeycloakGroups)
271+ }
259272 case *providers.GoogleProvider:
260273 if o.GoogleServiceAccountJSON != "" {
261274 file, err := os.Open(o.GoogleServiceAccountJSON)
@@ -275,15 +288,17 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
275288 p.SetTeam(o.BitbucketTeam)
276289 p.SetRepository(o.BitbucketRepository)
277290 case *providers.OIDCProvider:
278− p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
279− p.UserIDClaim = o.UserIDClaim
280− p.GroupsClaim = o.OIDCGroupsClaim
281291 if p.Verifier == nil {
282292 msgs = append(msgs, "oidc provider requires an oidc issuer URL")
283293 }
284294 case *providers.GitLabProvider:
285− p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
286295 p.Groups = o.GitLabGroup
296+ err := p.AddProjects(o.GitlabProjects)
297+ if err != nil {
298+ msgs = append(msgs, "failed to setup gitlab project access level")
299+ }
300+ p.SetAllowedGroups(p.PrefixAllowedGroups())
301+ p.SetProjectScope()
287302
288303 if p.Verifier == nil {
289304 // Initialize with default verifier for gitlab.com
pkg/validation/validation_suite_test.go+1
@@ -10,6 +10,7 @@ import (
1010
1111func TestValidationSuite(t *testing.T) {
1212 logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
1314
1415 RegisterFailHandler(Fail)
1516 RunSpecs(t, "Validation Suite")
providers/azure.go+3−3
@@ -172,7 +172,7 @@ func (p *AzureProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.
172172 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
173173 }
174174
175− fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
175+ logger.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
176176 return true, nil
177177}
178178
@@ -279,7 +279,7 @@ func (p *AzureProvider) GetLoginURL(redirectURI, state string) string {
279279 return a.String()
280280}
281281
282−// ValidateSessionState validates the AccessToken
283−func (p *AzureProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
282+// ValidateSession validates the AccessToken
283+func (p *AzureProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
284284 return validateToken(ctx, p, s.AccessToken, makeAzureHeader(s.AccessToken))
285285}
providers/digitalocean.go+1−1
@@ -82,7 +82,7 @@ func (p *DigitalOceanProvider) GetEmailAddress(ctx context.Context, s *sessions.
8282 return email, nil
8383}
8484
85−// ValidateSessionState validates the AccessToken
85+// ValidateSession validates the AccessToken
8686func (p *DigitalOceanProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
8787 return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
8888}
providers/github.go+2−2
@@ -102,7 +102,7 @@ func (p *GitHubProvider) SetUsers(users []string) {
102102 p.Users = users
103103}
104104
105−// EnrichSessionState updates the User & Email after the initial Redeem
105+// EnrichSession updates the User & Email after the initial Redeem
106106func (p *GitHubProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
107107 err := p.getEmail(ctx, s)
108108 if err != nil {
@@ -111,7 +111,7 @@ func (p *GitHubProvider) EnrichSession(ctx context.Context, s *sessions.SessionS
111111 return p.getUser(ctx, s)
112112}
113113
114−// ValidateSessionState validates the AccessToken
114+// ValidateSession validates the AccessToken
115115func (p *GitHubProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
116116 return validateToken(ctx, p, s.AccessToken, makeGitHubHeader(s.AccessToken))
117117}
providers/gitlab.go+177−38
@@ -3,10 +3,13 @@ package providers
33import (
44 "context"
55 "fmt"
6+ "net/url"
7+ "strconv"
8+ "strings"
69 "time"
710
8− oidc "github.com/coreos/go-oidc"
911 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1013 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
1114 "golang.org/x/oauth2"
1215)
@@ -15,9 +18,50 @@ import (
1518type GitLabProvider struct {
1619 *ProviderData
1720
18− Groups []string
19− Verifier *oidc.IDTokenVerifier
20− AllowUnverifiedEmail bool
21+ Groups []string
22+ Projects []*GitlabProject
23+}
24+
25+// GitlabProject represents a Gitlab project constraint entity
26+type GitlabProject struct {
27+ Name string
28+ AccessLevel int
29+}
30+
31+// newGitlabProject Creates a new GitlabProject struct from project string formatted as namespace/project=accesslevel
32+// if no accesslevel provided, use the default one
33+func newGitlabproject(project string) (*GitlabProject, error) {
34+ // default access level is 20
35+ defaultAccessLevel := 20
36+ // see https://docs.gitlab.com/ee/api/members.html#valid-access-levels
37+ validAccessLevel := [4]int{10, 20, 30, 40}
38+
39+ parts := strings.SplitN(project, "=", 2)
40+
41+ if len(parts) == 2 {
42+ lvl, err := strconv.Atoi(parts[1])
43+ if err != nil {
44+ return nil, err
45+ }
46+
47+ for _, valid := range validAccessLevel {
48+ if lvl == valid {
49+ return &GitlabProject{
50+ Name: parts[0],
51+ AccessLevel: lvl},
52+ err
53+ }
54+ }
55+
56+ return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0])
57+
58+ }
59+
60+ return &GitlabProject{
61+ Name: project,
62+ AccessLevel: defaultAccessLevel},
63+ nil
64+
2165}
2266
2367var _ Provider = (*GitLabProvider)(nil)
@@ -57,13 +101,26 @@ func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) (
57101 if err != nil {
58102 return nil, fmt.Errorf("token exchange: %v", err)
59103 }
60− s, err = p.createSessionState(ctx, token)
104+ s, err = p.createSession(ctx, token)
61105 if err != nil {
62106 return nil, fmt.Errorf("unable to update session: %v", err)
63107 }
64108 return
65109}
66110
111+// SetProjectScope ensure read_api is added to scope when filtering on projects
112+func (p *GitLabProvider) SetProjectScope() {
113+ if len(p.Projects) > 0 {
114+ for _, val := range strings.Split(p.Scope, " ") {
115+ if val == "read_api" {
116+ return
117+ }
118+
119+ }
120+ p.Scope += " read_api"
121+ }
122+}
123+
67124// RefreshSessionIfNeeded checks if the session has expired and uses the
68125// RefreshToken to fetch a new ID token if required
69126func (p *GitLabProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
@@ -78,7 +135,7 @@ func (p *GitLabProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions
78135 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
79136 }
80137
81− fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
138+ logger.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
82139 return true, nil
83140}
84141
@@ -103,7 +160,7 @@ func (p *GitLabProvider) redeemRefreshToken(ctx context.Context, s *sessions.Ses
103160 if err != nil {
104161 return fmt.Errorf("failed to get token: %v", err)
105162 }
106− newSession, err := p.createSessionState(ctx, token)
163+ newSession, err := p.createSession(ctx, token)
107164 if err != nil {
108165 return fmt.Errorf("unable to update session: %v", err)
109166 }
@@ -144,56 +201,86 @@ func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionSta
144201 return &userInfo, nil
145202}
146203
147−func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error {
148− if len(p.Groups) == 0 {
149− return nil
150− }
204+type gitlabPermissionAccess struct {
205+ AccessLevel int `json:"access_level"`
206+}
207+
208+type gitlabProjectPermission struct {
209+ ProjectAccess *gitlabPermissionAccess `json:"project_access"`
210+ GroupAccess *gitlabPermissionAccess `json:"group_access"`
211+}
212+
213+type gitlabProjectInfo struct {
214+ Name string `json:"name"`
215+ Archived bool `json:"archived"`
216+ PathWithNamespace string `json:"path_with_namespace"`
217+ Permissions gitlabProjectPermission `json:"permissions"`
218+}
219+
220+func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.SessionState, project string) (*gitlabProjectInfo, error) {
221+ var projectInfo gitlabProjectInfo
151222
152− // Collect user group memberships
153− membershipSet := make(map[string]bool)
154− for _, group := range userInfo.Groups {
155− membershipSet[group] = true
223+ endpointURL := &url.URL{
224+ Scheme: p.LoginURL.Scheme,
225+ Host: p.LoginURL.Host,
226+ Path: "/api/v4/projects/",
156227 }
157228
158− // Find a valid group that they are a member of
159− for _, validGroup := range p.Groups {
160− if _, ok := membershipSet[validGroup]; ok {
161− return nil
162− }
229+ err := requests.New(fmt.Sprintf("%s%s", endpointURL.String(), url.QueryEscape(project))).
230+ WithContext(ctx).
231+ SetHeader("Authorization", "Bearer "+s.AccessToken).
232+ Do().
233+ UnmarshalInto(&projectInfo)
234+
235+ if err != nil {
236+ return nil, fmt.Errorf("failed to get project info: %v", err)
163237 }
164238
165− return fmt.Errorf("user is not a member of '%s'", p.Groups)
239+ return &projectInfo, nil
166240}
167241
168−func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
169− rawIDToken, ok := token.Extra("id_token").(string)
170− if !ok {
171− return nil, fmt.Errorf("token response did not contain an id_token")
242+// AddProjects adds Gitlab projects from options to GitlabProvider struct
243+func (p *GitLabProvider) AddProjects(projects []string) error {
244+ for _, project := range projects {
245+ gp, err := newGitlabproject(project)
246+ if err != nil {
247+ return err
248+ }
249+
250+ p.Projects = append(p.Projects, gp)
172251 }
173252
174− // Parse and verify ID Token payload.
175− idToken, err := p.Verifier.Verify(ctx, rawIDToken)
253+ return nil
254+}
255+
256+func (p *GitLabProvider) createSession(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
257+ idToken, err := p.verifyIDToken(ctx, token)
176258 if err != nil {
177− return nil, fmt.Errorf("could not verify id_token: %v", err)
259+ switch err {
260+ case ErrMissingIDToken:
261+ return nil, fmt.Errorf("token response did not contain an id_token")
262+ default:
263+ return nil, fmt.Errorf("could not verify id_token: %v", err)
264+ }
178265 }
179266
180267 created := time.Now()
181268 return &sessions.SessionState{
182269 AccessToken: token.AccessToken,
183− IDToken: rawIDToken,
270+ IDToken: getIDToken(token),
184271 RefreshToken: token.RefreshToken,
185272 CreatedAt: &created,
186273 ExpiresOn: &idToken.Expiry,
187274 }, nil
188275}
189276
190−// ValidateSessionState checks that the session's IDToken is still valid
277+// ValidateSession checks that the session's IDToken is still valid
191278func (p *GitLabProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
192279 _, err := p.Verifier.Verify(ctx, s.IDToken)
193280 return err == nil
194281}
195282
196−// GetEmailAddress returns the Account email address
283+// EnrichSession adds values and data from the Gitlab endpoint to current session
197284func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
198285 // Retrieve user info
199286 userInfo, err := p.getUserInfo(ctx, s)
@@ -206,15 +293,67 @@ func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionS
206293 return fmt.Errorf("user email is not verified")
207294 }
208295
209− // Check group membership
210− // TODO (@NickMeves) - Refactor to Authorize
211− err = p.verifyGroupMembership(userInfo)
212− if err != nil {
213− return fmt.Errorf("group membership check failed: %v", err)
214− }
215−
216296 s.User = userInfo.Username
217297 s.Email = userInfo.Email
218298
299+ p.addGroupsToSession(ctx, s)
300+
301+ p.addProjectsToSession(ctx, s)
302+
219303 return nil
304+
305+}
306+
307+// addGroupsToSession projects into session.Groups
308+func (p *GitLabProvider) addGroupsToSession(ctx context.Context, s *sessions.SessionState) {
309+ // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
310+ for _, group := range p.Groups {
311+ s.Groups = append(s.Groups, fmt.Sprintf("group:%s", group))
312+ }
313+}
314+
315+// addProjectsToSession adds projects matching user access requirements into the session state groups list
316+// This method prefix projects names with `project` to specify group kind
317+func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) {
318+ // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
319+ for _, project := range p.Projects {
320+ projectInfo, err := p.getProjectInfo(ctx, s, project.Name)
321+
322+ if err != nil {
323+ logger.Errorf("Warning: project info request failed: %v", err)
324+ continue
325+ }
326+
327+ if !projectInfo.Archived {
328+ perms := projectInfo.Permissions.ProjectAccess
329+ if perms == nil {
330+ // use group project access as fallback
331+ perms = projectInfo.Permissions.GroupAccess
332+ }
333+
334+ if perms.AccessLevel >= project.AccessLevel {
335+ s.Groups = append(s.Groups, fmt.Sprintf("project:%s", project.Name))
336+ } else {
337+ logger.Errorf("Warning: user %q does not have the minimum required access level for project %q", s.Email, project.Name)
338+ }
339+ } else {
340+ logger.Errorf("Warning: project %s is archived", project.Name)
341+ }
342+
343+ }
344+
345+}
346+
347+// PrefixAllowedGroups returns a list of allowed groups, prefixed by their `kind` value
348+func (p *GitLabProvider) PrefixAllowedGroups() (groups []string) {
349+
350+ for _, val := range p.Groups {
351+ groups = append(groups, fmt.Sprintf("group:%s", val))
352+ }
353+
354+ for _, val := range p.Projects {
355+ groups = append(groups, fmt.Sprintf("project:%s", val.Name))
356+ }
357+
358+ return groups
220359}
providers/gitlab_test.go+210−67
@@ -2,13 +2,15 @@ package providers
22
33import (
44 "context"
5+ "errors"
56 "net/http"
67 "net/http/httptest"
78 "net/url"
8− "testing"
99
1010 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11− "github.com/stretchr/testify/assert"
11+ . "github.com/onsi/ginkgo"
12+ . "github.com/onsi/ginkgo/extensions/table"
13+ . "github.com/onsi/gomega"
1214)
1315
1416func testGitLabProvider(hostname string) *GitLabProvider {
@@ -39,100 +41,241 @@ func testGitLabBackend() *httptest.Server {
3941 "groups": ["foo", "bar"]
4042 }
4143 `
44+ projectInfo := `
45+ {
46+ "name": "MyProject",
47+ "archived": false,
48+ "path_with_namespace": "my_group/my_project",
49+ "permissions": {
50+ "project_access": null,
51+ "group_access": {
52+ "access_level": 30,
53+ "notification_level": 3
54+ }
55+ }
56+ }
57+ `
58+
59+ personalProjectInfo := `
60+ {
61+ "name": "MyPersonalProject",
62+ "archived": false,
63+ "path_with_namespace": "my_profile/my_personal_project",
64+ "permissions": {
65+ "project_access": {
66+ "access_level": 30,
67+ "notification_level": 3
68+ },
69+ "group_access": null
70+ }
71+ }
72+ `
73+
74+ archivedProjectInfo := `
75+ {
76+ "name": "MyArchivedProject",
77+ "archived": true,
78+ "path_with_namespace": "my_group/my_archived_project",
79+ "permissions": {
80+ "project_access": {
81+ "access_level": 30,
82+ "notification_level": 3
83+ },
84+ "group_access": null
85+ }
86+ }
87+ `
88+
4289 authHeader := "Bearer gitlab_access_token"
4390
4491 return httptest.NewServer(http.HandlerFunc(
4592 func(w http.ResponseWriter, r *http.Request) {
46− if r.URL.Path == "/oauth/userinfo" {
93+ switch r.URL.Path {
94+ case "/oauth/userinfo":
4795 if r.Header["Authorization"][0] == authHeader {
4896 w.WriteHeader(200)
4997 w.Write([]byte(userInfo))
5098 } else {
5199 w.WriteHeader(401)
52100 }
53− } else {
101+ case "/api/v4/projects/my_group/my_project":
102+ if r.Header["Authorization"][0] == authHeader {
103+ w.WriteHeader(200)
104+ w.Write([]byte(projectInfo))
105+ } else {
106+ w.WriteHeader(401)
107+ }
108+ case "/api/v4/projects/my_group/my_archived_project":
109+ if r.Header["Authorization"][0] == authHeader {
110+ w.WriteHeader(200)
111+ w.Write([]byte(archivedProjectInfo))
112+ } else {
113+ w.WriteHeader(401)
114+ }
115+ case "/api/v4/projects/my_profile/my_personal_project":
116+ if r.Header["Authorization"][0] == authHeader {
117+ w.WriteHeader(200)
118+ w.Write([]byte(personalProjectInfo))
119+ } else {
120+ w.WriteHeader(401)
121+ }
122+ case "/api/v4/projects/my_group/my_bad_project":
123+ w.WriteHeader(403)
124+ default:
54125 w.WriteHeader(404)
55126 }
56127 }))
57128}
58129
59−func TestGitLabProviderBadToken(t *testing.T) {
60− b := testGitLabBackend()
61− defer b.Close()
130+var _ = Describe("Gitlab Provider Tests", func() {
131+ var p *GitLabProvider
132+ var b *httptest.Server
62133
63− bURL, _ := url.Parse(b.URL)
64− p := testGitLabProvider(bURL.Host)
134+ BeforeEach(func() {
135+ b = testGitLabBackend()
65136
66− session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
67− err := p.EnrichSession(context.Background(), session)
68− assert.Error(t, err)
69−}
137+ bURL, err := url.Parse(b.URL)
138+ Expect(err).To(BeNil())
70139
71−func TestGitLabProviderUnverifiedEmailDenied(t *testing.T) {
72− b := testGitLabBackend()
73− defer b.Close()
140+ p = testGitLabProvider(bURL.Host)
141+ })
74142
75− bURL, _ := url.Parse(b.URL)
76− p := testGitLabProvider(bURL.Host)
143+ AfterEach(func() {
144+ b.Close()
145+ })
77146
78− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
79− err := p.EnrichSession(context.Background(), session)
80− assert.Error(t, err)
81−}
147+ Context("with bad token", func() {
148+ It("should trigger an error", func() {
149+ p.AllowUnverifiedEmail = false
150+ session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
151+ err := p.EnrichSession(context.Background(), session)
152+ Expect(err).To(MatchError(errors.New("failed to retrieve user info: error getting user info: unexpected status \"401\": ")))
153+ })
154+ })
82155
83−func TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) {
84− b := testGitLabBackend()
85− defer b.Close()
156+ Context("when filtering on email", func() {
157+ type emailsTableInput struct {
158+ expectedError error
159+ expectedValue string
160+ allowUnverifiedEmail bool
161+ }
86162
87− bURL, _ := url.Parse(b.URL)
88− p := testGitLabProvider(bURL.Host)
89− p.AllowUnverifiedEmail = true
163+ DescribeTable("should return expected results",
164+ func(in emailsTableInput) {
165+ p.AllowUnverifiedEmail = in.allowUnverifiedEmail
166+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
90167
91− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
92− err := p.EnrichSession(context.Background(), session)
93− assert.NoError(t, err)
94− assert.Equal(t, "foo@bar.com", session.Email)
95−}
168+ err := p.EnrichSession(context.Background(), session)
96169
97−func TestGitLabProviderUsername(t *testing.T) {
98− b := testGitLabBackend()
99− defer b.Close()
170+ if in.expectedError != nil {
171+ Expect(err).To(MatchError(err))
172+ } else {
173+ Expect(err).To(BeNil())
174+ Expect(session.Email).To(Equal(in.expectedValue))
175+ }
176+ },
177+ Entry("unverified email denied", emailsTableInput{
178+ expectedError: errors.New("user email is not verified"),
179+ allowUnverifiedEmail: false,
180+ }),
181+ Entry("unverified email allowed", emailsTableInput{
182+ expectedError: nil,
183+ expectedValue: "foo@bar.com",
184+ allowUnverifiedEmail: true,
185+ }),
186+ )
187+ })
100188
101− bURL, _ := url.Parse(b.URL)
102− p := testGitLabProvider(bURL.Host)
103− p.AllowUnverifiedEmail = true
189+ Context("when filtering on gitlab entities (groups and projects)", func() {
190+ type entitiesTableInput struct {
191+ expectedValue []string
192+ projects []string
193+ groups []string
194+ }
104195
105− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
106− err := p.EnrichSession(context.Background(), session)
107− assert.NoError(t, err)
108− assert.Equal(t, "FooBar", session.User)
109−}
196+ DescribeTable("should return expected results",
197+ func(in entitiesTableInput) {
198+ p.AllowUnverifiedEmail = true
199+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
110200
111−func TestGitLabProviderGroupMembershipValid(t *testing.T) {
112− b := testGitLabBackend()
113− defer b.Close()
201+ err := p.AddProjects(in.projects)
202+ Expect(err).To(BeNil())
203+ p.SetProjectScope()
114204
115− bURL, _ := url.Parse(b.URL)
116− p := testGitLabProvider(bURL.Host)
117− p.AllowUnverifiedEmail = true
118− p.Groups = []string{"foo"}
205+ if len(in.groups) > 0 {
206+ p.Groups = in.groups
207+ }
119208
120− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
121− err := p.EnrichSession(context.Background(), session)
122− assert.NoError(t, err)
123− assert.Equal(t, "FooBar", session.User)
124−}
209+ err = p.EnrichSession(context.Background(), session)
125210
126−func TestGitLabProviderGroupMembershipMissing(t *testing.T) {
127− b := testGitLabBackend()
128− defer b.Close()
211+ Expect(err).To(BeNil())
212+ Expect(session.Groups).To(Equal(in.expectedValue))
213+ },
214+ Entry("project membership valid on group project", entitiesTableInput{
215+ expectedValue: []string{"project:my_group/my_project"},
216+ projects: []string{"my_group/my_project"},
217+ }),
218+ Entry("project membership invalid on group project, insufficient access level level", entitiesTableInput{
219+ expectedValue: nil,
220+ projects: []string{"my_group/my_project=40"},
221+ }),
222+ Entry("project membership valid on personnal project", entitiesTableInput{
223+ expectedValue: []string{"project:my_profile/my_personal_project"},
224+ projects: []string{"my_profile/my_personal_project"},
225+ }),
226+ Entry("project membership invalid on personnal project, insufficient access level", entitiesTableInput{
227+ expectedValue: nil,
228+ projects: []string{"my_profile/my_personal_project=40"},
229+ }),
230+ Entry("project membership invalid", entitiesTableInput{
231+ expectedValue: nil,
232+ projects: []string{"my_group/my_bad_project"},
233+ }),
234+ Entry("group membership valid", entitiesTableInput{
235+ expectedValue: []string{"group:foo"},
236+ groups: []string{"foo"},
237+ }),
238+ Entry("groups and projects", entitiesTableInput{
239+ expectedValue: []string{"group:foo", "group:baz", "project:my_group/my_project", "project:my_profile/my_personal_project"},
240+ groups: []string{"foo", "baz"},
241+ projects: []string{"my_group/my_project", "my_profile/my_personal_project"},
242+ }),
243+ Entry("archived projects", entitiesTableInput{
244+ expectedValue: nil,
245+ groups: []string{},
246+ projects: []string{"my_group/my_archived_project"},
247+ }),
248+ )
129249
130− bURL, _ := url.Parse(b.URL)
131− p := testGitLabProvider(bURL.Host)
132− p.AllowUnverifiedEmail = true
133− p.Groups = []string{"baz"}
250+ })
134251
135− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
136− err := p.EnrichSession(context.Background(), session)
137− assert.Error(t, err)
138−}
252+ Context("when generating group list from multiple kind", func() {
253+ type entitiesTableInput struct {
254+ projects []string
255+ groups []string
256+ }
257+
258+ DescribeTable("should prefix entities with group kind", func(in entitiesTableInput) {
259+ p.Groups = in.groups
260+ err := p.AddProjects(in.projects)
261+ Expect(err).To(BeNil())
262+
263+ all := p.PrefixAllowedGroups()
264+
265+ Expect(len(all)).To(Equal(len(in.projects) + len(in.groups)))
266+ },
267+ Entry("simple test case", entitiesTableInput{
268+ projects: []string{"my_group/my_project", "my_group/my_other_project"},
269+ groups: []string{"mygroup", "myothergroup"},
270+ }),
271+ Entry("projects only", entitiesTableInput{
272+ projects: []string{"my_group/my_project", "my_group/my_other_project"},
273+ groups: []string{},
274+ }),
275+ Entry("groups only", entitiesTableInput{
276+ projects: []string{},
277+ groups: []string{"mygroup", "myothergroup"},
278+ }),
279+ )
280+ })
281+})
providers/google.go+2−2
@@ -177,10 +177,10 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
177177 }, nil
178178}
179179
180−// EnrichSessionState checks the listed Google Groups configured and adds any
180+// EnrichSession checks the listed Google Groups configured and adds any
181181// that the user is a member of to session.Groups.
182182func (p *GoogleProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
183− // TODO (@NickMeves) - Move to pure EnrichSessionState logic and stop
183+ // TODO (@NickMeves) - Move to pure EnrichSession logic and stop
184184 // reusing legacy `groupValidator`.
185185 //
186186 // This is called here to get the validator to do the `session.Groups`
providers/internal_util_test.go+20−20
@@ -20,30 +20,30 @@ func updateURL(url *url.URL, hostname string) {
2020 url.Host = hostname
2121}
2222
23−type ValidateSessionStateTestProvider struct {
23+type ValidateSessionTestProvider struct {
2424 *ProviderData
2525}
2626
27−var _ Provider = (*ValidateSessionStateTestProvider)(nil)
27+var _ Provider = (*ValidateSessionTestProvider)(nil)
2828
29−func (tp *ValidateSessionStateTestProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
29+func (tp *ValidateSessionTestProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
3030 return "", errors.New("not implemented")
3131}
3232
3333// Note that we're testing the internal validateToken() used to implement
34−// several Provider's ValidateSessionState() implementations
35−func (tp *ValidateSessionStateTestProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
34+// several Provider's ValidateSession() implementations
35+func (tp *ValidateSessionTestProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
3636 return false
3737}
3838
3939type ValidateSessionStateTest struct {
4040 backend *httptest.Server
4141 responseCode int
42− provider *ValidateSessionStateTestProvider
42+ provider *ValidateSessionTestProvider
4343 header http.Header
4444}
4545
46−func NewValidateSessionStateTest() *ValidateSessionStateTest {
46+func NewValidateSessionTest() *ValidateSessionStateTest {
4747 var vtTest ValidateSessionStateTest
4848
4949 vtTest.backend = httptest.NewServer(
@@ -73,7 +73,7 @@ func NewValidateSessionStateTest() *ValidateSessionStateTest {
7373
7474 }))
7575 backendURL, _ := url.Parse(vtTest.backend.URL)
76− vtTest.provider = &ValidateSessionStateTestProvider{
76+ vtTest.provider = &ValidateSessionTestProvider{
7777 ProviderData: &ProviderData{
7878 ValidateURL: &url.URL{
7979 Scheme: "http",
@@ -90,14 +90,14 @@ func (vtTest *ValidateSessionStateTest) Close() {
9090 vtTest.backend.Close()
9191}
9292
93−func TestValidateSessionStateValidToken(t *testing.T) {
94− vtTest := NewValidateSessionStateTest()
93+func TestValidateSessionValidToken(t *testing.T) {
94+ vtTest := NewValidateSessionTest()
9595 defer vtTest.Close()
9696 assert.Equal(t, true, validateToken(context.Background(), vtTest.provider, "foobar", nil))
9797}
9898
99−func TestValidateSessionStateValidTokenWithHeaders(t *testing.T) {
100− vtTest := NewValidateSessionStateTest()
99+func TestValidateSessionValidTokenWithHeaders(t *testing.T) {
100+ vtTest := NewValidateSessionTest()
101101 defer vtTest.Close()
102102 vtTest.header = make(http.Header)
103103 vtTest.header.Set("Authorization", "Bearer foobar")
@@ -105,28 +105,28 @@ func TestValidateSessionStateValidTokenWithHeaders(t *testing.T) {
105105 validateToken(context.Background(), vtTest.provider, "foobar", vtTest.header))
106106}
107107
108−func TestValidateSessionStateEmptyToken(t *testing.T) {
109− vtTest := NewValidateSessionStateTest()
108+func TestValidateSessionEmptyToken(t *testing.T) {
109+ vtTest := NewValidateSessionTest()
110110 defer vtTest.Close()
111111 assert.Equal(t, false, validateToken(context.Background(), vtTest.provider, "", nil))
112112}
113113
114−func TestValidateSessionStateEmptyValidateURL(t *testing.T) {
115− vtTest := NewValidateSessionStateTest()
114+func TestValidateSessionEmptyValidateURL(t *testing.T) {
115+ vtTest := NewValidateSessionTest()
116116 defer vtTest.Close()
117117 vtTest.provider.Data().ValidateURL = nil
118118 assert.Equal(t, false, validateToken(context.Background(), vtTest.provider, "foobar", nil))
119119}
120120
121−func TestValidateSessionStateRequestNetworkFailure(t *testing.T) {
122− vtTest := NewValidateSessionStateTest()
121+func TestValidateSessionRequestNetworkFailure(t *testing.T) {
122+ vtTest := NewValidateSessionTest()
123123 // Close immediately to simulate a network failure
124124 vtTest.Close()
125125 assert.Equal(t, false, validateToken(context.Background(), vtTest.provider, "foobar", nil))
126126}
127127
128−func TestValidateSessionStateExpiredToken(t *testing.T) {
129− vtTest := NewValidateSessionStateTest()
128+func TestValidateSessionExpiredToken(t *testing.T) {
129+ vtTest := NewValidateSessionTest()
130130 defer vtTest.Close()
131131 vtTest.responseCode = 401
132132 assert.Equal(t, false, validateToken(context.Background(), vtTest.provider, "foobar", nil))
providers/keycloak.go+24−25
@@ -2,6 +2,7 @@ package providers
22
33import (
44 "context"
5+ "fmt"
56 "net/url"
67
78 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@@ -11,7 +12,6 @@ import (
1112
1213type KeycloakProvider struct {
1314 *ProviderData
14− Group string
1515}
1616
1717var _ Provider = (*KeycloakProvider)(nil)
@@ -47,6 +47,7 @@ var (
4747 }
4848)
4949
50+// NewKeycloakProvider creates a KeyCloakProvider using the passed ProviderData
5051func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
5152 p.setProviderDefaults(providerDefaults{
5253 name: keycloakProviderName,
@@ -59,41 +60,39 @@ func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
5960 return &KeycloakProvider{ProviderData: p}
6061}
6162
62−func (p *KeycloakProvider) SetGroup(group string) {
63− p.Group = group
64−}
63+// EnrichSession uses the Keycloak userinfo endpoint to populate the session's
64+// email and groups.
65+func (p *KeycloakProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
66+ // Fallback to ValidateURL if ProfileURL not set for legacy compatibility
67+ profileURL := p.ValidateURL.String()
68+ if p.ProfileURL.String() != "" {
69+ profileURL = p.ProfileURL.String()
70+ }
6571
66−func (p *KeycloakProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
67− json, err := requests.New(p.ValidateURL.String()).
72+ json, err := requests.New(profileURL).
6873 WithContext(ctx).
6974 SetHeader("Authorization", "Bearer "+s.AccessToken).
7075 Do().
7176 UnmarshalJSON()
7277 if err != nil {
73− logger.Errorf("failed making request %s", err)
74− return "", err
78+ logger.Errorf("failed making request %v", err)
79+ return err
7580 }
7681
77− if p.Group != "" {
78− var groups, err = json.Get("groups").Array()
79− if err != nil {
80− logger.Printf("groups not found %s", err)
81− return "", err
82− }
83−
84− var found = false
85− for i := range groups {
86− if groups[i].(string) == p.Group {
87− found = true
88− break
82+ groups, err := json.Get("groups").StringArray()
83+ if err == nil {
84+ for _, group := range groups {
85+ if group != "" {
86+ s.Groups = append(s.Groups, group)
8987 }
9088 }
89+ }
9190
92− if !found {
93− logger.Printf("group not found, access denied")
94− return "", nil
95− }
91+ email, err := json.Get("email").String()
92+ if err != nil {
93+ return fmt.Errorf("unable to extract email from userinfo endpoint: %v", err)
9694 }
95+ s.Email = email
9796
98− return json.Get("email").String()
97+ return nil
9998}
providers/keycloak_test.go+180−133
@@ -2,17 +2,24 @@ package providers
22
33import (
44 "context"
5+ "errors"
6+ "fmt"
57 "net/http"
68 "net/http/httptest"
79 "net/url"
8− "testing"
910
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12+ . "github.com/onsi/ginkgo"
13+ . "github.com/onsi/ginkgo/extensions/table"
1114 . "github.com/onsi/gomega"
12− "github.com/stretchr/testify/assert"
1315)
1416
15−func testKeycloakProvider(hostname, group string) *KeycloakProvider {
17+const (
18+ keycloakAccessToken = "eyJKeycloak.eyJAccess.Token"
19+ keycloakUserinfoPath = "/api/v3/user"
20+)
21+
22+func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
1623 p := NewKeycloakProvider(
1724 &ProviderData{
1825 ProviderName: "",
@@ -22,142 +29,182 @@ func testKeycloakProvider(hostname, group string) *KeycloakProvider {
2229 ValidateURL: &url.URL{},
2330 Scope: ""})
2431
25− if group != "" {
26− p.SetGroup(group)
27− }
32+ if backend != nil {
33+ bURL, err := url.Parse(backend.URL)
34+ if err != nil {
35+ return nil, err
36+ }
37+ hostname := bURL.Host
2838
29− if hostname != "" {
3039 updateURL(p.Data().LoginURL, hostname)
3140 updateURL(p.Data().RedeemURL, hostname)
3241 updateURL(p.Data().ProfileURL, hostname)
3342 updateURL(p.Data().ValidateURL, hostname)
3443 }
35− return p
36−}
3744
38−func testKeycloakBackend(payload string) *httptest.Server {
39− path := "/api/v3/user"
40−
41− return httptest.NewServer(http.HandlerFunc(
42− func(w http.ResponseWriter, r *http.Request) {
43− url := r.URL
44− if url.Path != path {
45− w.WriteHeader(404)
46− } else if !IsAuthorizedInHeader(r.Header) {
47− w.WriteHeader(403)
48− } else {
49− w.WriteHeader(200)
50− w.Write([]byte(payload))
51− }
52− }))
45+ return p, nil
5346}
5447
55−func TestKeycloakProviderDefaults(t *testing.T) {
56− p := testKeycloakProvider("", "")
57− assert.NotEqual(t, nil, p)
58− assert.Equal(t, "Keycloak", p.Data().ProviderName)
59− assert.Equal(t, "https://keycloak.org/oauth/authorize",
60− p.Data().LoginURL.String())
61− assert.Equal(t, "https://keycloak.org/oauth/token",
62− p.Data().RedeemURL.String())
63− assert.Equal(t, "https://keycloak.org/api/v3/user",
64− p.Data().ValidateURL.String())
65− assert.Equal(t, "api", p.Data().Scope)
66−}
67−
68−func TestNewKeycloakProvider(t *testing.T) {
69− g := NewWithT(t)
70−
71− // Test that defaults are set when calling for a new provider with nothing set
72− providerData := NewKeycloakProvider(&ProviderData{}).Data()
73− g.Expect(providerData.ProviderName).To(Equal("Keycloak"))
74− g.Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
75− g.Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
76− g.Expect(providerData.ProfileURL.String()).To(Equal(""))
77− g.Expect(providerData.ValidateURL.String()).To(Equal("https://keycloak.org/api/v3/user"))
78− g.Expect(providerData.Scope).To(Equal("api"))
79−}
80−
81−func TestKeycloakProviderOverrides(t *testing.T) {
82− p := NewKeycloakProvider(
83− &ProviderData{
84− LoginURL: &url.URL{
85− Scheme: "https",
86− Host: "example.com",
87− Path: "/oauth/auth"},
88− RedeemURL: &url.URL{
89− Scheme: "https",
90− Host: "example.com",
91− Path: "/oauth/token"},
92− ValidateURL: &url.URL{
93− Scheme: "https",
94− Host: "example.com",
95− Path: "/api/v3/user"},
96− Scope: "profile"})
97− assert.NotEqual(t, nil, p)
98− assert.Equal(t, "Keycloak", p.Data().ProviderName)
99− assert.Equal(t, "https://example.com/oauth/auth",
100− p.Data().LoginURL.String())
101− assert.Equal(t, "https://example.com/oauth/token",
102− p.Data().RedeemURL.String())
103− assert.Equal(t, "https://example.com/api/v3/user",
104− p.Data().ValidateURL.String())
105− assert.Equal(t, "profile", p.Data().Scope)
106−}
107−
108−func TestKeycloakProviderGetEmailAddress(t *testing.T) {
109− b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}")
110− defer b.Close()
111−
112− bURL, _ := url.Parse(b.URL)
113− p := testKeycloakProvider(bURL.Host, "")
114−
115− session := CreateAuthorizedSession()
116− email, err := p.GetEmailAddress(context.Background(), session)
117− assert.Equal(t, nil, err)
118− assert.Equal(t, "michael.bland@gsa.gov", email)
119−}
120−
121−func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) {
122− b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}")
123− defer b.Close()
124−
125− bURL, _ := url.Parse(b.URL)
126− p := testKeycloakProvider(bURL.Host, "test-grp1")
127−
128− session := CreateAuthorizedSession()
129− email, err := p.GetEmailAddress(context.Background(), session)
130− assert.Equal(t, nil, err)
131− assert.Equal(t, "michael.bland@gsa.gov", email)
132−}
133−
134−// Note that trying to trigger the "failed building request" case is not
135−// practical, since the only way it can fail is if the URL fails to parse.
136−func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) {
137− b := testKeycloakBackend("unused payload")
138− defer b.Close()
139−
140− bURL, _ := url.Parse(b.URL)
141− p := testKeycloakProvider(bURL.Host, "")
142−
143− // We'll trigger a request failure by using an unexpected access
144− // token. Alternatively, we could allow the parsing of the payload as
145− // JSON to fail.
146− session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
147− email, err := p.GetEmailAddress(context.Background(), session)
148− assert.NotEqual(t, nil, err)
149− assert.Equal(t, "", email)
150−}
151−
152−func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
153− b := testKeycloakBackend("{\"foo\": \"bar\"}")
154− defer b.Close()
155−
156− bURL, _ := url.Parse(b.URL)
157− p := testKeycloakProvider(bURL.Host, "")
158−
159− session := CreateAuthorizedSession()
160− email, err := p.GetEmailAddress(context.Background(), session)
161− assert.NotEqual(t, nil, err)
162− assert.Equal(t, "", email)
163−}
48+var _ = Describe("Keycloak Provider Tests", func() {
49+ Context("New Provider Init", func() {
50+ It("uses defaults", func() {
51+ providerData := NewKeycloakProvider(&ProviderData{}).Data()
52+ Expect(providerData.ProviderName).To(Equal("Keycloak"))
53+ Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
54+ Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
55+ Expect(providerData.ProfileURL.String()).To(Equal(""))
56+ Expect(providerData.ValidateURL.String()).To(Equal("https://keycloak.org/api/v3/user"))
57+ Expect(providerData.Scope).To(Equal("api"))
58+ })
59+
60+ It("overrides defaults", func() {
61+ p := NewKeycloakProvider(
62+ &ProviderData{
63+ LoginURL: &url.URL{
64+ Scheme: "https",
65+ Host: "example.com",
66+ Path: "/oauth/auth"},
67+ RedeemURL: &url.URL{
68+ Scheme: "https",
69+ Host: "example.com",
70+ Path: "/oauth/token"},
71+ ProfileURL: &url.URL{
72+ Scheme: "https",
73+ Host: "example.com",
74+ Path: "/api/v3/user"},
75+ ValidateURL: &url.URL{
76+ Scheme: "https",
77+ Host: "example.com",
78+ Path: "/api/v3/user"},
79+ Scope: "profile"})
80+ providerData := p.Data()
81+
82+ Expect(providerData.ProviderName).To(Equal("Keycloak"))
83+ Expect(providerData.LoginURL.String()).To(Equal("https://example.com/oauth/auth"))
84+ Expect(providerData.RedeemURL.String()).To(Equal("https://example.com/oauth/token"))
85+ Expect(providerData.ProfileURL.String()).To(Equal("https://example.com/api/v3/user"))
86+ Expect(providerData.ValidateURL.String()).To(Equal("https://example.com/api/v3/user"))
87+ Expect(providerData.Scope).To(Equal("profile"))
88+ })
89+ })
90+
91+ Context("EnrichSession", func() {
92+ type enrichSessionTableInput struct {
93+ backendHandler http.HandlerFunc
94+ expectedError error
95+ expectedEmail string
96+ expectedGroups []string
97+ }
98+
99+ DescribeTable("should return expected results",
100+ func(in enrichSessionTableInput) {
101+ backend := httptest.NewServer(in.backendHandler)
102+ p, err := testKeycloakProvider(backend)
103+ Expect(err).To(BeNil())
104+
105+ p.ProfileURL, err = url.Parse(
106+ fmt.Sprintf("%s%s", backend.URL, keycloakUserinfoPath),
107+ )
108+ Expect(err).To(BeNil())
109+
110+ session := &sessions.SessionState{AccessToken: keycloakAccessToken}
111+ err = p.EnrichSession(context.Background(), session)
112+
113+ if in.expectedError != nil {
114+ Expect(err).To(Equal(in.expectedError))
115+ } else {
116+ Expect(err).To(BeNil())
117+ }
118+
119+ Expect(session.Email).To(Equal(in.expectedEmail))
120+
121+ if in.expectedGroups != nil {
122+ Expect(session.Groups).To(Equal(in.expectedGroups))
123+ } else {
124+ Expect(session.Groups).To(BeNil())
125+ }
126+ },
127+ Entry("email and multiple groups", enrichSessionTableInput{
128+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
129+ w.WriteHeader(200)
130+ _, err := w.Write([]byte(`
131+ {
132+ "email": "michael.bland@gsa.gov",
133+ "groups": [
134+ "test-grp1",
135+ "test-grp2"
136+ ]
137+ }
138+ `))
139+ if err != nil {
140+ panic(err)
141+ }
142+ },
143+ expectedError: nil,
144+ expectedEmail: "michael.bland@gsa.gov",
145+ expectedGroups: []string{"test-grp1", "test-grp2"},
146+ }),
147+ Entry("email and single group", enrichSessionTableInput{
148+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
149+ w.WriteHeader(200)
150+ _, err := w.Write([]byte(`
151+ {
152+ "email": "michael.bland@gsa.gov",
153+ "groups": ["test-grp1"]
154+ }
155+ `))
156+ if err != nil {
157+ panic(err)
158+ }
159+ },
160+ expectedError: nil,
161+ expectedEmail: "michael.bland@gsa.gov",
162+ expectedGroups: []string{"test-grp1"},
163+ }),
164+ Entry("email and no groups", enrichSessionTableInput{
165+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
166+ w.WriteHeader(200)
167+ _, err := w.Write([]byte(`
168+ {
169+ "email": "michael.bland@gsa.gov"
170+ }
171+ `))
172+ if err != nil {
173+ panic(err)
174+ }
175+ },
176+ expectedError: nil,
177+ expectedEmail: "michael.bland@gsa.gov",
178+ expectedGroups: nil,
179+ }),
180+ Entry("missing email", enrichSessionTableInput{
181+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
182+ w.WriteHeader(200)
183+ _, err := w.Write([]byte(`
184+ {
185+ "groups": [
186+ "test-grp1",
187+ "test-grp2"
188+ ]
189+ }
190+ `))
191+ if err != nil {
192+ panic(err)
193+ }
194+ },
195+ expectedError: errors.New(
196+ "unable to extract email from userinfo endpoint: type assertion to string failed"),
197+ expectedEmail: "",
198+ expectedGroups: []string{"test-grp1", "test-grp2"},
199+ }),
200+ Entry("request failure", enrichSessionTableInput{
201+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
202+ w.WriteHeader(500)
203+ },
204+ expectedError: errors.New(`unexpected status "500": `),
205+ expectedEmail: "",
206+ expectedGroups: nil,
207+ }),
208+ )
209+ })
210+})
providers/logingov.go+9−2
@@ -3,9 +3,10 @@ package providers
33import (
44 "bytes"
55 "context"
6+ "crypto/rand"
67 "crypto/rsa"
78 "fmt"
8− "math/rand"
9+ "math/big"
910 "net/url"
1011 "time"
1112
@@ -34,7 +35,13 @@ var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
3435func randSeq(n int) string {
3536 b := make([]rune, n)
3637 for i := range b {
37− b[i] = letters[rand.Intn(len(letters))]
38+ max := big.NewInt(int64(len(letters)))
39+ bigN, err := rand.Int(rand.Reader, max)
40+ if err != nil {
41+ // This should never happen
42+ panic(err)
43+ }
44+ b[i] = letters[bigN.Int64()]
3845 }
3946 return string(b)
4047}
providers/oidc.go+108−200
@@ -2,29 +2,20 @@ package providers
22
33import (
44 "context"
5− "encoding/json"
5+ "errors"
66 "fmt"
77 "reflect"
8− "strings"
98 "time"
109
11− oidc "github.com/coreos/go-oidc"
12− "golang.org/x/oauth2"
13−
1410 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1511 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1612 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
13+ "golang.org/x/oauth2"
1714)
1815
19−const emailClaim = "email"
20−
2116// OIDCProvider represents an OIDC based Identity Provider
2217type OIDCProvider struct {
2318 *ProviderData
24−
25− AllowUnverifiedEmail bool
26− UserIDClaim string
27− GroupsClaim string
2819}
2920
3021// NewOIDCProvider initiates a new OIDCProvider
@@ -36,10 +27,10 @@ func NewOIDCProvider(p *ProviderData) *OIDCProvider {
3627var _ Provider = (*OIDCProvider)(nil)
3728
3829// Redeem exchanges the OAuth2 authentication token for an ID token
39−func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
30+func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (*sessions.SessionState, error) {
4031 clientSecret, err := p.GetClientSecret()
4132 if err != nil {
42− return
33+ return nil, err
4334 }
4435
4536 c := oauth2.Config{
@@ -52,23 +43,74 @@ func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (s
5243 }
5344 token, err := c.Exchange(ctx, code)
5445 if err != nil {
55− return nil, fmt.Errorf("token exchange: %v", err)
46+ return nil, fmt.Errorf("token exchange failed: %v", err)
5647 }
5748
58− // in the initial exchange the id token is mandatory
59− idToken, err := p.findVerifiedIDToken(ctx, token)
60− if err != nil {
61− return nil, fmt.Errorf("could not verify id_token: %v", err)
62− } else if idToken == nil {
63− return nil, fmt.Errorf("token response did not contain an id_token")
49+ return p.createSession(ctx, token, false)
50+}
51+
52+// EnrichSession is called after Redeem to allow providers to enrich session fields
53+// such as User, Email, Groups with provider specific API calls.
54+func (p *OIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
55+ if p.ProfileURL.String() == "" {
56+ if s.Email == "" {
57+ return errors.New("id_token did not contain an email and profileURL is not defined")
58+ }
59+ return nil
6460 }
6561
66− s, err = p.createSessionState(ctx, token, idToken)
62+ // Try to get missing emails or groups from a profileURL
63+ if s.Email == "" || s.Groups == nil {
64+ err := p.enrichFromProfileURL(ctx, s)
65+ if err != nil {
66+ logger.Errorf("Warning: Profile URL request failed: %v", err)
67+ }
68+ }
69+
70+ // If a mandatory email wasn't set, error at this point.
71+ if s.Email == "" {
72+ return errors.New("neither the id_token nor the profileURL set an email")
73+ }
74+ return nil
75+}
76+
77+// enrichFromProfileURL enriches a session's Email & Groups via the JSON response of
78+// an OIDC profile URL
79+func (p *OIDCProvider) enrichFromProfileURL(ctx context.Context, s *sessions.SessionState) error {
80+ respJSON, err := requests.New(p.ProfileURL.String()).
81+ WithContext(ctx).
82+ WithHeaders(makeOIDCHeader(s.AccessToken)).
83+ Do().
84+ UnmarshalJSON()
6785 if err != nil {
68− return nil, fmt.Errorf("unable to update session: %v", err)
86+ return err
87+ }
88+
89+ email, err := respJSON.Get(p.EmailClaim).String()
90+ if err == nil && s.Email == "" {
91+ s.Email = email
6992 }
7093
71− return
94+ if len(s.Groups) > 0 {
95+ return nil
96+ }
97+ for _, group := range coerceArray(respJSON, p.GroupsClaim) {
98+ formatted, err := formatGroup(group)
99+ if err != nil {
100+ logger.Errorf("Warning: unable to format group of type %s with error %s",
101+ reflect.TypeOf(group), err)
102+ continue
103+ }
104+ s.Groups = append(s.Groups, formatted)
105+ }
106+
107+ return nil
108+}
109+
110+// ValidateSession checks that the session's IDToken is still valid
111+func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
112+ _, err := p.Verifier.Verify(ctx, s.IDToken)
113+ return err == nil
72114}
73115
74116// RefreshSessionIfNeeded checks if the session has expired and uses the
@@ -83,14 +125,16 @@ func (p *OIDCProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.S
83125 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
84126 }
85127
86− fmt.Printf("refreshed access token %s (expired on %s)\n", s, s.ExpiresOn)
128+ logger.Printf("refreshed session: %s", s)
87129 return true, nil
88130}
89131
90−func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) {
132+// redeemRefreshToken uses a RefreshToken with the RedeemURL to refresh the
133+// Access Token and (probably) the ID Token.
134+func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) error {
91135 clientSecret, err := p.GetClientSecret()
92136 if err != nil {
93− return
137+ return err
94138 }
95139
96140 c := oauth2.Config{
@@ -109,19 +153,14 @@ func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sessi
109153 return fmt.Errorf("failed to get token: %v", err)
110154 }
111155
112− // in the token refresh response the id_token is optional
113− idToken, err := p.findVerifiedIDToken(ctx, token)
114− if err != nil {
115− return fmt.Errorf("unable to extract id_token from response: %v", err)
116− }
117−
118− newSession, err := p.createSessionState(ctx, token, idToken)
156+ newSession, err := p.createSession(ctx, token, true)
119157 if err != nil {
120158 return fmt.Errorf("unable create new session state from response: %v", err)
121159 }
122160
123− // It's possible that if the refresh token isn't in the token response the session will not contain an id token
124− // if it doesn't it's probably better to retain the old one
161+ // It's possible that if the refresh token isn't in the token response the
162+ // session will not contain an id token.
163+ // If it doesn't it's probably better to retain the old one
125164 if newSession.IDToken != "" {
126165 s.IDToken = newSession.IDToken
127166 s.Email = newSession.Email
@@ -135,193 +174,62 @@ func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sessi
135174 s.CreatedAt = newSession.CreatedAt
136175 s.ExpiresOn = newSession.ExpiresOn
137176
138− return
139−}
140−
141−func (p *OIDCProvider) findVerifiedIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
142−
143− getIDToken := func() (string, bool) {
144− rawIDToken, _ := token.Extra("id_token").(string)
145− return rawIDToken, len(strings.TrimSpace(rawIDToken)) > 0
146− }
147−
148− if rawIDToken, present := getIDToken(); present {
149− verifiedIDToken, err := p.Verifier.Verify(ctx, rawIDToken)
150− return verifiedIDToken, err
151− }
152− return nil, nil
153−}
154−
155−func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Token, idToken *oidc.IDToken) (*sessions.SessionState, error) {
156−
157− var newSession *sessions.SessionState
158−
159− if idToken == nil {
160− newSession = &sessions.SessionState{}
161− } else {
162− var err error
163− newSession, err = p.createSessionStateInternal(ctx, idToken, token)
164− if err != nil {
165− return nil, err
166− }
167− }
168−
169− created := time.Now()
170− newSession.AccessToken = token.AccessToken
171− newSession.RefreshToken = token.RefreshToken
172− newSession.CreatedAt = &created
173− newSession.ExpiresOn = &token.Expiry
174− return newSession, nil
177+ return nil
175178}
176179
180+// CreateSessionFromToken converts Bearer IDTokens into sessions
177181func (p *OIDCProvider) CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error) {
178182 idToken, err := p.Verifier.Verify(ctx, token)
179183 if err != nil {
180184 return nil, err
181185 }
182186
183− newSession, err := p.createSessionStateInternal(ctx, idToken, nil)
187+ ss, err := p.buildSessionFromClaims(idToken)
184188 if err != nil {
185189 return nil, err
186190 }
187191
188− newSession.AccessToken = token
189− newSession.IDToken = token
190− newSession.RefreshToken = ""
191− newSession.ExpiresOn = &idToken.Expiry
192−
193− return newSession, nil
194−}
195−
196−func (p *OIDCProvider) createSessionStateInternal(ctx context.Context, idToken *oidc.IDToken, token *oauth2.Token) (*sessions.SessionState, error) {
197−
198− newSession := &sessions.SessionState{}
199−
200− if idToken == nil {
201− return newSession, nil
202− }
203−
204− claims, err := p.findClaimsFromIDToken(ctx, idToken, token)
205− if err != nil {
206− return nil, fmt.Errorf("couldn't extract claims from id_token (%v)", err)
207− }
208−
209− if token != nil {
210− newSession.IDToken = token.Extra("id_token").(string)
211− }
212−
213− newSession.Email = claims.UserID // TODO Rename SessionState.Email to .UserID in the near future
214−
215− newSession.User = claims.Subject
216− newSession.Groups = claims.Groups
217− newSession.PreferredUsername = claims.PreferredUsername
218−
219− verifyEmail := (p.UserIDClaim == emailClaim) && !p.AllowUnverifiedEmail
220− if verifyEmail && claims.Verified != nil && !*claims.Verified {
221− return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.UserID)
222− }
223−
224− return newSession, nil
225−}
226−
227−// ValidateSessionState checks that the session's IDToken is still valid
228−func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
229− _, err := p.Verifier.Verify(ctx, s.IDToken)
230− return err == nil
231−}
232−
233−func (p *OIDCProvider) findClaimsFromIDToken(ctx context.Context, idToken *oidc.IDToken, token *oauth2.Token) (*OIDCClaims, error) {
234− claims := &OIDCClaims{}
235−
236− // Extract default claims.
237− if err := idToken.Claims(&claims); err != nil {
238− return nil, fmt.Errorf("failed to parse default id_token claims: %v", err)
239− }
240− // Extract custom claims.
241− if err := idToken.Claims(&claims.rawClaims); err != nil {
242− return nil, fmt.Errorf("failed to parse all id_token claims: %v", err)
243− }
244−
245− userID := claims.rawClaims[p.UserIDClaim]
246− if userID != nil {
247− claims.UserID = fmt.Sprint(userID)
192+ // Allow empty Email in Bearer case since we can't hit the ProfileURL
193+ if ss.Email == "" {
194+ ss.Email = ss.User
248195 }
249196
250− claims.Groups = p.extractGroupsFromRawClaims(claims.rawClaims)
251−
252− // userID claim was not present or was empty in the ID Token
253− if claims.UserID == "" {
254− // BearerToken case, allow empty UserID
255− // ProfileURL checks below won't work since we don't have an access token
256− if token == nil {
257− claims.UserID = claims.Subject
258− return claims, nil
259− }
260−
261− profileURL := p.ProfileURL.String()
262− if profileURL == "" || token.AccessToken == "" {
263− return nil, fmt.Errorf("id_token did not contain user ID claim (%q)", p.UserIDClaim)
264− }
265−
266− // If the userinfo endpoint profileURL is defined, then there is a chance the userinfo
267− // contents at the profileURL contains the email.
268− // Make a query to the userinfo endpoint, and attempt to locate the email from there.
269− respJSON, err := requests.New(profileURL).
270− WithContext(ctx).
271− WithHeaders(makeOIDCHeader(token.AccessToken)).
272− Do().
273− UnmarshalJSON()
274− if err != nil {
275− return nil, err
276− }
277−
278− userID, err := respJSON.Get(p.UserIDClaim).String()
279− if err != nil {
280− return nil, fmt.Errorf("neither id_token nor userinfo endpoint contained user ID claim (%q)", p.UserIDClaim)
281− }
282−
283− claims.UserID = userID
284− }
197+ ss.AccessToken = token
198+ ss.IDToken = token
199+ ss.RefreshToken = ""
200+ ss.ExpiresOn = &idToken.Expiry
285201
286− return claims, nil
202+ return ss, nil
287203}
288204
289−func (p *OIDCProvider) extractGroupsFromRawClaims(rawClaims map[string]interface{}) []string {
290− groups := []string{}
291−
292− rawGroups, ok := rawClaims[p.GroupsClaim].([]interface{})
293− if rawGroups != nil && ok {
294− for _, rawGroup := range rawGroups {
295− formattedGroup, err := formatGroup(rawGroup)
296− if err != nil {
297− logger.Errorf("unable to format group of type %s with error %s", reflect.TypeOf(rawGroup), err)
298− continue
205+// createSession takes an oauth2.Token and creates a SessionState from it.
206+// It alters behavior if called from Redeem vs Refresh
207+func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, refresh bool) (*sessions.SessionState, error) {
208+ idToken, err := p.verifyIDToken(ctx, token)
209+ if err != nil {
210+ switch err {
211+ case ErrMissingIDToken:
212+ // IDToken is mandatory in Redeem but optional in Refresh
213+ if !refresh {
214+ return nil, errors.New("token response did not contain an id_token")
299215 }
300− groups = append(groups, formattedGroup)
216+ default:
217+ return nil, fmt.Errorf("could not verify id_token: %v", err)
301218 }
302219 }
303220
304− return groups
221+ ss, err := p.buildSessionFromClaims(idToken)
222+ if err != nil {
223+ return nil, err
224+ }
305225
306−}
226+ ss.AccessToken = token.AccessToken
227+ ss.RefreshToken = token.RefreshToken
228+ ss.IDToken = getIDToken(token)
307229
308−func formatGroup(rawGroup interface{}) (string, error) {
309− group, ok := rawGroup.(string)
310− if !ok {
311− jsonGroup, err := json.Marshal(rawGroup)
312− if err != nil {
313− return "", err
314− }
315− group = string(jsonGroup)
316− }
317− return group, nil
318−}
230+ created := time.Now()
231+ ss.CreatedAt = &created
232+ ss.ExpiresOn = &token.Expiry
319233
320−type OIDCClaims struct {
321− rawClaims map[string]interface{}
322− UserID string
323− Subject string `json:"sub"`
324− Verified *bool `json:"email_verified"`
325− PreferredUsername string `json:"preferred_username"`
326− Groups []string `json:"-"`
234+ return ss, nil
327235}
providers/oidc_test.go+358−215
@@ -2,42 +2,18 @@ package providers
22
33import (
44 "context"
5− "crypto/rand"
6− "crypto/rsa"
7− "encoding/base64"
85 "encoding/json"
96 "errors"
10− "fmt"
117 "net/http"
128 "net/http/httptest"
139 "net/url"
14− "strings"
1510 "testing"
16− "time"
1711
1812 "github.com/coreos/go-oidc"
19− "github.com/dgrijalva/jwt-go"
20− "github.com/stretchr/testify/assert"
21− "golang.org/x/oauth2"
22−
2313 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14+ "github.com/stretchr/testify/assert"
2415)
2516
26−const accessToken = "access_token"
27−const refreshToken = "refresh_token"
28−const clientID = "https://test.myapp.com"
29−const secret = "secret"
30−
31−type idTokenClaims struct {
32− Name string `json:"name,omitempty"`
33− Email string `json:"email,omitempty"`
34− Phone string `json:"phone_number,omitempty"`
35− Picture string `json:"picture,omitempty"`
36− Groups interface{} `json:"groups,omitempty"`
37− OtherGroups interface{} `json:"other_groups,omitempty"`
38− jwt.StandardClaims
39−}
40−
4117type redeemTokenResponse struct {
4218 AccessToken string `json:"access_token"`
4319 RefreshToken string `json:"refresh_token"`
@@ -46,88 +22,12 @@ type redeemTokenResponse struct {
4622 IDToken string `json:"id_token,omitempty"`
4723}
4824
49−var defaultIDToken idTokenClaims = idTokenClaims{
50− "Jane Dobbs",
51− "janed@me.com",
52− "+4798765432",
53− "http://mugbook.com/janed/me.jpg",
54− []string{"test:a", "test:b"},
55− []string{"test:c", "test:d"},
56− jwt.StandardClaims{
57− Audience: "https://test.myapp.com",
58− ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
59− Id: "id-some-id",
60− IssuedAt: time.Now().Unix(),
61− Issuer: "https://issuer.example.com",
62− NotBefore: 0,
63− Subject: "123456789",
64− },
65−}
66−
67−var customGroupClaimIDToken idTokenClaims = idTokenClaims{
68− "Jane Dobbs",
69− "janed@me.com",
70− "+4798765432",
71− "http://mugbook.com/janed/me.jpg",
72− []map[string]interface{}{
73− {
74− "groupId": "Admin Group Id",
75− "roles": []string{"Admin"},
76− },
77− },
78− []string{"test:c", "test:d"},
79− jwt.StandardClaims{
80− Audience: "https://test.myapp.com",
81− ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
82− Id: "id-some-id",
83− IssuedAt: time.Now().Unix(),
84− Issuer: "https://issuer.example.com",
85− NotBefore: 0,
86− Subject: "123456789",
87− },
88−}
89−
90−var minimalIDToken idTokenClaims = idTokenClaims{
91− "",
92− "",
93− "",
94− "",
95− []string{},
96− []string{},
97− jwt.StandardClaims{
98− Audience: "https://test.myapp.com",
99− ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
100− Id: "id-some-id",
101− IssuedAt: time.Now().Unix(),
102− Issuer: "https://issuer.example.com",
103− NotBefore: 0,
104− Subject: "minimal",
105− },
106−}
107−
108−type fakeKeySetStub struct{}
109−
110−func (fakeKeySetStub) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
111− decodeString, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
112− if err != nil {
113− return nil, err
114− }
115− tokenClaims := &idTokenClaims{}
116− err = json.Unmarshal(decodeString, tokenClaims)
117−
118− if err != nil || tokenClaims.Id == "this-id-fails-validation" {
119− return nil, fmt.Errorf("the validation failed for subject [%v]", tokenClaims.Subject)
120− }
121−
122− return decodeString, err
123−}
124−
12525func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
12626
12727 providerData := &ProviderData{
12828 ProviderName: "oidc",
129− ClientID: clientID,
130− ClientSecret: secret,
29+ ClientID: oidcClientID,
30+ ClientSecret: oidcSecret,
13131 LoginURL: &url.URL{
13232 Scheme: serverURL.Scheme,
13333 Host: serverURL.Host,
@@ -144,18 +44,17 @@ func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
14444 Scheme: serverURL.Scheme,
14545 Host: serverURL.Host,
14646 Path: "/api"},
147− Scope: "openid profile offline_access",
47+ Scope: "openid profile offline_access",
48+ EmailClaim: "email",
49+ GroupsClaim: "groups",
14850 Verifier: oidc.NewVerifier(
149− "https://issuer.example.com",
150− fakeKeySetStub{},
151− &oidc.Config{ClientID: clientID},
51+ oidcIssuer,
52+ mockJWKS{},
53+ &oidc.Config{ClientID: oidcClientID},
15254 ),
15355 }
15456
155− p := &OIDCProvider{
156− ProviderData: providerData,
157− UserIDClaim: "email",
158− }
57+ p := &OIDCProvider{ProviderData: providerData}
15958
16059 return p
16160}
@@ -169,22 +68,7 @@ func newOIDCServer(body []byte) (*url.URL, *httptest.Server) {
16968 return u, s
17069}
17170
172−func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
173− key, _ := rsa.GenerateKey(rand.Reader, 2048)
174− standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
175− return standardClaims.SignedString(key)
176−}
177−
178−func newOauth2Token() *oauth2.Token {
179− return &oauth2.Token{
180− AccessToken: accessToken,
181− TokenType: "Bearer",
182− RefreshToken: refreshToken,
183− Expiry: time.Time{}.Add(time.Duration(5) * time.Second),
184− }
185−}
186−
187−func newTestSetup(body []byte) (*httptest.Server, *OIDCProvider) {
71+func newTestOIDCSetup(body []byte) (*httptest.Server, *OIDCProvider) {
18872 redeemURL, server := newOIDCServer(body)
18973 provider := newOIDCProvider(redeemURL)
19074 return server, provider
@@ -201,7 +85,7 @@ func TestOIDCProviderRedeem(t *testing.T) {
20185 IDToken: idToken,
20286 })
20387
204− server, provider := newTestSetup(body)
88+ server, provider := newTestOIDCSetup(body)
20589 defer server.Close()
20690
20791 session, err := provider.Redeem(context.Background(), provider.RedeemURL.String(), "code1234")
@@ -224,8 +108,8 @@ func TestOIDCProviderRedeem_custom_userid(t *testing.T) {
224108 IDToken: idToken,
225109 })
226110
227− server, provider := newTestSetup(body)
228− provider.UserIDClaim = "phone_number"
111+ server, provider := newTestOIDCSetup(body)
112+ provider.EmailClaim = "phone_number"
229113 defer server.Close()
230114
231115 session, err := provider.Redeem(context.Background(), provider.RedeemURL.String(), "code1234")
@@ -233,6 +117,333 @@ func TestOIDCProviderRedeem_custom_userid(t *testing.T) {
233117 assert.Equal(t, defaultIDToken.Phone, session.Email)
234118}
235119
120+func TestOIDCProvider_EnrichSession(t *testing.T) {
121+ testCases := map[string]struct {
122+ ExistingSession *sessions.SessionState
123+ EmailClaim string
124+ GroupsClaim string
125+ ProfileJSON map[string]interface{}
126+ ExpectedError error
127+ ExpectedSession *sessions.SessionState
128+ }{
129+ "Already Populated": {
130+ ExistingSession: &sessions.SessionState{
131+ User: "already",
132+ Email: "already@populated.com",
133+ Groups: []string{"already", "populated"},
134+ IDToken: idToken,
135+ AccessToken: accessToken,
136+ RefreshToken: refreshToken,
137+ },
138+ EmailClaim: "email",
139+ GroupsClaim: "groups",
140+ ProfileJSON: map[string]interface{}{
141+ "email": "new@thing.com",
142+ "groups": []string{"new", "thing"},
143+ },
144+ ExpectedError: nil,
145+ ExpectedSession: &sessions.SessionState{
146+ User: "already",
147+ Email: "already@populated.com",
148+ Groups: []string{"already", "populated"},
149+ IDToken: idToken,
150+ AccessToken: accessToken,
151+ RefreshToken: refreshToken,
152+ },
153+ },
154+ "Missing Email": {
155+ ExistingSession: &sessions.SessionState{
156+ User: "missing.email",
157+ Groups: []string{"already", "populated"},
158+ IDToken: idToken,
159+ AccessToken: accessToken,
160+ RefreshToken: refreshToken,
161+ },
162+ EmailClaim: "email",
163+ GroupsClaim: "groups",
164+ ProfileJSON: map[string]interface{}{
165+ "email": "found@email.com",
166+ "groups": []string{"new", "thing"},
167+ },
168+ ExpectedError: nil,
169+ ExpectedSession: &sessions.SessionState{
170+ User: "missing.email",
171+ Email: "found@email.com",
172+ Groups: []string{"already", "populated"},
173+ IDToken: idToken,
174+ AccessToken: accessToken,
175+ RefreshToken: refreshToken,
176+ },
177+ },
178+
179+ "Missing Email Only in Profile URL": {
180+ ExistingSession: &sessions.SessionState{
181+ User: "missing.email",
182+ IDToken: idToken,
183+ AccessToken: accessToken,
184+ RefreshToken: refreshToken,
185+ },
186+ EmailClaim: "email",
187+ GroupsClaim: "groups",
188+ ProfileJSON: map[string]interface{}{
189+ "email": "found@email.com",
190+ },
191+ ExpectedError: nil,
192+ ExpectedSession: &sessions.SessionState{
193+ User: "missing.email",
194+ Email: "found@email.com",
195+ IDToken: idToken,
196+ AccessToken: accessToken,
197+ RefreshToken: refreshToken,
198+ },
199+ },
200+ "Missing Email with Custom Claim": {
201+ ExistingSession: &sessions.SessionState{
202+ User: "missing.email",
203+ Groups: []string{"already", "populated"},
204+ IDToken: idToken,
205+ AccessToken: accessToken,
206+ RefreshToken: refreshToken,
207+ },
208+ EmailClaim: "weird",
209+ GroupsClaim: "groups",
210+ ProfileJSON: map[string]interface{}{
211+ "weird": "weird@claim.com",
212+ "groups": []string{"new", "thing"},
213+ },
214+ ExpectedError: nil,
215+ ExpectedSession: &sessions.SessionState{
216+ User: "missing.email",
217+ Email: "weird@claim.com",
218+ Groups: []string{"already", "populated"},
219+ IDToken: idToken,
220+ AccessToken: accessToken,
221+ RefreshToken: refreshToken,
222+ },
223+ },
224+ "Missing Email not in Profile URL": {
225+ ExistingSession: &sessions.SessionState{
226+ User: "missing.email",
227+ Groups: []string{"already", "populated"},
228+ IDToken: idToken,
229+ AccessToken: accessToken,
230+ RefreshToken: refreshToken,
231+ },
232+ EmailClaim: "email",
233+ GroupsClaim: "groups",
234+ ProfileJSON: map[string]interface{}{
235+ "groups": []string{"new", "thing"},
236+ },
237+ ExpectedError: errors.New("neither the id_token nor the profileURL set an email"),
238+ ExpectedSession: &sessions.SessionState{
239+ User: "missing.email",
240+ Groups: []string{"already", "populated"},
241+ IDToken: idToken,
242+ AccessToken: accessToken,
243+ RefreshToken: refreshToken,
244+ },
245+ },
246+ "Missing Groups": {
247+ ExistingSession: &sessions.SessionState{
248+ User: "already",
249+ Email: "already@populated.com",
250+ Groups: nil,
251+ IDToken: idToken,
252+ AccessToken: accessToken,
253+ RefreshToken: refreshToken,
254+ },
255+ EmailClaim: "email",
256+ GroupsClaim: "groups",
257+ ProfileJSON: map[string]interface{}{
258+ "email": "new@thing.com",
259+ "groups": []string{"new", "thing"},
260+ },
261+ ExpectedError: nil,
262+ ExpectedSession: &sessions.SessionState{
263+ User: "already",
264+ Email: "already@populated.com",
265+ Groups: []string{"new", "thing"},
266+ IDToken: idToken,
267+ AccessToken: accessToken,
268+ RefreshToken: refreshToken,
269+ },
270+ },
271+ "Missing Groups with Complex Groups in Profile URL": {
272+ ExistingSession: &sessions.SessionState{
273+ User: "already",
274+ Email: "already@populated.com",
275+ Groups: nil,
276+ IDToken: idToken,
277+ AccessToken: accessToken,
278+ RefreshToken: refreshToken,
279+ },
280+ EmailClaim: "email",
281+ GroupsClaim: "groups",
282+ ProfileJSON: map[string]interface{}{
283+ "email": "new@thing.com",
284+ "groups": []map[string]interface{}{
285+ {
286+ "groupId": "Admin Group Id",
287+ "roles": []string{"Admin"},
288+ },
289+ },
290+ },
291+ ExpectedError: nil,
292+ ExpectedSession: &sessions.SessionState{
293+ User: "already",
294+ Email: "already@populated.com",
295+ Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
296+ IDToken: idToken,
297+ AccessToken: accessToken,
298+ RefreshToken: refreshToken,
299+ },
300+ },
301+ "Missing Groups with Singleton Complex Group in Profile URL": {
302+ ExistingSession: &sessions.SessionState{
303+ User: "already",
304+ Email: "already@populated.com",
305+ Groups: nil,
306+ IDToken: idToken,
307+ AccessToken: accessToken,
308+ RefreshToken: refreshToken,
309+ },
310+ EmailClaim: "email",
311+ GroupsClaim: "groups",
312+ ProfileJSON: map[string]interface{}{
313+ "email": "new@thing.com",
314+ "groups": map[string]interface{}{
315+ "groupId": "Admin Group Id",
316+ "roles": []string{"Admin"},
317+ },
318+ },
319+ ExpectedError: nil,
320+ ExpectedSession: &sessions.SessionState{
321+ User: "already",
322+ Email: "already@populated.com",
323+ Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
324+ IDToken: idToken,
325+ AccessToken: accessToken,
326+ RefreshToken: refreshToken,
327+ },
328+ },
329+ "Empty Groups Claims": {
330+ ExistingSession: &sessions.SessionState{
331+ User: "already",
332+ Email: "already@populated.com",
333+ Groups: []string{},
334+ IDToken: idToken,
335+ AccessToken: accessToken,
336+ RefreshToken: refreshToken,
337+ },
338+ EmailClaim: "email",
339+ GroupsClaim: "groups",
340+ ProfileJSON: map[string]interface{}{
341+ "email": "new@thing.com",
342+ "groups": []string{"new", "thing"},
343+ },
344+ ExpectedError: nil,
345+ ExpectedSession: &sessions.SessionState{
346+ User: "already",
347+ Email: "already@populated.com",
348+ Groups: []string{},
349+ IDToken: idToken,
350+ AccessToken: accessToken,
351+ RefreshToken: refreshToken,
352+ },
353+ },
354+ "Missing Groups with Custom Claim": {
355+ ExistingSession: &sessions.SessionState{
356+ User: "already",
357+ Email: "already@populated.com",
358+ Groups: nil,
359+ IDToken: idToken,
360+ AccessToken: accessToken,
361+ RefreshToken: refreshToken,
362+ },
363+ EmailClaim: "email",
364+ GroupsClaim: "roles",
365+ ProfileJSON: map[string]interface{}{
366+ "email": "new@thing.com",
367+ "roles": []string{"new", "thing", "roles"},
368+ },
369+ ExpectedError: nil,
370+ ExpectedSession: &sessions.SessionState{
371+ User: "already",
372+ Email: "already@populated.com",
373+ Groups: []string{"new", "thing", "roles"},
374+ IDToken: idToken,
375+ AccessToken: accessToken,
376+ RefreshToken: refreshToken,
377+ },
378+ },
379+ "Missing Groups String Profile URL Response": {
380+ ExistingSession: &sessions.SessionState{
381+ User: "already",
382+ Email: "already@populated.com",
383+ Groups: nil,
384+ IDToken: idToken,
385+ AccessToken: accessToken,
386+ RefreshToken: refreshToken,
387+ },
388+ EmailClaim: "email",
389+ GroupsClaim: "groups",
390+ ProfileJSON: map[string]interface{}{
391+ "email": "new@thing.com",
392+ "groups": "singleton",
393+ },
394+ ExpectedError: nil,
395+ ExpectedSession: &sessions.SessionState{
396+ User: "already",
397+ Email: "already@populated.com",
398+ Groups: []string{"singleton"},
399+ IDToken: idToken,
400+ AccessToken: accessToken,
401+ RefreshToken: refreshToken,
402+ },
403+ },
404+ "Missing Groups in both Claims and Profile URL": {
405+ ExistingSession: &sessions.SessionState{
406+ User: "already",
407+ Email: "already@populated.com",
408+ IDToken: idToken,
409+ AccessToken: accessToken,
410+ RefreshToken: refreshToken,
411+ },
412+ EmailClaim: "email",
413+ GroupsClaim: "groups",
414+ ProfileJSON: map[string]interface{}{
415+ "email": "new@thing.com",
416+ },
417+ ExpectedError: nil,
418+ ExpectedSession: &sessions.SessionState{
419+ User: "already",
420+ Email: "already@populated.com",
421+ IDToken: idToken,
422+ AccessToken: accessToken,
423+ RefreshToken: refreshToken,
424+ },
425+ },
426+ }
427+ for testName, tc := range testCases {
428+ t.Run(testName, func(t *testing.T) {
429+ jsonResp, err := json.Marshal(tc.ProfileJSON)
430+ assert.NoError(t, err)
431+
432+ server, provider := newTestOIDCSetup(jsonResp)
433+ provider.ProfileURL, err = url.Parse(server.URL)
434+ assert.NoError(t, err)
435+
436+ provider.EmailClaim = tc.EmailClaim
437+ provider.GroupsClaim = tc.GroupsClaim
438+ defer server.Close()
439+
440+ err = provider.EnrichSession(context.Background(), tc.ExistingSession)
441+ assert.Equal(t, tc.ExpectedError, err)
442+ assert.Equal(t, *tc.ExpectedSession, *tc.ExistingSession)
443+ })
444+ }
445+}
446+
236447func TestOIDCProviderRefreshSessionIfNeededWithoutIdToken(t *testing.T) {
237448
238449 idToken, _ := newSignedTestIDToken(defaultIDToken)
@@ -243,7 +454,7 @@ func TestOIDCProviderRefreshSessionIfNeededWithoutIdToken(t *testing.T) {
243454 RefreshToken: refreshToken,
244455 })
245456
246− server, provider := newTestSetup(body)
457+ server, provider := newTestOIDCSetup(body)
247458 defer server.Close()
248459
249460 existingSession := &sessions.SessionState{
@@ -277,7 +488,7 @@ func TestOIDCProviderRefreshSessionIfNeededWithIdToken(t *testing.T) {
277488 IDToken: idToken,
278489 })
279490
280− server, provider := newTestSetup(body)
491+ server, provider := newTestOIDCSetup(body)
281492 defer server.Close()
282493
283494 existingSession := &sessions.SessionState{
@@ -300,48 +511,45 @@ func TestOIDCProviderRefreshSessionIfNeededWithIdToken(t *testing.T) {
300511}
301512
302513func TestOIDCProviderCreateSessionFromToken(t *testing.T) {
303− const profileURLEmail = "janed@me.com"
304−
305514 testCases := map[string]struct {
306515 IDToken idTokenClaims
307516 GroupsClaim string
308517 ExpectedUser string
309518 ExpectedEmail string
310− ExpectedGroups interface{}
519+ ExpectedGroups []string
311520 }{
312521 "Default IDToken": {
313522 IDToken: defaultIDToken,
314523 GroupsClaim: "groups",
315− ExpectedUser: defaultIDToken.Subject,
316− ExpectedEmail: defaultIDToken.Email,
524+ ExpectedUser: "123456789",
525+ ExpectedEmail: "janed@me.com",
317526 ExpectedGroups: []string{"test:a", "test:b"},
318527 },
319528 "Minimal IDToken with no email claim": {
320529 IDToken: minimalIDToken,
321530 GroupsClaim: "groups",
322− ExpectedUser: minimalIDToken.Subject,
323− ExpectedEmail: minimalIDToken.Subject,
324− ExpectedGroups: []string{},
531+ ExpectedUser: "123456789",
532+ ExpectedEmail: "123456789",
533+ ExpectedGroups: nil,
325534 },
326535 "Custom Groups Claim": {
327536 IDToken: defaultIDToken,
328− GroupsClaim: "other_groups",
329− ExpectedUser: defaultIDToken.Subject,
330− ExpectedEmail: defaultIDToken.Email,
537+ GroupsClaim: "roles",
538+ ExpectedUser: "123456789",
539+ ExpectedEmail: "janed@me.com",
331540 ExpectedGroups: []string{"test:c", "test:d"},
332541 },
333− "Custom Groups Claim2": {
334− IDToken: customGroupClaimIDToken,
542+ "Complex Groups Claim": {
543+ IDToken: complexGroupsIDToken,
335544 GroupsClaim: "groups",
336− ExpectedUser: customGroupClaimIDToken.Subject,
337− ExpectedEmail: customGroupClaimIDToken.Email,
545+ ExpectedUser: "123456789",
546+ ExpectedEmail: "complex@claims.com",
338547 ExpectedGroups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
339548 },
340549 }
341550 for testName, tc := range testCases {
342551 t.Run(testName, func(t *testing.T) {
343− jsonResp := []byte(fmt.Sprintf(`{"email":"%s"}`, profileURLEmail))
344− server, provider := newTestSetup(jsonResp)
552+ server, provider := newTestOIDCSetup([]byte(`{}`))
345553 provider.GroupsClaim = tc.GroupsClaim
346554 defer server.Close()
347555
@@ -353,75 +561,10 @@ func TestOIDCProviderCreateSessionFromToken(t *testing.T) {
353561
354562 assert.Equal(t, tc.ExpectedUser, ss.User)
355563 assert.Equal(t, tc.ExpectedEmail, ss.Email)
564+ assert.Equal(t, tc.ExpectedGroups, ss.Groups)
356565 assert.Equal(t, rawIDToken, ss.IDToken)
357566 assert.Equal(t, rawIDToken, ss.AccessToken)
358− assert.Equal(t, tc.ExpectedGroups, ss.Groups)
359567 assert.Equal(t, "", ss.RefreshToken)
360568 })
361569 }
362570}
363−
364−func TestOIDCProvider_findVerifiedIdToken(t *testing.T) {
365−
366− server, provider := newTestSetup([]byte(""))
367−
368− defer server.Close()
369−
370− token := newOauth2Token()
371− signedIDToken, _ := newSignedTestIDToken(defaultIDToken)
372− tokenWithIDToken := token.WithExtra(map[string]interface{}{
373− "id_token": signedIDToken,
374− })
375−
376− verifiedIDToken, err := provider.findVerifiedIDToken(context.Background(), tokenWithIDToken)
377− assert.Equal(t, true, err == nil)
378− if verifiedIDToken == nil {
379− t.Fatal("verifiedIDToken is nil")
380− }
381− assert.Equal(t, defaultIDToken.Issuer, verifiedIDToken.Issuer)
382− assert.Equal(t, defaultIDToken.Subject, verifiedIDToken.Subject)
383−
384− // When the validation fails the response should be nil
385− defaultIDToken.Id = "this-id-fails-validation"
386− signedIDToken, _ = newSignedTestIDToken(defaultIDToken)
387− tokenWithIDToken = token.WithExtra(map[string]interface{}{
388− "id_token": signedIDToken,
389− })
390−
391− verifiedIDToken, err = provider.findVerifiedIDToken(context.Background(), tokenWithIDToken)
392− assert.Equal(t, errors.New("failed to verify signature: the validation failed for subject [123456789]"), err)
393− assert.Equal(t, true, verifiedIDToken == nil)
394−
395− // When there is no id token in the oauth token
396− verifiedIDToken, err = provider.findVerifiedIDToken(context.Background(), newOauth2Token())
397− assert.Equal(t, nil, err)
398− assert.Equal(t, true, verifiedIDToken == nil)
399−}
400−
401−func Test_formatGroup(t *testing.T) {
402− testCases := map[string]struct {
403− RawGroup interface{}
404− ExpectedFormattedGroupValue string
405− }{
406− "String Group": {
407− RawGroup: "group",
408− ExpectedFormattedGroupValue: "group",
409− },
410− "Map Group": {
411− RawGroup: map[string]string{"id": "1", "name": "Test"},
412− ExpectedFormattedGroupValue: "{\"id\":\"1\",\"name\":\"Test\"}",
413− },
414− "List Group": {
415− RawGroup: []string{"First", "Second"},
416− ExpectedFormattedGroupValue: "[\"First\",\"Second\"]",
417− },
418− }
419−
420− for testName, tc := range testCases {
421− t.Run(testName, func(t *testing.T) {
422− formattedGroup, err := formatGroup(tc.RawGroup)
423− assert.Nil(t, err)
424− assert.Equal(t, tc.ExpectedFormattedGroupValue, formattedGroup)
425− })
426− }
427−}
providers/provider_data.go+130−1
@@ -1,12 +1,23 @@
11package providers
22
33import (
4+ "context"
45 "errors"
6+ "fmt"
57 "io/ioutil"
68 "net/url"
9+ "reflect"
10+ "strings"
711
812 "github.com/coreos/go-oidc"
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
914 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
15+ "golang.org/x/oauth2"
16+)
17+
18+const (
19+ OIDCEmailClaim = "email"
20+ OIDCGroupsClaim = "groups"
1021)
1122
1223// ProviderData contains information required to configure all implementations
@@ -27,7 +38,12 @@ type ProviderData struct {
2738 ClientSecretFile string
2839 Scope string
2940 Prompt string
30− Verifier *oidc.IDTokenVerifier
41+
42+ // Common OIDC options for any OIDC-based providers to consume
43+ AllowUnverifiedEmail bool
44+ EmailClaim string
45+ GroupsClaim string
46+ Verifier *oidc.IDTokenVerifier
3147
3248 // Universal Group authorization data structure
3349 // any provider can set to consume
@@ -94,3 +110,116 @@ func defaultURL(u *url.URL, d *url.URL) *url.URL {
94110 }
95111 return &url.URL{}
96112}
113+
114+// ****************************************************************************
115+// These private OIDC helper methods are available to any providers that are
116+// OIDC compliant
117+// ****************************************************************************
118+
119+// OIDCClaims is a struct to unmarshal the OIDC claims from an ID Token payload
120+type OIDCClaims struct {
121+ Subject string `json:"sub"`
122+ Email string `json:"-"`
123+ Groups []string `json:"-"`
124+ Verified *bool `json:"email_verified"`
125+
126+ raw map[string]interface{}
127+}
128+
129+func (p *ProviderData) verifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
130+ rawIDToken := getIDToken(token)
131+ if strings.TrimSpace(rawIDToken) == "" {
132+ return nil, ErrMissingIDToken
133+ }
134+ if p.Verifier == nil {
135+ return nil, ErrMissingOIDCVerifier
136+ }
137+ return p.Verifier.Verify(ctx, rawIDToken)
138+}
139+
140+// buildSessionFromClaims uses IDToken claims to populate a fresh SessionState
141+// with non-Token related fields.
142+func (p *ProviderData) buildSessionFromClaims(idToken *oidc.IDToken) (*sessions.SessionState, error) {
143+ ss := &sessions.SessionState{}
144+
145+ if idToken == nil {
146+ return ss, nil
147+ }
148+
149+ claims, err := p.getClaims(idToken)
150+ if err != nil {
151+ return nil, fmt.Errorf("couldn't extract claims from id_token (%v)", err)
152+ }
153+
154+ ss.User = claims.Subject
155+ ss.Email = claims.Email
156+ ss.Groups = claims.Groups
157+
158+ // TODO (@NickMeves) Deprecate for dynamic claim to session mapping
159+ if pref, ok := claims.raw["preferred_username"].(string); ok {
160+ ss.PreferredUsername = pref
161+ }
162+
163+ // `email_verified` must be present and explicitly set to `false` to be
164+ // considered unverified.
165+ verifyEmail := (p.EmailClaim == OIDCEmailClaim) && !p.AllowUnverifiedEmail
166+ if verifyEmail && claims.Verified != nil && !*claims.Verified {
167+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
168+ }
169+
170+ return ss, nil
171+}
172+
173+// getClaims extracts IDToken claims into an OIDCClaims
174+func (p *ProviderData) getClaims(idToken *oidc.IDToken) (*OIDCClaims, error) {
175+ claims := &OIDCClaims{}
176+
177+ // Extract default claims.
178+ if err := idToken.Claims(&claims); err != nil {
179+ return nil, fmt.Errorf("failed to parse default id_token claims: %v", err)
180+ }
181+ // Extract custom claims.
182+ if err := idToken.Claims(&claims.raw); err != nil {
183+ return nil, fmt.Errorf("failed to parse all id_token claims: %v", err)
184+ }
185+
186+ email := claims.raw[p.EmailClaim]
187+ if email != nil {
188+ claims.Email = fmt.Sprint(email)
189+ }
190+ claims.Groups = p.extractGroups(claims.raw)
191+
192+ return claims, nil
193+}
194+
195+// extractGroups extracts groups from a claim to a list in a type safe manner.
196+// If the claim isn't present, `nil` is returned. If the groups claim is
197+// present but empty, `[]string{}` is returned.
198+func (p *ProviderData) extractGroups(claims map[string]interface{}) []string {
199+ rawClaim, ok := claims[p.GroupsClaim]
200+ if !ok {
201+ return nil
202+ }
203+
204+ // Handle traditional list-based groups as well as non-standard singleton
205+ // based groups. Both variants support complex objects if needed.
206+ var claimGroups []interface{}
207+ switch raw := rawClaim.(type) {
208+ case []interface{}:
209+ claimGroups = raw
210+ case interface{}:
211+ claimGroups = []interface{}{raw}
212+ }
213+
214+ groups := []string{}
215+ for _, rawGroup := range claimGroups {
216+ formattedGroup, err := formatGroup(rawGroup)
217+ if err != nil {
218+ logger.Errorf("Warning: unable to format group of type %s with error %s",
219+ reflect.TypeOf(rawGroup), err)
220+ continue
221+ }
222+ groups = append(groups, formattedGroup)
223+ }
224+ return groups
225+}
providers/provider_data_test.gonew file+437
@@ -0,0 +1,437 @@
1+package providers
2+
3+import (
4+ "context"
5+ "crypto/rand"
6+ "crypto/rsa"
7+ "encoding/base64"
8+ "encoding/json"
9+ "errors"
10+ "fmt"
11+ "strings"
12+ "testing"
13+ "time"
14+
15+ "github.com/coreos/go-oidc"
16+ "github.com/dgrijalva/jwt-go"
17+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
18+ . "github.com/onsi/gomega"
19+ "golang.org/x/oauth2"
20+)
21+
22+const (
23+ idToken = "eyJfoobar123.eyJbaz987.IDToken"
24+ accessToken = "eyJfoobar123.eyJbaz987.AccessToken"
25+ refreshToken = "eyJfoobar123.eyJbaz987.RefreshToken"
26+
27+ oidcIssuer = "https://issuer.example.com"
28+ oidcClientID = "https://test.myapp.com"
29+ oidcSecret = "SuperSecret123456789"
30+
31+ failureTokenID = "this-id-fails-verification"
32+)
33+
34+var (
35+ verified = true
36+ unverified = false
37+
38+ standardClaims = jwt.StandardClaims{
39+ Audience: oidcClientID,
40+ ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
41+ Id: "id-some-id",
42+ IssuedAt: time.Now().Unix(),
43+ Issuer: oidcIssuer,
44+ NotBefore: 0,
45+ Subject: "123456789",
46+ }
47+
48+ defaultIDToken = idTokenClaims{
49+ Name: "Jane Dobbs",
50+ Email: "janed@me.com",
51+ Phone: "+4798765432",
52+ Picture: "http://mugbook.com/janed/me.jpg",
53+ Groups: []string{"test:a", "test:b"},
54+ Roles: []string{"test:c", "test:d"},
55+ Verified: &verified,
56+ StandardClaims: standardClaims,
57+ }
58+
59+ complexGroupsIDToken = idTokenClaims{
60+ Name: "Complex Claim",
61+ Email: "complex@claims.com",
62+ Phone: "+5439871234",
63+ Picture: "http://mugbook.com/complex/claims.jpg",
64+ Groups: []map[string]interface{}{
65+ {
66+ "groupId": "Admin Group Id",
67+ "roles": []string{"Admin"},
68+ },
69+ },
70+ Roles: []string{"test:simple", "test:roles"},
71+ Verified: &verified,
72+ StandardClaims: standardClaims,
73+ }
74+
75+ unverifiedIDToken = idTokenClaims{
76+ Name: "Mystery Man",
77+ Email: "unverified@email.com",
78+ Phone: "+4025205729",
79+ Picture: "http://mugbook.com/unverified/email.jpg",
80+ Groups: []string{"test:a", "test:b"},
81+ Roles: []string{"test:c", "test:d"},
82+ Verified: &unverified,
83+ StandardClaims: standardClaims,
84+ }
85+
86+ minimalIDToken = idTokenClaims{
87+ StandardClaims: standardClaims,
88+ }
89+)
90+
91+type idTokenClaims struct {
92+ Name string `json:"preferred_username,omitempty"`
93+ Email string `json:"email,omitempty"`
94+ Phone string `json:"phone_number,omitempty"`
95+ Picture string `json:"picture,omitempty"`
96+ Groups interface{} `json:"groups,omitempty"`
97+ Roles interface{} `json:"roles,omitempty"`
98+ Verified *bool `json:"email_verified,omitempty"`
99+ jwt.StandardClaims
100+}
101+
102+type mockJWKS struct{}
103+
104+func (mockJWKS) VerifySignature(_ context.Context, jwt string) ([]byte, error) {
105+ decoded, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
106+ if err != nil {
107+ return nil, err
108+ }
109+
110+ tokenClaims := &idTokenClaims{}
111+ err = json.Unmarshal(decoded, tokenClaims)
112+ if err != nil || tokenClaims.Id == failureTokenID {
113+ return nil, fmt.Errorf("the validation failed for subject [%v]", tokenClaims.Subject)
114+ }
115+
116+ return decoded, nil
117+}
118+
119+func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
120+ key, _ := rsa.GenerateKey(rand.Reader, 2048)
121+ standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
122+ return standardClaims.SignedString(key)
123+}
124+
125+func newTestOauth2Token() *oauth2.Token {
126+ return &oauth2.Token{
127+ AccessToken: accessToken,
128+ TokenType: "Bearer",
129+ RefreshToken: refreshToken,
130+ Expiry: time.Time{}.Add(time.Duration(5) * time.Second),
131+ }
132+}
133+
134+func TestProviderData_verifyIDToken(t *testing.T) {
135+ failureIDToken := defaultIDToken
136+ failureIDToken.Id = failureTokenID
137+
138+ testCases := map[string]struct {
139+ IDToken *idTokenClaims
140+ Verifier bool
141+ ExpectIDToken bool
142+ ExpectedError error
143+ }{
144+ "Valid ID Token": {
145+ IDToken: &defaultIDToken,
146+ Verifier: true,
147+ ExpectIDToken: true,
148+ ExpectedError: nil,
149+ },
150+ "Invalid ID Token": {
151+ IDToken: &failureIDToken,
152+ Verifier: true,
153+ ExpectIDToken: false,
154+ ExpectedError: errors.New("failed to verify signature: the validation failed for subject [123456789]"),
155+ },
156+ "Missing ID Token": {
157+ IDToken: nil,
158+ Verifier: true,
159+ ExpectIDToken: false,
160+ ExpectedError: ErrMissingIDToken,
161+ },
162+ "OIDC Verifier not Configured": {
163+ IDToken: &defaultIDToken,
164+ Verifier: false,
165+ ExpectIDToken: false,
166+ ExpectedError: ErrMissingOIDCVerifier,
167+ },
168+ }
169+
170+ for testName, tc := range testCases {
171+ t.Run(testName, func(t *testing.T) {
172+ g := NewWithT(t)
173+
174+ token := newTestOauth2Token()
175+ if tc.IDToken != nil {
176+ idToken, err := newSignedTestIDToken(*tc.IDToken)
177+ g.Expect(err).ToNot(HaveOccurred())
178+ token = token.WithExtra(map[string]interface{}{
179+ "id_token": idToken,
180+ })
181+ }
182+
183+ provider := &ProviderData{}
184+ if tc.Verifier {
185+ provider.Verifier = oidc.NewVerifier(
186+ oidcIssuer,
187+ mockJWKS{},
188+ &oidc.Config{ClientID: oidcClientID},
189+ )
190+ }
191+ verified, err := provider.verifyIDToken(context.Background(), token)
192+ if err != nil {
193+ g.Expect(err).To(Equal(tc.ExpectedError))
194+ }
195+
196+ if tc.ExpectIDToken {
197+ g.Expect(verified).ToNot(BeNil())
198+ g.Expect(*verified).To(BeAssignableToTypeOf(oidc.IDToken{}))
199+ } else {
200+ g.Expect(verified).To(BeNil())
201+ }
202+ })
203+ }
204+}
205+
206+func TestProviderData_buildSessionFromClaims(t *testing.T) {
207+ testCases := map[string]struct {
208+ IDToken idTokenClaims
209+ AllowUnverified bool
210+ EmailClaim string
211+ GroupsClaim string
212+ ExpectedError error
213+ ExpectedSession *sessions.SessionState
214+ }{
215+ "Standard": {
216+ IDToken: defaultIDToken,
217+ AllowUnverified: false,
218+ EmailClaim: "email",
219+ GroupsClaim: "groups",
220+ ExpectedSession: &sessions.SessionState{
221+ User: "123456789",
222+ Email: "janed@me.com",
223+ Groups: []string{"test:a", "test:b"},
224+ PreferredUsername: "Jane Dobbs",
225+ },
226+ },
227+ "Unverified Denied": {
228+ IDToken: unverifiedIDToken,
229+ AllowUnverified: false,
230+ EmailClaim: "email",
231+ GroupsClaim: "groups",
232+ ExpectedError: errors.New("email in id_token (unverified@email.com) isn't verified"),
233+ },
234+ "Unverified Allowed": {
235+ IDToken: unverifiedIDToken,
236+ AllowUnverified: true,
237+ EmailClaim: "email",
238+ GroupsClaim: "groups",
239+ ExpectedSession: &sessions.SessionState{
240+ User: "123456789",
241+ Email: "unverified@email.com",
242+ Groups: []string{"test:a", "test:b"},
243+ PreferredUsername: "Mystery Man",
244+ },
245+ },
246+ "Complex Groups": {
247+ IDToken: complexGroupsIDToken,
248+ AllowUnverified: true,
249+ EmailClaim: "email",
250+ GroupsClaim: "groups",
251+ ExpectedSession: &sessions.SessionState{
252+ User: "123456789",
253+ Email: "complex@claims.com",
254+ Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
255+ PreferredUsername: "Complex Claim",
256+ },
257+ },
258+ "Email Claim Switched": {
259+ IDToken: unverifiedIDToken,
260+ AllowUnverified: true,
261+ EmailClaim: "phone_number",
262+ GroupsClaim: "groups",
263+ ExpectedSession: &sessions.SessionState{
264+ User: "123456789",
265+ Email: "+4025205729",
266+ Groups: []string{"test:a", "test:b"},
267+ PreferredUsername: "Mystery Man",
268+ },
269+ },
270+ "Email Claim Switched to Non String": {
271+ IDToken: unverifiedIDToken,
272+ AllowUnverified: true,
273+ EmailClaim: "roles",
274+ GroupsClaim: "groups",
275+ ExpectedSession: &sessions.SessionState{
276+ User: "123456789",
277+ Email: "[test:c test:d]",
278+ Groups: []string{"test:a", "test:b"},
279+ PreferredUsername: "Mystery Man",
280+ },
281+ },
282+ "Email Claim Non Existent": {
283+ IDToken: unverifiedIDToken,
284+ AllowUnverified: true,
285+ EmailClaim: "aksjdfhjksadh",
286+ GroupsClaim: "groups",
287+ ExpectedSession: &sessions.SessionState{
288+ User: "123456789",
289+ Email: "",
290+ Groups: []string{"test:a", "test:b"},
291+ PreferredUsername: "Mystery Man",
292+ },
293+ },
294+ "Groups Claim Switched": {
295+ IDToken: defaultIDToken,
296+ AllowUnverified: false,
297+ EmailClaim: "email",
298+ GroupsClaim: "roles",
299+ ExpectedSession: &sessions.SessionState{
300+ User: "123456789",
301+ Email: "janed@me.com",
302+ Groups: []string{"test:c", "test:d"},
303+ PreferredUsername: "Jane Dobbs",
304+ },
305+ },
306+ "Groups Claim Non Existent": {
307+ IDToken: defaultIDToken,
308+ AllowUnverified: false,
309+ EmailClaim: "email",
310+ GroupsClaim: "alskdjfsalkdjf",
311+ ExpectedSession: &sessions.SessionState{
312+ User: "123456789",
313+ Email: "janed@me.com",
314+ Groups: nil,
315+ PreferredUsername: "Jane Dobbs",
316+ },
317+ },
318+ }
319+ for testName, tc := range testCases {
320+ t.Run(testName, func(t *testing.T) {
321+ g := NewWithT(t)
322+
323+ provider := &ProviderData{
324+ Verifier: oidc.NewVerifier(
325+ oidcIssuer,
326+ mockJWKS{},
327+ &oidc.Config{ClientID: oidcClientID},
328+ ),
329+ }
330+ provider.AllowUnverifiedEmail = tc.AllowUnverified
331+ provider.EmailClaim = tc.EmailClaim
332+ provider.GroupsClaim = tc.GroupsClaim
333+
334+ rawIDToken, err := newSignedTestIDToken(tc.IDToken)
335+ g.Expect(err).ToNot(HaveOccurred())
336+
337+ idToken, err := provider.Verifier.Verify(context.Background(), rawIDToken)
338+ g.Expect(err).ToNot(HaveOccurred())
339+
340+ ss, err := provider.buildSessionFromClaims(idToken)
341+ if err != nil {
342+ g.Expect(err).To(Equal(tc.ExpectedError))
343+ }
344+ if ss != nil {
345+ g.Expect(ss).To(Equal(tc.ExpectedSession))
346+ }
347+ })
348+ }
349+}
350+
351+func TestProviderData_extractGroups(t *testing.T) {
352+ testCases := map[string]struct {
353+ Claims map[string]interface{}
354+ GroupsClaim string
355+ ExpectedGroups []string
356+ }{
357+ "Standard String Groups": {
358+ Claims: map[string]interface{}{
359+ "email": "this@does.not.matter.com",
360+ "groups": []interface{}{"three", "string", "groups"},
361+ },
362+ GroupsClaim: "groups",
363+ ExpectedGroups: []string{"three", "string", "groups"},
364+ },
365+ "Different Claim Name": {
366+ Claims: map[string]interface{}{
367+ "email": "this@does.not.matter.com",
368+ "roles": []interface{}{"three", "string", "roles"},
369+ },
370+ GroupsClaim: "roles",
371+ ExpectedGroups: []string{"three", "string", "roles"},
372+ },
373+ "Numeric Groups": {
374+ Claims: map[string]interface{}{
375+ "email": "this@does.not.matter.com",
376+ "groups": []interface{}{1, 2, 3},
377+ },
378+ GroupsClaim: "groups",
379+ ExpectedGroups: []string{"1", "2", "3"},
380+ },
381+ "Complex Groups": {
382+ Claims: map[string]interface{}{
383+ "email": "this@does.not.matter.com",
384+ "groups": []interface{}{
385+ map[string]interface{}{
386+ "groupId": "Admin Group Id",
387+ "roles": []string{"Admin"},
388+ },
389+ 12345,
390+ "Just::A::String",
391+ },
392+ },
393+ GroupsClaim: "groups",
394+ ExpectedGroups: []string{
395+ "{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}",
396+ "12345",
397+ "Just::A::String",
398+ },
399+ },
400+ "Missing Groups Claim Returns Nil": {
401+ Claims: map[string]interface{}{
402+ "email": "this@does.not.matter.com",
403+ },
404+ GroupsClaim: "groups",
405+ ExpectedGroups: nil,
406+ },
407+ "Non List Groups": {
408+ Claims: map[string]interface{}{
409+ "email": "this@does.not.matter.com",
410+ "groups": "singleton",
411+ },
412+ GroupsClaim: "groups",
413+ ExpectedGroups: []string{"singleton"},
414+ },
415+ }
416+ for testName, tc := range testCases {
417+ t.Run(testName, func(t *testing.T) {
418+ g := NewWithT(t)
419+
420+ provider := &ProviderData{
421+ Verifier: oidc.NewVerifier(
422+ oidcIssuer,
423+ mockJWKS{},
424+ &oidc.Config{ClientID: oidcClientID},
425+ ),
426+ }
427+ provider.GroupsClaim = tc.GroupsClaim
428+
429+ groups := provider.extractGroups(tc.Claims)
430+ if tc.ExpectedGroups != nil {
431+ g.Expect(groups).To(Equal(tc.ExpectedGroups))
432+ } else {
433+ g.Expect(groups).To(BeNil())
434+ }
435+ })
436+ }
437+}
providers/provider_default.go+11−3
@@ -22,6 +22,14 @@ var (
2222 // code
2323 ErrMissingCode = errors.New("missing code")
2424
25+ // ErrMissingIDToken is returned when an oidc.Token does not contain the
26+ // extra `id_token` field for an IDToken.
27+ ErrMissingIDToken = errors.New("missing id_token")
28+
29+ // ErrMissingOIDCVerifier is returned when a provider didn't set `Verifier`
30+ // but an attempt to call `Verifier.Verify` was about to be made.
31+ ErrMissingOIDCVerifier = errors.New("oidc verifier is not configured")
32+
2533 _ Provider = (*ProviderData)(nil)
2634)
2735
@@ -86,12 +94,12 @@ func (p *ProviderData) GetLoginURL(redirectURI, state string) string {
8694}
8795
8896// GetEmailAddress returns the Account email address
89−// DEPRECATED: Migrate to EnrichSessionState
97+// Deprecated: Migrate to EnrichSession
9098func (p *ProviderData) GetEmailAddress(_ context.Context, _ *sessions.SessionState) (string, error) {
9199 return "", ErrNotImplemented
92100}
93101
94−// EnrichSessionState is called after Redeem to allow providers to enrich session fields
102+// EnrichSession is called after Redeem to allow providers to enrich session fields
95103// such as User, Email, Groups with provider specific API calls.
96104func (p *ProviderData) EnrichSession(_ context.Context, _ *sessions.SessionState) error {
97105 return nil
@@ -113,7 +121,7 @@ func (p *ProviderData) Authorize(_ context.Context, s *sessions.SessionState) (b
113121 return false, nil
114122}
115123
116−// ValidateSessionState validates the AccessToken
124+// ValidateSession validates the AccessToken
117125func (p *ProviderData) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
118126 return validateToken(ctx, p, s.AccessToken, nil)
119127}
providers/providers.go+1−1
@@ -9,7 +9,7 @@ import (
99// Provider represents an upstream identity provider implementation
1010type Provider interface {
1111 Data() *ProviderData
12− // DEPRECATED: Migrate to EnrichSessionState
12+ // Deprecated: Migrate to EnrichSession
1313 GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error)
1414 Redeem(ctx context.Context, redirectURI, code string) (*sessions.SessionState, error)
1515 EnrichSession(ctx context.Context, s *sessions.SessionState) error
providers/providers_suite_test.gonew file+17
@@ -0,0 +1,17 @@
1+package providers_test
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestProviderSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
14+
15+ RegisterFailHandler(Fail)
16+ RunSpecs(t, "Providers")
17+}
providers/util.go+43
@@ -1,9 +1,13 @@
11package providers
22
33import (
4+ "encoding/json"
45 "fmt"
56 "net/http"
67 "net/url"
8+
9+ "github.com/bitly/go-simplejson"
10+ "golang.org/x/oauth2"
711)
812
913const (
@@ -55,3 +59,42 @@ func makeLoginURL(p *ProviderData, redirectURI, state string, extraParams url.Va
5559 a.RawQuery = params.Encode()
5660 return a
5761}
62+
63+// getIDToken extracts an IDToken stored in the `Extra` fields of an
64+// oauth2.Token
65+func getIDToken(token *oauth2.Token) string {
66+ idToken, ok := token.Extra("id_token").(string)
67+ if !ok {
68+ return ""
69+ }
70+ return idToken
71+}
72+
73+// formatGroup coerces an OIDC groups claim into a string
74+// If it is non-string, marshal it into JSON.
75+func formatGroup(rawGroup interface{}) (string, error) {
76+ if group, ok := rawGroup.(string); ok {
77+ return group, nil
78+ }
79+
80+ jsonGroup, err := json.Marshal(rawGroup)
81+ if err != nil {
82+ return "", err
83+ }
84+ return string(jsonGroup), nil
85+}
86+
87+// coerceArray extracts a field from simplejson.Json that might be a
88+// singleton or a list and coerces it into a list.
89+func coerceArray(sj *simplejson.Json, key string) []interface{} {
90+ array, err := sj.Get(key).Array()
91+ if err == nil {
92+ return array
93+ }
94+
95+ single := sj.Get(key).Interface()
96+ if single == nil {
97+ return nil
98+ }
99+ return []interface{}{single}
100+}
providers/util_test.go+48−1
@@ -5,9 +5,10 @@ import (
55 "testing"
66
77 . "github.com/onsi/gomega"
8+ "golang.org/x/oauth2"
89)
910
10−func TestMakeAuhtorizationHeader(t *testing.T) {
11+func Test_makeAuthorizationHeader(t *testing.T) {
1112 testCases := []struct {
1213 name string
1314 prefix string
@@ -64,3 +65,49 @@ func TestMakeAuhtorizationHeader(t *testing.T) {
6465 })
6566 }
6667}
68+
69+func Test_getIDToken(t *testing.T) {
70+ const idToken = "eyJfoobar.eyJfoobar.12345asdf"
71+ g := NewWithT(t)
72+
73+ token := &oauth2.Token{}
74+ g.Expect(getIDToken(token)).To(Equal(""))
75+
76+ extraToken := token.WithExtra(map[string]interface{}{
77+ "id_token": idToken,
78+ })
79+ g.Expect(getIDToken(extraToken)).To(Equal(idToken))
80+}
81+
82+func Test_formatGroup(t *testing.T) {
83+ testCases := map[string]struct {
84+ rawGroup interface{}
85+ expected string
86+ }{
87+ "String Group": {
88+ rawGroup: "group",
89+ expected: "group",
90+ },
91+ "Numeric Group": {
92+ rawGroup: 123,
93+ expected: "123",
94+ },
95+ "Map Group": {
96+ rawGroup: map[string]string{"id": "1", "name": "Test"},
97+ expected: "{\"id\":\"1\",\"name\":\"Test\"}",
98+ },
99+ "List Group": {
100+ rawGroup: []string{"First", "Second"},
101+ expected: "[\"First\",\"Second\"]",
102+ },
103+ }
104+
105+ for testName, tc := range testCases {
106+ t.Run(testName, func(t *testing.T) {
107+ g := NewWithT(t)
108+ formattedGroup, err := formatGroup(tc.rawGroup)
109+ g.Expect(err).ToNot(HaveOccurred())
110+ g.Expect(formattedGroup).To(Equal(tc.expected))
111+ })
112+ }
113+}
templates.godeleted−187
@@ -1,187 +0,0 @@
1−package main
2−
3−import (
4− "html/template"
5− "path"
6− "strings"
7−
8− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
9−)
10−
11−func loadTemplates(dir string) *template.Template {
12− if dir == "" {
13− return getTemplates()
14− }
15− logger.Printf("using custom template directory %q", dir)
16− funcMap := template.FuncMap{
17− "ToUpper": strings.ToUpper,
18− "ToLower": strings.ToLower,
19− }
20− t, err := template.New("").Funcs(funcMap).ParseFiles(path.Join(dir, "sign_in.html"), path.Join(dir, "error.html"))
21− if err != nil {
22− logger.Fatalf("failed parsing template %s", err)
23− }
24− return t
25−}
26−
27−func getTemplates() *template.Template {
28− t, err := template.New("foo").Parse(`{{define "sign_in.html"}}
29−<!DOCTYPE html>
30−<html lang="en" charset="utf-8">
31−<head>
32− <title>Sign In</title>
33− <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
34− <style>
35− body {
36− font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
37− font-size: 14px;
38− line-height: 1.42857143;
39− color: #333;
40− background: #f0f0f0;
41− }
42− .signin {
43− display:block;
44− margin:20px auto;
45− max-width:400px;
46− background: #fff;
47− border:1px solid #ccc;
48− border-radius: 10px;
49− padding: 20px;
50− }
51− .center {
52− text-align:center;
53− }
54− .btn {
55− color: #fff;
56− background-color: #428bca;
57− border: 1px solid #357ebd;
58− -webkit-border-radius: 4;
59− -moz-border-radius: 4;
60− border-radius: 4px;
61− font-size: 14px;
62− padding: 6px 12px;
63− text-decoration: none;
64− cursor: pointer;
65− }
66−
67− .btn:hover {
68− background-color: #3071a9;
69− border-color: #285e8e;
70− text-decoration: none;
71− }
72− label {
73− display: inline-block;
74− max-width: 100%;
75− margin-bottom: 5px;
76− font-weight: 700;
77− }
78− input {
79− display: block;
80− width: 100%;
81− height: 34px;
82− padding: 6px 12px;
83− font-size: 14px;
84− line-height: 1.42857143;
85− color: #555;
86− background-color: #fff;
87− background-image: none;
88− border: 1px solid #ccc;
89− border-radius: 4px;
90− -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
91− box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
92− -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
93− -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
94− transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
95− margin:0;
96− box-sizing: border-box;
97− }
98− footer {
99− display:block;
100− font-size:10px;
101− color:#aaa;
102− text-align:center;
103− margin-bottom:10px;
104− }
105− footer a {
106− display:inline-block;
107− height:25px;
108− line-height:25px;
109− color:#aaa;
110− text-decoration:underline;
111− }
112− footer a:hover {
113− color:#aaa;
114− }
115− </style>
116−</head>
117−<body>
118− <div class="signin center">
119− <form method="GET" action="{{.ProxyPrefix}}/start">
120− <input type="hidden" name="rd" value="{{.Redirect}}">
121− {{ if .SignInMessage }}
122− <p>{{.SignInMessage}}</p>
123− {{ end}}
124− <button type="submit" class="btn">Sign in with {{.ProviderName}}</button><br/>
125− </form>
126− </div>
127−
128− {{ if .CustomLogin }}
129− <div class="signin">
130− <form method="POST" action="{{.ProxyPrefix}}/sign_in">
131− <input type="hidden" name="rd" value="{{.Redirect}}">
132− <label for="username">Username:</label><input type="text" name="username" id="username" size="10"><br/>
133− <label for="password">Password:</label><input type="password" name="password" id="password" size="10"><br/>
134− <button type="submit" class="btn">Sign In</button>
135− </form>
136− </div>
137− {{ end }}
138− <script>
139− if (window.location.hash) {
140− (function() {
141− var inputs = document.getElementsByName('rd');
142− for (var i = 0; i < inputs.length; i++) {
143− // Add hash, but make sure it is only added once
144− var idx = inputs[i].value.indexOf('#');
145− if (idx >= 0) {
146− // Remove existing hash from URL
147− inputs[i].value = inputs[i].value.substr(0, idx);
148− }
149− inputs[i].value += window.location.hash;
150− }
151− })();
152− }
153− </script>
154− <footer>
155− {{ if eq .Footer "-" }}
156− {{ else if eq .Footer ""}}
157− Secured with <a href="https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy">OAuth2 Proxy</a> version {{.Version}}
158− {{ else }}
159− {{.Footer}}
160− {{ end }}
161− </footer>
162−</body>
163−</html>
164−{{end}}`)
165− if err != nil {
166− logger.Fatalf("failed parsing template %s", err)
167− }
168−
169− t, err = t.Parse(`{{define "error.html"}}
170−<!DOCTYPE html>
171−<html lang="en" charset="utf-8">
172−<head>
173− <title>{{.Title}}</title>
174− <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
175−</head>
176−<body>
177− <h2>{{.Title}}</h2>
178− <p>{{.Message}}</p>
179− <hr>
180− <p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p>
181−</body>
182−</html>{{end}}`)
183− if err != nil {
184− logger.Fatalf("failed parsing template %s", err)
185− }
186− return t
187−}
templates_test.godeleted−62
@@ -1,62 +0,0 @@
1−package main
2−
3−import (
4− "bytes"
5− "io/ioutil"
6− "log"
7− "os"
8− "path/filepath"
9− "testing"
10−
11− "github.com/stretchr/testify/assert"
12−)
13−
14−func TestLoadTemplates(t *testing.T) {
15− data := struct {
16− TestString string
17− }{
18− TestString: "Testing",
19− }
20−
21− templates := loadTemplates("")
22− assert.NotEqual(t, templates, nil)
23−
24− var defaultSignin bytes.Buffer
25− templates.ExecuteTemplate(&defaultSignin, "sign_in.html", data)
26− assert.Equal(t, "\n<!DOCTYPE html>", defaultSignin.String()[0:16])
27−
28− var defaultError bytes.Buffer
29− templates.ExecuteTemplate(&defaultError, "error.html", data)
30− assert.Equal(t, "\n<!DOCTYPE html>", defaultError.String()[0:16])
31−
32− dir, err := ioutil.TempDir("", "templatetest")
33− if err != nil {
34− log.Fatal(err)
35− }
36− defer os.RemoveAll(dir)
37−
38− templateHTML := `{{.TestString}} {{.TestString | ToLower}} {{.TestString | ToUpper}}`
39− signInFile := filepath.Join(dir, "sign_in.html")
40− if err := ioutil.WriteFile(signInFile, []byte(templateHTML), 0666); err != nil {
41− log.Fatal(err)
42− }
43− errorFile := filepath.Join(dir, "error.html")
44− if err := ioutil.WriteFile(errorFile, []byte(templateHTML), 0666); err != nil {
45− log.Fatal(err)
46− }
47− templates = loadTemplates(dir)
48− assert.NotEqual(t, templates, nil)
49−
50− var sitpl bytes.Buffer
51− templates.ExecuteTemplate(&sitpl, "sign_in.html", data)
52− assert.Equal(t, "Testing testing TESTING", sitpl.String())
53−
54− var errtpl bytes.Buffer
55− templates.ExecuteTemplate(&errtpl, "error.html", data)
56− assert.Equal(t, "Testing testing TESTING", errtpl.String())
57−}
58−
59−func TestTemplatesCompile(t *testing.T) {
60− templates := getTemplates()
61− assert.NotEqual(t, templates, nil)
62−}
tools/tools.gonew file+7
@@ -0,0 +1,7 @@
1+// +build tools
2+
3+package tools
4+
5+import (
6+ _ "github.com/oauth2-proxy/tools/reference-gen/cmd/reference-gen"
7+)