Compare commits

..

202 Commits

Author SHA1 Message Date
Joel Speed
44cdcc79c3
Merge pull request #227 from Ofinka/keycloak-provider
Add keycloak provider
2019-09-25 21:39:11 +01:00
Dan Bond
a122ac60e4
Fix CHANGELOG errors 2019-09-25 13:33:58 -07:00
Dan Bond
85a1ed5135
Merge branch 'master' into keycloak-provider 2019-09-25 13:21:46 -07:00
Nelson Menezes
82a3d5afdc Add clarification about plural env vars (#252) 2019-08-27 09:15:33 -07:00
Joel Speed
6683e35008
Merge pull request #250 from pusher/dependabot/bundler/docs/nokogiri-1.10.4
Bump nokogiri from 1.10.1 to 1.10.4 in /docs
2019-08-21 11:09:48 +01:00
dependabot[bot]
b83b7565f3
Bump nokogiri from 1.10.1 to 1.10.4 in /docs
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.1 to 1.10.4.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.10.1...v1.10.4)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-21 10:05:52 +00:00
Henry Jenkins
71dfd44149
Merge branch 'master' into keycloak-provider 2019-08-17 08:10:37 +01:00
Joel Speed
d00c14a2a7
Merge pull request #247 from pusher/release-v4.0.0
Update changelog for v4.0.0 release
2019-08-16 15:19:32 +01:00
Joel Speed
44ea6920a7
Update changelog for v4.0.0 release 2019-08-16 15:06:53 +01:00
aledeganopix4d
fa6c4792a1 Add Bitbucket provider. (#201)
Add a new provider for Bitbucket,
can be configured from the options
specifying team and/or repository
that the user must be part/have access
to in order to grant login.
2019-08-16 14:53:22 +01:00
Joel Speed
a165928458
Merge pull request #226 from continusec/makeheadersettingdeterministic
Made setting of proxied headers deterministic based on configuration alone
2019-08-16 14:41:14 +01:00
Adam Eijdenberg
d5d4878a29 Made setting of proxied headers deterministic based on configuration
alone

Previously some headers that are normally set by the proxy (and may be
replied upstream for authorization decisiions) were not being set
depending on values in the users sesssion.

This change ensure that if a given header is sometimes set, it will
always be either set or removed.

It might be worth considerating always deleting these headers if we
didn't add them.
2019-08-16 11:44:43 +10:00
Joel Speed
c4559ea372
Merge pull request #241 from thought-machine/fix-docs-links
Fix links in docs
2019-08-15 12:07:37 +01:00
Henry Jenkins
a65d38d181
Merge branch 'master' into fix-docs-links 2019-08-14 12:04:23 +01:00
Henry Jenkins
57851f6850
Merge pull request #239 from bradym/docFormat
Docs only:  format Command Line Options using a table
2019-08-14 12:03:03 +01:00
Joel Speed
7e3ad6b215
Merge branch 'master' into docFormat 2019-08-14 11:12:54 +01:00
Henry Jenkins
c941f3ce0d
Merge branch 'master' into fix-docs-links 2019-08-13 21:23:45 +01:00
Henry Jenkins
9240538939
Merge pull request #244 from ferhatelmas/typo-fix
Fix some typos
2019-08-13 21:23:18 +01:00
Brady Mitchell
272fb96024 add back nginx-auth-request <a name 2019-08-13 09:12:48 -07:00
Brady Mitchell
bc5fc5a513 remove unnecessary <a> tags 2019-08-13 09:01:38 -07:00
Dan Bond
49e124eb87
Merge branch 'master' into typo-fix 2019-08-13 16:25:23 +01:00
Dan Bond
6453e78db3
Merge branch 'master' into docFormat 2019-08-13 16:22:18 +01:00
Joel Speed
b167744b0a
Merge pull request #145 from rtluckie/feature/add_oidc_userinfo_support
Add OIDC support for UserInfo Endpoint Email Verification
2019-08-13 15:35:51 +01:00
ferhat elmas
fb52bdb90c Fix some typos 2019-08-13 12:42:23 +02:00
Ryan Luckie
c457eeb711
Merge branch 'master' into feature/add_oidc_userinfo_support 2019-08-12 17:32:29 -05:00
Brady Mitchell
9938bb95d9
Merge branch 'master' into docFormat 2019-08-11 17:23:33 -07:00
Brady Mitchell
4b985992d8 add missing header border 2019-08-11 17:21:32 -07:00
Henry Jenkins
8b61559b8d Fix links in docs
- Fixed a bunch of references to the repo, which were 404ing
- Fixed a couple of things that 301/302ed
- Fixed some in page references
2019-08-11 16:07:03 +01:00
Henry Jenkins
e1b70dc9f0
Merge pull request #240 from vitaliytv/patch-1
[docs] Fix link to oauth2_proxy.cfg
2019-08-11 15:20:58 +01:00
Vitalii Tverdokhlib
9e37de53e3
docs: fix path to oauth2_proxy.cfg 2019-08-11 14:55:19 +03:00
Brady Mitchell
18156713e3 indent content in ordered list, fixes 165 2019-08-10 21:46:13 -07:00
Brady Mitchell
14c25c1d8a use a table for command line options 2019-08-10 21:45:18 -07:00
Joel Speed
a91cce7ab9
Merge pull request #238 from thought-machine/fix-typo
Fix typos in changelog
2019-08-07 19:36:03 +01:00
hjenkins
02dfa87f11 Fix typos in changelog 2019-08-07 18:00:37 +01:00
jansinger
7134d22bcc New flag "-ssl-upstream-insecure-skip-validation" (#234)
* New flag "-ssl-upstream-insecure-skip-validation" to skip SSL validation for upstreams with self generated / invalid SSL certificates.

* Fix tests for modified NewReverseProxy method.

* Added change to the changelog.

* Remove duplicate entries from changelog.
2019-08-07 17:48:53 +01:00
mikesiegel
d85660248c Adding docs for how to configure Okta for the OIDC provider (#235)
* Adding documentation for Okta OIDC provider.

* additional clean up.

* Clearer heading

* Forgot a word.

* updated documentation based on ReillyProcentive review.

* Per steakunderscore review: removed defaults. Removed extra hardening steps (expiration, https only etc) not directly related to setting up Okta w/ OIDC
2019-08-07 11:57:18 +01:00
Henry Jenkins
64672c34eb
Merge pull request #236 from thought-machine/slack
Adds reference to slack channel in readme
2019-08-06 12:47:10 +01:00
Dan Bond
c3eac4f6d4
Merge branch 'master' into slack 2019-08-06 12:23:45 +01:00
Alexander Overvoorde
4de49983fb Rework GitLab provider (#231)
* Initial version of OIDC based GitLab provider

* Add support for email domain check to GitLab provider

* Add gitlab.com as default issuer for GitLab provider

* Update documentation for GitLab provider

* Update unit tests for new GitLab provider implementation

* Update CHANGELOG for GitLab provider

* Rename GitLab test access token as response to linter
2019-08-06 12:20:54 +01:00
hjenkins
5f9a65f6b1 Adds reference to slack channel in readme 2019-08-06 12:16:03 +01:00
Justin Palpant
7d910c0ae8 Check Google group membership with hasMember and get. (#224)
* Check Google group membership with hasMember and get.

This PR is an enhancement built on
https://github.com/pusher/oauth2_proxy/pull/160. That PR reduces the
number of calls to the Google Admin API and simplifies the code by
using the hasMember method. It also supports checking membership in
nested groups.

However, the above message doesn't handle members who are not a part
of the domain. The hasMember API returns a 400 for that case. As a
fallback, when the API returns a 400, this change will try using the
`get` API which works as expected for members who aren't a part of the
domain. Supporting members who belong to the Google group but aren't
part of the domain is a requested feature from
https://github.com/pusher/oauth2_proxy/issues/95.

https://developers.google.com/admin-sdk/directory/v1/reference/members/get

Note that nested members who are not a part of the domain will not be
correctly detected with this change.

* Update CHANGELOG.

* Fix incorrect JSON and stop escaping strings.

* Add comments for each scenario.
2019-08-06 10:38:24 +01:00
Henry Jenkins
69c723af81
Merge pull request #232 from ReillyBrogan/fix-changelog-typos
[DOCS] Fix a bunch of places where the repo link was incorrect
2019-08-05 11:28:21 +01:00
Joel Speed
a882788efb
Merge branch 'master' into fix-changelog-typos 2019-08-05 11:17:28 +01:00
Joel Speed
88a7f9f483
Merge pull request #233 from steakunderscore/remove-dep
Remove dep from Travis CI & pre-install modules
2019-08-05 09:44:16 +01:00
hjenkins
8a24dd797f Download modules in travis install step 2019-08-05 09:26:42 +01:00
Henry Jenkins
d346219293 Remove dep from Travis CI
Was missed from previous switch to go modules
2019-08-04 21:24:21 +01:00
Reilly Brogan
1ab63304a1 Fix a bunch of places where the repo link was incorrect 2019-08-03 13:22:42 -05:00
Karel Pokorny
436936836d Fix typo in env tag 2019-07-31 14:39:34 +02:00
Karel Pokorny
a025228a6d Set env tag appropriately 2019-07-31 14:36:13 +02:00
Karel Pokorny
4eab98e65b Fix travis analysis 2019-07-28 16:58:11 +02:00
Karel Pokorny
53524875d1 Get rid of dependencies on bitly/oauth2_proxy/api 2019-07-28 16:46:16 +02:00
Karel Pokorny
800a3694c2 Add docs and record in CHANGELOG 2019-07-28 16:26:09 +02:00
Karel Pokorny
583ec18fa2 Add keycloak provider 2019-07-28 15:54:39 +02:00
Joel Speed
3f219bd85c
Merge pull request #225 from pusher/fix-codeowners
Fix CODEOWNERS file
2019-07-24 09:37:15 +01:00
Joel Speed
23309adc7c
Fix CODEOWNERS file 2019-07-24 09:21:08 +01:00
Joel Speed
6c4aca957e
Merge pull request #223 from pusher/maintainers
Add MAINTAINERS and update CODEOWNERS
2019-07-23 16:45:32 +01:00
Joel Speed
e48d28d1b9
Add MAINTAINERS and update CODEOWNERS 2019-07-23 16:20:45 +01:00
Ryan Luckie
4a6b703c54 Update CHANGELOG 2019-07-19 09:03:01 -05:00
Ryan Luckie
93cb575d7c Fix error message for clarity 2019-07-19 08:59:29 -05:00
Ryan Luckie
f537720b52 fix lint errors 2019-07-19 08:57:05 -05:00
Ryan Luckie
122ec45dd8 Requested changes 2019-07-19 08:55:14 -05:00
Ryan Luckie
0d94f5e515 fix lint error 2019-07-19 08:53:20 -05:00
Ryan Luckie
2eecf756e4 Add OIDC support for UserInfo Endpoint Email Verification
* Current OIDC implementation asserts that user email check must come
from JWT token claims. OIDC specification also allows for source
of user email to be fetched from userinfo profile endpoint.
http://openid.net/specs/openid-connect-core-1_0.html#UserInfo

* First, attempt to retrieve email from JWT token claims.  Then fall back to
requesting email from userinfo endpoint.

* Don't fallback to subject for email

https://github.com/bitly/oauth2_proxy/pull/481
2019-07-19 08:53:20 -05:00
Joel Speed
8635391543
Merge pull request #178 from kskewes/pinglog
Add silence-ping-logging flag
2019-07-19 11:30:31 +01:00
Karl
f29e353586
Update options.go
Co-Authored-By: Joel Speed <Joel.speed@hotmail.co.uk>
2019-07-19 22:11:53 +12:00
Joel Speed
2c104c4e7d
Merge pull request #211 from steakunderscore/go-mod
Switch from dep to go modules
2019-07-17 10:21:09 +01:00
Joel Speed
7bf00b7f4a
Merge pull request #213 from pusher/fix-tls-flags
Correct TLS Flags broken in #186
2019-07-17 10:10:18 +01:00
Joel Speed
7b1132df13
Fix tls-*-file docs 2019-07-17 09:58:11 +01:00
Karl Skewes
6bf3f2a51b
Correct tls cert flag name per 186 2019-07-16 13:32:57 +01:00
Karl Skewes
f00a474d91 Correct tls cert flag name per 186 2019-07-16 11:39:06 +12:00
Karl Skewes
b57d7f77e1 Use ok naming convention for map presence check 2019-07-16 10:06:29 +12:00
Karl Skewes
84da3c3d8c update changelog with both flags 2019-07-16 10:06:29 +12:00
Karl Skewes
9ed5623f2a Change env vars to suit incoming PR186 2019-07-16 10:05:10 +12:00
Karl Skewes
7236039b9d remove remnant from rebase 2019-07-16 10:04:09 +12:00
Karl Skewes
289dfce28a logger.go ExcludedPaths changed to slice of paths.
- `logger.go` convert slice of paths to map for quicker lookup
- `options.go` combines csv paths and pingpath into slice
2019-07-16 10:04:09 +12:00
Karl Skewes
4e10cc76e0 Add silence ping logging flag using ExcludePath
- Add `ping-path` option to enable switching on and passing to `logger.go`
  Default remains unchanged at: `"/ping"`
- Add note in configuration.md about silence flag taking precedence

Potential tests:
- `options.go` sets `logger.SetExcludePath` based on silence flag?
- Changing `PingPath` reflected in router?
2019-07-16 09:46:53 +12:00
Karl Skewes
08021429ea formatting and extra test
Can probably slim down the `ExcludePath` tests.
2019-07-16 09:43:48 +12:00
Karl Skewes
c4f20fff3d Add exclude logging path option
Useful for excluding /ping endpoint to reduce log volume.
This is somewhat more verbose than a simple bool to disable logging of
the `/ping` endpoint.

Perhaps better to add `-silence-ping-logging` bool flag to `options.go` and
pass in the `/ping` endpoint as part of `logger` declaration in `options.go`.

Could be extended into a slice of paths similar to go-gin's `SkipPaths`:
https://github.com/gin-gonic/gin/blob/master/logger.go#L46
2019-07-16 09:43:47 +12:00
Karl Skewes
ec97000169 Add silence ping logging flag
Add ability to silence logging of requests to /ping endpoint, reducing
log clutter

Pros:
- Don't have to change all handlers to set/not set silent ping logging
- Don't have to duplicate `loggingHandler` (this could be preferable yet)

Cons:
- Leaking oauth2proxy logic into `package logger`
- Defining default pingPath in two locations

Alternative:
- Add generic exclude path to `logger.go` and pass in `/ping`.
2019-07-16 09:42:24 +12:00
Henry Jenkins
03f218a63c Ensure gomodules are used when downloading 2019-07-15 21:49:38 +01:00
Henry Jenkins
bc81a0f6e4 Merge branch 'master' into go-mod
* master:
  Move docker dep commands to earlier in the build
2019-07-15 21:38:55 +01:00
Joel Speed
e952ab4bdf
Merge pull request #209 from dekimsey/improve-docker-rebuild-caching
Move docker dep commands to earlier in the build
2019-07-15 16:09:22 +01:00
Henry Jenkins
56f51417ae
Merge branch 'master' into go-mod 2019-07-15 16:08:21 +01:00
Daniel Kimsey
816c2a6da9 Move docker dep commands to earlier in the build
This will let Docker cache the results of the vendor dependencies.
Making re-builds during testing faster.

Also clean-up spurious test & rm in ./configure
2019-07-15 10:00:34 -05:00
Joel Speed
d7e88a4718
Merge pull request #186 from pusher/consistent-config
Make configuration consistent
2019-07-15 15:35:11 +01:00
Joel Speed
874c147e04
Fix tls-key-file and tls-cert-file consistency 2019-07-15 12:01:44 +01:00
Joel Speed
bdcdfb74f9
Update docs and changelog 2019-07-15 12:01:43 +01:00
Joel Speed
f0d006259e
Ensure all options use a consistent format for flag vs cfg vs env 2019-07-15 11:59:46 +01:00
Joel Speed
6311fa2950
Merge pull request #187 from pusher/refactor
Move root packages to pkg folder
2019-07-15 11:43:50 +01:00
Joel Speed
630db3769b
Merge branch 'master' into refactor 2019-07-15 11:30:43 +01:00
Joel Speed
4bc0a91e2e
Merge pull request #210 from steakunderscore/alpine-3-10
Update to Alpine 3.10
2019-07-15 11:25:12 +01:00
Henry Jenkins
179ee6c2db Update CHANGELOG 2019-07-14 13:51:46 +01:00
Henry Jenkins
e92e2f0cb4 Update CHANGELOG 2019-07-14 13:32:37 +01:00
Henry Jenkins
27bdb194b1 Update to Alpine 3.10 2019-07-13 22:14:05 +01:00
Henry Jenkins
c98ff79aba Update other docker files 2019-07-13 22:12:20 +01:00
Henry Jenkins
e245ef4854 Switch from dep to go mod
Update modules to avoid issues with golangci-lint
2019-07-13 21:54:45 +01:00
Joel Speed
a83c5eabb6
Merge pull request #159 from djfinlay/wip/allow-unverified-email
Create option to skip verified email check in OIDC provider
2019-07-11 16:38:17 +01:00
Daryl Finlay
9823971b7d Make insecure-oidc-allow-unverified-email configuration usage consistent 2019-07-11 15:58:31 +01:00
Daryl Finlay
776d063b98 Update changelog to include --insecure-oidc-allow-unverified-email 2019-07-11 15:30:57 +01:00
Daryl Finlay
39b6a42d43 Mark option to skip verified email check as insecure 2019-07-11 15:29:48 +01:00
Daryl Finlay
018a25be04 Create option to skip verified email check in OIDC provider 2019-07-11 15:29:48 +01:00
Joel Speed
ecd0f89c84
Merge pull request #206 from nniikkoollaaii/feature/update_docs_nginx_auth_request
update configuration.md auth_request section
2019-07-10 09:38:21 +01:00
Seip, Nikolai
387a7267e1 update configuration.md auth_request section 2019-07-10 10:26:31 +02:00
Joel Speed
4eefc01600
Merge pull request #195 from steakunderscore/banner-flag
Adds banner flag
2019-07-04 11:24:16 +01:00
Henry Jenkins
aa37564655
Merge branch 'master' into banner-flag 2019-07-02 14:03:21 +01:00
Joel Speed
85c5cef783
Merge pull request #198 from steakunderscore/switch_to_golangci-lint
Switch linter to golangci-lint
2019-07-01 16:37:26 +01:00
hjenkins
ce7e384095 Remove TODO vetshadow as it's part of govet 2019-07-01 16:27:19 +01:00
Henry Jenkins
b9cfa8f49f Add changelog entry 2019-06-25 16:42:24 +01:00
Henry Jenkins
924eab6355 Adds banner flag
This is to override what's displayed on the main page.
2019-06-25 16:41:51 +01:00
Henry Jenkins
5bcb998e6b Update changelog 2019-06-23 21:39:13 +01:00
Henry Jenkins
d24aacdb5c Fix lint errors 2019-06-23 21:39:13 +01:00
Henry Jenkins
411adf6f21 Switch linter to golangci-lint 2019-06-23 20:44:16 +01:00
Joel Speed
317f09f41e
Merge pull request #65 from lsst/jwt_bearer_passthrough
JWT bearer passthrough
2019-06-21 15:40:34 +01:00
Brian Van Klaveren
3881955605 Update unit tests for ValidateGroup 2019-06-20 16:57:20 -07:00
Brian Van Klaveren
bd651df3c2 Ensure groups in JWT Bearer tokens are also validated
Fix a minor auth logging bug
2019-06-20 13:40:04 -07:00
Brian Van Klaveren
058ffd1047 Update unit tests for username 2019-06-17 13:11:49 -07:00
Brian Van Klaveren
5a50f6223f Do not infer username from email 2019-06-17 12:58:40 -07:00
Brian Van Klaveren
100f126405 Make JwtIssuer struct private 2019-06-17 12:52:44 -07:00
Brian Van Klaveren
2f6dcf3b5f Move refreshing code to block acquiring cookied session 2019-06-17 12:52:44 -07:00
Brian Van Klaveren
48dbb391bc Move around CHANGELOG.md update 2019-06-17 12:52:44 -07:00
Brian Van Klaveren
54d91c69cc Use logger instead of log 2019-06-17 12:52:13 -07:00
Brian Van Klaveren
350c1cd127 Use JwtIssuer struct when parsing 2019-06-17 12:52:13 -07:00
Brian Van Klaveren
58b06ce761 Fall back to using sub if email is none (as in PR #57) 2019-06-17 12:52:13 -07:00
Brian Van Klaveren
79acef9036 Clarify skip-jwt-bearer-tokens default and add env tags 2019-06-17 12:52:13 -07:00
Brian Van Klaveren
10f65e0381 Add a more realistic test for JWT passthrough 2019-06-17 12:52:13 -07:00
Brian Van Klaveren
1ff74d322a Fix imports 2019-06-17 12:52:13 -07:00
Brian Van Klaveren
69cb34a04e Add unit tests for JWT -> session translation 2019-06-17 12:52:13 -07:00
Brian Van Klaveren
187960e9d8 Improve token pattern matching
Unit tests for token discovery
2019-06-17 12:52:13 -07:00
Brian Van Klaveren
8413c30c26 Update changelog with info about -skip-jwt-bearer-tokens 2019-06-17 12:52:13 -07:00
Brian Van Klaveren
b895f49c52 Use idToken expiry because that's the time checked for refresh
RefreshSessionIfNeeded checks the token expiry, we want to use
the ID token's expiry
2019-06-17 12:51:35 -07:00
Brian Van Klaveren
8083501da6 Support JWT Bearer Token and Pass through 2019-06-17 12:51:35 -07:00
Joel Speed
0af18d6d7c
Merge pull request #141 from openai/googleGroupEmail
Check google group membership based on email address
2019-06-15 14:05:56 +02:00
Joel Speed
77e1fff753
Merge pull request #185 from jonas/check-against-validate-url-string
Only validate tokens if ValidateURL resolves to a non-empty string
2019-06-15 12:30:03 +02:00
Joel Speed
0d6fa6216d
Merge pull request #180 from govau/littletidyups
Minor restructure for greater confidence that only authenticated requests are proxied
2019-06-15 12:21:54 +02:00
Joel Speed
6366690927
Fix gofmt for changed files 2019-06-15 11:34:00 +02:00
Joel Speed
417fde190c
Update changelog 2019-06-15 11:33:59 +02:00
Joel Speed
fb9616160e
Move logger to pkg/logger 2019-06-15 11:33:58 +02:00
Joel Speed
d1ef14becc
Move cookie to pkg/encryption 2019-06-15 11:33:57 +02:00
Adam Eijdenberg
d69560d020 No need for case when only 2 conditions 2019-06-15 18:48:27 +10:00
Jonas Fonseca
7a8fb58ad1
Only validate tokens if ValidateURL resolves to a non-empty string
Fix an unsupported protocol scheme error when validating tokens by
ensuring that the ValidateURL generates a non-empty string. The Azure
provider doesn't define any ValidateURL and therefore uses the default
value of `url.Parse("")` which is not `nil`.

The following log summary shows the issue:

    2019/06/14 12:26:04 oauthproxy.go:799: 10.244.1.3:34112 ("10.244.1.1") refreshing 16h26m29s old session cookie for Session{email:jonas.fonseca@example.com user:jonas.fonseca token:true} (refresh after 1h0m0s)
    2019/06/14 12:26:04 internal_util.go:60: GET ?access_token=eyJ0...
    2019/06/14 12:26:04 internal_util.go:61: token validation request failed: Get ?access_token=eyJ0...: unsupported protocol scheme ""
    2019/06/14 12:26:04 oauthproxy.go:822: 10.244.1.3:34112 ("10.244.1.1") removing session. error validating Session{email:jonas.fonseca@example.com user:jonas.fonseca token:true}
2019-06-14 12:52:22 -04:00
Joel Speed
8027cc454e
Move api to pkg/requests 2019-06-08 07:40:43 +01:00
Adam Eijdenberg
f35c82bb0f The AuthOnly path also needs the response headers set 2019-06-07 14:25:12 +10:00
Adam Eijdenberg
9e59b4f62e Restructure so that serving data from upstream is only done when explicity allowed, rather
than as implicit dangling else
2019-06-07 13:50:44 +10:00
Joel Speed
572646e0d5
Merge pull request #175 from govau/bumpoidc
Bump go-oidc
2019-06-06 17:54:25 +01:00
Joel Speed
78feaec6fa
Merge branch 'master' into bumpoidc 2019-06-06 17:38:19 +01:00
Joel Speed
55a853cf51
Merge pull request #155 from lsst/redis-session-store
Redis session store
2019-06-05 11:39:47 +01:00
Brian Van Klaveren
405f9b3bb0 Update CHANGELOG with descriptions about redis support
Add updates from master
2019-06-05 00:12:11 -07:00
Joel Speed
4721da02f2 Ensure SessionStores can handle recieving cookies for the wrong implementation
(cherry picked from commit 131206cf41)
2019-06-05 00:11:42 -07:00
Joel Speed
c1ae0ca807 Make sure the cookie exists before we clear the session in redis
(cherry picked from commit 6d7f0ab57d)
2019-06-05 00:11:42 -07:00
Joel Speed
22199fa417 Fix ticket retrieval with an invalid ticket
(cherry picked from commit 66bbf146ec)
2019-06-05 00:11:42 -07:00
Joel Speed
3155ada287 Ensure sessions are refreshable in redis session store
(cherry picked from commit 48edce3003)
2019-06-05 00:11:42 -07:00
Joel Speed
2e2327af6c Check SaveSession works when an existing session is present
(cherry picked from commit 9dc1a96d81)
2019-06-05 00:11:42 -07:00
Brian Van Klaveren
ae0258a203 Documentation updates around Redis and Redis Sentinel use 2019-06-05 00:11:42 -07:00
Joel Speed
518c1d3e8e Add Redis sentinel compatibility
(cherry picked from commit ff36b61f8c)
2019-06-05 00:11:42 -07:00
Brian Van Klaveren
fc06e2dbef Update documentation and changelog for redis store 2019-06-05 00:11:42 -07:00
Joel Speed
5095c3647d Add redis-connection-url flag 2019-06-05 00:10:51 -07:00
Joel Speed
4f5dbace9f Refactor persistent tests with more Context 2019-06-05 00:10:51 -07:00
Joel Speed
7e7bfb5daf Stop miniredis after each test 2019-06-05 00:10:51 -07:00
Joel Speed
bc3d75a2ed Run persistent tests with multiple option groups 2019-06-05 00:10:51 -07:00
Joel Speed
42f14a41d9 Clean up persistent SessionStore tests 2019-06-05 00:10:51 -07:00
Joel Speed
a7693cc72a Tranfser all cookies in tests 2019-06-05 00:10:51 -07:00
Joel Speed
93df7d9132 Remove spurious comment 2019-06-05 00:10:51 -07:00
Joel Speed
a6b8f7bde2 Rename expire -> expiration 2019-06-05 00:10:51 -07:00
Joel Speed
2f61e42c37 More obvious comment on CFB 2019-06-05 00:10:51 -07:00
Joel Speed
f435fa68ab Make loadSessionFromString private 2019-06-05 00:10:51 -07:00
Joel Speed
130d03758d Fix comments on Redis options 2019-06-05 00:10:51 -07:00
Joel Speed
7a1fc52e33 Fix go-redis version pin 2019-06-05 00:10:51 -07:00
Joel Speed
b255ed56ef Sign cookies in the Redis Session store 2019-06-05 00:10:51 -07:00
Joel Speed
2c566a5f5b Use session CreatedAt for cookie timings 2019-06-05 00:10:51 -07:00
Joel Speed
296d989e58 Simplify redis store options 2019-06-05 00:10:51 -07:00
Brian Van Klaveren
f2562e8973 Pin version of go-redis 2019-06-05 00:10:51 -07:00
Brian Van Klaveren
42731f0617 Check cookie error and doc on cookie handling 2019-06-05 00:10:51 -07:00
Brian Van Klaveren
b1bd3280db Add support for a redis session store 2019-06-05 00:10:51 -07:00
Brian Van Klaveren
e881612ea6 Fix session_state type 2019-06-05 00:10:51 -07:00
Adam Eijdenberg
b6c60f52ee Bump go-oidc 2019-06-04 10:58:35 +10:00
Joel Speed
1355c1ce30
Merge pull request #170 from zeha/release-tarballs-as-before
Make release tarballs look like bitly's
2019-06-03 16:23:30 +01:00
Joel Speed
df6b6b7ce0
Merge pull request #176 from govau/fixnogopath
Stop assuming that GOPATH is always set, and is a single directory
2019-06-03 16:21:39 +01:00
Joel Speed
40cf6b2626
Merge pull request #168 from pusher/drop-1.11
Drop Go 1.11 from Travis CI
2019-06-03 15:22:35 +01:00
Joel Speed
006322562d
Bump go version in configure to check for go 1.12 2019-06-03 14:59:58 +01:00
Joel Speed
f0b6f1525b
Update changelog 2019-06-03 14:59:56 +01:00
Joel Speed
29fb71fac5
Drop Go 1.11 from Travis CI 2019-06-03 14:59:16 +01:00
Adam Eijdenberg
37475637cd Install gometalinter in travis instead 2019-06-03 17:53:47 +10:00
Adam Eijdenberg
e7d29590cd Fix travis so that if "configure" fails, it doesn't try to run make 2019-06-03 17:47:51 +10:00
Adam Eijdenberg
b05eb71adf Stop assuming that GOPATH is always set, and is a single directory
As of I think go1.8 GOPATH is by default $HOME/go so it is incorrect to
assume that it is set.

If not set, then the Makefile assumes gometalinter will be in
/bin/gometalinter, which it likely is not, and thus fails.

We could change configure to set GOPATH in the .env, however then we
would be assuming that GOPATH is a single entry - whereas like other
paths, it can contain more than one value.

So instead this commit stops trying to install gometalinter, and like
dep, it assumes that it is installed prior.

(and since the current behaviour of the Makefile is affecting state
external to the project, that seems more logical)
2019-06-03 17:25:48 +10:00
Joel Speed
0d56a4c570
Merge pull request #171 from benbro/master
Fix repo link
2019-06-01 10:06:05 +01:00
benbro
60bb8fc7ea
Fix repo link 2019-06-01 05:36:28 +03:00
Chris Hofstaedtler
076484297e Make release tarballs look like bitly's
Fixes #162
2019-05-31 14:46:54 +02:00
Joel Speed
e374805f8e
Merge pull request #169 from kskewes/alpine3.9
Update Docker base Alpine image to 3.9
2019-05-31 09:10:02 +01:00
Karl Skewes
d3f0cb43ca Update Alpine to 3.9 2019-05-31 18:54:20 +12:00
Joel Speed
f26ed5f3d1
Merge pull request #166 from cschyma/patch-1
fix typo
2019-05-28 14:32:41 +01:00
Christian Schyma
91346df5ac
fix typo 2019-05-28 15:26:22 +02:00
Joel Speed
10e240c8bf
Merge pull request #148 from pusher/proxy-session-store
Proxy session store
2019-05-20 12:55:39 +02:00
Joel Speed
d40a61613e
Update Changelog 2019-05-20 11:39:41 +02:00
Joel Speed
093f9da881
Move cipher creation to options and away from oauth2_proxy.go 2019-05-20 11:26:13 +02:00
Joel Speed
76bd23738f
Simplify cookie creation form *options.CookieOptions 2019-05-20 11:26:12 +02:00
Joel Speed
37e31b5f09
Remove dead code 2019-05-20 11:26:11 +02:00
Joel Speed
c61f3a1c65
Use SessionStore for session in proxy 2019-05-20 11:26:10 +02:00
Joel Speed
34cbe0497c
Add CreatedAt to SessionState 2019-05-20 11:26:09 +02:00
Joel Speed
fbee5eae16
Initialise SessionStore in Options 2019-05-20 11:26:04 +02:00
Joel Speed
17e97ab884
Merge pull request #147 from pusher/session-store
Add initial session-store interface and implementation
2019-05-20 10:18:47 +01:00
Benjamin Chess
7179c5796a make unable to fetch user a warning message 2019-05-08 08:29:38 -07:00
Benjamin Chess
3f2fab10e6 check google group based on email address 2019-05-02 17:11:25 -07:00
69 changed files with 3694 additions and 1396 deletions

10
.github/CODEOWNERS vendored
View File

@ -1,6 +1,6 @@
# Default owner should be a Pusher cloud-team member unless overridden by later # Default owner should be a Pusher cloud-team member or another maintainer
# rules in this file # unless overridden by later rules in this file
* @pusher/cloud-team * @pusher/cloud-team @syscll @steakunderscore
# login.gov provider # login.gov provider
# Note: If @timothy-spencer terms out of his appointment, your best bet # Note: If @timothy-spencer terms out of his appointment, your best bet
@ -10,3 +10,7 @@
# or the public devops channel at https://chat.18f.gov/). # or the public devops channel at https://chat.18f.gov/).
providers/logingov.go @timothy-spencer providers/logingov.go @timothy-spencer
providers/logingov_test.go @timothy-spencer providers/logingov_test.go @timothy-spencer
# Bitbucket provider
providers/bitbucket.go @aledeganopix4d
providers/bitbucket_test.go @aledeganopix4d

13
.golangci.yml Normal file
View File

@ -0,0 +1,13 @@
run:
deadline: 120s
linters:
enable:
- govet
- golint
- ineffassign
- goconst
- deadcode
- gofmt
- goimports
enable-all: false
disable-all: true

View File

@ -1,16 +1,12 @@
language: go language: go
go: go:
- 1.11.x
- 1.12.x - 1.12.x
install: install:
# Fetch dependencies # Fetch dependencies
- wget -O dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.17.1
- chmod +x dep - GO111MODULE=on go mod download
- mv dep $GOPATH/bin/dep
script: script:
- ./configure - ./configure && make test
# Run tests
- make test
sudo: false sudo: false
notifications: notifications:
email: false email: false

View File

@ -1,16 +1,85 @@
# Vx.x.x (Pre-release) # Vx.x.x (Pre-release)
## Changes since v4.0.0
- [#227](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka)
# v4.0.0
## Release Highlights
- Documentation is now on a [microsite](https://pusher.github.io/oauth2_proxy/)
- Health check logging can now be disabled for quieter logs
- Authorization Header JWTs can now be verified by the proxy to skip authentication for machine users
- Sessions can now be stored in Redis. This reduces refresh failures and uses smaller cookies (Recommended for those using OIDC refreshing)
- Logging overhaul allows customisable logging formats
## Important Notes
- This release includes a number of breaking changes that will require users to
reconfigure their proxies. Please read the Breaking Changes below thoroughly.
## Breaking Changes ## Breaking Changes
- [#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) - [#231](https://github.com/pusher/oauth2_proxy/pull/231) Rework GitLab provider
- This PR changes the configuration options for the GitLab provider to use
a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than
explicit `-login-url`, `-redeem-url` and `-validate-url` parameters.
- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent
- This PR changes configuration options so that all flags have a config counterpart
of the same name but with underscores (`_`) in place of hyphens (`-`).
This change affects the following flags:
- The `--tls-key` flag is now `--tls-key-file` to be consistent with existing
file flags and the existing config and environment settings
- The `--tls-cert` flag is now `--tls-cert-file` to be consistent with existing
file flags and the existing config and environment settings
This change affects the following existing configuration options:
- The `proxy-prefix` option is now `proxy_prefix`.
This PR changes environment variables so that all flags have an environment
counterpart of the same name but capitalised, with underscores (`_`) in place
of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`.
This change affects the following existing environment variables:
- The `OAUTH2_SKIP_OIDC_DISCOVERY` environment variable is now `OAUTH2_PROXY_SKIP_OIDC_DISCOVERY`.
- The `OAUTH2_OIDC_JWKS_URL` environment variable is now `OAUTH2_PROXY_OIDC_JWKS_URL`.
- [#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
- 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 - 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
a username. a username.
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 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
the user's full email address instead. the user's full email address instead.
- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Pre-built binary tarballs changed format
- The pre-built binary tarballs again match the format of the [bitly](https://github.com/bitly/oauth2_proxy) repository, where the unpacked directory
has the same name as the tarball and the binary is always named `oauth2_proxy`. This was done to restore compatibility with third-party automation
recipes like https://github.com/jhoblitt/puppet-oauth2_proxy.
## Changes since v3.2.0 ## Changes since v3.2.0
- [#147](https://github.com/pusher/outh2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed) - [#234](https://github.com/pusher/oauth2_proxy/pull/234) Added option `-ssl-upstream-insecure-skip-validation` to skip validation of upstream SSL certificates (@jansinger)
- [#224](https://github.com/pusher/oauth2_proxy/pull/224) Check Google group membership using hasMember to support nested groups and external users (@jpalpant)
- [#231](https://github.com/pusher/oauth2_proxy/pull/231) Add optional group membership and email domain checks to the GitLab provider (@Overv)
- [#226](https://github.com/pusher/oauth2_proxy/pull/226) Made setting of proxied headers deterministic based on configuration alone (@aeijdenberg)
- [#178](https://github.com/pusher/oauth2_proxy/pull/178) Add Silence Ping Logging and Exclude Logging Paths flags (@kskewes)
- [#209](https://github.com/pusher/oauth2_proxy/pull/209) Improve docker build caching of layers (@dekimsey)
- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent (@JoelSpeed)
- [#187](https://github.com/pusher/oauth2_proxy/pull/187) Move root packages to pkg folder (@JoelSpeed)
- [#65](https://github.com/pusher/oauth2_proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
the `-skip-jwt-bearer-token` options. (@brianv0)
- 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
(e.g. `https://example.com/.well-known/jwks.json`).
- [#180](https://github.com/pusher/oauth2_proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
- [#175](https://github.com/pusher/oauth2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
- Includes fix for potential signature checking issue when OIDC discovery is skipped.
- [#155](https://github.com/pusher/oauth2_proxy/pull/155) Add RedisSessionStore implementation (@brianv0, @JoelSpeed)
- Implement flags to configure the redis session store
- `-session-store-type=redis` Sets the store type to redis
- `-redis-connection-url` Sets the Redis connection URL
- `-redis-use-sentinel=true` Enables Redis Sentinel support
- `-redis-sentinel-master-name` Sets the Sentinel master name, if sentinel is enabled
- `-redis-sentinel-connection-urls` Defines the Redis Sentinel Connection URLs, if sentinel is enabled
- Introduces the concept of a session ticket. Tickets are composed of the cookie name, a session ID, and a secret.
- Redis Sessions are stored encrypted with a per-session secret
- Added tests for server based session stores
- [#168](https://github.com/pusher/oauth2_proxy/pull/168) Drop Go 1.11 support in Travis (@JoelSpeed)
- [#169](https://github.com/pusher/oauth2_proxy/pull/169) Update Alpine to 3.9 (@kskewes)
- [#148](https://github.com/pusher/oauth2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed)
- [#147](https://github.com/pusher/oauth2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed)
- Allows for multiple different session storage implementations including client and server side - Allows for multiple different session storage implementations including client and server side
- Adds tests suite for interface to ensure consistency across implementations - Adds tests suite for interface to ensure consistency across implementations
- Refactor some configuration options (around cookies) into packages - Refactor some configuration options (around cookies) into packages
@ -32,8 +101,21 @@
- Implement two new flags to customize the logging format - Implement two new flags to customize the logging format
- `-standard-logging-format` Sets the format for standard logging - `-standard-logging-format` Sets the format for standard logging
- `-auth-logging-format` Sets the format for auth logging - `-auth-logging-format` Sets the format for auth logging
- [#111](https://github.com/pusher/oauth2_proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer) - [#111](https://github.com/pusher/oauth2_proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer)
- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Restore binary tarball contents to be compatible with bitlys original tarballs (@zeha)
- [#185](https://github.com/pusher/oauth2_proxy/pull/185) Fix an unsupported protocol scheme error during token validation when using the Azure provider (@jonas)
- [#141](https://github.com/pusher/oauth2_proxy/pull/141) Check google group membership based on email address (@bchess)
- Google Group membership is additionally checked via email address, allowing users outside a GSuite domain to be authorized.
- [#195](https://github.com/pusher/oauth2_proxy/pull/195) Add `-banner` flag for overriding the banner line that is displayed (@steakunderscore)
- [#198](https://github.com/pusher/oauth2_proxy/pull/198) Switch from gometalinter to golangci-lint (@steakunderscore)
- [#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)
- [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore)
- [#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)
- Implement flags to enable Bitbucket authentication:
- `-bitbucket-repository` Restrict authorization to users that can access this repository
- `-bitbucket-team` Restrict authorization to users that are part of this Bitbucket team
- [#211](https://github.com/pusher/oauth2_proxy/pull/211) Switch from dep to go modules (@steakunderscore)
- [#145](https://github.com/pusher/oauth2_proxy/pull/145) Add support for OIDC UserInfo endpoint email verification (@rtluckie)
# v3.2.0 # v3.2.0

View File

@ -1,15 +1,17 @@
FROM golang:1.12-stretch AS builder FROM golang:1.12-stretch AS builder
# Download tools # Download tools
RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
RUN chmod +x $GOPATH/bin/dep
# Copy sources # Copy sources
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
COPY . .
# Fetch dependencies # Fetch dependencies
RUN dep ensure --vendor-only COPY go.mod go.sum ./
RUN GO111MODULE=on go mod download
# Now pull in our code
COPY . .
# Build binary and make sure there is at least an empty key file. # Build binary and make sure there is at least an empty key file.
# This is useful for GCP App Engine custom runtime builds, because # This is useful for GCP App Engine custom runtime builds, because
@ -20,7 +22,7 @@ RUN dep ensure --vendor-only
RUN ./configure && make build && touch jwt_signing_key.pem RUN ./configure && make build && touch jwt_signing_key.pem
# Copy binary to alpine # Copy binary to alpine
FROM alpine:3.8 FROM alpine:3.10
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem

View File

@ -1,15 +1,17 @@
FROM golang:1.12-stretch AS builder FROM golang:1.12-stretch AS builder
# Download tools # Download tools
RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
RUN chmod +x $GOPATH/bin/dep
# Copy sources # Copy sources
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
COPY . .
# Fetch dependencies # Fetch dependencies
RUN dep ensure --vendor-only COPY go.mod go.sum ./
RUN GO111MODULE=on go mod download
# Now pull in our code
COPY . .
# Build binary and make sure there is at least an empty key file. # Build binary and make sure there is at least an empty key file.
# This is useful for GCP App Engine custom runtime builds, because # This is useful for GCP App Engine custom runtime builds, because
@ -20,7 +22,7 @@ RUN dep ensure --vendor-only
RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem
# Copy binary to alpine # Copy binary to alpine
FROM arm64v8/alpine:3.8 FROM arm64v8/alpine:3.10
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem

View File

@ -1,15 +1,17 @@
FROM golang:1.12-stretch AS builder FROM golang:1.12-stretch AS builder
# Download tools # Download tools
RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
RUN chmod +x $GOPATH/bin/dep
# Copy sources # Copy sources
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
COPY . .
# Fetch dependencies # Fetch dependencies
RUN dep ensure --vendor-only COPY go.mod go.sum ./
RUN GO111MODULE=on go mod download
# Now pull in our code
COPY . .
# Build binary and make sure there is at least an empty key file. # Build binary and make sure there is at least an empty key file.
# This is useful for GCP App Engine custom runtime builds, because # This is useful for GCP App Engine custom runtime builds, because
@ -20,7 +22,7 @@ RUN dep ensure --vendor-only
RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem
# Copy binary to alpine # Copy binary to alpine
FROM arm32v6/alpine:3.8 FROM arm32v6/alpine:3.10
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem

365
Gopkg.lock generated
View File

@ -1,365 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:b24249f5a5e6fbe1eddc94b25973172339ccabeadef4779274f3ed0167c18812"
name = "cloud.google.com/go"
packages = ["compute/metadata"]
pruneopts = ""
revision = "2d3a6656c17a60b0815b7e06ab0be04eacb6e613"
version = "v0.16.0"
[[projects]]
digest = "1:289dd4d7abfb3ad2b5f728fbe9b1d5c1bf7d265a3eb9ef92869af1f7baba4c7a"
name = "github.com/BurntSushi/toml"
packages = ["."]
pruneopts = ""
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
version = "v0.3.0"
[[projects]]
digest = "1:512883404c2a99156e410e9880e3bb35ecccc0c07c1159eb204b5f3ef3c431b3"
name = "github.com/bitly/go-simplejson"
packages = ["."]
pruneopts = ""
revision = "aabad6e819789e569bd6aabf444c935aa9ba1e44"
version = "v0.5.0"
[[projects]]
branch = "v2"
digest = "1:e5a238f8fa890e529d7e493849bbae8988c9e70344e4630cc4f9a11b00516afb"
name = "github.com/coreos/go-oidc"
packages = ["."]
pruneopts = ""
revision = "77e7f2010a464ade7338597afe650dfcffbe2ca8"
[[projects]]
digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = ""
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
digest = "1:6098222470fe0172157ce9bbef5d2200df4edde17ee649c5d6e48330e4afa4c6"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
pruneopts = ""
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
branch = "master"
digest = "1:3b760d3b93f994df8eb1d9ebfad17d3e9e37edcb7f7efaa15b427c0d7a64f4e4"
name = "github.com/golang/protobuf"
packages = ["proto"]
pruneopts = ""
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
[[projects]]
digest = "1:b3c5b95e56c06f5aa72cb2500e6ee5f44fcd122872d4fec2023a488e561218bc"
name = "github.com/hpcloud/tail"
packages = [
".",
"ratelimiter",
"util",
"watch",
"winfile",
]
pruneopts = ""
revision = "a30252cb686a21eb2d0b98132633053ec2f7f1e5"
version = "v1.0.0"
[[projects]]
digest = "1:af67386ca553c04c6222f7b5b2f17bc97a5dfb3b81b706882c7fd8c72c30cf8f"
name = "github.com/mbland/hmacauth"
packages = ["."]
pruneopts = ""
revision = "107c17adcc5eccc9935cd67d9bc2feaf5255d2cb"
version = "1.0.2"
[[projects]]
branch = "master"
digest = "1:15c0562bca5d78ac087fb39c211071dc124e79fb18f8b7c3f8a0bc7ffcb2a38e"
name = "github.com/mreiferson/go-options"
packages = ["."]
pruneopts = ""
revision = "20ba7d382d05facb01e02eb777af0c5f229c5c95"
[[projects]]
digest = "1:a3735b0978a8b53fc2ac97a6f46ca1189f0712a00df86d6ec4cf26c1a25e6d77"
name = "github.com/onsi/ginkgo"
packages = [
".",
"config",
"internal/codelocation",
"internal/containernode",
"internal/failer",
"internal/leafnodes",
"internal/remote",
"internal/spec",
"internal/spec_iterator",
"internal/specrunner",
"internal/suite",
"internal/testingtproxy",
"internal/writer",
"reporters",
"reporters/stenographer",
"reporters/stenographer/support/go-colorable",
"reporters/stenographer/support/go-isatty",
"types",
]
pruneopts = ""
revision = "eea6ad008b96acdaa524f5b409513bf062b500ad"
version = "v1.8.0"
[[projects]]
digest = "1:dbafce2fddb1ca331646fe2ac9c9413980368b19a60a4406a6e5861680bd73be"
name = "github.com/onsi/gomega"
packages = [
".",
"format",
"internal/assertion",
"internal/asyncassertion",
"internal/oraclematcher",
"internal/testingtsupport",
"matchers",
"matchers/support/goraph/bipartitegraph",
"matchers/support/goraph/edge",
"matchers/support/goraph/node",
"matchers/support/goraph/util",
"types",
]
pruneopts = ""
revision = "90e289841c1ed79b7a598a7cd9959750cb5e89e2"
version = "v1.5.0"
[[projects]]
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = ""
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:386e12afcfd8964907c92dffd106860c0dedd71dbefae14397b77b724a13343b"
name = "github.com/pquerna/cachecontrol"
packages = [
".",
"cacheobject",
]
pruneopts = ""
revision = "0dec1b30a0215bb68605dfc568e8855066c9202d"
[[projects]]
digest = "1:3926a4ec9a4ff1a072458451aa2d9b98acd059a45b38f7335d31e06c3d6a0159"
name = "github.com/stretchr/testify"
packages = [
"assert",
"require",
]
pruneopts = ""
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
version = "v1.1.4"
[[projects]]
branch = "master"
digest = "1:39630a0e2844fc4297c27caacb394a9fd342f869292284a62f856877adab65bc"
name = "github.com/yhat/wsutil"
packages = ["."]
pruneopts = ""
revision = "1d66fa95c997864ba4d8479f56609620fe542928"
[[projects]]
branch = "master"
digest = "1:f6a006d27619a4d93bf9b66fe1999b8c8d1fa62bdc63af14f10fbe6fcaa2aa1a"
name = "golang.org/x/crypto"
packages = [
"bcrypt",
"blowfish",
"ed25519",
"ed25519/internal/edwards25519",
]
pruneopts = ""
revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94"
[[projects]]
branch = "master"
digest = "1:130b1bec86c62e121967ee0c69d9c263dc2d3ffe6c7c9a82aca4071c4d068861"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp",
"html",
"html/atom",
"html/charset",
"websocket",
]
pruneopts = ""
revision = "9dfe39835686865bff950a07b394c12a98ddc811"
[[projects]]
branch = "master"
digest = "1:4a61176e8386727e4847b21a5a2625ce56b9c518bc543a28226503e701265db0"
name = "golang.org/x/oauth2"
packages = [
".",
"google",
"internal",
"jws",
"jwt",
]
pruneopts = ""
revision = "9ff8ebcc8e241d46f52ecc5bff0e5a2f2dbef402"
[[projects]]
branch = "master"
digest = "1:67a6e61e60283fd7dce50eba228080bff8805d9d69b2f121d7ec2260d120c4a8"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = ""
revision = "ca7f33d4116e3a1f9425755d6a44e7ed9b4c97df"
[[projects]]
digest = "1:740b51a55815493a8d0f2b1e0d0ae48fe48953bf7eaf3fcc4198823bf67768c0"
name = "golang.org/x/text"
packages = [
"encoding",
"encoding/charmap",
"encoding/htmlindex",
"encoding/internal",
"encoding/internal/identifier",
"encoding/japanese",
"encoding/korean",
"encoding/simplifiedchinese",
"encoding/traditionalchinese",
"encoding/unicode",
"internal/gen",
"internal/language",
"internal/language/compact",
"internal/tag",
"internal/utf8internal",
"language",
"runes",
"transform",
"unicode/cldr",
]
pruneopts = ""
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
version = "v0.3.2"
[[projects]]
branch = "master"
digest = "1:dc1fb726dbbe79c86369941eae1e3b431b8fc6f11dbd37f7899dc758a43cc3ed"
name = "google.golang.org/api"
packages = [
"admin/directory/v1",
"gensupport",
"googleapi",
"googleapi/internal/uritemplates",
]
pruneopts = ""
revision = "8791354e7ab150705ede13637a18c1fcc16b62e8"
[[projects]]
digest = "1:934fb8966f303ede63aa405e2c8d7f0a427a05ea8df335dfdc1833dd4d40756f"
name = "google.golang.org/appengine"
packages = [
".",
"internal",
"internal/app_identity",
"internal/base",
"internal/datastore",
"internal/log",
"internal/modules",
"internal/remote_api",
"internal/urlfetch",
"urlfetch",
]
pruneopts = ""
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
digest = "1:eb53021a8aa3f599d29c7102e65026242bdedce998a54837dc67f14b6a97c5fd"
name = "gopkg.in/fsnotify.v1"
packages = ["."]
pruneopts = ""
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
source = "https://github.com/fsnotify/fsnotify.git"
version = "v1.4.7"
[[projects]]
digest = "1:cb5b2a45a3dd41c01ff779c54ae4c8aab0271d6d3b3f734c8a8bd2c890299ef2"
name = "gopkg.in/fsnotify/fsnotify.v1"
packages = ["."]
pruneopts = ""
revision = "836bfd95fecc0f1511dd66bdbf2b5b61ab8b00b6"
version = "v1.2.11"
[[projects]]
digest = "1:11c58e19ff7ce22740423bb933f1ddca3bf575def40d5ac3437ec12871b1648b"
name = "gopkg.in/natefinch/lumberjack.v2"
packages = ["."]
pruneopts = ""
revision = "a96e63847dc3c67d17befa69c303767e2f84e54f"
version = "v2.1"
[[projects]]
digest = "1:be4ed0a2b15944dd777a663681a39260ed05f9c4e213017ed2e2255622c8820c"
name = "gopkg.in/square/go-jose.v2"
packages = [
".",
"cipher",
"json",
]
pruneopts = ""
revision = "f8f38de21b4dcd69d0413faf231983f5fd6634b1"
version = "v2.1.3"
[[projects]]
branch = "v1"
digest = "1:a96d16bd088460f2e0685d46c39bcf1208ba46e0a977be2df49864ec7da447dd"
name = "gopkg.in/tomb.v1"
packages = ["."]
pruneopts = ""
revision = "dd632973f1e7218eb1089048e0798ec9ae7dceb8"
[[projects]]
digest = "1:cedccf16b71e86db87a24f8d4c70b0a855872eb967cb906a66b95de56aefbd0d"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = ""
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/BurntSushi/toml",
"github.com/bitly/go-simplejson",
"github.com/coreos/go-oidc",
"github.com/dgrijalva/jwt-go",
"github.com/mbland/hmacauth",
"github.com/mreiferson/go-options",
"github.com/onsi/ginkgo",
"github.com/onsi/gomega",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
"github.com/yhat/wsutil",
"golang.org/x/crypto/bcrypt",
"golang.org/x/net/websocket",
"golang.org/x/oauth2",
"golang.org/x/oauth2/google",
"google.golang.org/api/admin/directory/v1",
"google.golang.org/api/googleapi",
"gopkg.in/fsnotify/fsnotify.v1",
"gopkg.in/natefinch/lumberjack.v2",
"gopkg.in/square/go-jose.v2",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,48 +0,0 @@
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
[[constraint]]
name = "github.com/BurntSushi/toml"
version = "~0.3.0"
[[constraint]]
name = "github.com/bitly/go-simplejson"
version = "~0.5.0"
[[constraint]]
branch = "v2"
name = "github.com/coreos/go-oidc"
[[constraint]]
branch = "master"
name = "github.com/mreiferson/go-options"
[[constraint]]
name = "github.com/stretchr/testify"
version = "~1.1.4"
[[constraint]]
branch = "master"
name = "golang.org/x/oauth2"
[[constraint]]
branch = "master"
name = "google.golang.org/api"
[[constraint]]
name = "gopkg.in/fsnotify/fsnotify.v1"
version = "~1.2.0"
[[override]]
name = "gopkg.in/fsnotify.v1"
source = "https://github.com/fsnotify/fsnotify.git"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
name = "gopkg.in/natefinch/lumberjack.v2"
version = "2.1.0"

3
MAINTAINERS Normal file
View File

@ -0,0 +1,3 @@
Joel Speed <joel.speed@hotmail.co.uk> (@JoelSpeed)
Dan Bond (@syscll)
Henry Jenkins <henry@henryjenkins.name> (@steakunderscore)

View File

@ -4,7 +4,7 @@ VERSION := $(shell git describe --always --dirty --tags 2>/dev/null || echo "und
.NOTPARALLEL: .NOTPARALLEL:
.PHONY: all .PHONY: all
all: dep lint $(BINARY) all: lint $(BINARY)
.PHONY: clean .PHONY: clean
clean: clean:
@ -15,36 +15,15 @@ clean:
distclean: clean distclean: clean
rm -rf vendor rm -rf vendor
BIN_DIR := $(GOPATH)/bin
GOMETALINTER := $(BIN_DIR)/gometalinter
$(GOMETALINTER):
$(GO) get -u github.com/alecthomas/gometalinter
gometalinter --install %> /dev/null
.PHONY: lint .PHONY: lint
lint: $(GOMETALINTER) lint:
$(GOMETALINTER) --vendor --disable-all \ GO111MODULE=on $(GOLANGCILINT) run
--enable=vet \
--enable=vetshadow \
--enable=golint \
--enable=ineffassign \
--enable=goconst \
--enable=deadcode \
--enable=gofmt \
--enable=goimports \
--deadline=120s \
--tests ./...
.PHONY: dep
dep:
$(DEP) ensure --vendor-only
.PHONY: build .PHONY: build
build: clean $(BINARY) build: clean $(BINARY)
$(BINARY): $(BINARY):
CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/pusher/oauth2_proxy GO111MODULE=on CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/pusher/oauth2_proxy
.PHONY: docker .PHONY: docker
docker: docker:
@ -75,24 +54,34 @@ docker-push-all: docker-push
docker push quay.io/pusher/oauth2_proxy:${VERSION}-armv6 docker push quay.io/pusher/oauth2_proxy:${VERSION}-armv6
.PHONY: test .PHONY: test
test: dep lint test: lint
$(GO) test -v -race ./... GO111MODULE=on $(GO) test -v -race ./...
.PHONY: release .PHONY: release
release: lint test release: lint test
mkdir release mkdir release
GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-darwin-amd64 github.com/pusher/oauth2_proxy mkdir release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)
GOOS=linux GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-linux-amd64 github.com/pusher/oauth2_proxy mkdir release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)
GOOS=linux GOARCH=arm64 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-linux-arm64 github.com/pusher/oauth2_proxy mkdir release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)
GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-linux-armv6 github.com/pusher/oauth2_proxy mkdir release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)
GOOS=windows GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-windows-amd64 github.com/pusher/oauth2_proxy mkdir release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)
shasum -a 256 release/$(BINARY)-darwin-amd64 > release/$(BINARY)-darwin-amd64-sha256sum.txt GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
shasum -a 256 release/$(BINARY)-linux-amd64 > release/$(BINARY)-linux-amd64-sha256sum.txt -o release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
shasum -a 256 release/$(BINARY)-linux-arm64 > release/$(BINARY)-linux-arm64-sha256sum.txt GO111MODULE=on GOOS=linux GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
shasum -a 256 release/$(BINARY)-linux-armv6 > release/$(BINARY)-linux-armv6-sha256sum.txt -o release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
shasum -a 256 release/$(BINARY)-windows-amd64 > release/$(BINARY)-windows-amd64-sha256sum.txt GO111MODULE=on GOOS=linux GOARCH=arm64 go build -ldflags="-X main.VERSION=${VERSION}" \
tar -czvf release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION).tar.gz release/$(BINARY)-darwin-amd64 -o release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
tar -czvf release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION).tar.gz release/$(BINARY)-linux-amd64 GO111MODULE=on GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X main.VERSION=${VERSION}" \
tar -czvf release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION).tar.gz release/$(BINARY)-linux-arm64 -o release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
tar -czvf release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION).tar.gz release/$(BINARY)-linux-armv6 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
tar -czvf release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION).tar.gz release/$(BINARY)-windows-amd64 -o release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
shasum -a 256 release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).darwin-amd64-sha256sum.txt
shasum -a 256 release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-amd64-sha256sum.txt
shasum -a 256 release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-arm64-sha256sum.txt
shasum -a 256 release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-armv6-sha256sum.txt
shasum -a 256 release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).windows-amd64-sha256sum.txt
tar -C release -czvf release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)
tar -C release -czvf release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)
tar -C release -czvf release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)
tar -C release -czvf release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)
tar -C release -czvf release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)

View File

@ -15,7 +15,7 @@ A list of changes can be seen in the [CHANGELOG](CHANGELOG.md).
1. Choose how to deploy: 1. Choose how to deploy:
a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v3.2.0`) a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v4.0.0`)
b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin` b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin`
@ -25,7 +25,7 @@ Prebuilt binaries can be validated by extracting the file and verifying it again
``` ```
sha256sum -c sha256sum.txt 2>&1 | grep OK sha256sum -c sha256sum.txt 2>&1 | grep OK
oauth2_proxy-3.2.0.linux-amd64: OK oauth2_proxy-4.0.0.linux-amd64: OK
``` ```
2. [Select a Provider and Register an OAuth Application with a Provider](https://pusher.github.io/oauth2_proxy/auth-configuration) 2. [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).
![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png) ![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png)
## Getting Involved
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/).
## Contributing ## Contributing
Please see our [Contributing](CONTRIBUTING.md) guidelines. Please see our [Contributing](CONTRIBUTING.md) guidelines.

16
configure vendored
View File

@ -13,13 +13,9 @@ for arg in "$@"; do
"--with-go") "--with-go")
desired[go]="${arg##*=}" desired[go]="${arg##*=}"
;; ;;
"--with-dep")
desired[dep]="${arg##*=}"
;;
"--help") "--help")
printf "${GREEN}$0${NC}\n" printf "${GREEN}$0${NC}\n"
printf " available options:\n" printf " available options:\n"
printf " --with-dep=${BLUE}<path_to_dep_binary>${NC}\n"
printf " --with-go=${BLUE}<path_to_go_binary>${NC}\n" printf " --with-go=${BLUE}<path_to_go_binary>${NC}\n"
exit 0 exit 0
;; ;;
@ -81,7 +77,7 @@ check_for() {
check_go_version() { check_go_version() {
echo -n "Checking go version... " echo -n "Checking go version... "
GO_VERSION=$(${tools[go]} version | ${tools[awk]} '{where = match($0, /[0-9]\.[0-9]+\.[0-9]*/); if (where != 0) print substr($0, RSTART, RLENGTH)}') GO_VERSION=$(${tools[go]} version | ${tools[awk]} '{where = match($0, /[0-9]\.[0-9]+\.[0-9]*/); if (where != 0) print substr($0, RSTART, RLENGTH)}')
vercomp $GO_VERSION 1.11 vercomp $GO_VERSION 1.12
case $? in case $? in
0) ;& 0) ;&
1) 1)
@ -91,7 +87,7 @@ check_go_version() {
;; ;;
2) 2)
printf "${RED}" printf "${RED}"
echo "$GO_VERSION < 1.11" echo "$GO_VERSION < 1.12"
exit 1 exit 1
;; ;;
esac esac
@ -116,16 +112,14 @@ check_go_env() {
cd ${0%/*} cd ${0%/*}
if [ ! -f .env ]; then rm -fv .env
rm .env
fi
check_for make check_for make
check_for awk check_for awk
check_for go check_for go
check_go_version check_go_version
check_go_env check_go_env
check_for dep check_for golangci-lint
echo echo
@ -133,7 +127,7 @@ cat <<- EOF > .env
MAKE := "${tools[make]}" MAKE := "${tools[make]}"
GO := "${tools[go]}" GO := "${tools[go]}"
GO_VERSION := ${tools[go_version]} GO_VERSION := ${tools[go_version]}
DEP := "${tools[dep]}" GOLANGCILINT := "${tools[golangci-lint]}"
EOF EOF
echo "Environment configuration written to .env" echo "Environment configuration written to .env"

View File

@ -1,5 +1,5 @@
## OAuth2 Proxy Config File ## OAuth2 Proxy Config File
## https://github.com/bitly/oauth2_proxy ## https://github.com/pusher/oauth2_proxy
## <addr>:<port> to listen on for HTTP/HTTPS clients ## <addr>:<port> to listen on for HTTP/HTTPS clients
# http_address = "127.0.0.1:4180" # http_address = "127.0.0.1:4180"

View File

@ -12,7 +12,7 @@ to validate accounts by email, domain or group.
**Note:** This repository was forked from [bitly/OAuth2_Proxy](https://github.com/bitly/oauth2_proxy) on 27/11/2018. **Note:** This repository was forked from [bitly/OAuth2_Proxy](https://github.com/bitly/oauth2_proxy) on 27/11/2018.
Versions v3.0.0 and up are from this fork and will have diverged from any changes in the original fork. Versions v3.0.0 and up are from this fork and will have diverged from any changes in the original fork.
A list of changes can be seen in the [CHANGELOG](CHANGELOG.md). A list of changes can be seen in the [CHANGELOG]({{ site.gitweb }}/CHANGELOG.md).
[![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy) [![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy)

View File

@ -15,6 +15,7 @@ Valid providers are :
- [Azure](#azure-auth-provider) - [Azure](#azure-auth-provider)
- [Facebook](#facebook-auth-provider) - [Facebook](#facebook-auth-provider)
- [GitHub](#github-auth-provider) - [GitHub](#github-auth-provider)
- [Keycloak](#keycloak-auth-provider)
- [GitLab](#gitlab-auth-provider) - [GitLab](#gitlab-auth-provider)
- [LinkedIn](#linkedin-auth-provider) - [LinkedIn](#linkedin-auth-provider)
- [login.gov](#logingov-provider) - [login.gov](#logingov-provider)
@ -101,15 +102,31 @@ If you are using GitHub enterprise, make sure you set the following to the appro
-redeem-url="http(s)://<enterprise github host>/login/oauth/access_token" -redeem-url="http(s)://<enterprise github host>/login/oauth/access_token"
-validate-url="http(s)://<enterprise github host>/api/v3" -validate-url="http(s)://<enterprise github host>/api/v3"
### Keycloak Auth Provider
1. Create new client in your Keycloak with **Access Type** 'confidental'.
2. Create a mapper with **Mapper Type** 'Group Membership'.
Make sure you set the following to the appropriate url:
-provider=keycloak
-client-id=<client you have created>
-client-secret=<your client's secret>
-login-url="http(s)://<keycloak host>/realms/<your realm>/protocol/openid-connect/auth"
-redeem-url="http(s)://<keycloak host>/realms/master/<your realm>/openid-connect/auth/token"
-validate-url="http(s)://<keycloak host>/realms/master/<your realm>/openid-connect/userinfo"
### GitLab Auth Provider ### GitLab Auth Provider
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) 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.
Restricting by group membership is possible with the following option:
-gitlab-group="": restrict logins to members of any of these groups (slug), separated by a comma
If you are using self-hosted GitLab, make sure you set the following to the appropriate URL: If you are using self-hosted GitLab, make sure you set the following to the appropriate URL:
-login-url="<your gitlab url>/oauth/authorize" -oidc-issuer-url="<your gitlab url>"
-redeem-url="<your gitlab url>/oauth/token"
-validate-url="<your gitlab url>/api/v4/user"
### LinkedIn Auth Provider ### LinkedIn Auth Provider
@ -124,7 +141,7 @@ For LinkedIn, the registration steps are:
### Microsoft Azure AD Provider ### Microsoft Azure AD Provider
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/). 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).
Take note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server. Take note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server.
@ -144,6 +161,56 @@ OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many ma
-cookie-secure=false -cookie-secure=false
-email-domain example.com -email-domain example.com
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
the following steps:
#### Configuring the OIDC Provider with Okta
1. Log in to Okta using an administrative account. It is suggested you try this in preview first, `example.oktapreview.com`
2. (OPTIONAL) If you want to configure authorization scopes and claims to be passed on to multiple applications,
you may wish to configure an authorization server for each application. Otherwise, the provided `default` will work.
* Navigate to **Security** then select **API**
* Click **Add Authorization Server**, if this option is not available you may require an additional license for a custom authorization server.
* Fill out the **Name** with something to describe the application you are protecting. e.g. 'Example App'.
* For **Audience**, pick the URL of the application you wish to protect: https://example.corp.com
* Fill out a **Description**
* Add any **Access Policies** you wish to configure to limit application access.
* The default settings will work for other options.
[See Okta documentation for more information on Authorization Servers](https://developer.okta.com/docs/guides/customize-authz-server/overview/)
3. Navigate to **Applications** then select **Add Application**.
* Select **Web** for the **Platform** setting.
* Select **OpenID Connect** and click **Create**
* Pick an **Application Name** such as `Example App`.
* Set the **Login redirect URI** to `https://example.corp.com`.
* Under **General** set the **Allowed grant types** to `Authorization Code` and `Refresh Token`.
* Leave the rest as default, taking note of the `Client ID` and `Client Secret`.
* Under **Assignments** select the users or groups you wish to access your application.
4. Create a configuration file like the following:
```
provider = "oidc"
redirect_url = "https://example.corp.com"
oidc_issuer_url = "https://corp.okta.com/oauth2/abCd1234"
upstreams = [
"https://example.corp.com"
]
email_domains = [
"corp.com"
]
client_id = "XXXXX"
client_secret = "YYYYY"
pass_access_token = true
cookie_secret = "ZZZZZ"
skip_provider_button = true
```
The `oidc_issuer_url` is based on URL from your **Authorization Server**'s **Issuer** field in step 2, or simply https://corp.okta.com
The `client_id` and `client_secret` are configured in the application settings.
Generate a unique `client_secret` to encrypt the cookie.
Then you can start the oauth2_proxy with `./oauth2_proxy -config /etc/example.cfg`
### login.gov Provider ### login.gov Provider
login.gov is an OIDC provider for the US Government. login.gov is an OIDC provider for the US Government.
@ -225,7 +292,7 @@ To authorize by email domain use `--email-domain=yourcompany.com`. To authorize
## Adding a new Provider ## Adding a new Provider
Follow the examples in the [`providers` package](providers/) to define a new Follow the examples in the [`providers` package]({{ site.gitweb }}/providers/) to define a new
`Provider` instance. Add a new `case` to `Provider` instance. Add a new `case` to
[`providers.New()`](providers/providers.go) to allow `oauth2_proxy` to use the [`providers.New()`]({{ site.gitweb }}/providers/providers.go) to allow `oauth2_proxy` to use the
new `Provider`. new `Provider`.

View File

@ -9,7 +9,7 @@ nav_order: 4
There are two recommended configurations. There are two recommended configurations.
1. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert=/path/to/cert.pem` and `--tls-key=/path/to/cert.key`. 1. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert-file=/path/to/cert.pem` and `--tls-key-file=/path/to/cert.key`.
The command line to run `oauth2_proxy` in this configuration would look like this: The command line to run `oauth2_proxy` in this configuration would look like this:
@ -17,8 +17,8 @@ The command line to run `oauth2_proxy` in this configuration would look like thi
./oauth2_proxy \ ./oauth2_proxy \
--email-domain="yourcompany.com" \ --email-domain="yourcompany.com" \
--upstream=http://127.0.0.1:8080/ \ --upstream=http://127.0.0.1:8080/ \
--tls-cert=/path/to/cert.pem \ --tls-cert-file=/path/to/cert.pem \
--tls-key=/path/to/cert.key \ --tls-key-file=/path/to/cert.key \
--cookie-secret=... \ --cookie-secret=... \
--cookie-secure=true \ --cookie-secure=true \
--provider=... \ --provider=... \

View File

@ -11,7 +11,7 @@ If `signature_key` is defined, proxied requests will be signed with the
`GAP-Signature` header, which is a [Hash-based Message Authentication Code `GAP-Signature` header, which is a [Hash-based Message Authentication Code
(HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) (HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code)
of selected request information and the request body [see `SIGNATURE_HEADERS` of selected request information and the request body [see `SIGNATURE_HEADERS`
in `oauthproxy.go`](./oauthproxy.go). in `oauthproxy.go`]({{ site.gitweb }}/oauthproxy.go).
`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`) `signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`)

View File

@ -208,7 +208,7 @@ GEM
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
minitest (5.11.3) minitest (5.11.3)
multipart-post (2.0.0) multipart-post (2.0.0)
nokogiri (1.10.1) nokogiri (1.10.4)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
octokit (4.13.0) octokit (4.13.0)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)

View File

@ -18,6 +18,7 @@ description: >- # this means to ignore newlines until "baseurl:"
OAuth2_Proxy documentation site OAuth2_Proxy documentation site
baseurl: "/oauth2_proxy" # the subpath of your site, e.g. /blog baseurl: "/oauth2_proxy" # the subpath of your site, e.g. /blog
url: "https://pusher.github.io" # the base hostname & protocol for your site, e.g. http://example.com url: "https://pusher.github.io" # the base hostname & protocol for your site, e.g. http://example.com
gitweb: "https://github.com/pusher/oauth2_proxy/blob/master"
# Build settings # Build settings
markdown: kramdown markdown: kramdown

View File

@ -14,89 +14,101 @@ To generate a strong cookie secret use `python -c 'import os,base64; print base6
### Config File ### Config File
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` 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`
### Command Line Options ### Command Line Options
``` | Option | Type | Description | Default |
Usage of oauth2_proxy: | ------ | ---- | ----------- | ------- |
-acr-values string: optional, used by login.gov (default "http://idmanagement.gov/ns/assurance/loa/1") | `-acr-values` | string | optional, used by login.gov | `"http://idmanagement.gov/ns/assurance/loa/1"` |
-approval-prompt string: OAuth approval_prompt (default "force") | `-approval-prompt` | string | OAuth approval_prompt | `"force"` |
-auth-logging: Log authentication attempts (default true) | `-auth-logging` | bool | Log authentication attempts | true |
-auth-logging-format string: Template for authentication log lines (see "Logging Configuration" paragraph below) | `-auth-logging-format` | string | Template for authentication log lines | see [Logging Configuration](#logging-configuration) |
-authenticated-emails-file string: authenticate against emails via file (one per line) | `-authenticated-emails-file` | string | authenticate against emails via file (one per line) | |
-azure-tenant string: go to a tenant-specific or common (tenant-independent) endpoint. (default "common") | `-azure-tenant string` | string | go to a tenant-specific or common (tenant-independent) endpoint. | `"common"` |
-basic-auth-password string: the password to set when passing the HTTP Basic Auth header | `-basic-auth-password` | string | the password to set when passing the HTTP Basic Auth header | |
-client-id string: the OAuth Client ID: ie: "123456.apps.googleusercontent.com" | `-client-id` | string | the OAuth Client ID: ie: `"123456.apps.googleusercontent.com"` | |
-client-secret string: the OAuth Client Secret | `-client-secret` | string | the OAuth Client Secret | |
-config string: path to config file | `-config` | string | path to config file | |
-cookie-domain string: an optional cookie domain to force cookies to (ie: .yourcompany.com) | `-cookie-domain` | string | an optional cookie domain to force cookies to (ie: `.yourcompany.com`) | |
-cookie-expire duration: expire timeframe for cookie (default 168h0m0s) | `-cookie-expire` | duration | expire timeframe for cookie | 168h0m0s |
-cookie-httponly: set HttpOnly cookie flag (default true) | `-cookie-httponly` | bool | set HttpOnly cookie flag | true |
-cookie-name string: the name of the cookie that the oauth_proxy creates (default "_oauth2_proxy") | `-cookie-name` | string | the name of the cookie that the oauth_proxy creates | `"_oauth2_proxy"` |
-cookie-path string: an optional cookie path to force cookies to (ie: /poc/)* (default "/") | `-cookie-path` | string | an optional cookie path to force cookies to (ie: `/poc/`) | `"/"` |
-cookie-refresh duration: refresh the cookie after this duration; 0 to disable | `-cookie-refresh` | duration | refresh the cookie after this duration; `0` to disable | |
-cookie-secret string: the seed string for secure cookies (optionally base64 encoded) | `-cookie-secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
-cookie-secure: set secure (HTTPS) cookie flag (default true) | `-cookie-secure` | bool | set secure (HTTPS) cookie flag | true |
-custom-templates-dir string: path to custom html templates | `-custom-templates-dir` | string | path to custom html templates | |
-display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true) | `-display-htpasswd-form` | bool | display username / password login form if an htpasswd file is provided | true |
-email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email | `-email-domain` | string | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | |
-flush-interval: period between flushing response buffers when streaming responses (default "1s") | `-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`) | |
-footer string: custom footer string. Use "-" to disable default footer. | `-exclude-logging-paths` | string | comma separated list of paths to exclude from logging, eg: `"/ping,/path2"` |`""` (no paths excluded) |
-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) | `-flush-interval` | duration | period between flushing response buffers when streaming responses | `"1s"` |
-github-org string: restrict logins to members of this organisation | `-banner` | string | custom banner string. Use `"-"` to disable default banner. | |
-github-team string: restrict logins to members of any of these teams (slug), separated by a comma | `-footer` | string | custom footer string. Use `"-"` to disable default footer. | |
-google-admin-email string: the google admin to impersonate for api calls | `-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 |
-google-group value: restrict logins to members of this google group (may be given multiple times). | `-github-org` | string | restrict logins to members of this organisation | |
-google-service-account-json string: the path to the service account json credentials | `-github-team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
-htpasswd-file string: additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption | `-gitlab-group` | string | restrict logins to members of any of these groups (slug), separated by a comma | |
-http-address string: [http://]<addr>:<port> or unix://<path> to listen on for HTTP clients (default "127.0.0.1:4180") | `-google-admin-email` | string | the google admin to impersonate for api calls | |
-https-address string: <addr>:<port> to listen on for HTTPS clients (default ":443") | `-google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
-logging-compress: Should rotated log files be compressed using gzip (default false) | `-google-service-account-json` | string | the path to the service account json credentials | |
-logging-filename string: File to log requests to, empty for stdout (default to stdout) | `-htpasswd-file` | string | additionally authenticate against a htpasswd file. Entries must be created with `htpasswd -s` for SHA encryption | |
-logging-local-time: If the time in log files and backup filenames are local or UTC time (default true) | `-http-address` | string | `[http://]<addr>:<port>` or `unix://<path>` to listen on for HTTP clients | `"127.0.0.1:4180"` |
-logging-max-age int: Maximum number of days to retain old log files (default 7) | `-https-address` | string | `<addr>:<port>` to listen on for HTTPS clients | `":443"` |
-logging-max-backups int: Maximum number of old log files to retain; 0 to disable (default 0) | `-logging-compress` | bool | Should rotated log files be compressed using gzip | false |
-logging-max-size int: Maximum size in megabytes of the log file before rotation (default 100) | `-logging-filename` | string | File to log requests to, empty for `stdout` | `""` (stdout) |
-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 | `-logging-local-time` | bool | Use local time in log files and backup filenames instead of UTC | true (local time) |
-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 | `-logging-max-age` | int | Maximum number of days to retain old log files | 7 |
-login-url string: Authentication endpoint | `-logging-max-backups` | int | Maximum number of old log files to retain; 0 to disable | 0 |
-oidc-issuer-url: the OpenID Connect issuer URL. ie: "https://accounts.google.com" | `-logging-max-size` | int | Maximum size in megabytes of the log file before rotation | 100 |
-oidc-jwks-url string: OIDC JWKS URI for token verification; required if OIDC discovery is disabled | `-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 | |
-pass-access-token: pass OAuth access_token to upstream via X-Forwarded-Access-Token header | `-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 | |
-pass-authorization-header: pass OIDC IDToken to upstream via Authorization Bearer header | `-login-url` | string | Authentication endpoint | |
-pass-basic-auth: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream (default true) | `-insecure-oidc-allow-unverified-email` | bool | don't fail if an email address in an id_token is not verified | false |
-pass-host-header: pass the request Host Header to upstream (default true) | `-oidc-issuer-url` | string | the OpenID Connect issuer URL. ie: `"https://accounts.google.com"` | |
-pass-user-headers: pass X-Forwarded-User and X-Forwarded-Email information to upstream (default true) | `-oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
-profile-url string: Profile access endpoint | `-pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header | false |
-provider string: OAuth provider (default "google") | `-pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
-proxy-prefix string: the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in) (default "/oauth2") | `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream | true |
-proxy-websockets: enables WebSocket proxying (default true) | `-pass-host-header` | bool | pass the request Host Header to upstream | true |
-pubjwk-url string: JWK pubkey access endpoint: required by login.gov | `-pass-user-headers` | bool | pass X-Forwarded-User and X-Forwarded-Email information to upstream | true |
-redeem-url string: Token redemption endpoint | `-profile-url` | string | Profile access endpoint | |
-redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" | `-provider` | string | OAuth provider | google |
-request-logging: Log requests to stdout (default true) | `-ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
-request-logging-format: Template for request log lines (see "Logging Configuration" paragraph below) | `-proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
-resource string: The resource that is protected (Azure AD only) | `-proxy-websockets` | bool | enables WebSocket proxying | true |
-scope string: OAuth scope specification | `-pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | |
-session-store-type: Session data storage backend (default: cookie) | `-redeem-url` | string | Token redemption endpoint | |
-set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) | `-redirect-url` | string | the OAuth Redirect URL. ie: `"https://internalapp.yourcompany.com/oauth2/callback"` | |
-set-authorization-header: set Authorization Bearer response header (useful in Nginx auth_request mode) | `-redis-connection-url` | string | URL of redis server for redis session storage (eg: `redis://HOST[:PORT]`) | |
-signature-key string: GAP-Signature request signature key (algorithm:secretkey) | `-redis-sentinel-master-name` | string | Redis sentinel master name. Used in conjunction with `--redis-use-sentinel` | |
-skip-auth-preflight: will skip authentication for OPTIONS requests | `-redis-sentinel-connection-urls` | string \| list | List of Redis sentinel connection URLs (eg `redis://HOST[:PORT]`). Used in conjunction with `--redis-use-sentinel` | |
-skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times) | `-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 |
-skip-oidc-discovery: bypass OIDC endpoint discovery. login-url, redeem-url and oidc-jwks-url must be configured in this case | `-request-logging` | bool | Log requests | true |
-skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start | `-request-logging-format` | string | Template for request log lines | see [Logging Configuration](#logging-configuration) |
-ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS | `-resource` | string | The resource that is protected (Azure AD only) | |
-standard-logging: Log standard runtime information (default true) | `-scope` | string | OAuth scope specification | |
-standard-logging-format string: Template for standard log lines (see "Logging Configuration" paragraph below) | `-session-store-type` | string | Session data storage backend | cookie |
-tls-cert string: path to certificate file | `-set-xauthrequest` | bool | set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) | false |
-tls-key string: path to private key file | `-set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
-upstream value: the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path | `-signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
-validate-url string: Access token validation endpoint | `-silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
-version: print version string | `-skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
-whitelist-domain: allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com) | `-skip-auth-regex` | string | bypass authentication for requests paths that match (may be given multiple times) | |
``` | `-skip-jwt-bearer-tokens` | bool | will skip requests that have verified JWT bearer tokens | false |
| `-skip-oidc-discovery` | bool | bypass OIDC endpoint discovery. `-login-url`, `-redeem-url` and `-oidc-jwks-url` must be configured in this case | false |
| `-skip-provider-button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
| `-ssl-insecure-skip-verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
| `-ssl-upstream-insecure-skip-verify` | bool | skip validation of certificates presented when using HTTPS upstreams | false |
| `-standard-logging` | bool | Log standard runtime information | true |
| `-standard-logging-format` | string | Template for standard log lines | see [Logging Configuration](#logging-configuration) |
| `-tls-cert-file` | string | path to certificate file | |
| `-tls-key-file` | string | path to private key file | |
| `-upstream` | string \| list | the http url(s) of the upstream endpoint or `file://` paths for static files. Routing is based on the path | |
| `-validate-url` | string | Access token validation endpoint | |
| `-version` | n/a | print version string | |
| `-whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | |
Note, when using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL. Note, when using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL.
@ -112,17 +124,16 @@ Multiple upstreams can either be configured by supplying a comma separated list
### Environment variables ### Environment variables
The following environment variables can be used in place of the corresponding command-line arguments: Every command line argument can be specified as an environment variable by
prefixing it with `OAUTH2_PROXY_`, capitalising it, and replacing hypens (`-`)
with underscores (`_`). If the argument can be specified multiple times, the
environment variable should be plural (trailing `S`).
- `OAUTH2_PROXY_CLIENT_ID` This is particularly useful for storing secrets outside of a configuration file
- `OAUTH2_PROXY_CLIENT_SECRET` or the command line.
- `OAUTH2_PROXY_COOKIE_NAME`
- `OAUTH2_PROXY_COOKIE_SECRET` For example, the `--cookie-secret` flag becomes `OAUTH2_PROXY_COOKIE_SECRET`,
- `OAUTH2_PROXY_COOKIE_DOMAIN` and the `--email-domain` flag becomes `OAUTH2_PROXY_EMAIL_DOMAINS`.
- `OAUTH2_PROXY_COOKIE_PATH`
- `OAUTH2_PROXY_COOKIE_EXPIRE`
- `OAUTH2_PROXY_COOKIE_REFRESH`
- `OAUTH2_PROXY_SIGNATURE_KEY`
## Logging Configuration ## Logging Configuration
@ -134,6 +145,8 @@ There are three different types of logging: standard, authentication, and HTTP r
Each type of logging has their own configurable format and variables. By default these formats are similar to the Apache Combined Log. Each type of logging has their own configurable format and variables. By default these formats are similar to the Apache Combined Log.
Logging of requests to the `/ping` endpoint can be disabled with `-silence-ping-logging` reducing log volume. This flag appends the `-ping-path` to `-exclude-logging-paths`.
### Auth Log Format ### Auth Log Format
Authentication logs are logs which are guaranteed to contain a username or email address of a user attempting to authenticate. These logs are output by default in the below format: Authentication logs are logs which are guaranteed to contain a username or email address of a user attempting to authenticate. These logs are output by default in the below format:
@ -306,3 +319,5 @@ nginx.ingress.kubernetes.io/configuration-snippet: |
end end
} }
``` ```
You have to substitute *name* with the actual cookie name you configured via --cookie-name parameter. If you don't set a custom cookie name the variable should be "$upstream_cookie__oauth2_proxy_1" instead of "$upstream_cookie_name_1" and the new cookie-name should be "_oauth2_proxy_1=" instead of "name_1=".

View File

@ -15,7 +15,8 @@ The OAuth2 Proxy uses a Cookie to track user sessions and will store the session
data in one of the available session storage backends. data in one of the available session storage backends.
At present the available backends are (as passed to `--session-store-type`): At present the available backends are (as passed to `--session-store-type`):
- [cookie](cookie-storage) (deafult) - [cookie](#cookie-storage) (default)
- [redis](#redis-storage)
### Cookie Storage ### Cookie Storage
@ -32,3 +33,35 @@ The following should be known when using this implementation:
- Since multiple requests can be made concurrently to the OAuth2 Proxy, this session implementation - Since multiple requests can be made concurrently to the OAuth2 Proxy, this session implementation
cannot lock sessions and while updating and refreshing sessions, there can be conflicts which force cannot lock sessions and while updating and refreshing sessions, there can be conflicts which force
users to re-authenticate users to re-authenticate
### Redis Storage
The Redis Storage backend stores sessions, encrypted, in redis. Instead sending all the information
back the the client for storage, as in the [Cookie storage](cookie-storage), a ticket is sent back
to the user as the cookie value instead.
A ticket is composed as the following:
`{CookieName}-{ticketID}.{secret}`
Where:
- The `CookieName` is the OAuth2 cookie name (_oauth2_proxy by default)
- The `ticketID` is a 128 bit random number, hex-encoded
- The `secret` is a 128 bit random number, base64url encoded (no padding). The secret is unique for every session.
- The pair of `{CookieName}-{ticketID}` comprises a ticket handle, and thus, the redis key
to which the session is stored. The encoded session is encrypted with the secret and stored
in redis via the `SETEX` command.
Encrypting every session uniquely protects the refresh/access/id tokens stored in the session from
disclosure.
#### Usage
When using the redis store, specify `--session-store-type=redis` as well as the Redis connection URL, via
`--redis-connection-url=redis://host[:port][/db-number]`.
You may also configure the store for Redis Sentinel. In this case, you will want to use the
`--redis-use-sentinel=true` flag, as well as configure the flags `--redis-sentinel-master-name`
and `--redis-sentinel-connection-urls` appropriately.

87
go.mod Normal file
View File

@ -0,0 +1,87 @@
module github.com/pusher/oauth2_proxy
go 1.12
require (
cloud.google.com/go v0.41.0 // indirect
github.com/BurntSushi/toml v0.3.1
github.com/OpenPeeDeeP/depguard v1.0.0 // indirect
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5
github.com/bitly/go-simplejson v0.5.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/coreos/bbolt v1.3.3 // indirect
github.com/coreos/etcd v3.3.13+incompatible // indirect
github.com/coreos/go-oidc v2.0.0+incompatible
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fatih/color v1.7.0 // indirect
github.com/go-critic/go-critic v0.3.4 // indirect
github.com/go-kit/kit v0.9.0 // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/go-redis/redis v6.15.2+incompatible
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
github.com/golangci/golangci-lint v1.17.1 // indirect
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
github.com/gostaticanalysis/analysisutil v0.0.2 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.9.4 // indirect
github.com/kisielk/errcheck v1.2.0 // indirect
github.com/klauspost/compress v1.7.2 // indirect
github.com/klauspost/cpuid v1.2.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pty v1.1.8 // indirect
github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e
github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect
github.com/onsi/ginkgo v1.8.0
github.com/onsi/gomega v1.5.0
github.com/pelletier/go-toml v1.4.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 // indirect
github.com/prometheus/common v0.6.0 // indirect
github.com/prometheus/procfs v0.0.3 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/rogpeppe/go-internal v1.3.0 // indirect
github.com/russross/blackfriday v2.0.0+incompatible // indirect
github.com/shirou/gopsutil v2.18.12+incompatible // indirect
github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cobra v0.0.5 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.4.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/stretchr/testify v1.3.0
github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822 // indirect
github.com/ugorji/go v1.1.7 // indirect
github.com/valyala/fasthttp v1.4.0 // indirect
github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997
go.etcd.io/bbolt v1.3.3 // indirect
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f // indirect
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 // indirect
golang.org/x/mobile v0.0.0-20190711165009-e47acb2ca7f9 // indirect
golang.org/x/net v0.0.0-20190628185345-da137c7871d7
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d // indirect
google.golang.org/api v0.7.0
google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 // indirect
google.golang.org/grpc v1.22.0 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.2.11
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3
gopkg.in/square/go-jose.v2 v2.1.3
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect
)

531
go.sum Normal file
View File

@ -0,0 +1,531 @@
cloud.google.com/go v0.16.0 h1:alV/SO2XpH+lrvqjDl94dYez7FfeT8ptayazgWwHPIU=
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.41.0 h1:NFvqUTDnSNYPX5oReekmB+D+90jrJIcVImxQ3qrBVgM=
cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg=
github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM=
github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U=
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5 h1:+xnalaRl7JEs6xynGsLgGilz75ljDYZTFKuCadGquPY=
github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5/go.mod h1:8cBZ4R1fh1lx8l4UVit3jNxyybdDi+rjnukCwTYVQE0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc=
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
github.com/go-toolsmith/astinfo v1.0.0/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w=
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 h1:MRhC9XbUjE6XDOInSJ8pwHuPagqsyO89QDU9IdVhe3o=
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0=
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc=
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc h1:XRFao922N8F3EcIXBSNX8Iywk+GI0dxD/8FicMX2D/c=
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk=
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM=
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/gostaticanalysis/analysisutil v0.0.2 h1:OZ4/Q9Lt9bzdyyjAgAWzJfL5dSwPrbkN+6UOHwYeJDM=
github.com/gostaticanalysis/analysisutil v0.0.2/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.7.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e h1:eMYU396eZUQ/ex49JNVJOEhShOhQe3Lf/opF61nFtlA=
github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05 h1:9cELXrXqZu2sczHBZHRpZ+84SR27+yXSKb1MBiUaPhA=
github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05/go.mod h1:zHtCks/HQvOt8ATyfwVe3JJq2PPuImzXINPRTC03+9w=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 h1:0XM1XL/OFFJjXsYXlG30spTkV/E9+gmd5GD1w2HE8xM=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/prometheus/tsdb v0.9.1/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ=
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822 h1:uVnVN3IUKAVcB3xG26bThgwXkWaGFc9i5qFHYKy4TKc=
github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/fasthttp v1.4.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 h1:1+FQ4Ns+UZtUiQ4lP0sTCyKSQ0EXoiwAdHZB0Pd5t9Q=
github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997/go.mod h1:DIGbh/f5XMAessMV/uaIik81gkDVjUeQ9ApdaU7wRKE=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583 h1:SZPG5w7Qxq7bMcMVl6e3Ht2X7f+AAGQdzjkbyOnNNZ8=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3 h1:f4/ZD59VsBOaJmWeI2yqtHvJhmRRPzi73C88ZtfhAIk=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190711165009-e47acb2ca7f9/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24 h1:nP0LlV1P7+z/qtbjHygz+Bba7QsbB4MqdhGJmAyicuI=
golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190506115046-ca7f33d4116e h1:bq5BY1tGuaK8HxuwN6pT6kWgTVLeJ5KwuyBpsl1CZL4=
golang.org/x/sys v0.0.0-20190506115046-ca7f33d4116e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d h1:JZPFnINSinLUdC0BJDoKhrky8niIzXMPIY2oR07+I+E=
golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/api v0.0.0-20171116170945-8791354e7ab1 h1:g6iAMpIfX2EaDmaU3Nm8KcWAuf9yDiM3uE5a7/9gZao=
google.golang.org/api v0.0.0-20171116170945-8791354e7ab1/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/appengine v1.0.0 h1:dN4LljjBKVChsv0XCSI+zbyzdqrkEwX5LQFUMRSGqOc=
google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 h1:5pOB7se0B2+IssELuQUs6uoBgYJenkU2AQlvopc2sRw=
google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/fsnotify/fsnotify.v1 v1.2.11 h1:m6l7lekIm1I7UEqyXifLaHywMLxwlWea1o3zUGDGQHs=
gopkg.in/fsnotify/fsnotify.v1 v1.2.11/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3 h1:AFxeG48hTWHhDTQDk/m2gorfVHUEa9vo3tp3D7TzwjI=
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.1.3 h1:/FoFBTvlJN6MTTVCe9plTOG+YydzkjvDGxiSPzIyoDM=
gopkg.in/square/go-jose.v2 v2.1.3/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g=
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o=
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=

View File

@ -7,7 +7,7 @@ import (
"io" "io"
"os" "os"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
) )
// Server represents an HTTP server // Server represents an HTTP server

View File

@ -8,6 +8,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const localhost = "127.0.0.1"
const host = "test-server"
func TestGCPHealthcheckLiveness(t *testing.T) { func TestGCPHealthcheckLiveness(t *testing.T) {
handler := func(w http.ResponseWriter, req *http.Request) { handler := func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("test")) w.Write([]byte("test"))
@ -16,8 +19,8 @@ func TestGCPHealthcheckLiveness(t *testing.T) {
h := gcpHealthcheck(http.HandlerFunc(handler)) h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/liveness_check", nil) r, _ := http.NewRequest("GET", "/liveness_check", nil)
r.RemoteAddr = "127.0.0.1" r.RemoteAddr = localhost
r.Host = "test-server" r.Host = host
h.ServeHTTP(rw, r) h.ServeHTTP(rw, r)
assert.Equal(t, 200, rw.Code) assert.Equal(t, 200, rw.Code)
@ -32,8 +35,8 @@ func TestGCPHealthcheckReadiness(t *testing.T) {
h := gcpHealthcheck(http.HandlerFunc(handler)) h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/readiness_check", nil) r, _ := http.NewRequest("GET", "/readiness_check", nil)
r.RemoteAddr = "127.0.0.1" r.RemoteAddr = localhost
r.Host = "test-server" r.Host = host
h.ServeHTTP(rw, r) h.ServeHTTP(rw, r)
assert.Equal(t, 200, rw.Code) assert.Equal(t, 200, rw.Code)
@ -48,8 +51,8 @@ func TestGCPHealthcheckNotHealthcheck(t *testing.T) {
h := gcpHealthcheck(http.HandlerFunc(handler)) h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/not_any_check", nil) r, _ := http.NewRequest("GET", "/not_any_check", nil)
r.RemoteAddr = "127.0.0.1" r.RemoteAddr = localhost
r.Host = "test-server" r.Host = host
h.ServeHTTP(rw, r) h.ServeHTTP(rw, r)
assert.Equal(t, "test", rw.Body.String()) assert.Equal(t, "test", rw.Body.String())
@ -63,8 +66,8 @@ func TestGCPHealthcheckIngress(t *testing.T) {
h := gcpHealthcheck(http.HandlerFunc(handler)) h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil) r, _ := http.NewRequest("GET", "/", nil)
r.RemoteAddr = "127.0.0.1" r.RemoteAddr = localhost
r.Host = "test-server" r.Host = host
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent) r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
h.ServeHTTP(rw, r) h.ServeHTTP(rw, r)
@ -80,8 +83,8 @@ func TestGCPHealthcheckNotIngress(t *testing.T) {
h := gcpHealthcheck(http.HandlerFunc(handler)) h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/foo", nil) r, _ := http.NewRequest("GET", "/foo", nil)
r.RemoteAddr = "127.0.0.1" r.RemoteAddr = localhost
r.Host = "test-server" r.Host = host
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent) r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
h.ServeHTTP(rw, r) h.ServeHTTP(rw, r)
@ -96,8 +99,8 @@ func TestGCPHealthcheckNotIngressPut(t *testing.T) {
h := gcpHealthcheck(http.HandlerFunc(handler)) h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
r, _ := http.NewRequest("PUT", "/", nil) r, _ := http.NewRequest("PUT", "/", nil)
r.RemoteAddr = "127.0.0.1" r.RemoteAddr = localhost
r.Host = "test-server" r.Host = host
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent) r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
h.ServeHTTP(rw, r) h.ServeHTTP(rw, r)

View File

@ -10,7 +10,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
) )
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status // responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status
@ -75,18 +75,19 @@ func (l *responseLogger) Status() int {
return l.status return l.status
} }
// Size returns teh response size // Size returns the response size
func (l *responseLogger) Size() int { func (l *responseLogger) Size() int {
return l.size return l.size
} }
// Flush sends any buffered data to the client
func (l *responseLogger) Flush() { func (l *responseLogger) Flush() {
if flusher, ok := l.w.(http.Flusher); ok { if flusher, ok := l.w.(http.Flusher); ok {
flusher.Flush() flusher.Flush()
} }
} }
// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its friends // loggingHandler is the http.Handler implementation for LoggingHandler
type loggingHandler struct { type loggingHandler struct {
handler http.Handler handler http.Handler
} }

View File

@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
) )
func TestLoggingHandler_ServeHTTP(t *testing.T) { func TestLoggingHandler_ServeHTTP(t *testing.T) {
@ -17,10 +17,23 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) {
tests := []struct { tests := []struct {
Format, Format,
ExpectedLogMessage string ExpectedLogMessage,
Path string
ExcludePaths []string
SilencePingLogging bool
}{ }{
{logger.DefaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", logger.FormatTimestamp(ts))}, {logger.DefaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", logger.FormatTimestamp(ts)), "/foo/bar", []string{}, false},
{"{{.RequestMethod}}", "GET\n"}, {logger.DefaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", logger.FormatTimestamp(ts)), "/foo/bar", []string{}, true},
{logger.DefaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", logger.FormatTimestamp(ts)), "/foo/bar", []string{"/ping"}, false},
{logger.DefaultRequestLoggingFormat, "", "/foo/bar", []string{"/foo/bar"}, false},
{logger.DefaultRequestLoggingFormat, "", "/ping", []string{}, true},
{logger.DefaultRequestLoggingFormat, "", "/ping", []string{"/ping"}, false},
{logger.DefaultRequestLoggingFormat, "", "/ping", []string{"/ping"}, true},
{logger.DefaultRequestLoggingFormat, "", "/ping", []string{"/foo/bar", "/ping"}, false},
{"{{.RequestMethod}}", "GET\n", "/foo/bar", []string{}, true},
{"{{.RequestMethod}}", "GET\n", "/foo/bar", []string{"/ping"}, false},
{"{{.RequestMethod}}", "GET\n", "/ping", []string{}, false},
{"{{.RequestMethod}}", "", "/ping", []string{"/ping"}, true},
} }
for _, test := range tests { for _, test := range tests {
@ -36,9 +49,13 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) {
logger.SetOutput(buf) logger.SetOutput(buf)
logger.SetReqTemplate(test.Format) logger.SetReqTemplate(test.Format)
if test.SilencePingLogging {
test.ExcludePaths = append(test.ExcludePaths, "/ping")
}
logger.SetExcludePaths(test.ExcludePaths)
h := LoggingHandler(http.HandlerFunc(handler)) h := LoggingHandler(http.HandlerFunc(handler))
r, _ := http.NewRequest("GET", "/foo/bar", nil) r, _ := http.NewRequest("GET", test.Path, nil)
r.RemoteAddr = "127.0.0.1" r.RemoteAddr = "127.0.0.1"
r.Host = "test-server" r.Host = "test-server"

34
main.go
View File

@ -12,7 +12,7 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
options "github.com/mreiferson/go-options" options "github.com/mreiferson/go-options"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
) )
func main() { func main() {
@ -23,15 +23,17 @@ func main() {
whitelistDomains := StringArray{} whitelistDomains := StringArray{}
upstreams := StringArray{} upstreams := StringArray{}
skipAuthRegex := StringArray{} skipAuthRegex := StringArray{}
jwtIssuers := StringArray{}
googleGroups := StringArray{} googleGroups := StringArray{}
redisSentinelConnectionURLs := StringArray{}
config := flagSet.String("config", "", "path to config file") config := flagSet.String("config", "", "path to config file")
showVersion := flagSet.Bool("version", false, "print version string") showVersion := flagSet.Bool("version", false, "print version string")
flagSet.String("http-address", "127.0.0.1:4180", "[http://]<addr>:<port> or unix://<path> to listen on for HTTP clients") flagSet.String("http-address", "127.0.0.1:4180", "[http://]<addr>:<port> or unix://<path> to listen on for HTTP clients")
flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients") flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
flagSet.String("tls-cert", "", "path to certificate file") flagSet.String("tls-cert-file", "", "path to certificate file")
flagSet.String("tls-key", "", "path to private key file") flagSet.String("tls-key-file", "", "path to private key file")
flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"") flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)") flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)")
flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path") flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path")
@ -45,14 +47,21 @@ func main() {
flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)") flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start") flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests") flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS") flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")
flagSet.Bool("ssl-upstream-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS upstreams")
flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses") flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
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)")
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)") flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
flagSet.String("keycloak-group", "", "restrict login to members of this group.")
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
flagSet.String("github-org", "", "restrict logins to members of this organisation") flagSet.String("github-org", "", "restrict logins to members of this organisation")
flagSet.String("github-team", "", "restrict logins to members of this team") flagSet.String("github-team", "", "restrict logins to members of this team")
flagSet.String("gitlab-group", "", "restrict logins to members of this group")
flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).") flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).")
flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls") flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
flagSet.String("google-service-account-json", "", "the path to the service account json credentials") flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
@ -62,8 +71,10 @@ func main() {
flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption or \"htpasswd -B\" for bcrypt encryption") flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption or \"htpasswd -B\" for bcrypt encryption")
flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided") flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided")
flagSet.String("custom-templates-dir", "", "path to custom html templates") flagSet.String("custom-templates-dir", "", "path to custom html templates")
flagSet.String("banner", "", "custom banner string. Use \"-\" to disable default banner.")
flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.") flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)") flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks")
flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying") flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying")
flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates") flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates")
@ -76,6 +87,10 @@ func main() {
flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
flagSet.String("session-store-type", "cookie", "the session storage provider to use") flagSet.String("session-store-type", "cookie", "the session storage provider to use")
flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
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")
flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel")
flagSet.Var(&redisSentinelConnectionURLs, "redis-sentinel-connection-urls", "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel")
flagSet.String("logging-filename", "", "File to log requests to, empty for stdout") flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation") flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation")
@ -89,12 +104,15 @@ func main() {
flagSet.Bool("request-logging", true, "Log HTTP requests") flagSet.Bool("request-logging", true, "Log HTTP requests")
flagSet.String("request-logging-format", logger.DefaultRequestLoggingFormat, "Template for HTTP request log lines") flagSet.String("request-logging-format", logger.DefaultRequestLoggingFormat, "Template for HTTP request log lines")
flagSet.String("exclude-logging-paths", "", "Exclude logging requests to paths (eg: '/path1,/path2,/path3')")
flagSet.Bool("silence-ping-logging", false, "Disable logging of requests to ping endpoint")
flagSet.Bool("auth-logging", true, "Log authentication attempts") flagSet.Bool("auth-logging", true, "Log authentication attempts")
flagSet.String("auth-logging-format", logger.DefaultAuthLoggingFormat, "Template for authentication log lines") flagSet.String("auth-logging-format", logger.DefaultAuthLoggingFormat, "Template for authentication log lines")
flagSet.String("provider", "google", "OAuth provider") flagSet.String("provider", "google", "OAuth provider")
flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)") flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified")
flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints") flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints")
flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)") flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
flagSet.String("login-url", "", "Authentication endpoint") flagSet.String("login-url", "", "Authentication endpoint")
@ -140,7 +158,13 @@ func main() {
validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile) validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile)
oauthproxy := NewOAuthProxy(opts, validator) oauthproxy := NewOAuthProxy(opts, validator)
if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" { if len(opts.Banner) >= 1 {
if opts.Banner == "-" {
oauthproxy.SignInMessage = ""
} else {
oauthproxy.SignInMessage = opts.Banner
}
} else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
if len(opts.EmailDomains) > 1 { if len(opts.EmailDomains) > 1 {
oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", ")) oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
} else if opts.EmailDomains[0] != "*" { } else if opts.EmailDomains[0] != "*" {

View File

@ -1,6 +1,8 @@
package main package main
import ( import (
"context"
"crypto/tls"
b64 "encoding/base64" b64 "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -13,10 +15,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/coreos/go-oidc"
"github.com/mbland/hmacauth" "github.com/mbland/hmacauth"
"github.com/pusher/oauth2_proxy/cookie" sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/logger"
"github.com/pusher/oauth2_proxy/providers" "github.com/pusher/oauth2_proxy/providers"
"github.com/yhat/wsutil" "github.com/yhat/wsutil"
) )
@ -29,10 +32,6 @@ const (
httpScheme = "http" httpScheme = "http"
httpsScheme = "https" httpsScheme = "https"
// Cookies are limited to 4kb including the length of the cookie name,
// the cookie name can be up to 256 bytes
maxCookieLength = 3840
applicationJSON = "application/json" applicationJSON = "application/json"
) )
@ -51,6 +50,11 @@ var SignatureHeaders = []string{
"Gap-Auth", "Gap-Auth",
} }
var (
// ErrNeedsLogin means the user should be redirected to the login page
ErrNeedsLogin = errors.New("redirect to login page")
)
// OAuthProxy is the main authentication proxy // OAuthProxy is the main authentication proxy
type OAuthProxy struct { type OAuthProxy struct {
CookieSeed string CookieSeed string
@ -75,6 +79,7 @@ type OAuthProxy struct {
redirectURL *url.URL // the url to receive requests at redirectURL *url.URL // the url to receive requests at
whitelistDomains []string whitelistDomains []string
provider providers.Provider provider providers.Provider
sessionStore sessionsapi.SessionStore
ProxyPrefix string ProxyPrefix string
SignInMessage string SignInMessage string
HtpasswdFile *HtpasswdFile HtpasswdFile *HtpasswdFile
@ -88,11 +93,13 @@ type OAuthProxy struct {
PassAccessToken bool PassAccessToken bool
SetAuthorization bool SetAuthorization bool
PassAuthorization bool PassAuthorization bool
CookieCipher *cookie.Cipher
skipAuthRegex []string skipAuthRegex []string
skipAuthPreflight bool skipAuthPreflight bool
skipJwtBearerTokens bool
jwtBearerVerifiers []*oidc.IDTokenVerifier
compiledRegex []*regexp.Regexp compiledRegex []*regexp.Regexp
templates *template.Template templates *template.Template
Banner string
Footer string Footer string
} }
@ -122,9 +129,14 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// NewReverseProxy creates a new reverse proxy for proxying requests to upstream // NewReverseProxy creates a new reverse proxy for proxying requests to upstream
// servers // servers
func NewReverseProxy(target *url.URL, flushInterval time.Duration) (proxy *httputil.ReverseProxy) { func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) {
proxy = httputil.NewSingleHostReverseProxy(target) proxy = httputil.NewSingleHostReverseProxy(target)
proxy.FlushInterval = flushInterval proxy.FlushInterval = opts.FlushInterval
if opts.SSLUpstreamInsecureSkipVerify {
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
return proxy return proxy
} }
@ -155,9 +167,9 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
} }
// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url // NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) (restProxy http.Handler) { func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
u.Path = "" u.Path = ""
proxy := NewReverseProxy(u, opts.FlushInterval) proxy := NewReverseProxy(u, opts)
if !opts.PassHostHeader { if !opts.PassHostHeader {
setProxyUpstreamHostHeader(proxy, u) setProxyUpstreamHostHeader(proxy, u)
} else { } else {
@ -171,7 +183,12 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma
wsURL := &url.URL{Scheme: wsScheme, Host: u.Host} wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
wsProxy = wsutil.NewSingleHostReverseProxy(wsURL) wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
} }
return &UpstreamProxy{u.Host, proxy, wsProxy, auth} return &UpstreamProxy{
upstream: u.Host,
handler: proxy,
wsHandler: wsProxy,
auth: auth,
}
} }
// NewOAuthProxy creates a new instance of OOuthProxy from the options provided // NewOAuthProxy creates a new instance of OOuthProxy from the options provided
@ -196,7 +213,13 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
} }
logger.Printf("mapping path %q => file system %q", path, u.Path) logger.Printf("mapping path %q => file system %q", path, u.Path)
proxy := NewFileServer(path, u.Path) proxy := NewFileServer(path, u.Path)
serveMux.Handle(path, &UpstreamProxy{path, proxy, nil, nil}) uProxy := UpstreamProxy{
upstream: path,
handler: proxy,
wsHandler: nil,
auth: nil,
}
serveMux.Handle(path, &uProxy)
default: default:
panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme)) panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
} }
@ -205,6 +228,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
logger.Printf("compiled skip-auth-regex => %q", u) logger.Printf("compiled skip-auth-regex => %q", u)
} }
if opts.SkipJwtBearerTokens {
logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
for _, issuer := range opts.ExtraJwtIssuers {
logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
}
}
redirectURL := opts.redirectURL redirectURL := opts.redirectURL
if redirectURL.Path == "" { if redirectURL.Path == "" {
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
@ -218,15 +247,6 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, refresh) logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, refresh)
var cipher *cookie.Cipher
if opts.PassAccessToken || opts.SetAuthorization || opts.PassAuthorization || (opts.CookieRefresh != time.Duration(0)) {
var err error
cipher, err = cookie.NewCipher(secretBytes(opts.CookieSecret))
if err != nil {
logger.Fatal("cookie-secret error: ", err)
}
}
return &OAuthProxy{ return &OAuthProxy{
CookieName: opts.CookieName, CookieName: opts.CookieName,
CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"), CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
@ -240,7 +260,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
Validator: validator, Validator: validator,
RobotsPath: "/robots.txt", RobotsPath: "/robots.txt",
PingPath: "/ping", PingPath: opts.PingPath,
SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix), SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix), OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
@ -249,11 +269,14 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
ProxyPrefix: opts.ProxyPrefix, ProxyPrefix: opts.ProxyPrefix,
provider: opts.provider, provider: opts.provider,
sessionStore: opts.sessionStore,
serveMux: serveMux, serveMux: serveMux,
redirectURL: redirectURL, redirectURL: redirectURL,
whitelistDomains: opts.WhitelistDomains, whitelistDomains: opts.WhitelistDomains,
skipAuthRegex: opts.SkipAuthRegex, skipAuthRegex: opts.SkipAuthRegex,
skipAuthPreflight: opts.SkipAuthPreflight, skipAuthPreflight: opts.SkipAuthPreflight,
skipJwtBearerTokens: opts.SkipJwtBearerTokens,
jwtBearerVerifiers: opts.jwtBearerVerifiers,
compiledRegex: opts.CompiledRegex, compiledRegex: opts.CompiledRegex,
SetXAuthRequest: opts.SetXAuthRequest, SetXAuthRequest: opts.SetXAuthRequest,
PassBasicAuth: opts.PassBasicAuth, PassBasicAuth: opts.PassBasicAuth,
@ -263,8 +286,8 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
SetAuthorization: opts.SetAuthorization, SetAuthorization: opts.SetAuthorization,
PassAuthorization: opts.PassAuthorization, PassAuthorization: opts.PassAuthorization,
SkipProviderButton: opts.SkipProviderButton, SkipProviderButton: opts.SkipProviderButton,
CookieCipher: cipher,
templates: loadTemplates(opts.CustomTemplatesDir), templates: loadTemplates(opts.CustomTemplatesDir),
Banner: opts.Banner,
Footer: opts.Footer, Footer: opts.Footer,
} }
} }
@ -293,7 +316,7 @@ func (p *OAuthProxy) displayCustomLoginForm() bool {
return p.HtpasswdFile != nil && p.DisplayHtpasswdForm return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
} }
func (p *OAuthProxy) redeemCode(host, code string) (s *sessions.SessionState, err error) { func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState, err error) {
if code == "" { if code == "" {
return nil, errors.New("missing code") return nil, errors.New("missing code")
} }
@ -316,104 +339,6 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *sessions.SessionState, er
return return
} }
// MakeSessionCookie creates an http.Cookie containing the authenticated user's
// authentication details
func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie {
if value != "" {
value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
}
c := p.makeCookie(req, p.CookieName, value, expiration, now)
if len(c.Value) > 4096-len(p.CookieName) {
return splitCookie(c)
}
return []*http.Cookie{c}
}
func copyCookie(c *http.Cookie) *http.Cookie {
return &http.Cookie{
Name: c.Name,
Value: c.Value,
Path: c.Path,
Domain: c.Domain,
Expires: c.Expires,
RawExpires: c.RawExpires,
MaxAge: c.MaxAge,
Secure: c.Secure,
HttpOnly: c.HttpOnly,
Raw: c.Raw,
Unparsed: c.Unparsed,
}
}
// splitCookie reads the full cookie generated to store the session and splits
// it into a slice of cookies which fit within the 4kb cookie limit indexing
// the cookies from 0
func splitCookie(c *http.Cookie) []*http.Cookie {
if len(c.Value) < maxCookieLength {
return []*http.Cookie{c}
}
cookies := []*http.Cookie{}
valueBytes := []byte(c.Value)
count := 0
for len(valueBytes) > 0 {
new := copyCookie(c)
new.Name = fmt.Sprintf("%s_%d", c.Name, count)
count++
if len(valueBytes) < maxCookieLength {
new.Value = string(valueBytes)
valueBytes = []byte{}
} else {
newValue := valueBytes[:maxCookieLength]
valueBytes = valueBytes[maxCookieLength:]
new.Value = string(newValue)
}
cookies = append(cookies, new)
}
return cookies
}
// joinCookies takes a slice of cookies from the request and reconstructs the
// full session cookie
func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) {
if len(cookies) == 0 {
return nil, fmt.Errorf("list of cookies must be > 0")
}
if len(cookies) == 1 {
return cookies[0], nil
}
c := copyCookie(cookies[0])
for i := 1; i < len(cookies); i++ {
c.Value += cookies[i].Value
}
c.Name = strings.TrimRight(c.Name, "_0")
return c, nil
}
// loadCookie retreieves the sessions state cookie from the http request.
// If a single cookie is present this will be returned, otherwise it attempts
// to reconstruct a cookie split up by splitCookie
func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) {
c, err := req.Cookie(cookieName)
if err == nil {
return c, nil
}
cookies := []*http.Cookie{}
err = nil
count := 0
for err == nil {
var c *http.Cookie
c, err = req.Cookie(fmt.Sprintf("%s_%d", cookieName, count))
if err == nil {
cookies = append(cookies, c)
count++
}
}
if len(cookies) == 0 {
return nil, fmt.Errorf("Could not find cookie %s", cookieName)
}
return joinCookies(cookies)
}
// MakeCSRFCookie creates a cookie for CSRF // MakeCSRFCookie creates a cookie for CSRF
func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie { func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
return p.makeCookie(req, p.CSRFCookieName, value, expiration, now) return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
@ -454,66 +379,18 @@ func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, va
// ClearSessionCookie creates a cookie to unset the user's authentication cookie // ClearSessionCookie creates a cookie to unset the user's authentication cookie
// stored in the user's session // stored in the user's session
func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) { func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
var cookies []*http.Cookie return p.sessionStore.Clear(rw, req)
// matches CookieName, CookieName_<number>
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", p.CookieName))
for _, c := range req.Cookies() {
if cookieNameRegex.MatchString(c.Name) {
clearCookie := p.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())
http.SetCookie(rw, clearCookie)
cookies = append(cookies, clearCookie)
}
}
// ugly hack because default domain changed
if p.CookieDomain == "" && len(cookies) > 0 {
clr2 := *cookies[0]
clr2.Domain = req.Host
http.SetCookie(rw, &clr2)
}
}
// SetSessionCookie adds the user's session cookie to the response
func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
for _, c := range p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()) {
http.SetCookie(rw, c)
}
} }
// LoadCookiedSession reads the user's authentication details from the request // LoadCookiedSession reads the user's authentication details from the request
func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessions.SessionState, time.Duration, error) { func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
var age time.Duration return p.sessionStore.Load(req)
c, err := loadCookie(req, p.CookieName)
if err != nil {
// always http.ErrNoCookie
return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
}
val, timestamp, ok := cookie.Validate(c, p.CookieSeed, p.CookieExpire)
if !ok {
return nil, age, errors.New("Cookie Signature not valid")
}
session, err := p.provider.SessionFromCookie(val, p.CookieCipher)
if err != nil {
return nil, age, err
}
age = time.Now().Truncate(time.Second).Sub(timestamp)
return session, age, nil
} }
// SaveSession creates a new session cookie value and sets this on the response // SaveSession creates a new session cookie value and sets this on the response
func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error { func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
value, err := p.provider.CookieForSession(s, p.CookieCipher) return p.sessionStore.Save(rw, req, s)
if err != nil {
return err
}
p.SetSessionCookie(rw, req, value)
return nil
} }
// RobotsTxt disallows scraping pages from the OAuthProxy // RobotsTxt disallows scraping pages from the OAuthProxy
@ -636,20 +513,19 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
} }
// IsWhitelistedRequest is used to check if auth should be skipped for this request // IsWhitelistedRequest is used to check if auth should be skipped for this request
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) { func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
} }
// IsWhitelistedPath is used to check if the request path is allowed without auth // IsWhitelistedPath is used to check if the request path is allowed without auth
func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) { func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
for _, u := range p.compiledRegex { for _, u := range p.compiledRegex {
ok = u.MatchString(path) if u.MatchString(path) {
if ok { return true
return
} }
} }
return return false
} }
func getRemoteAddr(req *http.Request) (s string) { func getRemoteAddr(req *http.Request) (s string) {
@ -694,7 +570,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
user, ok := p.ManualSignIn(rw, req) user, ok := p.ManualSignIn(rw, req)
if ok { if ok {
session := &sessions.SessionState{User: user} session := &sessionsapi.SessionState{User: user}
p.SaveSession(rw, req, session) p.SaveSession(rw, req, session)
http.Redirect(rw, req, redirect, 302) http.Redirect(rw, req, redirect, 302)
} else { } else {
@ -714,7 +590,7 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
// OAuthStart starts the OAuth2 authentication flow // OAuthStart starts the OAuth2 authentication flow
func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
nonce, err := cookie.Nonce() nonce, err := encryption.Nonce()
if err != nil { if err != nil {
logger.Printf("Error obtaining nonce: %s", err.Error()) logger.Printf("Error obtaining nonce: %s", err.Error())
p.ErrorPage(rw, 500, "Internal Error", err.Error()) p.ErrorPage(rw, 500, "Internal Error", err.Error())
@ -793,57 +669,89 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
} }
http.Redirect(rw, req, redirect, 302) http.Redirect(rw, req, redirect, 302)
} else { } else {
logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Invalid authentication via OAuth2: unauthorized") logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account") p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
} }
} }
// AuthenticateOnly checks whether the user is currently logged in // AuthenticateOnly checks whether the user is currently logged in
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) { func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
status := p.Authenticate(rw, req) session, err := p.getAuthenticatedSession(rw, req)
if status == http.StatusAccepted { if err != nil {
rw.WriteHeader(http.StatusAccepted)
} else {
http.Error(rw, "unauthorized request", http.StatusUnauthorized) http.Error(rw, "unauthorized request", http.StatusUnauthorized)
return
} }
// we are authenticated
p.addHeadersForProxying(rw, req, session)
rw.WriteHeader(http.StatusAccepted)
} }
// Proxy proxies the user request if the user is authenticated else it prompts // Proxy proxies the user request if the user is authenticated else it prompts
// them to authenticate // them to authenticate
func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
status := p.Authenticate(rw, req) session, err := p.getAuthenticatedSession(rw, req)
if status == http.StatusInternalServerError { switch err {
p.ErrorPage(rw, http.StatusInternalServerError, case nil:
"Internal Error", "Internal Error") // we are authenticated
} else if status == http.StatusForbidden { p.addHeadersForProxying(rw, req, session)
p.serveMux.ServeHTTP(rw, req)
case ErrNeedsLogin:
// we need to send the user to a login screen
if isAjax(req) {
// no point redirecting an AJAX request
p.ErrorJSON(rw, http.StatusUnauthorized)
return
}
if p.SkipProviderButton { if p.SkipProviderButton {
p.OAuthStart(rw, req) p.OAuthStart(rw, req)
} else { } else {
p.SignInPage(rw, req, http.StatusForbidden) p.SignInPage(rw, req, http.StatusForbidden)
} }
} else if status == http.StatusUnauthorized {
p.ErrorJSON(rw, status) default:
} else { // unknown error
p.serveMux.ServeHTTP(rw, req) logger.Printf("Unexpected internal error: %s", err)
} p.ErrorPage(rw, http.StatusInternalServerError,
"Internal Error", "Internal Error")
} }
// Authenticate checks whether a user is authenticated }
func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
// Returns nil, ErrNeedsLogin if user needs to login.
// Set-Cookie headers may be set on the response as a side-effect of calling this method.
func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
var session *sessionsapi.SessionState
var err error
var saveSession, clearSession, revalidated bool var saveSession, clearSession, revalidated bool
remoteAddr := getRemoteAddr(req)
session, sessionAge, err := p.LoadCookiedSession(req) if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
session, err = p.GetJwtSession(req)
if err != nil {
logger.Printf("Error retrieving session from token in Authorization header: %s", err)
}
if session != nil {
saveSession = false
}
}
remoteAddr := getRemoteAddr(req)
if session == nil {
session, err = p.LoadCookiedSession(req)
if err != nil { if err != nil {
logger.Printf("Error loading cookied session: %s", err) logger.Printf("Error loading cookied session: %s", err)
} }
if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", sessionAge, session, p.CookieRefresh) if session != nil {
if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
saveSession = true saveSession = true
} }
var ok bool if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session) logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
clearSession = true clearSession = true
session = nil session = nil
@ -851,6 +759,8 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
saveSession = true saveSession = true
revalidated = true revalidated = true
} }
}
}
if session != nil && session.IsExpired() { if session != nil && session.IsExpired() {
logger.Printf("Removing session: token expired %s", session) logger.Printf("Removing session: token expired %s", session)
@ -868,18 +778,20 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
} }
} }
if session != nil && session.Email != "" && !p.Validator(session.Email) { if session != nil && session.Email != "" {
if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session) logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
session = nil session = nil
saveSession = false saveSession = false
clearSession = true clearSession = true
} }
}
if saveSession && session != nil { if saveSession && session != nil {
err = p.SaveSession(rw, req, session) err = p.SaveSession(rw, req, session)
if err != nil { if err != nil {
logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err) logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
return http.StatusInternalServerError return nil, err
} }
} }
@ -895,57 +807,83 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
} }
if session == nil { if session == nil {
// Check if is an ajax request and return unauthorized to avoid a redirect return nil, ErrNeedsLogin
// to the login page
if p.isAjax(req) {
return http.StatusUnauthorized
}
return http.StatusForbidden
} }
// At this point, the user is authenticated. proxy normally return session, nil
}
// addHeadersForProxying adds the appropriate headers the request / response for proxying
func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
if p.PassBasicAuth { if p.PassBasicAuth {
req.SetBasicAuth(session.User, p.BasicAuthPassword) req.SetBasicAuth(session.User, p.BasicAuthPassword)
req.Header["X-Forwarded-User"] = []string{session.User} req.Header["X-Forwarded-User"] = []string{session.User}
if session.Email != "" { if session.Email != "" {
req.Header["X-Forwarded-Email"] = []string{session.Email} req.Header["X-Forwarded-Email"] = []string{session.Email}
} else {
req.Header.Del("X-Forwarded-Email")
} }
} }
if p.PassUserHeaders { if p.PassUserHeaders {
req.Header["X-Forwarded-User"] = []string{session.User} req.Header["X-Forwarded-User"] = []string{session.User}
if session.Email != "" { if session.Email != "" {
req.Header["X-Forwarded-Email"] = []string{session.Email} req.Header["X-Forwarded-Email"] = []string{session.Email}
} else {
req.Header.Del("X-Forwarded-Email")
} }
} }
if p.SetXAuthRequest { if p.SetXAuthRequest {
rw.Header().Set("X-Auth-Request-User", session.User) rw.Header().Set("X-Auth-Request-User", session.User)
if session.Email != "" { if session.Email != "" {
rw.Header().Set("X-Auth-Request-Email", session.Email) rw.Header().Set("X-Auth-Request-Email", session.Email)
} else {
rw.Header().Del("X-Auth-Request-Email")
} }
if p.PassAccessToken && session.AccessToken != "" {
if p.PassAccessToken {
if session.AccessToken != "" {
rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken) rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
} else {
rw.Header().Del("X-Auth-Request-Access-Token")
} }
} }
if p.PassAccessToken && session.AccessToken != "" { }
if p.PassAccessToken {
if session.AccessToken != "" {
req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken} req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
} else {
req.Header.Del("X-Forwarded-Access-Token")
} }
if p.PassAuthorization && session.IDToken != "" { }
if p.PassAuthorization {
if session.IDToken != "" {
req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)} req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
} else {
req.Header.Del("Authorization")
} }
if p.SetAuthorization && session.IDToken != "" { }
if p.SetAuthorization {
if session.IDToken != "" {
rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken)) rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
} else {
rw.Header().Del("Authorization")
} }
}
if session.Email == "" { if session.Email == "" {
rw.Header().Set("GAP-Auth", session.User) rw.Header().Set("GAP-Auth", session.User)
} else { } else {
rw.Header().Set("GAP-Auth", session.Email) rw.Header().Set("GAP-Auth", session.Email)
} }
return http.StatusAccepted
} }
// CheckBasicAuth checks the requests Authorization header for basic auth // CheckBasicAuth checks the requests Authorization header for basic auth
// credentials and authenticates these against the proxies HtpasswdFile // credentials and authenticates these against the proxies HtpasswdFile
func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessions.SessionState, error) { func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
if p.HtpasswdFile == nil { if p.HtpasswdFile == nil {
return nil, nil return nil, nil
} }
@ -967,14 +905,14 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessions.SessionState,
} }
if p.HtpasswdFile.Validate(pair[0], pair[1]) { if p.HtpasswdFile.Validate(pair[0], pair[1]) {
logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File") logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
return &sessions.SessionState{User: pair[0]}, nil return &sessionsapi.SessionState{User: pair[0]}, nil
} }
logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File") logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
return nil, nil return nil, nil
} }
// isAjax checks if a request is an ajax request // isAjax checks if a request is an ajax request
func (p *OAuthProxy) isAjax(req *http.Request) bool { func isAjax(req *http.Request) bool {
acceptValues, ok := req.Header["accept"] acceptValues, ok := req.Header["accept"]
if !ok { if !ok {
acceptValues = req.Header["Accept"] acceptValues = req.Header["Accept"]
@ -988,8 +926,97 @@ func (p *OAuthProxy) isAjax(req *http.Request) bool {
return false return false
} }
// ErrorJSON returns the error code witht an application/json mime type // ErrorJSON returns the error code with an application/json mime type
func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) { func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
rw.Header().Set("Content-Type", applicationJSON) rw.Header().Set("Content-Type", applicationJSON)
rw.WriteHeader(code) rw.WriteHeader(code)
} }
// GetJwtSession loads a session based on a JWT token in the authorization header.
func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
rawBearerToken, err := p.findBearerToken(req)
if err != nil {
return nil, err
}
ctx := context.Background()
var session *sessionsapi.SessionState
for _, verifier := range p.jwtBearerVerifiers {
bearerToken, err := verifier.Verify(ctx, rawBearerToken)
if err != nil {
logger.Printf("failed to verify bearer token: %v", err)
continue
}
var claims struct {
Subject string `json:"sub"`
Email string `json:"email"`
Verified *bool `json:"email_verified"`
}
if err := bearerToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
}
if claims.Email == "" {
claims.Email = claims.Subject
}
if claims.Verified != nil && !*claims.Verified {
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
}
session = &sessionsapi.SessionState{
AccessToken: rawBearerToken,
IDToken: rawBearerToken,
RefreshToken: "",
ExpiresOn: bearerToken.Expiry,
Email: claims.Email,
User: claims.Email,
}
return session, nil
}
return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
}
// findBearerToken finds a valid JWT token from the Authorization header of a given request.
func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
auth := req.Header.Get("Authorization")
s := strings.SplitN(auth, " ", 2)
if len(s) != 2 {
return "", fmt.Errorf("invalid authorization header %s", auth)
}
jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
var rawBearerToken string
if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
rawBearerToken = s[1]
} else if s[0] == "Basic" {
// Check if we have a Bearer token masquerading in Basic
b, err := b64.StdEncoding.DecodeString(s[1])
if err != nil {
return "", err
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
return "", fmt.Errorf("invalid format %s", b)
}
user, password := pair[0], pair[1]
// check user, user+password, or just password for a token
if jwtRegex.MatchString(user) {
// Support blank passwords or magic `x-oauth-basic` passwords - nothing else
if password == "" || password == "x-oauth-basic" {
rawBearerToken = user
}
} else if jwtRegex.MatchString(password) {
// support passwords and ignore user
rawBearerToken = password
}
}
if rawBearerToken == "" {
return "", fmt.Errorf("no valid bearer token found in authorization header")
}
return rawBearerToken, nil
}

View File

@ -1,8 +1,10 @@
package main package main
import ( import (
"context"
"crypto" "crypto"
"encoding/base64" "encoding/base64"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@ -14,9 +16,11 @@ import (
"testing" "testing"
"time" "time"
"github.com/coreos/go-oidc"
"github.com/mbland/hmacauth" "github.com/mbland/hmacauth"
"github.com/pusher/oauth2_proxy/logger"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/logger"
"github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
"github.com/pusher/oauth2_proxy/providers" "github.com/pusher/oauth2_proxy/providers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -118,7 +122,7 @@ func TestNewReverseProxy(t *testing.T) {
backendHost := net.JoinHostPort(backendHostname, backendPort) backendHost := net.JoinHostPort(backendHostname, backendPort)
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/") proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
proxyHandler := NewReverseProxy(proxyURL, time.Second) proxyHandler := NewReverseProxy(proxyURL, &Options{FlushInterval: time.Second})
setProxyUpstreamHostHeader(proxyHandler, proxyURL) setProxyUpstreamHostHeader(proxyHandler, proxyURL)
frontend := httptest.NewServer(proxyHandler) frontend := httptest.NewServer(proxyHandler)
defer frontend.Close() defer frontend.Close()
@ -140,7 +144,7 @@ func TestEncodedSlashes(t *testing.T) {
defer backend.Close() defer backend.Close()
b, _ := url.Parse(backend.URL) b, _ := url.Parse(backend.URL)
proxyHandler := NewReverseProxy(b, time.Second) proxyHandler := NewReverseProxy(b, &Options{FlushInterval: time.Second})
setProxyDirector(proxyHandler) setProxyDirector(proxyHandler)
frontend := httptest.NewServer(proxyHandler) frontend := httptest.NewServer(proxyHandler)
defer frontend.Close() defer frontend.Close()
@ -159,9 +163,9 @@ func TestEncodedSlashes(t *testing.T) {
func TestRobotsTxt(t *testing.T) { func TestRobotsTxt(t *testing.T) {
opts := NewOptions() opts := NewOptions()
opts.ClientID = "bazquux" opts.ClientID = "asdlkjx"
opts.ClientSecret = "foobar" opts.ClientSecret = "alkgks"
opts.CookieSecret = "xyzzyplugh" opts.CookieSecret = "asdkugkj"
opts.Validate() opts.Validate()
proxy := NewOAuthProxy(opts, func(string) bool { return true }) proxy := NewOAuthProxy(opts, func(string) bool { return true })
@ -174,9 +178,9 @@ func TestRobotsTxt(t *testing.T) {
func TestIsValidRedirect(t *testing.T) { func TestIsValidRedirect(t *testing.T) {
opts := NewOptions() opts := NewOptions()
opts.ClientID = "bazquux" opts.ClientID = "skdlfj"
opts.ClientSecret = "foobar" opts.ClientSecret = "fgkdsgj"
opts.CookieSecret = "xyzzyplugh" opts.CookieSecret = "ljgiogbj"
// Should match domains that are exactly foo.bar and any subdomain of bar.foo // Should match domains that are exactly foo.bar and any subdomain of bar.foo
opts.WhitelistDomains = []string{"foo.bar", ".bar.foo"} opts.WhitelistDomains = []string{"foo.bar", ".bar.foo"}
opts.Validate() opts.Validate()
@ -227,6 +231,7 @@ type TestProvider struct {
*providers.ProviderData *providers.ProviderData
EmailAddress string EmailAddress string
ValidToken bool ValidToken bool
GroupValidator func(string) bool
} }
func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider { func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
@ -251,6 +256,9 @@ func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
Scope: "profile.email", Scope: "profile.email",
}, },
EmailAddress: emailAddress, EmailAddress: emailAddress,
GroupValidator: func(s string) bool {
return true
},
} }
} }
@ -262,6 +270,13 @@ func (tp *TestProvider) ValidateSessionState(session *sessions.SessionState) boo
return tp.ValidToken return tp.ValidToken
} }
func (tp *TestProvider) ValidateGroup(email string) bool {
if tp.GroupValidator != nil {
return tp.GroupValidator(email)
}
return true
}
func TestBasicAuthPassword(t *testing.T) { func TestBasicAuthPassword(t *testing.T) {
providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.Printf("%#v", r) logger.Printf("%#v", r)
@ -283,8 +298,8 @@ func TestBasicAuthPassword(t *testing.T) {
// The CookieSecret must be 32 bytes in order to create the AES // The CookieSecret must be 32 bytes in order to create the AES
// cipher. // cipher.
opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
opts.ClientID = "bazquux" opts.ClientID = "dlgkj"
opts.ClientSecret = "foobar" opts.ClientSecret = "alkgret"
opts.CookieSecure = false opts.CookieSecure = false
opts.PassBasicAuth = true opts.PassBasicAuth = true
opts.PassUserHeaders = true opts.PassUserHeaders = true
@ -377,8 +392,8 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
// The CookieSecret must be 32 bytes in order to create the AES // The CookieSecret must be 32 bytes in order to create the AES
// cipher. // cipher.
t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
t.opts.ClientID = "bazquux" t.opts.ClientID = "slgkj"
t.opts.ClientSecret = "foobar" t.opts.ClientSecret = "gfjgojl"
t.opts.CookieSecure = false t.opts.CookieSecure = false
t.opts.PassAccessToken = opts.PassAccessToken t.opts.PassAccessToken = opts.PassAccessToken
t.opts.Validate() t.opts.Validate()
@ -503,9 +518,9 @@ func NewSignInPageTest(skipProvider bool) *SignInPageTest {
var sipTest SignInPageTest var sipTest SignInPageTest
sipTest.opts = NewOptions() sipTest.opts = NewOptions()
sipTest.opts.CookieSecret = "foobar" sipTest.opts.CookieSecret = "adklsj2"
sipTest.opts.ClientID = "bazquux" sipTest.opts.ClientID = "lkdgj"
sipTest.opts.ClientSecret = "xyzzyplugh" sipTest.opts.ClientSecret = "sgiufgoi"
sipTest.opts.SkipProviderButton = skipProvider sipTest.opts.SkipProviderButton = skipProvider
sipTest.opts.Validate() sipTest.opts.Validate()
@ -600,12 +615,17 @@ type ProcessCookieTestOpts struct {
providerValidateCookieResponse bool providerValidateCookieResponse bool
} }
func NewProcessCookieTest(opts ProcessCookieTestOpts) *ProcessCookieTest { type OptionsModifier func(*Options)
func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) *ProcessCookieTest {
var pcTest ProcessCookieTest var pcTest ProcessCookieTest
pcTest.opts = NewOptions() pcTest.opts = NewOptions()
pcTest.opts.ClientID = "bazquux" for _, modifier := range modifiers {
pcTest.opts.ClientSecret = "xyzzyplugh" modifier(pcTest.opts)
}
pcTest.opts.ClientID = "asdfljk"
pcTest.opts.ClientSecret = "lkjfdsig"
pcTest.opts.CookieSecret = "0123456789abcdefabcd" pcTest.opts.CookieSecret = "0123456789abcdefabcd"
// First, set the CookieRefresh option so proxy.AesCipher is created, // First, set the CookieRefresh option so proxy.AesCipher is created,
// needed to encrypt the access_token. // needed to encrypt the access_token.
@ -634,32 +654,34 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
}) })
} }
func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) []*http.Cookie { func NewProcessCookieTestWithOptionsModifiers(modifiers ...OptionsModifier) *ProcessCookieTest {
return p.proxy.MakeSessionCookie(p.req, value, p.opts.CookieExpire, ref) return NewProcessCookieTest(ProcessCookieTestOpts{
providerValidateCookieResponse: true,
}, modifiers...)
} }
func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState, ref time.Time) error { func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState) error {
value, err := p.proxy.provider.CookieForSession(s, p.proxy.CookieCipher) err := p.proxy.SaveSession(p.rw, p.req, s)
if err != nil { if err != nil {
return err return err
} }
for _, c := range p.proxy.MakeSessionCookie(p.req, value, p.proxy.CookieExpire, ref) { for _, cookie := range p.rw.Result().Cookies() {
p.req.AddCookie(c) p.req.AddCookie(cookie)
} }
return nil return nil
} }
func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, time.Duration, error) { func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, error) {
return p.proxy.LoadCookiedSession(p.req) return p.proxy.LoadCookiedSession(p.req)
} }
func TestLoadCookiedSession(t *testing.T) { func TestLoadCookiedSession(t *testing.T) {
pcTest := NewProcessCookieTestWithDefaults() pcTest := NewProcessCookieTestWithDefaults()
startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token"} startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token", CreatedAt: time.Now()}
pcTest.SaveSession(startSession, time.Now()) pcTest.SaveSession(startSession)
session, _, err := pcTest.LoadCookiedSession() session, err := pcTest.LoadCookiedSession()
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, startSession.Email, session.Email) assert.Equal(t, startSession.Email, session.Email)
assert.Equal(t, "john.doe@example.com", session.User) assert.Equal(t, "john.doe@example.com", session.User)
@ -669,7 +691,7 @@ func TestLoadCookiedSession(t *testing.T) {
func TestProcessCookieNoCookieError(t *testing.T) { func TestProcessCookieNoCookieError(t *testing.T) {
pcTest := NewProcessCookieTestWithDefaults() pcTest := NewProcessCookieTestWithDefaults()
session, _, err := pcTest.LoadCookiedSession() session, err := pcTest.LoadCookiedSession()
assert.Equal(t, "Cookie \"_oauth2_proxy\" not present", err.Error()) assert.Equal(t, "Cookie \"_oauth2_proxy\" not present", err.Error())
if session != nil { if session != nil {
t.Errorf("expected nil session. got %#v", session) t.Errorf("expected nil session. got %#v", session)
@ -677,29 +699,31 @@ func TestProcessCookieNoCookieError(t *testing.T) {
} }
func TestProcessCookieRefreshNotSet(t *testing.T) { func TestProcessCookieRefreshNotSet(t *testing.T) {
pcTest := NewProcessCookieTestWithDefaults() pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
pcTest.proxy.CookieExpire = time.Duration(23) * time.Hour opts.CookieExpire = time.Duration(23) * time.Hour
})
reference := time.Now().Add(time.Duration(-2) * time.Hour) reference := time.Now().Add(time.Duration(-2) * time.Hour)
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
pcTest.SaveSession(startSession, reference) pcTest.SaveSession(startSession)
session, age, err := pcTest.LoadCookiedSession() session, err := pcTest.LoadCookiedSession()
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
if age < time.Duration(-2)*time.Hour { if session.Age() < time.Duration(-2)*time.Hour {
t.Errorf("cookie too young %v", age) t.Errorf("cookie too young %v", session.Age())
} }
assert.Equal(t, startSession.Email, session.Email) assert.Equal(t, startSession.Email, session.Email)
} }
func TestProcessCookieFailIfCookieExpired(t *testing.T) { func TestProcessCookieFailIfCookieExpired(t *testing.T) {
pcTest := NewProcessCookieTestWithDefaults() pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
pcTest.proxy.CookieExpire = time.Duration(24) * time.Hour opts.CookieExpire = time.Duration(24) * time.Hour
})
reference := time.Now().Add(time.Duration(25) * time.Hour * -1) reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
pcTest.SaveSession(startSession, reference) pcTest.SaveSession(startSession)
session, _, err := pcTest.LoadCookiedSession() session, err := pcTest.LoadCookiedSession()
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
if session != nil { if session != nil {
t.Errorf("expected nil session %#v", session) t.Errorf("expected nil session %#v", session)
@ -707,22 +731,23 @@ func TestProcessCookieFailIfCookieExpired(t *testing.T) {
} }
func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) { func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
pcTest := NewProcessCookieTestWithDefaults() pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
pcTest.proxy.CookieExpire = time.Duration(24) * time.Hour opts.CookieExpire = time.Duration(24) * time.Hour
})
reference := time.Now().Add(time.Duration(25) * time.Hour * -1) reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
pcTest.SaveSession(startSession, reference) pcTest.SaveSession(startSession)
pcTest.proxy.CookieRefresh = time.Hour pcTest.proxy.CookieRefresh = time.Hour
session, _, err := pcTest.LoadCookiedSession() session, err := pcTest.LoadCookiedSession()
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
if session != nil { if session != nil {
t.Errorf("expected nil session %#v", session) t.Errorf("expected nil session %#v", session)
} }
} }
func NewAuthOnlyEndpointTest() *ProcessCookieTest { func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) *ProcessCookieTest {
pcTest := NewProcessCookieTestWithDefaults() pcTest := NewProcessCookieTestWithOptionsModifiers(modifiers...)
pcTest.req, _ = http.NewRequest("GET", pcTest.req, _ = http.NewRequest("GET",
pcTest.opts.ProxyPrefix+"/auth", nil) pcTest.opts.ProxyPrefix+"/auth", nil)
return pcTest return pcTest
@ -731,8 +756,8 @@ func NewAuthOnlyEndpointTest() *ProcessCookieTest {
func TestAuthOnlyEndpointAccepted(t *testing.T) { func TestAuthOnlyEndpointAccepted(t *testing.T) {
test := NewAuthOnlyEndpointTest() test := NewAuthOnlyEndpointTest()
startSession := &sessions.SessionState{ startSession := &sessions.SessionState{
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
test.SaveSession(startSession, time.Now()) test.SaveSession(startSession)
test.proxy.ServeHTTP(test.rw, test.req) test.proxy.ServeHTTP(test.rw, test.req)
assert.Equal(t, http.StatusAccepted, test.rw.Code) assert.Equal(t, http.StatusAccepted, test.rw.Code)
@ -750,12 +775,13 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
} }
func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) { func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
test := NewAuthOnlyEndpointTest() test := NewAuthOnlyEndpointTest(func(opts *Options) {
test.proxy.CookieExpire = time.Duration(24) * time.Hour opts.CookieExpire = time.Duration(24) * time.Hour
})
reference := time.Now().Add(time.Duration(25) * time.Hour * -1) reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
startSession := &sessions.SessionState{ startSession := &sessions.SessionState{
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
test.SaveSession(startSession, reference) test.SaveSession(startSession)
test.proxy.ServeHTTP(test.rw, test.req) test.proxy.ServeHTTP(test.rw, test.req)
assert.Equal(t, http.StatusUnauthorized, test.rw.Code) assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
@ -766,8 +792,8 @@ func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) { func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
test := NewAuthOnlyEndpointTest() test := NewAuthOnlyEndpointTest()
startSession := &sessions.SessionState{ startSession := &sessions.SessionState{
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
test.SaveSession(startSession, time.Now()) test.SaveSession(startSession)
test.validateUser = false test.validateUser = false
test.proxy.ServeHTTP(test.rw, test.req) test.proxy.ServeHTTP(test.rw, test.req)
@ -776,6 +802,25 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
assert.Equal(t, "unauthorized request\n", string(bodyBytes)) assert.Equal(t, "unauthorized request\n", string(bodyBytes))
} }
func TestAuthOnlyEndpointUnauthorizedOnProviderGroupValidationFailure(t *testing.T) {
test := NewAuthOnlyEndpointTest()
startSession := &sessions.SessionState{
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
test.SaveSession(startSession)
provider := &TestProvider{
ValidToken: true,
GroupValidator: func(s string) bool {
return false
},
}
test.proxy.provider = provider
test.proxy.ServeHTTP(test.rw, test.req)
assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
assert.Equal(t, "unauthorized request\n", string(bodyBytes))
}
func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
var pcTest ProcessCookieTest var pcTest ProcessCookieTest
@ -797,8 +842,8 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
pcTest.opts.ProxyPrefix+"/auth", nil) pcTest.opts.ProxyPrefix+"/auth", nil)
startSession := &sessions.SessionState{ startSession := &sessions.SessionState{
User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token"} User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: time.Now()}
pcTest.SaveSession(startSession, time.Now()) pcTest.SaveSession(startSession)
pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req) pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
assert.Equal(t, http.StatusAccepted, pcTest.rw.Code) assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
@ -815,9 +860,9 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
opts := NewOptions() opts := NewOptions()
opts.Upstreams = append(opts.Upstreams, upstream.URL) opts.Upstreams = append(opts.Upstreams, upstream.URL)
opts.ClientID = "bazquux" opts.ClientID = "aljsal"
opts.ClientSecret = "foobar" opts.ClientSecret = "jglkfsdgj"
opts.CookieSecret = "xyzzyplugh" opts.CookieSecret = "dkfjgdls"
opts.SkipAuthPreflight = true opts.SkipAuthPreflight = true
opts.Validate() opts.Validate()
@ -930,11 +975,11 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
state := &sessions.SessionState{ state := &sessions.SessionState{
Email: "mbland@acm.org", AccessToken: "my_access_token"} Email: "mbland@acm.org", AccessToken: "my_access_token"}
value, err := proxy.provider.CookieForSession(state, proxy.CookieCipher) err = proxy.SaveSession(st.rw, req, state)
if err != nil { if err != nil {
panic(err) panic(err)
} }
for _, c := range proxy.MakeSessionCookie(req, value, proxy.CookieExpire, time.Now()) { for _, c := range st.rw.Result().Cookies() {
req.AddCookie(c) req.AddCookie(c)
} }
// This is used by the upstream to validate the signature. // This is used by the upstream to validate the signature.
@ -954,8 +999,8 @@ func TestNoRequestSignature(t *testing.T) {
func TestRequestSignatureGetRequest(t *testing.T) { func TestRequestSignatureGetRequest(t *testing.T) {
st := NewSignatureTest() st := NewSignatureTest()
defer st.Close() defer st.Close()
st.opts.SignatureKey = "sha1:foobar" st.opts.SignatureKey = "sha1:7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d"
st.MakeRequestWithExpectedKey("GET", "", "foobar") st.MakeRequestWithExpectedKey("GET", "", "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d")
assert.Equal(t, 200, st.rw.Code) assert.Equal(t, 200, st.rw.Code)
assert.Equal(t, st.rw.Body.String(), "signatures match") assert.Equal(t, st.rw.Body.String(), "signatures match")
} }
@ -963,9 +1008,9 @@ func TestRequestSignatureGetRequest(t *testing.T) {
func TestRequestSignaturePostRequest(t *testing.T) { func TestRequestSignaturePostRequest(t *testing.T) {
st := NewSignatureTest() st := NewSignatureTest()
defer st.Close() defer st.Close()
st.opts.SignatureKey = "sha1:foobar" st.opts.SignatureKey = "sha1:d90df39e2d19282840252612dd7c81421a372f61"
payload := `{ "hello": "world!" }` payload := `{ "hello": "world!" }`
st.MakeRequestWithExpectedKey("POST", payload, "foobar") st.MakeRequestWithExpectedKey("POST", payload, "d90df39e2d19282840252612dd7c81421a372f61")
assert.Equal(t, 200, st.rw.Code) assert.Equal(t, 200, st.rw.Code)
assert.Equal(t, st.rw.Body.String(), "signatures match") assert.Equal(t, st.rw.Body.String(), "signatures match")
} }
@ -1011,9 +1056,9 @@ type ajaxRequestTest struct {
func newAjaxRequestTest() *ajaxRequestTest { func newAjaxRequestTest() *ajaxRequestTest {
test := &ajaxRequestTest{} test := &ajaxRequestTest{}
test.opts = NewOptions() test.opts = NewOptions()
test.opts.CookieSecret = "foobar" test.opts.CookieSecret = "sdflsw"
test.opts.ClientID = "bazquux" test.opts.ClientID = "gkljfdl"
test.opts.ClientSecret = "xyzzyplugh" test.opts.ClientSecret = "sdflkjs"
test.opts.Validate() test.opts.Validate()
test.proxy = NewOAuthProxy(test.opts, func(email string) bool { test.proxy = NewOAuthProxy(test.opts, func(email string) bool {
return true return true
@ -1068,7 +1113,12 @@ func TestAjaxForbiddendRequest(t *testing.T) {
} }
func TestClearSplitCookie(t *testing.T) { func TestClearSplitCookie(t *testing.T) {
p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"} opts := NewOptions()
opts.CookieName = "oauth2"
opts.CookieDomain = "abc"
store, err := cookie.NewCookieSessionStore(&opts.SessionOptions, &opts.CookieOptions)
assert.Equal(t, err, nil)
p := OAuthProxy{CookieName: opts.CookieName, CookieDomain: opts.CookieDomain, sessionStore: store}
var rw = httptest.NewRecorder() var rw = httptest.NewRecorder()
req := httptest.NewRequest("get", "/", nil) req := httptest.NewRequest("get", "/", nil)
@ -1092,7 +1142,12 @@ func TestClearSplitCookie(t *testing.T) {
} }
func TestClearSingleCookie(t *testing.T) { func TestClearSingleCookie(t *testing.T) {
p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"} opts := NewOptions()
opts.CookieName = "oauth2"
opts.CookieDomain = "abc"
store, err := cookie.NewCookieSessionStore(&opts.SessionOptions, &opts.CookieOptions)
assert.Equal(t, err, nil)
p := OAuthProxy{CookieName: opts.CookieName, CookieDomain: opts.CookieDomain, sessionStore: store}
var rw = httptest.NewRecorder() var rw = httptest.NewRecorder()
req := httptest.NewRequest("get", "/", nil) req := httptest.NewRequest("get", "/", nil)
@ -1110,3 +1165,173 @@ func TestClearSingleCookie(t *testing.T) {
assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries") assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries")
} }
type NoOpKeySet struct {
}
func (NoOpKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) {
splitStrings := strings.Split(jwt, ".")
payloadString := splitStrings[1]
jsonString, err := base64.RawURLEncoding.DecodeString(payloadString)
return []byte(jsonString), err
}
func TestGetJwtSession(t *testing.T) {
/* token payload:
{
"sub": "1234567890",
"aud": "https://test.myapp.com",
"name": "John Doe",
"email": "john@example.com",
"iss": "https://issuer.example.com",
"iat": 1553691215,
"exp": 1912151821
}
*/
goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
"WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
"E1LCJleHAiOjE5MTIxNTE4MjF9." +
"rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
"OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
keyset := NoOpKeySet{}
verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
&oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
test := NewAuthOnlyEndpointTest(func(opts *Options) {
opts.PassAuthorization = true
opts.SetAuthorization = true
opts.SetXAuthRequest = true
opts.SkipJwtBearerTokens = true
opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier)
})
tp, _ := test.proxy.provider.(*TestProvider)
tp.GroupValidator = func(s string) bool {
return true
}
authHeader := fmt.Sprintf("Bearer %s", goodJwt)
test.req.Header = map[string][]string{
"Authorization": {authHeader},
}
// Bearer
session, _ := test.proxy.GetJwtSession(test.req)
assert.Equal(t, session.User, "john@example.com")
assert.Equal(t, session.Email, "john@example.com")
assert.Equal(t, session.ExpiresOn, time.Unix(1912151821, 0))
assert.Equal(t, session.IDToken, goodJwt)
test.proxy.ServeHTTP(test.rw, test.req)
if test.rw.Code >= 400 {
t.Fatalf("expected 3xx got %d", test.rw.Code)
}
// Check PassAuthorization, should overwrite Basic header
assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "john@example.com")
assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
// SetAuthorization and SetXAuthRequest
assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "john@example.com")
assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
}
func TestJwtUnauthorizedOnGroupValidationFailure(t *testing.T) {
goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
"WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
"E1LCJleHAiOjE5MTIxNTE4MjF9." +
"rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
"OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
keyset := NoOpKeySet{}
verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
&oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
test := NewAuthOnlyEndpointTest(func(opts *Options) {
opts.PassAuthorization = true
opts.SetAuthorization = true
opts.SetXAuthRequest = true
opts.SkipJwtBearerTokens = true
opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier)
})
tp, _ := test.proxy.provider.(*TestProvider)
// Verify ValidateGroup fails JWT authorization
tp.GroupValidator = func(s string) bool {
return false
}
authHeader := fmt.Sprintf("Bearer %s", goodJwt)
test.req.Header = map[string][]string{
"Authorization": {authHeader},
}
test.proxy.ServeHTTP(test.rw, test.req)
if test.rw.Code != http.StatusUnauthorized {
t.Fatalf("expected 401 got %d", test.rw.Code)
}
}
func TestFindJwtBearerToken(t *testing.T) {
p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"}
getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}}
validToken := "eyJfoobar.eyJfoobar.12345asdf"
var token string
// Bearer
getReq.Header = map[string][]string{
"Authorization": {fmt.Sprintf("Bearer %s", validToken)},
}
token, _ = p.findBearerToken(getReq)
assert.Equal(t, validToken, token)
// Basic - no password
getReq.SetBasicAuth(token, "")
token, _ = p.findBearerToken(getReq)
assert.Equal(t, validToken, token)
// Basic - sentinel password
getReq.SetBasicAuth(token, "x-oauth-basic")
token, _ = p.findBearerToken(getReq)
assert.Equal(t, validToken, token)
// Basic - any username, password matching jwt pattern
getReq.SetBasicAuth("any-username-you-could-wish-for", token)
token, _ = p.findBearerToken(getReq)
assert.Equal(t, validToken, token)
failures := []string{
// Too many parts
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
// Not enough parts
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA",
// Invalid encrypted key
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.//////.dGVzdA.dGVzdA.dGVzdA",
// Invalid IV
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.//////.dGVzdA.dGVzdA",
// Invalid ciphertext
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.//////.dGVzdA",
// Invalid tag
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.//////",
// Invalid header
"W10.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
// Invalid header
"######.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
// Missing alc/enc params
"e30.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
}
for _, failure := range failures {
getReq.Header = map[string][]string{
"Authorization": {fmt.Sprintf("Bearer %s", failure)},
}
_, err := p.findBearerToken(getReq)
assert.Error(t, err)
}
fmt.Printf("%s", token)
}

View File

@ -17,8 +17,11 @@ import (
oidc "github.com/coreos/go-oidc" oidc "github.com/coreos/go-oidc"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/mbland/hmacauth" "github.com/mbland/hmacauth"
"github.com/pusher/oauth2_proxy/logger"
"github.com/pusher/oauth2_proxy/pkg/apis/options" "github.com/pusher/oauth2_proxy/pkg/apis/options"
sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/pusher/oauth2_proxy/pkg/logger"
"github.com/pusher/oauth2_proxy/pkg/sessions"
"github.com/pusher/oauth2_proxy/providers" "github.com/pusher/oauth2_proxy/providers"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
@ -26,28 +29,34 @@ import (
// Options holds Configuration Options that can be set by Command Line Flag, // Options holds Configuration Options that can be set by Command Line Flag,
// or Config File // or Config File
type Options struct { type Options struct {
ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy-prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"` ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"`
PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"`
ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"` ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"`
HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"` HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"`
HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"` HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"`
RedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"` RedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"`
ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"` ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"` ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"` TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"`
TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"`
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"` AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"`
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"` AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"`
BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"`
EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"` EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"`
WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"` WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"` GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"`
GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"` GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"`
GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group" env:"OAUTH2_PROXY_GITLAB_GROUP"`
GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"` GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"`
GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"` GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"`
GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"` GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"`
HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"` HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"`
DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"` DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"`
CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"` CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"`
Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"`
Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"` Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"`
// Embed CookieOptions // Embed CookieOptions
@ -58,6 +67,8 @@ type Options struct {
Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"` Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"` SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"`
ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"`
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"` PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"` BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"` PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
@ -65,6 +76,7 @@ type Options struct {
SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"` SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"`
PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"` PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"`
SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY"` SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY"`
SSLUpstreamInsecureSkipVerify bool `flag:"ssl-upstream-insecure-skip-verify" cfg:"ssl_upstream_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_UPSTREAM_INSECURE_SKIP_VERIFY"`
SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest" env:"OAUTH2_PROXY_SET_XAUTHREQUEST"` SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest" env:"OAUTH2_PROXY_SET_XAUTHREQUEST"`
SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header" env:"OAUTH2_PROXY_SET_AUTHORIZATION_HEADER"` SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header" env:"OAUTH2_PROXY_SET_AUTHORIZATION_HEADER"`
PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header" env:"OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER"` PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header" env:"OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER"`
@ -75,8 +87,9 @@ type Options struct {
// potential overrides. // potential overrides.
Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"` Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"`
OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"` OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"`
SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_SKIP_OIDC_DISCOVERY"` InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"`
OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_OIDC_JWKS_URL"` SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_PROXY_SKIP_OIDC_DISCOVERY"`
OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_PROXY_OIDC_JWKS_URL"`
LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"` LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"`
RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"` RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"`
ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"` ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"`
@ -86,19 +99,20 @@ type Options struct {
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"`
// Configuration values for logging // Configuration values for logging
LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_LOGGING_FILENAME"` LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_PROXY_LOGGING_FILENAME"`
LoggingMaxSize int `flag:"logging-max-size" cfg:"logging_max_size" env:"OAUTH2_LOGGING_MAX_SIZE"` LoggingMaxSize int `flag:"logging-max-size" cfg:"logging_max_size" env:"OAUTH2_PROXY_LOGGING_MAX_SIZE"`
LoggingMaxAge int `flag:"logging-max-age" cfg:"logging_max_age" env:"OAUTH2_LOGGING_MAX_AGE"` LoggingMaxAge int `flag:"logging-max-age" cfg:"logging_max_age" env:"OAUTH2_PROXY_LOGGING_MAX_AGE"`
LoggingMaxBackups int `flag:"logging-max-backups" cfg:"logging_max_backups" env:"OAUTH2_LOGGING_MAX_BACKUPS"` LoggingMaxBackups int `flag:"logging-max-backups" cfg:"logging_max_backups" env:"OAUTH2_PROXY_LOGGING_MAX_BACKUPS"`
LoggingLocalTime bool `flag:"logging-local-time" cfg:"logging_local_time" env:"OAUTH2_LOGGING_LOCAL_TIME"` LoggingLocalTime bool `flag:"logging-local-time" cfg:"logging_local_time" env:"OAUTH2_PROXY_LOGGING_LOCAL_TIME"`
LoggingCompress bool `flag:"logging-compress" cfg:"logging_compress" env:"OAUTH2_LOGGING_COMPRESS"` LoggingCompress bool `flag:"logging-compress" cfg:"logging_compress" env:"OAUTH2_PROXY_LOGGING_COMPRESS"`
StandardLogging bool `flag:"standard-logging" cfg:"standard_logging" env:"OAUTH2_STANDARD_LOGGING"` StandardLogging bool `flag:"standard-logging" cfg:"standard_logging" env:"OAUTH2_PROXY_STANDARD_LOGGING"`
StandardLoggingFormat string `flag:"standard-logging-format" cfg:"standard_logging_format" env:"OAUTH2_STANDARD_LOGGING_FORMAT"` StandardLoggingFormat string `flag:"standard-logging-format" cfg:"standard_logging_format" env:"OAUTH2_PROXY_STANDARD_LOGGING_FORMAT"`
RequestLogging bool `flag:"request-logging" cfg:"request_logging" env:"OAUTH2_REQUEST_LOGGING"` RequestLogging bool `flag:"request-logging" cfg:"request_logging" env:"OAUTH2_PROXY_REQUEST_LOGGING"`
RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format" env:"OAUTH2_REQUEST_LOGGING_FORMAT"` RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format" env:"OAUTH2_PROXY_REQUEST_LOGGING_FORMAT"`
AuthLogging bool `flag:"auth-logging" cfg:"auth_logging" env:"OAUTH2_LOGGING_AUTH_LOGGING"` ExcludeLoggingPaths string `flag:"exclude-logging-paths" cfg:"exclude_logging_paths" env:"OAUTH2_PROXY_EXCLUDE_LOGGING_PATHS"`
AuthLoggingFormat string `flag:"auth-logging-format" cfg:"auth_logging_format" env:"OAUTH2_AUTH_LOGGING_FORMAT"` SilencePingLogging bool `flag:"silence-ping-logging" cfg:"silence_ping_logging" env:"OAUTH2_PROXY_SILENCE_PING_LOGGING"`
AuthLogging bool `flag:"auth-logging" cfg:"auth_logging" env:"OAUTH2_PROXY_LOGGING_AUTH_LOGGING"`
AuthLoggingFormat string `flag:"auth-logging-format" cfg:"auth_logging_format" env:"OAUTH2_PROXY_AUTH_LOGGING_FORMAT"`
SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"` SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"`
AcrValues string `flag:"acr-values" cfg:"acr_values" env:"OAUTH2_PROXY_ACR_VALUES"` AcrValues string `flag:"acr-values" cfg:"acr_values" env:"OAUTH2_PROXY_ACR_VALUES"`
JWTKey string `flag:"jwt-key" cfg:"jwt_key" env:"OAUTH2_PROXY_JWT_KEY"` JWTKey string `flag:"jwt-key" cfg:"jwt_key" env:"OAUTH2_PROXY_JWT_KEY"`
@ -111,8 +125,10 @@ type Options struct {
proxyURLs []*url.URL proxyURLs []*url.URL
CompiledRegex []*regexp.Regexp CompiledRegex []*regexp.Regexp
provider providers.Provider provider providers.Provider
sessionStore sessionsapi.SessionStore
signatureData *SignatureData signatureData *SignatureData
oidcVerifier *oidc.IDTokenVerifier oidcVerifier *oidc.IDTokenVerifier
jwtBearerVerifiers []*oidc.IDTokenVerifier
} }
// SignatureData holds hmacauth signature hash and key // SignatureData holds hmacauth signature hash and key
@ -125,6 +141,7 @@ type SignatureData struct {
func NewOptions() *Options { func NewOptions() *Options {
return &Options{ return &Options{
ProxyPrefix: "/oauth2", ProxyPrefix: "/oauth2",
PingPath: "/ping",
ProxyWebSockets: true, ProxyWebSockets: true,
HTTPAddress: "127.0.0.1:4180", HTTPAddress: "127.0.0.1:4180",
HTTPSAddress: ":443", HTTPSAddress: ":443",
@ -136,6 +153,9 @@ func NewOptions() *Options {
CookieExpire: time.Duration(168) * time.Hour, CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(0), CookieRefresh: time.Duration(0),
}, },
SessionOptions: options.SessionOptions{
Type: "cookie",
},
SetXAuthRequest: false, SetXAuthRequest: false,
SkipAuthPreflight: false, SkipAuthPreflight: false,
PassBasicAuth: true, PassBasicAuth: true,
@ -145,6 +165,7 @@ func NewOptions() *Options {
SetAuthorization: false, SetAuthorization: false,
PassAuthorization: false, PassAuthorization: false,
ApprovalPrompt: "force", ApprovalPrompt: "force",
InsecureOIDCAllowUnverifiedEmail: false,
SkipOIDCDiscovery: false, SkipOIDCDiscovery: false,
LoggingFilename: "", LoggingFilename: "",
LoggingMaxSize: 100, LoggingMaxSize: 100,
@ -152,6 +173,8 @@ func NewOptions() *Options {
LoggingMaxBackups: 0, LoggingMaxBackups: 0,
LoggingLocalTime: true, LoggingLocalTime: true,
LoggingCompress: false, LoggingCompress: false,
ExcludeLoggingPaths: "",
SilencePingLogging: false,
StandardLogging: true, StandardLogging: true,
StandardLoggingFormat: logger.DefaultStandardLoggingFormat, StandardLoggingFormat: logger.DefaultStandardLoggingFormat,
RequestLogging: true, RequestLogging: true,
@ -161,6 +184,12 @@ func NewOptions() *Options {
} }
} }
// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier.
type jwtIssuer struct {
issuerURI string
audience string
}
func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) { func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
parsed, err := url.Parse(toParse) parsed, err := url.Parse(toParse)
if err != nil { if err != nil {
@ -237,6 +266,25 @@ func (o *Options) Validate() error {
} }
} }
if o.SkipJwtBearerTokens {
// If we are using an oidc provider, go ahead and add that provider to the list
if o.oidcVerifier != nil {
o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier)
}
// Configure extra issuers
if len(o.ExtraJwtIssuers) > 0 {
var jwtIssuers []jwtIssuer
jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs)
for _, jwtIssuer := range jwtIssuers {
verifier, err := newVerifierFromJwtIssuer(jwtIssuer)
if err != nil {
msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err))
}
o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier)
}
}
}
o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs) o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
for _, u := range o.Upstreams { for _, u := range o.Upstreams {
@ -261,7 +309,8 @@ func (o *Options) Validate() error {
} }
msgs = parseProviderInfo(o, msgs) msgs = parseProviderInfo(o, msgs)
if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) { var cipher *encryption.Cipher
if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.CookieRefresh != time.Duration(0)) {
validCookieSecretSize := false validCookieSecretSize := false
for _, i := range []int{16, 24, 32} { for _, i := range []int{16, 24, 32} {
if len(secretBytes(o.CookieSecret)) == i { if len(secretBytes(o.CookieSecret)) == i {
@ -283,8 +332,22 @@ func (o *Options) Validate() error {
"pass_access_token == true or "+ "pass_access_token == true or "+
"cookie_refresh != 0, but is %d bytes.%s", "cookie_refresh != 0, but is %d bytes.%s",
len(secretBytes(o.CookieSecret)), suffix)) len(secretBytes(o.CookieSecret)), suffix))
} else {
var err error
cipher, err = encryption.NewCipher(secretBytes(o.CookieSecret))
if err != nil {
msgs = append(msgs, fmt.Sprintf("cookie-secret error: %v", err))
} }
} }
}
o.SessionOptions.Cipher = cipher
sessionStore, err := sessions.NewSessionStore(&o.SessionOptions, &o.CookieOptions)
if err != nil {
msgs = append(msgs, fmt.Sprintf("error initialising session storage: %v", err))
} else {
o.sessionStore = sessionStore
}
if o.CookieRefresh >= o.CookieExpire { if o.CookieRefresh >= o.CookieExpire {
msgs = append(msgs, fmt.Sprintf( msgs = append(msgs, fmt.Sprintf(
@ -336,6 +399,8 @@ func parseProviderInfo(o *Options, msgs []string) []string {
p.Configure(o.AzureTenant) p.Configure(o.AzureTenant)
case *providers.GitHubProvider: case *providers.GitHubProvider:
p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
case *providers.KeycloakProvider:
p.SetGroup(o.KeycloakGroup)
case *providers.GoogleProvider: case *providers.GoogleProvider:
if o.GoogleServiceAccountJSON != "" { if o.GoogleServiceAccountJSON != "" {
file, err := os.Open(o.GoogleServiceAccountJSON) file, err := os.Open(o.GoogleServiceAccountJSON)
@ -345,12 +410,39 @@ func parseProviderInfo(o *Options, msgs []string) []string {
p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file) p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
} }
} }
case *providers.BitbucketProvider:
p.SetTeam(o.BitbucketTeam)
p.SetRepository(o.BitbucketRepository)
case *providers.OIDCProvider: case *providers.OIDCProvider:
p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
if o.oidcVerifier == nil { if o.oidcVerifier == nil {
msgs = append(msgs, "oidc provider requires an oidc issuer URL") msgs = append(msgs, "oidc provider requires an oidc issuer URL")
} else { } else {
p.Verifier = o.oidcVerifier p.Verifier = o.oidcVerifier
} }
case *providers.GitLabProvider:
p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
p.Group = o.GitLabGroup
p.EmailDomains = o.EmailDomains
if o.oidcVerifier != nil {
p.Verifier = o.oidcVerifier
} else {
// Initialize with default verifier for gitlab.com
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, "https://gitlab.com")
if err != nil {
msgs = append(msgs, "failed to initialize oidc provider for gitlab.com")
} else {
p.Verifier = provider.Verifier(&oidc.Config{
ClientID: o.ClientID,
})
p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
p.RedeemURL, msgs = parseURL(provider.Endpoint().TokenURL, "redeem", msgs)
}
}
case *providers.LoginGovProvider: case *providers.LoginGovProvider:
p.AcrValues = o.AcrValues p.AcrValues = o.AcrValues
p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs) p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
@ -404,10 +496,49 @@ func parseSignatureKey(o *Options, msgs []string) []string {
return append(msgs, "unsupported signature hash algorithm: "+ return append(msgs, "unsupported signature hash algorithm: "+
o.SignatureKey) o.SignatureKey)
} }
o.signatureData = &SignatureData{hash, secretKey} o.signatureData = &SignatureData{hash: hash, key: secretKey}
return msgs return msgs
} }
// parseJwtIssuers takes in an array of strings in the form of issuer=audience
// and parses to an array of jwtIssuer structs.
func parseJwtIssuers(issuers []string, msgs []string) ([]jwtIssuer, []string) {
var parsedIssuers []jwtIssuer
for _, jwtVerifier := range issuers {
components := strings.Split(jwtVerifier, "=")
if len(components) < 2 {
msgs = append(msgs, fmt.Sprintf("invalid jwt verifier uri=audience spec: %s", jwtVerifier))
continue
}
uri, audience := components[0], strings.Join(components[1:], "=")
parsedIssuers = append(parsedIssuers, jwtIssuer{issuerURI: uri, audience: audience})
}
return parsedIssuers, msgs
}
// newVerifierFromJwtIssuer takes in issuer information in jwtIssuer info and returns
// a verifier for that issuer.
func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error) {
config := &oidc.Config{
ClientID: jwtIssuer.audience,
}
// Try as an OpenID Connect Provider first
var verifier *oidc.IDTokenVerifier
provider, err := oidc.NewProvider(context.Background(), jwtIssuer.issuerURI)
if err != nil {
// Try as JWKS URI
jwksURI := strings.TrimSuffix(jwtIssuer.issuerURI, "/") + "/.well-known/jwks.json"
_, err := http.NewRequest("GET", jwksURI, nil)
if err != nil {
return nil, err
}
verifier = oidc.NewVerifier(jwtIssuer.issuerURI, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
} else {
verifier = provider.Verifier(config)
}
return verifier, nil
}
func validateCookieName(o *Options, msgs []string) []string { func validateCookieName(o *Options, msgs []string) []string {
cookie := &http.Cookie{Name: o.CookieName} cookie := &http.Cookie{Name: o.CookieName}
if cookie.String() == "" { if cookie.String() == "" {
@ -478,6 +609,14 @@ func setupLogger(o *Options, msgs []string) []string {
logger.SetAuthTemplate(o.AuthLoggingFormat) logger.SetAuthTemplate(o.AuthLoggingFormat)
logger.SetReqTemplate(o.RequestLoggingFormat) logger.SetReqTemplate(o.RequestLoggingFormat)
excludePaths := make([]string, 0)
excludePaths = append(excludePaths, strings.Split(o.ExcludeLoggingPaths, ",")...)
if o.SilencePingLogging {
excludePaths = append(excludePaths, o.PingPath)
}
logger.SetExcludePaths(excludePaths)
if !o.LoggingLocalTime { if !o.LoggingLocalTime {
logger.SetFlags(logger.Flags() | logger.LUTC) logger.SetFlags(logger.Flags() | logger.LUTC)
} }

View File

@ -1,9 +1,13 @@
package options package options
import "github.com/pusher/oauth2_proxy/pkg/encryption"
// SessionOptions contains configuration options for the SessionStore providers. // SessionOptions contains configuration options for the SessionStore providers.
type SessionOptions struct { type SessionOptions struct {
Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"` Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
Cipher *encryption.Cipher
CookieStoreOptions CookieStoreOptions
RedisStoreOptions
} }
// CookieSessionStoreType is used to indicate the CookieSessionStore should be // CookieSessionStoreType is used to indicate the CookieSessionStore should be
@ -12,3 +16,15 @@ var CookieSessionStoreType = "cookie"
// CookieStoreOptions contains configuration options for the CookieSessionStore. // CookieStoreOptions contains configuration options for the CookieSessionStore.
type CookieStoreOptions struct{} type CookieStoreOptions struct{}
// RedisSessionStoreType is used to indicate the RedisSessionStore should be
// used for storing sessions.
var RedisSessionStoreType = "redis"
// RedisStoreOptions contains configuration options for the RedisSessionStore.
type RedisStoreOptions struct {
RedisConnectionURL string `flag:"redis-connection-url" cfg:"redis_connection_url" env:"OAUTH2_PROXY_REDIS_CONNECTION_URL"`
UseSentinel bool `flag:"redis-use-sentinel" cfg:"redis_use_sentinel" env:"OAUTH2_PROXY_REDIS_USE_SENTINEL"`
SentinelMasterName string `flag:"redis-sentinel-master-name" cfg:"redis_sentinel_master_name" env:"OAUTH2_PROXY_REDIS_SENTINEL_MASTER_NAME"`
SentinelConnectionURLs []string `flag:"redis-sentinel-connection-urls" cfg:"redis_sentinel_connection_urls" env:"OAUTH2_PROXY_REDIS_SENTINEL_CONNECTION_URLS"`
}

View File

@ -7,13 +7,14 @@ import (
"strings" "strings"
"time" "time"
"github.com/pusher/oauth2_proxy/cookie" "github.com/pusher/oauth2_proxy/pkg/encryption"
) )
// SessionState is used to store information about the currently authenticated user session // SessionState is used to store information about the currently authenticated user session
type SessionState struct { type SessionState struct {
AccessToken string `json:",omitempty"` AccessToken string `json:",omitempty"`
IDToken string `json:",omitempty"` IDToken string `json:",omitempty"`
CreatedAt time.Time `json:"-"`
ExpiresOn time.Time `json:"-"` ExpiresOn time.Time `json:"-"`
RefreshToken string `json:",omitempty"` RefreshToken string `json:",omitempty"`
Email string `json:",omitempty"` Email string `json:",omitempty"`
@ -23,6 +24,7 @@ type SessionState struct {
// SessionStateJSON is used to encode SessionState into JSON without exposing time.Time zero value // SessionStateJSON is used to encode SessionState into JSON without exposing time.Time zero value
type SessionStateJSON struct { type SessionStateJSON struct {
*SessionState *SessionState
CreatedAt *time.Time `json:",omitempty"`
ExpiresOn *time.Time `json:",omitempty"` ExpiresOn *time.Time `json:",omitempty"`
} }
@ -34,6 +36,14 @@ func (s *SessionState) IsExpired() bool {
return false return false
} }
// Age returns the age of a session
func (s *SessionState) Age() time.Duration {
if !s.CreatedAt.IsZero() {
return time.Now().Truncate(time.Second).Sub(s.CreatedAt)
}
return 0
}
// String constructs a summary of the session state // String constructs a summary of the session state
func (s *SessionState) String() string { func (s *SessionState) String() string {
o := fmt.Sprintf("Session{email:%s user:%s", s.Email, s.User) o := fmt.Sprintf("Session{email:%s user:%s", s.Email, s.User)
@ -43,6 +53,9 @@ func (s *SessionState) String() string {
if s.IDToken != "" { if s.IDToken != "" {
o += " id_token:true" o += " id_token:true"
} }
if !s.CreatedAt.IsZero() {
o += fmt.Sprintf(" created:%s", s.CreatedAt)
}
if !s.ExpiresOn.IsZero() { if !s.ExpiresOn.IsZero() {
o += fmt.Sprintf(" expires:%s", s.ExpiresOn) o += fmt.Sprintf(" expires:%s", s.ExpiresOn)
} }
@ -53,7 +66,7 @@ func (s *SessionState) String() string {
} }
// EncodeSessionState returns string representation of the current session // EncodeSessionState returns string representation of the current session
func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) { func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) {
var ss SessionState var ss SessionState
if c == nil { if c == nil {
// Store only Email and User when cipher is unavailable // Store only Email and User when cipher is unavailable
@ -95,6 +108,9 @@ func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {
} }
// Embed SessionState and ExpiresOn pointer into SessionStateJSON // Embed SessionState and ExpiresOn pointer into SessionStateJSON
ssj := &SessionStateJSON{SessionState: &ss} ssj := &SessionStateJSON{SessionState: &ss}
if !ss.CreatedAt.IsZero() {
ssj.CreatedAt = &ss.CreatedAt
}
if !ss.ExpiresOn.IsZero() { if !ss.ExpiresOn.IsZero() {
ssj.ExpiresOn = &ss.ExpiresOn ssj.ExpiresOn = &ss.ExpiresOn
} }
@ -117,7 +133,7 @@ func legacyDecodeSessionStatePlain(v string) (*SessionState, error) {
// legacyDecodeSessionState attempts to decode the session state string // legacyDecodeSessionState attempts to decode the session state string
// generated by v3.1.0 or older // generated by v3.1.0 or older
func legacyDecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) { func legacyDecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
chunks := strings.Split(v, "|") chunks := strings.Split(v, "|")
if c == nil { if c == nil {
@ -160,13 +176,16 @@ func legacyDecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error)
} }
// DecodeSessionState decodes the session cookie string into a SessionState // DecodeSessionState decodes the session cookie string into a SessionState
func DecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) { func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
var ssj SessionStateJSON var ssj SessionStateJSON
var ss *SessionState var ss *SessionState
err := json.Unmarshal([]byte(v), &ssj) err := json.Unmarshal([]byte(v), &ssj)
if err == nil && ssj.SessionState != nil { if err == nil && ssj.SessionState != nil {
// Extract SessionState and ExpiresOn value from SessionStateJSON // Extract SessionState and CreatedAt,ExpiresOn value from SessionStateJSON
ss = ssj.SessionState ss = ssj.SessionState
if ssj.CreatedAt != nil {
ss.CreatedAt = *ssj.CreatedAt
}
if ssj.ExpiresOn != nil { if ssj.ExpiresOn != nil {
ss.ExpiresOn = *ssj.ExpiresOn ss.ExpiresOn = *ssj.ExpiresOn
} }
@ -184,14 +203,14 @@ func DecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) {
User: ss.User, User: ss.User,
} }
} else { } else {
// Backward compatibility with using unecrypted Email // Backward compatibility with using unencrypted Email
if ss.Email != "" { if ss.Email != "" {
decryptedEmail, errEmail := c.Decrypt(ss.Email) decryptedEmail, errEmail := c.Decrypt(ss.Email)
if errEmail == nil { if errEmail == nil {
ss.Email = decryptedEmail ss.Email = decryptedEmail
} }
} }
// Backward compatibility with using unecrypted User // Backward compatibility with using unencrypted User
if ss.User != "" { if ss.User != "" {
decryptedUser, errUser := c.Decrypt(ss.User) decryptedUser, errUser := c.Decrypt(ss.User)
if errUser == nil { if errUser == nil {

View File

@ -5,8 +5,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -14,14 +14,15 @@ const secret = "0123456789abcdefghijklmnopqrstuv"
const altSecret = "0000000000abcdefghijklmnopqrstuv" const altSecret = "0000000000abcdefghijklmnopqrstuv"
func TestSessionStateSerialization(t *testing.T) { func TestSessionStateSerialization(t *testing.T) {
c, err := cookie.NewCipher([]byte(secret)) c, err := encryption.NewCipher([]byte(secret))
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
c2, err := cookie.NewCipher([]byte(altSecret)) c2, err := encryption.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
s := &sessions.SessionState{ s := &sessions.SessionState{
Email: "user@domain.com", Email: "user@domain.com",
AccessToken: "token1234", AccessToken: "token1234",
IDToken: "rawtoken1234", IDToken: "rawtoken1234",
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321", RefreshToken: "refresh4321",
} }
@ -35,6 +36,7 @@ func TestSessionStateSerialization(t *testing.T) {
assert.Equal(t, s.Email, ss.Email) assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.AccessToken, ss.AccessToken) assert.Equal(t, s.AccessToken, ss.AccessToken)
assert.Equal(t, s.IDToken, ss.IDToken) assert.Equal(t, s.IDToken, ss.IDToken)
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix()) assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.Equal(t, s.RefreshToken, ss.RefreshToken) assert.Equal(t, s.RefreshToken, ss.RefreshToken)
@ -44,6 +46,7 @@ func TestSessionStateSerialization(t *testing.T) {
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.NotEqual(t, "user@domain.com", ss.User) assert.NotEqual(t, "user@domain.com", ss.User)
assert.NotEqual(t, s.Email, ss.Email) assert.NotEqual(t, s.Email, ss.Email)
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix()) assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.NotEqual(t, s.AccessToken, ss.AccessToken) assert.NotEqual(t, s.AccessToken, ss.AccessToken)
assert.NotEqual(t, s.IDToken, ss.IDToken) assert.NotEqual(t, s.IDToken, ss.IDToken)
@ -51,14 +54,15 @@ func TestSessionStateSerialization(t *testing.T) {
} }
func TestSessionStateSerializationWithUser(t *testing.T) { func TestSessionStateSerializationWithUser(t *testing.T) {
c, err := cookie.NewCipher([]byte(secret)) c, err := encryption.NewCipher([]byte(secret))
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
c2, err := cookie.NewCipher([]byte(altSecret)) c2, err := encryption.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
s := &sessions.SessionState{ s := &sessions.SessionState{
User: "just-user", User: "just-user",
Email: "user@domain.com", Email: "user@domain.com",
AccessToken: "token1234", AccessToken: "token1234",
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321", RefreshToken: "refresh4321",
} }
@ -71,6 +75,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
assert.Equal(t, s.User, ss.User) assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email) assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.AccessToken, ss.AccessToken) assert.Equal(t, s.AccessToken, ss.AccessToken)
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix()) assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.Equal(t, s.RefreshToken, ss.RefreshToken) assert.Equal(t, s.RefreshToken, ss.RefreshToken)
@ -80,6 +85,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.NotEqual(t, s.User, ss.User) assert.NotEqual(t, s.User, ss.User)
assert.NotEqual(t, s.Email, ss.Email) assert.NotEqual(t, s.Email, ss.Email)
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix()) assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.NotEqual(t, s.AccessToken, ss.AccessToken) assert.NotEqual(t, s.AccessToken, ss.AccessToken)
assert.NotEqual(t, s.RefreshToken, ss.RefreshToken) assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
@ -89,6 +95,7 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
s := &sessions.SessionState{ s := &sessions.SessionState{
Email: "user@domain.com", Email: "user@domain.com",
AccessToken: "token1234", AccessToken: "token1234",
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321", RefreshToken: "refresh4321",
} }
@ -109,6 +116,7 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
User: "just-user", User: "just-user",
Email: "user@domain.com", Email: "user@domain.com",
AccessToken: "token1234", AccessToken: "token1234",
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321", RefreshToken: "refresh4321",
} }
@ -138,7 +146,7 @@ func TestExpired(t *testing.T) {
type testCase struct { type testCase struct {
sessions.SessionState sessions.SessionState
Encoded string Encoded string
Cipher *cookie.Cipher Cipher *encryption.Cipher
Error bool Error bool
} }
@ -147,6 +155,7 @@ type testCase struct {
// Currently only tests without cipher here because we have no way to mock // Currently only tests without cipher here because we have no way to mock
// the random generator used in EncodeSessionState. // the random generator used in EncodeSessionState.
func TestEncodeSessionState(t *testing.T) { func TestEncodeSessionState(t *testing.T) {
c := time.Now()
e := time.Now().Add(time.Duration(1) * time.Hour) e := time.Now().Add(time.Duration(1) * time.Hour)
testCases := []testCase{ testCases := []testCase{
@ -163,6 +172,7 @@ func TestEncodeSessionState(t *testing.T) {
User: "just-user", User: "just-user",
AccessToken: "token1234", AccessToken: "token1234",
IDToken: "rawtoken1234", IDToken: "rawtoken1234",
CreatedAt: c,
ExpiresOn: e, ExpiresOn: e,
RefreshToken: "refresh4321", RefreshToken: "refresh4321",
}, },
@ -185,12 +195,15 @@ func TestEncodeSessionState(t *testing.T) {
// TestDecodeSessionState testssessions.DecodeSessionState with the test vector // TestDecodeSessionState testssessions.DecodeSessionState with the test vector
func TestDecodeSessionState(t *testing.T) { func TestDecodeSessionState(t *testing.T) {
created := time.Now()
createdJSON, _ := created.MarshalJSON()
createdString := string(createdJSON)
e := time.Now().Add(time.Duration(1) * time.Hour) e := time.Now().Add(time.Duration(1) * time.Hour)
eJSON, _ := e.MarshalJSON() eJSON, _ := e.MarshalJSON()
eString := string(eJSON) eString := string(eJSON)
eUnix := e.Unix() eUnix := e.Unix()
c, err := cookie.NewCipher([]byte(secret)) c, err := encryption.NewCipher([]byte(secret))
assert.NoError(t, err) assert.NoError(t, err)
testCases := []testCase{ testCases := []testCase{
@ -219,7 +232,7 @@ func TestDecodeSessionState(t *testing.T) {
Email: "user@domain.com", Email: "user@domain.com",
User: "just-user", User: "just-user",
}, },
Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","ExpiresOn":%s}`, eString), Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
}, },
{ {
SessionState: sessions.SessionState{ SessionState: sessions.SessionState{
@ -227,10 +240,11 @@ func TestDecodeSessionState(t *testing.T) {
User: "just-user", User: "just-user",
AccessToken: "token1234", AccessToken: "token1234",
IDToken: "rawtoken1234", IDToken: "rawtoken1234",
CreatedAt: created,
ExpiresOn: e, ExpiresOn: e,
RefreshToken: "refresh4321", RefreshToken: "refresh4321",
}, },
Encoded: fmt.Sprintf(`{"Email":"FsKKYrTWZWrxSOAqA/fTNAUZS5QWCqOBjuAbBlbVOw==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw==","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","ExpiresOn":%s}`, eString), Encoded: fmt.Sprintf(`{"Email":"FsKKYrTWZWrxSOAqA/fTNAUZS5QWCqOBjuAbBlbVOw==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw==","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
Cipher: c, Cipher: c,
}, },
{ {
@ -316,3 +330,14 @@ func TestDecodeSessionState(t *testing.T) {
} }
} }
} }
func TestSessionStateAge(t *testing.T) {
ss := &sessions.SessionState{}
// Created at unset so should be 0
assert.Equal(t, time.Duration(0), ss.Age())
// Set CreatedAt to 1 hour ago
ss.CreatedAt = time.Now().Add(-1 * time.Hour)
assert.Equal(t, time.Hour, ss.Age().Round(time.Minute))
}

View File

@ -6,7 +6,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/apis/options"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
// MakeCookie constructs a cookie from the given parameters, // MakeCookie constructs a cookie from the given parameters,
@ -32,3 +33,9 @@ func MakeCookie(req *http.Request, name string, value string, path string, domai
Expires: now.Add(expiration), Expires: now.Add(expiration),
} }
} }
// MakeCookieFromOptions constructs a cookie based on the givemn *options.CookieOptions,
// value and creation time
func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.CookieOptions, expiration time.Duration, now time.Time) *http.Cookie {
return MakeCookie(req, name, value, opts.CookiePath, opts.CookieDomain, opts.CookieHTTPOnly, opts.CookieSecure, expiration, now)
}

View File

@ -1,4 +1,4 @@
package cookie package encryption
import ( import (
"crypto/aes" "crypto/aes"

View File

@ -1,4 +1,4 @@
package cookie package encryption
import ( import (
"encoding/base64" "encoding/base64"

View File

@ -1,4 +1,4 @@
package cookie package encryption
import ( import (
"crypto/rand" "crypto/rand"

View File

@ -88,6 +88,7 @@ type Logger struct {
stdEnabled bool stdEnabled bool
authEnabled bool authEnabled bool
reqEnabled bool reqEnabled bool
excludePaths map[string]struct{}
stdLogTemplate *template.Template stdLogTemplate *template.Template
authTemplate *template.Template authTemplate *template.Template
reqTemplate *template.Template reqTemplate *template.Template
@ -101,6 +102,7 @@ func New(flag int) *Logger {
stdEnabled: true, stdEnabled: true,
authEnabled: true, authEnabled: true,
reqEnabled: true, reqEnabled: true,
excludePaths: nil,
stdLogTemplate: template.Must(template.New("std-log").Parse(DefaultStandardLoggingFormat)), stdLogTemplate: template.Must(template.New("std-log").Parse(DefaultStandardLoggingFormat)),
authTemplate: template.Must(template.New("auth-log").Parse(DefaultAuthLoggingFormat)), authTemplate: template.Must(template.New("auth-log").Parse(DefaultAuthLoggingFormat)),
reqTemplate: template.Must(template.New("req-log").Parse(DefaultRequestLoggingFormat)), reqTemplate: template.Must(template.New("req-log").Parse(DefaultRequestLoggingFormat)),
@ -177,6 +179,10 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url.
return return
} }
if _, ok := l.excludePaths[url.Path]; ok {
return
}
duration := float64(time.Now().Sub(ts)) / float64(time.Second) duration := float64(time.Now().Sub(ts)) / float64(time.Second)
if username == "" { if username == "" {
@ -302,6 +308,16 @@ func (l *Logger) SetReqEnabled(e bool) {
l.reqEnabled = e l.reqEnabled = e
} }
// SetExcludePaths sets the paths to exclude from logging.
func (l *Logger) SetExcludePaths(s []string) {
l.mu.Lock()
defer l.mu.Unlock()
l.excludePaths = make(map[string]struct{})
for _, p := range s {
l.excludePaths[p] = struct{}{}
}
}
// SetStandardTemplate sets the template for standard logging. // SetStandardTemplate sets the template for standard logging.
func (l *Logger) SetStandardTemplate(t string) { func (l *Logger) SetStandardTemplate(t string) {
l.mu.Lock() l.mu.Lock()
@ -365,6 +381,11 @@ func SetReqEnabled(e bool) {
std.SetReqEnabled(e) std.SetReqEnabled(e)
} }
// SetExcludePaths sets the path to exclude from logging, eg: health checks
func SetExcludePaths(s []string) {
std.SetExcludePaths(s)
}
// SetStandardTemplate sets the template for standard logging for // SetStandardTemplate sets the template for standard logging for
// the standard logger. // the standard logger.
func SetStandardTemplate(t string) { func SetStandardTemplate(t string) {

View File

@ -1,4 +1,4 @@
package api package requests
import ( import (
"encoding/json" "encoding/json"
@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"github.com/bitly/go-simplejson" "github.com/bitly/go-simplejson"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
) )
// Request parses the request body into a simplejson.Json object // Request parses the request body into a simplejson.Json object

View File

@ -1,4 +1,4 @@
package api package requests
import ( import (
"io/ioutil" "io/ioutil"

View File

@ -8,10 +8,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/options" "github.com/pusher/oauth2_proxy/pkg/apis/options"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/cookies" "github.com/pusher/oauth2_proxy/pkg/cookies"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/pusher/oauth2_proxy/pkg/sessions/utils" "github.com/pusher/oauth2_proxy/pkg/sessions/utils"
) )
@ -27,36 +27,33 @@ var _ sessions.SessionStore = &SessionStore{}
// SessionStore is an implementation of the sessions.SessionStore // SessionStore is an implementation of the sessions.SessionStore
// interface that stores sessions in client side cookies // interface that stores sessions in client side cookies
type SessionStore struct { type SessionStore struct {
CookieCipher *cookie.Cipher CookieOptions *options.CookieOptions
CookieDomain string CookieCipher *encryption.Cipher
CookieExpire time.Duration
CookieHTTPOnly bool
CookieName string
CookiePath string
CookieSecret string
CookieSecure bool
} }
// Save takes a sessions.SessionState and stores the information from it // Save takes a sessions.SessionState and stores the information from it
// within Cookies set on the HTTP response writer // within Cookies set on the HTTP response writer
func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error { func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error {
if ss.CreatedAt.IsZero() {
ss.CreatedAt = time.Now()
}
value, err := utils.CookieForSession(ss, s.CookieCipher) value, err := utils.CookieForSession(ss, s.CookieCipher)
if err != nil { if err != nil {
return err return err
} }
s.setSessionCookie(rw, req, value) s.setSessionCookie(rw, req, value, ss.CreatedAt)
return nil return nil
} }
// Load reads sessions.SessionState information from Cookies within the // Load reads sessions.SessionState information from Cookies within the
// HTTP request object // HTTP request object
func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) { func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
c, err := loadCookie(req, s.CookieName) c, err := loadCookie(req, s.CookieOptions.CookieName)
if err != nil { if err != nil {
// always http.ErrNoCookie // always http.ErrNoCookie
return nil, fmt.Errorf("Cookie %q not present", s.CookieName) return nil, fmt.Errorf("Cookie %q not present", s.CookieOptions.CookieName)
} }
val, _, ok := cookie.Validate(c, s.CookieSecret, s.CookieExpire) val, _, ok := encryption.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire)
if !ok { if !ok {
return nil, errors.New("Cookie Signature not valid") return nil, errors.New("Cookie Signature not valid")
} }
@ -74,11 +71,11 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
var cookies []*http.Cookie var cookies []*http.Cookie
// matches CookieName, CookieName_<number> // matches CookieName, CookieName_<number>
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieName)) var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieOptions.CookieName))
for _, c := range req.Cookies() { for _, c := range req.Cookies() {
if cookieNameRegex.MatchString(c.Name) { if cookieNameRegex.MatchString(c.Name) {
clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1) clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())
http.SetCookie(rw, clearCookie) http.SetCookie(rw, clearCookie)
cookies = append(cookies, clearCookie) cookies = append(cookies, clearCookie)
@ -89,60 +86,42 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
} }
// setSessionCookie adds the user's session cookie to the response // setSessionCookie adds the user's session cookie to the response
func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string) { func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string, created time.Time) {
for _, c := range s.makeSessionCookie(req, val, s.CookieExpire, time.Now()) { for _, c := range s.makeSessionCookie(req, val, created) {
http.SetCookie(rw, c) http.SetCookie(rw, c)
} }
} }
// makeSessionCookie creates an http.Cookie containing the authenticated user's // makeSessionCookie creates an http.Cookie containing the authenticated user's
// authentication details // authentication details
func (s *SessionStore) makeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie { func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie {
if value != "" { if value != "" {
value = cookie.SignedValue(s.CookieSecret, s.CookieName, value, now) value = encryption.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now)
} }
c := s.makeCookie(req, s.CookieName, value, expiration) c := s.makeCookie(req, s.CookieOptions.CookieName, value, s.CookieOptions.CookieExpire, now)
if len(c.Value) > 4096-len(s.CookieName) { if len(c.Value) > 4096-len(s.CookieOptions.CookieName) {
return splitCookie(c) return splitCookie(c)
} }
return []*http.Cookie{c} return []*http.Cookie{c}
} }
func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie { func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
return cookies.MakeCookie( return cookies.MakeCookieFromOptions(
req, req,
name, name,
value, value,
s.CookiePath, s.CookieOptions,
s.CookieDomain,
s.CookieHTTPOnly,
s.CookieSecure,
expiration, expiration,
time.Now(), now,
) )
} }
// NewCookieSessionStore initialises a new instance of the SessionStore from // NewCookieSessionStore initialises a new instance of the SessionStore from
// the configuration given // the configuration given
func NewCookieSessionStore(opts options.CookieStoreOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) { func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
var cipher *cookie.Cipher
if len(cookieOpts.CookieSecret) > 0 {
var err error
cipher, err = cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
if err != nil {
return nil, fmt.Errorf("unable to create cipher: %v", err)
}
}
return &SessionStore{ return &SessionStore{
CookieCipher: cipher, CookieCipher: opts.Cipher,
CookieDomain: cookieOpts.CookieDomain, CookieOptions: cookieOpts,
CookieExpire: cookieOpts.CookieExpire,
CookieHTTPOnly: cookieOpts.CookieHTTPOnly,
CookieName: cookieOpts.CookieName,
CookiePath: cookieOpts.CookiePath,
CookieSecret: cookieOpts.CookieSecret,
CookieSecure: cookieOpts.CookieSecure,
}, nil }, nil
} }

View File

@ -0,0 +1,305 @@
package redis
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/go-redis/redis"
"github.com/pusher/oauth2_proxy/pkg/apis/options"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/cookies"
"github.com/pusher/oauth2_proxy/pkg/encryption"
)
// TicketData is a structure representing the ticket used in server session storage
type TicketData struct {
TicketID string
Secret []byte
}
// SessionStore is an implementation of the sessions.SessionStore
// interface that stores sessions in redis
type SessionStore struct {
CookieCipher *encryption.Cipher
CookieOptions *options.CookieOptions
Client *redis.Client
}
// NewRedisSessionStore initialises a new instance of the SessionStore from
// the configuration given
func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
client, err := newRedisClient(opts.RedisStoreOptions)
if err != nil {
return nil, fmt.Errorf("error constructing redis client: %v", err)
}
rs := &SessionStore{
Client: client,
CookieCipher: opts.Cipher,
CookieOptions: cookieOpts,
}
return rs, nil
}
func newRedisClient(opts options.RedisStoreOptions) (*redis.Client, error) {
if opts.UseSentinel {
client := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: opts.SentinelMasterName,
SentinelAddrs: opts.SentinelConnectionURLs,
})
return client, nil
}
opt, err := redis.ParseURL(opts.RedisConnectionURL)
if err != nil {
return nil, fmt.Errorf("unable to parse redis url: %s", err)
}
client := redis.NewClient(opt)
return client, nil
}
// Save takes a sessions.SessionState and stores the information from it
// to redies, and adds a new ticket cookie on the HTTP response writer
func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
if s.CreatedAt.IsZero() {
s.CreatedAt = time.Now()
}
// Old sessions that we are refreshing would have a request cookie
// New sessions don't, so we ignore the error. storeValue will check requestCookie
requestCookie, _ := req.Cookie(store.CookieOptions.CookieName)
value, err := s.EncodeSessionState(store.CookieCipher)
if err != nil {
return err
}
ticketString, err := store.storeValue(value, store.CookieOptions.CookieExpire, requestCookie)
if err != nil {
return err
}
ticketCookie := store.makeCookie(
req,
ticketString,
store.CookieOptions.CookieExpire,
s.CreatedAt,
)
http.SetCookie(rw, ticketCookie)
return nil
}
// Load reads sessions.SessionState information from a ticket
// cookie within the HTTP request object
func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
requestCookie, err := req.Cookie(store.CookieOptions.CookieName)
if err != nil {
return nil, fmt.Errorf("error loading session: %s", err)
}
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
if !ok {
return nil, fmt.Errorf("Cookie Signature not valid")
}
session, err := store.loadSessionFromString(val)
if err != nil {
return nil, fmt.Errorf("error loading session: %s", err)
}
return session, nil
}
// loadSessionFromString loads the session based on the ticket value
func (store *SessionStore) loadSessionFromString(value string) (*sessions.SessionState, error) {
ticket, err := decodeTicket(store.CookieOptions.CookieName, value)
if err != nil {
return nil, err
}
result, err := store.Client.Get(ticket.asHandle(store.CookieOptions.CookieName)).Result()
if err != nil {
return nil, err
}
resultBytes := []byte(result)
block, err := aes.NewCipher(ticket.Secret)
if err != nil {
return nil, err
}
// Use secret as the IV too, because each entry has it's own key
stream := cipher.NewCFBDecrypter(block, ticket.Secret)
stream.XORKeyStream(resultBytes, resultBytes)
session, err := sessions.DecodeSessionState(string(resultBytes), store.CookieCipher)
if err != nil {
return nil, err
}
return session, nil
}
// Clear clears any saved session information for a given ticket cookie
// from redis, and then clears the session
func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
// We go ahead and clear the cookie first, always.
clearCookie := store.makeCookie(
req,
"",
time.Hour*-1,
time.Now(),
)
http.SetCookie(rw, clearCookie)
// If there was an existing cookie we should clear the session in redis
requestCookie, err := req.Cookie(store.CookieOptions.CookieName)
if err != nil && err == http.ErrNoCookie {
// No existing cookie so can't clear redis
return nil
} else if err != nil {
return fmt.Errorf("error retrieving cookie: %v", err)
}
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
if !ok {
return fmt.Errorf("Cookie Signature not valid")
}
// We only return an error if we had an issue with redis
// If there's an issue decoding the ticket, ignore it
ticket, _ := decodeTicket(store.CookieOptions.CookieName, val)
if ticket != nil {
_, err := store.Client.Del(ticket.asHandle(store.CookieOptions.CookieName)).Result()
if err != nil {
return fmt.Errorf("error clearing cookie from redis: %s", err)
}
}
return nil
}
// makeCookie makes a cookie, signing the value if present
func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
if value != "" {
value = encryption.SignedValue(store.CookieOptions.CookieSecret, store.CookieOptions.CookieName, value, now)
}
return cookies.MakeCookieFromOptions(
req,
store.CookieOptions.CookieName,
value,
store.CookieOptions,
expires,
now,
)
}
func (store *SessionStore) storeValue(value string, expiration time.Duration, requestCookie *http.Cookie) (string, error) {
ticket, err := store.getTicket(requestCookie)
if err != nil {
return "", fmt.Errorf("error getting ticket: %v", err)
}
ciphertext := make([]byte, len(value))
block, err := aes.NewCipher(ticket.Secret)
if err != nil {
return "", fmt.Errorf("error initiating cipher block %s", err)
}
// Use secret as the Initialization Vector too, because each entry has it's own key
stream := cipher.NewCFBEncrypter(block, ticket.Secret)
stream.XORKeyStream(ciphertext, []byte(value))
handle := ticket.asHandle(store.CookieOptions.CookieName)
err = store.Client.Set(handle, ciphertext, expiration).Err()
if err != nil {
return "", err
}
return ticket.encodeTicket(store.CookieOptions.CookieName), nil
}
// getTicket retrieves an existing ticket from the cookie if present,
// or creates a new ticket
func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, error) {
if requestCookie == nil {
return newTicket()
}
// An existing cookie exists, try to retrieve the ticket
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
if !ok {
// Cookie is invalid, create a new ticket
return newTicket()
}
// Valid cookie, decode the ticket
ticket, err := decodeTicket(store.CookieOptions.CookieName, val)
if err != nil {
// If we can't decode the ticket we have to create a new one
return newTicket()
}
return ticket, nil
}
func newTicket() (*TicketData, error) {
rawID := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, rawID); err != nil {
return nil, fmt.Errorf("failed to create new ticket ID %s", err)
}
// ticketID is hex encoded
ticketID := fmt.Sprintf("%x", rawID)
secret := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, secret); err != nil {
return nil, fmt.Errorf("failed to create initialization vector %s", err)
}
ticket := &TicketData{
TicketID: ticketID,
Secret: secret,
}
return ticket, nil
}
func (ticket *TicketData) asHandle(prefix string) string {
return fmt.Sprintf("%s-%s", prefix, ticket.TicketID)
}
func decodeTicket(cookieName string, ticketString string) (*TicketData, error) {
prefix := cookieName + "-"
if !strings.HasPrefix(ticketString, prefix) {
return nil, fmt.Errorf("failed to decode ticket handle")
}
trimmedTicket := strings.TrimPrefix(ticketString, prefix)
ticketParts := strings.Split(trimmedTicket, ".")
if len(ticketParts) != 2 {
return nil, fmt.Errorf("failed to decode ticket")
}
ticketID, secretBase64 := ticketParts[0], ticketParts[1]
// ticketID must be a hexadecimal string
_, err := hex.DecodeString(ticketID)
if err != nil {
return nil, fmt.Errorf("server ticket failed sanity checks")
}
secret, err := base64.RawURLEncoding.DecodeString(secretBase64)
if err != nil {
return nil, fmt.Errorf("failed to decode initialization vector %s", err)
}
ticketData := &TicketData{
TicketID: ticketID,
Secret: secret,
}
return ticketData, nil
}
func (ticket *TicketData) encodeTicket(prefix string) string {
handle := ticket.asHandle(prefix)
ticketString := handle + "." + base64.RawURLEncoding.EncodeToString(ticket.Secret)
return ticketString
}

View File

@ -6,13 +6,16 @@ import (
"github.com/pusher/oauth2_proxy/pkg/apis/options" "github.com/pusher/oauth2_proxy/pkg/apis/options"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/sessions/cookie" "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
"github.com/pusher/oauth2_proxy/pkg/sessions/redis"
) )
// NewSessionStore creates a SessionStore from the provided configuration // NewSessionStore creates a SessionStore from the provided configuration
func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) { func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
switch opts.Type { switch opts.Type {
case options.CookieSessionStoreType: case options.CookieSessionStoreType:
return cookie.NewCookieSessionStore(opts.CookieStoreOptions, cookieOpts) return cookie.NewCookieSessionStore(opts, cookieOpts)
case options.RedisSessionStoreType:
return redis.NewRedisSessionStore(opts, cookieOpts)
default: default:
return nil, fmt.Errorf("unknown session store type '%s'", opts.Type) return nil, fmt.Errorf("unknown session store type '%s'", opts.Type)
} }

View File

@ -5,16 +5,22 @@ import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"strings"
"testing" "testing"
"time" "time"
"github.com/alicebob/miniredis"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/pusher/oauth2_proxy/pkg/apis/options" "github.com/pusher/oauth2_proxy/pkg/apis/options"
sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions" sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/cookies" "github.com/pusher/oauth2_proxy/pkg/cookies"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/pusher/oauth2_proxy/pkg/sessions" "github.com/pusher/oauth2_proxy/pkg/sessions"
"github.com/pusher/oauth2_proxy/pkg/sessions/cookie" sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
"github.com/pusher/oauth2_proxy/pkg/sessions/redis"
"github.com/pusher/oauth2_proxy/pkg/sessions/utils"
) )
func TestSessionStore(t *testing.T) { func TestSessionStore(t *testing.T) {
@ -30,6 +36,7 @@ var _ = Describe("NewSessionStore", func() {
var response *httptest.ResponseRecorder var response *httptest.ResponseRecorder
var session *sessionsapi.SessionState var session *sessionsapi.SessionState
var ss sessionsapi.SessionStore var ss sessionsapi.SessionStore
var mr *miniredis.Miniredis
CheckCookieOptions := func() { CheckCookieOptions := func() {
Context("the cookies returned", func() { Context("the cookies returned", func() {
@ -72,11 +79,67 @@ var _ = Describe("NewSessionStore", func() {
} }
}) })
It("have a signature timestamp matching session.CreatedAt", func() {
for _, cookie := range cookies {
if cookie.Value != "" {
parts := strings.Split(cookie.Value, "|")
Expect(parts).To(HaveLen(3))
Expect(parts[1]).To(Equal(strconv.Itoa(int(session.CreatedAt.Unix()))))
}
}
})
}) })
} }
SessionStoreInterfaceTests := func() { // The following should only be for server stores
PersistentSessionStoreTests := func() {
Context("when Clear is called on a persistent store", func() {
var resultCookies []*http.Cookie
BeforeEach(func() {
req := httptest.NewRequest("GET", "http://example.com/", nil)
saveResp := httptest.NewRecorder()
err := ss.Save(saveResp, req, session)
Expect(err).ToNot(HaveOccurred())
resultCookies = saveResp.Result().Cookies()
for _, c := range resultCookies {
request.AddCookie(c)
}
err = ss.Clear(response, request)
Expect(err).ToNot(HaveOccurred())
})
Context("attempting to Load", func() {
var loadedAfterClear *sessionsapi.SessionState
var loadErr error
BeforeEach(func() {
loadReq := httptest.NewRequest("GET", "http://example.com/", nil)
for _, c := range resultCookies {
loadReq.AddCookie(c)
}
loadedAfterClear, loadErr = ss.Load(loadReq)
})
It("returns an empty session", func() {
Expect(loadedAfterClear).To(BeNil())
})
It("returns an error", func() {
Expect(loadErr).To(HaveOccurred())
})
})
CheckCookieOptions()
})
}
SessionStoreInterfaceTests := func(persistent bool) {
Context("when Save is called", func() { Context("when Save is called", func() {
Context("with no existing session", func() {
BeforeEach(func() { BeforeEach(func() {
err := ss.Save(response, request, session) err := ss.Save(response, request, session)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
@ -86,23 +149,72 @@ var _ = Describe("NewSessionStore", func() {
Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty()) Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty())
}) })
It("Ensures the session CreatedAt is not zero", func() {
Expect(session.CreatedAt.IsZero()).To(BeFalse())
})
})
Context("with a broken session", func() {
BeforeEach(func() {
By("Using a valid cookie with a different providers session encoding")
broken := "BrokenSessionFromADifferentSessionImplementation"
value := encryption.SignedValue(cookieOpts.CookieSecret, cookieOpts.CookieName, broken, time.Now())
cookie := cookies.MakeCookieFromOptions(request, cookieOpts.CookieName, value, cookieOpts, cookieOpts.CookieExpire, time.Now())
request.AddCookie(cookie)
err := ss.Save(response, request, session)
Expect(err).ToNot(HaveOccurred())
})
It("sets a `set-cookie` header in the response", func() {
Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty())
})
It("Ensures the session CreatedAt is not zero", func() {
Expect(session.CreatedAt.IsZero()).To(BeFalse())
})
})
Context("with an expired saved session", func() {
var err error
BeforeEach(func() {
By("saving a session")
req := httptest.NewRequest("GET", "http://example.com/", nil)
saveResp := httptest.NewRecorder()
err = ss.Save(saveResp, req, session)
Expect(err).ToNot(HaveOccurred())
By("and clearing the session")
for _, c := range saveResp.Result().Cookies() {
request.AddCookie(c)
}
clearResp := httptest.NewRecorder()
err = ss.Clear(clearResp, request)
Expect(err).ToNot(HaveOccurred())
By("then saving a request with the cleared session")
err = ss.Save(response, request, session)
})
It("no error should occur", func() {
Expect(err).ToNot(HaveOccurred())
})
})
CheckCookieOptions() CheckCookieOptions()
}) })
Context("when Clear is called", func() { Context("when Clear is called", func() {
BeforeEach(func() { BeforeEach(func() {
cookie := cookies.MakeCookie(request, req := httptest.NewRequest("GET", "http://example.com/", nil)
cookieOpts.CookieName, saveResp := httptest.NewRecorder()
"foo", err := ss.Save(saveResp, req, session)
cookieOpts.CookiePath, Expect(err).ToNot(HaveOccurred())
cookieOpts.CookieDomain,
cookieOpts.CookieHTTPOnly, for _, c := range saveResp.Result().Cookies() {
cookieOpts.CookieSecure, request.AddCookie(c)
cookieOpts.CookieExpire, }
time.Now(), err = ss.Clear(response, request)
)
request.AddCookie(cookie)
err := ss.Clear(response, request)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
@ -114,16 +226,10 @@ var _ = Describe("NewSessionStore", func() {
}) })
Context("when Load is called", func() { Context("when Load is called", func() {
LoadSessionTests := func() {
var loadedSession *sessionsapi.SessionState var loadedSession *sessionsapi.SessionState
BeforeEach(func() { BeforeEach(func() {
req := httptest.NewRequest("GET", "http://example.com/", nil) var err error
resp := httptest.NewRecorder()
err := ss.Save(resp, req, session)
Expect(err).ToNot(HaveOccurred())
for _, cookie := range resp.Result().Cookies() {
request.AddCookie(cookie)
}
loadedSession, err = ss.Load(request) loadedSession, err = ss.Load(request)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
@ -138,19 +244,80 @@ var _ = Describe("NewSessionStore", func() {
// Can't compare time.Time using Equal() so remove ExpiresOn from sessions // Can't compare time.Time using Equal() so remove ExpiresOn from sessions
l := *loadedSession l := *loadedSession
l.CreatedAt = time.Time{}
l.ExpiresOn = time.Time{} l.ExpiresOn = time.Time{}
s := *session s := *session
s.CreatedAt = time.Time{}
s.ExpiresOn = time.Time{} s.ExpiresOn = time.Time{}
Expect(l).To(Equal(s)) Expect(l).To(Equal(s))
// Compare time.Time separately // Compare time.Time separately
Expect(loadedSession.CreatedAt.Equal(session.CreatedAt)).To(BeTrue())
Expect(loadedSession.ExpiresOn.Equal(session.ExpiresOn)).To(BeTrue()) Expect(loadedSession.ExpiresOn.Equal(session.ExpiresOn)).To(BeTrue())
} }
}) })
})
} }
RunSessionTests := func() { BeforeEach(func() {
req := httptest.NewRequest("GET", "http://example.com/", nil)
resp := httptest.NewRecorder()
err := ss.Save(resp, req, session)
Expect(err).ToNot(HaveOccurred())
for _, cookie := range resp.Result().Cookies() {
request.AddCookie(cookie)
}
})
Context("before the refresh period", func() {
LoadSessionTests()
})
// Test TTLs and cleanup of persistent session storage
// For non-persistent we rely on the browser cookie lifecycle
if persistent {
Context("after the refresh period, but before the cookie expire period", func() {
BeforeEach(func() {
switch ss.(type) {
case *redis.SessionStore:
mr.FastForward(cookieOpts.CookieRefresh + time.Minute)
}
})
LoadSessionTests()
})
Context("after the cookie expire period", func() {
var loadedSession *sessionsapi.SessionState
var err error
BeforeEach(func() {
switch ss.(type) {
case *redis.SessionStore:
mr.FastForward(cookieOpts.CookieExpire + time.Minute)
}
loadedSession, err = ss.Load(request)
Expect(err).To(HaveOccurred())
})
It("returns an error loading the session", func() {
Expect(err).To(HaveOccurred())
})
It("returns an empty session", func() {
Expect(loadedSession).To(BeNil())
})
})
}
})
if persistent {
PersistentSessionStoreTests()
}
}
RunSessionTests := func(persistent bool) {
Context("with default options", func() { Context("with default options", func() {
BeforeEach(func() { BeforeEach(func() {
var err error var err error
@ -158,7 +325,7 @@ var _ = Describe("NewSessionStore", func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
SessionStoreInterfaceTests() SessionStoreInterfaceTests(persistent)
}) })
Context("with non-default options", func() { Context("with non-default options", func() {
@ -167,7 +334,7 @@ var _ = Describe("NewSessionStore", func() {
CookieName: "_cookie_name", CookieName: "_cookie_name",
CookiePath: "/path", CookiePath: "/path",
CookieExpire: time.Duration(72) * time.Hour, CookieExpire: time.Duration(72) * time.Hour,
CookieRefresh: time.Duration(3600), CookieRefresh: time.Duration(2) * time.Hour,
CookieSecure: false, CookieSecure: false,
CookieHTTPOnly: false, CookieHTTPOnly: false,
CookieDomain: "example.com", CookieDomain: "example.com",
@ -178,21 +345,25 @@ var _ = Describe("NewSessionStore", func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
SessionStoreInterfaceTests() SessionStoreInterfaceTests(persistent)
}) })
Context("with a cookie-secret set", func() { Context("with a cipher", func() {
BeforeEach(func() { BeforeEach(func() {
secret := make([]byte, 32) secret := make([]byte, 32)
_, err := rand.Read(secret) _, err := rand.Read(secret)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
cookieOpts.CookieSecret = base64.URLEncoding.EncodeToString(secret) cookieOpts.CookieSecret = base64.URLEncoding.EncodeToString(secret)
cipher, err := encryption.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
Expect(err).ToNot(HaveOccurred())
Expect(cipher).ToNot(BeNil())
opts.Cipher = cipher
ss, err = sessions.NewSessionStore(opts, cookieOpts) ss, err = sessions.NewSessionStore(opts, cookieOpts)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
SessionStoreInterfaceTests() SessionStoreInterfaceTests(persistent)
}) })
} }
@ -205,7 +376,7 @@ var _ = Describe("NewSessionStore", func() {
CookieName: "_oauth2_proxy", CookieName: "_oauth2_proxy",
CookiePath: "/", CookiePath: "/",
CookieExpire: time.Duration(168) * time.Hour, CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(0), CookieRefresh: time.Duration(1) * time.Hour,
CookieSecure: true, CookieSecure: true,
CookieHTTPOnly: true, CookieHTTPOnly: true,
} }
@ -231,11 +402,35 @@ var _ = Describe("NewSessionStore", func() {
It("creates a cookie.SessionStore", func() { It("creates a cookie.SessionStore", func() {
ss, err := sessions.NewSessionStore(opts, cookieOpts) ss, err := sessions.NewSessionStore(opts, cookieOpts)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(ss).To(BeAssignableToTypeOf(&cookie.SessionStore{})) Expect(ss).To(BeAssignableToTypeOf(&sessionscookie.SessionStore{}))
}) })
Context("the cookie.SessionStore", func() { Context("the cookie.SessionStore", func() {
RunSessionTests() RunSessionTests(false)
})
})
Context("with type 'redis'", func() {
BeforeEach(func() {
var err error
mr, err = miniredis.Run()
Expect(err).ToNot(HaveOccurred())
opts.Type = options.RedisSessionStoreType
opts.RedisConnectionURL = "redis://" + mr.Addr()
})
AfterEach(func() {
mr.Close()
})
It("creates a redis.SessionStore", func() {
ss, err := sessions.NewSessionStore(opts, cookieOpts)
Expect(err).NotTo(HaveOccurred())
Expect(ss).To(BeAssignableToTypeOf(&redis.SessionStore{}))
})
Context("the redis.SessionStore", func() {
RunSessionTests(true)
}) })
}) })

View File

@ -3,17 +3,17 @@ package utils
import ( import (
"encoding/base64" "encoding/base64"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
) )
// CookieForSession serializes a session state for storage in a cookie // CookieForSession serializes a session state for storage in a cookie
func CookieForSession(s *sessions.SessionState, c *cookie.Cipher) (string, error) { func CookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) {
return s.EncodeSessionState(c) return s.EncodeSessionState(c)
} }
// SessionFromCookie deserializes a session from a cookie value // SessionFromCookie deserializes a session from a cookie value
func SessionFromCookie(v string, c *cookie.Cipher) (s *sessions.SessionState, err error) { func SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) {
return sessions.DecodeSessionState(v, c) return sessions.DecodeSessionState(v, c)
} }

View File

@ -7,9 +7,9 @@ import (
"net/url" "net/url"
"github.com/bitly/go-simplejson" "github.com/bitly/go-simplejson"
"github.com/pusher/oauth2_proxy/api"
"github.com/pusher/oauth2_proxy/logger"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/logger"
"github.com/pusher/oauth2_proxy/pkg/requests"
) )
// AzureProvider represents an Azure based Identity Provider // AzureProvider represents an Azure based Identity Provider
@ -102,7 +102,7 @@ func (p *AzureProvider) GetEmailAddress(s *sessions.SessionState) (string, error
} }
req.Header = getAzureHeader(s.AccessToken) req.Header = getAzureHeader(s.AccessToken)
json, err := api.Request(req) json, err := requests.Request(req)
if err != nil { if err != nil {
return "", err return "", err

163
providers/bitbucket.go Normal file
View File

@ -0,0 +1,163 @@
package providers
import (
"net/http"
"net/url"
"strings"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/logger"
"github.com/pusher/oauth2_proxy/pkg/requests"
)
// BitbucketProvider represents an Bitbucket based Identity Provider
type BitbucketProvider struct {
*ProviderData
Team string
Repository string
}
// NewBitbucketProvider initiates a new BitbucketProvider
func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
p.ProviderName = "Bitbucket"
if p.LoginURL == nil || p.LoginURL.String() == "" {
p.LoginURL = &url.URL{
Scheme: "https",
Host: "bitbucket.org",
Path: "/site/oauth2/authorize",
}
}
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
p.RedeemURL = &url.URL{
Scheme: "https",
Host: "bitbucket.org",
Path: "/site/oauth2/access_token",
}
}
if p.ValidateURL == nil || p.ValidateURL.String() == "" {
p.ValidateURL = &url.URL{
Scheme: "https",
Host: "api.bitbucket.org",
Path: "/2.0/user/emails",
}
}
if p.Scope == "" {
p.Scope = "email"
}
return &BitbucketProvider{ProviderData: p}
}
// SetTeam defines the Bitbucket team the user must be part of
func (p *BitbucketProvider) SetTeam(team string) {
p.Team = team
if !strings.Contains(p.Scope, "team") {
p.Scope += " team"
}
}
// SetRepository defines the repository the user must have access to
func (p *BitbucketProvider) SetRepository(repository string) {
p.Repository = repository
if !strings.Contains(p.Scope, "repository") {
p.Scope += " repository"
}
}
// GetEmailAddress returns the email of the authenticated user
func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
var emails struct {
Values []struct {
Email string `json:"email"`
Primary bool `json:"is_primary"`
}
}
var teams struct {
Values []struct {
Name string `json:"username"`
}
}
var repositories struct {
Values []struct {
FullName string `json:"full_name"`
}
}
req, err := http.NewRequest("GET",
p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
if err != nil {
logger.Printf("failed building request %s", err)
return "", err
}
err = requests.RequestJSON(req, &emails)
if err != nil {
logger.Printf("failed making request %s", err)
return "", err
}
if p.Team != "" {
teamURL := &url.URL{}
*teamURL = *p.ValidateURL
teamURL.Path = "/2.0/teams"
req, err = http.NewRequest("GET",
teamURL.String()+"?role=member&access_token="+s.AccessToken, nil)
if err != nil {
logger.Printf("failed building request %s", err)
return "", err
}
err = requests.RequestJSON(req, &teams)
if err != nil {
logger.Printf("failed requesting teams membership %s", err)
return "", err
}
var found = false
for _, team := range teams.Values {
if p.Team == team.Name {
found = true
break
}
}
if found != true {
logger.Print("team membership test failed, access denied")
return "", nil
}
}
if p.Repository != "" {
repositoriesURL := &url.URL{}
*repositoriesURL = *p.ValidateURL
repositoriesURL.Path = "/2.0/repositories/" + strings.Split(p.Repository, "/")[0]
req, err = http.NewRequest("GET",
repositoriesURL.String()+"?role=contributor"+
"&q=full_name="+url.QueryEscape("\""+p.Repository+"\"")+
"&access_token="+s.AccessToken,
nil)
if err != nil {
logger.Printf("failed building request %s", err)
return "", err
}
err = requests.RequestJSON(req, &repositories)
if err != nil {
logger.Printf("failed checking repository access %s", err)
return "", err
}
var found = false
for _, repository := range repositories.Values {
if p.Repository == repository.FullName {
found = true
break
}
}
if found != true {
logger.Print("repository access test failed, access denied")
return "", nil
}
}
for _, email := range emails.Values {
if email.Primary {
return email.Email, nil
}
}
return "", nil
}

170
providers/bitbucket_test.go Normal file
View File

@ -0,0 +1,170 @@
package providers
import (
"log"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
)
func testBitbucketProvider(hostname, team string, repository string) *BitbucketProvider {
p := NewBitbucketProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: ""})
if team != "" {
p.SetTeam(team)
}
if repository != "" {
p.SetRepository(repository)
}
if hostname != "" {
updateURL(p.Data().LoginURL, hostname)
updateURL(p.Data().RedeemURL, hostname)
updateURL(p.Data().ProfileURL, hostname)
updateURL(p.Data().ValidateURL, hostname)
}
return p
}
func testBitbucketBackend(payload string) *httptest.Server {
paths := map[string]bool{
"/2.0/user/emails": true,
"/2.0/teams": true,
}
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
url := r.URL
if !paths[url.Path] {
log.Printf("%s not in %+v\n", url.Path, paths)
w.WriteHeader(404)
} else if r.URL.Query().Get("access_token") != "imaginary_access_token" {
w.WriteHeader(403)
} else {
w.WriteHeader(200)
w.Write([]byte(payload))
}
}))
}
func TestBitbucketProviderDefaults(t *testing.T) {
p := testBitbucketProvider("", "", "")
assert.NotEqual(t, nil, p)
assert.Equal(t, "Bitbucket", p.Data().ProviderName)
assert.Equal(t, "https://bitbucket.org/site/oauth2/authorize",
p.Data().LoginURL.String())
assert.Equal(t, "https://bitbucket.org/site/oauth2/access_token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://api.bitbucket.org/2.0/user/emails",
p.Data().ValidateURL.String())
assert.Equal(t, "email", p.Data().Scope)
}
func TestBitbucketProviderScopeAdjustForTeam(t *testing.T) {
p := testBitbucketProvider("", "test-team", "")
assert.NotEqual(t, nil, p)
assert.Equal(t, "email team", p.Data().Scope)
}
func TestBitbucketProviderScopeAdjustForRepository(t *testing.T) {
p := testBitbucketProvider("", "", "rest-repo")
assert.NotEqual(t, nil, p)
assert.Equal(t, "email repository", p.Data().Scope)
}
func TestBitbucketProviderOverrides(t *testing.T) {
p := NewBitbucketProvider(
&ProviderData{
LoginURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/auth"},
RedeemURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/token"},
ValidateURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api/v3/user"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "Bitbucket", p.Data().ProviderName)
assert.Equal(t, "https://example.com/oauth/auth",
p.Data().LoginURL.String())
assert.Equal(t, "https://example.com/oauth/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://example.com/api/v3/user",
p.Data().ValidateURL.String())
assert.Equal(t, "profile", p.Data().Scope)
}
func TestBitbucketProviderGetEmailAddress(t *testing.T) {
b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true } ] }")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testBitbucketProvider(bURL.Host, "", "")
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email)
}
func TestBitbucketProviderGetEmailAddressAndGroup(t *testing.T) {
b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true, \"username\": \"bioinformatics\" } ] }")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testBitbucketProvider(bURL.Host, "bioinformatics", "")
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email)
}
// Note that trying to trigger the "failed building request" case is not
// practical, since the only way it can fail is if the URL fails to parse.
func TestBitbucketProviderGetEmailAddressFailedRequest(t *testing.T) {
b := testBitbucketBackend("unused payload")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testBitbucketProvider(bURL.Host, "", "")
// We'll trigger a request failure by using an unexpected access
// token. Alternatively, we could allow the parsing of the payload as
// JSON to fail.
session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}
func TestBitbucketProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
b := testBitbucketBackend("{\"foo\": \"bar\"}")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testBitbucketProvider(bURL.Host, "", "")
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, "", email)
assert.Equal(t, nil, err)
}

View File

@ -6,8 +6,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/pusher/oauth2_proxy/api"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/requests"
) )
// FacebookProvider represents an Facebook based Identity Provider // FacebookProvider represents an Facebook based Identity Provider
@ -69,7 +69,7 @@ func (p *FacebookProvider) GetEmailAddress(s *sessions.SessionState) (string, er
Email string Email string
} }
var r result var r result
err = api.RequestJSON(req, &r) err = requests.RequestJSON(req, &r)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -10,8 +10,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/pusher/oauth2_proxy/logger"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
// GitHubProvider represents an GitHub based Identity Provider // GitHubProvider represents an GitHub based Identity Provider

View File

@ -1,62 +1,258 @@
package providers package providers
import ( import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http" "net/http"
"net/url" "strings"
"time"
"github.com/pusher/oauth2_proxy/api" oidc "github.com/coreos/go-oidc"
"github.com/pusher/oauth2_proxy/logger"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"golang.org/x/oauth2"
) )
// GitLabProvider represents an GitLab based Identity Provider // GitLabProvider represents a GitLab based Identity Provider
type GitLabProvider struct { type GitLabProvider struct {
*ProviderData *ProviderData
Group string
EmailDomains []string
Verifier *oidc.IDTokenVerifier
AllowUnverifiedEmail bool
} }
// NewGitLabProvider initiates a new GitLabProvider // NewGitLabProvider initiates a new GitLabProvider
func NewGitLabProvider(p *ProviderData) *GitLabProvider { func NewGitLabProvider(p *ProviderData) *GitLabProvider {
p.ProviderName = "GitLab" p.ProviderName = "GitLab"
if p.LoginURL == nil || p.LoginURL.String() == "" {
p.LoginURL = &url.URL{
Scheme: "https",
Host: "gitlab.com",
Path: "/oauth/authorize",
}
}
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
p.RedeemURL = &url.URL{
Scheme: "https",
Host: "gitlab.com",
Path: "/oauth/token",
}
}
if p.ValidateURL == nil || p.ValidateURL.String() == "" {
p.ValidateURL = &url.URL{
Scheme: "https",
Host: "gitlab.com",
Path: "/api/v4/user",
}
}
if p.Scope == "" { if p.Scope == "" {
p.Scope = "read_user" p.Scope = "openid email"
} }
return &GitLabProvider{ProviderData: p} return &GitLabProvider{ProviderData: p}
} }
// Redeem exchanges the OAuth2 authentication token for an ID token
func (p *GitLabProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
ctx := context.Background()
c := oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: p.RedeemURL.String(),
},
RedirectURL: redirectURL,
}
token, err := c.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("token exchange: %v", err)
}
s, err = p.createSessionState(ctx, token)
if err != nil {
return nil, fmt.Errorf("unable to update session: %v", err)
}
return
}
// RefreshSessionIfNeeded checks if the session has expired and uses the
// RefreshToken to fetch a new ID token if required
func (p *GitLabProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
return false, nil
}
origExpiration := s.ExpiresOn
err := p.redeemRefreshToken(s)
if err != nil {
return false, fmt.Errorf("unable to redeem refresh token: %v", err)
}
fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
return true, nil
}
func (p *GitLabProvider) redeemRefreshToken(s *sessions.SessionState) (err error) {
c := oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: p.RedeemURL.String(),
},
}
ctx := context.Background()
t := &oauth2.Token{
RefreshToken: s.RefreshToken,
Expiry: time.Now().Add(-time.Hour),
}
token, err := c.TokenSource(ctx, t).Token()
if err != nil {
return fmt.Errorf("failed to get token: %v", err)
}
newSession, err := p.createSessionState(ctx, token)
if err != nil {
return fmt.Errorf("unable to update session: %v", err)
}
s.AccessToken = newSession.AccessToken
s.IDToken = newSession.IDToken
s.RefreshToken = newSession.RefreshToken
s.CreatedAt = newSession.CreatedAt
s.ExpiresOn = newSession.ExpiresOn
s.Email = newSession.Email
return
}
type gitlabUserInfo struct {
Username string `json:"nickname"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Groups []string `json:"groups"`
}
func (p *GitLabProvider) getUserInfo(s *sessions.SessionState) (*gitlabUserInfo, error) {
// Retrieve user info JSON
// https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
// Build user info url from login url of GitLab instance
userInfoURL := *p.LoginURL
userInfoURL.Path = "/oauth/userinfo"
req, err := http.NewRequest("GET", userInfoURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create user info request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+s.AccessToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to perform user info request: %v", err)
}
var body []byte
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("failed to read user info response: %v", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("got %d during user info request: %s", resp.StatusCode, body)
}
var userInfo gitlabUserInfo
err = json.Unmarshal(body, &userInfo)
if err != nil {
return nil, fmt.Errorf("failed to parse user info: %v", err)
}
return &userInfo, nil
}
func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error {
if p.Group == "" {
return nil
}
// Collect user group memberships
membershipSet := make(map[string]bool)
for _, group := range userInfo.Groups {
membershipSet[group] = true
}
// Find a valid group that they are a member of
validGroups := strings.Split(p.Group, " ")
for _, validGroup := range validGroups {
if _, ok := membershipSet[validGroup]; ok {
return nil
}
}
return fmt.Errorf("user is not a member of '%s'", p.Group)
}
func (p *GitLabProvider) verifyEmailDomain(userInfo *gitlabUserInfo) error {
if len(p.EmailDomains) == 0 || p.EmailDomains[0] == "*" {
return nil
}
for _, domain := range p.EmailDomains {
if strings.HasSuffix(userInfo.Email, domain) {
return nil
}
}
return fmt.Errorf("user email is not one of the valid domains '%v'", p.EmailDomains)
}
func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("token response did not contain an id_token")
}
// Parse and verify ID Token payload.
idToken, err := p.Verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("could not verify id_token: %v", err)
}
return &sessions.SessionState{
AccessToken: token.AccessToken,
IDToken: rawIDToken,
RefreshToken: token.RefreshToken,
CreatedAt: time.Now(),
ExpiresOn: idToken.Expiry,
}, nil
}
// ValidateSessionState checks that the session's IDToken is still valid
func (p *GitLabProvider) ValidateSessionState(s *sessions.SessionState) bool {
ctx := context.Background()
_, err := p.Verifier.Verify(ctx, s.IDToken)
if err != nil {
return false
}
return true
}
// GetEmailAddress returns the Account email address // GetEmailAddress returns the Account email address
func (p *GitLabProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { func (p *GitLabProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
// Retrieve user info
userInfo, err := p.getUserInfo(s)
if err != nil {
return "", fmt.Errorf("failed to retrieve user info: %v", err)
}
req, err := http.NewRequest("GET", // Check if email is verified
p.ValidateURL.String()+"?access_token="+s.AccessToken, nil) if !p.AllowUnverifiedEmail && !userInfo.EmailVerified {
return "", fmt.Errorf("user email is not verified")
}
// Check if email has valid domain
err = p.verifyEmailDomain(userInfo)
if err != nil { if err != nil {
logger.Printf("failed building request %s", err) return "", fmt.Errorf("email domain check failed: %v", err)
return "", err
} }
json, err := api.Request(req)
// Check group membership
err = p.verifyGroupMembership(userInfo)
if err != nil { if err != nil {
logger.Printf("failed making request %s", err) return "", fmt.Errorf("group membership check failed: %v", err)
return "", err
} }
return json.Get("email").String()
return userInfo.Email, nil
}
// GetUserName returns the Account user name
func (p *GitLabProvider) GetUserName(s *sessions.SessionState) (string, error) {
userInfo, err := p.getUserInfo(s)
if err != nil {
return "", fmt.Errorf("failed to retrieve user info: %v", err)
}
return userInfo.Username, nil
} }

View File

@ -25,104 +25,142 @@ func testGitLabProvider(hostname string) *GitLabProvider {
updateURL(p.Data().ProfileURL, hostname) updateURL(p.Data().ProfileURL, hostname)
updateURL(p.Data().ValidateURL, hostname) updateURL(p.Data().ValidateURL, hostname)
} }
return p return p
} }
func testGitLabBackend(payload string) *httptest.Server { func testGitLabBackend() *httptest.Server {
path := "/api/v4/user" userInfo := `
query := "access_token=imaginary_access_token" {
"nickname": "FooBar",
"email": "foo@bar.com",
"email_verified": false,
"groups": ["foo", "bar"]
}
`
authHeader := "Bearer gitlab_access_token"
return httptest.NewServer(http.HandlerFunc( return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != path || r.URL.RawQuery != query { if r.URL.Path == "/oauth/userinfo" {
w.WriteHeader(404) if r.Header["Authorization"][0] == authHeader {
} else {
w.WriteHeader(200) w.WriteHeader(200)
w.Write([]byte(payload)) w.Write([]byte(userInfo))
} else {
w.WriteHeader(401)
}
} else {
w.WriteHeader(404)
} }
})) }))
} }
func TestGitLabProviderDefaults(t *testing.T) { func TestGitLabProviderBadToken(t *testing.T) {
p := testGitLabProvider("") b := testGitLabBackend()
assert.NotEqual(t, nil, p)
assert.Equal(t, "GitLab", p.Data().ProviderName)
assert.Equal(t, "https://gitlab.com/oauth/authorize",
p.Data().LoginURL.String())
assert.Equal(t, "https://gitlab.com/oauth/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://gitlab.com/api/v4/user",
p.Data().ValidateURL.String())
assert.Equal(t, "read_user", p.Data().Scope)
}
func TestGitLabProviderOverrides(t *testing.T) {
p := NewGitLabProvider(
&ProviderData{
LoginURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/auth"},
RedeemURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/token"},
ValidateURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api/v4/user"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "GitLab", p.Data().ProviderName)
assert.Equal(t, "https://example.com/oauth/auth",
p.Data().LoginURL.String())
assert.Equal(t, "https://example.com/oauth/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://example.com/api/v4/user",
p.Data().ValidateURL.String())
assert.Equal(t, "profile", p.Data().Scope)
}
func TestGitLabProviderGetEmailAddress(t *testing.T) {
b := testGitLabBackend("{\"email\": \"michael.bland@gsa.gov\"}")
defer b.Close() defer b.Close()
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testGitLabProvider(bURL.Host) p := testGitLabProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
_, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
}
func TestGitLabProviderUnverifiedEmailDenied(t *testing.T) {
b := testGitLabBackend()
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testGitLabProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
_, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
}
func TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) {
b := testGitLabBackend()
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testGitLabProvider(bURL.Host)
p.AllowUnverifiedEmail = true
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
email, err := p.GetEmailAddress(session) email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email) assert.Equal(t, "foo@bar.com", email)
} }
// Note that trying to trigger the "failed building request" case is not func TestGitLabProviderUsername(t *testing.T) {
// practical, since the only way it can fail is if the URL fails to parse. b := testGitLabBackend()
func TestGitLabProviderGetEmailAddressFailedRequest(t *testing.T) {
b := testGitLabBackend("unused payload")
defer b.Close() defer b.Close()
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testGitLabProvider(bURL.Host) p := testGitLabProvider(bURL.Host)
p.AllowUnverifiedEmail = true
// We'll trigger a request failure by using an unexpected access session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
// token. Alternatively, we could allow the parsing of the payload as username, err := p.GetUserName(session)
// JSON to fail. assert.Equal(t, nil, err)
session := &sessions.SessionState{AccessToken: "unexpected_access_token"} assert.Equal(t, "FooBar", username)
email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
} }
func TestGitLabProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { func TestGitLabProviderGroupMembershipValid(t *testing.T) {
b := testGitLabBackend("{\"foo\": \"bar\"}") b := testGitLabBackend()
defer b.Close() defer b.Close()
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testGitLabProvider(bURL.Host) p := testGitLabProvider(bURL.Host)
p.AllowUnverifiedEmail = true
p.Group = "foo"
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
email, err := p.GetEmailAddress(session) email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "", email) assert.Equal(t, "foo@bar.com", email)
}
func TestGitLabProviderGroupMembershipMissing(t *testing.T) {
b := testGitLabBackend()
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testGitLabProvider(bURL.Host)
p.AllowUnverifiedEmail = true
p.Group = "baz"
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
_, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
}
func TestGitLabProviderEmailDomainValid(t *testing.T) {
b := testGitLabBackend()
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testGitLabProvider(bURL.Host)
p.AllowUnverifiedEmail = true
p.EmailDomains = []string{"bar.com"}
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Equal(t, "foo@bar.com", email)
}
func TestGitLabProviderEmailDomainInvalid(t *testing.T) {
b := testGitLabBackend()
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testGitLabProvider(bURL.Host)
p.AllowUnverifiedEmail = true
p.EmailDomains = []string{"baz.com"}
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
_, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
} }

View File

@ -13,8 +13,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/pusher/oauth2_proxy/logger"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/logger"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
admin "google.golang.org/api/admin/directory/v1" admin "google.golang.org/api/admin/directory/v1"
@ -149,6 +149,7 @@ func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionSt
s = &sessions.SessionState{ s = &sessions.SessionState{
AccessToken: jsonResponse.AccessToken, AccessToken: jsonResponse.AccessToken,
IDToken: jsonResponse.IDToken, IDToken: jsonResponse.IDToken,
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second), ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
RefreshToken: jsonResponse.RefreshToken, RefreshToken: jsonResponse.RefreshToken,
Email: c.Email, Email: c.Email,
@ -188,67 +189,42 @@ func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Serv
} }
func userInGroup(service *admin.Service, groups []string, email string) bool { func userInGroup(service *admin.Service, groups []string, email string) bool {
user, err := fetchUser(service, email)
if err != nil {
logger.Printf("error fetching user: %v", err)
return false
}
id := user.Id
custID := user.CustomerId
for _, group := range groups { for _, group := range groups {
members, err := fetchGroupMembers(service, group) // Use the HasMember API to checking for the user's presence in each group or nested subgroups
if err != nil { req := service.Members.HasMember(group, email)
if err, ok := err.(*googleapi.Error); ok && err.Code == 404 {
logger.Printf("error fetching members for group %s: group does not exist", group)
} else {
logger.Printf("error fetching group members: %v", err)
return false
}
}
for _, member := range members {
switch member.Type {
case "CUSTOMER":
if member.Id == custID {
return true
}
case "USER":
if member.Id == id {
return true
}
}
}
}
return false
}
func fetchUser(service *admin.Service, email string) (*admin.User, error) {
user, err := service.Users.Get(email).Do()
return user, err
}
func fetchGroupMembers(service *admin.Service, group string) ([]*admin.Member, error) {
members := []*admin.Member{}
pageToken := ""
for {
req := service.Members.List(group)
if pageToken != "" {
req.PageToken(pageToken)
}
r, err := req.Do() r, err := req.Do()
if err != nil { if err != nil {
return nil, err err, ok := err.(*googleapi.Error)
if ok && err.Code == 404 {
logger.Printf("error checking membership in group %s: group does not exist", group)
} else if ok && err.Code == 400 {
// It is possible for Members.HasMember to return false even if the email is a group member.
// One case that can cause this is if the user email is from a different domain than the group,
// e.g. "member@otherdomain.com" in the group "group@mydomain.com" will result in a 400 error
// from the HasMember API. In that case, attempt to query the member object directly from the group.
req := service.Members.Get(group, email)
r, err := req.Do()
if err != nil {
logger.Printf("error using get API to check member %s of google group %s: user not in the group", email, group)
continue
} }
for _, member := range r.Members {
members = append(members, member) // If the non-domain user is found within the group, still verify that they are "ACTIVE".
// Do not count the user as belonging to a group if they have another status ("ARCHIVED", "SUSPENDED", or "UNKNOWN").
if r.Status == "ACTIVE" {
return true
} }
if r.NextPageToken == "" { } else {
break logger.Printf("error checking group membership: %v", err)
} }
pageToken = r.NextPageToken continue
} }
return members, nil if r.IsMember {
return true
}
}
return false
} }
// ValidateGroup validates that the provided email exists in the configured Google // ValidateGroup validates that the provided email exists in the configured Google

View File

@ -1,14 +1,19 @@
package providers package providers
import ( import (
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
admin "google.golang.org/api/admin/directory/v1"
option "google.golang.org/api/option"
) )
func newRedeemServer(body []byte) (*url.URL, *httptest.Server) { func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
@ -179,3 +184,56 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
} }
} }
func TestGoogleProviderUserInGroup(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/groups/group@example.com/hasMember/member-in-domain@example.com" {
fmt.Fprintln(w, `{"isMember": true}`)
} else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-in-domain@example.com" {
fmt.Fprintln(w, `{"isMember": false}`)
} else if r.URL.Path == "/groups/group@example.com/hasMember/member-out-of-domain@otherexample.com" {
http.Error(
w,
`{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`,
http.StatusBadRequest,
)
} else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-out-of-domain@otherexample.com" {
http.Error(
w,
`{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`,
http.StatusBadRequest,
)
} else if r.URL.Path == "/groups/group@example.com/members/non-member-out-of-domain@otherexample.com" {
// 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
http.Error(
w,
`{"error": {"errors": [{"domain": "global","reason": "notFound","message": "Resource Not Found: memberKey"}],"code": 404,"message": "Resource Not Found: memberKey"}}`,
http.StatusNotFound,
)
} else if r.URL.Path == "/groups/group@example.com/members/member-out-of-domain@otherexample.com" {
fmt.Fprintln(w,
`{"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"}}`,
)
}
}))
defer ts.Close()
client := ts.Client()
ctx := context.Background()
service, err := admin.NewService(ctx, option.WithHTTPClient(client))
service.BasePath = ts.URL
assert.Equal(t, nil, err)
result := userInGroup(service, []string{"group@example.com"}, "member-in-domain@example.com")
assert.True(t, result)
result = userInGroup(service, []string{"group@example.com"}, "member-out-of-domain@otherexample.com")
assert.True(t, result)
result = userInGroup(service, []string{"group@example.com"}, "non-member-in-domain@example.com")
assert.False(t, result)
result = userInGroup(service, []string{"group@example.com"}, "non-member-out-of-domain@otherexample.com")
assert.False(t, result)
}

View File

@ -5,8 +5,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/pusher/oauth2_proxy/api" "github.com/pusher/oauth2_proxy/pkg/logger"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/requests"
) )
// stripToken is a helper function to obfuscate "access_token" // stripToken is a helper function to obfuscate "access_token"
@ -47,7 +47,7 @@ func stripParam(param, endpoint string) string {
// validateToken returns true if token is valid // validateToken returns true if token is valid
func validateToken(p Provider, accessToken string, header http.Header) bool { func validateToken(p Provider, accessToken string, header http.Header) bool {
if accessToken == "" || p.Data().ValidateURL == nil { if accessToken == "" || p.Data().ValidateURL == nil || p.Data().ValidateURL.String() == "" {
return false return false
} }
endpoint := p.Data().ValidateURL.String() endpoint := p.Data().ValidateURL.String()
@ -55,7 +55,7 @@ func validateToken(p Provider, accessToken string, header http.Header) bool {
params := url.Values{"access_token": {accessToken}} params := url.Values{"access_token": {accessToken}}
endpoint = endpoint + "?" + params.Encode() endpoint = endpoint + "?" + params.Encode()
} }
resp, err := api.RequestUnparsedResponse(endpoint, header) resp, err := requests.RequestUnparsedResponse(endpoint, header)
if err != nil { if err != nil {
logger.Printf("GET %s", stripToken(endpoint)) logger.Printf("GET %s", stripToken(endpoint))
logger.Printf("token validation request failed: %s", err) logger.Printf("token validation request failed: %s", err)

86
providers/keycloak.go Normal file
View File

@ -0,0 +1,86 @@
package providers
import (
"net/http"
"net/url"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/logger"
"github.com/pusher/oauth2_proxy/pkg/requests"
)
type KeycloakProvider struct {
*ProviderData
Group string
}
func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
p.ProviderName = "Keycloak"
if p.LoginURL == nil || p.LoginURL.String() == "" {
p.LoginURL = &url.URL{
Scheme: "https",
Host: "keycloak.org",
Path: "/oauth/authorize",
}
}
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
p.RedeemURL = &url.URL{
Scheme: "https",
Host: "keycloak.org",
Path: "/oauth/token",
}
}
if p.ValidateURL == nil || p.ValidateURL.String() == "" {
p.ValidateURL = &url.URL{
Scheme: "https",
Host: "keycloak.org",
Path: "/api/v3/user",
}
}
if p.Scope == "" {
p.Scope = "api"
}
return &KeycloakProvider{ProviderData: p}
}
func (p *KeycloakProvider) SetGroup(group string) {
p.Group = group
}
func (p *KeycloakProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
req, err := http.NewRequest("GET", p.ValidateURL.String(), nil)
req.Header.Set("Authorization", "Bearer "+s.AccessToken)
if err != nil {
logger.Printf("failed building request %s", err)
return "", err
}
json, err := requests.Request(req)
if err != nil {
logger.Printf("failed making request %s", err)
return "", err
}
if p.Group != "" {
var groups, err = json.Get("groups").Array()
if err != nil {
logger.Printf("groups not found %s", err)
return "", err
}
var found = false
for i := range groups {
if groups[i].(string) == p.Group {
found = true
break
}
}
if found != true {
logger.Printf("group not found, access denied")
return "", nil
}
}
return json.Get("email").String()
}

151
providers/keycloak_test.go Normal file
View File

@ -0,0 +1,151 @@
package providers
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/bmizerany/assert"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
)
const imaginaryAccessToken = "imaginary_access_token"
const bearerAccessToken = "Bearer " + imaginaryAccessToken
func testKeycloakProvider(hostname, group string) *KeycloakProvider {
p := NewKeycloakProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: ""})
if group != "" {
p.SetGroup(group)
}
if hostname != "" {
updateURL(p.Data().LoginURL, hostname)
updateURL(p.Data().RedeemURL, hostname)
updateURL(p.Data().ProfileURL, hostname)
updateURL(p.Data().ValidateURL, hostname)
}
return p
}
func testKeycloakBackend(payload string) *httptest.Server {
path := "/api/v3/user"
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
url := r.URL
if url.Path != path {
w.WriteHeader(404)
} else if r.Header.Get("Authorization") != bearerAccessToken {
w.WriteHeader(403)
} else {
w.WriteHeader(200)
w.Write([]byte(payload))
}
}))
}
func TestKeycloakProviderDefaults(t *testing.T) {
p := testKeycloakProvider("", "")
assert.NotEqual(t, nil, p)
assert.Equal(t, "Keycloak", p.Data().ProviderName)
assert.Equal(t, "https://keycloak.org/oauth/authorize",
p.Data().LoginURL.String())
assert.Equal(t, "https://keycloak.org/oauth/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://keycloak.org/api/v3/user",
p.Data().ValidateURL.String())
assert.Equal(t, "api", p.Data().Scope)
}
func TestKeycloakProviderOverrides(t *testing.T) {
p := NewKeycloakProvider(
&ProviderData{
LoginURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/auth"},
RedeemURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/token"},
ValidateURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api/v3/user"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "Keycloak", p.Data().ProviderName)
assert.Equal(t, "https://example.com/oauth/auth",
p.Data().LoginURL.String())
assert.Equal(t, "https://example.com/oauth/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://example.com/api/v3/user",
p.Data().ValidateURL.String())
assert.Equal(t, "profile", p.Data().Scope)
}
func TestKeycloakProviderGetEmailAddress(t *testing.T) {
b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testKeycloakProvider(bURL.Host, "")
session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email)
}
func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) {
b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testKeycloakProvider(bURL.Host, "test-grp1")
session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email)
}
// Note that trying to trigger the "failed building request" case is not
// practical, since the only way it can fail is if the URL fails to parse.
func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) {
b := testKeycloakBackend("unused payload")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testKeycloakProvider(bURL.Host, "")
// We'll trigger a request failure by using an unexpected access
// token. Alternatively, we could allow the parsing of the payload as
// JSON to fail.
session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}
func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
b := testKeycloakBackend("{\"foo\": \"bar\"}")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testKeycloakProvider(bURL.Host, "")
session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}

View File

@ -6,8 +6,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/pusher/oauth2_proxy/api"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/requests"
) )
// LinkedInProvider represents an LinkedIn based Identity Provider // LinkedInProvider represents an LinkedIn based Identity Provider
@ -61,7 +61,7 @@ func (p *LinkedInProvider) GetEmailAddress(s *sessions.SessionState) (string, er
} }
req.Header = getLinkedInHeader(s.AccessToken) req.Header = getLinkedInHeader(s.AccessToken)
json, err := api.Request(req) json, err := requests.Request(req)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -252,6 +252,7 @@ func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.Session
s = &sessions.SessionState{ s = &sessions.SessionState{
AccessToken: jsonResponse.AccessToken, AccessToken: jsonResponse.AccessToken,
IDToken: jsonResponse.IDToken, IDToken: jsonResponse.IDToken,
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second), ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
Email: email, Email: email,
} }

View File

@ -3,10 +3,13 @@ package providers
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"time" "time"
oidc "github.com/coreos/go-oidc" oidc "github.com/coreos/go-oidc"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/requests"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -15,6 +18,7 @@ type OIDCProvider struct {
*ProviderData *ProviderData
Verifier *oidc.IDTokenVerifier Verifier *oidc.IDTokenVerifier
AllowUnverifiedEmail bool
} }
// NewOIDCProvider initiates a new OIDCProvider // NewOIDCProvider initiates a new OIDCProvider
@ -87,6 +91,7 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error)
s.AccessToken = newSession.AccessToken s.AccessToken = newSession.AccessToken
s.IDToken = newSession.IDToken s.IDToken = newSession.IDToken
s.RefreshToken = newSession.RefreshToken s.RefreshToken = newSession.RefreshToken
s.CreatedAt = newSession.CreatedAt
s.ExpiresOn = newSession.ExpiresOn s.ExpiresOn = newSession.ExpiresOn
s.Email = newSession.Email s.Email = newSession.Email
return return
@ -115,10 +120,33 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
} }
if claims.Email == "" { if claims.Email == "" {
// TODO: Try getting email from /userinfo before falling back to Subject if p.ProfileURL.String() == "" {
claims.Email = claims.Subject return nil, fmt.Errorf("id_token did not contain an email")
} }
if claims.Verified != nil && !*claims.Verified {
// If the userinfo endpoint profileURL is defined, then there is a chance the userinfo
// contents at the profileURL contains the email.
// Make a query to the userinfo endpoint, and attempt to locate the email from there.
req, err := http.NewRequest("GET", p.ProfileURL.String(), nil)
if err != nil {
return nil, err
}
req.Header = getOIDCHeader(token.AccessToken)
respJSON, err := requests.Request(req)
if err != nil {
return nil, err
}
email, err := respJSON.Get("email").String()
if err != nil {
return nil, fmt.Errorf("Neither id_token nor userinfo endpoint contained an email")
}
claims.Email = email
}
if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified {
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email) return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
} }
@ -126,7 +154,8 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
IDToken: rawIDToken, IDToken: rawIDToken,
RefreshToken: token.RefreshToken, RefreshToken: token.RefreshToken,
ExpiresOn: token.Expiry, CreatedAt: time.Now(),
ExpiresOn: idToken.Expiry,
Email: claims.Email, Email: claims.Email,
User: claims.Subject, User: claims.Subject,
}, nil }, nil
@ -142,3 +171,10 @@ func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool {
return true return true
} }
func getOIDCHeader(accessToken string) http.Header {
header := make(http.Header)
header.Set("Accept", "application/json")
header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
return header
}

View File

@ -8,9 +8,10 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"time"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
) )
// Redeem provides a default implementation of the OAuth2 token redemption process // Redeem provides a default implementation of the OAuth2 token redemption process
@ -72,7 +73,7 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionStat
return return
} }
if a := v.Get("access_token"); a != "" { if a := v.Get("access_token"); a != "" {
s = &sessions.SessionState{AccessToken: a} s = &sessions.SessionState{AccessToken: a, CreatedAt: time.Now()}
} else { } else {
err = fmt.Errorf("no access token found %s", body) err = fmt.Errorf("no access token found %s", body)
} }
@ -95,12 +96,12 @@ func (p *ProviderData) GetLoginURL(redirectURI, state string) string {
} }
// CookieForSession serializes a session state for storage in a cookie // CookieForSession serializes a session state for storage in a cookie
func (p *ProviderData) CookieForSession(s *sessions.SessionState, c *cookie.Cipher) (string, error) { func (p *ProviderData) CookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) {
return s.EncodeSessionState(c) return s.EncodeSessionState(c)
} }
// SessionFromCookie deserializes a session from a cookie value // SessionFromCookie deserializes a session from a cookie value
func (p *ProviderData) SessionFromCookie(v string, c *cookie.Cipher) (s *sessions.SessionState, err error) { func (p *ProviderData) SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) {
return sessions.DecodeSessionState(v, c) return sessions.DecodeSessionState(v, c)
} }

View File

@ -1,8 +1,8 @@
package providers package providers
import ( import (
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
) )
// Provider represents an upstream identity provider implementation // Provider represents an upstream identity provider implementation
@ -15,8 +15,8 @@ type Provider interface {
ValidateSessionState(*sessions.SessionState) bool ValidateSessionState(*sessions.SessionState) bool
GetLoginURL(redirectURI, finalRedirect string) string GetLoginURL(redirectURI, finalRedirect string) string
RefreshSessionIfNeeded(*sessions.SessionState) (bool, error) RefreshSessionIfNeeded(*sessions.SessionState) (bool, error)
SessionFromCookie(string, *cookie.Cipher) (*sessions.SessionState, error) SessionFromCookie(string, *encryption.Cipher) (*sessions.SessionState, error)
CookieForSession(*sessions.SessionState, *cookie.Cipher) (string, error) CookieForSession(*sessions.SessionState, *encryption.Cipher) (string, error)
} }
// New provides a new Provider based on the configured provider string // New provides a new Provider based on the configured provider string
@ -28,6 +28,8 @@ func New(provider string, p *ProviderData) Provider {
return NewFacebookProvider(p) return NewFacebookProvider(p)
case "github": case "github":
return NewGitHubProvider(p) return NewGitHubProvider(p)
case "keycloak":
return NewKeycloakProvider(p)
case "azure": case "azure":
return NewAzureProvider(p) return NewAzureProvider(p)
case "gitlab": case "gitlab":
@ -36,6 +38,8 @@ func New(provider string, p *ProviderData) Provider {
return NewOIDCProvider(p) return NewOIDCProvider(p)
case "login.gov": case "login.gov":
return NewLoginGovProvider(p) return NewLoginGovProvider(p)
case "bitbucket":
return NewBitbucketProvider(p)
default: default:
return NewGoogleProvider(p) return NewGoogleProvider(p)
} }

View File

@ -4,7 +4,7 @@ import (
"html/template" "html/template"
"path" "path"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
) )
func loadTemplates(dir string) *template.Template { func loadTemplates(dir string) *template.Template {

View File

@ -8,7 +8,7 @@ import (
"sync/atomic" "sync/atomic"
"unsafe" "unsafe"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
) )
// UserMap holds information from the authenticated emails file // UserMap holds information from the authenticated emails file

View File

@ -7,7 +7,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/logger"
fsnotify "gopkg.in/fsnotify/fsnotify.v1" fsnotify "gopkg.in/fsnotify/fsnotify.v1"
) )

View File

@ -2,7 +2,7 @@
package main package main
import "github.com/pusher/oauth2_proxy/logger" import "github.com/pusher/oauth2_proxy/pkg/logger"
func WatchForUpdates(filename string, done <-chan bool, action func()) { func WatchForUpdates(filename string, done <-chan bool, action func()) {
logger.Printf("file watching not implemented on this platform") logger.Printf("file watching not implemented on this platform")