git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge branch 'master' into keycloak-provider
71dfd44Henry Jenkins
.github/CODEOWNERS+4
@@ -10,3 +10,7 @@
10# or the public devops channel at https://chat.18f.gov/).
10# or the public devops channel at https://chat.18f.gov/).
11providers/logingov.go @timothy-spencer
11providers/logingov.go @timothy-spencer
12providers/logingov_test.go @timothy-spencer
12providers/logingov_test.go @timothy-spencer
13+
14+# Bitbucket provider
15+providers/bitbucket.go @aledeganopix4d
16+providers/bitbucket_test.go @aledeganopix4d
.travis.yml+1−3
@@ -3,10 +3,8 @@ go:
3 - 1.12.x
3 - 1.12.x
4install:
4install:
5 # Fetch dependencies
5 # Fetch dependencies
6− - wget -O dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
7− - chmod +x dep
8− - mv dep $GOPATH/bin/dep
9 - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.17.1
6 - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.17.1
7+ - GO111MODULE=on go mod download
10script:
8script:
11 - ./configure && make test
9 - ./configure && make test
12sudo: false
10sudo: false
CHANGELOG.md+44−15
@@ -1,7 +1,28 @@
1# Vx.x.x (Pre-release)
1# Vx.x.x (Pre-release)
2
2
3+## Changes since v4.0.0
4+
5+- [#226](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka)
6+
7+# v4.0.0
8+
9+## Release Highlights
10+- Documentation is now on a [microsite](https://pusher.github.io/oauth2_proxy/)
11+- Health check logging can now be disabled for quieter logs
12+- Authorization Header JWTs can now be verified by the proxy to skip authentication for machine users
13+- Sessions can now be stored in Redis. This reduces refresh failures and uses smaller cookies (Recommended for those using OIDC refreshing)
14+- Logging overhaul allows customisable logging formats
15+
16+## Important Notes
17+- This release includes a number of breaking changes that will require users to
18+reconfigure their proxies. Please read the Breaking Changes below thoroughly.
19+
3## Breaking Changes
20## Breaking Changes
4
21
22+- [#231](https://github.com/pusher/oauth2_proxy/pull/231) Rework GitLab provider
23+ - This PR changes the configuration options for the GitLab provider to use
24+ a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than
25+ explicit `-login-url`, `-redeem-url` and `-validate-url` parameters.
5- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent
26- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent
6 - This PR changes configuration options so that all flags have a config counterpart
27 - This PR changes configuration options so that all flags have a config counterpart
7 of the same name but with underscores (`_`) in place of hyphens (`-`).
28 of the same name but with underscores (`_`) in place of hyphens (`-`).
@@ -18,8 +39,7 @@
18 This change affects the following existing environment variables:
39 This change affects the following existing environment variables:
19 - The `OAUTH2_SKIP_OIDC_DISCOVERY` environment variable is now `OAUTH2_PROXY_SKIP_OIDC_DISCOVERY`.
40 - The `OAUTH2_SKIP_OIDC_DISCOVERY` environment variable is now `OAUTH2_PROXY_SKIP_OIDC_DISCOVERY`.
20 - The `OAUTH2_OIDC_JWKS_URL` environment variable is now `OAUTH2_PROXY_OIDC_JWKS_URL`.
41 - The `OAUTH2_OIDC_JWKS_URL` environment variable is now `OAUTH2_PROXY_OIDC_JWKS_URL`.
21−
22−- [#146](https://github.com/pusher/oauth2_proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field (@gargath)
42+- [#146](https://github.com/pusher/oauth2_proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field
23 - This change modifies the contents of the `X-Forwarded-User` header supplied by the proxy for users where the auth response from the IdP did not contain
43 - This change modifies the contents of the `X-Forwarded-User` header supplied by the proxy for users where the auth response from the IdP did not contain
24 a username.
44 a username.
25 In that case, this header used to only contain the local part of the user's email address (e.g. `john.doe` for `john.doe@example.com`) but now contains
45 In that case, this header used to only contain the local part of the user's email address (e.g. `john.doe` for `john.doe@example.com`) but now contains
@@ -31,19 +51,24 @@
31
51
32## Changes since v3.2.0
52## Changes since v3.2.0
33
53
34−- [#226](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka)
35- [#178](https://github.com/pusher/outh2_proxy/pull/178) Add Silence Ping Logging and Exclude Logging Paths flags (@kskewes)
54- [#178](https://github.com/pusher/outh2_proxy/pull/178) Add Silence Ping Logging and Exclude Logging Paths flags (@kskewes)
36- [#209](https://github.com/pusher/outh2_proxy/pull/209) Improve docker build caching of layers (@dekimsey)
55- [#209](https://github.com/pusher/outh2_proxy/pull/209) Improve docker build caching of layers (@dekimsey)
56+- [#234](https://github.com/pusher/oauth2_proxy/pull/234) Added option `-ssl-upstream-insecure-skip-validation` to skip validation of upstream SSL certificates (@jansinger)
57+- [#224](https://github.com/pusher/oauth2_proxy/pull/224) Check Google group membership using hasMember to support nested groups and external users (@jpalpant)
58+- [#231](https://github.com/pusher/oauth2_proxy/pull/231) Add optional group membership and email domain checks to the GitLab provider (@Overv)
59+- [#226](https://github.com/pusher/oauth2_proxy/pull/226) Made setting of proxied headers deterministic based on configuration alone (@aeijdenberg)
60+- [#178](https://github.com/pusher/oauth2_proxy/pull/178) Add Silence Ping Logging and Exclude Logging Paths flags (@kskewes)
61+- [#209](https://github.com/pusher/oauth2_proxy/pull/209) Improve docker build caching of layers (@dekimsey)
37- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent (@JoelSpeed)
62- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent (@JoelSpeed)
38- [#187](https://github.com/pusher/oauth2_proxy/pull/187) Move root packages to pkg folder (@JoelSpeed)
63- [#187](https://github.com/pusher/oauth2_proxy/pull/187) Move root packages to pkg folder (@JoelSpeed)
39- [#65](https://github.com/pusher/oauth2_proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
64- [#65](https://github.com/pusher/oauth2_proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
40− the `-skip-jwt-bearer-token` options.
65+ the `-skip-jwt-bearer-token` options. (@brianv0)
41 - 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
66 - 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
42 (e.g. `https://example.com/.well-known/jwks.json`).
67 (e.g. `https://example.com/.well-known/jwks.json`).
43−- [#180](https://github.com/pusher/outh2_proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
68+- [#180](https://github.com/pusher/oauth2_proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
44−- [#175](https://github.com/pusher/outh2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
69+- [#175](https://github.com/pusher/oauth2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
45 - Includes fix for potential signature checking issue when OIDC discovery is skipped.
70 - Includes fix for potential signature checking issue when OIDC discovery is skipped.
46−- [#155](https://github.com/pusher/outh2_proxy/pull/155) Add RedisSessionStore implementation (@brianv0, @JoelSpeed)
71+- [#155](https://github.com/pusher/oauth2_proxy/pull/155) Add RedisSessionStore implementation (@brianv0, @JoelSpeed)
47 - Implement flags to configure the redis session store
72 - Implement flags to configure the redis session store
48 - `-session-store-type=redis` Sets the store type to redis
73 - `-session-store-type=redis` Sets the store type to redis
49 - `-redis-connection-url` Sets the Redis connection URL
74 - `-redis-connection-url` Sets the Redis connection URL
@@ -53,10 +78,10 @@
53 - Introduces the concept of a session ticket. Tickets are composed of the cookie name, a session ID, and a secret.
78 - Introduces the concept of a session ticket. Tickets are composed of the cookie name, a session ID, and a secret.
54 - Redis Sessions are stored encrypted with a per-session secret
79 - Redis Sessions are stored encrypted with a per-session secret
55 - Added tests for server based session stores
80 - Added tests for server based session stores
56−- [#168](https://github.com/pusher/outh2_proxy/pull/168) Drop Go 1.11 support in Travis (@JoelSpeed)
81+- [#168](https://github.com/pusher/oauth2_proxy/pull/168) Drop Go 1.11 support in Travis (@JoelSpeed)
57−- [#169](https://github.com/pusher/outh2_proxy/pull/169) Update Alpine to 3.9 (@kskewes)
82+- [#169](https://github.com/pusher/oauth2_proxy/pull/169) Update Alpine to 3.9 (@kskewes)
58−- [#148](https://github.com/pusher/outh2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed)
83+- [#148](https://github.com/pusher/oauth2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed)
59−- [#147](https://github.com/pusher/outh2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed)
84+- [#147](https://github.com/pusher/oauth2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed)
60 - Allows for multiple different session storage implementations including client and server side
85 - Allows for multiple different session storage implementations including client and server side
61 - Adds tests suite for interface to ensure consistency across implementations
86 - Adds tests suite for interface to ensure consistency across implementations
62 - Refactor some configuration options (around cookies) into packages
87 - Refactor some configuration options (around cookies) into packages
@@ -78,17 +103,21 @@
78 - Implement two new flags to customize the logging format
103 - Implement two new flags to customize the logging format
79 - `-standard-logging-format` Sets the format for standard logging
104 - `-standard-logging-format` Sets the format for standard logging
80 - `-auth-logging-format` Sets the format for auth logging
105 - `-auth-logging-format` Sets the format for auth logging
81−
82- [#111](https://github.com/pusher/oauth2_proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer)
106- [#111](https://github.com/pusher/oauth2_proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer)
83- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Restore binary tarball contents to be compatible with bitlys original tarballs (@zeha)
107- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Restore binary tarball contents to be compatible with bitlys original tarballs (@zeha)
84- [#185](https://github.com/pusher/oauth2_proxy/pull/185) Fix an unsupported protocol scheme error during token validation when using the Azure provider (@jonas)
108- [#185](https://github.com/pusher/oauth2_proxy/pull/185) Fix an unsupported protocol scheme error during token validation when using the Azure provider (@jonas)
85- [#141](https://github.com/pusher/oauth2_proxy/pull/141) Check google group membership based on email address (@bchess)
109- [#141](https://github.com/pusher/oauth2_proxy/pull/141) Check google group membership based on email address (@bchess)
86 - Google Group membership is additionally checked via email address, allowing users outside a GSuite domain to be authorized.
110 - Google Group membership is additionally checked via email address, allowing users outside a GSuite domain to be authorized.
87−- [#195](https://github.com/pusher/outh2_proxy/pull/195) Add `-banner` flag for overriding the banner line that is displayed (@steakunderscore)
111+- [#195](https://github.com/pusher/oauth2_proxy/pull/195) Add `-banner` flag for overriding the banner line that is displayed (@steakunderscore)
88−- [#198](https://github.com/pusher/outh2_proxy/pull/198) Switch from gometalinter to golangci-lint (@steakunderscore)
112+- [#198](https://github.com/pusher/oauth2_proxy/pull/198) Switch from gometalinter to golangci-lint (@steakunderscore)
89−- [#159](https://github.com/pusher/oauth2_proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email`
113+- [#159](https://github.com/pusher/oauth2_proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email` (@djfinlay)
90- [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore)
114- [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore)
115+- [#201](https://github.com/pusher/oauth2_proxy/pull/201) Add Bitbucket as new OAuth2 provider, accepts email, team and repository permissions to determine authorization (@aledeganopix4d)
116+ - Implement flags to enable Bitbucket authentication:
117+ - `-bitbucket-repository` Restrict authorization to users that can access this repository
118+ - `-bitbucket-team` Restrict authorization to users that are part of this Bitbucket team
91- [#211](https://github.com/pusher/oauth2_proxy/pull/211) Switch from dep to go modules (@steakunderscore)
119- [#211](https://github.com/pusher/oauth2_proxy/pull/211) Switch from dep to go modules (@steakunderscore)
120+- [#145](https://github.com/pusher/oauth2_proxy/pull/145) Add support for OIDC UserInfo endpoint email verification (@rtluckie)
92
121
93# v3.2.0
122# v3.2.0
94
123
README.md+6−2
@@ -15,7 +15,7 @@ A list of changes can be seen in the [CHANGELOG](CHANGELOG.md).
15
15
161. Choose how to deploy:
161. Choose how to deploy:
17
17
18− a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v3.2.0`)
18+ a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v4.0.0`)
19
19
20 b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin`
20 b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin`
21
21
@@ -25,7 +25,7 @@ Prebuilt binaries can be validated by extracting the file and verifying it again
25
25
26```
26```
27sha256sum -c sha256sum.txt 2>&1 | grep OK
27sha256sum -c sha256sum.txt 2>&1 | grep OK
28−oauth2_proxy-3.2.0.linux-amd64: OK
28+oauth2_proxy-4.0.0.linux-amd64: OK
29```
29```
30
30
312. [Select a Provider and Register an OAuth Application with a Provider](https://pusher.github.io/oauth2_proxy/auth-configuration)
312. [Select a Provider and Register an OAuth Application with a Provider](https://pusher.github.io/oauth2_proxy/auth-configuration)
@@ -38,6 +38,10 @@ Read the docs on our [Docs site](https://pusher.github.io/oauth2_proxy).
38
38
39![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png)
39![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png)
40
40
41+## Getting Involved
42+
43+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/).
44+
41## Contributing
45## Contributing
42
46
43Please see our [Contributing](CONTRIBUTING.md) guidelines.
47Please see our [Contributing](CONTRIBUTING.md) guidelines.
docs/0_index.md+2−2
@@ -12,7 +12,7 @@ to validate accounts by email, domain or group.
12
12
13**Note:** This repository was forked from [bitly/OAuth2_Proxy](https://github.com/bitly/oauth2_proxy) on 27/11/2018.
13**Note:** This repository was forked from [bitly/OAuth2_Proxy](https://github.com/bitly/oauth2_proxy) on 27/11/2018.
14Versions v3.0.0 and up are from this fork and will have diverged from any changes in the original fork.
14Versions v3.0.0 and up are from this fork and will have diverged from any changes in the original fork.
15−A list of changes can be seen in the [CHANGELOG](CHANGELOG.md).
15+A list of changes can be seen in the [CHANGELOG]({{ site.gitweb }}/CHANGELOG.md).
16
16
17[![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy)
17[![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy)
18
18
@@ -20,4 +20,4 @@ A list of changes can be seen in the [CHANGELOG](CHANGELOG.md).
20
20
21## Architecture
21## Architecture
22
22
23−![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png)
23+![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png)
docs/2_auth.md+59−7
@@ -118,13 +118,15 @@ Make sure you set the following to the appropriate url:
118
118
119### GitLab Auth Provider
119### GitLab Auth Provider
120
120
121−Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](http://doc.gitlab.com/ce/integration/oauth_provider.html)
121+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.
122+
123+Restricting by group membership is possible with the following option:
124+
125+ -gitlab-group="": restrict logins to members of any of these groups (slug), separated by a comma
122
126
123If you are using self-hosted GitLab, make sure you set the following to the appropriate URL:
127If you are using self-hosted GitLab, make sure you set the following to the appropriate URL:
124
128
125− -login-url="<your gitlab url>/oauth/authorize"
126− -redeem-url="<your gitlab url>/oauth/token"
127− -validate-url="<your gitlab url>/api/v4/user"
129+ -oidc-issuer-url="<your gitlab url>"
128
130
129### LinkedIn Auth Provider
131### LinkedIn Auth Provider
130
132
@@ -139,7 +141,7 @@ For LinkedIn, the registration steps are:
139
141
140### Microsoft Azure AD Provider
142### Microsoft Azure AD Provider
141
143
142−For adding an application to the Microsoft Azure AD follow [these steps to add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/).
144+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).
143
145
144Take 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.
146Take 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.
145
147
@@ -159,6 +161,56 @@ OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many ma
159 -cookie-secure=false
161 -cookie-secure=false
160 -email-domain example.com
162 -email-domain example.com
161
163
164+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
165+the following steps:
166+
167+#### Configuring the OIDC Provider with Okta
168+
169+1. Log in to Okta using an administrative account. It is suggested you try this in preview first, `example.oktapreview.com`
170+2. (OPTIONAL) If you want to configure authorization scopes and claims to be passed on to multiple applications,
171+you may wish to configure an authorization server for each application. Otherwise, the provided `default` will work.
172+* Navigate to **Security** then select **API**
173+* Click **Add Authorization Server**, if this option is not available you may require an additional license for a custom authorization server.
174+* Fill out the **Name** with something to describe the application you are protecting. e.g. 'Example App'.
175+* For **Audience**, pick the URL of the application you wish to protect: https://example.corp.com
176+* Fill out a **Description**
177+* Add any **Access Policies** you wish to configure to limit application access.
178+* The default settings will work for other options.
179+[See Okta documentation for more information on Authorization Servers](https://developer.okta.com/docs/guides/customize-authz-server/overview/)
180+3. Navigate to **Applications** then select **Add Application**.
181+* Select **Web** for the **Platform** setting.
182+* Select **OpenID Connect** and click **Create**
183+* Pick an **Application Name** such as `Example App`.
184+* Set the **Login redirect URI** to `https://example.corp.com`.
185+* Under **General** set the **Allowed grant types** to `Authorization Code` and `Refresh Token`.
186+* Leave the rest as default, taking note of the `Client ID` and `Client Secret`.
187+* Under **Assignments** select the users or groups you wish to access your application.
188+4. Create a configuration file like the following:
189+
190+```
191+provider = "oidc"
192+redirect_url = "https://example.corp.com"
193+oidc_issuer_url = "https://corp.okta.com/oauth2/abCd1234"
194+upstreams = [
195+ "https://example.corp.com"
196+]
197+email_domains = [
198+ "corp.com"
199+]
200+client_id = "XXXXX"
201+client_secret = "YYYYY"
202+pass_access_token = true
203+cookie_secret = "ZZZZZ"
204+skip_provider_button = true
205+```
206+
207+The `oidc_issuer_url` is based on URL from your **Authorization Server**'s **Issuer** field in step 2, or simply https://corp.okta.com
208+The `client_id` and `client_secret` are configured in the application settings.
209+Generate a unique `client_secret` to encrypt the cookie.
210+
211+Then you can start the oauth2_proxy with `./oauth2_proxy -config /etc/example.cfg`
212+
213+
162### login.gov Provider
214### login.gov Provider
163
215
164login.gov is an OIDC provider for the US Government.
216login.gov is an OIDC provider for the US Government.
@@ -240,7 +292,7 @@ To authorize by email domain use `--email-domain=yourcompany.com`. To authorize
240
292
241## Adding a new Provider
293## Adding a new Provider
242
294
243−Follow the examples in the [`providers` package](providers/) to define a new
295+Follow the examples in the [`providers` package]({{ site.gitweb }}/providers/) to define a new
244`Provider` instance. Add a new `case` to
296`Provider` instance. Add a new `case` to
245−[`providers.New()`](providers/providers.go) to allow `oauth2_proxy` to use the
297+[`providers.New()`]({{ site.gitweb }}/providers/providers.go) to allow `oauth2_proxy` to use the
246new `Provider`.
298new `Provider`.
docs/4_tls.md+49−49
@@ -11,63 +11,63 @@ There are two recommended configurations.
11
11
121. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert-file=/path/to/cert.pem` and `--tls-key-file=/path/to/cert.key`.
121. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert-file=/path/to/cert.pem` and `--tls-key-file=/path/to/cert.key`.
13
13
14−The command line to run `oauth2_proxy` in this configuration would look like this:
14+ The command line to run `oauth2_proxy` in this configuration would look like this:
15
15
16−```bash
16+ ```bash
17−./oauth2_proxy \
17+ ./oauth2_proxy \
18− --email-domain="yourcompany.com" \
18+ --email-domain="yourcompany.com" \
19− --upstream=http://127.0.0.1:8080/ \
19+ --upstream=http://127.0.0.1:8080/ \
20− --tls-cert-file=/path/to/cert.pem \
20+ --tls-cert-file=/path/to/cert.pem \
21− --tls-key-file=/path/to/cert.key \
21+ --tls-key-file=/path/to/cert.key \
22− --cookie-secret=... \
22+ --cookie-secret=... \
23− --cookie-secure=true \
23+ --cookie-secure=true \
24− --provider=... \
24+ --provider=... \
25− --client-id=... \
25+ --client-id=... \
26− --client-secret=...
26+ --client-secret=...
27−```
27+ ```
28
28
292. Configure SSL Termination with [Nginx](http://nginx.org/) (example config below), Amazon ELB, Google Cloud Platform Load Balancing, or ....
292. Configure SSL Termination with [Nginx](http://nginx.org/) (example config below), Amazon ELB, Google Cloud Platform Load Balancing, or ....
30
30
31−Because `oauth2_proxy` listens on `127.0.0.1:4180` by default, to listen on all interfaces (needed when using an
31+ Because `oauth2_proxy` listens on `127.0.0.1:4180` by default, to listen on all interfaces (needed when using an
32−external load balancer like Amazon ELB or Google Platform Load Balancing) use `--http-address="0.0.0.0:4180"` or
32+ external load balancer like Amazon ELB or Google Platform Load Balancing) use `--http-address="0.0.0.0:4180"` or
33−`--http-address="http://:4180"`.
33+ `--http-address="http://:4180"`.
34
34
35−Nginx will listen on port `443` and handle SSL connections while proxying to `oauth2_proxy` on port `4180`.
35+ Nginx will listen on port `443` and handle SSL connections while proxying to `oauth2_proxy` on port `4180`.
36−`oauth2_proxy` will then authenticate requests for an upstream application. The external endpoint for this example
36+ `oauth2_proxy` will then authenticate requests for an upstream application. The external endpoint for this example
37−would be `https://internal.yourcompany.com/`.
37+ would be `https://internal.yourcompany.com/`.
38
38
39−An example Nginx config follows. Note the use of `Strict-Transport-Security` header to pin requests to SSL
39+ An example Nginx config follows. Note the use of `Strict-Transport-Security` header to pin requests to SSL
40−via [HSTS](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security):
40+ via [HSTS](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security):
41
41
42−```
42+ ```
43−server {
43+ server {
44− listen 443 default ssl;
44+ listen 443 default ssl;
45− server_name internal.yourcompany.com;
45+ server_name internal.yourcompany.com;
46− ssl_certificate /path/to/cert.pem;
46+ ssl_certificate /path/to/cert.pem;
47− ssl_certificate_key /path/to/cert.key;
47+ ssl_certificate_key /path/to/cert.key;
48− add_header Strict-Transport-Security max-age=2592000;
48+ add_header Strict-Transport-Security max-age=2592000;
49
49
50− location / {
50+ location / {
51− proxy_pass http://127.0.0.1:4180;
51+ proxy_pass http://127.0.0.1:4180;
52− proxy_set_header Host $host;
52+ proxy_set_header Host $host;
53− proxy_set_header X-Real-IP $remote_addr;
53+ proxy_set_header X-Real-IP $remote_addr;
54− proxy_set_header X-Scheme $scheme;
54+ proxy_set_header X-Scheme $scheme;
55− proxy_connect_timeout 1;
55+ proxy_connect_timeout 1;
56− proxy_send_timeout 30;
56+ proxy_send_timeout 30;
57− proxy_read_timeout 30;
57+ proxy_read_timeout 30;
58+ }
58 }
59 }
59−}
60−```
60+ ```
61
61
62−The command line to run `oauth2_proxy` in this configuration would look like this:
62+ The command line to run `oauth2_proxy` in this configuration would look like this:
63
63
64−```bash
64+ ```bash
65−./oauth2_proxy \
65+ ./oauth2_proxy \
66− --email-domain="yourcompany.com" \
66+ --email-domain="yourcompany.com" \
67− --upstream=http://127.0.0.1:8080/ \
67+ --upstream=http://127.0.0.1:8080/ \
68− --cookie-secret=... \
68+ --cookie-secret=... \
69− --cookie-secure=true \
69+ --cookie-secure=true \
70− --provider=... \
70+ --provider=... \
71− --client-id=... \
71+ --client-id=... \
72− --client-secret=...
72+ --client-secret=...
73−```
73+ ```
docs/6_request_signatures.md+1−1
@@ -11,7 +11,7 @@ If `signature_key` is defined, proxied requests will be signed with the
11`GAP-Signature` header, which is a [Hash-based Message Authentication Code
11`GAP-Signature` header, which is a [Hash-based Message Authentication Code
12(HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code)
12(HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code)
13of selected request information and the request body [see `SIGNATURE_HEADERS`
13of selected request information and the request body [see `SIGNATURE_HEADERS`
14−in `oauthproxy.go`](./oauthproxy.go).
14+in `oauthproxy.go`]({{ site.gitweb }}/oauthproxy.go).
15
15
16`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`)
16`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`)
17
17
docs/_config.yml+1
@@ -18,6 +18,7 @@ description: >- # this means to ignore newlines until "baseurl:"
18 OAuth2_Proxy documentation site
18 OAuth2_Proxy documentation site
19baseurl: "/oauth2_proxy" # the subpath of your site, e.g. /blog
19baseurl: "/oauth2_proxy" # the subpath of your site, e.g. /blog
20url: "https://pusher.github.io" # the base hostname & protocol for your site, e.g. http://example.com
20url: "https://pusher.github.io" # the base hostname & protocol for your site, e.g. http://example.com
21+gitweb: "https://github.com/pusher/oauth2_proxy/blob/master"
21
22
22# Build settings
23# Build settings
23markdown: kramdown
24markdown: kramdown
docs/configuration/configuration.md+92−91
@@ -14,100 +14,101 @@ To generate a strong cookie secret use `python -c 'import os,base64; print base6
14
14
15### Config File
15### Config File
16
16
17−An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/oauth2_proxy.cfg`
17+An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/oauth2_proxy.cfg`
18
18
19### Command Line Options
19### Command Line Options
20
20
21−```
21+| Option | Type | Description | Default |
22−Usage of oauth2_proxy:
22+| ------ | ---- | ----------- | ------- |
23− -acr-values string: optional, used by login.gov (default "http://idmanagement.gov/ns/assurance/loa/1")
23+| `-acr-values` | string | optional, used by login.gov | `"http://idmanagement.gov/ns/assurance/loa/1"` |
24− -approval-prompt string: OAuth approval_prompt (default "force")
24+| `-approval-prompt` | string | OAuth approval_prompt | `"force"` |
25− -auth-logging: Log authentication attempts (default true)
25+| `-auth-logging` | bool | Log authentication attempts | true |
26− -auth-logging-format string: Template for authentication log lines (see "Logging Configuration" paragraph below)
26+| `-auth-logging-format` | string | Template for authentication log lines | see [Logging Configuration](#logging-configuration) |
27− -authenticated-emails-file string: authenticate against emails via file (one per line)
27+| `-authenticated-emails-file` | string | authenticate against emails via file (one per line) | |
28− -azure-tenant string: go to a tenant-specific or common (tenant-independent) endpoint. (default "common")
28+| `-azure-tenant string` | string | go to a tenant-specific or common (tenant-independent) endpoint. | `"common"` |
29− -basic-auth-password string: the password to set when passing the HTTP Basic Auth header
29+| `-basic-auth-password` | string | the password to set when passing the HTTP Basic Auth header | |
30− -client-id string: the OAuth Client ID: ie: "123456.apps.googleusercontent.com"
30+| `-client-id` | string | the OAuth Client ID: ie: `"123456.apps.googleusercontent.com"` | |
31− -client-secret string: the OAuth Client Secret
31+| `-client-secret` | string | the OAuth Client Secret | |
32− -config string: path to config file
32+| `-config` | string | path to config file | |
33− -cookie-domain string: an optional cookie domain to force cookies to (ie: .yourcompany.com)
33+| `-cookie-domain` | string | an optional cookie domain to force cookies to (ie: `.yourcompany.com`) | |
34− -cookie-expire duration: expire timeframe for cookie (default 168h0m0s)
34+| `-cookie-expire` | duration | expire timeframe for cookie | 168h0m0s |
35− -cookie-httponly: set HttpOnly cookie flag (default true)
35+| `-cookie-httponly` | bool | set HttpOnly cookie flag | true |
36− -cookie-name string: the name of the cookie that the oauth_proxy creates (default "_oauth2_proxy")
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 (ie: /poc/)* (default "/")
37+| `-cookie-path` | string | an optional cookie path to force cookies to (ie: `/poc/`) | `"/"` |
38− -cookie-refresh duration: refresh the cookie after this duration; 0 to disable
38+| `-cookie-refresh` | duration | refresh the cookie after this duration; `0` to disable | |
39− -cookie-secret string: the seed string for secure cookies (optionally base64 encoded)
39+| `-cookie-secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
40− -cookie-secure: set secure (HTTPS) cookie flag (default true)
40+| `-cookie-secure` | bool | set secure (HTTPS) cookie flag | true |
41− -custom-templates-dir string: path to custom html templates
41+| `-custom-templates-dir` | string | path to custom html templates | |
42− -display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true)
42+| `-display-htpasswd-form` | bool | display username / password login form if an htpasswd file is provided | true |
43− -email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
43+| `-email-domain` | string | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | |
44− -extra-jwt-issuers: if -skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)
44+| `-extra-jwt-issuers` | string | if `-skip-jwt-bearer-tokens` is set, a list of extra JWT `issuer=audience` pairs (where the issuer URL has a `.well-known/openid-configuration` or a `.well-known/jwks.json`) | |
45− -exclude-logging-paths: comma separated list of paths to exclude from logging, eg: "/ping,/path2" (default "" = no paths excluded)
45+| `-exclude-logging-paths` | string | comma separated list of paths to exclude from logging, eg: `"/ping,/path2"` |`""` (no paths excluded) |
46− -flush-interval: period between flushing response buffers when streaming responses (default "1s")
46+| `-flush-interval` | duration | period between flushing response buffers when streaming responses | `"1s"` |
47− -banner string: custom banner string. Use "-" to disable default banner.
47+| `-banner` | string | custom banner string. Use `"-"` to disable default banner. | |
48− -footer string: custom footer string. Use "-" to disable default footer.
48+| `-footer` | string | custom footer string. Use `"-"` to disable default footer. | |
49− -gcp-healthchecks: 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 (default false)
49+| `-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 |
50− -github-org string: restrict logins to members of this organisation
50+| `-github-org` | string | restrict logins to members of this organisation | |
51− -github-team string: restrict logins to members of any of these teams (slug), separated by a comma
51+| `-github-team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
52+| `-gitlab-group` | string | restrict logins to members of any of these groups (slug), separated by a comma | |
52− -google-admin-email string: the google admin to impersonate for api calls
53+| `-google-admin-email` | string | the google admin to impersonate for api calls | |
53− -google-group value: restrict logins to members of this google group (may be given multiple times).
54+| `-google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
54− -google-service-account-json string: the path to the service account json credentials
55+| `-google-service-account-json` | string | the path to the service account json credentials | |
55− -htpasswd-file string: additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
56+| `-htpasswd-file` | string | additionally authenticate against a htpasswd file. Entries must be created with `htpasswd -s` for SHA encryption | |
56− -http-address string: [http://]<addr>:<port> or unix://<path> to listen on for HTTP clients (default "127.0.0.1:4180")
57+| `-http-address` | string | `[http://]<addr>:<port>` or `unix://<path>` to listen on for HTTP clients | `"127.0.0.1:4180"` |
57− -https-address string: <addr>:<port> to listen on for HTTPS clients (default ":443")
58+| `-https-address` | string | `<addr>:<port>` to listen on for HTTPS clients | `":443"` |
58− -logging-compress: Should rotated log files be compressed using gzip (default false)
59+| `-logging-compress` | bool | Should rotated log files be compressed using gzip | false |
59− -logging-filename string: File to log requests to, empty for stdout (default to stdout)
60+| `-logging-filename` | string | File to log requests to, empty for `stdout` | `""` (stdout) |
60− -logging-local-time: If the time in log files and backup filenames are local or UTC time (default true)
61+| `-logging-local-time` | bool | Use local time in log files and backup filenames instead of UTC | true (local time) |
61− -logging-max-age int: Maximum number of days to retain old log files (default 7)
62+| `-logging-max-age` | int | Maximum number of days to retain old log files | 7 |
62− -logging-max-backups int: Maximum number of old log files to retain; 0 to disable (default 0)
63+| `-logging-max-backups` | int | Maximum number of old log files to retain; 0 to disable | 0 |
63− -logging-max-size int: Maximum size in megabytes of the log file before rotation (default 100)
64+| `-logging-max-size` | int | Maximum size in megabytes of the log file before rotation | 100 |
64− -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
65+| `-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 | |
65− -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
66+| `-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 | |
66− -login-url string: Authentication endpoint
67+| `-login-url` | string | Authentication endpoint | |
67− -insecure-oidc-allow-unverified-email: don't fail if an email address in an id_token is not verified
68+| `-insecure-oidc-allow-unverified-email` | bool | don't fail if an email address in an id_token is not verified | false |
68− -oidc-issuer-url: the OpenID Connect issuer URL. ie: "https://accounts.google.com"
69+| `-oidc-issuer-url` | string | the OpenID Connect issuer URL. ie: `"https://accounts.google.com"` | |
69− -oidc-jwks-url string: OIDC JWKS URI for token verification; required if OIDC discovery is disabled
70+| `-oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
70− -pass-access-token: pass OAuth access_token to upstream via X-Forwarded-Access-Token header
71+| `-pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header | false |
71− -pass-authorization-header: pass OIDC IDToken to upstream via Authorization Bearer header
72+| `-pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
72− -pass-basic-auth: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream (default true)
73+| `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream | true |
73− -pass-host-header: pass the request Host Header to upstream (default true)
74+| `-pass-host-header` | bool | pass the request Host Header to upstream | true |
74− -pass-user-headers: pass X-Forwarded-User and X-Forwarded-Email information to upstream (default true)
75+| `-pass-user-headers` | bool | pass X-Forwarded-User and X-Forwarded-Email information to upstream | true |
75− -profile-url string: Profile access endpoint
76+| `-profile-url` | string | Profile access endpoint | |
76− -provider string: OAuth provider (default "google")
77+| `-provider` | string | OAuth provider | google |
77− -ping-path string: the ping endpoint that can be used for basic health checks (default "/ping")
78+| `-ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
78− -proxy-prefix string: the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in) (default "/oauth2")
79+| `-proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
79− -proxy-websockets: enables WebSocket proxying (default true)
80+| `-proxy-websockets` | bool | enables WebSocket proxying | true |
80− -pubjwk-url string: JWK pubkey access endpoint: required by login.gov
81+| `-pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | |
81− -redeem-url string: Token redemption endpoint
82+| `-redeem-url` | string | Token redemption endpoint | |
82− -redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
83+| `-redirect-url` | string | the OAuth Redirect URL. ie: `"https://internalapp.yourcompany.com/oauth2/callback"` | |
83− -redis-connection-url string: URL of redis server for redis session storage (eg: redis://HOST[:PORT])
84+| `-redis-connection-url` | string | URL of redis server for redis session storage (eg: `redis://HOST[:PORT]`) | |
84− -redis-sentinel-master-name string: Redis sentinel master name. Used in conjuction with --redis-use-sentinel
85+| `-redis-sentinel-master-name` | string | Redis sentinel master name. Used in conjunction with `--redis-use-sentinel` | |
85− -redis-sentinel-connection-urls: List of Redis sentinel conneciton URLs (eg redis://HOST[:PORT]). Used in conjuction with --redis-use-sentinel
86+| `-redis-sentinel-connection-urls` | string \| list | List of Redis sentinel connection URLs (eg `redis://HOST[:PORT]`). Used in conjunction with `--redis-use-sentinel` | |
86− -redis-use-sentinel: Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature (default: false)
87+| `-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 |
87− -request-logging: Log requests to stdout (default true)
88+| `-request-logging` | bool | Log requests | true |
88− -request-logging-format: Template for request log lines (see "Logging Configuration" paragraph below)
89+| `-request-logging-format` | string | Template for request log lines | see [Logging Configuration](#logging-configuration) |
89− -resource string: The resource that is protected (Azure AD only)
90+| `-resource` | string | The resource that is protected (Azure AD only) | |
90− -scope string: OAuth scope specification
91+| `-scope` | string | OAuth scope specification | |
91− -session-store-type: Session data storage backend (default: cookie)
92+| `-session-store-type` | string | Session data storage backend | cookie |
92− -set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)
93+| `-set-xauthrequest` | bool | set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) | false |
93− -set-authorization-header: set Authorization Bearer response header (useful in Nginx auth_request mode)
94+| `-set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
94− -signature-key string: GAP-Signature request signature key (algorithm:secretkey)
95+| `-signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
95− -silence-ping-logging bool: disable logging of requests to ping endpoint (default false)
96+| `-silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
96− -skip-auth-preflight: will skip authentication for OPTIONS requests
97+| `-skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
97− -skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)
98+| `-skip-auth-regex` | string | bypass authentication for requests paths that match (may be given multiple times) | |
98− -skip-jwt-bearer-tokens: will skip requests that have verified JWT bearer tokens
99+| `-skip-jwt-bearer-tokens` | bool | will skip requests that have verified JWT bearer tokens | false |
99− -skip-oidc-discovery: bypass OIDC endpoint discovery. login-url, redeem-url and oidc-jwks-url must be configured in this case
100+| `-skip-oidc-discovery` | bool | bypass OIDC endpoint discovery. `-login-url`, `-redeem-url` and `-oidc-jwks-url` must be configured in this case | false |
100− -skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start
101+| `-skip-provider-button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
101− -ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS
102+| `-ssl-insecure-skip-verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
103+| `-ssl-upstream-insecure-skip-verify` | bool | skip validation of certificates presented when using HTTPS upstreams | false |
102− -standard-logging: Log standard runtime information (default true)
104+| `-standard-logging` | bool | Log standard runtime information | true |
103− -standard-logging-format string: Template for standard log lines (see "Logging Configuration" paragraph below)
105+| `-standard-logging-format` | string | Template for standard log lines | see [Logging Configuration](#logging-configuration) |
104− -tls-cert-file string: path to certificate file
106+| `-tls-cert-file` | string | path to certificate file | |
105− -tls-key-file string: path to private key file
107+| `-tls-key-file` | string | path to private key file | |
106− -upstream value: the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path
108+| `-upstream` | string \| list | the http url(s) of the upstream endpoint or `file://` paths for static files. Routing is based on the path | |
107− -validate-url string: Access token validation endpoint
109+| `-validate-url` | string | Access token validation endpoint | |
108− -version: print version string
110+| `-version` | n/a | print version string | |
109− -whitelist-domain: allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)
111+| `-whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | |
110−```
111
112
112Note, when using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL.
113Note, when using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL.
113
114
docs/configuration/sessions.md+2−2
@@ -15,8 +15,8 @@ The OAuth2 Proxy uses a Cookie to track user sessions and will store the session
15data in one of the available session storage backends.
15data in one of the available session storage backends.
16
16
17At present the available backends are (as passed to `--session-store-type`):
17At present the available backends are (as passed to `--session-store-type`):
18−- [cookie](cookie-storage) (default)
18+- [cookie](#cookie-storage) (default)
19−- [redis](redis-storage)
19+- [redis](#redis-storage)
20
20
21### Cookie Storage
21### Cookie Storage
22
22
main.go+7−3
@@ -47,7 +47,8 @@ func main() {
47 flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
47 flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
48 flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
48 flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
49 flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
49 flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
50− flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS")
50+ flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")
51+ flagSet.Bool("ssl-upstream-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS upstreams")
51 flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
52 flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
52 flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
53 flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
53 flagSet.Var(&jwtIssuers, "extra-jwt-issuers", "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
54 flagSet.Var(&jwtIssuers, "extra-jwt-issuers", "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
@@ -56,8 +57,11 @@ func main() {
56 flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
57 flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
57 flagSet.String("keycloak-group", "", "restrict login to members of this group.")
58 flagSet.String("keycloak-group", "", "restrict login to members of this group.")
58 flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
59 flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
60+ flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
61+ flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
59 flagSet.String("github-org", "", "restrict logins to members of this organisation")
62 flagSet.String("github-org", "", "restrict logins to members of this organisation")
60 flagSet.String("github-team", "", "restrict logins to members of this team")
63 flagSet.String("github-team", "", "restrict logins to members of this team")
64+ flagSet.String("gitlab-group", "", "restrict logins to members of this group")
61 flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).")
65 flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).")
62 flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
66 flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
63 flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
67 flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
@@ -85,8 +89,8 @@ func main() {
85 flagSet.String("session-store-type", "cookie", "the session storage provider to use")
89 flagSet.String("session-store-type", "cookie", "the session storage provider to use")
86 flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
90 flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
87 flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature")
91 flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature")
88− flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjuction with --redis-use-sentinel")
92+ flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel")
89− flagSet.Var(&redisSentinelConnectionURLs, "redis-sentinel-connection-urls", "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjuction with --redis-use-sentinel")
93+ flagSet.Var(&redisSentinelConnectionURLs, "redis-sentinel-connection-urls", "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel")
90
94
91 flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
95 flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
92 flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation")
96 flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation")
oauthproxy.go+46−12
@@ -2,6 +2,7 @@ package main
2
2
3import (
3import (
4 "context"
4 "context"
5+ "crypto/tls"
5 b64 "encoding/base64"
6 b64 "encoding/base64"
6 "errors"
7 "errors"
7 "fmt"
8 "fmt"
@@ -128,9 +129,14 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
128
129
129// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
130// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
130// servers
131// servers
131−func NewReverseProxy(target *url.URL, flushInterval time.Duration) (proxy *httputil.ReverseProxy) {
132+func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) {
132 proxy = httputil.NewSingleHostReverseProxy(target)
133 proxy = httputil.NewSingleHostReverseProxy(target)
133− proxy.FlushInterval = flushInterval
134+ proxy.FlushInterval = opts.FlushInterval
135+ if opts.SSLUpstreamInsecureSkipVerify {
136+ proxy.Transport = &http.Transport{
137+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
138+ }
139+ }
134 return proxy
140 return proxy
135}
141}
136
142
@@ -163,7 +169,7 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
163// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
169// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
164func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
170func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
165 u.Path = ""
171 u.Path = ""
166− proxy := NewReverseProxy(u, opts.FlushInterval)
172+ proxy := NewReverseProxy(u, opts)
167 if !opts.PassHostHeader {
173 if !opts.PassHostHeader {
168 setProxyUpstreamHostHeader(proxy, u)
174 setProxyUpstreamHostHeader(proxy, u)
169 } else {
175 } else {
@@ -814,32 +820,60 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
814 req.Header["X-Forwarded-User"] = []string{session.User}
820 req.Header["X-Forwarded-User"] = []string{session.User}
815 if session.Email != "" {
821 if session.Email != "" {
816 req.Header["X-Forwarded-Email"] = []string{session.Email}
822 req.Header["X-Forwarded-Email"] = []string{session.Email}
823+ } else {
824+ req.Header.Del("X-Forwarded-Email")
817 }
825 }
818 }
826 }
827+
819 if p.PassUserHeaders {
828 if p.PassUserHeaders {
820 req.Header["X-Forwarded-User"] = []string{session.User}
829 req.Header["X-Forwarded-User"] = []string{session.User}
821 if session.Email != "" {
830 if session.Email != "" {
822 req.Header["X-Forwarded-Email"] = []string{session.Email}
831 req.Header["X-Forwarded-Email"] = []string{session.Email}
832+ } else {
833+ req.Header.Del("X-Forwarded-Email")
823 }
834 }
824 }
835 }
836+
825 if p.SetXAuthRequest {
837 if p.SetXAuthRequest {
826 rw.Header().Set("X-Auth-Request-User", session.User)
838 rw.Header().Set("X-Auth-Request-User", session.User)
827 if session.Email != "" {
839 if session.Email != "" {
828 rw.Header().Set("X-Auth-Request-Email", session.Email)
840 rw.Header().Set("X-Auth-Request-Email", session.Email)
841+ } else {
842+ rw.Header().Del("X-Auth-Request-Email")
829 }
843 }
844+
830− if p.PassAccessToken && session.AccessToken != "" {
845+ if p.PassAccessToken {
846+ if session.AccessToken != "" {
831− rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
847+ rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
848+ } else {
849+ rw.Header().Del("X-Auth-Request-Access-Token")
850+ }
832 }
851 }
833 }
852 }
853+
834− if p.PassAccessToken && session.AccessToken != "" {
854+ if p.PassAccessToken {
855+ if session.AccessToken != "" {
835− req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
856+ req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
857+ } else {
858+ req.Header.Del("X-Forwarded-Access-Token")
859+ }
836 }
860 }
861+
837− if p.PassAuthorization && session.IDToken != "" {
862+ if p.PassAuthorization {
863+ if session.IDToken != "" {
838− req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
864+ req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
865+ } else {
866+ req.Header.Del("Authorization")
867+ }
839 }
868 }
840− if p.SetAuthorization && session.IDToken != "" {
869+ if p.SetAuthorization {
870+ if session.IDToken != "" {
841− rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
871+ rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
872+ } else {
873+ rw.Header().Del("Authorization")
874+ }
842 }
875 }
876+
843 if session.Email == "" {
877 if session.Email == "" {
844 rw.Header().Set("GAP-Auth", session.User)
878 rw.Header().Set("GAP-Auth", session.User)
845 } else {
879 } else {
@@ -892,7 +926,7 @@ func isAjax(req *http.Request) bool {
892 return false
926 return false
893}
927}
894
928
895−// ErrorJSON returns the error code witht an application/json mime type
929+// ErrorJSON returns the error code with an application/json mime type
896func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
930func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
897 rw.Header().Set("Content-Type", applicationJSON)
931 rw.Header().Set("Content-Type", applicationJSON)
898 rw.WriteHeader(code)
932 rw.WriteHeader(code)
oauthproxy_test.go+2−2
@@ -122,7 +122,7 @@ func TestNewReverseProxy(t *testing.T) {
122 backendHost := net.JoinHostPort(backendHostname, backendPort)
122 backendHost := net.JoinHostPort(backendHostname, backendPort)
123 proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
123 proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
124
124
125− proxyHandler := NewReverseProxy(proxyURL, time.Second)
125+ proxyHandler := NewReverseProxy(proxyURL, &Options{FlushInterval: time.Second})
126 setProxyUpstreamHostHeader(proxyHandler, proxyURL)
126 setProxyUpstreamHostHeader(proxyHandler, proxyURL)
127 frontend := httptest.NewServer(proxyHandler)
127 frontend := httptest.NewServer(proxyHandler)
128 defer frontend.Close()
128 defer frontend.Close()
@@ -144,7 +144,7 @@ func TestEncodedSlashes(t *testing.T) {
144 defer backend.Close()
144 defer backend.Close()
145
145
146 b, _ := url.Parse(backend.URL)
146 b, _ := url.Parse(backend.URL)
147− proxyHandler := NewReverseProxy(b, time.Second)
147+ proxyHandler := NewReverseProxy(b, &Options{FlushInterval: time.Second})
148 setProxyDirector(proxyHandler)
148 setProxyDirector(proxyHandler)
149 frontend := httptest.NewServer(proxyHandler)
149 frontend := httptest.NewServer(proxyHandler)
150 defer frontend.Close()
150 defer frontend.Close()
options.go+46−16
@@ -43,10 +43,13 @@ type Options struct {
43 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
43 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
44 KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"`
44 KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"`
45 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
45 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
46+ BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"`
47+ BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"`
46 EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"`
48 EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"`
47 WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
49 WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
48 GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"`
50 GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"`
49 GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"`
51 GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"`
52+ GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group" env:"OAUTH2_PROXY_GITLAB_GROUP"`
50 GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"`
53 GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"`
51 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"`
54 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"`
52 GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"`
55 GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"`
@@ -62,22 +65,23 @@ type Options struct {
62 // Embed SessionOptions
65 // Embed SessionOptions
63 options.SessionOptions
66 options.SessionOptions
64
67
65− Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
68+ Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
66− SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
69+ SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
67− SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"`
70+ SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"`
68− ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"`
71+ ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"`
69− PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
72+ PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
70− BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
73+ BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
71− PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
74+ PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
72− PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"`
75+ PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"`
73− SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"`
76+ SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"`
74− PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"`
77+ PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"`
75− SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY"`
78+ SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY"`
79+ SSLUpstreamInsecureSkipVerify bool `flag:"ssl-upstream-insecure-skip-verify" cfg:"ssl_upstream_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_UPSTREAM_INSECURE_SKIP_VERIFY"`
76− SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest" env:"OAUTH2_PROXY_SET_XAUTHREQUEST"`
80+ SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest" env:"OAUTH2_PROXY_SET_XAUTHREQUEST"`
77− SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header" env:"OAUTH2_PROXY_SET_AUTHORIZATION_HEADER"`
81+ SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header" env:"OAUTH2_PROXY_SET_AUTHORIZATION_HEADER"`
78− PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header" env:"OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER"`
82+ PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header" env:"OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER"`
79− SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight" env:"OAUTH2_PROXY_SKIP_AUTH_PREFLIGHT"`
83+ SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight" env:"OAUTH2_PROXY_SKIP_AUTH_PREFLIGHT"`
80− FlushInterval time.Duration `flag:"flush-interval" cfg:"flush_interval" env:"OAUTH2_PROXY_FLUSH_INTERVAL"`
84+ FlushInterval time.Duration `flag:"flush-interval" cfg:"flush_interval" env:"OAUTH2_PROXY_FLUSH_INTERVAL"`
81
85
82 // These options allow for other providers besides Google, with
86 // These options allow for other providers besides Google, with
83 // potential overrides.
87 // potential overrides.
@@ -406,6 +410,9 @@ func parseProviderInfo(o *Options, msgs []string) []string {
406 p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
410 p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
407 }
411 }
408 }
412 }
413+ case *providers.BitbucketProvider:
414+ p.SetTeam(o.BitbucketTeam)
415+ p.SetRepository(o.BitbucketRepository)
409 case *providers.OIDCProvider:
416 case *providers.OIDCProvider:
410 p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
417 p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
411 if o.oidcVerifier == nil {
418 if o.oidcVerifier == nil {
@@ -413,6 +420,29 @@ func parseProviderInfo(o *Options, msgs []string) []string {
413 } else {
420 } else {
414 p.Verifier = o.oidcVerifier
421 p.Verifier = o.oidcVerifier
415 }
422 }
423+ case *providers.GitLabProvider:
424+ p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
425+ p.Group = o.GitLabGroup
426+ p.EmailDomains = o.EmailDomains
427+
428+ if o.oidcVerifier != nil {
429+ p.Verifier = o.oidcVerifier
430+ } else {
431+ // Initialize with default verifier for gitlab.com
432+ ctx := context.Background()
433+
434+ provider, err := oidc.NewProvider(ctx, "https://gitlab.com")
435+ if err != nil {
436+ msgs = append(msgs, "failed to initialize oidc provider for gitlab.com")
437+ } else {
438+ p.Verifier = provider.Verifier(&oidc.Config{
439+ ClientID: o.ClientID,
440+ })
441+
442+ p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
443+ p.RedeemURL, msgs = parseURL(provider.Endpoint().TokenURL, "redeem", msgs)
444+ }
445+ }
416 case *providers.LoginGovProvider:
446 case *providers.LoginGovProvider:
417 p.AcrValues = o.AcrValues
447 p.AcrValues = o.AcrValues
418 p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
448 p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
providers/bitbucket.gonew file+163
@@ -0,0 +1,163 @@
1+package providers
2+
3+import (
4+ "net/http"
5+ "net/url"
6+ "strings"
7+
8+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
9+ "github.com/pusher/oauth2_proxy/pkg/logger"
10+ "github.com/pusher/oauth2_proxy/pkg/requests"
11+)
12+
13+// BitbucketProvider represents an Bitbucket based Identity Provider
14+type BitbucketProvider struct {
15+ *ProviderData
16+ Team string
17+ Repository string
18+}
19+
20+// NewBitbucketProvider initiates a new BitbucketProvider
21+func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
22+ p.ProviderName = "Bitbucket"
23+ if p.LoginURL == nil || p.LoginURL.String() == "" {
24+ p.LoginURL = &url.URL{
25+ Scheme: "https",
26+ Host: "bitbucket.org",
27+ Path: "/site/oauth2/authorize",
28+ }
29+ }
30+ if p.RedeemURL == nil || p.RedeemURL.String() == "" {
31+ p.RedeemURL = &url.URL{
32+ Scheme: "https",
33+ Host: "bitbucket.org",
34+ Path: "/site/oauth2/access_token",
35+ }
36+ }
37+ if p.ValidateURL == nil || p.ValidateURL.String() == "" {
38+ p.ValidateURL = &url.URL{
39+ Scheme: "https",
40+ Host: "api.bitbucket.org",
41+ Path: "/2.0/user/emails",
42+ }
43+ }
44+ if p.Scope == "" {
45+ p.Scope = "email"
46+ }
47+ return &BitbucketProvider{ProviderData: p}
48+}
49+
50+// SetTeam defines the Bitbucket team the user must be part of
51+func (p *BitbucketProvider) SetTeam(team string) {
52+ p.Team = team
53+ if !strings.Contains(p.Scope, "team") {
54+ p.Scope += " team"
55+ }
56+}
57+
58+// SetRepository defines the repository the user must have access to
59+func (p *BitbucketProvider) SetRepository(repository string) {
60+ p.Repository = repository
61+ if !strings.Contains(p.Scope, "repository") {
62+ p.Scope += " repository"
63+ }
64+}
65+
66+// GetEmailAddress returns the email of the authenticated user
67+func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
68+
69+ var emails struct {
70+ Values []struct {
71+ Email string `json:"email"`
72+ Primary bool `json:"is_primary"`
73+ }
74+ }
75+ var teams struct {
76+ Values []struct {
77+ Name string `json:"username"`
78+ }
79+ }
80+ var repositories struct {
81+ Values []struct {
82+ FullName string `json:"full_name"`
83+ }
84+ }
85+ req, err := http.NewRequest("GET",
86+ p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
87+ if err != nil {
88+ logger.Printf("failed building request %s", err)
89+ return "", err
90+ }
91+ err = requests.RequestJSON(req, &emails)
92+ if err != nil {
93+ logger.Printf("failed making request %s", err)
94+ return "", err
95+ }
96+
97+ if p.Team != "" {
98+ teamURL := &url.URL{}
99+ *teamURL = *p.ValidateURL
100+ teamURL.Path = "/2.0/teams"
101+ req, err = http.NewRequest("GET",
102+ teamURL.String()+"?role=member&access_token="+s.AccessToken, nil)
103+ if err != nil {
104+ logger.Printf("failed building request %s", err)
105+ return "", err
106+ }
107+ err = requests.RequestJSON(req, &teams)
108+ if err != nil {
109+ logger.Printf("failed requesting teams membership %s", err)
110+ return "", err
111+ }
112+ var found = false
113+ for _, team := range teams.Values {
114+ if p.Team == team.Name {
115+ found = true
116+ break
117+ }
118+ }
119+ if found != true {
120+ logger.Print("team membership test failed, access denied")
121+ return "", nil
122+ }
123+ }
124+
125+ if p.Repository != "" {
126+ repositoriesURL := &url.URL{}
127+ *repositoriesURL = *p.ValidateURL
128+ repositoriesURL.Path = "/2.0/repositories/" + strings.Split(p.Repository, "/")[0]
129+ req, err = http.NewRequest("GET",
130+ repositoriesURL.String()+"?role=contributor"+
131+ "&q=full_name="+url.QueryEscape("\""+p.Repository+"\"")+
132+ "&access_token="+s.AccessToken,
133+ nil)
134+ if err != nil {
135+ logger.Printf("failed building request %s", err)
136+ return "", err
137+ }
138+ err = requests.RequestJSON(req, &repositories)
139+ if err != nil {
140+ logger.Printf("failed checking repository access %s", err)
141+ return "", err
142+ }
143+ var found = false
144+ for _, repository := range repositories.Values {
145+ if p.Repository == repository.FullName {
146+ found = true
147+ break
148+ }
149+ }
150+ if found != true {
151+ logger.Print("repository access test failed, access denied")
152+ return "", nil
153+ }
154+ }
155+
156+ for _, email := range emails.Values {
157+ if email.Primary {
158+ return email.Email, nil
159+ }
160+ }
161+
162+ return "", nil
163+}
providers/bitbucket_test.gonew file+170
@@ -0,0 +1,170 @@
1+package providers
2+
3+import (
4+ "log"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "testing"
9+
10+ "github.com/stretchr/testify/assert"
11+
12+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
13+)
14+
15+func testBitbucketProvider(hostname, team string, repository string) *BitbucketProvider {
16+ p := NewBitbucketProvider(
17+ &ProviderData{
18+ ProviderName: "",
19+ LoginURL: &url.URL{},
20+ RedeemURL: &url.URL{},
21+ ProfileURL: &url.URL{},
22+ ValidateURL: &url.URL{},
23+ Scope: ""})
24+
25+ if team != "" {
26+ p.SetTeam(team)
27+ }
28+
29+ if repository != "" {
30+ p.SetRepository(repository)
31+ }
32+
33+ if hostname != "" {
34+ updateURL(p.Data().LoginURL, hostname)
35+ updateURL(p.Data().RedeemURL, hostname)
36+ updateURL(p.Data().ProfileURL, hostname)
37+ updateURL(p.Data().ValidateURL, hostname)
38+ }
39+ return p
40+}
41+
42+func testBitbucketBackend(payload string) *httptest.Server {
43+ paths := map[string]bool{
44+ "/2.0/user/emails": true,
45+ "/2.0/teams": true,
46+ }
47+
48+ return httptest.NewServer(http.HandlerFunc(
49+ func(w http.ResponseWriter, r *http.Request) {
50+ url := r.URL
51+ if !paths[url.Path] {
52+ log.Printf("%s not in %+v\n", url.Path, paths)
53+ w.WriteHeader(404)
54+ } else if r.URL.Query().Get("access_token") != "imaginary_access_token" {
55+ w.WriteHeader(403)
56+ } else {
57+ w.WriteHeader(200)
58+ w.Write([]byte(payload))
59+ }
60+ }))
61+}
62+
63+func TestBitbucketProviderDefaults(t *testing.T) {
64+ p := testBitbucketProvider("", "", "")
65+ assert.NotEqual(t, nil, p)
66+ assert.Equal(t, "Bitbucket", p.Data().ProviderName)
67+ assert.Equal(t, "https://bitbucket.org/site/oauth2/authorize",
68+ p.Data().LoginURL.String())
69+ assert.Equal(t, "https://bitbucket.org/site/oauth2/access_token",
70+ p.Data().RedeemURL.String())
71+ assert.Equal(t, "https://api.bitbucket.org/2.0/user/emails",
72+ p.Data().ValidateURL.String())
73+ assert.Equal(t, "email", p.Data().Scope)
74+}
75+
76+func TestBitbucketProviderScopeAdjustForTeam(t *testing.T) {
77+ p := testBitbucketProvider("", "test-team", "")
78+ assert.NotEqual(t, nil, p)
79+ assert.Equal(t, "email team", p.Data().Scope)
80+}
81+
82+func TestBitbucketProviderScopeAdjustForRepository(t *testing.T) {
83+ p := testBitbucketProvider("", "", "rest-repo")
84+ assert.NotEqual(t, nil, p)
85+ assert.Equal(t, "email repository", p.Data().Scope)
86+}
87+
88+func TestBitbucketProviderOverrides(t *testing.T) {
89+ p := NewBitbucketProvider(
90+ &ProviderData{
91+ LoginURL: &url.URL{
92+ Scheme: "https",
93+ Host: "example.com",
94+ Path: "/oauth/auth"},
95+ RedeemURL: &url.URL{
96+ Scheme: "https",
97+ Host: "example.com",
98+ Path: "/oauth/token"},
99+ ValidateURL: &url.URL{
100+ Scheme: "https",
101+ Host: "example.com",
102+ Path: "/api/v3/user"},
103+ Scope: "profile"})
104+ assert.NotEqual(t, nil, p)
105+ assert.Equal(t, "Bitbucket", p.Data().ProviderName)
106+ assert.Equal(t, "https://example.com/oauth/auth",
107+ p.Data().LoginURL.String())
108+ assert.Equal(t, "https://example.com/oauth/token",
109+ p.Data().RedeemURL.String())
110+ assert.Equal(t, "https://example.com/api/v3/user",
111+ p.Data().ValidateURL.String())
112+ assert.Equal(t, "profile", p.Data().Scope)
113+}
114+
115+func TestBitbucketProviderGetEmailAddress(t *testing.T) {
116+ b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true } ] }")
117+ defer b.Close()
118+
119+ bURL, _ := url.Parse(b.URL)
120+ p := testBitbucketProvider(bURL.Host, "", "")
121+
122+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
123+ email, err := p.GetEmailAddress(session)
124+ assert.Equal(t, nil, err)
125+ assert.Equal(t, "michael.bland@gsa.gov", email)
126+}
127+
128+func TestBitbucketProviderGetEmailAddressAndGroup(t *testing.T) {
129+ b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true, \"username\": \"bioinformatics\" } ] }")
130+ defer b.Close()
131+
132+ bURL, _ := url.Parse(b.URL)
133+ p := testBitbucketProvider(bURL.Host, "bioinformatics", "")
134+
135+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
136+ email, err := p.GetEmailAddress(session)
137+ assert.Equal(t, nil, err)
138+ assert.Equal(t, "michael.bland@gsa.gov", email)
139+}
140+
141+// Note that trying to trigger the "failed building request" case is not
142+// practical, since the only way it can fail is if the URL fails to parse.
143+func TestBitbucketProviderGetEmailAddressFailedRequest(t *testing.T) {
144+ b := testBitbucketBackend("unused payload")
145+ defer b.Close()
146+
147+ bURL, _ := url.Parse(b.URL)
148+ p := testBitbucketProvider(bURL.Host, "", "")
149+
150+ // We'll trigger a request failure by using an unexpected access
151+ // token. Alternatively, we could allow the parsing of the payload as
152+ // JSON to fail.
153+ session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
154+ email, err := p.GetEmailAddress(session)
155+ assert.NotEqual(t, nil, err)
156+ assert.Equal(t, "", email)
157+}
158+
159+func TestBitbucketProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
160+ b := testBitbucketBackend("{\"foo\": \"bar\"}")
161+ defer b.Close()
162+
163+ bURL, _ := url.Parse(b.URL)
164+ p := testBitbucketProvider(bURL.Host, "", "")
165+
166+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
167+ email, err := p.GetEmailAddress(session)
168+ assert.Equal(t, "", email)
169+ assert.Equal(t, nil, err)
170+}
providers/gitlab.go+227−31
@@ -1,62 +1,258 @@
1package providers
1package providers
2
2
3import (
3import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "io/ioutil"
4 "net/http"
8 "net/http"
5− "net/url"
9+ "strings"
10+ "time"
6
11
12+ oidc "github.com/coreos/go-oidc"
7 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
13 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
8− "github.com/pusher/oauth2_proxy/pkg/logger"
14+ "golang.org/x/oauth2"
9− "github.com/pusher/oauth2_proxy/pkg/requests"
10)
15)
11
16
12−// GitLabProvider represents an GitLab based Identity Provider
17+// GitLabProvider represents a GitLab based Identity Provider
13type GitLabProvider struct {
18type GitLabProvider struct {
14 *ProviderData
19 *ProviderData
20+
21+ Group string
22+ EmailDomains []string
23+
24+ Verifier *oidc.IDTokenVerifier
25+ AllowUnverifiedEmail bool
15}
26}
16
27
17// NewGitLabProvider initiates a new GitLabProvider
28// NewGitLabProvider initiates a new GitLabProvider
18func NewGitLabProvider(p *ProviderData) *GitLabProvider {
29func NewGitLabProvider(p *ProviderData) *GitLabProvider {
19 p.ProviderName = "GitLab"
30 p.ProviderName = "GitLab"
20− if p.LoginURL == nil || p.LoginURL.String() == "" {
31+
21− p.LoginURL = &url.URL{
32+ if p.Scope == "" {
22− Scheme: "https",
33+ p.Scope = "openid email"
23− Host: "gitlab.com",
34+ }
24− Path: "/oauth/authorize",
35+
36+ return &GitLabProvider{ProviderData: p}
37+}
38+
39+// Redeem exchanges the OAuth2 authentication token for an ID token
40+func (p *GitLabProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
41+ ctx := context.Background()
42+ c := oauth2.Config{
43+ ClientID: p.ClientID,
44+ ClientSecret: p.ClientSecret,
45+ Endpoint: oauth2.Endpoint{
46+ TokenURL: p.RedeemURL.String(),
47+ },
48+ RedirectURL: redirectURL,
49+ }
50+ token, err := c.Exchange(ctx, code)
51+ if err != nil {
52+ return nil, fmt.Errorf("token exchange: %v", err)
53+ }
54+ s, err = p.createSessionState(ctx, token)
55+ if err != nil {
56+ return nil, fmt.Errorf("unable to update session: %v", err)
57+ }
58+ return
59+}
60+
61+// RefreshSessionIfNeeded checks if the session has expired and uses the
62+// RefreshToken to fetch a new ID token if required
63+func (p *GitLabProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
64+ if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
65+ return false, nil
66+ }
67+
68+ origExpiration := s.ExpiresOn
69+
70+ err := p.redeemRefreshToken(s)
71+ if err != nil {
72+ return false, fmt.Errorf("unable to redeem refresh token: %v", err)
73+ }
74+
75+ fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
76+ return true, nil
77+}
78+
79+func (p *GitLabProvider) redeemRefreshToken(s *sessions.SessionState) (err error) {
80+ c := oauth2.Config{
81+ ClientID: p.ClientID,
82+ ClientSecret: p.ClientSecret,
83+ Endpoint: oauth2.Endpoint{
84+ TokenURL: p.RedeemURL.String(),
85+ },
86+ }
87+ ctx := context.Background()
88+ t := &oauth2.Token{
89+ RefreshToken: s.RefreshToken,
90+ Expiry: time.Now().Add(-time.Hour),
91+ }
92+ token, err := c.TokenSource(ctx, t).Token()
93+ if err != nil {
94+ return fmt.Errorf("failed to get token: %v", err)
95+ }
96+ newSession, err := p.createSessionState(ctx, token)
97+ if err != nil {
98+ return fmt.Errorf("unable to update session: %v", err)
99+ }
100+ s.AccessToken = newSession.AccessToken
101+ s.IDToken = newSession.IDToken
102+ s.RefreshToken = newSession.RefreshToken
103+ s.CreatedAt = newSession.CreatedAt
104+ s.ExpiresOn = newSession.ExpiresOn
105+ s.Email = newSession.Email
106+ return
107+}
108+
109+type gitlabUserInfo struct {
110+ Username string `json:"nickname"`
111+ Email string `json:"email"`
112+ EmailVerified bool `json:"email_verified"`
113+ Groups []string `json:"groups"`
114+}
115+
116+func (p *GitLabProvider) getUserInfo(s *sessions.SessionState) (*gitlabUserInfo, error) {
117+ // Retrieve user info JSON
118+ // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
119+
120+ // Build user info url from login url of GitLab instance
121+ userInfoURL := *p.LoginURL
122+ userInfoURL.Path = "/oauth/userinfo"
123+
124+ req, err := http.NewRequest("GET", userInfoURL.String(), nil)
125+ if err != nil {
126+ return nil, fmt.Errorf("failed to create user info request: %v", err)
127+ }
128+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
129+
130+ resp, err := http.DefaultClient.Do(req)
131+ if err != nil {
132+ return nil, fmt.Errorf("failed to perform user info request: %v", err)
25− }
133+ }
134+ var body []byte
135+ body, err = ioutil.ReadAll(resp.Body)
136+ resp.Body.Close()
137+ if err != nil {
138+ return nil, fmt.Errorf("failed to read user info response: %v", err)
26 }
139 }
27− if p.RedeemURL == nil || p.RedeemURL.String() == "" {
140+
28− p.RedeemURL = &url.URL{
141+ if resp.StatusCode != 200 {
29− Scheme: "https",
142+ return nil, fmt.Errorf("got %d during user info request: %s", resp.StatusCode, body)
30− Host: "gitlab.com",
143+ }
31− Path: "/oauth/token",
144+
145+ var userInfo gitlabUserInfo
146+ err = json.Unmarshal(body, &userInfo)
147+ if err != nil {
148+ return nil, fmt.Errorf("failed to parse user info: %v", err)
149+ }
150+
151+ return &userInfo, nil
152+}
153+
154+func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error {
155+ if p.Group == "" {
156+ return nil
157+ }
158+
159+ // Collect user group memberships
160+ membershipSet := make(map[string]bool)
161+ for _, group := range userInfo.Groups {
162+ membershipSet[group] = true
163+ }
164+
165+ // Find a valid group that they are a member of
166+ validGroups := strings.Split(p.Group, " ")
167+ for _, validGroup := range validGroups {
168+ if _, ok := membershipSet[validGroup]; ok {
169+ return nil
32 }
170 }
33 }
171 }
34− if p.ValidateURL == nil || p.ValidateURL.String() == "" {
172+
35− p.ValidateURL = &url.URL{
173+ return fmt.Errorf("user is not a member of '%s'", p.Group)
36− Scheme: "https",
174+}
37− Host: "gitlab.com",
175+
38− Path: "/api/v4/user",
176+func (p *GitLabProvider) verifyEmailDomain(userInfo *gitlabUserInfo) error {
177+ if len(p.EmailDomains) == 0 || p.EmailDomains[0] == "*" {
178+ return nil
179+ }
180+
181+ for _, domain := range p.EmailDomains {
182+ if strings.HasSuffix(userInfo.Email, domain) {
183+ return nil
39 }
184 }
40 }
185 }
41− if p.Scope == "" {
186+
42− p.Scope = "read_user"
187+ return fmt.Errorf("user email is not one of the valid domains '%v'", p.EmailDomains)
188+}
189+
190+func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
191+ rawIDToken, ok := token.Extra("id_token").(string)
192+ if !ok {
193+ return nil, fmt.Errorf("token response did not contain an id_token")
43 }
194 }
44− return &GitLabProvider{ProviderData: p}
195+
196+ // Parse and verify ID Token payload.
197+ idToken, err := p.Verifier.Verify(ctx, rawIDToken)
198+ if err != nil {
199+ return nil, fmt.Errorf("could not verify id_token: %v", err)
200+ }
201+
202+ return &sessions.SessionState{
203+ AccessToken: token.AccessToken,
204+ IDToken: rawIDToken,
205+ RefreshToken: token.RefreshToken,
206+ CreatedAt: time.Now(),
207+ ExpiresOn: idToken.Expiry,
208+ }, nil
209+}
210+
211+// ValidateSessionState checks that the session's IDToken is still valid
212+func (p *GitLabProvider) ValidateSessionState(s *sessions.SessionState) bool {
213+ ctx := context.Background()
214+ _, err := p.Verifier.Verify(ctx, s.IDToken)
215+ if err != nil {
216+ return false
217+ }
218+
219+ return true
45}
220}
46
221
47// GetEmailAddress returns the Account email address
222// GetEmailAddress returns the Account email address
48func (p *GitLabProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
223func (p *GitLabProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
224+ // Retrieve user info
225+ userInfo, err := p.getUserInfo(s)
226+ if err != nil {
227+ return "", fmt.Errorf("failed to retrieve user info: %v", err)
228+ }
229+
230+ // Check if email is verified
231+ if !p.AllowUnverifiedEmail && !userInfo.EmailVerified {
232+ return "", fmt.Errorf("user email is not verified")
233+ }
234+
235+ // Check if email has valid domain
236+ err = p.verifyEmailDomain(userInfo)
237+ if err != nil {
238+ return "", fmt.Errorf("email domain check failed: %v", err)
239+ }
49
240
50− req, err := http.NewRequest("GET",
241+ // Check group membership
51− p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
242+ err = p.verifyGroupMembership(userInfo)
52 if err != nil {
243 if err != nil {
53− logger.Printf("failed building request %s", err)
244+ return "", fmt.Errorf("group membership check failed: %v", err)
54− return "", err
55 }
245 }
56− json, err := requests.Request(req)
246+
247+ return userInfo.Email, nil
248+}
249+
250+// GetUserName returns the Account user name
251+func (p *GitLabProvider) GetUserName(s *sessions.SessionState) (string, error) {
252+ userInfo, err := p.getUserInfo(s)
57 if err != nil {
253 if err != nil {
58− logger.Printf("failed making request %s", err)
254+ return "", fmt.Errorf("failed to retrieve user info: %v", err)
59− return "", err
60 }
255 }
61− return json.Get("email").String()
256+
257+ return userInfo.Username, nil
62}
258}
providers/gitlab_test.go+98−60
@@ -25,104 +25,142 @@ func testGitLabProvider(hostname string) *GitLabProvider {
25 updateURL(p.Data().ProfileURL, hostname)
25 updateURL(p.Data().ProfileURL, hostname)
26 updateURL(p.Data().ValidateURL, hostname)
26 updateURL(p.Data().ValidateURL, hostname)
27 }
27 }
28+
28 return p
29 return p
29}
30}
30
31
31−func testGitLabBackend(payload string) *httptest.Server {
32+func testGitLabBackend() *httptest.Server {
32− path := "/api/v4/user"
33+ userInfo := `
33− query := "access_token=imaginary_access_token"
34+ {
35+ "nickname": "FooBar",
36+ "email": "foo@bar.com",
37+ "email_verified": false,
38+ "groups": ["foo", "bar"]
39+ }
40+ `
41+ authHeader := "Bearer gitlab_access_token"
34
42
35 return httptest.NewServer(http.HandlerFunc(
43 return httptest.NewServer(http.HandlerFunc(
36 func(w http.ResponseWriter, r *http.Request) {
44 func(w http.ResponseWriter, r *http.Request) {
37− if r.URL.Path != path || r.URL.RawQuery != query {
45+ if r.URL.Path == "/oauth/userinfo" {
46+ if r.Header["Authorization"][0] == authHeader {
47+ w.WriteHeader(200)
48+ w.Write([]byte(userInfo))
49+ } else {
38− w.WriteHeader(404)
50+ w.WriteHeader(401)
51+ }
39 } else {
52 } else {
40− w.WriteHeader(200)
53+ w.WriteHeader(404)
41− w.Write([]byte(payload))
42 }
54 }
43 }))
55 }))
44}
56}
45
57
46−func TestGitLabProviderDefaults(t *testing.T) {
58+func TestGitLabProviderBadToken(t *testing.T) {
59+ b := testGitLabBackend()
60+ defer b.Close()
61+
62+ bURL, _ := url.Parse(b.URL)
47− p := testGitLabProvider("")
63+ p := testGitLabProvider(bURL.Host)
64+
65+ session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
66+ _, err := p.GetEmailAddress(session)
48− assert.NotEqual(t, nil, p)
67+ assert.NotEqual(t, nil, err)
49− assert.Equal(t, "GitLab", p.Data().ProviderName)
50− assert.Equal(t, "https://gitlab.com/oauth/authorize",
51− p.Data().LoginURL.String())
52− assert.Equal(t, "https://gitlab.com/oauth/token",
53− p.Data().RedeemURL.String())
54− assert.Equal(t, "https://gitlab.com/api/v4/user",
55− p.Data().ValidateURL.String())
56− assert.Equal(t, "read_user", p.Data().Scope)
57}
68}
58
69
59−func TestGitLabProviderOverrides(t *testing.T) {
70+func TestGitLabProviderUnverifiedEmailDenied(t *testing.T) {
60− p := NewGitLabProvider(
71+ b := testGitLabBackend()
61− &ProviderData{
72+ defer b.Close()
62− LoginURL: &url.URL{
73+
63− Scheme: "https",
74+ bURL, _ := url.Parse(b.URL)
64− Host: "example.com",
75+ p := testGitLabProvider(bURL.Host)
65− Path: "/oauth/auth"},
76+
66− RedeemURL: &url.URL{
77+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
67− Scheme: "https",
78+ _, err := p.GetEmailAddress(session)
68− Host: "example.com",
69− Path: "/oauth/token"},
70− ValidateURL: &url.URL{
71− Scheme: "https",
72− Host: "example.com",
73− Path: "/api/v4/user"},
74− Scope: "profile"})
75− assert.NotEqual(t, nil, p)
79+ assert.NotEqual(t, nil, err)
76− assert.Equal(t, "GitLab", p.Data().ProviderName)
77− assert.Equal(t, "https://example.com/oauth/auth",
78− p.Data().LoginURL.String())
79− assert.Equal(t, "https://example.com/oauth/token",
80− p.Data().RedeemURL.String())
81− assert.Equal(t, "https://example.com/api/v4/user",
82− p.Data().ValidateURL.String())
83− assert.Equal(t, "profile", p.Data().Scope)
84}
80}
85
81
86−func TestGitLabProviderGetEmailAddress(t *testing.T) {
82+func TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) {
87− b := testGitLabBackend("{\"email\": \"michael.bland@gsa.gov\"}")
83+ b := testGitLabBackend()
88 defer b.Close()
84 defer b.Close()
89
85
90 bURL, _ := url.Parse(b.URL)
86 bURL, _ := url.Parse(b.URL)
91 p := testGitLabProvider(bURL.Host)
87 p := testGitLabProvider(bURL.Host)
88+ p.AllowUnverifiedEmail = true
92
89
93− session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
90+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
94 email, err := p.GetEmailAddress(session)
91 email, err := p.GetEmailAddress(session)
95 assert.Equal(t, nil, err)
92 assert.Equal(t, nil, err)
96− assert.Equal(t, "michael.bland@gsa.gov", email)
93+ assert.Equal(t, "foo@bar.com", email)
97}
94}
98
95
99−// Note that trying to trigger the "failed building request" case is not
96+func TestGitLabProviderUsername(t *testing.T) {
100−// practical, since the only way it can fail is if the URL fails to parse.
101−func TestGitLabProviderGetEmailAddressFailedRequest(t *testing.T) {
102− b := testGitLabBackend("unused payload")
97+ b := testGitLabBackend()
103 defer b.Close()
98 defer b.Close()
104
99
105 bURL, _ := url.Parse(b.URL)
100 bURL, _ := url.Parse(b.URL)
106 p := testGitLabProvider(bURL.Host)
101 p := testGitLabProvider(bURL.Host)
102+ p.AllowUnverifiedEmail = true
107
103
108− // We'll trigger a request failure by using an unexpected access
104+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
109− // token. Alternatively, we could allow the parsing of the payload as
105+ username, err := p.GetUserName(session)
110− // JSON to fail.
106+ assert.Equal(t, nil, err)
107+ assert.Equal(t, "FooBar", username)
108+}
109+
110+func TestGitLabProviderGroupMembershipValid(t *testing.T) {
111+ b := testGitLabBackend()
112+ defer b.Close()
113+
114+ bURL, _ := url.Parse(b.URL)
115+ p := testGitLabProvider(bURL.Host)
116+ p.AllowUnverifiedEmail = true
117+ p.Group = "foo"
118+
111− session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
119+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
112 email, err := p.GetEmailAddress(session)
120 email, err := p.GetEmailAddress(session)
121+ assert.Equal(t, nil, err)
122+ assert.Equal(t, "foo@bar.com", email)
123+}
124+
125+func TestGitLabProviderGroupMembershipMissing(t *testing.T) {
126+ b := testGitLabBackend()
127+ defer b.Close()
128+
129+ bURL, _ := url.Parse(b.URL)
130+ p := testGitLabProvider(bURL.Host)
131+ p.AllowUnverifiedEmail = true
132+ p.Group = "baz"
133+
134+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
135+ _, err := p.GetEmailAddress(session)
113 assert.NotEqual(t, nil, err)
136 assert.NotEqual(t, nil, err)
114− assert.Equal(t, "", email)
115}
137}
116
138
117−func TestGitLabProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
139+func TestGitLabProviderEmailDomainValid(t *testing.T) {
118− b := testGitLabBackend("{\"foo\": \"bar\"}")
140+ b := testGitLabBackend()
119 defer b.Close()
141 defer b.Close()
120
142
121 bURL, _ := url.Parse(b.URL)
143 bURL, _ := url.Parse(b.URL)
122 p := testGitLabProvider(bURL.Host)
144 p := testGitLabProvider(bURL.Host)
145+ p.AllowUnverifiedEmail = true
146+ p.EmailDomains = []string{"bar.com"}
123
147
124− session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
148+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
125 email, err := p.GetEmailAddress(session)
149 email, err := p.GetEmailAddress(session)
150+ assert.Equal(t, nil, err)
151+ assert.Equal(t, "foo@bar.com", email)
152+}
153+
154+func TestGitLabProviderEmailDomainInvalid(t *testing.T) {
155+ b := testGitLabBackend()
156+ defer b.Close()
157+
158+ bURL, _ := url.Parse(b.URL)
159+ p := testGitLabProvider(bURL.Host)
160+ p.AllowUnverifiedEmail = true
161+ p.EmailDomains = []string{"baz.com"}
162+
163+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
164+ _, err := p.GetEmailAddress(session)
126 assert.NotEqual(t, nil, err)
165 assert.NotEqual(t, nil, err)
127− assert.Equal(t, "", email)
128}
166}
providers/google.go+26−55
@@ -189,71 +189,42 @@ func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Serv
189}
189}
190
190
191func userInGroup(service *admin.Service, groups []string, email string) bool {
191func userInGroup(service *admin.Service, groups []string, email string) bool {
192− user, err := fetchUser(service, email)
193− if err != nil {
194− logger.Printf("Warning: unable to fetch user: %v", err)
195− user = nil
196− }
197−
198 for _, group := range groups {
192 for _, group := range groups {
199− members, err := fetchGroupMembers(service, group)
193+ // Use the HasMember API to checking for the user's presence in each group or nested subgroups
194+ req := service.Members.HasMember(group, email)
195+ r, err := req.Do()
200 if err != nil {
196 if err != nil {
201− if err, ok := err.(*googleapi.Error); ok && err.Code == 404 {
197+ err, ok := err.(*googleapi.Error)
198+ if ok && err.Code == 404 {
202− logger.Printf("error fetching members for group %s: group does not exist", group)
199+ logger.Printf("error checking membership in group %s: group does not exist", group)
203− } else {
200+ } else if ok && err.Code == 400 {
204− logger.Printf("error fetching group members: %v", err)
201+ // It is possible for Members.HasMember to return false even if the email is a group member.
205− return false
202+ // One case that can cause this is if the user email is from a different domain than the group,
206− }
203+ // e.g. "member@otherdomain.com" in the group "group@mydomain.com" will result in a 400 error
207− }
204+ // from the HasMember API. In that case, attempt to query the member object directly from the group.
205+ req := service.Members.Get(group, email)
206+ r, err := req.Do()
208
207
209− for _, member := range members {
208+ if err != nil {
210− if member.Email == email {
209+ logger.Printf("error using get API to check member %s of google group %s: user not in the group", email, group)
211− return true
212− }
213− if user == nil {
214− continue
210+ continue
215− }
216− switch member.Type {
217− case "CUSTOMER":
218− if member.Id == user.CustomerId {
219− return true
220 }
211 }
221− case "USER":
212+
222− if member.Id == user.Id {
213+ // If the non-domain user is found within the group, still verify that they are "ACTIVE".
214+ // Do not count the user as belonging to a group if they have another status ("ARCHIVED", "SUSPENDED", or "UNKNOWN").
215+ if r.Status == "ACTIVE" {
223 return true
216 return true
224 }
217 }
218+ } else {
219+ logger.Printf("error checking group membership: %v", err)
225 }
220 }
221+ continue
226 }
222 }
227− }
223+ if r.IsMember {
228− return false
229−}
230−
231−func fetchUser(service *admin.Service, email string) (*admin.User, error) {
232− user, err := service.Users.Get(email).Do()
233− return user, err
234−}
235−
236−func fetchGroupMembers(service *admin.Service, group string) ([]*admin.Member, error) {
237− members := []*admin.Member{}
238− pageToken := ""
239− for {
240− req := service.Members.List(group)
241− if pageToken != "" {
242− req.PageToken(pageToken)
243− }
244− r, err := req.Do()
245− if err != nil {
246− return nil, err
224+ return true
247− }
248− for _, member := range r.Members {
249− members = append(members, member)
250− }
251− if r.NextPageToken == "" {
252− break
253 }
225 }
254− pageToken = r.NextPageToken
255 }
226 }
256− return members, nil
227+ return false
257}
228}
258
229
259// ValidateGroup validates that the provided email exists in the configured Google
230// ValidateGroup validates that the provided email exists in the configured Google
providers/google_test.go+36−15
@@ -1,6 +1,7 @@
1package providers
1package providers
2
2
3import (
3import (
4+ "context"
4 "encoding/base64"
5 "encoding/base64"
5 "encoding/json"
6 "encoding/json"
6 "fmt"
7 "fmt"
@@ -12,6 +13,7 @@ import (
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/assert"
13
14
14 admin "google.golang.org/api/admin/directory/v1"
15 admin "google.golang.org/api/admin/directory/v1"
16+ option "google.golang.org/api/option"
15)
17)
16
18
17func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
19func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
@@ -185,34 +187,53 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
185
187
186func TestGoogleProviderUserInGroup(t *testing.T) {
188func TestGoogleProviderUserInGroup(t *testing.T) {
187 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
189 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
188− if r.URL.Path == "/users/member-by-email@example.com" {
190+ if r.URL.Path == "/groups/group@example.com/hasMember/member-in-domain@example.com" {
189− fmt.Fprintln(w, "{}")
191+ fmt.Fprintln(w, `{"isMember": true}`)
190− } else if r.URL.Path == "/users/non-member-by-email@example.com" {
192+ } else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-in-domain@example.com" {
191− fmt.Fprintln(w, "{}")
193+ fmt.Fprintln(w, `{"isMember": false}`)
192− } else if r.URL.Path == "/users/member-by-id@example.com" {
194+ } else if r.URL.Path == "/groups/group@example.com/hasMember/member-out-of-domain@otherexample.com" {
193− fmt.Fprintln(w, "{\"id\": \"member-id\"}")
195+ http.Error(
194− } else if r.URL.Path == "/users/non-member-by-id@example.com" {
196+ w,
195− fmt.Fprintln(w, "{\"id\": \"non-member-id\"}")
197+ `{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`,
198+ http.StatusBadRequest,
199+ )
196− } else if r.URL.Path == "/groups/group@example.com/members" {
200+ } else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-out-of-domain@otherexample.com" {
197− fmt.Fprintln(w, "{\"members\": [{\"email\": \"member-by-email@example.com\"}, {\"id\": \"member-id\", \"type\": \"USER\"}]}")
201+ http.Error(
202+ w,
203+ `{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`,
204+ http.StatusBadRequest,
205+ )
206+ } else if r.URL.Path == "/groups/group@example.com/members/non-member-out-of-domain@otherexample.com" {
207+ // note that the client currently doesn't care what this response text or code is - any error here results in failure to match the group
208+ http.Error(
209+ w,
210+ `{"error": {"errors": [{"domain": "global","reason": "notFound","message": "Resource Not Found: memberKey"}],"code": 404,"message": "Resource Not Found: memberKey"}}`,
211+ http.StatusNotFound,
212+ )
213+ } else if r.URL.Path == "/groups/group@example.com/members/member-out-of-domain@otherexample.com" {
214+ fmt.Fprintln(w,
215+ `{"kind": "admin#directory#member","etag":"12345","id":"1234567890","email": "member-out-of-domain@otherexample.com","role": "MEMBER","type": "USER","status": "ACTIVE","delivery_settings": "ALL_MAIL"}}`,
216+ )
198 }
217 }
199 }))
218 }))
200 defer ts.Close()
219 defer ts.Close()
201
220
202 client := ts.Client()
221 client := ts.Client()
222+ ctx := context.Background()
223+
203− service, err := admin.New(client)
224+ service, err := admin.NewService(ctx, option.WithHTTPClient(client))
204 service.BasePath = ts.URL
225 service.BasePath = ts.URL
205 assert.Equal(t, nil, err)
226 assert.Equal(t, nil, err)
206
227
207− result := userInGroup(service, []string{"group@example.com"}, "member-by-email@example.com")
228+ result := userInGroup(service, []string{"group@example.com"}, "member-in-domain@example.com")
208 assert.True(t, result)
229 assert.True(t, result)
209
230
210− result = userInGroup(service, []string{"group@example.com"}, "member-by-id@example.com")
231+ result = userInGroup(service, []string{"group@example.com"}, "member-out-of-domain@otherexample.com")
211 assert.True(t, result)
232 assert.True(t, result)
212
233
213− result = userInGroup(service, []string{"group@example.com"}, "non-member-by-id@example.com")
234+ result = userInGroup(service, []string{"group@example.com"}, "non-member-in-domain@example.com")
214 assert.False(t, result)
235 assert.False(t, result)
215
236
216− result = userInGroup(service, []string{"group@example.com"}, "non-member-by-email@example.com")
237+ result = userInGroup(service, []string{"group@example.com"}, "non-member-out-of-domain@otherexample.com")
217 assert.False(t, result)
238 assert.False(t, result)
218}
239}
providers/oidc.go+35−2
@@ -3,10 +3,13 @@ package providers
3import (
3import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6+ "net/http"
6 "time"
7 "time"
7
8
8 oidc "github.com/coreos/go-oidc"
9 oidc "github.com/coreos/go-oidc"
9 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
10 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
11+ "github.com/pusher/oauth2_proxy/pkg/requests"
12+
10 "golang.org/x/oauth2"
13 "golang.org/x/oauth2"
11)
14)
12
15
@@ -117,8 +120,31 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
117 }
120 }
118
121
119 if claims.Email == "" {
122 if claims.Email == "" {
120− // TODO: Try getting email from /userinfo before falling back to Subject
123+ if p.ProfileURL.String() == "" {
124+ return nil, fmt.Errorf("id_token did not contain an email")
125+ }
126+
127+ // If the userinfo endpoint profileURL is defined, then there is a chance the userinfo
128+ // contents at the profileURL contains the email.
129+ // Make a query to the userinfo endpoint, and attempt to locate the email from there.
130+
131+ req, err := http.NewRequest("GET", p.ProfileURL.String(), nil)
132+ if err != nil {
133+ return nil, err
134+ }
135+ req.Header = getOIDCHeader(token.AccessToken)
136+
137+ respJSON, err := requests.Request(req)
138+ if err != nil {
139+ return nil, err
140+ }
141+
142+ email, err := respJSON.Get("email").String()
143+ if err != nil {
144+ return nil, fmt.Errorf("Neither id_token nor userinfo endpoint contained an email")
145+ }
146+
121− claims.Email = claims.Subject
147+ claims.Email = email
122 }
148 }
123 if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified {
149 if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified {
124 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
150 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
@@ -145,3 +171,10 @@ func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool {
145
171
146 return true
172 return true
147}
173}
174+
175+func getOIDCHeader(accessToken string) http.Header {
176+ header := make(http.Header)
177+ header.Set("Accept", "application/json")
178+ header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
179+ return header
180+}
providers/providers.go+2
@@ -38,6 +38,8 @@ func New(provider string, p *ProviderData) Provider {
38 return NewOIDCProvider(p)
38 return NewOIDCProvider(p)
39 case "login.gov":
39 case "login.gov":
40 return NewLoginGovProvider(p)
40 return NewLoginGovProvider(p)
41+ case "bitbucket":
42+ return NewBitbucketProvider(p)
41 default:
43 default:
42 return NewGoogleProvider(p)
44 return NewGoogleProvider(p)
43 }
45 }