Compare commits

..

11 Commits

Author SHA1 Message Date
Joel Speed
acdd66c70c
Implement refreshing within OIDC provider 2018-06-20 11:42:06 +01:00
Joel Speed
2bdc656590
Add Dockerfile 2018-02-28 14:40:22 +00:00
Joel Speed
bcd5ac513c
Split large cookies 2018-02-28 11:25:54 +00:00
Joel Speed
7b7cc8fdc4
Update sessions state 2018-02-28 11:24:24 +00:00
Joel Speed
c1e1f38621
Add Authorization header flags 2018-02-28 11:24:24 +00:00
Joel Speed
b19f87a884
Add env option 2018-02-28 11:24:11 +00:00
Joel Speed
dace5cde18
Test explicit subdomain whitelisting 2018-02-28 11:24:10 +00:00
Joel Speed
3940d7e1cd
Remove duplicated logic 2018-02-28 11:24:10 +00:00
Joel Speed
543575a7ad
Make option name singular 2018-02-28 11:24:10 +00:00
Joel Speed
b31369d71d
Test IsValidRedirect method 2018-02-28 11:24:10 +00:00
Joel Speed
2e75a863be
Add whitelist domains flag 2018-02-28 11:24:10 +00:00
103 changed files with 2136 additions and 8685 deletions

View File

@ -1 +0,0 @@
Dockerfile.dev

16
.github/CODEOWNERS vendored
View File

@ -1,16 +0,0 @@
# Default owner should be a Pusher cloud-team member or another maintainer
# unless overridden by later rules in this file
* @pusher/cloud-team @syscll @steakunderscore
# login.gov provider
# Note: If @timothy-spencer terms out of his appointment, your best bet
# for finding somebody who can test the oauth2_proxy would be to ask somebody
# in the login.gov team (https://login.gov/developers/), the cloud.gov team
# (https://cloud.gov/docs/help/), or the 18F org (https://18f.gsa.gov/contact/
# or the public devops channel at https://chat.18f.gov/).
providers/logingov.go @timothy-spencer
providers/logingov_test.go @timothy-spencer
# Bitbucket provider
providers/bitbucket.go @aledeganopix4d
providers/bitbucket_test.go @aledeganopix4d

View File

@ -1,37 +0,0 @@
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1. <!--- Step 1 --->
2. <!--- Step 2 --->
3. <!--- Step 3 --->
4. <!--- Step 4 --->
## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
- Version used:

View File

@ -1,25 +0,0 @@
<!--- Provide a general summary of your changes in the Title above -->
## Description
<!--- Describe your changes in detail -->
## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My change requires a change to the documentation or CHANGELOG.
- [ ] I have updated the documentation/CHANGELOG accordingly.
- [ ] I have created a feature (non-master) branch for my PR.

11
.gitignore vendored
View File

@ -1,11 +1,9 @@
oauth2_proxy oauth2_proxy
vendor vendor
dist dist
release
.godeps .godeps
*.exe *.exe
.env
.bundle
# Go.gitignore # Go.gitignore
# Compiled Object files, Static and Dynamic libs (Shared Objects) # Compiled Object files, Static and Dynamic libs (Shared Objects)
@ -31,10 +29,3 @@ _testmain.go
# Editor swap/temp files # Editor swap/temp files
.*.swp .*.swp
# Dockerfile.dev is ignored by both git and docker
# for faster development cycle of docker build
# cp Dockerfile Dockerfile.dev
# vi Dockerfile.dev
# docker build -f Dockerfile.dev .
Dockerfile.dev

View File

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

View File

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

View File

@ -1,226 +0,0 @@
# 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
- [#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
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
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
- [#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
- Adds tests suite for interface to ensure consistency across implementations
- Refactor some configuration options (around cookies) into packages
- [#114](https://github.com/pusher/oauth2_proxy/pull/114), [#154](https://github.com/pusher/oauth2_proxy/pull/154) Documentation is now available live at our [docs website](https://pusher.github.io/oauth2_proxy/) (@JoelSpeed, @icelynjennings)
- [#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)
- [#144](https://github.com/pusher/oauth2_proxy/pull/144) Use GO 1.12 for ARM builds (@kskewes)
- [#142](https://github.com/pusher/oauth2_proxy/pull/142) ARM Docker USER fix (@kskewes)
- [#52](https://github.com/pusher/oauth2_proxy/pull/52) Logging Improvements (@MisterWil)
- Implement flags to configure file logging
- `-logging-filename` Defines the filename to log to
- `-logging-max-size` Defines the maximum
- `-logging-max-age` Defines the maximum age of backups to retain
- `-logging-max-backups` Defines the maximum number of rollover log files to retain
- `-logging-compress` Defines if rollover log files should be compressed
- `-logging-local-time` Defines if logging date and time should be local or UTC
- Implement two new flags to enable or disable specific logging types
- `-standard-logging` Enables or disables standard (not request or auth) logging
- `-auth-logging` Enables or disables auth logging
- Implement two new flags to customize the logging format
- `-standard-logging-format` Sets the format for standard 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)
- [#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
## Release highlights
- Internal restructure of session state storage to use JSON rather than proprietary scheme
- Added health check options for running on GCP behind a load balancer
- Improved support for protecting websockets
- Added provider for login.gov
- Allow manual configuration of OIDC providers
## Important notes
- Dockerfile user is now non-root, this may break your existing deployment
- In the OIDC provider, when no email is returned, the ID Token subject will be used
instead of returning an error
- GitHub user emails must now be primary and verified before authenticating
## Changes since v3.1.0
- [#96](https://github.com/bitly/oauth2_proxy/pull/96) Check if email is verified on GitHub (@caarlos0)
- [#110](https://github.com/pusher/oauth2_proxy/pull/110) Added GCP healthcheck option (@timothy-spencer)
- [#112](https://github.com/pusher/oauth2_proxy/pull/112) Improve websocket support (@gyson)
- [#63](https://github.com/pusher/oauth2_proxy/pull/63) Use encoding/json for SessionState serialization (@yaegashi)
- Use JSON to encode session state to be stored in browser cookies
- Implement legacy decode function to support existing cookies generated by older versions
- Add detailed table driven tests in session_state_test.go
- [#120](https://github.com/pusher/oauth2_proxy/pull/120) Encrypting user/email from cookie (@costelmoraru)
- [#55](https://github.com/pusher/oauth2_proxy/pull/55) Added login.gov provider (@timothy-spencer)
- [#55](https://github.com/pusher/oauth2_proxy/pull/55) Added environment variables for all config options (@timothy-spencer)
- [#70](https://github.com/pusher/oauth2_proxy/pull/70) Fix handling of splitted cookies (@einfachchr)
- [#92](https://github.com/pusher/oauth2_proxy/pull/92) Merge websocket proxy feature from openshift/oauth-proxy (@butzist)
- [#57](https://github.com/pusher/oauth2_proxy/pull/57) Fall back to using OIDC Subject instead of Email (@aigarius)
- [#85](https://github.com/pusher/oauth2_proxy/pull/85) Use non-root user in docker images (@kskewes)
- [#68](https://github.com/pusher/oauth2_proxy/pull/68) forward X-Auth-Access-Token header (@davidholsgrove)
- [#41](https://github.com/pusher/oauth2_proxy/pull/41) Added option to manually specify OIDC endpoints instead of relying on discovery
- [#83](https://github.com/pusher/oauth2_proxy/pull/83) Add `id_token` refresh to Google provider (@leki75)
- [#10](https://github.com/pusher/oauth2_proxy/pull/10) fix redirect url param handling (@dt-rush)
- [#122](https://github.com/pusher/oauth2_proxy/pull/122) Expose -cookie-path as configuration parameter (@costelmoraru)
- [#124](https://github.com/pusher/oauth2_proxy/pull/124) Use Go 1.12 for testing and build environments (@syscll)
# v3.1.0
## Release highlights
- Introduction of ARM releases and and general improvements to Docker builds
- Improvements to OIDC provider allowing pass-through of ID Tokens
- Multiple redirect domains can now be whitelisted
- Streamed responses are now flushed periodically
## Important notes
- If you have been using [#bitly/621](https://github.com/bitly/oauth2_proxy/pull/621)
and have cookies larger than the 4kb limit,
the cookie splitting pattern has changed and now uses `_` in place of `-` when
indexing cookies.
This will force users to reauthenticate the first time they use `v3.1.0`.
- Streamed responses will now be flushed every 1 second by default.
Previously streamed responses were flushed only when the buffer was full.
To retain the old behaviour set `--flush-interval=0`.
See [#23](https://github.com/pusher/oauth2_proxy/pull/23) for further details.
## Changes since v3.0.0
- [#14](https://github.com/pusher/oauth2_proxy/pull/14) OIDC ID Token, Authorization Headers, Refreshing and Verification (@joelspeed)
- Implement `pass-authorization-header` and `set-authorization-header` flags
- Implement token refreshing in OIDC provider
- Split cookies larger than 4k limit into multiple cookies
- Implement token validation in OIDC provider
- [#15](https://github.com/pusher/oauth2_proxy/pull/15) WhitelistDomains (@joelspeed)
- Add `--whitelist-domain` flag to allow redirection to approved domains after OAuth flow
- [#21](https://github.com/pusher/oauth2_proxy/pull/21) Docker Improvement (@yaegashi)
- Move Docker base image from debian to alpine
- Install ca-certificates in docker image
- [#23](https://github.com/pusher/oauth2_proxy/pull/23) Flushed streaming responses
- Long-running upstream responses will get flushed every <timeperiod> (1 second by default)
- [#24](https://github.com/pusher/oauth2_proxy/pull/24) Redirect fix (@agentgonzo)
- After a successful login, you will be redirected to your original URL rather than /
- [#35](https://github.com/pusher/oauth2_proxy/pull/35) arm and arm64 binary releases (@kskewes)
- Add armv6 and arm64 to Makefile `release` target
- [#37](https://github.com/pusher/oauth2_proxy/pull/37) cross build arm and arm64 docker images (@kskewes)
# v3.0.0
Adoption of OAuth2_Proxy by Pusher.
Project was hard forked and tidied however no logical changes have occurred since
v2.2 as released by Bitly.
## Changes since v2.2:
- [#7](https://github.com/pusher/oauth2_proxy/pull/7) Migration to Pusher (@joelspeed)
- Move automated build to debian base image
- Add Makefile
- Update CI to run `make test`
- Update Dockerfile to use `make clean oauth2_proxy`
- Update `VERSION` parameter to be set by `ldflags` from Git Status
- Remove lint and test scripts
- Remove Go v1.8.x from Travis CI testing
- Add CODEOWNERS file
- Add CONTRIBUTING guide
- Add Issue and Pull Request templates
- Add Dockerfile
- Fix fsnotify import
- Update README to reflect new repository ownership
- Update CI scripts to separate linting and testing
- Now using `gometalinter` for linting
- Move Go import path from `github.com/bitly/oauth2_proxy` to `github.com/pusher/oauth2_proxy`
- Repository forked on 27/11/18
- README updated to include note that this repository is forked
- CHANGLOG created to track changes to repository from original fork

View File

@ -1,24 +0,0 @@
# Contributing
To develop on this project, please fork the repo and clone into your `$GOPATH`.
Dependencies are **not** checked in so please download those separately.
Download the dependencies using [`dep`](https://github.com/golang/dep).
```bash
cd $GOPATH/src/github.com # Create this directory if it doesn't exist
git clone git@github.com:<YOUR_FORK>/oauth2_proxy pusher/oauth2_proxy
cd pusher/oauth2_proxy
./configure # Setup your environment variables
make dep
```
## Pull Requests and Issues
We track bugs and issues using Github.
If you find a bug, please open an Issue.
If you want to fix a bug, please fork, create a feature branch, fix the bug and
open a PR back to this repo.
Please mention the open bug issue number within your PR if applicable.

View File

@ -1,32 +1,10 @@
FROM golang:1.12-stretch AS builder FROM golang:1.9 AS builder
WORKDIR /go/src/github.com/bitly/oauth2_proxy
# Download tools
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
# Copy sources
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
# Fetch dependencies
COPY go.mod go.sum ./
RUN GO111MODULE=on go mod download
# Now pull in our code
COPY . . COPY . .
RUN go get -d -v; \
CGO_ENABLED=0 GOOS=linux go build
# Build binary and make sure there is at least an empty key file. FROM scratch
# This is useful for GCP App Engine custom runtime builds, because COPY --from=builder /go/src/github.com/bitly/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
# you cannot use multiline variables in their app.yaml, so you have to
# build the key into the container and then tell it where it is
# by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem
# in app.yaml instead.
RUN ./configure && make build && touch jwt_signing_key.pem
# Copy binary to alpine
FROM alpine:3.10
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/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
USER 2000:2000
ENTRYPOINT ["/bin/oauth2_proxy"] ENTRYPOINT ["/bin/oauth2_proxy"]

View File

@ -1,32 +0,0 @@
FROM golang:1.12-stretch AS builder
# Download tools
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
# Copy sources
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
# Fetch dependencies
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.
# This is useful for GCP App Engine custom runtime builds, because
# you cannot use multiline variables in their app.yaml, so you have to
# build the key into the container and then tell it where it is
# by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem
# in app.yaml instead.
RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem
# Copy binary to alpine
FROM arm64v8/alpine:3.10
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/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
USER 2000:2000
ENTRYPOINT ["/bin/oauth2_proxy"]

View File

@ -1,32 +0,0 @@
FROM golang:1.12-stretch AS builder
# Download tools
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
# Copy sources
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
# Fetch dependencies
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.
# This is useful for GCP App Engine custom runtime builds, because
# you cannot use multiline variables in their app.yaml, so you have to
# build the key into the container and then tell it where it is
# by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem
# in app.yaml instead.
RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem
# Copy binary to alpine
FROM arm32v6/alpine:3.10
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/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
USER 2000:2000
ENTRYPOINT ["/bin/oauth2_proxy"]

117
Gopkg.lock generated Normal file
View File

@ -0,0 +1,117 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "cloud.google.com/go"
packages = ["compute/metadata"]
revision = "2d3a6656c17a60b0815b7e06ab0be04eacb6e613"
version = "v0.16.0"
[[projects]]
name = "github.com/BurntSushi/toml"
packages = ["."]
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
version = "v0.3.0"
[[projects]]
name = "github.com/bitly/go-simplejson"
packages = ["."]
revision = "aabad6e819789e569bd6aabf444c935aa9ba1e44"
version = "v0.5.0"
[[projects]]
branch = "v2"
name = "github.com/coreos/go-oidc"
packages = ["."]
revision = "77e7f2010a464ade7338597afe650dfcffbe2ca8"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
[[projects]]
name = "github.com/mbland/hmacauth"
packages = ["."]
revision = "107c17adcc5eccc9935cd67d9bc2feaf5255d2cb"
version = "1.0.2"
[[projects]]
branch = "master"
name = "github.com/mreiferson/go-options"
packages = ["."]
revision = "77551d20752b54535462404ad9d877ebdb26e53d"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/pquerna/cachecontrol"
packages = [".","cacheobject"]
revision = "0dec1b30a0215bb68605dfc568e8855066c9202d"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
version = "v1.1.4"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ed25519","ed25519/internal/edwards25519"]
revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context","context/ctxhttp"]
revision = "9dfe39835686865bff950a07b394c12a98ddc811"
[[projects]]
branch = "master"
name = "golang.org/x/oauth2"
packages = [".","google","internal","jws","jwt"]
revision = "9ff8ebcc8e241d46f52ecc5bff0e5a2f2dbef402"
[[projects]]
branch = "master"
name = "google.golang.org/api"
packages = ["admin/directory/v1","gensupport","googleapi","googleapi/internal/uritemplates"]
revision = "8791354e7ab150705ede13637a18c1fcc16b62e8"
[[projects]]
name = "google.golang.org/appengine"
packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
name = "gopkg.in/fsnotify.v1"
packages = ["."]
revision = "836bfd95fecc0f1511dd66bdbf2b5b61ab8b00b6"
version = "v1.2.11"
[[projects]]
name = "gopkg.in/square/go-jose.v2"
packages = [".","cipher","json"]
revision = "f8f38de21b4dcd69d0413faf231983f5fd6634b1"
version = "v2.1.3"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "efab48a0e196c2a849bfbe9aa02d2ae28d281ce1bfe9f23720d648858eefc8e6"
solver-name = "gps-cdcl"
solver-version = 1

40
Gopkg.toml Normal file
View File

@ -0,0 +1,40 @@
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
[[constraint]]
name = "github.com/18F/hmacauth"
version = "~1.0.1"
[[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.v1"
version = "~1.2.0"

View File

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

View File

@ -1,87 +0,0 @@
include .env
BINARY := oauth2_proxy
VERSION := $(shell git describe --always --dirty --tags 2>/dev/null || echo "undefined")
.NOTPARALLEL:
.PHONY: all
all: lint $(BINARY)
.PHONY: clean
clean:
rm -rf release
rm -f $(BINARY)
.PHONY: distclean
distclean: clean
rm -rf vendor
.PHONY: lint
lint:
GO111MODULE=on $(GOLANGCILINT) run
.PHONY: build
build: clean $(BINARY)
$(BINARY):
GO111MODULE=on CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/pusher/oauth2_proxy
.PHONY: docker
docker:
docker build -f Dockerfile -t quay.io/pusher/oauth2_proxy:latest .
.PHONY: docker-all
docker-all: docker
docker build -f Dockerfile -t quay.io/pusher/oauth2_proxy:latest-amd64 .
docker build -f Dockerfile -t quay.io/pusher/oauth2_proxy:${VERSION} .
docker build -f Dockerfile -t quay.io/pusher/oauth2_proxy:${VERSION}-amd64 .
docker build -f Dockerfile.arm64 -t quay.io/pusher/oauth2_proxy:latest-arm64 .
docker build -f Dockerfile.arm64 -t quay.io/pusher/oauth2_proxy:${VERSION}-arm64 .
docker build -f Dockerfile.armv6 -t quay.io/pusher/oauth2_proxy:latest-armv6 .
docker build -f Dockerfile.armv6 -t quay.io/pusher/oauth2_proxy:${VERSION}-armv6 .
.PHONY: docker-push
docker-push:
docker push quay.io/pusher/oauth2_proxy:latest
.PHONY: docker-push-all
docker-push-all: docker-push
docker push quay.io/pusher/oauth2_proxy:latest-amd64
docker push quay.io/pusher/oauth2_proxy:${VERSION}
docker push quay.io/pusher/oauth2_proxy:${VERSION}-amd64
docker push quay.io/pusher/oauth2_proxy:latest-arm64
docker push quay.io/pusher/oauth2_proxy:${VERSION}-arm64
docker push quay.io/pusher/oauth2_proxy:latest-armv6
docker push quay.io/pusher/oauth2_proxy:${VERSION}-armv6
.PHONY: test
test: lint
GO111MODULE=on $(GO) test -v -race ./...
.PHONY: release
release: lint test
mkdir release
mkdir release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)
mkdir release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)
mkdir release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)
mkdir release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)
mkdir release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)
GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
-o release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
GO111MODULE=on GOOS=linux GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
-o release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
GO111MODULE=on GOOS=linux GOARCH=arm64 go build -ldflags="-X main.VERSION=${VERSION}" \
-o release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
GO111MODULE=on GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X main.VERSION=${VERSION}" \
-o release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
GO111MODULE=on GOOS=windows GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
-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)

438
README.md
View File

@ -1,47 +1,419 @@
# oauth2_proxy oauth2_proxy
=================
A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others) A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others)
to validate accounts by email, domain or group. 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. [![Build Status](https://secure.travis-ci.org/bitly/oauth2_proxy.svg?branch=master)](http://travis-ci.org/bitly/oauth2_proxy)
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).
[![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy)
![Sign In Page](https://cloud.githubusercontent.com/assets/45028/4970624/7feb7dd8-6886-11e4-93e0-c9904af44ea8.png) ![Sign In Page](https://cloud.githubusercontent.com/assets/45028/4970624/7feb7dd8-6886-11e4-93e0-c9904af44ea8.png)
## Installation ## Architecture
1. Choose how to deploy:
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`
c. Using the prebuilt docker image [quay.io/pusher/oauth2_proxy](https://quay.io/pusher/oauth2_proxy) (AMD64, ARMv6 and ARM64 tags available)
Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`.
```
sha256sum -c sha256sum.txt 2>&1 | grep 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)
3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](https://pusher.github.io/oauth2_proxy/configuration)
4. [Configure SSL or Deploy behind a SSL endpoint](https://pusher.github.io/oauth2_proxy/tls-configuration) (example provided for Nginx)
## Docs
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 ## Installation
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/). 1. Download [Prebuilt Binary](https://github.com/bitly/oauth2_proxy/releases) (current release is `v2.2`) or build with `$ go get github.com/bitly/oauth2_proxy` which will put the binary in `$GOROOT/bin`
Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v2.3`.
```
sha256sum -c sha256sum.txt 2>&1 | grep OK
oauth2_proxy-2.3.linux-amd64: OK
```
2. Select a Provider and Register an OAuth Application with a Provider
3. Configure OAuth2 Proxy using config file, command line options, or environment variables
4. Configure SSL or Deploy behind a SSL endpoint (example provided for Nginx)
## Contributing ## OAuth Provider Configuration
Please see our [Contributing](CONTRIBUTING.md) guidelines. You will need to register an OAuth application with a Provider (Google, GitHub or another provider), and configure it with Redirect URI(s) for the domain you intend to run `oauth2_proxy` on.
Valid providers are :
* [Google](#google-auth-provider) *default*
* [Azure](#azure-auth-provider)
* [Facebook](#facebook-auth-provider)
* [GitHub](#github-auth-provider)
* [GitLab](#gitlab-auth-provider)
* [LinkedIn](#linkedin-auth-provider)
The provider can be selected using the `provider` configuration value.
### Google Auth Provider
For Google, the registration steps are:
1. Create a new project: https://console.developers.google.com/project
2. Choose the new project from the top right project dropdown (only if another project is selected)
3. In the project Dashboard center pane, choose **"API Manager"**
4. In the left Nav pane, choose **"Credentials"**
5. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save.
6. In the center pane, choose **"Credentials"** tab.
* Open the **"New credentials"** drop down
* Choose **"OAuth client ID"**
* Choose **"Web application"**
* Application name is freeform, choose something appropriate
* Authorized JavaScript origins is your domain ex: `https://internal.yourcompany.com`
* Authorized redirect URIs is the location of oauth2/callback ex: `https://internal.yourcompany.com/oauth2/callback`
* Choose **"Create"**
4. Take note of the **Client ID** and **Client Secret**
It's recommended to refresh sessions on a short interval (1h) with `cookie-refresh` setting which validates that the account is still authorized.
#### Restrict auth to specific Google groups on your domain. (optional)
1. Create a service account: https://developers.google.com/identity/protocols/OAuth2ServiceAccount and make sure to download the json file.
2. Make note of the Client ID for a future step.
3. Under "APIs & Auth", choose APIs.
4. Click on Admin SDK and then Enable API.
5. Follow the steps on https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account and give the client id from step 2 the following oauth scopes:
```
https://www.googleapis.com/auth/admin.directory.group.readonly
https://www.googleapis.com/auth/admin.directory.user.readonly
```
6. Follow the steps on https://support.google.com/a/answer/60757 to enable Admin API access.
7. Create or choose an existing administrative email address on the Gmail domain to assign to the ```google-admin-email``` flag. This email will be impersonated by this client to make calls to the Admin SDK. See the note on the link from step 5 for the reason why.
8. Create or choose an existing email group and set that email to the ```google-group``` flag. You can pass multiple instances of this flag with different groups
and the user will be checked against all the provided groups.
9. Lock down the permissions on the json file downloaded from step 1 so only oauth2_proxy is able to read the file and set the path to the file in the ```google-service-account-json``` flag.
10. Restart oauth2_proxy.
Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ).
### Azure Auth Provider
1. [Add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/) to your Azure Active Directory tenant.
2. On the App properties page provide the correct Sign-On URL ie `https://internal.yourcompany.com/oauth2/callback`
3. If applicable take note of your `TenantID` and provide it via the `--azure-tenant=<YOUR TENANT ID>` commandline option. Default the `common` tenant is used.
The Azure AD auth provider uses `openid` as it default scope. It uses `https://graph.windows.net` as a default protected resource. It call to `https://graph.windows.net/me` to get the email address of the user that logs in.
### Facebook Auth Provider
1. Create a new FB App from <https://developers.facebook.com/>
2. Under FB Login, set your Valid OAuth redirect URIs to `https://internal.yourcompany.com/oauth2/callback`
### GitHub Auth Provider
1. Create a new project: https://github.com/settings/developers
2. Under `Authorization callback URL` enter the correct url ie `https://internal.yourcompany.com/oauth2/callback`
The GitHub auth provider supports two additional parameters to restrict authentication to Organization or Team level access. Restricting by org and team is normally accompanied with `--email-domain=*`
-github-org="": restrict logins to members of this organisation
-github-team="": restrict logins to members of any of these teams (slug), separated by a comma
If you are using GitHub enterprise, make sure you set the following to the appropriate url:
-login-url="http(s)://<enterprise github host>/login/oauth/authorize"
-redeem-url="http(s)://<enterprise github host>/login/oauth/access_token"
-validate-url="http(s)://<enterprise github host>/api/v3"
### 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)
If you are using self-hosted GitLab, make sure you set the following to the appropriate URL:
-login-url="<your gitlab url>/oauth/authorize"
-redeem-url="<your gitlab url>/oauth/token"
-validate-url="<your gitlab url>/api/v4/user"
### LinkedIn Auth Provider
For LinkedIn, the registration steps are:
1. Create a new project: https://www.linkedin.com/secure/developer
2. In the OAuth User Agreement section:
* In default scope, select r_basicprofile and r_emailaddress.
* In "OAuth 2.0 Redirect URLs", enter `https://internal.yourcompany.com/oauth2/callback`
3. Fill in the remaining required fields and Save.
4. Take note of the **Consumer Key / API Key** and **Consumer Secret / Secret Key**
### 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/).
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.
### OpenID Connect Provider
OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects. This provider was originally built against CoreOS Dex and we will use it as an example.
1. Launch a Dex instance using the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md).
2. Setup oauth2_proxy with the correct provider and using the default ports and callbacks.
3. Login with the fixture use in the dex guide and run the oauth2_proxy with the following args:
-provider oidc
-client-id oauth2_proxy
-client-secret proxy
-redirect-url http://127.0.0.1:4180/oauth2/callback
-oidc-issuer-url http://127.0.0.1:5556
-cookie-secure=false
-email-domain example.com
## Email Authentication
To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`.
## Configuration
`oauth2_proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables).
To generate a strong cookie secret use `python -c 'import os,base64; print base64.urlsafe_b64encode(os.urandom(16))'`
### 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`
### Command Line Options
```
Usage of oauth2_proxy:
-approval-prompt string: OAuth approval_prompt (default "force")
-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")
-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-secret string: the OAuth Client Secret
-config string: path to config file
-cookie-domain string: an optional cookie domain to force cookies to (ie: .yourcompany.com)
-cookie-expire duration: expire timeframe for cookie (default 168h0m0s)
-cookie-httponly: set HttpOnly cookie flag (default true)
-cookie-name string: the name of the cookie that the oauth_proxy creates (default "_oauth2_proxy")
-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-secure: set secure (HTTPS) cookie flag (default true)
-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)
-email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
-footer string: custom footer string. Use "-" to disable default footer.
-github-org string: restrict logins to members of this organisation
-github-team string: restrict logins to members of any of these teams (slug), separated by a comma
-google-admin-email string: the google admin to impersonate for api calls
-google-group value: restrict logins to members of this google group (may be given multiple times).
-google-service-account-json string: the path to the service account json credentials
-htpasswd-file string: additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
-http-address string: [http://]<addr>:<port> or unix://<path> to listen on for HTTP clients (default "127.0.0.1:4180")
-https-address string: <addr>:<port> to listen on for HTTPS clients (default ":443")
-login-url string: Authentication endpoint
-pass-access-token: pass OAuth access_token to upstream via X-Forwarded-Access-Token header
-pass-basic-auth: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream (default true)
-pass-host-header: pass the request Host Header to upstream (default true)
-pass-user-headers: pass X-Forwarded-User and X-Forwarded-Email information to upstream (default true)
-profile-url string: Profile access endpoint
-provider string: OAuth provider (default "google")
-proxy-prefix string: the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in) (default "/oauth2")
-redeem-url string: Token redemption endpoint
-redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
-request-logging: Log requests to stdout (default true)
-request-logging-format: Template for request log lines (see "Logging Format" paragraph below)
-resource string: The resource that is protected (Azure AD only)
-scope string: OAuth scope specification
-set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)
-signature-key string: GAP-Signature request signature key (algorithm:secretkey)
-skip-auth-preflight: will skip authentication for OPTIONS requests
-skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)
-skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start
-ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS
-tls-cert string: path to certificate file
-tls-key string: path to private key file
-upstream value: 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: print version string
```
See below for provider specific options
### Upstreams Configuration
`oauth2_proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter, that will forward all authenticated requests to be forwarded to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream.
Static file paths are configured as a file:// URL. `file:///var/www/static/` will serve the files from that directory at `http://[oauth2_proxy url]/var/www/static/`, which may not be what you want. You can provide the path to where the files should be available by adding a fragment to the configured URL. The value of the fragment will then be used to specify which path the files are available at. `file:///var/www/static/#/static/` will ie. make `/var/www/static/` available at `http://[oauth2_proxy url]/static/`.
Multiple upstreams can either be configured by supplying a comma separated list to the `-upstream` parameter, supplying the parameter multiple times or provinding a list in the [config file](#config-file). When multiple upstreams are used routing to them will be based on the path they are set up with.
### Environment variables
The following environment variables can be used in place of the corresponding command-line arguments:
- `OAUTH2_PROXY_CLIENT_ID`
- `OAUTH2_PROXY_CLIENT_SECRET`
- `OAUTH2_PROXY_COOKIE_NAME`
- `OAUTH2_PROXY_COOKIE_SECRET`
- `OAUTH2_PROXY_COOKIE_DOMAIN`
- `OAUTH2_PROXY_COOKIE_EXPIRE`
- `OAUTH2_PROXY_COOKIE_REFRESH`
- `OAUTH2_PROXY_SIGNATURE_KEY`
## SSL Configuration
There are two recommended configurations.
1) Configure SSL Terminiation with OAuth2 Proxy by providing a `--tls-cert=/path/to/cert.pem` and `--tls-key=/path/to/cert.key`.
The command line to run `oauth2_proxy` in this configuration would look like this:
```bash
./oauth2_proxy \
--email-domain="yourcompany.com" \
--upstream=http://127.0.0.1:8080/ \
--tls-cert=/path/to/cert.pem \
--tls-key=/path/to/cert.key \
--cookie-secret=... \
--cookie-secure=true \
--provider=... \
--client-id=... \
--client-secret=...
```
2) Configure SSL Termination with [Nginx](http://nginx.org/) (example config below), Amazon ELB, Google Cloud Platform Load Balancing, or ....
Because `oauth2_proxy` listens on `127.0.0.1:4180` by default, to listen on all interfaces (needed when using an
external load balancer like Amazon ELB or Google Platform Load Balancing) use `--http-address="0.0.0.0:4180"` or
`--http-address="http://:4180"`.
Nginx will listen on port `443` and handle SSL connections while proxying to `oauth2_proxy` on port `4180`.
`oauth2_proxy` will then authenticate requests for an upstream application. The external endpoint for this example
would be `https://internal.yourcompany.com/`.
An example Nginx config follows. Note the use of `Strict-Transport-Security` header to pin requests to SSL
via [HSTS](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security):
```
server {
listen 443 default ssl;
server_name internal.yourcompany.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/cert.key;
add_header Strict-Transport-Security max-age=2592000;
location / {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_connect_timeout 1;
proxy_send_timeout 30;
proxy_read_timeout 30;
}
}
```
The command line to run `oauth2_proxy` in this configuration would look like this:
```bash
./oauth2_proxy \
--email-domain="yourcompany.com" \
--upstream=http://127.0.0.1:8080/ \
--cookie-secret=... \
--cookie-secure=true \
--provider=... \
--client-id=... \
--client-secret=...
```
## Endpoint Documentation
OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable.
* /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info
* /ping - returns an 200 OK response
* /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
* /oauth2/start - a URL that will redirect to start the OAuth cycle
* /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url.
* /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request)
## Request signatures
If `signature_key` is defined, proxied requests will be signed with the
`GAP-Signature` header, which is a [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`
in `oauthproxy.go`](./oauthproxy.go).
`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`)
For more information about HMAC request signature validation, read the
following:
* [Amazon Web Services: Signing and Authenticating REST
Requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html)
* [rc3.org: Using HMAC to authenticate Web service
requests](http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/)
## Logging Format
By default, OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log.
```
<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>
```
If you require a different format than that, you can configure it with the `-request-logging-format` flag.
The default format is configured as follows:
```
{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}
```
[See `logMessageData` in `logging_handler.go`](./logging_handler.go) for all available variables.
## Adding a new Provider
Follow the examples in the [`providers` package](providers/) to define a new
`Provider` instance. Add a new `case` to
[`providers.New()`](providers/providers.go) to allow `oauth2_proxy` to use the
new `Provider`.
## <a name="nginx-auth-request"></a>Configuring for use with the Nginx `auth_request` directive
The [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) allows Nginx to authenticate requests via the oauth2_proxy's `/auth` endpoint, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the request through. For example:
```nginx
server {
listen 443 ssl;
server_name ...;
include ssl/ssl.conf;
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri;
}
location = /oauth2/auth {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
# nginx auth_request includes headers but not body
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
# pass information via X-User and X-Email headers to backend,
# requires running with --set-xauthrequest flag
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
# if you enabled --cookie-refresh, this is needed for it to work with auth_request
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
proxy_pass http://backend/;
# or "root /path/to/site;" or "fastcgi_pass ..." etc
}
}
```

View File

@ -1,25 +1,24 @@
package requests package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"github.com/bitly/go-simplejson" "github.com/bitly/go-simplejson"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
// Request parses the request body into a simplejson.Json object
func Request(req *http.Request) (*simplejson.Json, error) { func Request(req *http.Request) (*simplejson.Json, error) {
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
logger.Printf("%s %s %s", req.Method, req.URL, err) log.Printf("%s %s %s", req.Method, req.URL, err)
return nil, err return nil, err
} }
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
logger.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body) log.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -33,16 +32,15 @@ func Request(req *http.Request) (*simplejson.Json, error) {
return data, nil return data, nil
} }
// RequestJSON parses the request body into the given interface func RequestJson(req *http.Request, v interface{}) error {
func RequestJSON(req *http.Request, v interface{}) error {
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
logger.Printf("%s %s %s", req.Method, req.URL, err) log.Printf("%s %s %s", req.Method, req.URL, err)
return err return err
} }
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
logger.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body) log.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body)
if err != nil { if err != nil {
return err return err
} }
@ -52,7 +50,6 @@ func RequestJSON(req *http.Request, v interface{}) error {
return json.Unmarshal(body, v) return json.Unmarshal(body, v)
} }
// RequestUnparsedResponse performs a GET and returns the raw response object
func RequestUnparsedResponse(url string, header http.Header) (resp *http.Response, err error) { func RequestUnparsedResponse(url string, header http.Header) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {

View File

@ -1,21 +1,20 @@
package requests package api
import ( import (
"github.com/bitly/go-simplejson"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"github.com/bitly/go-simplejson"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func testBackend(responseCode int, payload string) *httptest.Server { func testBackend(response_code int, payload string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc( return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(responseCode) w.WriteHeader(response_code)
w.Write([]byte(payload)) w.Write([]byte(payload))
})) }))
} }

135
configure vendored
View File

@ -1,135 +0,0 @@
#!/usr/bin/env bash
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
declare -A tools=()
declare -A desired=()
for arg in "$@"; do
case ${arg%%=*} in
"--with-go")
desired[go]="${arg##*=}"
;;
"--help")
printf "${GREEN}$0${NC}\n"
printf " available options:\n"
printf " --with-go=${BLUE}<path_to_go_binary>${NC}\n"
exit 0
;;
*)
echo "Unknown option: $arg"
exit 2
;;
esac
done
vercomp () {
if [[ $1 == $2 ]]
then
return 0
fi
local IFS=.
local i ver1=($1) ver2=($2)
# fill empty fields in ver1 with zeros
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
do
ver1[i]=0
done
for ((i=0; i<${#ver1[@]}; i++))
do
if [[ -z ${ver2[i]} ]]
then
# fill empty fields in ver2 with zeros
ver2[i]=0
fi
if ((10#${ver1[i]} > 10#${ver2[i]}))
then
return 1
fi
if ((10#${ver1[i]} < 10#${ver2[i]}))
then
return 2
fi
done
return 0
}
check_for() {
echo -n "Checking for $1... "
if ! [ -z "${desired[$1]}" ]; then
TOOL_PATH="${desired[$1]}"
else
TOOL_PATH=$(command -v $1)
fi
if ! [ -x "$TOOL_PATH" -a -f "$TOOL_PATH" ]; then
printf "${RED}not found${NC}\n"
cd -
exit 1
else
printf "${GREEN}found${NC}\n"
tools[$1]=$TOOL_PATH
fi
}
check_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)}')
vercomp $GO_VERSION 1.12
case $? in
0) ;&
1)
printf "${GREEN}"
echo $GO_VERSION
printf "${NC}"
;;
2)
printf "${RED}"
echo "$GO_VERSION < 1.12"
exit 1
;;
esac
VERSION=$(${tools[go]} version | ${tools[awk]} '{print $3}')
tools["go_version"]="${VERSION}"
}
check_docker_version() {
echo -n "Checking docker version... "
DOCKER_VERSION=$(${tools[docker]} version | ${tools[awk]})
}
check_go_env() {
echo -n "Checking \$GOPATH... "
GOPATH="$(go env GOPATH)"
if [ -z "$GOPATH" ]; then
printf "${RED}invalid${NC} - GOPATH not set\n"
exit 1
fi
printf "${GREEN}valid${NC} - $GOPATH\n"
}
cd ${0%/*}
rm -fv .env
check_for make
check_for awk
check_for go
check_go_version
check_go_env
check_for golangci-lint
echo
cat <<- EOF > .env
MAKE := "${tools[make]}"
GO := "${tools[go]}"
GO_VERSION := ${tools[go_version]}
GOLANGCILINT := "${tools[golangci-lint]}"
EOF
echo "Environment configuration written to .env"
cd - > /dev/null

View File

@ -1,5 +1,5 @@
## OAuth2 Proxy Config File ## OAuth2 Proxy Config File
## https://github.com/pusher/oauth2_proxy ## https://github.com/bitly/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"
@ -18,18 +18,8 @@
# "http://127.0.0.1:8080/" # "http://127.0.0.1:8080/"
# ] # ]
## Logging configuration ## Log requests to stdout
#logging_filename = "" # request_logging = true
#logging_max_size = 100
#logging_max_age = 7
#logging_local_time = true
#logging_compress = false
#standard_logging = true
#standard_logging_format = "[{{.Timestamp}}] [{{.File}}] {{.Message}}"
#request_logging = true
#request_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}"
#auth_logging = true
#auth_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] [{{.Status}}] {{.Message}}"
## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream ## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
# pass_basic_auth = true # pass_basic_auth = true

View File

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

View File

@ -1,4 +1,4 @@
package encryption package cookie
import ( import (
"encoding/base64" "encoding/base64"
@ -24,11 +24,10 @@ func TestEncodeAndDecodeAccessToken(t *testing.T) {
} }
func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { func TestEncodeAndDecodeAccessTokenB64(t *testing.T) {
const secretBase64 = "A3Xbr6fu6Al0HkgrP1ztjb-mYiwmxgNPP-XbNsz1WBk=" const secret_b64 = "A3Xbr6fu6Al0HkgrP1ztjb-mYiwmxgNPP-XbNsz1WBk="
const token = "my access token" const token = "my access token"
secret, err := base64.URLEncoding.DecodeString(secretBase64) secret, err := base64.URLEncoding.DecodeString(secret_b64)
assert.Equal(t, nil, err)
c, err := NewCipher([]byte(secret)) c, err := NewCipher([]byte(secret))
assert.Equal(t, nil, err) assert.Equal(t, nil, err)

View File

@ -1,11 +1,10 @@
package encryption package cookie
import ( import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
) )
// Nonce generates a random 16 byte string to be used as a nonce
func Nonce() (nonce string, err error) { func Nonce() (nonce string, err error) {
b := make([]byte, 16) b := make([]byte, 16)
_, err = rand.Read(b) _, err = rand.Read(b)

3
docs/.gitignore vendored
View File

@ -1,3 +0,0 @@
_site
.sass-cache
.jekyll-metadata

View File

@ -1,23 +0,0 @@
---
layout: default
title: Home
permalink: /
nav_order: 0
---
# oauth2_proxy
A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others)
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.
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]({{ site.gitweb }}/CHANGELOG.md).
[![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy)
![Sign In Page](https://cloud.githubusercontent.com/assets/45028/4970624/7feb7dd8-6886-11e4-93e0-c9904af44ea8.png)
## Architecture
![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png)

View File

@ -1,27 +0,0 @@
---
layout: default
title: Installation
permalink: /installation
nav_order: 1
---
## Installation
1. Choose how to deploy:
a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v3.2.0`)
b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin`
c. Using the prebuilt docker image [quay.io/pusher/oauth2_proxy](https://quay.io/pusher/oauth2_proxy) (AMD64, ARMv6 and ARM64 tags available)
Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`.
```
sha256sum -c sha256sum.txt 2>&1 | grep OK
oauth2_proxy-3.2.0.linux-amd64: OK
```
2. [Select a Provider and Register an OAuth Application with a Provider](auth-configuration)
3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration)
4. [Configure SSL or Deploy behind a SSL endpoint](tls-configuration) (example provided for Nginx)

View File

@ -1,298 +0,0 @@
---
layout: default
title: Auth Configuration
permalink: /auth-configuration
nav_order: 2
---
## OAuth Provider Configuration
You will need to register an OAuth application with a Provider (Google, GitHub or another provider), and configure it with Redirect URI(s) for the domain you intend to run `oauth2_proxy` on.
Valid providers are :
- [Google](#google-auth-provider) _default_
- [Azure](#azure-auth-provider)
- [Facebook](#facebook-auth-provider)
- [GitHub](#github-auth-provider)
- [Keycloak](#keycloak-auth-provider)
- [GitLab](#gitlab-auth-provider)
- [LinkedIn](#linkedin-auth-provider)
- [login.gov](#logingov-provider)
The provider can be selected using the `provider` configuration value.
### Google Auth Provider
For Google, the registration steps are:
1. Create a new project: https://console.developers.google.com/project
2. Choose the new project from the top right project dropdown (only if another project is selected)
3. In the project Dashboard center pane, choose **"API Manager"**
4. In the left Nav pane, choose **"Credentials"**
5. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save.
6. In the center pane, choose **"Credentials"** tab.
- Open the **"New credentials"** drop down
- Choose **"OAuth client ID"**
- Choose **"Web application"**
- Application name is freeform, choose something appropriate
- Authorized JavaScript origins is your domain ex: `https://internal.yourcompany.com`
- Authorized redirect URIs is the location of oauth2/callback ex: `https://internal.yourcompany.com/oauth2/callback`
- Choose **"Create"**
7. Take note of the **Client ID** and **Client Secret**
It's recommended to refresh sessions on a short interval (1h) with `cookie-refresh` setting which validates that the account is still authorized.
#### Restrict auth to specific Google groups on your domain. (optional)
1. Create a service account: https://developers.google.com/identity/protocols/OAuth2ServiceAccount and make sure to download the json file.
2. Make note of the Client ID for a future step.
3. Under "APIs & Auth", choose APIs.
4. Click on Admin SDK and then Enable API.
5. Follow the steps on https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account and give the client id from step 2 the following oauth scopes:
```
https://www.googleapis.com/auth/admin.directory.group.readonly
https://www.googleapis.com/auth/admin.directory.user.readonly
```
6. Follow the steps on https://support.google.com/a/answer/60757 to enable Admin API access.
7. Create or choose an existing administrative email address on the Gmail domain to assign to the `google-admin-email` flag. This email will be impersonated by this client to make calls to the Admin SDK. See the note on the link from step 5 for the reason why.
8. Create or choose an existing email group and set that email to the `google-group` flag. You can pass multiple instances of this flag with different groups
and the user will be checked against all the provided groups.
9. Lock down the permissions on the json file downloaded from step 1 so only oauth2_proxy is able to read the file and set the path to the file in the `google-service-account-json` flag.
10. Restart oauth2_proxy.
Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ).
### Azure Auth Provider
1. Add an application: go to [https://portal.azure.com](https://portal.azure.com), choose **"Azure Active Directory"** in the left menu, select **"App registrations"** and then click on **"New app registration"**.
2. Pick a name and choose **"Webapp / API"** as application type. Use `https://internal.yourcompany.com` as Sign-on URL. Click **"Create"**.
3. On the **"Settings"** / **"Properties"** page of the app, pick a logo and select **"Multi-tenanted"** if you want to allow users from multiple organizations to access your app. Note down the application ID. Click **"Save"**.
4. On the **"Settings"** / **"Required Permissions"** page of the app, click on **"Windows Azure Active Directory"** and then on **"Access the directory as the signed in user"**. Hit **"Save"** and then then on **"Grant permissions"** (you might need another admin to do this).
5. On the **"Settings"** / **"Reply URLs"** page of the app, add `https://internal.yourcompanycom/oauth2/callback` for each host that you want to protect by the oauth2 proxy. Click **"Save"**.
6. On the **"Settings"** / **"Keys"** page of the app, add a new key and note down the value after hitting **"Save"**.
7. Configure the proxy with
```
--provider=azure
--client-id=<application ID from step 3>
--client-secret=<value from step 6>
```
### Facebook Auth Provider
1. Create a new FB App from <https://developers.facebook.com/>
2. Under FB Login, set your Valid OAuth redirect URIs to `https://internal.yourcompany.com/oauth2/callback`
### GitHub Auth Provider
1. Create a new project: https://github.com/settings/developers
2. Under `Authorization callback URL` enter the correct url ie `https://internal.yourcompany.com/oauth2/callback`
The GitHub auth provider supports two additional parameters to restrict authentication to Organization or Team level access. Restricting by org and team is normally accompanied with `--email-domain=*`
-github-org="": restrict logins to members of this organisation
-github-team="": restrict logins to members of any of these teams (slug), separated by a comma
If you are using GitHub enterprise, make sure you set the following to the appropriate url:
-login-url="http(s)://<enterprise github host>/login/oauth/authorize"
-redeem-url="http(s)://<enterprise github host>/login/oauth/access_token"
-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
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:
-oidc-issuer-url="<your gitlab url>"
### LinkedIn Auth Provider
For LinkedIn, the registration steps are:
1. Create a new project: https://www.linkedin.com/secure/developer
2. In the OAuth User Agreement section:
- In default scope, select r_basicprofile and r_emailaddress.
- In "OAuth 2.0 Redirect URLs", enter `https://internal.yourcompany.com/oauth2/callback`
3. Fill in the remaining required fields and Save.
4. Take note of the **Consumer Key / API Key** and **Consumer Secret / Secret Key**
### Microsoft Azure AD Provider
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.
### OpenID Connect Provider
OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects. This provider was originally built against CoreOS Dex and we will use it as an example.
1. Launch a Dex instance using the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md).
2. Setup oauth2_proxy with the correct provider and using the default ports and callbacks.
3. Login with the fixture use in the dex guide and run the oauth2_proxy with the following args:
-provider oidc
-client-id oauth2_proxy
-client-secret proxy
-redirect-url http://127.0.0.1:4180/oauth2/callback
-oidc-issuer-url http://127.0.0.1:5556
-cookie-secure=false
-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 is an OIDC provider for the US Government.
If you are a US Government agency, you can contact the login.gov team through the contact information
that you can find on https://login.gov/developers/ and work with them to understand how to get login.gov
accounts for integration/test and production access.
A developer guide is available here: https://developers.login.gov/, though this proxy handles everything
but the data you need to create to register your application in the login.gov dashboard.
As a demo, we will assume that you are running your application that you want to secure locally on
http://localhost:3000/, that you will be starting your proxy up on http://localhost:4180/, and that
you have an agency integration account for testing.
First, register your application in the dashboard. The important bits are:
* Identity protocol: make this `Openid connect`
* Issuer: do what they say for OpenID Connect. We will refer to this string as `${LOGINGOV_ISSUER}`.
* Public key: This is a self-signed certificate in .pem format generated from a 2048 bit RSA private key.
A quick way to do this is `openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes -subj '/C=US/ST=Washington/L=DC/O=GSA/OU=18F/CN=localhost'`,
The contents of the `key.pem` shall be referred to as `${OAUTH2_PROXY_JWT_KEY}`.
* Return to App URL: Make this be `http://localhost:4180/`
* Redirect URIs: Make this be `http://localhost:4180/oauth2/callback`.
* Attribute Bundle: Make sure that email is selected.
Now start the proxy up with the following options:
```
./oauth2_proxy -provider login.gov \
-client-id=${LOGINGOV_ISSUER} \
-redirect-url=http://localhost:4180/oauth2/callback \
-oidc-issuer-url=https://idp.int.identitysandbox.gov/ \
-cookie-secure=false \
-email-domain=gsa.gov \
-upstream=http://localhost:3000/ \
-cookie-secret=somerandomstring12341234567890AB \
-cookie-domain=localhost \
-skip-provider-button=true \
-pubjwk-url=https://idp.int.identitysandbox.gov/api/openid_connect/certs \
-profile-url=https://idp.int.identitysandbox.gov/api/openid_connect/userinfo \
-jwt-key="${OAUTH2_PROXY_JWT_KEY}"
```
You can also set all these options with environment variables, for use in cloud/docker environments.
One tricky thing that you may encounter is that some cloud environments will pass in environment
variables in a docker env-file, which does not allow multiline variables like a PEM file.
If you encounter this, then you can create a `jwt_signing_key.pem` file in the top level
directory of the repo which contains the key in PEM format and then do your docker build.
The docker build process will copy that file into your image which you can then access by
setting the `OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem`
environment variable, or by setting `-jwt-key-file=/etc/ssl/private/jwt_signing_key.pem` on the commandline.
Once it is running, you should be able to go to `http://localhost:4180/` in your browser,
get authenticated by the login.gov integration server, and then get proxied on to your
application running on `http://localhost:3000/`. In a real deployment, you would secure
your application with a firewall or something so that it was only accessible from the
proxy, and you would use real hostnames everywhere.
#### Skip OIDC discovery
Some providers do not support OIDC discovery via their issuer URL, so oauth2_proxy cannot simply grab the authorization, token and jwks URI endpoints from the provider's metadata.
In this case, you can set the `-skip-oidc-discovery` option, and supply those required endpoints manually:
```
-provider oidc
-client-id oauth2_proxy
-client-secret proxy
-redirect-url http://127.0.0.1:4180/oauth2/callback
-oidc-issuer-url http://127.0.0.1:5556
-skip-oidc-discovery
-login-url http://127.0.0.1:5556/authorize
-redeem-url http://127.0.0.1:5556/token
-oidc-jwks-url http://127.0.0.1:5556/keys
-cookie-secure=false
-email-domain example.com
```
## Email Authentication
To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`.
## Adding a new Provider
Follow the examples in the [`providers` package]({{ site.gitweb }}/providers/) to define a new
`Provider` instance. Add a new `case` to
[`providers.New()`]({{ site.gitweb }}/providers/providers.go) to allow `oauth2_proxy` to use the
new `Provider`.

View File

@ -1,24 +0,0 @@
---
layout: default
---
<style type="text/css" media="screen">
.container {
margin: 10px auto;
max-width: 600px;
text-align: center;
}
h1 {
margin: 30px 0;
font-size: 4em;
line-height: 1;
letter-spacing: -1px;
}
</style>
<div class="container">
<h1>404</h1>
<p><strong>Page not found :(</strong></p>
<p>The requested page could not be found.</p>
</div>

View File

@ -1,73 +0,0 @@
---
layout: default
title: TLS Configuration
permalink: /tls-configuration
nav_order: 4
---
## SSL Configuration
There are two recommended configurations.
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:
```bash
./oauth2_proxy \
--email-domain="yourcompany.com" \
--upstream=http://127.0.0.1:8080/ \
--tls-cert-file=/path/to/cert.pem \
--tls-key-file=/path/to/cert.key \
--cookie-secret=... \
--cookie-secure=true \
--provider=... \
--client-id=... \
--client-secret=...
```
2. Configure SSL Termination with [Nginx](http://nginx.org/) (example config below), Amazon ELB, Google Cloud Platform Load Balancing, or ....
Because `oauth2_proxy` listens on `127.0.0.1:4180` by default, to listen on all interfaces (needed when using an
external load balancer like Amazon ELB or Google Platform Load Balancing) use `--http-address="0.0.0.0:4180"` or
`--http-address="http://:4180"`.
Nginx will listen on port `443` and handle SSL connections while proxying to `oauth2_proxy` on port `4180`.
`oauth2_proxy` will then authenticate requests for an upstream application. The external endpoint for this example
would be `https://internal.yourcompany.com/`.
An example Nginx config follows. Note the use of `Strict-Transport-Security` header to pin requests to SSL
via [HSTS](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security):
```
server {
listen 443 default ssl;
server_name internal.yourcompany.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/cert.key;
add_header Strict-Transport-Security max-age=2592000;
location / {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_connect_timeout 1;
proxy_send_timeout 30;
proxy_read_timeout 30;
}
}
```
The command line to run `oauth2_proxy` in this configuration would look like this:
```bash
./oauth2_proxy \
--email-domain="yourcompany.com" \
--upstream=http://127.0.0.1:8080/ \
--cookie-secret=... \
--cookie-secure=true \
--provider=... \
--client-id=... \
--client-secret=...
```

View File

@ -1,17 +0,0 @@
---
layout: default
title: Endpoints
permalink: /endpoints
nav_order: 5
---
## Endpoint Documentation
OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable.
- /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info
- /ping - returns a 200 OK response, which is intended for use with health checks
- /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
- /oauth2/start - a URL that will redirect to start the OAuth cycle
- /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url.
- /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request)

View File

@ -1,24 +0,0 @@
---
layout: default
title: Request Signatures
permalink: /request-signatures
nav_order: 6
---
## Request signatures
If `signature_key` is defined, proxied requests will be signed with the
`GAP-Signature` header, which is a [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`
in `oauthproxy.go`]({{ site.gitweb }}/oauthproxy.go).
`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`)
For more information about HMAC request signature validation, read the
following:
- [Amazon Web Services: Signing and Authenticating REST
Requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html)
- [rc3.org: Using HMAC to authenticate Web service
requests](http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/)

View File

@ -1,11 +0,0 @@
source "https://rubygems.org"
gem "github-pages", group: :jekyll_plugins
# just-the-docs Jekyll theme
gem "just-the-docs"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.0" if Gem.win_platform?

View File

@ -1,254 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.2.10)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.4)
dnsruby (1.61.2)
addressable (~> 2.5)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
ethon (0.12.0)
ffi (>= 1.3.0)
eventmachine (1.2.7)
execjs (2.7.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
ffi (1.10.0)
forwardable-extended (2.6.0)
gemoji (3.0.0)
github-pages (193)
activesupport (= 4.2.10)
github-pages-health-check (= 1.8.1)
jekyll (= 3.7.4)
jekyll-avatar (= 0.6.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.5)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.11.0)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.9.4)
jekyll-mentions (= 1.4.1)
jekyll-optional-front-matter (= 0.3.0)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.2.0)
jekyll-redirect-from (= 0.14.0)
jekyll-relative-links (= 0.5.3)
jekyll-remote-theme (= 0.3.1)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.5.0)
jekyll-sitemap (= 1.2.0)
jekyll-swiss (= 0.4.0)
jekyll-theme-architect (= 0.1.1)
jekyll-theme-cayman (= 0.1.1)
jekyll-theme-dinky (= 0.1.1)
jekyll-theme-hacker (= 0.1.1)
jekyll-theme-leap-day (= 0.1.1)
jekyll-theme-merlot (= 0.1.1)
jekyll-theme-midnight (= 0.1.1)
jekyll-theme-minimal (= 0.1.1)
jekyll-theme-modernist (= 0.1.1)
jekyll-theme-primer (= 0.5.3)
jekyll-theme-slate (= 0.1.1)
jekyll-theme-tactile (= 0.1.1)
jekyll-theme-time-machine (= 0.1.1)
jekyll-titles-from-headings (= 0.5.1)
jemoji (= 0.10.1)
kramdown (= 1.17.0)
liquid (= 4.0.0)
listen (= 3.1.5)
mercenary (~> 0.3)
minima (= 2.5.0)
nokogiri (>= 1.8.2, < 2.0)
rouge (= 2.2.1)
terminal-table (~> 1.4)
github-pages-health-check (1.8.1)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (~> 4.0)
public_suffix (~> 2.0)
typhoeus (~> 1.3)
html-pipeline (2.10.0)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jekyll (3.7.4)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (~> 1.14)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.6.0)
jekyll (~> 3.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.2.0)
commonmarker (~> 0.14)
jekyll (>= 3.0, < 4.0)
jekyll-commonmark-ghpages (0.1.5)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1)
rouge (~> 2)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.11.0)
jekyll (~> 3.3)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.9.4)
jekyll (~> 3.1)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.4.1)
html-pipeline (~> 2.3)
jekyll (~> 3.0)
jekyll-optional-front-matter (0.3.0)
jekyll (~> 3.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.2.0)
jekyll (~> 3.0)
jekyll-redirect-from (0.14.0)
jekyll (~> 3.3)
jekyll-relative-links (0.5.3)
jekyll (~> 3.3)
jekyll-remote-theme (0.3.1)
jekyll (~> 3.5)
rubyzip (>= 1.2.1, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.5.0)
jekyll (~> 3.3)
jekyll-sitemap (1.2.0)
jekyll (~> 3.3)
jekyll-swiss (0.4.0)
jekyll-theme-architect (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.3)
jekyll (~> 3.5)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.1)
jekyll (~> 3.3)
jekyll-watch (2.1.2)
listen (~> 3.0)
jemoji (0.10.1)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (~> 3.0)
just-the-docs (0.1.6)
jekyll (~> 3.3)
rake (~> 10.0)
kramdown (1.17.0)
liquid (4.0.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
mercenary (0.3.6)
mini_portile2 (2.4.0)
minima (2.5.0)
jekyll (~> 3.5)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.11.3)
multipart-post (2.0.0)
nokogiri (1.10.4)
mini_portile2 (~> 2.4.0)
octokit (4.13.0)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (2.0.5)
rake (10.5.0)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
rouge (2.2.1)
ruby-enum (0.7.2)
i18n
ruby_dep (1.5.0)
rubyzip (1.2.2)
safe_yaml (1.0.4)
sass (3.7.3)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.3.1)
ethon (>= 0.9.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.4.1)
PLATFORMS
ruby
DEPENDENCIES
github-pages
just-the-docs
tzinfo-data
BUNDLED WITH
2.0.1

View File

@ -1,18 +0,0 @@
.PHONY: ruby
ruby:
@ if [ ! $$(which ruby) ]; then \
echo "Please install ruby version 2.1.0 or higher"; \
fi
.PHONY: bundle
bundle: ruby
@ if [ ! $$(which bundle) ]; then \
echo "Please install bundle: `gem install bundler`"; \
fi
vendor/bundle: bundle
bundle install --path vendor/bundle
.PHONY: serve
serve: vendor/bundle
bundle exec jekyll serve

View File

@ -1,13 +0,0 @@
# Docs
This folder contains our Jekyll based docs site which is hosted at
https://pusher.github.io/oauth2_proxy.
When making changes to this docs site, please test your changes locally:
```bash
make serve
```
To run the docs site locally you will need Ruby at version 2.1.0 or
higher and `bundle` (`gem install bundler` if you already have Ruby).

View File

@ -1,43 +0,0 @@
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely edit after that. If you find
# yourself editing this file very often, consider using Jekyll's data files
# feature for the data you need to update frequently.
#
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
# Site settings
# These are used to personalize your new site. If you look in the HTML files,
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.
title: OAuth2_Proxy
description: >- # this means to ignore newlines until "baseurl:"
OAuth2_Proxy documentation site
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
gitweb: "https://github.com/pusher/oauth2_proxy/blob/master"
# Build settings
markdown: kramdown
remote_theme: pmarsceill/just-the-docs
search_enabled: true
# Aux links for the upper right navigation
aux_links:
"OAuth2_Proxy on GitHub":
- "https://github.com/pusher/oauth2_proxy"
# Exclude from processing.
# The following items will not be processed, by default. Create a custom list
# to override the default setting.
# exclude:
# - Gemfile
# - Gemfile.lock
# - node_modules
# - vendor/bundle/
# - vendor/cache/
# - vendor/gems/
# - vendor/ruby/

View File

@ -1,12 +0,0 @@
---
---
{
{% for page in site.html_pages %}"{{ forloop.index0 }}": {
"id": "{{ forloop.index0 }}",
"title": "{{ page.title | xml_escape }}",
"content": "{{ page.content | markdownify | strip_html | xml_escape | remove: 'Table of contents' | strip_newlines | replace: '\', ' ' }}",
"url": "{{ page.url | absolute_url | xml_escape }}",
"relUrl": "{{ page.url | xml_escape }}"
}{% if forloop.last %}{% else %},
{% endif %}{% endfor %}
}

View File

@ -1,323 +0,0 @@
---
layout: default
title: Configuration
permalink: /docs/configuration
has_children: true
nav_order: 3
---
## Configuration
`oauth2_proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables).
To generate a strong cookie secret use `python -c 'import os,base64; print base64.urlsafe_b64encode(os.urandom(16))'`
### Config File
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
| Option | Type | Description | Default |
| ------ | ---- | ----------- | ------- |
| `-acr-values` | string | optional, used by login.gov | `"http://idmanagement.gov/ns/assurance/loa/1"` |
| `-approval-prompt` | string | OAuth approval_prompt | `"force"` |
| `-auth-logging` | bool | Log authentication attempts | true |
| `-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) | |
| `-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 | |
| `-client-id` | string | the OAuth Client ID: ie: `"123456.apps.googleusercontent.com"` | |
| `-client-secret` | string | the OAuth Client Secret | |
| `-config` | string | path to config file | |
| `-cookie-domain` | string | an optional cookie domain to force cookies to (ie: `.yourcompany.com`) | |
| `-cookie-expire` | duration | expire timeframe for cookie | 168h0m0s |
| `-cookie-httponly` | bool | set HttpOnly cookie flag | true |
| `-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/`) | `"/"` |
| `-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-secure` | bool | set secure (HTTPS) cookie flag | true |
| `-custom-templates-dir` | string | path to custom html templates | |
| `-display-htpasswd-form` | bool | display username / password login form if an htpasswd file is provided | true |
| `-email-domain` | string | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | |
| `-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`) | |
| `-exclude-logging-paths` | string | comma separated list of paths to exclude from logging, eg: `"/ping,/path2"` |`""` (no paths excluded) |
| `-flush-interval` | duration | period between flushing response buffers when streaming responses | `"1s"` |
| `-banner` | string | custom banner string. Use `"-"` to disable default banner. | |
| `-footer` | string | custom footer string. Use `"-"` to disable default footer. | |
| `-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 |
| `-github-org` | string | restrict logins to members of this organisation | |
| `-github-team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
| `-gitlab-group` | string | restrict logins to members of any of these groups (slug), separated by a comma | |
| `-google-admin-email` | string | the google admin to impersonate for api calls | |
| `-google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
| `-google-service-account-json` | string | the path to the service account json credentials | |
| `-htpasswd-file` | string | additionally authenticate against a htpasswd file. Entries must be created with `htpasswd -s` for SHA encryption | |
| `-http-address` | string | `[http://]<addr>:<port>` or `unix://<path>` to listen on for HTTP clients | `"127.0.0.1:4180"` |
| `-https-address` | string | `<addr>:<port>` to listen on for HTTPS clients | `":443"` |
| `-logging-compress` | bool | Should rotated log files be compressed using gzip | false |
| `-logging-filename` | string | File to log requests to, empty for `stdout` | `""` (stdout) |
| `-logging-local-time` | bool | Use local time in log files and backup filenames instead of UTC | true (local time) |
| `-logging-max-age` | int | Maximum number of days to retain old log files | 7 |
| `-logging-max-backups` | int | Maximum number of old log files to retain; 0 to disable | 0 |
| `-logging-max-size` | int | Maximum size in megabytes of the log file before rotation | 100 |
| `-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 | |
| `-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 | |
| `-login-url` | string | Authentication endpoint | |
| `-insecure-oidc-allow-unverified-email` | bool | don't fail if an email address in an id_token is not verified | false |
| `-oidc-issuer-url` | string | the OpenID Connect issuer URL. ie: `"https://accounts.google.com"` | |
| `-oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
| `-pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header | false |
| `-pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
| `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream | true |
| `-pass-host-header` | bool | pass the request Host Header to upstream | true |
| `-pass-user-headers` | bool | pass X-Forwarded-User and X-Forwarded-Email information to upstream | true |
| `-profile-url` | string | Profile access endpoint | |
| `-provider` | string | OAuth provider | google |
| `-ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
| `-proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
| `-proxy-websockets` | bool | enables WebSocket proxying | true |
| `-pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | |
| `-redeem-url` | string | Token redemption endpoint | |
| `-redirect-url` | string | the OAuth Redirect URL. ie: `"https://internalapp.yourcompany.com/oauth2/callback"` | |
| `-redis-connection-url` | string | URL of redis server for redis session storage (eg: `redis://HOST[:PORT]`) | |
| `-redis-sentinel-master-name` | string | Redis sentinel master name. Used in conjunction with `--redis-use-sentinel` | |
| `-redis-sentinel-connection-urls` | string \| list | List of Redis sentinel connection URLs (eg `redis://HOST[:PORT]`). Used in conjunction with `--redis-use-sentinel` | |
| `-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 |
| `-request-logging` | bool | Log requests | true |
| `-request-logging-format` | string | Template for request log lines | see [Logging Configuration](#logging-configuration) |
| `-resource` | string | The resource that is protected (Azure AD only) | |
| `-scope` | string | OAuth scope specification | |
| `-session-store-type` | string | Session data storage backend | cookie |
| `-set-xauthrequest` | bool | set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) | false |
| `-set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
| `-signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
| `-silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
| `-skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
| `-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.
See below for provider specific options
### Upstreams Configuration
`oauth2_proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter, that will forward all authenticated requests to be forwarded to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream.
Static file paths are configured as a file:// URL. `file:///var/www/static/` will serve the files from that directory at `http://[oauth2_proxy url]/var/www/static/`, which may not be what you want. You can provide the path to where the files should be available by adding a fragment to the configured URL. The value of the fragment will then be used to specify which path the files are available at. `file:///var/www/static/#/static/` will ie. make `/var/www/static/` available at `http://[oauth2_proxy url]/static/`.
Multiple upstreams can either be configured by supplying a comma separated list to the `-upstream` parameter, supplying the parameter multiple times or provinding a list in the [config file](#config-file). When multiple upstreams are used routing to them will be based on the path they are set up with.
### Environment variables
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`).
This is particularly useful for storing secrets outside of a configuration file
or the command line.
For example, the `--cookie-secret` flag becomes `OAUTH2_PROXY_COOKIE_SECRET`,
and the `--email-domain` flag becomes `OAUTH2_PROXY_EMAIL_DOMAINS`.
## Logging Configuration
By default, OAuth2 Proxy logs all output to stdout. Logging can be configured to output to a rotating log file using the `-logging-filename` command.
If logging to a file you can also configure the maximum file size (`-logging-max-size`), age (`-logging-max-age`), max backup logs (`-logging-max-backups`), and if backup logs should be compressed (`-logging-compress`).
There are three different types of logging: standard, authentication, and HTTP requests. These can each be enabled or disabled with `-standard-logging`, `-auth-logging`, and `-request-logging`.
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
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:
```
<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] [<STATUS>] <MESSAGE>
```
The status block will contain one of the below strings:
- `AuthSuccess` If a user has authenticated successfully by any method
- `AuthFailure` If the user failed to authenticate explicitly
- `AuthError` If there was an unexpected error during authentication
If you require a different format than that, you can configure it with the `-auth-logging-format` flag.
The default format is configured as follows:
```
{% raw %}{{.Client}} - {{.Username}} [{{.Timestamp}}] [{{.Status}}] {{.Message}}{% endraw %}
```
Available variables for auth logging:
| Variable | Example | Description |
| --- | --- | --- |
| Client | 74.125.224.72 | The client/remote IP address. Will use the X-Real-IP header it if exists. |
| Host | domain.com | The value of the Host header. |
| Protocol | HTTP/1.0 | The request protocol. |
| RequestMethod | GET | The request method. |
| Timestamp | 19/Mar/2015:17:20:19 -0400 | The date and time of the logging event. |
| UserAgent | - | The full user agent as reported by the requesting client. |
| Username | username@email.com | The email or username of the auth request. |
| Status | AuthSuccess | The status of the auth request. See above for details. |
| Message | Authenticated via OAuth2 | The details of the auth attempt. |
### Request Log Format
HTTP request logs will output by default in the below format:
```
<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>
```
If you require a different format than that, you can configure it with the `-request-logging-format` flag.
The default format is configured as follows:
```
{% raw %}{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}{% endraw %}
```
Available variables for request logging:
| Variable | Example | Description |
| --- | --- | --- |
| Client | 74.125.224.72 | The client/remote IP address. Will use the X-Real-IP header it if exists. |
| Host | domain.com | The value of the Host header. |
| Protocol | HTTP/1.0 | The request protocol. |
| RequestDuration | 0.001 | The time in seconds that a request took to process. |
| RequestMethod | GET | The request method. |
| RequestURI | "/oauth2/auth" | The URI path of the request. |
| ResponseSize | 12 | The size in bytes of the response. |
| StatusCode | 200 | The HTTP status code of the response. |
| Timestamp | 19/Mar/2015:17:20:19 -0400 | The date and time of the logging event. |
| Upstream | - | The upstream data of the HTTP request. |
| UserAgent | - | The full user agent as reported by the requesting client. |
| Username | username@email.com | The email or username of the auth request. |
### Standard Log Format
All other logging that is not covered by the above two types of logging will be output in this standard logging format. This includes configuration information at startup and errors that occur outside of a session. The default format is below:
```
[19/Mar/2015:17:20:19 -0400] [main.go:40] <MESSAGE>
```
If you require a different format than that, you can configure it with the `-standard-logging-format` flag. The default format is configured as follows:
```
{% raw %}[{{.Timestamp}}] [{{.File}}] {{.Message}}{% endraw %}
```
Available variables for standard logging:
| Variable | Example | Description |
| --- | --- | --- |
| Timestamp | 19/Mar/2015:17:20:19 -0400 | The date and time of the logging event. |
| File | main.go:40 | The file and line number of the logging statement. |
| Message | HTTP: listening on 127.0.0.1:4180 | The details of the log statement. |
## <a name="nginx-auth-request"></a>Configuring for use with the Nginx `auth_request` directive
The [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) allows Nginx to authenticate requests via the oauth2_proxy's `/auth` endpoint, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the request through. For example:
```nginx
server {
listen 443 ssl;
server_name ...;
include ssl/ssl.conf;
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri;
}
location = /oauth2/auth {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
# nginx auth_request includes headers but not body
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
# pass information via X-User and X-Email headers to backend,
# requires running with --set-xauthrequest flag
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
# if you enabled --pass-access-token, this will pass the token to the backend
auth_request_set $token $upstream_http_x_auth_request_access_token;
proxy_set_header X-Access-Token $token;
# if you enabled --cookie-refresh, this is needed for it to work with auth_request
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
# When using the --set-authorization-header flag, some provider's cookies can exceed the 4kb
# limit and so the OAuth2 Proxy splits these into multiple parts.
# Nginx normally only copies the first `Set-Cookie` header from the auth_request to the response,
# so if your cookies are larger than 4kb, you will need to extract additional cookies manually.
auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;
# Extract the Cookie attributes from the first Set-Cookie header and append them
# to the second part ($upstream_cookie_* variables only contain the raw cookie content)
if ($auth_cookie ~* "(; .*)") {
set $auth_cookie_name_0 $auth_cookie;
set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
}
# Send both Set-Cookie headers now if there was a second part
if ($auth_cookie_name_upstream_1) {
add_header Set-Cookie $auth_cookie_name_0;
add_header Set-Cookie $auth_cookie_name_1;
}
proxy_pass http://backend/;
# or "root /path/to/site;" or "fastcgi_pass ..." etc
}
}
```
If you use ingress-nginx in Kubernetes (which includes the Lua module), you also can use the following configuration snippet for your Ingress:
```yaml
nginx.ingress.kubernetes.io/auth-response-headers: Authorization
nginx.ingress.kubernetes.io/auth-signin: https://$host/oauth2/start?rd=$request_uri
nginx.ingress.kubernetes.io/auth-url: https://$host/oauth2/auth
nginx.ingress.kubernetes.io/configuration-snippet: |
auth_request_set $name_upstream_1 $upstream_cookie_name_1;
access_by_lua_block {
if ngx.var.name_upstream_1 ~= "" then
ngx.header["Set-Cookie"] = "name_1=" .. ngx.var.name_upstream_1 .. ngx.var.auth_cookie:match("(; .*)")
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

@ -1,67 +0,0 @@
---
layout: default
title: Sessions
permalink: /configuration
parent: Configuration
nav_order: 3
---
## Sessions
Sessions allow a user's authentication to be tracked between multiple HTTP
requests to a service.
The OAuth2 Proxy uses a Cookie to track user sessions and will store the session
data in one of the available session storage backends.
At present the available backends are (as passed to `--session-store-type`):
- [cookie](#cookie-storage) (default)
- [redis](#redis-storage)
### Cookie Storage
The Cookie storage backend is the default backend implementation and has
been used in the OAuth2 Proxy historically.
With the Cookie storage backend, all session information is stored in client
side cookies and transferred with each and every request.
The following should be known when using this implementation:
- Since all state is stored client side, this storage backend means that the OAuth2 Proxy is completely stateless
- Cookies are signed server side to prevent modification client-side
- It is recommended to set a `cookie-secret` which will ensure data is encrypted within the cookie data.
- 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
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.

View File

@ -6,36 +6,17 @@ import (
"strings" "strings"
) )
// EnvOptions holds program options loaded from the process environment
type EnvOptions map[string]interface{} type EnvOptions map[string]interface{}
// LoadEnvForStruct loads environment variables for each field in an options
// struct passed into it.
//
// Fields in the options struct must have an `env` and `cfg` tag to be read
// from the environment
func (cfg EnvOptions) LoadEnvForStruct(options interface{}) { func (cfg EnvOptions) LoadEnvForStruct(options interface{}) {
val := reflect.ValueOf(options) val := reflect.ValueOf(options).Elem()
var typ reflect.Type typ := val.Type()
if val.Kind() == reflect.Ptr {
typ = val.Elem().Type()
} else {
typ = val.Type()
}
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
// pull out the struct tags: // pull out the struct tags:
// flag - the name of the command line flag // flag - the name of the command line flag
// deprecated - (optional) the name of the deprecated command line flag // deprecated - (optional) the name of the deprecated command line flag
// cfg - (optional, defaults to underscored flag) the name of the config file option // cfg - (optional, defaults to underscored flag) the name of the config file option
field := typ.Field(i) field := typ.Field(i)
fieldV := reflect.Indirect(val).Field(i)
if field.Type.Kind() == reflect.Struct && field.Anonymous {
cfg.LoadEnvForStruct(fieldV.Interface())
continue
}
flagName := field.Tag.Get("flag") flagName := field.Tag.Get("flag")
envName := field.Tag.Get("env") envName := field.Tag.Get("env")
cfgName := field.Tag.Get("cfg") cfgName := field.Tag.Get("cfg")

View File

@ -1,46 +1,26 @@
package main_test package main
import ( import (
"os" "os"
"testing" "testing"
proxy "github.com/pusher/oauth2_proxy"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type EnvTest struct { type envTest struct {
TestField string `cfg:"target_field" env:"TEST_ENV_FIELD"` testField string `cfg:"target_field" env:"TEST_ENV_FIELD"`
EnvTestEmbed
}
type EnvTestEmbed struct {
TestFieldEmbed string `cfg:"target_field_embed" env:"TEST_ENV_FIELD_EMBED"`
} }
func TestLoadEnvForStruct(t *testing.T) { func TestLoadEnvForStruct(t *testing.T) {
cfg := make(proxy.EnvOptions) cfg := make(EnvOptions)
cfg.LoadEnvForStruct(&EnvTest{}) cfg.LoadEnvForStruct(&envTest{})
_, ok := cfg["target_field"] _, ok := cfg["target_field"]
assert.Equal(t, ok, false) assert.Equal(t, ok, false)
os.Setenv("TEST_ENV_FIELD", "1234abcd") os.Setenv("TEST_ENV_FIELD", "1234abcd")
cfg.LoadEnvForStruct(&EnvTest{}) cfg.LoadEnvForStruct(&envTest{})
v := cfg["target_field"] v := cfg["target_field"]
assert.Equal(t, v, "1234abcd") assert.Equal(t, v, "1234abcd")
} }
func TestLoadEnvForStructWithEmbeddedFields(t *testing.T) {
cfg := make(proxy.EnvOptions)
cfg.LoadEnvForStruct(&EnvTest{})
_, ok := cfg["target_field_embed"]
assert.Equal(t, ok, false)
os.Setenv("TEST_ENV_FIELD_EMBED", "1234abcd")
cfg.LoadEnvForStruct(&EnvTest{})
v := cfg["target_field_embed"]
assert.Equal(t, v, "1234abcd")
}

87
go.mod
View File

@ -1,87 +0,0 @@
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
View File

@ -1,531 +0,0 @@
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

@ -5,21 +5,17 @@ import (
"encoding/base64" "encoding/base64"
"encoding/csv" "encoding/csv"
"io" "io"
"log"
"os" "os"
"github.com/pusher/oauth2_proxy/pkg/logger"
"golang.org/x/crypto/bcrypt"
) )
// Lookup passwords in a htpasswd file // lookup passwords in a htpasswd file
// Passwords must be generated with -B for bcrypt or -s for SHA1. // The entries must have been created with -s for SHA encryption
// HtpasswdFile represents the structure of an htpasswd file
type HtpasswdFile struct { type HtpasswdFile struct {
Users map[string]string Users map[string]string
} }
// NewHtpasswdFromFile constructs an HtpasswdFile from the file at the path given
func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) { func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
r, err := os.Open(path) r, err := os.Open(path)
if err != nil { if err != nil {
@ -29,14 +25,13 @@ func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
return NewHtpasswd(r) return NewHtpasswd(r)
} }
// NewHtpasswd consctructs an HtpasswdFile from an io.Reader (opened file)
func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) { func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) {
csvReader := csv.NewReader(file) csv_reader := csv.NewReader(file)
csvReader.Comma = ':' csv_reader.Comma = ':'
csvReader.Comment = '#' csv_reader.Comment = '#'
csvReader.TrimLeadingSpace = true csv_reader.TrimLeadingSpace = true
records, err := csvReader.ReadAll() records, err := csv_reader.ReadAll()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -47,26 +42,19 @@ func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) {
return h, nil return h, nil
} }
// Validate checks a users password against the HtpasswdFile entries
func (h *HtpasswdFile) Validate(user string, password string) bool { func (h *HtpasswdFile) Validate(user string, password string) bool {
realPassword, exists := h.Users[user] realPassword, exists := h.Users[user]
if !exists { if !exists {
return false return false
} }
if realPassword[:5] == "{SHA}" {
shaPrefix := realPassword[:5]
if shaPrefix == "{SHA}" {
shaValue := realPassword[5:]
d := sha1.New() d := sha1.New()
d.Write([]byte(password)) d.Write([]byte(password))
return shaValue == base64.StdEncoding.EncodeToString(d.Sum(nil)) if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
return true
} }
} else {
bcryptPrefix := realPassword[:4] log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user)
if bcryptPrefix == "$2a$" || bcryptPrefix == "$2b$" || bcryptPrefix == "$2x$" || bcryptPrefix == "$2y$" {
return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password)) == nil
} }
logger.Printf("Invalid htpasswd entry for %s. Must be a SHA or bcrypt entry.", user)
return false return false
} }

View File

@ -2,14 +2,11 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/crypto/bcrypt" "testing"
) )
func TestSHA(t *testing.T) { func TestHtpasswd(t *testing.T) {
file := bytes.NewBuffer([]byte("testuser:{SHA}PaVBVZkYqAjCQCu6UBL2xgsnZhw=\n")) file := bytes.NewBuffer([]byte("testuser:{SHA}PaVBVZkYqAjCQCu6UBL2xgsnZhw=\n"))
h, err := NewHtpasswd(file) h, err := NewHtpasswd(file)
assert.Equal(t, err, nil) assert.Equal(t, err, nil)
@ -17,22 +14,3 @@ func TestSHA(t *testing.T) {
valid := h.Validate("testuser", "asdf") valid := h.Validate("testuser", "asdf")
assert.Equal(t, valid, true) assert.Equal(t, valid, true)
} }
func TestBcrypt(t *testing.T) {
hash1, err := bcrypt.GenerateFromPassword([]byte("password"), 1)
assert.Equal(t, err, nil)
hash2, err := bcrypt.GenerateFromPassword([]byte("top-secret"), 2)
assert.Equal(t, err, nil)
contents := fmt.Sprintf("testuser1:%s\ntestuser2:%s\n", hash1, hash2)
file := bytes.NewBuffer([]byte(contents))
h, err := NewHtpasswd(file)
assert.Equal(t, err, nil)
valid := h.Validate("testuser1", "password")
assert.Equal(t, valid, true)
valid = h.Validate("testuser2", "top-secret")
assert.Equal(t, valid, true)
}

76
http.go
View File

@ -2,21 +2,18 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"log"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
// Server represents an HTTP server
type Server struct { type Server struct {
Handler http.Handler Handler http.Handler
Opts *Options Opts *Options
} }
// ListenAndServe will serve traffic on HTTP or HTTPS depending on TLS options
func (s *Server) ListenAndServe() { func (s *Server) ListenAndServe() {
if s.Opts.TLSKeyFile != "" || s.Opts.TLSCertFile != "" { if s.Opts.TLSKeyFile != "" || s.Opts.TLSCertFile != "" {
s.ServeHTTPS() s.ServeHTTPS()
@ -25,53 +22,13 @@ func (s *Server) ListenAndServe() {
} }
} }
// Used with gcpHealthcheck()
const userAgentHeader = "User-Agent"
const googleHealthCheckUserAgent = "GoogleHC/1.0"
const rootPath = "/"
// gcpHealthcheck handles healthcheck queries from GCP.
func gcpHealthcheck(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check for liveness and readiness: used for Google App Engine
if r.URL.EscapedPath() == "/liveness_check" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
return
}
if r.URL.EscapedPath() == "/readiness_check" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
return
}
// Check for GKE ingress healthcheck: The ingress requires the root
// path of the target to return a 200 (OK) to indicate the service's good health. This can be quite a challenging demand
// depending on the application's path structure. This middleware filters out the requests from the health check by
//
// 1. checking that the request path is indeed the root path
// 2. ensuring that the User-Agent is "GoogleHC/1.0", the health checker
// 3. ensuring the request method is "GET"
if r.URL.Path == rootPath &&
r.Header.Get(userAgentHeader) == googleHealthCheckUserAgent &&
r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
return
}
h.ServeHTTP(w, r)
})
}
// ServeHTTP constructs a net.Listener and starts handling HTTP requests
func (s *Server) ServeHTTP() { func (s *Server) ServeHTTP() {
HTTPAddress := s.Opts.HTTPAddress httpAddress := s.Opts.HttpAddress
var scheme string scheme := ""
i := strings.Index(HTTPAddress, "://") i := strings.Index(httpAddress, "://")
if i > -1 { if i > -1 {
scheme = HTTPAddress[0:i] scheme = httpAddress[0:i]
} }
var networkType string var networkType string
@ -82,27 +39,26 @@ func (s *Server) ServeHTTP() {
networkType = scheme networkType = scheme
} }
slice := strings.SplitN(HTTPAddress, "//", 2) slice := strings.SplitN(httpAddress, "//", 2)
listenAddr := slice[len(slice)-1] listenAddr := slice[len(slice)-1]
listener, err := net.Listen(networkType, listenAddr) listener, err := net.Listen(networkType, listenAddr)
if err != nil { if err != nil {
logger.Fatalf("FATAL: listen (%s, %s) failed - %s", networkType, listenAddr, err) log.Fatalf("FATAL: listen (%s, %s) failed - %s", networkType, listenAddr, err)
} }
logger.Printf("HTTP: listening on %s", listenAddr) log.Printf("HTTP: listening on %s", listenAddr)
server := &http.Server{Handler: s.Handler} server := &http.Server{Handler: s.Handler}
err = server.Serve(listener) err = server.Serve(listener)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
logger.Printf("ERROR: http.Serve() - %s", err) log.Printf("ERROR: http.Serve() - %s", err)
} }
logger.Printf("HTTP: closing %s", listener.Addr()) log.Printf("HTTP: closing %s", listener.Addr())
} }
// ServeHTTPS constructs a net.Listener and starts handling HTTPS requests
func (s *Server) ServeHTTPS() { func (s *Server) ServeHTTPS() {
addr := s.Opts.HTTPSAddress addr := s.Opts.HttpsAddress
config := &tls.Config{ config := &tls.Config{
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS12,
@ -115,24 +71,24 @@ func (s *Server) ServeHTTPS() {
config.Certificates = make([]tls.Certificate, 1) config.Certificates = make([]tls.Certificate, 1)
config.Certificates[0], err = tls.LoadX509KeyPair(s.Opts.TLSCertFile, s.Opts.TLSKeyFile) config.Certificates[0], err = tls.LoadX509KeyPair(s.Opts.TLSCertFile, s.Opts.TLSKeyFile)
if err != nil { if err != nil {
logger.Fatalf("FATAL: loading tls config (%s, %s) failed - %s", s.Opts.TLSCertFile, s.Opts.TLSKeyFile, err) log.Fatalf("FATAL: loading tls config (%s, %s) failed - %s", s.Opts.TLSCertFile, s.Opts.TLSKeyFile, err)
} }
ln, err := net.Listen("tcp", addr) ln, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
logger.Fatalf("FATAL: listen (%s) failed - %s", addr, err) log.Fatalf("FATAL: listen (%s) failed - %s", addr, err)
} }
logger.Printf("HTTPS: listening on %s", ln.Addr()) log.Printf("HTTPS: listening on %s", ln.Addr())
tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config)
srv := &http.Server{Handler: s.Handler} srv := &http.Server{Handler: s.Handler}
err = srv.Serve(tlsListener) err = srv.Serve(tlsListener)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
logger.Printf("ERROR: https.Serve() - %s", err) log.Printf("ERROR: https.Serve() - %s", err)
} }
logger.Printf("HTTPS: closing %s", tlsListener.Addr()) log.Printf("HTTPS: closing %s", tlsListener.Addr())
} }
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted

View File

@ -1,108 +0,0 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
const localhost = "127.0.0.1"
const host = "test-server"
func TestGCPHealthcheckLiveness(t *testing.T) {
handler := func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("test"))
}
h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/liveness_check", nil)
r.RemoteAddr = localhost
r.Host = host
h.ServeHTTP(rw, r)
assert.Equal(t, 200, rw.Code)
assert.Equal(t, "OK", rw.Body.String())
}
func TestGCPHealthcheckReadiness(t *testing.T) {
handler := func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("test"))
}
h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/readiness_check", nil)
r.RemoteAddr = localhost
r.Host = host
h.ServeHTTP(rw, r)
assert.Equal(t, 200, rw.Code)
assert.Equal(t, "OK", rw.Body.String())
}
func TestGCPHealthcheckNotHealthcheck(t *testing.T) {
handler := func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("test"))
}
h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/not_any_check", nil)
r.RemoteAddr = localhost
r.Host = host
h.ServeHTTP(rw, r)
assert.Equal(t, "test", rw.Body.String())
}
func TestGCPHealthcheckIngress(t *testing.T) {
handler := func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("test"))
}
h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
r.RemoteAddr = localhost
r.Host = host
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
h.ServeHTTP(rw, r)
assert.Equal(t, 200, rw.Code)
assert.Equal(t, "", rw.Body.String())
}
func TestGCPHealthcheckNotIngress(t *testing.T) {
handler := func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("test"))
}
h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/foo", nil)
r.RemoteAddr = localhost
r.Host = host
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
h.ServeHTTP(rw, r)
assert.Equal(t, "test", rw.Body.String())
}
func TestGCPHealthcheckNotIngressPut(t *testing.T) {
handler := func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("test"))
}
h := gcpHealthcheck(http.HandlerFunc(handler))
rw := httptest.NewRecorder()
r, _ := http.NewRequest("PUT", "/", nil)
r.RemoteAddr = localhost
r.Host = host
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
h.ServeHTTP(rw, r)
assert.Equal(t, "test", rw.Body.String())
}

View File

@ -4,13 +4,17 @@
package main package main
import ( import (
"bufio" "fmt"
"errors" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"text/template"
"time" "time"
)
"github.com/pusher/oauth2_proxy/pkg/logger" const (
defaultRequestLoggingFormat = "{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}"
) )
// 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
@ -23,21 +27,10 @@ type responseLogger struct {
authInfo string authInfo string
} }
// Header returns the ResponseWriter's Header
func (l *responseLogger) Header() http.Header { func (l *responseLogger) Header() http.Header {
return l.w.Header() return l.w.Header()
} }
// Support Websocket
func (l *responseLogger) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) {
if hj, ok := l.w.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, errors.New("http.Hijacker is not available on writer")
}
// ExtractGAPMetadata extracts and removes GAP headers from the ResponseWriter's
// Header
func (l *responseLogger) ExtractGAPMetadata() { func (l *responseLogger) ExtractGAPMetadata() {
upstream := l.w.Header().Get("GAP-Upstream-Address") upstream := l.w.Header().Get("GAP-Upstream-Address")
if upstream != "" { if upstream != "" {
@ -51,7 +44,6 @@ func (l *responseLogger) ExtractGAPMetadata() {
} }
} }
// Write writes the response using the ResponseWriter
func (l *responseLogger) Write(b []byte) (int, error) { func (l *responseLogger) Write(b []byte) (int, error) {
if l.status == 0 { if l.status == 0 {
// The status will be StatusOK if WriteHeader has not been called yet // The status will be StatusOK if WriteHeader has not been called yet
@ -63,46 +55,106 @@ func (l *responseLogger) Write(b []byte) (int, error) {
return size, err return size, err
} }
// WriteHeader writes the status code for the Response
func (l *responseLogger) WriteHeader(s int) { func (l *responseLogger) WriteHeader(s int) {
l.ExtractGAPMetadata() l.ExtractGAPMetadata()
l.w.WriteHeader(s) l.w.WriteHeader(s)
l.status = s l.status = s
} }
// Status returns the response status code
func (l *responseLogger) Status() int { func (l *responseLogger) Status() int {
return l.status return l.status
} }
// 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 // logMessageData is the container for all values that are available as variables in the request logging format.
func (l *responseLogger) Flush() { // All values are pre-formatted strings so it is easy to use them in the format string.
if flusher, ok := l.w.(http.Flusher); ok { type logMessageData struct {
flusher.Flush() Client,
} Host,
Protocol,
RequestDuration,
RequestMethod,
RequestURI,
ResponseSize,
StatusCode,
Timestamp,
Upstream,
UserAgent,
Username string
} }
// loggingHandler is the http.Handler implementation for LoggingHandler // loggingHandler is the http.Handler implementation for LoggingHandlerTo and its friends
type loggingHandler struct { type loggingHandler struct {
writer io.Writer
handler http.Handler handler http.Handler
enabled bool
logTemplate *template.Template
} }
// LoggingHandler provides an http.Handler which logs requests to the HTTP server func LoggingHandler(out io.Writer, h http.Handler, v bool, requestLoggingTpl string) http.Handler {
func LoggingHandler(h http.Handler) http.Handler {
return loggingHandler{ return loggingHandler{
writer: out,
handler: h, handler: h,
enabled: v,
logTemplate: template.Must(template.New("request-log").Parse(requestLoggingTpl)),
} }
} }
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
t := time.Now() t := time.Now()
url := *req.URL url := *req.URL
responseLogger := &responseLogger{w: w} logger := &responseLogger{w: w}
h.handler.ServeHTTP(responseLogger, req) h.handler.ServeHTTP(logger, req)
logger.PrintReq(responseLogger.authInfo, responseLogger.upstream, req, url, t, responseLogger.Status(), responseLogger.Size()) if !h.enabled {
return
}
h.writeLogLine(logger.authInfo, logger.upstream, req, url, t, logger.Status(), logger.Size())
}
// Log entry for req similar to Apache Common Log Format.
// ts is the timestamp with which the entry should be logged.
// status, size are used to provide the response HTTP status and size.
func (h loggingHandler) writeLogLine(username, upstream string, req *http.Request, url url.URL, ts time.Time, status int, size int) {
if username == "" {
username = "-"
}
if upstream == "" {
upstream = "-"
}
if url.User != nil && username == "-" {
if name := url.User.Username(); name != "" {
username = name
}
}
client := req.Header.Get("X-Real-IP")
if client == "" {
client = req.RemoteAddr
}
if c, _, err := net.SplitHostPort(client); err == nil {
client = c
}
duration := float64(time.Now().Sub(ts)) / float64(time.Second)
h.logTemplate.Execute(h.writer, logMessageData{
Client: client,
Host: req.Host,
Protocol: req.Proto,
RequestDuration: fmt.Sprintf("%0.3f", duration),
RequestMethod: req.Method,
RequestURI: fmt.Sprintf("%q", url.RequestURI()),
ResponseSize: fmt.Sprintf("%d", size),
StatusCode: fmt.Sprintf("%d", status),
Timestamp: ts.Format("02/Jan/2006:15:04:05 -0700"),
Upstream: upstream,
UserAgent: fmt.Sprintf("%q", req.UserAgent()),
Username: username,
})
h.writer.Write([]byte("\n"))
} }

View File

@ -5,11 +5,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"time" "time"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
func TestLoggingHandler_ServeHTTP(t *testing.T) { func TestLoggingHandler_ServeHTTP(t *testing.T) {
@ -17,53 +14,29 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) {
tests := []struct { tests := []struct {
Format, Format,
ExpectedLogMessage, ExpectedLogMessage string
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)), "/foo/bar", []string{}, false}, {defaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", ts.Format("02/Jan/2006:15:04:05 -0700"))},
{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}, {"{{.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{"/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 {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
handler := func(w http.ResponseWriter, req *http.Request) { handler := func(w http.ResponseWriter, req *http.Request) {
_, ok := w.(http.Hijacker)
if !ok {
t.Error("http.Hijacker is not available")
}
w.Write([]byte("test")) w.Write([]byte("test"))
} }
logger.SetOutput(buf) h := LoggingHandler(buf, http.HandlerFunc(handler), true, test.Format)
logger.SetReqTemplate(test.Format)
if test.SilencePingLogging {
test.ExcludePaths = append(test.ExcludePaths, "/ping")
}
logger.SetExcludePaths(test.ExcludePaths)
h := LoggingHandler(http.HandlerFunc(handler))
r, _ := http.NewRequest("GET", test.Path, nil) r, _ := http.NewRequest("GET", "/foo/bar", nil)
r.RemoteAddr = "127.0.0.1" r.RemoteAddr = "127.0.0.1"
r.Host = "test-server" r.Host = "test-server"
h.ServeHTTP(httptest.NewRecorder(), r) h.ServeHTTP(httptest.NewRecorder(), r)
actual := buf.String() actual := buf.String()
if !strings.Contains(actual, test.ExpectedLogMessage) { if actual != test.ExpectedLogMessage {
t.Errorf("Log message was\n%s\ninstead of matching \n%s", actual, test.ExpectedLogMessage) t.Errorf("Log message was\n%s\ninstead of expected \n%s", actual, test.ExpectedLogMessage)
} }
} }
} }

94
main.go
View File

@ -3,37 +3,33 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"math/rand" "log"
"net/http"
"os" "os"
"runtime" "runtime"
"strings" "strings"
"time" "time"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
options "github.com/mreiferson/go-options" "github.com/mreiferson/go-options"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
func main() { func main() {
logger.SetFlags(logger.Lshortfile) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
flagSet := flag.NewFlagSet("oauth2_proxy", flag.ExitOnError) flagSet := flag.NewFlagSet("oauth2_proxy", flag.ExitOnError)
emailDomains := StringArray{} emailDomains := StringArray{}
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-file", "", "path to certificate file") flagSet.String("tls-cert", "", "path to certificate file")
flagSet.String("tls-key-file", "", "path to private key file") flagSet.String("tls-key", "", "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")
@ -47,74 +43,38 @@ 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 providers") flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS")
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.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")
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")
flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"") flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
flagSet.String("client-secret", "", "the OAuth Client Secret") flagSet.String("client-secret", "", "the OAuth Client Secret")
flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)") flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
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")
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.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")
flagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)") flagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)")
flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*") flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*")
flagSet.String("cookie-path", "/", "an optional cookie path to force cookies to (ie: /poc/)*")
flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable") flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable")
flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
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.Bool("request-logging", true, "Log requests to stdout")
flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])") flagSet.String("request-logging-format", defaultRequestLoggingFormat, "Template for log lines")
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.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation")
flagSet.Int("logging-max-age", 7, "Maximum number of days to retain old log files")
flagSet.Int("logging-max-backups", 0, "Maximum number of old log files to retain; 0 to disable")
flagSet.Bool("logging-local-time", true, "If the time in log files and backup filenames are local or UTC time")
flagSet.Bool("logging-compress", false, "Should rotated log files be compressed using gzip")
flagSet.Bool("standard-logging", true, "Log standard runtime information")
flagSet.String("standard-logging-format", logger.DefaultStandardLoggingFormat, "Template for standard log lines")
flagSet.Bool("request-logging", true, "Log HTTP requests")
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.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.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")
flagSet.String("redeem-url", "", "Token redemption endpoint") flagSet.String("redeem-url", "", "Token redemption endpoint")
flagSet.String("profile-url", "", "Profile access endpoint") flagSet.String("profile-url", "", "Profile access endpoint")
@ -124,16 +84,11 @@ func main() {
flagSet.String("approval-prompt", "force", "OAuth approval_prompt") flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")
flagSet.String("acr-values", "http://idmanagement.gov/ns/assurance/loa/1", "acr values string: optional, used by login.gov")
flagSet.String("jwt-key", "", "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")
flagSet.String("jwt-key-file", "", "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")
flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov")
flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")
flagSet.Parse(os.Args[1:]) flagSet.Parse(os.Args[1:])
if *showVersion { if *showVersion {
fmt.Printf("oauth2_proxy %s (built with %s)\n", VERSION, runtime.Version()) fmt.Printf("oauth2_proxy v%s (built with %s)\n", VERSION, runtime.Version())
return return
} }
@ -143,7 +98,7 @@ func main() {
if *config != "" { if *config != "" {
_, err := toml.DecodeFile(*config, &cfg) _, err := toml.DecodeFile(*config, &cfg)
if err != nil { if err != nil {
logger.Fatalf("ERROR: failed to load config file %s - %s", *config, err) log.Fatalf("ERROR: failed to load config file %s - %s", *config, err)
} }
} }
cfg.LoadEnvForStruct(opts) cfg.LoadEnvForStruct(opts)
@ -151,20 +106,13 @@ func main() {
err := opts.Validate() err := opts.Validate()
if err != nil { if err != nil {
logger.Printf("%s", err) log.Printf("%s", err)
os.Exit(1) os.Exit(1)
} }
validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile) validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile)
oauthproxy := NewOAuthProxy(opts, validator) oauthproxy := NewOAuthProxy(opts, validator)
if len(opts.Banner) >= 1 { if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
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] != "*" {
@ -173,24 +121,16 @@ func main() {
} }
if opts.HtpasswdFile != "" { if opts.HtpasswdFile != "" {
logger.Printf("using htpasswd file %s", opts.HtpasswdFile) log.Printf("using htpasswd file %s", opts.HtpasswdFile)
oauthproxy.HtpasswdFile, err = NewHtpasswdFromFile(opts.HtpasswdFile) oauthproxy.HtpasswdFile, err = NewHtpasswdFromFile(opts.HtpasswdFile)
oauthproxy.DisplayHtpasswdForm = opts.DisplayHtpasswdForm oauthproxy.DisplayHtpasswdForm = opts.DisplayHtpasswdForm
if err != nil { if err != nil {
logger.Fatalf("FATAL: unable to open %s %s", opts.HtpasswdFile, err) log.Fatalf("FATAL: unable to open %s %s", opts.HtpasswdFile, err)
} }
} }
rand.Seed(time.Now().UnixNano())
var handler http.Handler
if opts.GCPHealthChecks {
handler = gcpHealthcheck(LoggingHandler(oauthproxy))
} else {
handler = LoggingHandler(oauthproxy)
}
s := &Server{ s := &Server{
Handler: handler, Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging, opts.RequestLoggingFormat),
Opts: opts, Opts: opts,
} }
s.ListenAndServe() s.ListenAndServe()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,6 @@ import (
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -14,148 +13,100 @@ import (
"strings" "strings"
"time" "time"
"github.com/bitly/oauth2_proxy/providers"
oidc "github.com/coreos/go-oidc" oidc "github.com/coreos/go-oidc"
"github.com/dgrijalva/jwt-go"
"github.com/mbland/hmacauth" "github.com/mbland/hmacauth"
"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"
"gopkg.in/natefinch/lumberjack.v2"
) )
// Options holds Configuration Options that can be set by Command Line Flag, // 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"`
PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"` HttpAddress string `flag:"http-address" cfg:"http_address"`
ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"` HttpsAddress string `flag:"https-address" cfg:"https_address"`
HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"` RedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
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"`
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-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"` TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file"`
TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` TLSKeyFile string `flag:"tls-key" cfg:"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"`
KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"` AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"` EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
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"`
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"`
GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"` GitHubTeam string `flag:"github-team" cfg:"github_team"`
GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group" env:"OAUTH2_PROXY_GITLAB_GROUP"` GoogleGroups []string `flag:"google-group" cfg:"google_group"`
GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"` GoogleAdminEmail string `flag:"google-admin-email" cfg:"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"`
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"`
HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"` DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"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"`
CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"` Footer string `flag:"footer" cfg:"footer"`
Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"`
Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"`
// Embed CookieOptions CookieName string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
options.CookieOptions CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure"`
CookieHttpOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"`
// Embed SessionOptions Upstreams []string `flag:"upstream" cfg:"upstreams"`
options.SessionOptions SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"` BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"`
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"` PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"`
SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"` PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"` SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button"`
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"` PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers"`
BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"` SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"` SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest"`
PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"` SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header"`
SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"` PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header"`
PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"` SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
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"`
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"`
SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight" env:"OAUTH2_PROXY_SKIP_AUTH_PREFLIGHT"`
FlushInterval time.Duration `flag:"flush-interval" cfg:"flush_interval" env:"OAUTH2_PROXY_FLUSH_INTERVAL"`
// These options allow for other providers besides Google, with // These options allow for other providers besides Google, with
// potential overrides. // potential overrides.
Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"` Provider string `flag:"provider" cfg:"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"`
InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"` LoginURL string `flag:"login-url" cfg:"login_url"`
SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_PROXY_SKIP_OIDC_DISCOVERY"` RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_PROXY_OIDC_JWKS_URL"` ProfileURL string `flag:"profile-url" cfg:"profile_url"`
LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"` ProtectedResource string `flag:"resource" cfg:"resource"`
RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"` ValidateURL string `flag:"validate-url" cfg:"validate_url"`
ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"` Scope string `flag:"scope" cfg:"scope"`
ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"` ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`
ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"`
Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"` RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format"`
// Configuration values for logging
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_PROXY_LOGGING_MAX_SIZE"`
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_PROXY_LOGGING_MAX_BACKUPS"`
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_PROXY_LOGGING_COMPRESS"`
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_PROXY_STANDARD_LOGGING_FORMAT"`
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_PROXY_REQUEST_LOGGING_FORMAT"`
ExcludeLoggingPaths string `flag:"exclude-logging-paths" cfg:"exclude_logging_paths" env:"OAUTH2_PROXY_EXCLUDE_LOGGING_PATHS"`
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"`
JWTKey string `flag:"jwt-key" cfg:"jwt_key" env:"OAUTH2_PROXY_JWT_KEY"`
JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file" env:"OAUTH2_PROXY_JWT_KEY_FILE"`
PubJWKURL string `flag:"pubjwk-url" cfg:"pubjwk_url" env:"OAUTH2_PROXY_PUBJWK_URL"`
GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"`
// internal values that are set after config validation // internal values that are set after config validation
redirectURL *url.URL redirectURL *url.URL
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
type SignatureData struct { type SignatureData struct {
hash crypto.Hash hash crypto.Hash
key string key string
} }
// NewOptions constructs a new Options with defaulted values
func NewOptions() *Options { func NewOptions() *Options {
return &Options{ return &Options{
ProxyPrefix: "/oauth2", ProxyPrefix: "/oauth2",
PingPath: "/ping", HttpAddress: "127.0.0.1:4180",
ProxyWebSockets: true, HttpsAddress: ":443",
HTTPAddress: "127.0.0.1:4180",
HTTPSAddress: ":443",
DisplayHtpasswdForm: true, DisplayHtpasswdForm: true,
CookieOptions: options.CookieOptions{
CookieName: "_oauth2_proxy", CookieName: "_oauth2_proxy",
CookieSecure: true, CookieSecure: true,
CookieHTTPOnly: true, CookieHttpOnly: true,
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,
@ -165,42 +116,20 @@ func NewOptions() *Options {
SetAuthorization: false, SetAuthorization: false,
PassAuthorization: false, PassAuthorization: false,
ApprovalPrompt: "force", ApprovalPrompt: "force",
InsecureOIDCAllowUnverifiedEmail: false,
SkipOIDCDiscovery: false,
LoggingFilename: "",
LoggingMaxSize: 100,
LoggingMaxAge: 7,
LoggingMaxBackups: 0,
LoggingLocalTime: true,
LoggingCompress: false,
ExcludeLoggingPaths: "",
SilencePingLogging: false,
StandardLogging: true,
StandardLoggingFormat: logger.DefaultStandardLoggingFormat,
RequestLogging: true, RequestLogging: true,
RequestLoggingFormat: logger.DefaultRequestLoggingFormat, RequestLoggingFormat: defaultRequestLoggingFormat,
AuthLogging: true,
AuthLoggingFormat: logger.DefaultAuthLoggingFormat,
} }
} }
// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier. func parseURL(to_parse string, urltype string, msgs []string) (*url.URL, []string) {
type jwtIssuer struct { parsed, err := url.Parse(to_parse)
issuerURI string
audience string
}
func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
parsed, err := url.Parse(toParse)
if err != nil { if err != nil {
return nil, append(msgs, fmt.Sprintf( return nil, append(msgs, fmt.Sprintf(
"error parsing %s-url=%q %s", urltype, toParse, err)) "error parsing %s-url=%q %s", urltype, to_parse, err))
} }
return parsed, msgs return parsed, msgs
} }
// Validate checks that required options are set and validates those that they
// are of the correct format
func (o *Options) Validate() error { func (o *Options) Validate() error {
if o.SSLInsecureSkipVerify { if o.SSLInsecureSkipVerify {
// TODO: Accept a certificate bundle. // TODO: Accept a certificate bundle.
@ -217,8 +146,7 @@ func (o *Options) Validate() error {
if o.ClientID == "" { if o.ClientID == "" {
msgs = append(msgs, "missing setting: client-id") msgs = append(msgs, "missing setting: client-id")
} }
// login.gov uses a signed JWT to authenticate, not a client-secret if o.ClientSecret == "" {
if o.ClientSecret == "" && o.Provider != "login.gov" {
msgs = append(msgs, "missing setting: client-secret") msgs = append(msgs, "missing setting: client-secret")
} }
if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" { if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
@ -227,64 +155,21 @@ func (o *Options) Validate() error {
} }
if o.OIDCIssuerURL != "" { if o.OIDCIssuerURL != "" {
ctx := context.Background()
// Construct a manual IDTokenVerifier from issuer URL & JWKS URI
// instead of metadata discovery if we enable -skip-oidc-discovery.
// In this case we need to make sure the required endpoints for
// the provider are configured.
if o.SkipOIDCDiscovery {
if o.LoginURL == "" {
msgs = append(msgs, "missing setting: login-url")
}
if o.RedeemURL == "" {
msgs = append(msgs, "missing setting: redeem-url")
}
if o.OIDCJwksURL == "" {
msgs = append(msgs, "missing setting: oidc-jwks-url")
}
keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL)
o.oidcVerifier = oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{
ClientID: o.ClientID,
})
} else {
// Configure discoverable provider data. // Configure discoverable provider data.
provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL) provider, err := oidc.NewProvider(context.Background(), o.OIDCIssuerURL)
if err != nil { if err != nil {
return err return err
} }
o.oidcVerifier = provider.Verifier(&oidc.Config{ o.oidcVerifier = provider.Verifier(&oidc.Config{
ClientID: o.ClientID, ClientID: o.ClientID,
}) })
o.LoginURL = provider.Endpoint().AuthURL o.LoginURL = provider.Endpoint().AuthURL
o.RedeemURL = provider.Endpoint().TokenURL o.RedeemURL = provider.Endpoint().TokenURL
}
if o.Scope == "" { if o.Scope == "" {
o.Scope = "openid email profile" o.Scope = "openid email profile"
} }
} }
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 {
@ -309,19 +194,18 @@ func (o *Options) Validate() error {
} }
msgs = parseProviderInfo(o, msgs) msgs = parseProviderInfo(o, msgs)
var cipher *encryption.Cipher if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {
if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.CookieRefresh != time.Duration(0)) { valid_cookie_secret_size := 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 {
validCookieSecretSize = true valid_cookie_secret_size = true
} }
} }
var decoded bool var decoded bool
if string(secretBytes(o.CookieSecret)) != o.CookieSecret { if string(secretBytes(o.CookieSecret)) != o.CookieSecret {
decoded = true decoded = true
} }
if validCookieSecretSize == false { if valid_cookie_secret_size == false {
var suffix string var suffix string
if decoded { if decoded {
suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret) suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret)
@ -332,22 +216,8 @@ 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(
@ -371,7 +241,6 @@ func (o *Options) Validate() error {
msgs = parseSignatureKey(o, msgs) msgs = parseSignatureKey(o, msgs)
msgs = validateCookieName(o, msgs) msgs = validateCookieName(o, msgs)
msgs = setupLogger(o, msgs)
if len(msgs) != 0 { if len(msgs) != 0 {
return fmt.Errorf("Invalid configuration:\n %s", return fmt.Errorf("Invalid configuration:\n %s",
@ -399,8 +268,6 @@ 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)
@ -410,70 +277,12 @@ 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:
p.AcrValues = o.AcrValues
p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
// JWT key can be supplied via env variable or file in the filesystem, but not both.
switch {
case o.JWTKey != "" && o.JWTKeyFile != "":
msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options")
case o.JWTKey == "" && o.JWTKeyFile == "":
msgs = append(msgs, "login.gov provider requires a private key for signing JWTs")
case o.JWTKey != "":
// The JWT Key is in the commandline argument
signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.JWTKey))
if err != nil {
msgs = append(msgs, "could not parse RSA Private Key PEM")
} else {
p.JWTKey = signKey
}
case o.JWTKeyFile != "":
// The JWT key is in the filesystem
keyData, err := ioutil.ReadFile(o.JWTKeyFile)
if err != nil {
msgs = append(msgs, "could not read key file: "+o.JWTKeyFile)
}
signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
if err != nil {
msgs = append(msgs, "could not parse private key from PEM file:"+o.JWTKeyFile)
} else {
p.JWTKey = signKey
}
}
} }
return msgs return msgs
} }
@ -490,53 +299,13 @@ func parseSignatureKey(o *Options, msgs []string) []string {
} }
algorithm, secretKey := components[0], components[1] algorithm, secretKey := components[0], components[1]
var hash crypto.Hash if hash, err := hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
var err error
if hash, err = hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
return append(msgs, "unsupported signature hash algorithm: "+ return append(msgs, "unsupported signature hash algorithm: "+
o.SignatureKey) o.SignatureKey)
}
o.signatureData = &SignatureData{hash: hash, key: secretKey}
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 { } else {
verifier = provider.Verifier(config) o.signatureData = &SignatureData{hash, secretKey}
} }
return verifier, nil return msgs
} }
func validateCookieName(o *Options, msgs []string) []string { func validateCookieName(o *Options, msgs []string) []string {
@ -569,57 +338,3 @@ func secretBytes(secret string) []byte {
} }
return []byte(secret) return []byte(secret)
} }
func setupLogger(o *Options, msgs []string) []string {
// Setup the log file
if len(o.LoggingFilename) > 0 {
// Validate that the file/dir can be written
file, err := os.OpenFile(o.LoggingFilename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
if os.IsPermission(err) {
return append(msgs, "unable to write to log file: "+o.LoggingFilename)
}
}
file.Close()
logger.Printf("Redirecting logging to file: %s", o.LoggingFilename)
logWriter := &lumberjack.Logger{
Filename: o.LoggingFilename,
MaxSize: o.LoggingMaxSize, // megabytes
MaxAge: o.LoggingMaxAge, // days
MaxBackups: o.LoggingMaxBackups,
LocalTime: o.LoggingLocalTime,
Compress: o.LoggingCompress,
}
logger.SetOutput(logWriter)
}
// Supply a sanity warning to the logger if all logging is disabled
if !o.StandardLogging && !o.AuthLogging && !o.RequestLogging {
logger.Print("Warning: Logging disabled. No further logs will be shown.")
}
// Pass configuration values to the standard logger
logger.SetStandardEnabled(o.StandardLogging)
logger.SetAuthEnabled(o.AuthLogging)
logger.SetReqEnabled(o.RequestLogging)
logger.SetStandardTemplate(o.StandardLoggingFormat)
logger.SetAuthTemplate(o.AuthLoggingFormat)
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 {
logger.SetFlags(logger.Flags() | logger.LUTC)
}
return msgs
}

View File

@ -88,9 +88,9 @@ func TestProxyURLs(t *testing.T) {
o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8081") o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8081")
assert.Equal(t, nil, o.Validate()) assert.Equal(t, nil, o.Validate())
expected := []*url.URL{ expected := []*url.URL{
{Scheme: "http", Host: "127.0.0.1:8080", Path: "/"}, &url.URL{Scheme: "http", Host: "127.0.0.1:8080", Path: "/"},
// note the '/' was added // note the '/' was added
{Scheme: "http", Host: "127.0.0.1:8081", Path: "/"}, &url.URL{Scheme: "http", Host: "127.0.0.1:8081", Path: "/"},
} }
assert.Equal(t, expected, o.proxyURLs) assert.Equal(t, expected, o.proxyURLs)
} }
@ -251,26 +251,3 @@ func TestValidateCookieBadName(t *testing.T) {
assert.Equal(t, err.Error(), "Invalid configuration:\n"+ assert.Equal(t, err.Error(), "Invalid configuration:\n"+
fmt.Sprintf(" invalid cookie name: %q", o.CookieName)) fmt.Sprintf(" invalid cookie name: %q", o.CookieName))
} }
func TestSkipOIDCDiscovery(t *testing.T) {
o := testOptions()
o.Provider = "oidc"
o.OIDCIssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/"
o.SkipOIDCDiscovery = true
err := o.Validate()
assert.Equal(t, "Invalid configuration:\n"+
fmt.Sprintf(" missing setting: login-url\n missing setting: redeem-url\n missing setting: oidc-jwks-url"), err.Error())
o.LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in"
o.RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in"
o.OIDCJwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys"
assert.Equal(t, nil, o.Validate())
}
func TestGCPHealthcheck(t *testing.T) {
o := testOptions()
o.GCPHealthChecks = true
assert.Equal(t, nil, o.Validate())
}

View File

@ -1,15 +0,0 @@
package options
import "time"
// CookieOptions contains configuration options relating to Cookie configuration
type CookieOptions struct {
CookieName string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
CookiePath string `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"`
CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"`
CookieHTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"`
}

View File

@ -1,30 +0,0 @@
package options
import "github.com/pusher/oauth2_proxy/pkg/encryption"
// SessionOptions contains configuration options for the SessionStore providers.
type SessionOptions struct {
Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
Cipher *encryption.Cipher
CookieStoreOptions
RedisStoreOptions
}
// CookieSessionStoreType is used to indicate the CookieSessionStore should be
// used for storing sessions.
var CookieSessionStoreType = "cookie"
// CookieStoreOptions contains configuration options for the CookieSessionStore.
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

@ -1,12 +0,0 @@
package sessions
import (
"net/http"
)
// SessionStore is an interface to storing user sessions in the proxy
type SessionStore interface {
Save(rw http.ResponseWriter, req *http.Request, s *SessionState) error
Load(req *http.Request) (*SessionState, error)
Clear(rw http.ResponseWriter, req *http.Request) error
}

View File

@ -1,243 +0,0 @@
package sessions
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/pusher/oauth2_proxy/pkg/encryption"
)
// SessionState is used to store information about the currently authenticated user session
type SessionState struct {
AccessToken string `json:",omitempty"`
IDToken string `json:",omitempty"`
CreatedAt time.Time `json:"-"`
ExpiresOn time.Time `json:"-"`
RefreshToken string `json:",omitempty"`
Email string `json:",omitempty"`
User string `json:",omitempty"`
}
// SessionStateJSON is used to encode SessionState into JSON without exposing time.Time zero value
type SessionStateJSON struct {
*SessionState
CreatedAt *time.Time `json:",omitempty"`
ExpiresOn *time.Time `json:",omitempty"`
}
// IsExpired checks whether the session has expired
func (s *SessionState) IsExpired() bool {
if !s.ExpiresOn.IsZero() && s.ExpiresOn.Before(time.Now()) {
return true
}
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
func (s *SessionState) String() string {
o := fmt.Sprintf("Session{email:%s user:%s", s.Email, s.User)
if s.AccessToken != "" {
o += " token:true"
}
if s.IDToken != "" {
o += " id_token:true"
}
if !s.CreatedAt.IsZero() {
o += fmt.Sprintf(" created:%s", s.CreatedAt)
}
if !s.ExpiresOn.IsZero() {
o += fmt.Sprintf(" expires:%s", s.ExpiresOn)
}
if s.RefreshToken != "" {
o += " refresh_token:true"
}
return o + "}"
}
// EncodeSessionState returns string representation of the current session
func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) {
var ss SessionState
if c == nil {
// Store only Email and User when cipher is unavailable
ss.Email = s.Email
ss.User = s.User
} else {
ss = *s
var err error
if ss.Email != "" {
ss.Email, err = c.Encrypt(ss.Email)
if err != nil {
return "", err
}
}
if ss.User != "" {
ss.User, err = c.Encrypt(ss.User)
if err != nil {
return "", err
}
}
if ss.AccessToken != "" {
ss.AccessToken, err = c.Encrypt(ss.AccessToken)
if err != nil {
return "", err
}
}
if ss.IDToken != "" {
ss.IDToken, err = c.Encrypt(ss.IDToken)
if err != nil {
return "", err
}
}
if ss.RefreshToken != "" {
ss.RefreshToken, err = c.Encrypt(ss.RefreshToken)
if err != nil {
return "", err
}
}
}
// Embed SessionState and ExpiresOn pointer into SessionStateJSON
ssj := &SessionStateJSON{SessionState: &ss}
if !ss.CreatedAt.IsZero() {
ssj.CreatedAt = &ss.CreatedAt
}
if !ss.ExpiresOn.IsZero() {
ssj.ExpiresOn = &ss.ExpiresOn
}
b, err := json.Marshal(ssj)
return string(b), err
}
// legacyDecodeSessionStatePlain decodes older plain session state string
func legacyDecodeSessionStatePlain(v string) (*SessionState, error) {
chunks := strings.Split(v, " ")
if len(chunks) != 2 {
return nil, fmt.Errorf("invalid session state (legacy: expected 2 chunks for user/email got %d)", len(chunks))
}
user := strings.TrimPrefix(chunks[1], "user:")
email := strings.TrimPrefix(chunks[0], "email:")
return &SessionState{User: user, Email: email}, nil
}
// legacyDecodeSessionState attempts to decode the session state string
// generated by v3.1.0 or older
func legacyDecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
chunks := strings.Split(v, "|")
if c == nil {
if len(chunks) != 1 {
return nil, fmt.Errorf("invalid session state (legacy: expected 1 chunk for plain got %d)", len(chunks))
}
return legacyDecodeSessionStatePlain(chunks[0])
}
if len(chunks) != 4 && len(chunks) != 5 {
return nil, fmt.Errorf("invalid session state (legacy: expected 4 or 5 chunks for full got %d)", len(chunks))
}
i := 0
ss, err := legacyDecodeSessionStatePlain(chunks[i])
if err != nil {
return nil, err
}
i++
ss.AccessToken = chunks[i]
if len(chunks) == 5 {
// SessionState with IDToken in v3.1.0
i++
ss.IDToken = chunks[i]
}
i++
ts, err := strconv.Atoi(chunks[i])
if err != nil {
return nil, fmt.Errorf("invalid session state (legacy: wrong expiration time: %s)", err)
}
ss.ExpiresOn = time.Unix(int64(ts), 0)
i++
ss.RefreshToken = chunks[i]
return ss, nil
}
// DecodeSessionState decodes the session cookie string into a SessionState
func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
var ssj SessionStateJSON
var ss *SessionState
err := json.Unmarshal([]byte(v), &ssj)
if err == nil && ssj.SessionState != nil {
// Extract SessionState and CreatedAt,ExpiresOn value from SessionStateJSON
ss = ssj.SessionState
if ssj.CreatedAt != nil {
ss.CreatedAt = *ssj.CreatedAt
}
if ssj.ExpiresOn != nil {
ss.ExpiresOn = *ssj.ExpiresOn
}
} else {
// Try to decode a legacy string when json.Unmarshal failed
ss, err = legacyDecodeSessionState(v, c)
if err != nil {
return nil, err
}
}
if c == nil {
// Load only Email and User when cipher is unavailable
ss = &SessionState{
Email: ss.Email,
User: ss.User,
}
} else {
// Backward compatibility with using unencrypted Email
if ss.Email != "" {
decryptedEmail, errEmail := c.Decrypt(ss.Email)
if errEmail == nil {
ss.Email = decryptedEmail
}
}
// Backward compatibility with using unencrypted User
if ss.User != "" {
decryptedUser, errUser := c.Decrypt(ss.User)
if errUser == nil {
ss.User = decryptedUser
}
}
if ss.AccessToken != "" {
ss.AccessToken, err = c.Decrypt(ss.AccessToken)
if err != nil {
return nil, err
}
}
if ss.IDToken != "" {
ss.IDToken, err = c.Decrypt(ss.IDToken)
if err != nil {
return nil, err
}
}
if ss.RefreshToken != "" {
ss.RefreshToken, err = c.Decrypt(ss.RefreshToken)
if err != nil {
return nil, err
}
}
}
if ss.User == "" {
ss.User = ss.Email
}
return ss, nil
}

View File

@ -1,343 +0,0 @@
package sessions_test
import (
"fmt"
"testing"
"time"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/stretchr/testify/assert"
)
const secret = "0123456789abcdefghijklmnopqrstuv"
const altSecret = "0000000000abcdefghijklmnopqrstuv"
func TestSessionStateSerialization(t *testing.T) {
c, err := encryption.NewCipher([]byte(secret))
assert.Equal(t, nil, err)
c2, err := encryption.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err)
s := &sessions.SessionState{
Email: "user@domain.com",
AccessToken: "token1234",
IDToken: "rawtoken1234",
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(c)
assert.Equal(t, nil, err)
ss, err := sessions.DecodeSessionState(encoded, c)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, "user@domain.com", ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.AccessToken, ss.AccessToken)
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.RefreshToken, ss.RefreshToken)
// ensure a different cipher can't decode properly (ie: it gets gibberish)
ss, err = sessions.DecodeSessionState(encoded, c2)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.NotEqual(t, "user@domain.com", ss.User)
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.NotEqual(t, s.AccessToken, ss.AccessToken)
assert.NotEqual(t, s.IDToken, ss.IDToken)
assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
}
func TestSessionStateSerializationWithUser(t *testing.T) {
c, err := encryption.NewCipher([]byte(secret))
assert.Equal(t, nil, err)
c2, err := encryption.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err)
s := &sessions.SessionState{
User: "just-user",
Email: "user@domain.com",
AccessToken: "token1234",
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(c)
assert.Equal(t, nil, err)
ss, err := sessions.DecodeSessionState(encoded, c)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email)
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.RefreshToken, ss.RefreshToken)
// ensure a different cipher can't decode properly (ie: it gets gibberish)
ss, err = sessions.DecodeSessionState(encoded, c2)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.NotEqual(t, s.User, ss.User)
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.NotEqual(t, s.AccessToken, ss.AccessToken)
assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
}
func TestSessionStateSerializationNoCipher(t *testing.T) {
s := &sessions.SessionState{
Email: "user@domain.com",
AccessToken: "token1234",
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(nil)
assert.Equal(t, nil, err)
// only email should have been serialized
ss, err := sessions.DecodeSessionState(encoded, nil)
assert.Equal(t, nil, err)
assert.Equal(t, "user@domain.com", ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, "", ss.AccessToken)
assert.Equal(t, "", ss.RefreshToken)
}
func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
s := &sessions.SessionState{
User: "just-user",
Email: "user@domain.com",
AccessToken: "token1234",
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(nil)
assert.Equal(t, nil, err)
// only email should have been serialized
ss, err := sessions.DecodeSessionState(encoded, nil)
assert.Equal(t, nil, err)
assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, "", ss.AccessToken)
assert.Equal(t, "", ss.RefreshToken)
}
func TestExpired(t *testing.T) {
s := &sessions.SessionState{ExpiresOn: time.Now().Add(time.Duration(-1) * time.Minute)}
assert.Equal(t, true, s.IsExpired())
s = &sessions.SessionState{ExpiresOn: time.Now().Add(time.Duration(1) * time.Minute)}
assert.Equal(t, false, s.IsExpired())
s = &sessions.SessionState{}
assert.Equal(t, false, s.IsExpired())
}
type testCase struct {
sessions.SessionState
Encoded string
Cipher *encryption.Cipher
Error bool
}
// TestEncodeSessionState tests EncodeSessionState with the test vector
//
// Currently only tests without cipher here because we have no way to mock
// the random generator used in EncodeSessionState.
func TestEncodeSessionState(t *testing.T) {
c := time.Now()
e := time.Now().Add(time.Duration(1) * time.Hour)
testCases := []testCase{
{
SessionState: sessions.SessionState{
Email: "user@domain.com",
User: "just-user",
},
Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
},
{
SessionState: sessions.SessionState{
Email: "user@domain.com",
User: "just-user",
AccessToken: "token1234",
IDToken: "rawtoken1234",
CreatedAt: c,
ExpiresOn: e,
RefreshToken: "refresh4321",
},
Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
},
}
for i, tc := range testCases {
encoded, err := tc.EncodeSessionState(tc.Cipher)
t.Logf("i:%d Encoded:%#vsessions.SessionState:%#v Error:%#v", i, encoded, tc.SessionState, err)
if tc.Error {
assert.Error(t, err)
assert.Empty(t, encoded)
continue
}
assert.NoError(t, err)
assert.JSONEq(t, tc.Encoded, encoded)
}
}
// TestDecodeSessionState testssessions.DecodeSessionState with the test vector
func TestDecodeSessionState(t *testing.T) {
created := time.Now()
createdJSON, _ := created.MarshalJSON()
createdString := string(createdJSON)
e := time.Now().Add(time.Duration(1) * time.Hour)
eJSON, _ := e.MarshalJSON()
eString := string(eJSON)
eUnix := e.Unix()
c, err := encryption.NewCipher([]byte(secret))
assert.NoError(t, err)
testCases := []testCase{
{
SessionState: sessions.SessionState{
Email: "user@domain.com",
User: "just-user",
},
Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
},
{
SessionState: sessions.SessionState{
Email: "user@domain.com",
User: "user@domain.com",
},
Encoded: `{"Email":"user@domain.com"}`,
},
{
SessionState: sessions.SessionState{
User: "just-user",
},
Encoded: `{"User":"just-user"}`,
},
{
SessionState: sessions.SessionState{
Email: "user@domain.com",
User: "just-user",
},
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{
Email: "user@domain.com",
User: "just-user",
AccessToken: "token1234",
IDToken: "rawtoken1234",
CreatedAt: created,
ExpiresOn: e,
RefreshToken: "refresh4321",
},
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,
},
{
SessionState: sessions.SessionState{
Email: "user@domain.com",
User: "just-user",
},
Encoded: `{"Email":"EGTllJcOFC16b7LBYzLekaHAC5SMMSPdyUrg8hd25g==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw=="}`,
Cipher: c,
},
{
Encoded: `{"Email":"user@domain.com","User":"just-user","AccessToken":"X"}`,
Cipher: c,
Error: true,
},
{
Encoded: `{"Email":"user@domain.com","User":"just-user","IDToken":"XXXX"}`,
Cipher: c,
Error: true,
},
{
SessionState: sessions.SessionState{
User: "just-user",
Email: "user@domain.com",
},
Encoded: "email:user@domain.com user:just-user",
},
{
Encoded: "email:user@domain.com user:just-user||||",
Error: true,
},
{
Encoded: "email:user@domain.com user:just-user",
Cipher: c,
Error: true,
},
{
Encoded: "email:user@domain.com user:just-user|||99999999999999999999|",
Cipher: c,
Error: true,
},
{
SessionState: sessions.SessionState{
Email: "user@domain.com",
User: "just-user",
AccessToken: "token1234",
ExpiresOn: e,
RefreshToken: "refresh4321",
},
Encoded: fmt.Sprintf("email:user@domain.com user:just-user|I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==|%d|qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K", eUnix),
Cipher: c,
},
{
SessionState: sessions.SessionState{
Email: "user@domain.com",
User: "just-user",
AccessToken: "token1234",
IDToken: "rawtoken1234",
ExpiresOn: e,
RefreshToken: "refresh4321",
},
Encoded: fmt.Sprintf("email:user@domain.com user:just-user|I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==|xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==|%d|qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K", eUnix),
Cipher: c,
},
}
for i, tc := range testCases {
ss, err := sessions.DecodeSessionState(tc.Encoded, tc.Cipher)
t.Logf("i:%d Encoded:%#vsessions.SessionState:%#v Error:%#v", i, tc.Encoded, ss, err)
if tc.Error {
assert.Error(t, err)
assert.Nil(t, ss)
continue
}
assert.NoError(t, err)
if assert.NotNil(t, ss) {
assert.Equal(t, tc.User, ss.User)
assert.Equal(t, tc.Email, ss.Email)
assert.Equal(t, tc.AccessToken, ss.AccessToken)
assert.Equal(t, tc.RefreshToken, ss.RefreshToken)
assert.Equal(t, tc.IDToken, ss.IDToken)
assert.Equal(t, tc.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
}
}
}
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

@ -1,41 +0,0 @@
package cookies
import (
"net"
"net/http"
"strings"
"time"
"github.com/pusher/oauth2_proxy/pkg/apis/options"
"github.com/pusher/oauth2_proxy/pkg/logger"
)
// MakeCookie constructs a cookie from the given parameters,
// discovering the domain from the request if not specified.
func MakeCookie(req *http.Request, name string, value string, path string, domain string, httpOnly bool, secure bool, expiration time.Duration, now time.Time) *http.Cookie {
if domain != "" {
host := req.Host
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
if !strings.HasSuffix(host, domain) {
logger.Printf("Warning: request host is %q but using configured cookie domain of %q", host, domain)
}
}
return &http.Cookie{
Name: name,
Value: value,
Path: path,
Domain: domain,
HttpOnly: httpOnly,
Secure: secure,
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,473 +0,0 @@
package logger
import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"runtime"
"sync"
"text/template"
"time"
)
// AuthStatus defines the different types of auth logging that occur
type AuthStatus string
const (
// DefaultStandardLoggingFormat defines the default standard log format
DefaultStandardLoggingFormat = "[{{.Timestamp}}] [{{.File}}] {{.Message}}"
// DefaultAuthLoggingFormat defines the default auth log format
DefaultAuthLoggingFormat = "{{.Client}} - {{.Username}} [{{.Timestamp}}] [{{.Status}}] {{.Message}}"
// DefaultRequestLoggingFormat defines the default request log format
DefaultRequestLoggingFormat = "{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}"
// AuthSuccess indicates that an auth attempt has succeeded explicitly
AuthSuccess AuthStatus = "AuthSuccess"
// AuthFailure indicates that an auth attempt has failed explicitly
AuthFailure AuthStatus = "AuthFailure"
// AuthError indicates that an auth attempt has failed due to an error
AuthError AuthStatus = "AuthError"
// Llongfile flag to log full file name and line number: /a/b/c/d.go:23
Llongfile = 1 << iota
// Lshortfile flag to log final file name element and line number: d.go:23. overrides Llongfile
Lshortfile
// LUTC flag to log UTC datetime rather than the local time zone
LUTC
// LstdFlags flag for initial values for the logger
LstdFlags = Lshortfile
)
// These are the containers for all values that are available as variables in the logging formats.
// All values are pre-formatted strings so it is easy to use them in the format string.
type stdLogMessageData struct {
Timestamp,
File,
Message string
}
type authLogMessageData struct {
Client,
Host,
Protocol,
RequestMethod,
Timestamp,
UserAgent,
Username,
Status,
Message string
}
type reqLogMessageData struct {
Client,
Host,
Protocol,
RequestDuration,
RequestMethod,
RequestURI,
ResponseSize,
StatusCode,
Timestamp,
Upstream,
UserAgent,
Username string
}
// A Logger represents an active logging object that generates lines of
// output to an io.Writer passed through a formatter. Each logging
// operation makes a single call to the Writer's Write method. A Logger
// can be used simultaneously from multiple goroutines; it guarantees to
// serialize access to the Writer.
type Logger struct {
mu sync.Mutex
flag int
writer io.Writer
stdEnabled bool
authEnabled bool
reqEnabled bool
excludePaths map[string]struct{}
stdLogTemplate *template.Template
authTemplate *template.Template
reqTemplate *template.Template
}
// New creates a new Standarderr Logger.
func New(flag int) *Logger {
return &Logger{
writer: os.Stderr,
flag: flag,
stdEnabled: true,
authEnabled: true,
reqEnabled: true,
excludePaths: nil,
stdLogTemplate: template.Must(template.New("std-log").Parse(DefaultStandardLoggingFormat)),
authTemplate: template.Must(template.New("auth-log").Parse(DefaultAuthLoggingFormat)),
reqTemplate: template.Must(template.New("req-log").Parse(DefaultRequestLoggingFormat)),
}
}
var std = New(LstdFlags)
// Output a standard log template with a simple message.
// Write a final newline at the end of every message.
func (l *Logger) Output(calldepth int, message string) {
if !l.stdEnabled {
return
}
now := time.Now()
file := "???:0"
if l.flag&(Lshortfile|Llongfile) != 0 {
file = l.GetFileLineString(calldepth + 1)
}
l.mu.Lock()
defer l.mu.Unlock()
l.stdLogTemplate.Execute(l.writer, stdLogMessageData{
Timestamp: FormatTimestamp(now),
File: file,
Message: message,
})
l.writer.Write([]byte("\n"))
}
// PrintAuth writes auth info to the logger. Requires an http.Request to
// log request details. Remaining arguments are handled in the manner of
// fmt.Sprintf. Writes a final newline to the end of every message.
func (l *Logger) PrintAuth(username string, req *http.Request, status AuthStatus, format string, a ...interface{}) {
if !l.authEnabled {
return
}
now := time.Now()
if username == "" {
username = "-"
}
client := GetClient(req)
l.mu.Lock()
defer l.mu.Unlock()
l.authTemplate.Execute(l.writer, authLogMessageData{
Client: client,
Host: req.Host,
Protocol: req.Proto,
RequestMethod: req.Method,
Timestamp: FormatTimestamp(now),
UserAgent: fmt.Sprintf("%q", req.UserAgent()),
Username: username,
Status: fmt.Sprintf("%s", status),
Message: fmt.Sprintf(format, a...),
})
l.writer.Write([]byte("\n"))
}
// PrintReq writes request details to the Logger using the http.Request,
// url, and timestamp of the request. Writes a final newline to the end
// of every message.
func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url.URL, ts time.Time, status int, size int) {
if !l.reqEnabled {
return
}
if _, ok := l.excludePaths[url.Path]; ok {
return
}
duration := float64(time.Now().Sub(ts)) / float64(time.Second)
if username == "" {
username = "-"
}
if upstream == "" {
upstream = "-"
}
if url.User != nil && username == "-" {
if name := url.User.Username(); name != "" {
username = name
}
}
client := GetClient(req)
l.mu.Lock()
defer l.mu.Unlock()
l.reqTemplate.Execute(l.writer, reqLogMessageData{
Client: client,
Host: req.Host,
Protocol: req.Proto,
RequestDuration: fmt.Sprintf("%0.3f", duration),
RequestMethod: req.Method,
RequestURI: fmt.Sprintf("%q", url.RequestURI()),
ResponseSize: fmt.Sprintf("%d", size),
StatusCode: fmt.Sprintf("%d", status),
Timestamp: FormatTimestamp(ts),
Upstream: upstream,
UserAgent: fmt.Sprintf("%q", req.UserAgent()),
Username: username,
})
l.writer.Write([]byte("\n"))
}
// GetFileLineString will find the caller file and line number
// taking in to account the calldepth to iterate up the stack
// to find the non-logging call location.
func (l *Logger) GetFileLineString(calldepth int) string {
var file string
var line int
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
if l.flag&Lshortfile != 0 {
short := file
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
short = file[i+1:]
break
}
}
file = short
}
return fmt.Sprintf("%s:%d", file, line)
}
// GetClient parses an HTTP request for the client/remote IP address.
func GetClient(req *http.Request) string {
client := req.Header.Get("X-Real-IP")
if client == "" {
client = req.RemoteAddr
}
if c, _, err := net.SplitHostPort(client); err == nil {
client = c
}
return client
}
// FormatTimestamp returns a formatted timestamp.
func (l *Logger) FormatTimestamp(ts time.Time) string {
if l.flag&LUTC != 0 {
ts = ts.UTC()
}
return ts.Format("2006/01/02 15:04:05")
}
// Flags returns the output flags for the logger.
func (l *Logger) Flags() int {
l.mu.Lock()
defer l.mu.Unlock()
return l.flag
}
// SetFlags sets the output flags for the logger.
func (l *Logger) SetFlags(flag int) {
l.mu.Lock()
defer l.mu.Unlock()
l.flag = flag
}
// SetStandardEnabled enables or disables standard logging.
func (l *Logger) SetStandardEnabled(e bool) {
l.mu.Lock()
defer l.mu.Unlock()
l.stdEnabled = e
}
// SetAuthEnabled enables or disables auth logging.
func (l *Logger) SetAuthEnabled(e bool) {
l.mu.Lock()
defer l.mu.Unlock()
l.authEnabled = e
}
// SetReqEnabled enabled or disables request logging.
func (l *Logger) SetReqEnabled(e bool) {
l.mu.Lock()
defer l.mu.Unlock()
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.
func (l *Logger) SetStandardTemplate(t string) {
l.mu.Lock()
defer l.mu.Unlock()
l.stdLogTemplate = template.Must(template.New("std-log").Parse(t))
}
// SetAuthTemplate sets the template for auth logging.
func (l *Logger) SetAuthTemplate(t string) {
l.mu.Lock()
defer l.mu.Unlock()
l.authTemplate = template.Must(template.New("auth-log").Parse(t))
}
// SetReqTemplate sets the template for request logging.
func (l *Logger) SetReqTemplate(t string) {
l.mu.Lock()
defer l.mu.Unlock()
l.reqTemplate = template.Must(template.New("req-log").Parse(t))
}
// These functions utilize the standard logger.
// FormatTimestamp returns a formatted timestamp for the standard logger.
func FormatTimestamp(ts time.Time) string {
return std.FormatTimestamp(ts)
}
// Flags returns the output flags for the standard logger.
func Flags() int {
return std.Flags()
}
// SetFlags sets the output flags for the standard logger.
func SetFlags(flag int) {
std.SetFlags(flag)
}
// SetOutput sets the output destination for the standard logger.
func SetOutput(w io.Writer) {
std.mu.Lock()
defer std.mu.Unlock()
std.writer = w
}
// SetStandardEnabled enables or disables standard logging for the
// standard logger.
func SetStandardEnabled(e bool) {
std.SetStandardEnabled(e)
}
// SetAuthEnabled enables or disables auth logging for the standard
// logger.
func SetAuthEnabled(e bool) {
std.SetAuthEnabled(e)
}
// SetReqEnabled enables or disables request logging for the
// standard logger.
func SetReqEnabled(e bool) {
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
// the standard logger.
func SetStandardTemplate(t string) {
std.SetStandardTemplate(t)
}
// SetAuthTemplate sets the template for auth logging for the
// standard logger.
func SetAuthTemplate(t string) {
std.SetAuthTemplate(t)
}
// SetReqTemplate sets the template for request logging for the
// standard logger.
func SetReqTemplate(t string) {
std.SetReqTemplate(t)
}
// Print calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Print.
func Print(v ...interface{}) {
std.Output(2, fmt.Sprint(v...))
}
// Printf calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Printf.
func Printf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
}
// Println calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...))
}
// Fatal is equivalent to Print() followed by a call to os.Exit(1).
func Fatal(v ...interface{}) {
std.Output(2, fmt.Sprint(v...))
os.Exit(1)
}
// Fatalf is equivalent to Printf() followed by a call to os.Exit(1).
func Fatalf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
os.Exit(1)
}
// Fatalln is equivalent to Println() followed by a call to os.Exit(1).
func Fatalln(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...))
os.Exit(1)
}
// Panic is equivalent to Print() followed by a call to panic().
func Panic(v ...interface{}) {
s := fmt.Sprint(v...)
std.Output(2, s)
panic(s)
}
// Panicf is equivalent to Printf() followed by a call to panic().
func Panicf(format string, v ...interface{}) {
s := fmt.Sprintf(format, v...)
std.Output(2, s)
panic(s)
}
// Panicln is equivalent to Println() followed by a call to panic().
func Panicln(v ...interface{}) {
s := fmt.Sprintln(v...)
std.Output(2, s)
panic(s)
}
// PrintAuthf writes authentication details to the standard logger.
// Arguments are handled in the manner of fmt.Printf.
func PrintAuthf(username string, req *http.Request, status AuthStatus, format string, a ...interface{}) {
std.PrintAuth(username, req, status, format, a...)
}
// PrintReq writes request details to the standard logger.
func PrintReq(username, upstream string, req *http.Request, url url.URL, ts time.Time, status int, size int) {
std.PrintReq(username, upstream, req, url, ts, status, size)
}

View File

@ -1,211 +0,0 @@
package cookie
import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"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"
"github.com/pusher/oauth2_proxy/pkg/sessions/utils"
)
const (
// Cookies are limited to 4kb including the length of the cookie name,
// the cookie name can be up to 256 bytes
maxCookieLength = 3840
)
// Ensure CookieSessionStore implements the interface
var _ sessions.SessionStore = &SessionStore{}
// SessionStore is an implementation of the sessions.SessionStore
// interface that stores sessions in client side cookies
type SessionStore struct {
CookieOptions *options.CookieOptions
CookieCipher *encryption.Cipher
}
// Save takes a sessions.SessionState and stores the information from it
// within Cookies set on the HTTP response writer
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)
if err != nil {
return err
}
s.setSessionCookie(rw, req, value, ss.CreatedAt)
return nil
}
// Load reads sessions.SessionState information from Cookies within the
// HTTP request object
func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
c, err := loadCookie(req, s.CookieOptions.CookieName)
if err != nil {
// always http.ErrNoCookie
return nil, fmt.Errorf("Cookie %q not present", s.CookieOptions.CookieName)
}
val, _, ok := encryption.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire)
if !ok {
return nil, errors.New("Cookie Signature not valid")
}
session, err := utils.SessionFromCookie(val, s.CookieCipher)
if err != nil {
return nil, err
}
return session, nil
}
// Clear clears any saved session information by writing a cookie to
// clear the session
func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
var cookies []*http.Cookie
// matches CookieName, CookieName_<number>
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieOptions.CookieName))
for _, c := range req.Cookies() {
if cookieNameRegex.MatchString(c.Name) {
clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())
http.SetCookie(rw, clearCookie)
cookies = append(cookies, clearCookie)
}
}
return nil
}
// setSessionCookie adds the user's session cookie to the response
func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string, created time.Time) {
for _, c := range s.makeSessionCookie(req, val, created) {
http.SetCookie(rw, c)
}
}
// makeSessionCookie creates an http.Cookie containing the authenticated user's
// authentication details
func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie {
if value != "" {
value = encryption.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now)
}
c := s.makeCookie(req, s.CookieOptions.CookieName, value, s.CookieOptions.CookieExpire, now)
if len(c.Value) > 4096-len(s.CookieOptions.CookieName) {
return splitCookie(c)
}
return []*http.Cookie{c}
}
func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
return cookies.MakeCookieFromOptions(
req,
name,
value,
s.CookieOptions,
expiration,
now,
)
}
// NewCookieSessionStore initialises a new instance of the SessionStore from
// the configuration given
func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
return &SessionStore{
CookieCipher: opts.Cipher,
CookieOptions: cookieOpts,
}, nil
}
// 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
}
// 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)
}
// 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
}
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,
}
}

View File

@ -1,305 +0,0 @@
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

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

View File

@ -1,449 +0,0 @@
package sessions_test
import (
"crypto/rand"
"encoding/base64"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/alicebob/miniredis"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/pusher/oauth2_proxy/pkg/apis/options"
sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/cookies"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/pusher/oauth2_proxy/pkg/sessions"
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) {
RegisterFailHandler(Fail)
RunSpecs(t, "SessionStore")
}
var _ = Describe("NewSessionStore", func() {
var opts *options.SessionOptions
var cookieOpts *options.CookieOptions
var request *http.Request
var response *httptest.ResponseRecorder
var session *sessionsapi.SessionState
var ss sessionsapi.SessionStore
var mr *miniredis.Miniredis
CheckCookieOptions := func() {
Context("the cookies returned", func() {
var cookies []*http.Cookie
BeforeEach(func() {
cookies = response.Result().Cookies()
})
It("have the correct name set", func() {
if len(cookies) == 1 {
Expect(cookies[0].Name).To(Equal(cookieOpts.CookieName))
} else {
for _, cookie := range cookies {
Expect(cookie.Name).To(ContainSubstring(cookieOpts.CookieName))
}
}
})
It("have the correct path set", func() {
for _, cookie := range cookies {
Expect(cookie.Path).To(Equal(cookieOpts.CookiePath))
}
})
It("have the correct domain set", func() {
for _, cookie := range cookies {
Expect(cookie.Domain).To(Equal(cookieOpts.CookieDomain))
}
})
It("have the correct HTTPOnly set", func() {
for _, cookie := range cookies {
Expect(cookie.HttpOnly).To(Equal(cookieOpts.CookieHTTPOnly))
}
})
It("have the correct secure set", func() {
for _, cookie := range cookies {
Expect(cookie.Secure).To(Equal(cookieOpts.CookieSecure))
}
})
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()))))
}
}
})
})
}
// 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("with no existing session", func() {
BeforeEach(func() {
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 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()
})
Context("when Clear is called", func() {
BeforeEach(func() {
req := httptest.NewRequest("GET", "http://example.com/", nil)
saveResp := httptest.NewRecorder()
err := ss.Save(saveResp, req, session)
Expect(err).ToNot(HaveOccurred())
for _, c := range saveResp.Result().Cookies() {
request.AddCookie(c)
}
err = ss.Clear(response, request)
Expect(err).ToNot(HaveOccurred())
})
It("sets a `set-cookie` header in the response", func() {
Expect(response.Header().Get("Set-Cookie")).ToNot(BeEmpty())
})
CheckCookieOptions()
})
Context("when Load is called", func() {
LoadSessionTests := func() {
var loadedSession *sessionsapi.SessionState
BeforeEach(func() {
var err error
loadedSession, err = ss.Load(request)
Expect(err).ToNot(HaveOccurred())
})
It("loads a session equal to the original session", func() {
if cookieOpts.CookieSecret == "" {
// Only Email and User stored in session when encrypted
Expect(loadedSession.Email).To(Equal(session.Email))
Expect(loadedSession.User).To(Equal(session.User))
} else {
// All fields stored in session if encrypted
// Can't compare time.Time using Equal() so remove ExpiresOn from sessions
l := *loadedSession
l.CreatedAt = time.Time{}
l.ExpiresOn = time.Time{}
s := *session
s.CreatedAt = time.Time{}
s.ExpiresOn = time.Time{}
Expect(l).To(Equal(s))
// Compare time.Time separately
Expect(loadedSession.CreatedAt.Equal(session.CreatedAt)).To(BeTrue())
Expect(loadedSession.ExpiresOn.Equal(session.ExpiresOn)).To(BeTrue())
}
})
}
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() {
BeforeEach(func() {
var err error
ss, err = sessions.NewSessionStore(opts, cookieOpts)
Expect(err).ToNot(HaveOccurred())
})
SessionStoreInterfaceTests(persistent)
})
Context("with non-default options", func() {
BeforeEach(func() {
cookieOpts = &options.CookieOptions{
CookieName: "_cookie_name",
CookiePath: "/path",
CookieExpire: time.Duration(72) * time.Hour,
CookieRefresh: time.Duration(2) * time.Hour,
CookieSecure: false,
CookieHTTPOnly: false,
CookieDomain: "example.com",
}
var err error
ss, err = sessions.NewSessionStore(opts, cookieOpts)
Expect(err).ToNot(HaveOccurred())
})
SessionStoreInterfaceTests(persistent)
})
Context("with a cipher", func() {
BeforeEach(func() {
secret := make([]byte, 32)
_, err := rand.Read(secret)
Expect(err).ToNot(HaveOccurred())
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)
Expect(err).ToNot(HaveOccurred())
})
SessionStoreInterfaceTests(persistent)
})
}
BeforeEach(func() {
ss = nil
opts = &options.SessionOptions{}
// Set default options in CookieOptions
cookieOpts = &options.CookieOptions{
CookieName: "_oauth2_proxy",
CookiePath: "/",
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(1) * time.Hour,
CookieSecure: true,
CookieHTTPOnly: true,
}
session = &sessionsapi.SessionState{
AccessToken: "AccessToken",
IDToken: "IDToken",
ExpiresOn: time.Now().Add(1 * time.Hour),
RefreshToken: "RefreshToken",
Email: "john.doe@example.com",
User: "john.doe",
}
request = httptest.NewRequest("GET", "http://example.com/", nil)
response = httptest.NewRecorder()
})
Context("with type 'cookie'", func() {
BeforeEach(func() {
opts.Type = options.CookieSessionStoreType
})
It("creates a cookie.SessionStore", func() {
ss, err := sessions.NewSessionStore(opts, cookieOpts)
Expect(err).NotTo(HaveOccurred())
Expect(ss).To(BeAssignableToTypeOf(&sessionscookie.SessionStore{}))
})
Context("the cookie.SessionStore", func() {
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)
})
})
Context("with an invalid type", func() {
BeforeEach(func() {
opts.Type = "invalid-type"
})
It("returns an error", func() {
ss, err := sessions.NewSessionStore(opts, cookieOpts)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("unknown session store type 'invalid-type'"))
Expect(ss).To(BeNil())
})
})
})

View File

@ -1,41 +0,0 @@
package utils
import (
"encoding/base64"
"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
func CookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) {
return s.EncodeSessionState(c)
}
// SessionFromCookie deserializes a session from a cookie value
func SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) {
return sessions.DecodeSessionState(v, c)
}
// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
func SecretBytes(secret string) []byte {
b, err := base64.URLEncoding.DecodeString(addPadding(secret))
if err == nil {
return []byte(addPadding(string(b)))
}
return []byte(secret)
}
func addPadding(secret string) string {
padding := len(secret) % 4
switch padding {
case 1:
return secret + "==="
case 2:
return secret + "=="
case 3:
return secret + "="
default:
return secret
}
}

View File

@ -3,22 +3,18 @@ package providers
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/bitly/go-simplejson"
"github.com/bitly/oauth2_proxy/api"
"log"
"net/http" "net/http"
"net/url" "net/url"
"github.com/bitly/go-simplejson"
"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
type AzureProvider struct { type AzureProvider struct {
*ProviderData *ProviderData
Tenant string Tenant string
} }
// NewAzureProvider initiates a new AzureProvider
func NewAzureProvider(p *ProviderData) *AzureProvider { func NewAzureProvider(p *ProviderData) *AzureProvider {
p.ProviderName = "Azure" p.ProviderName = "Azure"
@ -43,7 +39,6 @@ func NewAzureProvider(p *ProviderData) *AzureProvider {
return &AzureProvider{ProviderData: p} return &AzureProvider{ProviderData: p}
} }
// Configure defaults the AzureProvider configuration options
func (p *AzureProvider) Configure(tenant string) { func (p *AzureProvider) Configure(tenant string) {
p.Tenant = tenant p.Tenant = tenant
if tenant == "" { if tenant == "" {
@ -65,9 +60,9 @@ func (p *AzureProvider) Configure(tenant string) {
} }
} }
func getAzureHeader(accessToken string) http.Header { func getAzureHeader(access_token string) http.Header {
header := make(http.Header) header := make(http.Header)
header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token))
return header return header
} }
@ -88,8 +83,7 @@ func getEmailFromJSON(json *simplejson.Json) (string, error) {
return email, err return email, err
} }
// GetEmailAddress returns the Account email address func (p *AzureProvider) GetEmailAddress(s *SessionState) (string, error) {
func (p *AzureProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
var email string var email string
var err error var err error
@ -102,7 +96,7 @@ func (p *AzureProvider) GetEmailAddress(s *sessions.SessionState) (string, error
} }
req.Header = getAzureHeader(s.AccessToken) req.Header = getAzureHeader(s.AccessToken)
json, err := requests.Request(req) json, err := api.Request(req)
if err != nil { if err != nil {
return "", err return "", err
@ -117,12 +111,12 @@ func (p *AzureProvider) GetEmailAddress(s *sessions.SessionState) (string, error
email, err = json.Get("userPrincipalName").String() email, err = json.Get("userPrincipalName").String()
if err != nil { if err != nil {
logger.Printf("failed making request %s", err) log.Printf("failed making request %s", err)
return "", err return "", err
} }
if email == "" { if email == "" {
logger.Printf("failed to get email address") log.Printf("failed to get email address")
return "", err return "", err
} }

View File

@ -6,7 +6,6 @@ import (
"net/url" "net/url"
"testing" "testing"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -111,7 +110,8 @@ func testAzureBackend(payload string) *httptest.Server {
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 { url := r.URL
if url.Path != path || url.RawQuery != query {
w.WriteHeader(404) w.WriteHeader(404)
} else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" { } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
w.WriteHeader(403) w.WriteHeader(403)
@ -129,7 +129,7 @@ func TestAzureProviderGetEmailAddress(t *testing.T) {
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testAzureProvider(bURL.Host) p := testAzureProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_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, "user@windows.net", email) assert.Equal(t, "user@windows.net", email)
@ -142,7 +142,7 @@ func TestAzureProviderGetEmailAddressMailNull(t *testing.T) {
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testAzureProvider(bURL.Host) p := testAzureProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_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, "user@windows.net", email) assert.Equal(t, "user@windows.net", email)
@ -155,7 +155,7 @@ func TestAzureProviderGetEmailAddressGetUserPrincipalName(t *testing.T) {
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testAzureProvider(bURL.Host) p := testAzureProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_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, "user@windows.net", email) assert.Equal(t, "user@windows.net", email)
@ -168,7 +168,7 @@ func TestAzureProviderGetEmailAddressFailToGetEmailAddress(t *testing.T) {
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testAzureProvider(bURL.Host) p := testAzureProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session) email, err := p.GetEmailAddress(session)
assert.Equal(t, "type assertion to string failed", err.Error()) assert.Equal(t, "type assertion to string failed", err.Error())
assert.Equal(t, "", email) assert.Equal(t, "", email)
@ -181,7 +181,7 @@ func TestAzureProviderGetEmailAddressEmptyUserPrincipalName(t *testing.T) {
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testAzureProvider(bURL.Host) p := testAzureProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_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, "", email) assert.Equal(t, "", email)
@ -194,7 +194,7 @@ func TestAzureProviderGetEmailAddressIncorrectOtherMails(t *testing.T) {
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testAzureProvider(bURL.Host) p := testAzureProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session) email, err := p.GetEmailAddress(session)
assert.Equal(t, "type assertion to string failed", err.Error()) assert.Equal(t, "type assertion to string failed", err.Error())
assert.Equal(t, "", email) assert.Equal(t, "", email)

View File

@ -1,163 +0,0 @@
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
}

View File

@ -1,170 +0,0 @@
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,16 +6,13 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/bitly/oauth2_proxy/api"
"github.com/pusher/oauth2_proxy/pkg/requests"
) )
// FacebookProvider represents an Facebook based Identity Provider
type FacebookProvider struct { type FacebookProvider struct {
*ProviderData *ProviderData
} }
// NewFacebookProvider initiates a new FacebookProvider
func NewFacebookProvider(p *ProviderData) *FacebookProvider { func NewFacebookProvider(p *ProviderData) *FacebookProvider {
p.ProviderName = "Facebook" p.ProviderName = "Facebook"
if p.LoginURL.String() == "" { if p.LoginURL.String() == "" {
@ -46,16 +43,15 @@ func NewFacebookProvider(p *ProviderData) *FacebookProvider {
return &FacebookProvider{ProviderData: p} return &FacebookProvider{ProviderData: p}
} }
func getFacebookHeader(accessToken string) http.Header { func getFacebookHeader(access_token string) http.Header {
header := make(http.Header) header := make(http.Header)
header.Set("Accept", "application/json") header.Set("Accept", "application/json")
header.Set("x-li-format", "json") header.Set("x-li-format", "json")
header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token))
return header return header
} }
// GetEmailAddress returns the Account email address func (p *FacebookProvider) GetEmailAddress(s *SessionState) (string, error) {
func (p *FacebookProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
if s.AccessToken == "" { if s.AccessToken == "" {
return "", errors.New("missing access token") return "", errors.New("missing access token")
} }
@ -69,7 +65,7 @@ func (p *FacebookProvider) GetEmailAddress(s *sessions.SessionState) (string, er
Email string Email string
} }
var r result var r result
err = requests.RequestJSON(req, &r) err = api.RequestJson(req, &r)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -79,7 +75,6 @@ func (p *FacebookProvider) GetEmailAddress(s *sessions.SessionState) (string, er
return r.Email, nil return r.Email, nil
} }
// ValidateSessionState validates the AccessToken func (p *FacebookProvider) ValidateSessionState(s *SessionState) bool {
func (p *FacebookProvider) ValidateSessionState(s *sessions.SessionState) bool {
return validateToken(p, s.AccessToken, getFacebookHeader(s.AccessToken)) return validateToken(p, s.AccessToken, getFacebookHeader(s.AccessToken))
} }

View File

@ -4,24 +4,20 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
// GitHubProvider represents an GitHub based Identity Provider
type GitHubProvider struct { type GitHubProvider struct {
*ProviderData *ProviderData
Org string Org string
Team string Team string
} }
// NewGitHubProvider initiates a new GitHubProvider
func NewGitHubProvider(p *ProviderData) *GitHubProvider { func NewGitHubProvider(p *ProviderData) *GitHubProvider {
p.ProviderName = "GitHub" p.ProviderName = "GitHub"
if p.LoginURL == nil || p.LoginURL.String() == "" { if p.LoginURL == nil || p.LoginURL.String() == "" {
@ -51,8 +47,6 @@ func NewGitHubProvider(p *ProviderData) *GitHubProvider {
} }
return &GitHubProvider{ProviderData: p} return &GitHubProvider{ProviderData: p}
} }
// SetOrgTeam adds GitHub org reading parameters to the OAuth2 scope
func (p *GitHubProvider) SetOrgTeam(org, team string) { func (p *GitHubProvider) SetOrgTeam(org, team string) {
p.Org = org p.Org = org
p.Team = team p.Team = team
@ -112,19 +106,19 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) {
} }
orgs = append(orgs, op...) orgs = append(orgs, op...)
pn++ pn += 1
} }
var presentOrgs []string var presentOrgs []string
for _, org := range orgs { for _, org := range orgs {
if p.Org == org.Login { if p.Org == org.Login {
logger.Printf("Found Github Organization: %q", org.Login) log.Printf("Found Github Organization: %q", org.Login)
return true, nil return true, nil
} }
presentOrgs = append(presentOrgs, org.Login) presentOrgs = append(presentOrgs, org.Login)
} }
logger.Printf("Missing Organization:%q in %v", p.Org, presentOrgs) log.Printf("Missing Organization:%q in %v", p.Org, presentOrgs)
return false, nil return false, nil
} }
@ -181,7 +175,7 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
ts := strings.Split(p.Team, ",") ts := strings.Split(p.Team, ",")
for _, t := range ts { for _, t := range ts {
if t == team.Slug { if t == team.Slug {
logger.Printf("Found Github Organization:%q Team:%q (Name:%q)", team.Org.Login, team.Slug, team.Name) log.Printf("Found Github Organization:%q Team:%q (Name:%q)", team.Org.Login, team.Slug, team.Name)
return true, nil return true, nil
} }
} }
@ -189,24 +183,22 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
} }
} }
if hasOrg { if hasOrg {
logger.Printf("Missing Team:%q from Org:%q in teams: %v", p.Team, p.Org, presentTeams) log.Printf("Missing Team:%q from Org:%q in teams: %v", p.Team, p.Org, presentTeams)
} else { } else {
var allOrgs []string var allOrgs []string
for org := range presentOrgs { for org, _ := range presentOrgs {
allOrgs = append(allOrgs, org) allOrgs = append(allOrgs, org)
} }
logger.Printf("Missing Organization:%q in %#v", p.Org, allOrgs) log.Printf("Missing Organization:%q in %#v", p.Org, allOrgs)
} }
return false, nil return false, nil
} }
// GetEmailAddress returns the Account email address func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
func (p *GitHubProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
var emails []struct { var emails []struct {
Email string `json:"email"` Email string `json:"email"`
Primary bool `json:"primary"` Primary bool `json:"primary"`
Verified bool `json:"verified"`
} }
// if we require an Org or Team, check that first // if we require an Org or Team, check that first
@ -244,14 +236,14 @@ func (p *GitHubProvider) GetEmailAddress(s *sessions.SessionState) (string, erro
resp.StatusCode, endpoint.String(), body) resp.StatusCode, endpoint.String(), body)
} }
logger.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body) log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
if err := json.Unmarshal(body, &emails); err != nil { if err := json.Unmarshal(body, &emails); err != nil {
return "", fmt.Errorf("%s unmarshaling %s", err, body) return "", fmt.Errorf("%s unmarshaling %s", err, body)
} }
for _, email := range emails { for _, email := range emails {
if email.Primary && email.Verified { if email.Primary {
return email.Email, nil return email.Email, nil
} }
} }
@ -259,8 +251,7 @@ func (p *GitHubProvider) GetEmailAddress(s *sessions.SessionState) (string, erro
return "", nil return "", nil
} }
// GetUserName returns the Account user name func (p *GitHubProvider) GetUserName(s *SessionState) (string, error) {
func (p *GitHubProvider) GetUserName(s *sessions.SessionState) (string, error) {
var user struct { var user struct {
Login string `json:"login"` Login string `json:"login"`
Email string `json:"email"` Email string `json:"email"`
@ -294,7 +285,7 @@ func (p *GitHubProvider) GetUserName(s *sessions.SessionState) (string, error) {
resp.StatusCode, endpoint.String(), body) resp.StatusCode, endpoint.String(), body)
} }
logger.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body) log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
if err := json.Unmarshal(body, &user); err != nil { if err := json.Unmarshal(body, &user); err != nil {
return "", fmt.Errorf("%s unmarshaling %s", err, body) return "", fmt.Errorf("%s unmarshaling %s", err, body)

View File

@ -6,7 +6,6 @@ import (
"net/url" "net/url"
"testing" "testing"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -30,18 +29,19 @@ func testGitHubProvider(hostname string) *GitHubProvider {
func testGitHubBackend(payload []string) *httptest.Server { func testGitHubBackend(payload []string) *httptest.Server {
pathToQueryMap := map[string][]string{ pathToQueryMap := map[string][]string{
"/user": {""}, "/user": []string{""},
"/user/emails": {""}, "/user/emails": []string{""},
"/user/orgs": {"limit=200&page=1", "limit=200&page=2", "limit=200&page=3"}, "/user/orgs": []string{"limit=200&page=1", "limit=200&page=2", "limit=200&page=3"},
} }
return httptest.NewServer(http.HandlerFunc( return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
query, ok := pathToQueryMap[r.URL.Path] url := r.URL
query, ok := pathToQueryMap[url.Path]
validQuery := false validQuery := false
index := 0 index := 0
for i, q := range query { for i, q := range query {
if q == r.URL.RawQuery { if q == url.RawQuery {
validQuery = true validQuery = true
index = i index = i
} }
@ -98,35 +98,22 @@ func TestGitHubProviderOverrides(t *testing.T) {
} }
func TestGitHubProviderGetEmailAddress(t *testing.T) { func TestGitHubProviderGetEmailAddress(t *testing.T) {
b := testGitHubBackend([]string{`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}) b := testGitHubBackend([]string{`[ {"email": "michael.bland@gsa.gov", "primary": true} ]`})
defer b.Close() defer b.Close()
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testGitHubProvider(bURL.Host) p := testGitHubProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_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, "michael.bland@gsa.gov", email)
} }
func TestGitHubProviderGetEmailAddressNotVerified(t *testing.T) {
b := testGitHubBackend([]string{`[ {"email": "michael.bland@gsa.gov", "verified": false, "primary": true} ]`})
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testGitHubProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Empty(t, "", email)
}
func TestGitHubProviderGetEmailAddressWithOrg(t *testing.T) { func TestGitHubProviderGetEmailAddressWithOrg(t *testing.T) {
b := testGitHubBackend([]string{ b := testGitHubBackend([]string{
`[ {"email": "michael.bland@gsa.gov", "primary": true, "verified": true, "login":"testorg"} ]`, `[ {"email": "michael.bland@gsa.gov", "primary": true, "login":"testorg"} ]`,
`[ {"email": "michael.bland1@gsa.gov", "primary": true, "verified": true, "login":"testorg1"} ]`, `[ {"email": "michael.bland1@gsa.gov", "primary": true, "login":"testorg1"} ]`,
`[ ]`, `[ ]`,
}) })
defer b.Close() defer b.Close()
@ -135,7 +122,7 @@ func TestGitHubProviderGetEmailAddressWithOrg(t *testing.T) {
p := testGitHubProvider(bURL.Host) p := testGitHubProvider(bURL.Host)
p.Org = "testorg1" p.Org = "testorg1"
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_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, "michael.bland@gsa.gov", email)
@ -153,7 +140,7 @@ func TestGitHubProviderGetEmailAddressFailedRequest(t *testing.T) {
// We'll trigger a request failure by using an unexpected access // We'll trigger a request failure by using an unexpected access
// token. Alternatively, we could allow the parsing of the payload as // token. Alternatively, we could allow the parsing of the payload as
// JSON to fail. // JSON to fail.
session := &sessions.SessionState{AccessToken: "unexpected_access_token"} session := &SessionState{AccessToken: "unexpected_access_token"}
email, err := p.GetEmailAddress(session) email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
assert.Equal(t, "", email) assert.Equal(t, "", email)
@ -166,7 +153,7 @@ func TestGitHubProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testGitHubProvider(bURL.Host) p := testGitHubProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session) email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
assert.Equal(t, "", email) assert.Equal(t, "", email)
@ -179,7 +166,7 @@ func TestGitHubProviderGetUserName(t *testing.T) {
bURL, _ := url.Parse(b.URL) bURL, _ := url.Parse(b.URL)
p := testGitHubProvider(bURL.Host) p := testGitHubProvider(bURL.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetUserName(session) email, err := p.GetUserName(session)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "mbland", email) assert.Equal(t, "mbland", email)

View File

@ -1,258 +1,58 @@
package providers package providers
import ( import (
"context" "log"
"encoding/json"
"fmt"
"io/ioutil"
"net/http" "net/http"
"strings" "net/url"
"time"
oidc "github.com/coreos/go-oidc" "github.com/bitly/oauth2_proxy/api"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"golang.org/x/oauth2"
) )
// 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
func NewGitLabProvider(p *ProviderData) *GitLabProvider { func NewGitLabProvider(p *ProviderData) *GitLabProvider {
p.ProviderName = "GitLab" p.ProviderName = "GitLab"
if p.LoginURL == nil || p.LoginURL.String() == "" {
if p.Scope == "" { p.LoginURL = &url.URL{
p.Scope = "openid email" 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 == "" {
p.Scope = "read_user"
} }
return &GitLabProvider{ProviderData: p} return &GitLabProvider{ProviderData: p}
} }
// Redeem exchanges the OAuth2 authentication token for an ID token func (p *GitLabProvider) GetEmailAddress(s *SessionState) (string, error) {
func (p *GitLabProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
ctx := context.Background() req, err := http.NewRequest("GET",
c := oauth2.Config{ p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: p.RedeemURL.String(),
},
RedirectURL: redirectURL,
}
token, err := c.Exchange(ctx, code)
if err != nil { if err != nil {
return nil, fmt.Errorf("token exchange: %v", err) log.Printf("failed building request %s", err)
return "", err
} }
s, err = p.createSessionState(ctx, token) json, err := api.Request(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to update session: %v", err) log.Printf("failed making request %s", err)
return "", err
} }
return return json.Get("email").String()
}
// 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
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)
}
// Check if email is verified
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 {
return "", fmt.Errorf("email domain check failed: %v", err)
}
// Check group membership
err = p.verifyGroupMembership(userInfo)
if err != nil {
return "", fmt.Errorf("group membership check failed: %v", err)
}
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

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

View File

@ -8,20 +8,18 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time" "time"
"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" "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/googleapi" "google.golang.org/api/googleapi"
) )
// GoogleProvider represents an Google based Identity Provider
type GoogleProvider struct { type GoogleProvider struct {
*ProviderData *ProviderData
RedeemRefreshURL *url.URL RedeemRefreshURL *url.URL
@ -30,13 +28,6 @@ type GoogleProvider struct {
GroupValidator func(string) bool GroupValidator func(string) bool
} }
type claims struct {
Subject string `json:"sub"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
// NewGoogleProvider initiates a new GoogleProvider
func NewGoogleProvider(p *ProviderData) *GoogleProvider { func NewGoogleProvider(p *ProviderData) *GoogleProvider {
p.ProviderName = "Google" p.ProviderName = "Google"
if p.LoginURL.String() == "" { if p.LoginURL.String() == "" {
@ -71,33 +62,34 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
} }
} }
func claimsFromIDToken(idToken string) (*claims, error) { func emailFromIdToken(idToken string) (string, error) {
// id_token is a base64 encode ID token payload // id_token is a base64 encode ID token payload
// https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
jwt := strings.Split(idToken, ".") jwt := strings.Split(idToken, ".")
jwtData := strings.TrimSuffix(jwt[1], "=") b, err := base64.RawURLEncoding.DecodeString(jwt[1])
b, err := base64.RawURLEncoding.DecodeString(jwtData)
if err != nil { if err != nil {
return nil, err return "", err
} }
c := &claims{} var email struct {
err = json.Unmarshal(b, c) Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
err = json.Unmarshal(b, &email)
if err != nil { if err != nil {
return nil, err return "", err
} }
if c.Email == "" { if email.Email == "" {
return nil, errors.New("missing email") return "", errors.New("missing email")
} }
if !c.EmailVerified { if !email.EmailVerified {
return nil, fmt.Errorf("email %s not listed as verified", c.Email) return "", fmt.Errorf("email %s not listed as verified", email.Email)
} }
return c, nil return email.Email, nil
} }
// Redeem exchanges the OAuth2 authentication token for an ID token func (p *GoogleProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
if code == "" { if code == "" {
err = errors.New("missing code") err = errors.New("missing code")
return return
@ -136,24 +128,23 @@ func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionSt
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"` ExpiresIn int64 `json:"expires_in"`
IDToken string `json:"id_token"` IdToken string `json:"id_token"`
} }
err = json.Unmarshal(body, &jsonResponse) err = json.Unmarshal(body, &jsonResponse)
if err != nil { if err != nil {
return return
} }
c, err := claimsFromIDToken(jsonResponse.IDToken) var email string
email, err = emailFromIdToken(jsonResponse.IdToken)
if err != nil { if err != nil {
return return
} }
s = &sessions.SessionState{ s = &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: email,
User: c.Subject,
} }
return return
} }
@ -172,75 +163,98 @@ func (p *GoogleProvider) SetGroupRestriction(groups []string, adminEmail string,
func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Service { func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Service {
data, err := ioutil.ReadAll(credentialsReader) data, err := ioutil.ReadAll(credentialsReader)
if err != nil { if err != nil {
logger.Fatal("can't read Google credentials file:", err) log.Fatal("can't read Google credentials file:", err)
} }
conf, err := google.JWTConfigFromJSON(data, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope) conf, err := google.JWTConfigFromJSON(data, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope)
if err != nil { if err != nil {
logger.Fatal("can't load Google credentials file:", err) log.Fatal("can't load Google credentials file:", err)
} }
conf.Subject = adminEmail conf.Subject = adminEmail
client := conf.Client(oauth2.NoContext) client := conf.Client(oauth2.NoContext)
adminService, err := admin.New(client) adminService, err := admin.New(client)
if err != nil { if err != nil {
logger.Fatal(err) log.Fatal(err)
} }
return adminService return adminService
} }
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 {
log.Printf("error fetching user: %v", err)
return false
}
id := user.Id
custID := user.CustomerId
for _, group := range groups { for _, group := range groups {
// Use the HasMember API to checking for the user's presence in each group or nested subgroups members, err := fetchGroupMembers(service, group)
req := service.Members.HasMember(group, email)
r, err := req.Do()
if err != nil { if err != nil {
err, ok := err.(*googleapi.Error) if err, ok := err.(*googleapi.Error); ok && err.Code == 404 {
if ok && err.Code == 404 { log.Printf("error fetching members for group %s: group does not exist", group)
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
}
// 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
}
} else { } else {
logger.Printf("error checking group membership: %v", err) log.Printf("error fetching group members: %v", err)
return false
} }
continue
} }
if r.IsMember {
for _, member := range members {
switch member.Type {
case "CUSTOMER":
if member.Id == custID {
return true return true
} }
case "USER":
if member.Id == id {
return true
}
}
}
} }
return false 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()
if err != nil {
return nil, err
}
for _, member := range r.Members {
members = append(members, member)
}
if r.NextPageToken == "" {
break
}
pageToken = r.NextPageToken
}
return members, nil
}
// ValidateGroup validates that the provided email exists in the configured Google // ValidateGroup validates that the provided email exists in the configured Google
// group(s). // group(s).
func (p *GoogleProvider) ValidateGroup(email string) bool { func (p *GoogleProvider) ValidateGroup(email string) bool {
return p.GroupValidator(email) return p.GroupValidator(email)
} }
// RefreshSessionIfNeeded checks if the session has expired and uses the func (p *GoogleProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
// RefreshToken to fetch a new ID token if required
func (p *GoogleProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" { if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
return false, nil return false, nil
} }
newToken, newIDToken, duration, err := p.redeemRefreshToken(s.RefreshToken) newToken, duration, err := p.redeemRefreshToken(s.RefreshToken)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -252,13 +266,12 @@ func (p *GoogleProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool,
origExpiration := s.ExpiresOn origExpiration := s.ExpiresOn
s.AccessToken = newToken s.AccessToken = newToken
s.IDToken = newIDToken
s.ExpiresOn = time.Now().Add(duration).Truncate(time.Second) s.ExpiresOn = time.Now().Add(duration).Truncate(time.Second)
logger.Printf("refreshed access token %s (expired on %s)", s, origExpiration) log.Printf("refreshed access token %s (expired on %s)", s, origExpiration)
return true, nil return true, nil
} }
func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, idToken string, expires time.Duration, err error) { func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, expires time.Duration, err error) {
// https://developers.google.com/identity/protocols/OAuth2WebServer#refresh // https://developers.google.com/identity/protocols/OAuth2WebServer#refresh
params := url.Values{} params := url.Values{}
params.Add("client_id", p.ClientID) params.Add("client_id", p.ClientID)
@ -291,14 +304,12 @@ func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string,
var data struct { var data struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"` ExpiresIn int64 `json:"expires_in"`
IDToken string `json:"id_token"`
} }
err = json.Unmarshal(body, &data) err = json.Unmarshal(body, &data)
if err != nil { if err != nil {
return return
} }
token = data.AccessToken token = data.AccessToken
idToken = data.IDToken
expires = time.Duration(data.ExpiresIn) * time.Second expires = time.Duration(data.ExpiresIn) * time.Second
return return
} }

View File

@ -1,19 +1,14 @@
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) {
@ -86,7 +81,7 @@ type redeemResponse struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"` ExpiresIn int64 `json:"expires_in"`
IDToken string `json:"id_token"` IdToken string `json:"id_token"`
} }
func TestGoogleProviderGetEmailAddress(t *testing.T) { func TestGoogleProviderGetEmailAddress(t *testing.T) {
@ -95,7 +90,7 @@ func TestGoogleProviderGetEmailAddress(t *testing.T) {
AccessToken: "a1234", AccessToken: "a1234",
ExpiresIn: 10, ExpiresIn: 10,
RefreshToken: "refresh12345", RefreshToken: "refresh12345",
IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": "michael.bland@gsa.gov", "email_verified":true}`)), IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": "michael.bland@gsa.gov", "email_verified":true}`)),
}) })
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
var server *httptest.Server var server *httptest.Server
@ -132,7 +127,7 @@ func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
p := newGoogleProvider() p := newGoogleProvider()
body, err := json.Marshal(redeemResponse{ body, err := json.Marshal(redeemResponse{
AccessToken: "a1234", AccessToken: "a1234",
IDToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`, IdToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`,
}) })
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
var server *httptest.Server var server *httptest.Server
@ -151,7 +146,7 @@ func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
body, err := json.Marshal(redeemResponse{ body, err := json.Marshal(redeemResponse{
AccessToken: "a1234", AccessToken: "a1234",
IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": michael.bland@gsa.gov}`)), IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": michael.bland@gsa.gov}`)),
}) })
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
var server *httptest.Server var server *httptest.Server
@ -170,7 +165,7 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
p := newGoogleProvider() p := newGoogleProvider()
body, err := json.Marshal(redeemResponse{ body, err := json.Marshal(redeemResponse{
AccessToken: "a1234", AccessToken: "a1234",
IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)), IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)),
}) })
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
var server *httptest.Server var server *httptest.Server
@ -184,56 +179,3 @@ 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

@ -2,11 +2,11 @@ package providers
import ( import (
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"net/url" "net/url"
"github.com/pusher/oauth2_proxy/pkg/logger" "github.com/bitly/oauth2_proxy/api"
"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"
@ -24,14 +24,14 @@ func stripToken(endpoint string) string {
func stripParam(param, endpoint string) string { func stripParam(param, endpoint string) string {
u, err := url.Parse(endpoint) u, err := url.Parse(endpoint)
if err != nil { if err != nil {
logger.Printf("error attempting to strip %s: %s", param, err) log.Printf("error attempting to strip %s: %s", param, err)
return endpoint return endpoint
} }
if u.RawQuery != "" { if u.RawQuery != "" {
values, err := url.ParseQuery(u.RawQuery) values, err := url.ParseQuery(u.RawQuery)
if err != nil { if err != nil {
logger.Printf("error attempting to strip %s: %s", param, err) log.Printf("error attempting to strip %s: %s", param, err)
return u.String() return u.String()
} }
@ -46,29 +46,34 @@ 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, access_token string, header http.Header) bool {
if accessToken == "" || p.Data().ValidateURL == nil || p.Data().ValidateURL.String() == "" { if access_token == "" || p.Data().ValidateURL == nil {
return false return false
} }
endpoint := p.Data().ValidateURL.String() endpoint := p.Data().ValidateURL.String()
if len(header) == 0 { if len(header) == 0 {
params := url.Values{"access_token": {accessToken}} params := url.Values{"access_token": {access_token}}
endpoint = endpoint + "?" + params.Encode() endpoint = endpoint + "?" + params.Encode()
} }
resp, err := requests.RequestUnparsedResponse(endpoint, header) resp, err := api.RequestUnparsedResponse(endpoint, header)
if err != nil { if err != nil {
logger.Printf("GET %s", stripToken(endpoint)) log.Printf("GET %s", stripToken(endpoint))
logger.Printf("token validation request failed: %s", err) log.Printf("token validation request failed: %s", err)
return false return false
} }
body, _ := ioutil.ReadAll(resp.Body) body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
logger.Printf("%d GET %s %s", resp.StatusCode, stripToken(endpoint), body) log.Printf("%d GET %s %s", resp.StatusCode, stripToken(endpoint), body)
if resp.StatusCode == 200 { if resp.StatusCode == 200 {
return true return true
} }
logger.Printf("token validation request failed: status %d - %s", resp.StatusCode, body) log.Printf("token validation request failed: status %d - %s", resp.StatusCode, body)
return false return false
} }
func updateURL(url *url.URL, hostname string) {
url.Scheme = "http"
url.Host = hostname
}

View File

@ -7,52 +7,46 @@ import (
"net/url" "net/url"
"testing" "testing"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func updateURL(url *url.URL, hostname string) {
url.Scheme = "http"
url.Host = hostname
}
type ValidateSessionStateTestProvider struct { type ValidateSessionStateTestProvider struct {
*ProviderData *ProviderData
} }
func (tp *ValidateSessionStateTestProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { func (tp *ValidateSessionStateTestProvider) GetEmailAddress(s *SessionState) (string, error) {
return "", errors.New("not implemented") return "", errors.New("not implemented")
} }
// Note that we're testing the internal validateToken() used to implement // Note that we're testing the internal validateToken() used to implement
// several Provider's ValidateSessionState() implementations // several Provider's ValidateSessionState() implementations
func (tp *ValidateSessionStateTestProvider) ValidateSessionState(s *sessions.SessionState) bool { func (tp *ValidateSessionStateTestProvider) ValidateSessionState(s *SessionState) bool {
return false return false
} }
type ValidateSessionStateTest struct { type ValidateSessionStateTest struct {
backend *httptest.Server backend *httptest.Server
responseCode int response_code int
provider *ValidateSessionStateTestProvider provider *ValidateSessionStateTestProvider
header http.Header header http.Header
} }
func NewValidateSessionStateTest() *ValidateSessionStateTest { func NewValidateSessionStateTest() *ValidateSessionStateTest {
var vtTest ValidateSessionStateTest var vt_test ValidateSessionStateTest
vtTest.backend = httptest.NewServer( vt_test.backend = httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/oauth/tokeninfo" { if r.URL.Path != "/oauth/tokeninfo" {
w.WriteHeader(500) w.WriteHeader(500)
w.Write([]byte("unknown URL")) w.Write([]byte("unknown URL"))
} }
tokenParam := r.FormValue("access_token") token_param := r.FormValue("access_token")
if tokenParam == "" { if token_param == "" {
missing := false missing := false
receivedHeaders := r.Header received_headers := r.Header
for k := range vtTest.header { for k, _ := range vt_test.header {
received := receivedHeaders.Get(k) received := received_headers.Get(k)
expected := vtTest.header.Get(k) expected := vt_test.header.Get(k)
if received == "" || received != expected { if received == "" || received != expected {
missing = true missing = true
} }
@ -62,68 +56,68 @@ func NewValidateSessionStateTest() *ValidateSessionStateTest {
w.Write([]byte("no token param and missing or incorrect headers")) w.Write([]byte("no token param and missing or incorrect headers"))
} }
} }
w.WriteHeader(vtTest.responseCode) w.WriteHeader(vt_test.response_code)
w.Write([]byte("only code matters; contents disregarded")) w.Write([]byte("only code matters; contents disregarded"))
})) }))
backendURL, _ := url.Parse(vtTest.backend.URL) backend_url, _ := url.Parse(vt_test.backend.URL)
vtTest.provider = &ValidateSessionStateTestProvider{ vt_test.provider = &ValidateSessionStateTestProvider{
ProviderData: &ProviderData{ ProviderData: &ProviderData{
ValidateURL: &url.URL{ ValidateURL: &url.URL{
Scheme: "http", Scheme: "http",
Host: backendURL.Host, Host: backend_url.Host,
Path: "/oauth/tokeninfo", Path: "/oauth/tokeninfo",
}, },
}, },
} }
vtTest.responseCode = 200 vt_test.response_code = 200
return &vtTest return &vt_test
} }
func (vtTest *ValidateSessionStateTest) Close() { func (vt_test *ValidateSessionStateTest) Close() {
vtTest.backend.Close() vt_test.backend.Close()
} }
func TestValidateSessionStateValidToken(t *testing.T) { func TestValidateSessionStateValidToken(t *testing.T) {
vtTest := NewValidateSessionStateTest() vt_test := NewValidateSessionStateTest()
defer vtTest.Close() defer vt_test.Close()
assert.Equal(t, true, validateToken(vtTest.provider, "foobar", nil)) assert.Equal(t, true, validateToken(vt_test.provider, "foobar", nil))
} }
func TestValidateSessionStateValidTokenWithHeaders(t *testing.T) { func TestValidateSessionStateValidTokenWithHeaders(t *testing.T) {
vtTest := NewValidateSessionStateTest() vt_test := NewValidateSessionStateTest()
defer vtTest.Close() defer vt_test.Close()
vtTest.header = make(http.Header) vt_test.header = make(http.Header)
vtTest.header.Set("Authorization", "Bearer foobar") vt_test.header.Set("Authorization", "Bearer foobar")
assert.Equal(t, true, assert.Equal(t, true,
validateToken(vtTest.provider, "foobar", vtTest.header)) validateToken(vt_test.provider, "foobar", vt_test.header))
} }
func TestValidateSessionStateEmptyToken(t *testing.T) { func TestValidateSessionStateEmptyToken(t *testing.T) {
vtTest := NewValidateSessionStateTest() vt_test := NewValidateSessionStateTest()
defer vtTest.Close() defer vt_test.Close()
assert.Equal(t, false, validateToken(vtTest.provider, "", nil)) assert.Equal(t, false, validateToken(vt_test.provider, "", nil))
} }
func TestValidateSessionStateEmptyValidateURL(t *testing.T) { func TestValidateSessionStateEmptyValidateURL(t *testing.T) {
vtTest := NewValidateSessionStateTest() vt_test := NewValidateSessionStateTest()
defer vtTest.Close() defer vt_test.Close()
vtTest.provider.Data().ValidateURL = nil vt_test.provider.Data().ValidateURL = nil
assert.Equal(t, false, validateToken(vtTest.provider, "foobar", nil)) assert.Equal(t, false, validateToken(vt_test.provider, "foobar", nil))
} }
func TestValidateSessionStateRequestNetworkFailure(t *testing.T) { func TestValidateSessionStateRequestNetworkFailure(t *testing.T) {
vtTest := NewValidateSessionStateTest() vt_test := NewValidateSessionStateTest()
// Close immediately to simulate a network failure // Close immediately to simulate a network failure
vtTest.Close() vt_test.Close()
assert.Equal(t, false, validateToken(vtTest.provider, "foobar", nil)) assert.Equal(t, false, validateToken(vt_test.provider, "foobar", nil))
} }
func TestValidateSessionStateExpiredToken(t *testing.T) { func TestValidateSessionStateExpiredToken(t *testing.T) {
vtTest := NewValidateSessionStateTest() vt_test := NewValidateSessionStateTest()
defer vtTest.Close() defer vt_test.Close()
vtTest.responseCode = 401 vt_test.response_code = 401
assert.Equal(t, false, validateToken(vtTest.provider, "foobar", nil)) assert.Equal(t, false, validateToken(vt_test.provider, "foobar", nil))
} }
func TestStripTokenNotPresent(t *testing.T) { func TestStripTokenNotPresent(t *testing.T) {

View File

@ -1,86 +0,0 @@
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()
}

View File

@ -1,151 +0,0 @@
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,16 +6,13 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/bitly/oauth2_proxy/api"
"github.com/pusher/oauth2_proxy/pkg/requests"
) )
// LinkedInProvider represents an LinkedIn based Identity Provider
type LinkedInProvider struct { type LinkedInProvider struct {
*ProviderData *ProviderData
} }
// NewLinkedInProvider initiates a new LinkedInProvider
func NewLinkedInProvider(p *ProviderData) *LinkedInProvider { func NewLinkedInProvider(p *ProviderData) *LinkedInProvider {
p.ProviderName = "LinkedIn" p.ProviderName = "LinkedIn"
if p.LoginURL.String() == "" { if p.LoginURL.String() == "" {
@ -42,16 +39,15 @@ func NewLinkedInProvider(p *ProviderData) *LinkedInProvider {
return &LinkedInProvider{ProviderData: p} return &LinkedInProvider{ProviderData: p}
} }
func getLinkedInHeader(accessToken string) http.Header { func getLinkedInHeader(access_token string) http.Header {
header := make(http.Header) header := make(http.Header)
header.Set("Accept", "application/json") header.Set("Accept", "application/json")
header.Set("x-li-format", "json") header.Set("x-li-format", "json")
header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token))
return header return header
} }
// GetEmailAddress returns the Account email address func (p *LinkedInProvider) GetEmailAddress(s *SessionState) (string, error) {
func (p *LinkedInProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
if s.AccessToken == "" { if s.AccessToken == "" {
return "", errors.New("missing access token") return "", errors.New("missing access token")
} }
@ -61,7 +57,7 @@ func (p *LinkedInProvider) GetEmailAddress(s *sessions.SessionState) (string, er
} }
req.Header = getLinkedInHeader(s.AccessToken) req.Header = getLinkedInHeader(s.AccessToken)
json, err := requests.Request(req) json, err := api.Request(req)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -73,7 +69,6 @@ func (p *LinkedInProvider) GetEmailAddress(s *sessions.SessionState) (string, er
return email, nil return email, nil
} }
// ValidateSessionState validates the AccessToken func (p *LinkedInProvider) ValidateSessionState(s *SessionState) bool {
func (p *LinkedInProvider) ValidateSessionState(s *sessions.SessionState) bool {
return validateToken(p, s.AccessToken, getLinkedInHeader(s.AccessToken)) return validateToken(p, s.AccessToken, getLinkedInHeader(s.AccessToken))
} }

View File

@ -6,7 +6,6 @@ import (
"net/url" "net/url"
"testing" "testing"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -32,7 +31,8 @@ func testLinkedInBackend(payload string) *httptest.Server {
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 { url := r.URL
if url.Path != path {
w.WriteHeader(404) w.WriteHeader(404)
} else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" { } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
w.WriteHeader(403) w.WriteHeader(403)
@ -95,10 +95,10 @@ func TestLinkedInProviderGetEmailAddress(t *testing.T) {
b := testLinkedInBackend(`"user@linkedin.com"`) b := testLinkedInBackend(`"user@linkedin.com"`)
defer b.Close() defer b.Close()
bURL, _ := url.Parse(b.URL) b_url, _ := url.Parse(b.URL)
p := testLinkedInProvider(bURL.Host) p := testLinkedInProvider(b_url.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_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, "user@linkedin.com", email) assert.Equal(t, "user@linkedin.com", email)
@ -108,13 +108,13 @@ func TestLinkedInProviderGetEmailAddressFailedRequest(t *testing.T) {
b := testLinkedInBackend("unused payload") b := testLinkedInBackend("unused payload")
defer b.Close() defer b.Close()
bURL, _ := url.Parse(b.URL) b_url, _ := url.Parse(b.URL)
p := testLinkedInProvider(bURL.Host) p := testLinkedInProvider(b_url.Host)
// We'll trigger a request failure by using an unexpected access // We'll trigger a request failure by using an unexpected access
// token. Alternatively, we could allow the parsing of the payload as // token. Alternatively, we could allow the parsing of the payload as
// JSON to fail. // JSON to fail.
session := &sessions.SessionState{AccessToken: "unexpected_access_token"} session := &SessionState{AccessToken: "unexpected_access_token"}
email, err := p.GetEmailAddress(session) email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
assert.Equal(t, "", email) assert.Equal(t, "", email)
@ -124,10 +124,10 @@ func TestLinkedInProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
b := testLinkedInBackend("{\"foo\": \"bar\"}") b := testLinkedInBackend("{\"foo\": \"bar\"}")
defer b.Close() defer b.Close()
bURL, _ := url.Parse(b.URL) b_url, _ := url.Parse(b.URL)
p := testLinkedInProvider(bURL.Host) p := testLinkedInProvider(b_url.Host)
session := &sessions.SessionState{AccessToken: "imaginary_access_token"} session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session) email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
assert.Equal(t, "", email) assert.Equal(t, "", email)

View File

@ -1,277 +0,0 @@
package providers
import (
"bytes"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"gopkg.in/square/go-jose.v2"
)
// LoginGovProvider represents an OIDC based Identity Provider
type LoginGovProvider struct {
*ProviderData
// TODO (@timothy-spencer): Ideally, the nonce would be in the session state, but the session state
// is created only upon code redemption, not during the auth, when this must be supplied.
Nonce string
AcrValues string
JWTKey *rsa.PrivateKey
PubJWKURL *url.URL
}
// For generating a nonce
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randSeq(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
// NewLoginGovProvider initiates a new LoginGovProvider
func NewLoginGovProvider(p *ProviderData) *LoginGovProvider {
p.ProviderName = "login.gov"
if p.LoginURL == nil || p.LoginURL.String() == "" {
p.LoginURL = &url.URL{
Scheme: "https",
Host: "secure.login.gov",
Path: "/openid_connect/authorize",
}
}
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
p.RedeemURL = &url.URL{
Scheme: "https",
Host: "secure.login.gov",
Path: "/api/openid_connect/token",
}
}
if p.ProfileURL == nil || p.ProfileURL.String() == "" {
p.ProfileURL = &url.URL{
Scheme: "https",
Host: "secure.login.gov",
Path: "/api/openid_connect/userinfo",
}
}
if p.Scope == "" {
p.Scope = "email openid"
}
return &LoginGovProvider{
ProviderData: p,
Nonce: randSeq(32),
}
}
type loginGovCustomClaims struct {
Acr string `json:"acr"`
Nonce string `json:"nonce"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Birthdate string `json:"birthdate"`
AtHash string `json:"at_hash"`
CHash string `json:"c_hash"`
jwt.StandardClaims
}
// checkNonce checks the nonce in the id_token
func checkNonce(idToken string, p *LoginGovProvider) (err error) {
token, err := jwt.ParseWithClaims(idToken, &loginGovCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
resp, myerr := http.Get(p.PubJWKURL.String())
if myerr != nil {
return nil, myerr
}
if resp.StatusCode != 200 {
myerr = fmt.Errorf("got %d from %q", resp.StatusCode, p.PubJWKURL.String())
return nil, myerr
}
body, myerr := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if myerr != nil {
return nil, myerr
}
var pubkeys jose.JSONWebKeySet
myerr = json.Unmarshal(body, &pubkeys)
if myerr != nil {
return nil, myerr
}
pubkey := pubkeys.Keys[0]
return pubkey.Key, nil
})
if err != nil {
return
}
claims := token.Claims.(*loginGovCustomClaims)
if claims.Nonce != p.Nonce {
err = fmt.Errorf("nonce validation failed")
return
}
return
}
func emailFromUserInfo(accessToken string, userInfoEndpoint string) (email string, err error) {
// query the user info endpoint for user attributes
var req *http.Request
req, err = http.NewRequest("GET", userInfoEndpoint, nil)
if err != nil {
return
}
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
var body []byte
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return
}
if resp.StatusCode != 200 {
err = fmt.Errorf("got %d from %q %s", resp.StatusCode, userInfoEndpoint, body)
return
}
// parse the user attributes from the data we got and make sure that
// the email address has been validated.
var emailData struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
err = json.Unmarshal(body, &emailData)
if err != nil {
return
}
if emailData.Email == "" {
err = fmt.Errorf("missing email")
return
}
email = emailData.Email
if !emailData.EmailVerified {
err = fmt.Errorf("email %s not listed as verified", email)
return
}
return
}
// Redeem exchanges the OAuth2 authentication token for an ID token
func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
if code == "" {
err = errors.New("missing code")
return
}
claims := &jwt.StandardClaims{
Issuer: p.ClientID,
Subject: p.ClientID,
Audience: p.RedeemURL.String(),
ExpiresAt: int64(time.Now().Add(time.Duration(5 * time.Minute)).Unix()),
Id: randSeq(32),
}
token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims)
ss, err := token.SignedString(p.JWTKey)
if err != nil {
return
}
params := url.Values{}
params.Add("client_assertion", ss)
params.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
params.Add("code", code)
params.Add("grant_type", "authorization_code")
var req *http.Request
req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
var resp *http.Response
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
var body []byte
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return
}
if resp.StatusCode != 200 {
err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
return
}
// Get the token from the body that we got from the token endpoint.
var jsonResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
err = json.Unmarshal(body, &jsonResponse)
if err != nil {
return
}
// check nonce here
err = checkNonce(jsonResponse.IDToken, p)
if err != nil {
return
}
// Get the email address
var email string
email, err = emailFromUserInfo(jsonResponse.AccessToken, p.ProfileURL.String())
if err != nil {
return
}
// Store the data that we found in the session state
s = &sessions.SessionState{
AccessToken: jsonResponse.AccessToken,
IDToken: jsonResponse.IDToken,
CreatedAt: time.Now(),
ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
Email: email,
}
return
}
// GetLoginURL overrides GetLoginURL to add login.gov parameters
func (p *LoginGovProvider) GetLoginURL(redirectURI, state string) string {
var a url.URL
a = *p.LoginURL
params, _ := url.ParseQuery(a.RawQuery)
params.Set("redirect_uri", redirectURI)
params.Set("approval_prompt", p.ApprovalPrompt)
params.Add("scope", p.Scope)
params.Set("client_id", p.ClientID)
params.Set("response_type", "code")
params.Add("state", state)
params.Add("acr_values", p.AcrValues)
params.Add("nonce", p.Nonce)
a.RawQuery = params.Encode()
return a.String()
}

View File

@ -1,290 +0,0 @@
package providers
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
"gopkg.in/square/go-jose.v2"
)
type MyKeyData struct {
PubKey crypto.PublicKey
PrivKey *rsa.PrivateKey
PubJWK jose.JSONWebKey
}
func newLoginGovServer(body []byte) (*url.URL, *httptest.Server) {
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write(body)
}))
u, _ := url.Parse(s.URL)
return u, s
}
func newLoginGovProvider() (l *LoginGovProvider, serverKey *MyKeyData, err error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return
}
serverKey = &MyKeyData{
PubKey: key.Public(),
PrivKey: key,
PubJWK: jose.JSONWebKey{
Key: key.Public(),
KeyID: "testkey",
Algorithm: string(jose.RS256),
Use: "sig",
},
}
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return
}
l = NewLoginGovProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: ""})
l.JWTKey = privateKey
l.Nonce = "fakenonce"
return
}
func TestLoginGovProviderDefaults(t *testing.T) {
p, _, err := newLoginGovProvider()
assert.NotEqual(t, nil, p)
assert.NoError(t, err)
assert.Equal(t, "login.gov", p.Data().ProviderName)
assert.Equal(t, "https://secure.login.gov/openid_connect/authorize",
p.Data().LoginURL.String())
assert.Equal(t, "https://secure.login.gov/api/openid_connect/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://secure.login.gov/api/openid_connect/userinfo",
p.Data().ProfileURL.String())
assert.Equal(t, "email openid", p.Data().Scope)
}
func TestLoginGovProviderOverrides(t *testing.T) {
p := NewLoginGovProvider(
&ProviderData{
LoginURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/auth"},
RedeemURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/token"},
ProfileURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/profile"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "login.gov", 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/oauth/profile",
p.Data().ProfileURL.String())
assert.Equal(t, "profile", p.Data().Scope)
}
func TestLoginGovProviderSessionData(t *testing.T) {
p, serverkey, err := newLoginGovProvider()
assert.NotEqual(t, nil, p)
assert.NoError(t, err)
// Set up the redeem endpoint here
type loginGovRedeemResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
IDToken string `json:"id_token"`
}
expiresIn := int64(60)
type MyCustomClaims struct {
Acr string `json:"acr"`
Nonce string `json:"nonce"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Birthdate string `json:"birthdate"`
AtHash string `json:"at_hash"`
CHash string `json:"c_hash"`
jwt.StandardClaims
}
claims := MyCustomClaims{
"http://idmanagement.gov/ns/assurance/loa/1",
"fakenonce",
"timothy.spencer@gsa.gov",
true,
"",
"",
"",
"",
"",
jwt.StandardClaims{
Audience: "Audience",
ExpiresAt: time.Now().Unix() + expiresIn,
Id: "foo",
IssuedAt: time.Now().Unix(),
Issuer: "https://idp.int.login.gov",
NotBefore: time.Now().Unix() - 1,
Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
},
}
idtoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signedidtoken, err := idtoken.SignedString(serverkey.PrivKey)
assert.NoError(t, err)
body, err := json.Marshal(loginGovRedeemResponse{
AccessToken: "a1234",
TokenType: "Bearer",
ExpiresIn: expiresIn,
IDToken: signedidtoken,
})
assert.NoError(t, err)
var server *httptest.Server
p.RedeemURL, server = newLoginGovServer(body)
defer server.Close()
// Set up the user endpoint here
type loginGovUserResponse struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Subject string `json:"sub"`
}
userbody, err := json.Marshal(loginGovUserResponse{
Email: "timothy.spencer@gsa.gov",
EmailVerified: true,
Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
})
assert.NoError(t, err)
var userserver *httptest.Server
p.ProfileURL, userserver = newLoginGovServer(userbody)
defer userserver.Close()
// Set up the PubJWKURL endpoint here used to verify the JWT
var pubkeys jose.JSONWebKeySet
pubkeys.Keys = append(pubkeys.Keys, serverkey.PubJWK)
pubjwkbody, err := json.Marshal(pubkeys)
assert.NoError(t, err)
var pubjwkserver *httptest.Server
p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody)
defer pubjwkserver.Close()
session, err := p.Redeem("http://redirect/", "code1234")
assert.NoError(t, err)
assert.NotEqual(t, session, nil)
assert.Equal(t, "timothy.spencer@gsa.gov", session.Email)
assert.Equal(t, "a1234", session.AccessToken)
// The test ought to run in under 2 seconds. If not, you may need to bump this up.
assert.InDelta(t, session.ExpiresOn.Unix(), time.Now().Unix()+expiresIn, 2)
}
func TestLoginGovProviderBadNonce(t *testing.T) {
p, serverkey, err := newLoginGovProvider()
assert.NotEqual(t, nil, p)
assert.NoError(t, err)
// Set up the redeem endpoint here
type loginGovRedeemResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
IDToken string `json:"id_token"`
}
expiresIn := int64(60)
type MyCustomClaims struct {
Acr string `json:"acr"`
Nonce string `json:"nonce"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Birthdate string `json:"birthdate"`
AtHash string `json:"at_hash"`
CHash string `json:"c_hash"`
jwt.StandardClaims
}
claims := MyCustomClaims{
"http://idmanagement.gov/ns/assurance/loa/1",
"badfakenonce",
"timothy.spencer@gsa.gov",
true,
"",
"",
"",
"",
"",
jwt.StandardClaims{
Audience: "Audience",
ExpiresAt: time.Now().Unix() + expiresIn,
Id: "foo",
IssuedAt: time.Now().Unix(),
Issuer: "https://idp.int.login.gov",
NotBefore: time.Now().Unix() - 1,
Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
},
}
idtoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signedidtoken, err := idtoken.SignedString(serverkey.PrivKey)
assert.NoError(t, err)
body, err := json.Marshal(loginGovRedeemResponse{
AccessToken: "a1234",
TokenType: "Bearer",
ExpiresIn: expiresIn,
IDToken: signedidtoken,
})
assert.NoError(t, err)
var server *httptest.Server
p.RedeemURL, server = newLoginGovServer(body)
defer server.Close()
// Set up the user endpoint here
type loginGovUserResponse struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Subject string `json:"sub"`
}
userbody, err := json.Marshal(loginGovUserResponse{
Email: "timothy.spencer@gsa.gov",
EmailVerified: true,
Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
})
assert.NoError(t, err)
var userserver *httptest.Server
p.ProfileURL, userserver = newLoginGovServer(userbody)
defer userserver.Close()
// Set up the PubJWKURL endpoint here used to verify the JWT
var pubkeys jose.JSONWebKeySet
pubkeys.Keys = append(pubkeys.Keys, serverkey.PubJWK)
pubjwkbody, err := json.Marshal(pubkeys)
assert.NoError(t, err)
var pubjwkserver *httptest.Server
p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody)
defer pubjwkserver.Close()
_, err = p.Redeem("http://redirect/", "code1234")
// The "badfakenonce" in the idtoken above should cause this to error out
assert.Error(t, err)
}

View File

@ -3,32 +3,25 @@ package providers
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"time" "time"
oidc "github.com/coreos/go-oidc"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/requests"
"golang.org/x/oauth2" "golang.org/x/oauth2"
oidc "github.com/coreos/go-oidc"
) )
// OIDCProvider represents an OIDC based Identity Provider
type OIDCProvider struct { type OIDCProvider struct {
*ProviderData *ProviderData
Verifier *oidc.IDTokenVerifier Verifier *oidc.IDTokenVerifier
AllowUnverifiedEmail bool
} }
// NewOIDCProvider initiates a new OIDCProvider
func NewOIDCProvider(p *ProviderData) *OIDCProvider { func NewOIDCProvider(p *ProviderData) *OIDCProvider {
p.ProviderName = "OpenID Connect" p.ProviderName = "OpenID Connect"
return &OIDCProvider{ProviderData: p} return &OIDCProvider{ProviderData: p}
} }
// Redeem exchanges the OAuth2 authentication token for an ID token func (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
func (p *OIDCProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
ctx := context.Background() ctx := context.Background()
c := oauth2.Config{ c := oauth2.Config{
ClientID: p.ClientID, ClientID: p.ClientID,
@ -42,16 +35,14 @@ func (p *OIDCProvider) Redeem(redirectURL, code string) (s *sessions.SessionStat
if err != nil { if err != nil {
return nil, fmt.Errorf("token exchange: %v", err) return nil, fmt.Errorf("token exchange: %v", err)
} }
s, err = p.createSessionState(ctx, token) s, err = p.createSessionState(token, ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to update session: %v", err) return nil, fmt.Errorf("unable to update session: %v", err)
} }
return return
} }
// RefreshSessionIfNeeded checks if the session has expired and uses the func (p *OIDCProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
// RefreshToken to fetch a new ID token if required
func (p *OIDCProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" { if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
return false, nil return false, nil
} }
@ -67,7 +58,7 @@ func (p *OIDCProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, e
return true, nil return true, nil
} }
func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error) { func (p *OIDCProvider) redeemRefreshToken(s *SessionState) (err error) {
c := oauth2.Config{ c := oauth2.Config{
ClientID: p.ClientID, ClientID: p.ClientID,
ClientSecret: p.ClientSecret, ClientSecret: p.ClientSecret,
@ -84,20 +75,19 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error)
if err != nil { if err != nil {
return fmt.Errorf("failed to get token: %v", err) return fmt.Errorf("failed to get token: %v", err)
} }
newSession, err := p.createSessionState(ctx, token) newSession, err := p.createSessionState(token, ctx)
if err != nil { if err != nil {
return fmt.Errorf("unable to update session: %v", err) return fmt.Errorf("unable to update session: %v", err)
} }
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
} }
func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) { func (p *OIDCProvider) createSessionState(token *oauth2.Token, ctx context.Context) (*SessionState, error) {
rawIDToken, ok := token.Extra("id_token").(string) rawIDToken, ok := token.Extra("id_token").(string)
if !ok { if !ok {
return nil, fmt.Errorf("token response did not contain an id_token") return nil, fmt.Errorf("token response did not contain an id_token")
@ -111,7 +101,6 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
// Extract custom claims. // Extract custom claims.
var claims struct { var claims struct {
Subject string `json:"sub"`
Email string `json:"email"` Email string `json:"email"`
Verified *bool `json:"email_verified"` Verified *bool `json:"email_verified"`
} }
@ -120,61 +109,17 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
} }
if claims.Email == "" { if claims.Email == "" {
if p.ProfileURL.String() == "" {
return nil, fmt.Errorf("id_token did not contain an email") 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)
} }
return &sessions.SessionState{ return &SessionState{
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
IDToken: rawIDToken, IdToken: rawIDToken,
RefreshToken: token.RefreshToken, RefreshToken: token.RefreshToken,
CreatedAt: time.Now(), ExpiresOn: token.Expiry,
ExpiresOn: idToken.Expiry,
Email: claims.Email, Email: claims.Email,
User: claims.Subject,
}, nil }, nil
} }
// ValidateSessionState checks that the session's IDToken is still valid
func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool {
ctx := context.Background()
_, err := p.Verifier.Verify(ctx, s.IDToken)
if err != nil {
return false
}
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

@ -4,8 +4,6 @@ import (
"net/url" "net/url"
) )
// ProviderData contains information required to configure all implementations
// of OAuth2 providers
type ProviderData struct { type ProviderData struct {
ProviderName string ProviderName string
ClientID string ClientID string
@ -19,5 +17,4 @@ type ProviderData struct {
ApprovalPrompt string ApprovalPrompt string
} }
// Data returns the ProviderData
func (p *ProviderData) Data() *ProviderData { return p } func (p *ProviderData) Data() *ProviderData { return p }

View File

@ -8,14 +8,11 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"time"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/bitly/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/encryption"
) )
// Redeem provides a default implementation of the OAuth2 token redemption process func (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err error) {
func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
if code == "" { if code == "" {
err = errors.New("missing code") err = errors.New("missing code")
return return
@ -61,7 +58,7 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionStat
} }
err = json.Unmarshal(body, &jsonResponse) err = json.Unmarshal(body, &jsonResponse)
if err == nil { if err == nil {
s = &sessions.SessionState{ s = &SessionState{
AccessToken: jsonResponse.AccessToken, AccessToken: jsonResponse.AccessToken,
} }
return return
@ -73,7 +70,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, CreatedAt: time.Now()} s = &SessionState{AccessToken: a}
} else { } else {
err = fmt.Errorf("no access token found %s", body) err = fmt.Errorf("no access token found %s", body)
} }
@ -96,22 +93,21 @@ 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 *encryption.Cipher) (string, error) { func (p *ProviderData) CookieForSession(s *SessionState, c *cookie.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 *encryption.Cipher) (s *sessions.SessionState, err error) { func (p *ProviderData) SessionFromCookie(v string, c *cookie.Cipher) (s *SessionState, err error) {
return sessions.DecodeSessionState(v, c) return DecodeSessionState(v, c)
} }
// GetEmailAddress returns the Account email address func (p *ProviderData) GetEmailAddress(s *SessionState) (string, error) {
func (p *ProviderData) GetEmailAddress(s *sessions.SessionState) (string, error) {
return "", errors.New("not implemented") return "", errors.New("not implemented")
} }
// GetUserName returns the Account username // GetUserName returns the Account username
func (p *ProviderData) GetUserName(s *sessions.SessionState) (string, error) { func (p *ProviderData) GetUserName(s *SessionState) (string, error) {
return "", errors.New("not implemented") return "", errors.New("not implemented")
} }
@ -121,13 +117,11 @@ func (p *ProviderData) ValidateGroup(email string) bool {
return true return true
} }
// ValidateSessionState validates the AccessToken func (p *ProviderData) ValidateSessionState(s *SessionState) bool {
func (p *ProviderData) ValidateSessionState(s *sessions.SessionState) bool {
return validateToken(p, s.AccessToken, nil) return validateToken(p, s.AccessToken, nil)
} }
// RefreshSessionIfNeeded should refresh the user's session if required and // RefreshSessionIfNeeded
// do nothing if a refresh is not required func (p *ProviderData) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
func (p *ProviderData) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
return false, nil return false, nil
} }

View File

@ -4,13 +4,12 @@ import (
"testing" "testing"
"time" "time"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestRefresh(t *testing.T) { func TestRefresh(t *testing.T) {
p := &ProviderData{} p := &ProviderData{}
refreshed, err := p.RefreshSessionIfNeeded(&sessions.SessionState{ refreshed, err := p.RefreshSessionIfNeeded(&SessionState{
ExpiresOn: time.Now().Add(time.Duration(-11) * time.Minute), ExpiresOn: time.Now().Add(time.Duration(-11) * time.Minute),
}) })
assert.Equal(t, false, refreshed) assert.Equal(t, false, refreshed)

View File

@ -1,25 +1,22 @@
package providers package providers
import ( import (
"github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/bitly/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/encryption"
) )
// Provider represents an upstream identity provider implementation
type Provider interface { type Provider interface {
Data() *ProviderData Data() *ProviderData
GetEmailAddress(*sessions.SessionState) (string, error) GetEmailAddress(*SessionState) (string, error)
GetUserName(*sessions.SessionState) (string, error) GetUserName(*SessionState) (string, error)
Redeem(string, string) (*sessions.SessionState, error) Redeem(string, string) (*SessionState, error)
ValidateGroup(string) bool ValidateGroup(string) bool
ValidateSessionState(*sessions.SessionState) bool ValidateSessionState(*SessionState) bool
GetLoginURL(redirectURI, finalRedirect string) string GetLoginURL(redirectURI, finalRedirect string) string
RefreshSessionIfNeeded(*sessions.SessionState) (bool, error) RefreshSessionIfNeeded(*SessionState) (bool, error)
SessionFromCookie(string, *encryption.Cipher) (*sessions.SessionState, error) SessionFromCookie(string, *cookie.Cipher) (*SessionState, error)
CookieForSession(*sessions.SessionState, *encryption.Cipher) (string, error) CookieForSession(*SessionState, *cookie.Cipher) (string, error)
} }
// New provides a new Provider based on the configured provider string
func New(provider string, p *ProviderData) Provider { func New(provider string, p *ProviderData) Provider {
switch provider { switch provider {
case "linkedin": case "linkedin":
@ -28,18 +25,12 @@ 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":
return NewGitLabProvider(p) return NewGitLabProvider(p)
case "oidc": case "oidc":
return NewOIDCProvider(p) return NewOIDCProvider(p)
case "login.gov":
return NewLoginGovProvider(p)
case "bitbucket":
return NewBitbucketProvider(p)
default: default:
return NewGoogleProvider(p) return NewGoogleProvider(p)
} }

136
providers/session_state.go Normal file
View File

@ -0,0 +1,136 @@
package providers
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/bitly/oauth2_proxy/cookie"
)
type SessionState struct {
AccessToken string
IdToken string
ExpiresOn time.Time
RefreshToken string
Email string
User string
}
func (s *SessionState) IsExpired() bool {
if !s.ExpiresOn.IsZero() && s.ExpiresOn.Before(time.Now()) {
return true
}
return false
}
func (s *SessionState) String() string {
o := fmt.Sprintf("Session{%s", s.accountInfo())
if s.AccessToken != "" {
o += " token:true"
}
if s.IdToken != "" {
o += " id_token:true"
}
if !s.ExpiresOn.IsZero() {
o += fmt.Sprintf(" expires:%s", s.ExpiresOn)
}
if s.RefreshToken != "" {
o += " refresh_token:true"
}
return o + "}"
}
func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {
if c == nil || s.AccessToken == "" {
return s.accountInfo(), nil
}
return s.EncryptedString(c)
}
func (s *SessionState) accountInfo() string {
return fmt.Sprintf("email:%s user:%s", s.Email, s.User)
}
func (s *SessionState) EncryptedString(c *cookie.Cipher) (string, error) {
var err error
if c == nil {
panic("error. missing cipher")
}
a := s.AccessToken
if a != "" {
if a, err = c.Encrypt(a); err != nil {
return "", err
}
}
i := s.IdToken
if i != "" {
fmt.Printf("Encrytping ID Token")
if i, err = c.Encrypt(i); err != nil {
return "", err
}
}
r := s.RefreshToken
if r != "" {
if r, err = c.Encrypt(r); err != nil {
return "", err
}
}
return fmt.Sprintf("%s|%s|%s|%d|%s", s.accountInfo(), a, i, s.ExpiresOn.Unix(), r), nil
}
func decodeSessionStatePlain(v string) (s *SessionState, err error) {
chunks := strings.Split(v, " ")
if len(chunks) != 2 {
return nil, fmt.Errorf("could not decode session state: expected 2 chunks got %d", len(chunks))
}
email := strings.TrimPrefix(chunks[0], "email:")
user := strings.TrimPrefix(chunks[1], "user:")
if user == "" {
user = strings.Split(email, "@")[0]
}
return &SessionState{User: user, Email: email}, nil
}
func DecodeSessionState(v string, c *cookie.Cipher) (s *SessionState, err error) {
if c == nil {
return decodeSessionStatePlain(v)
}
chunks := strings.Split(v, "|")
if len(chunks) != 5 {
err = fmt.Errorf("invalid number of fields (got %d expected 5)", len(chunks))
return
}
sessionState, err := decodeSessionStatePlain(chunks[0])
if err != nil {
return nil, err
}
if chunks[1] != "" {
if sessionState.AccessToken, err = c.Decrypt(chunks[1]); err != nil {
return nil, err
}
}
if chunks[2] != "" {
if sessionState.IdToken, err = c.Decrypt(chunks[2]); err != nil {
return nil, err
}
}
ts, _ := strconv.Atoi(chunks[3])
sessionState.ExpiresOn = time.Unix(int64(ts), 0)
if chunks[4] != "" {
if sessionState.RefreshToken, err = c.Decrypt(chunks[4]); err != nil {
return nil, err
}
}
return sessionState, nil
}

View File

@ -0,0 +1,155 @@
package providers
import (
"fmt"
"strings"
"testing"
"time"
"github.com/bitly/oauth2_proxy/cookie"
"github.com/stretchr/testify/assert"
)
const secret = "0123456789abcdefghijklmnopqrstuv"
const altSecret = "0000000000abcdefghijklmnopqrstuv"
func TestSessionStateSerialization(t *testing.T) {
c, err := cookie.NewCipher([]byte(secret))
assert.Equal(t, nil, err)
c2, err := cookie.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err)
s := &SessionState{
Email: "user@domain.com",
AccessToken: "token1234",
IdToken: "rawtoken1234",
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(c)
assert.Equal(t, nil, err)
assert.Equal(t, 4, strings.Count(encoded, "|"))
ss, err := DecodeSessionState(encoded, c)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, "user", ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.AccessToken, ss.AccessToken)
assert.Equal(t, s.IdToken, ss.IdToken)
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.Equal(t, s.RefreshToken, ss.RefreshToken)
// ensure a different cipher can't decode properly (ie: it gets gibberish)
ss, err = DecodeSessionState(encoded, c2)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, "user", ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.NotEqual(t, s.AccessToken, ss.AccessToken)
assert.NotEqual(t, s.IdToken, ss.IdToken)
assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
}
func TestSessionStateSerializationWithUser(t *testing.T) {
c, err := cookie.NewCipher([]byte(secret))
assert.Equal(t, nil, err)
c2, err := cookie.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err)
s := &SessionState{
User: "just-user",
Email: "user@domain.com",
AccessToken: "token1234",
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(c)
assert.Equal(t, nil, err)
assert.Equal(t, 4, strings.Count(encoded, "|"))
ss, err := DecodeSessionState(encoded, c)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.AccessToken, ss.AccessToken)
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.Equal(t, s.RefreshToken, ss.RefreshToken)
// ensure a different cipher can't decode properly (ie: it gets gibberish)
ss, err = DecodeSessionState(encoded, c2)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.NotEqual(t, s.AccessToken, ss.AccessToken)
assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
}
func TestSessionStateSerializationNoCipher(t *testing.T) {
s := &SessionState{
Email: "user@domain.com",
AccessToken: "token1234",
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(nil)
assert.Equal(t, nil, err)
expected := fmt.Sprintf("email:%s user:", s.Email)
assert.Equal(t, expected, encoded)
// only email should have been serialized
ss, err := DecodeSessionState(encoded, nil)
assert.Equal(t, nil, err)
assert.Equal(t, "user", ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, "", ss.AccessToken)
assert.Equal(t, "", ss.RefreshToken)
}
func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
s := &SessionState{
User: "just-user",
Email: "user@domain.com",
AccessToken: "token1234",
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(nil)
assert.Equal(t, nil, err)
expected := fmt.Sprintf("email:%s user:%s", s.Email, s.User)
assert.Equal(t, expected, encoded)
// only email should have been serialized
ss, err := DecodeSessionState(encoded, nil)
assert.Equal(t, nil, err)
assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, "", ss.AccessToken)
assert.Equal(t, "", ss.RefreshToken)
}
func TestSessionStateAccountInfo(t *testing.T) {
s := &SessionState{
Email: "user@domain.com",
User: "just-user",
}
expected := fmt.Sprintf("email:%v user:%v", s.Email, s.User)
assert.Equal(t, expected, s.accountInfo())
s.Email = ""
expected = fmt.Sprintf("email:%v user:%v", s.Email, s.User)
assert.Equal(t, expected, s.accountInfo())
}
func TestExpired(t *testing.T) {
s := &SessionState{ExpiresOn: time.Now().Add(time.Duration(-1) * time.Minute)}
assert.Equal(t, true, s.IsExpired())
s = &SessionState{ExpiresOn: time.Now().Add(time.Duration(1) * time.Minute)}
assert.Equal(t, false, s.IsExpired())
s = &SessionState{}
assert.Equal(t, false, s.IsExpired())
}

View File

@ -4,21 +4,13 @@ import (
"strings" "strings"
) )
// StringArray is a type alias for a slice of strings
type StringArray []string type StringArray []string
// Get returns the slice of strings
func (a *StringArray) Get() interface{} {
return []string(*a)
}
// Set appends a string to the StringArray
func (a *StringArray) Set(s string) error { func (a *StringArray) Set(s string) error {
*a = append(*a, s) *a = append(*a, s)
return nil return nil
} }
// String joins elements of the StringArray into a single comma separated string
func (a *StringArray) String() string { func (a *StringArray) String() string {
return strings.Join(*a, ",") return strings.Join(*a, ",")
} }

View File

@ -2,19 +2,18 @@ package main
import ( import (
"html/template" "html/template"
"log"
"path" "path"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
func loadTemplates(dir string) *template.Template { func loadTemplates(dir string) *template.Template {
if dir == "" { if dir == "" {
return getTemplates() return getTemplates()
} }
logger.Printf("using custom template directory %q", dir) log.Printf("using custom template directory %q", dir)
t, err := template.New("").ParseFiles(path.Join(dir, "sign_in.html"), path.Join(dir, "error.html")) t, err := template.New("").ParseFiles(path.Join(dir, "sign_in.html"), path.Join(dir, "error.html"))
if err != nil { if err != nil {
logger.Fatalf("failed parsing template %s", err) log.Fatalf("failed parsing template %s", err)
} }
return t return t
} }
@ -143,7 +142,7 @@ func getTemplates() *template.Template {
<footer> <footer>
{{ if eq .Footer "-" }} {{ if eq .Footer "-" }}
{{ else if eq .Footer ""}} {{ else if eq .Footer ""}}
Secured with <a href="https://github.com/pusher/oauth2_proxy#oauth2_proxy">OAuth2 Proxy</a> version {{.Version}} Secured with <a href="https://github.com/bitly/oauth2_proxy#oauth2_proxy">OAuth2 Proxy</a> version {{.Version}}
{{ else }} {{ else }}
{{.Footer}} {{.Footer}}
{{ end }} {{ end }}
@ -152,7 +151,7 @@ func getTemplates() *template.Template {
</html> </html>
{{end}}`) {{end}}`)
if err != nil { if err != nil {
logger.Fatalf("failed parsing template %s", err) log.Fatalf("failed parsing template %s", err)
} }
t, err = t.Parse(`{{define "error.html"}} t, err = t.Parse(`{{define "error.html"}}
@ -170,7 +169,7 @@ func getTemplates() *template.Template {
</body> </body>
</html>{{end}}`) </html>{{end}}`)
if err != nil { if err != nil {
logger.Fatalf("failed parsing template %s", err) log.Fatalf("failed parsing template %s", err)
} }
return t return t
} }

14
test.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
EXIT_CODE=0
echo "gofmt"
diff -u <(echo -n) <(gofmt -d $(find . -type f -name '*.go' -not -path "./vendor/*")) || EXIT_CODE=1
for pkg in $(go list ./... | grep -v '/vendor/' ); do
echo "testing $pkg"
echo "go vet $pkg"
go vet "$pkg" || EXIT_CODE=1
echo "go test -v $pkg"
go test -v -timeout 90s "$pkg" || EXIT_CODE=1
echo "go test -v -race $pkg"
GOMAXPROCS=4 go test -v -timeout 90s0s -race "$pkg" || EXIT_CODE=1
done
exit $EXIT_CODE

View File

@ -3,27 +3,24 @@ package main
import ( import (
"encoding/csv" "encoding/csv"
"fmt" "fmt"
"log"
"os" "os"
"strings" "strings"
"sync/atomic" "sync/atomic"
"unsafe" "unsafe"
"github.com/pusher/oauth2_proxy/pkg/logger"
) )
// UserMap holds information from the authenticated emails file
type UserMap struct { type UserMap struct {
usersFile string usersFile string
m unsafe.Pointer m unsafe.Pointer
} }
// NewUserMap parses the authenticated emails file into a new UserMap
func NewUserMap(usersFile string, done <-chan bool, onUpdate func()) *UserMap { func NewUserMap(usersFile string, done <-chan bool, onUpdate func()) *UserMap {
um := &UserMap{usersFile: usersFile} um := &UserMap{usersFile: usersFile}
m := make(map[string]bool) m := make(map[string]bool)
atomic.StorePointer(&um.m, unsafe.Pointer(&m)) atomic.StorePointer(&um.m, unsafe.Pointer(&m))
if usersFile != "" { if usersFile != "" {
logger.Printf("using authenticated emails file %s", usersFile) log.Printf("using authenticated emails file %s", usersFile)
WatchForUpdates(usersFile, done, func() { WatchForUpdates(usersFile, done, func() {
um.LoadAuthenticatedEmailsFile() um.LoadAuthenticatedEmailsFile()
onUpdate() onUpdate()
@ -33,28 +30,25 @@ func NewUserMap(usersFile string, done <-chan bool, onUpdate func()) *UserMap {
return um return um
} }
// IsValid checks if an email is allowed
func (um *UserMap) IsValid(email string) (result bool) { func (um *UserMap) IsValid(email string) (result bool) {
m := *(*map[string]bool)(atomic.LoadPointer(&um.m)) m := *(*map[string]bool)(atomic.LoadPointer(&um.m))
_, result = m[email] _, result = m[email]
return return
} }
// LoadAuthenticatedEmailsFile loads the authenticated emails file from disk
// and parses the contents as CSV
func (um *UserMap) LoadAuthenticatedEmailsFile() { func (um *UserMap) LoadAuthenticatedEmailsFile() {
r, err := os.Open(um.usersFile) r, err := os.Open(um.usersFile)
if err != nil { if err != nil {
logger.Fatalf("failed opening authenticated-emails-file=%q, %s", um.usersFile, err) log.Fatalf("failed opening authenticated-emails-file=%q, %s", um.usersFile, err)
} }
defer r.Close() defer r.Close()
csvReader := csv.NewReader(r) csv_reader := csv.NewReader(r)
csvReader.Comma = ',' csv_reader.Comma = ','
csvReader.Comment = '#' csv_reader.Comment = '#'
csvReader.TrimLeadingSpace = true csv_reader.TrimLeadingSpace = true
records, err := csvReader.ReadAll() records, err := csv_reader.ReadAll()
if err != nil { if err != nil {
logger.Printf("error reading authenticated-emails-file=%q, %s", um.usersFile, err) log.Printf("error reading authenticated-emails-file=%q, %s", um.usersFile, err)
return return
} }
updated := make(map[string]bool) updated := make(map[string]bool)
@ -97,7 +91,6 @@ func newValidatorImpl(domains []string, usersFile string,
return validator return validator
} }
// NewValidator constructs a function to validate email addresses
func NewValidator(domains []string, usersFile string) func(string) bool { func NewValidator(domains []string, usersFile string) func(string) bool {
return newValidatorImpl(domains, usersFile, nil, func() {}) return newValidatorImpl(domains, usersFile, nil, func() {})
} }

View File

@ -8,15 +8,15 @@ import (
) )
type ValidatorTest struct { type ValidatorTest struct {
authEmailFile *os.File auth_email_file *os.File
done chan bool done chan bool
updateSeen bool update_seen bool
} }
func NewValidatorTest(t *testing.T) *ValidatorTest { func NewValidatorTest(t *testing.T) *ValidatorTest {
vt := &ValidatorTest{} vt := &ValidatorTest{}
var err error var err error
vt.authEmailFile, err = ioutil.TempFile("", "test_auth_emails_") vt.auth_email_file, err = ioutil.TempFile("", "test_auth_emails_")
if err != nil { if err != nil {
t.Fatal("failed to create temp file: " + err.Error()) t.Fatal("failed to create temp file: " + err.Error())
} }
@ -26,27 +26,27 @@ func NewValidatorTest(t *testing.T) *ValidatorTest {
func (vt *ValidatorTest) TearDown() { func (vt *ValidatorTest) TearDown() {
vt.done <- true vt.done <- true
os.Remove(vt.authEmailFile.Name()) os.Remove(vt.auth_email_file.Name())
} }
func (vt *ValidatorTest) NewValidator(domains []string, func (vt *ValidatorTest) NewValidator(domains []string,
updated chan<- bool) func(string) bool { updated chan<- bool) func(string) bool {
return newValidatorImpl(domains, vt.authEmailFile.Name(), return newValidatorImpl(domains, vt.auth_email_file.Name(),
vt.done, func() { vt.done, func() {
if vt.updateSeen == false { if vt.update_seen == false {
updated <- true updated <- true
vt.updateSeen = true vt.update_seen = true
} }
}) })
} }
// This will close vt.authEmailFile. // This will close vt.auth_email_file.
func (vt *ValidatorTest) WriteEmails(t *testing.T, emails []string) { func (vt *ValidatorTest) WriteEmails(t *testing.T, emails []string) {
defer vt.authEmailFile.Close() defer vt.auth_email_file.Close()
vt.authEmailFile.WriteString(strings.Join(emails, "\n")) vt.auth_email_file.WriteString(strings.Join(emails, "\n"))
if err := vt.authEmailFile.Close(); err != nil { if err := vt.auth_email_file.Close(); err != nil {
t.Fatal("failed to close temp file " + t.Fatal("failed to close temp file " +
vt.authEmailFile.Name() + ": " + err.Error()) vt.auth_email_file.Name() + ": " + err.Error())
} }
} }

View File

@ -12,18 +12,18 @@ import (
func (vt *ValidatorTest) UpdateEmailFileViaCopyingOver( func (vt *ValidatorTest) UpdateEmailFileViaCopyingOver(
t *testing.T, emails []string) { t *testing.T, emails []string) {
origFile := vt.authEmailFile orig_file := vt.auth_email_file
var err error var err error
vt.authEmailFile, err = ioutil.TempFile("", "test_auth_emails_") vt.auth_email_file, err = ioutil.TempFile("", "test_auth_emails_")
if err != nil { if err != nil {
t.Fatal("failed to create temp file for copy: " + err.Error()) t.Fatal("failed to create temp file for copy: " + err.Error())
} }
vt.WriteEmails(t, emails) vt.WriteEmails(t, emails)
err = os.Rename(vt.authEmailFile.Name(), origFile.Name()) err = os.Rename(vt.auth_email_file.Name(), orig_file.Name())
if err != nil { if err != nil {
t.Fatal("failed to copy over temp file: " + err.Error()) t.Fatal("failed to copy over temp file: " + err.Error())
} }
vt.authEmailFile = origFile vt.auth_email_file = orig_file
} }
func TestValidatorOverwriteEmailListViaCopyingOver(t *testing.T) { func TestValidatorOverwriteEmailListViaCopyingOver(t *testing.T) {

View File

@ -10,8 +10,8 @@ import (
func (vt *ValidatorTest) UpdateEmailFile(t *testing.T, emails []string) { func (vt *ValidatorTest) UpdateEmailFile(t *testing.T, emails []string) {
var err error var err error
vt.authEmailFile, err = os.OpenFile( vt.auth_email_file, err = os.OpenFile(
vt.authEmailFile.Name(), os.O_WRONLY|os.O_CREATE, 0600) vt.auth_email_file.Name(), os.O_WRONLY|os.O_CREATE, 0600)
if err != nil { if err != nil {
t.Fatal("failed to re-open temp file for updates") t.Fatal("failed to re-open temp file for updates")
} }
@ -20,24 +20,24 @@ func (vt *ValidatorTest) UpdateEmailFile(t *testing.T, emails []string) {
func (vt *ValidatorTest) UpdateEmailFileViaRenameAndReplace( func (vt *ValidatorTest) UpdateEmailFileViaRenameAndReplace(
t *testing.T, emails []string) { t *testing.T, emails []string) {
origFile := vt.authEmailFile orig_file := vt.auth_email_file
var err error var err error
vt.authEmailFile, err = ioutil.TempFile("", "test_auth_emails_") vt.auth_email_file, err = ioutil.TempFile("", "test_auth_emails_")
if err != nil { if err != nil {
t.Fatal("failed to create temp file for rename and replace: " + t.Fatal("failed to create temp file for rename and replace: " +
err.Error()) err.Error())
} }
vt.WriteEmails(t, emails) vt.WriteEmails(t, emails)
movedName := origFile.Name() + "-moved" moved_name := orig_file.Name() + "-moved"
err = os.Rename(origFile.Name(), movedName) err = os.Rename(orig_file.Name(), moved_name)
err = os.Rename(vt.authEmailFile.Name(), origFile.Name()) err = os.Rename(vt.auth_email_file.Name(), orig_file.Name())
if err != nil { if err != nil {
t.Fatal("failed to rename and replace temp file: " + t.Fatal("failed to rename and replace temp file: " +
err.Error()) err.Error())
} }
vt.authEmailFile = origFile vt.auth_email_file = orig_file
os.Remove(movedName) os.Remove(moved_name)
} }
func TestValidatorOverwriteEmailListDirectly(t *testing.T) { func TestValidatorOverwriteEmailListDirectly(t *testing.T) {

Some files were not shown because too many files have changed in this diff Show More