diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 47d0e56..c7f2960 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,6 @@ -# Default owner should be a Pusher cloud-team member unless overridden by later -# rules in this file -* @pusher/cloud-team +# 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 @@ -10,3 +10,7 @@ # 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 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a0658e1 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,13 @@ +run: + deadline: 120s +linters: + enable: + - govet + - golint + - ineffassign + - goconst + - deadcode + - gofmt + - goimports + enable-all: false + disable-all: true diff --git a/.travis.yml b/.travis.yml index c5cdc33..0537736 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,12 @@ language: go go: - - 1.11.x - 1.12.x install: # Fetch dependencies - - wget -O dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 - - chmod +x dep - - mv dep $GOPATH/bin/dep + - 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: - - ./configure - # Run tests - - make test + - ./configure && make test sudo: false notifications: email: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 529ab4d..44cd0cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,83 @@ # Vx.x.x (Pre-release) +## Changes since v4.0.0 + +# 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 -- [#146](https://github.com/pusher/oauth2_proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field (@gargath) +- [#231](https://github.com/pusher/oauth2_proxy/pull/231) Rework GitLab provider + - This PR changes the configuration options for the GitLab provider to use + a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than + explicit `-login-url`, `-redeem-url` and `-validate-url` parameters. +- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent + - This PR changes configuration options so that all flags have a config counterpart + of the same name but with underscores (`_`) in place of hyphens (`-`). + This change affects the following flags: + - The `--tls-key` flag is now `--tls-key-file` to be consistent with existing + file flags and the existing config and environment settings + - The `--tls-cert` flag is now `--tls-cert-file` to be consistent with existing + file flags and the existing config and environment settings + This change affects the following existing configuration options: + - The `proxy-prefix` option is now `proxy_prefix`. + This PR changes environment variables so that all flags have an environment + counterpart of the same name but capitalised, with underscores (`_`) in place + of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`. + This change affects the following existing environment variables: + - The `OAUTH2_SKIP_OIDC_DISCOVERY` environment variable is now `OAUTH2_PROXY_SKIP_OIDC_DISCOVERY`. + - The `OAUTH2_OIDC_JWKS_URL` environment variable is now `OAUTH2_PROXY_OIDC_JWKS_URL`. +- [#146](https://github.com/pusher/oauth2_proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field - This change modifies the contents of the `X-Forwarded-User` header supplied by the proxy for users where the auth response from the IdP did not contain 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 -- [#169](https://github.com/pusher/outh2_proxy/pull/169) Update Alpine to 3.9 (@kskewes) -- [#148](https://github.com/pusher/outh2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed) -- [#147](https://github.com/pusher/outh2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed) +- [#234](https://github.com/pusher/oauth2_proxy/pull/234) Added option `-ssl-upstream-insecure-skip-validation` to skip validation of upstream SSL certificates (@jansinger) +- [#224](https://github.com/pusher/oauth2_proxy/pull/224) Check Google group membership using hasMember to support nested groups and external users (@jpalpant) +- [#231](https://github.com/pusher/oauth2_proxy/pull/231) Add optional group membership and email domain checks to the GitLab provider (@Overv) +- [#226](https://github.com/pusher/oauth2_proxy/pull/226) Made setting of proxied headers deterministic based on configuration alone (@aeijdenberg) +- [#178](https://github.com/pusher/oauth2_proxy/pull/178) Add Silence Ping Logging and Exclude Logging Paths flags (@kskewes) +- [#209](https://github.com/pusher/oauth2_proxy/pull/209) Improve docker build caching of layers (@dekimsey) +- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent (@JoelSpeed) +- [#187](https://github.com/pusher/oauth2_proxy/pull/187) Move root packages to pkg folder (@JoelSpeed) +- [#65](https://github.com/pusher/oauth2_proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via + the `-skip-jwt-bearer-token` options. (@brianv0) + - Additional verifiers can be configured via the `-extra-jwt-issuers` flag if the JWT issuers is either an OpenID provider or has a JWKS URL + (e.g. `https://example.com/.well-known/jwks.json`). +- [#180](https://github.com/pusher/oauth2_proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg). +- [#175](https://github.com/pusher/oauth2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg). + - Includes fix for potential signature checking issue when OIDC discovery is skipped. +- [#155](https://github.com/pusher/oauth2_proxy/pull/155) Add RedisSessionStore implementation (@brianv0, @JoelSpeed) + - Implement flags to configure the redis session store + - `-session-store-type=redis` Sets the store type to redis + - `-redis-connection-url` Sets the Redis connection URL + - `-redis-use-sentinel=true` Enables Redis Sentinel support + - `-redis-sentinel-master-name` Sets the Sentinel master name, if sentinel is enabled + - `-redis-sentinel-connection-urls` Defines the Redis Sentinel Connection URLs, if sentinel is enabled + - Introduces the concept of a session ticket. Tickets are composed of the cookie name, a session ID, and a secret. + - Redis Sessions are stored encrypted with a per-session secret + - Added tests for server based session stores +- [#168](https://github.com/pusher/oauth2_proxy/pull/168) Drop Go 1.11 support in Travis (@JoelSpeed) +- [#169](https://github.com/pusher/oauth2_proxy/pull/169) Update Alpine to 3.9 (@kskewes) +- [#148](https://github.com/pusher/oauth2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed) +- [#147](https://github.com/pusher/oauth2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed) - Allows for multiple different session storage implementations including client and server side - Adds tests suite for interface to ensure consistency across implementations - Refactor some configuration options (around cookies) into packages @@ -34,8 +99,21 @@ - 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 diff --git a/Dockerfile b/Dockerfile index f6649a9..db18b41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,17 @@ FROM golang:1.12-stretch AS builder # Download tools -RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 -RUN chmod +x $GOPATH/bin/dep +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 -COPY . . # Fetch dependencies -RUN dep ensure --vendor-only +COPY go.mod go.sum ./ +RUN GO111MODULE=on go mod download + +# Now pull in our code +COPY . . # Build binary and make sure there is at least an empty key file. # This is useful for GCP App Engine custom runtime builds, because @@ -20,7 +22,7 @@ RUN dep ensure --vendor-only RUN ./configure && make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM alpine:3.9 +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 diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 7f011ba..5b3c1f5 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -1,15 +1,17 @@ FROM golang:1.12-stretch AS builder # Download tools -RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 -RUN chmod +x $GOPATH/bin/dep +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 -COPY . . # Fetch dependencies -RUN dep ensure --vendor-only +COPY go.mod go.sum ./ +RUN GO111MODULE=on go mod download + +# Now pull in our code +COPY . . # Build binary and make sure there is at least an empty key file. # This is useful for GCP App Engine custom runtime builds, because @@ -20,7 +22,7 @@ RUN dep ensure --vendor-only RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM arm64v8/alpine:3.9 +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 diff --git a/Dockerfile.armv6 b/Dockerfile.armv6 index e2c57b9..5f5fe75 100644 --- a/Dockerfile.armv6 +++ b/Dockerfile.armv6 @@ -1,15 +1,17 @@ FROM golang:1.12-stretch AS builder # Download tools -RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 -RUN chmod +x $GOPATH/bin/dep +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 -COPY . . # Fetch dependencies -RUN dep ensure --vendor-only +COPY go.mod go.sum ./ +RUN GO111MODULE=on go mod download + +# Now pull in our code +COPY . . # Build binary and make sure there is at least an empty key file. # This is useful for GCP App Engine custom runtime builds, because @@ -20,7 +22,7 @@ RUN dep ensure --vendor-only RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM arm32v6/alpine:3.9 +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 diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 01af4d2..0000000 --- a/Gopkg.lock +++ /dev/null @@ -1,365 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - digest = "1:b24249f5a5e6fbe1eddc94b25973172339ccabeadef4779274f3ed0167c18812" - name = "cloud.google.com/go" - packages = ["compute/metadata"] - pruneopts = "" - revision = "2d3a6656c17a60b0815b7e06ab0be04eacb6e613" - version = "v0.16.0" - -[[projects]] - digest = "1:289dd4d7abfb3ad2b5f728fbe9b1d5c1bf7d265a3eb9ef92869af1f7baba4c7a" - name = "github.com/BurntSushi/toml" - packages = ["."] - pruneopts = "" - revision = "b26d9c308763d68093482582cea63d69be07a0f0" - version = "v0.3.0" - -[[projects]] - digest = "1:512883404c2a99156e410e9880e3bb35ecccc0c07c1159eb204b5f3ef3c431b3" - name = "github.com/bitly/go-simplejson" - packages = ["."] - pruneopts = "" - revision = "aabad6e819789e569bd6aabf444c935aa9ba1e44" - version = "v0.5.0" - -[[projects]] - branch = "v2" - digest = "1:e5a238f8fa890e529d7e493849bbae8988c9e70344e4630cc4f9a11b00516afb" - name = "github.com/coreos/go-oidc" - packages = ["."] - pruneopts = "" - revision = "77e7f2010a464ade7338597afe650dfcffbe2ca8" - -[[projects]] - digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" - name = "github.com/davecgh/go-spew" - packages = ["spew"] - pruneopts = "" - revision = "346938d642f2ec3594ed81d874461961cd0faa76" - version = "v1.1.0" - -[[projects]] - digest = "1:6098222470fe0172157ce9bbef5d2200df4edde17ee649c5d6e48330e4afa4c6" - name = "github.com/dgrijalva/jwt-go" - packages = ["."] - pruneopts = "" - revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" - version = "v3.2.0" - -[[projects]] - branch = "master" - digest = "1:3b760d3b93f994df8eb1d9ebfad17d3e9e37edcb7f7efaa15b427c0d7a64f4e4" - name = "github.com/golang/protobuf" - packages = ["proto"] - pruneopts = "" - revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" - -[[projects]] - digest = "1:b3c5b95e56c06f5aa72cb2500e6ee5f44fcd122872d4fec2023a488e561218bc" - name = "github.com/hpcloud/tail" - packages = [ - ".", - "ratelimiter", - "util", - "watch", - "winfile", - ] - pruneopts = "" - revision = "a30252cb686a21eb2d0b98132633053ec2f7f1e5" - version = "v1.0.0" - -[[projects]] - digest = "1:af67386ca553c04c6222f7b5b2f17bc97a5dfb3b81b706882c7fd8c72c30cf8f" - name = "github.com/mbland/hmacauth" - packages = ["."] - pruneopts = "" - revision = "107c17adcc5eccc9935cd67d9bc2feaf5255d2cb" - version = "1.0.2" - -[[projects]] - branch = "master" - digest = "1:15c0562bca5d78ac087fb39c211071dc124e79fb18f8b7c3f8a0bc7ffcb2a38e" - name = "github.com/mreiferson/go-options" - packages = ["."] - pruneopts = "" - revision = "20ba7d382d05facb01e02eb777af0c5f229c5c95" - -[[projects]] - digest = "1:a3735b0978a8b53fc2ac97a6f46ca1189f0712a00df86d6ec4cf26c1a25e6d77" - name = "github.com/onsi/ginkgo" - packages = [ - ".", - "config", - "internal/codelocation", - "internal/containernode", - "internal/failer", - "internal/leafnodes", - "internal/remote", - "internal/spec", - "internal/spec_iterator", - "internal/specrunner", - "internal/suite", - "internal/testingtproxy", - "internal/writer", - "reporters", - "reporters/stenographer", - "reporters/stenographer/support/go-colorable", - "reporters/stenographer/support/go-isatty", - "types", - ] - pruneopts = "" - revision = "eea6ad008b96acdaa524f5b409513bf062b500ad" - version = "v1.8.0" - -[[projects]] - digest = "1:dbafce2fddb1ca331646fe2ac9c9413980368b19a60a4406a6e5861680bd73be" - name = "github.com/onsi/gomega" - packages = [ - ".", - "format", - "internal/assertion", - "internal/asyncassertion", - "internal/oraclematcher", - "internal/testingtsupport", - "matchers", - "matchers/support/goraph/bipartitegraph", - "matchers/support/goraph/edge", - "matchers/support/goraph/node", - "matchers/support/goraph/util", - "types", - ] - pruneopts = "" - revision = "90e289841c1ed79b7a598a7cd9959750cb5e89e2" - version = "v1.5.0" - -[[projects]] - digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - pruneopts = "" - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - -[[projects]] - branch = "master" - digest = "1:386e12afcfd8964907c92dffd106860c0dedd71dbefae14397b77b724a13343b" - name = "github.com/pquerna/cachecontrol" - packages = [ - ".", - "cacheobject", - ] - pruneopts = "" - revision = "0dec1b30a0215bb68605dfc568e8855066c9202d" - -[[projects]] - digest = "1:3926a4ec9a4ff1a072458451aa2d9b98acd059a45b38f7335d31e06c3d6a0159" - name = "github.com/stretchr/testify" - packages = [ - "assert", - "require", - ] - pruneopts = "" - revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" - version = "v1.1.4" - -[[projects]] - branch = "master" - digest = "1:39630a0e2844fc4297c27caacb394a9fd342f869292284a62f856877adab65bc" - name = "github.com/yhat/wsutil" - packages = ["."] - pruneopts = "" - revision = "1d66fa95c997864ba4d8479f56609620fe542928" - -[[projects]] - branch = "master" - digest = "1:f6a006d27619a4d93bf9b66fe1999b8c8d1fa62bdc63af14f10fbe6fcaa2aa1a" - name = "golang.org/x/crypto" - packages = [ - "bcrypt", - "blowfish", - "ed25519", - "ed25519/internal/edwards25519", - ] - pruneopts = "" - revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94" - -[[projects]] - branch = "master" - digest = "1:130b1bec86c62e121967ee0c69d9c263dc2d3ffe6c7c9a82aca4071c4d068861" - name = "golang.org/x/net" - packages = [ - "context", - "context/ctxhttp", - "html", - "html/atom", - "html/charset", - "websocket", - ] - pruneopts = "" - revision = "9dfe39835686865bff950a07b394c12a98ddc811" - -[[projects]] - branch = "master" - digest = "1:4a61176e8386727e4847b21a5a2625ce56b9c518bc543a28226503e701265db0" - name = "golang.org/x/oauth2" - packages = [ - ".", - "google", - "internal", - "jws", - "jwt", - ] - pruneopts = "" - revision = "9ff8ebcc8e241d46f52ecc5bff0e5a2f2dbef402" - -[[projects]] - branch = "master" - digest = "1:67a6e61e60283fd7dce50eba228080bff8805d9d69b2f121d7ec2260d120c4a8" - name = "golang.org/x/sys" - packages = ["unix"] - pruneopts = "" - revision = "ca7f33d4116e3a1f9425755d6a44e7ed9b4c97df" - -[[projects]] - digest = "1:740b51a55815493a8d0f2b1e0d0ae48fe48953bf7eaf3fcc4198823bf67768c0" - name = "golang.org/x/text" - packages = [ - "encoding", - "encoding/charmap", - "encoding/htmlindex", - "encoding/internal", - "encoding/internal/identifier", - "encoding/japanese", - "encoding/korean", - "encoding/simplifiedchinese", - "encoding/traditionalchinese", - "encoding/unicode", - "internal/gen", - "internal/language", - "internal/language/compact", - "internal/tag", - "internal/utf8internal", - "language", - "runes", - "transform", - "unicode/cldr", - ] - pruneopts = "" - revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475" - version = "v0.3.2" - -[[projects]] - branch = "master" - digest = "1:dc1fb726dbbe79c86369941eae1e3b431b8fc6f11dbd37f7899dc758a43cc3ed" - name = "google.golang.org/api" - packages = [ - "admin/directory/v1", - "gensupport", - "googleapi", - "googleapi/internal/uritemplates", - ] - pruneopts = "" - revision = "8791354e7ab150705ede13637a18c1fcc16b62e8" - -[[projects]] - digest = "1:934fb8966f303ede63aa405e2c8d7f0a427a05ea8df335dfdc1833dd4d40756f" - name = "google.golang.org/appengine" - packages = [ - ".", - "internal", - "internal/app_identity", - "internal/base", - "internal/datastore", - "internal/log", - "internal/modules", - "internal/remote_api", - "internal/urlfetch", - "urlfetch", - ] - pruneopts = "" - revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" - version = "v1.0.0" - -[[projects]] - digest = "1:eb53021a8aa3f599d29c7102e65026242bdedce998a54837dc67f14b6a97c5fd" - name = "gopkg.in/fsnotify.v1" - packages = ["."] - pruneopts = "" - revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" - source = "https://github.com/fsnotify/fsnotify.git" - version = "v1.4.7" - -[[projects]] - digest = "1:cb5b2a45a3dd41c01ff779c54ae4c8aab0271d6d3b3f734c8a8bd2c890299ef2" - name = "gopkg.in/fsnotify/fsnotify.v1" - packages = ["."] - pruneopts = "" - revision = "836bfd95fecc0f1511dd66bdbf2b5b61ab8b00b6" - version = "v1.2.11" - -[[projects]] - digest = "1:11c58e19ff7ce22740423bb933f1ddca3bf575def40d5ac3437ec12871b1648b" - name = "gopkg.in/natefinch/lumberjack.v2" - packages = ["."] - pruneopts = "" - revision = "a96e63847dc3c67d17befa69c303767e2f84e54f" - version = "v2.1" - -[[projects]] - digest = "1:be4ed0a2b15944dd777a663681a39260ed05f9c4e213017ed2e2255622c8820c" - name = "gopkg.in/square/go-jose.v2" - packages = [ - ".", - "cipher", - "json", - ] - pruneopts = "" - revision = "f8f38de21b4dcd69d0413faf231983f5fd6634b1" - version = "v2.1.3" - -[[projects]] - branch = "v1" - digest = "1:a96d16bd088460f2e0685d46c39bcf1208ba46e0a977be2df49864ec7da447dd" - name = "gopkg.in/tomb.v1" - packages = ["."] - pruneopts = "" - revision = "dd632973f1e7218eb1089048e0798ec9ae7dceb8" - -[[projects]] - digest = "1:cedccf16b71e86db87a24f8d4c70b0a855872eb967cb906a66b95de56aefbd0d" - name = "gopkg.in/yaml.v2" - packages = ["."] - pruneopts = "" - revision = "51d6538a90f86fe93ac480b35f37b2be17fef232" - version = "v2.2.2" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "github.com/BurntSushi/toml", - "github.com/bitly/go-simplejson", - "github.com/coreos/go-oidc", - "github.com/dgrijalva/jwt-go", - "github.com/mbland/hmacauth", - "github.com/mreiferson/go-options", - "github.com/onsi/ginkgo", - "github.com/onsi/gomega", - "github.com/stretchr/testify/assert", - "github.com/stretchr/testify/require", - "github.com/yhat/wsutil", - "golang.org/x/crypto/bcrypt", - "golang.org/x/net/websocket", - "golang.org/x/oauth2", - "golang.org/x/oauth2/google", - "google.golang.org/api/admin/directory/v1", - "google.golang.org/api/googleapi", - "gopkg.in/fsnotify/fsnotify.v1", - "gopkg.in/natefinch/lumberjack.v2", - "gopkg.in/square/go-jose.v2", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index 732bbcb..0000000 --- a/Gopkg.toml +++ /dev/null @@ -1,48 +0,0 @@ - -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# - -[[constraint]] - name = "github.com/BurntSushi/toml" - version = "~0.3.0" - -[[constraint]] - name = "github.com/bitly/go-simplejson" - version = "~0.5.0" - -[[constraint]] - branch = "v2" - name = "github.com/coreos/go-oidc" - -[[constraint]] - branch = "master" - name = "github.com/mreiferson/go-options" - -[[constraint]] - name = "github.com/stretchr/testify" - version = "~1.1.4" - -[[constraint]] - branch = "master" - name = "golang.org/x/oauth2" - -[[constraint]] - branch = "master" - name = "google.golang.org/api" - -[[constraint]] - name = "gopkg.in/fsnotify/fsnotify.v1" - version = "~1.2.0" - -[[override]] - name = "gopkg.in/fsnotify.v1" - source = "https://github.com/fsnotify/fsnotify.git" - -[[constraint]] - branch = "master" - name = "golang.org/x/crypto" - -[[constraint]] - name = "gopkg.in/natefinch/lumberjack.v2" - version = "2.1.0" diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 0000000..25fb475 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,3 @@ +Joel Speed (@JoelSpeed) +Dan Bond (@syscll) +Henry Jenkins (@steakunderscore) diff --git a/Makefile b/Makefile index 0dca566..f41f320 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ VERSION := $(shell git describe --always --dirty --tags 2>/dev/null || echo "und .NOTPARALLEL: .PHONY: all -all: dep lint $(BINARY) +all: lint $(BINARY) .PHONY: clean clean: @@ -15,36 +15,15 @@ clean: distclean: clean rm -rf vendor -BIN_DIR := $(GOPATH)/bin -GOMETALINTER := $(BIN_DIR)/gometalinter - -$(GOMETALINTER): - $(GO) get -u github.com/alecthomas/gometalinter - gometalinter --install %> /dev/null - .PHONY: lint -lint: $(GOMETALINTER) - $(GOMETALINTER) --vendor --disable-all \ - --enable=vet \ - --enable=vetshadow \ - --enable=golint \ - --enable=ineffassign \ - --enable=goconst \ - --enable=deadcode \ - --enable=gofmt \ - --enable=goimports \ - --deadline=120s \ - --tests ./... - -.PHONY: dep -dep: - $(DEP) ensure --vendor-only +lint: + GO111MODULE=on $(GOLANGCILINT) run .PHONY: build build: clean $(BINARY) $(BINARY): - CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/pusher/oauth2_proxy + GO111MODULE=on CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/pusher/oauth2_proxy .PHONY: docker docker: @@ -75,24 +54,34 @@ docker-push-all: docker-push docker push quay.io/pusher/oauth2_proxy:${VERSION}-armv6 .PHONY: test -test: dep lint - $(GO) test -v -race ./... +test: lint + GO111MODULE=on $(GO) test -v -race ./... .PHONY: release release: lint test mkdir release - GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-darwin-amd64 github.com/pusher/oauth2_proxy - GOOS=linux GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-linux-amd64 github.com/pusher/oauth2_proxy - GOOS=linux GOARCH=arm64 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-linux-arm64 github.com/pusher/oauth2_proxy - GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-linux-armv6 github.com/pusher/oauth2_proxy - GOOS=windows GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" -o release/$(BINARY)-windows-amd64 github.com/pusher/oauth2_proxy - shasum -a 256 release/$(BINARY)-darwin-amd64 > release/$(BINARY)-darwin-amd64-sha256sum.txt - shasum -a 256 release/$(BINARY)-linux-amd64 > release/$(BINARY)-linux-amd64-sha256sum.txt - shasum -a 256 release/$(BINARY)-linux-arm64 > release/$(BINARY)-linux-arm64-sha256sum.txt - shasum -a 256 release/$(BINARY)-linux-armv6 > release/$(BINARY)-linux-armv6-sha256sum.txt - shasum -a 256 release/$(BINARY)-windows-amd64 > release/$(BINARY)-windows-amd64-sha256sum.txt - tar -czvf release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION).tar.gz release/$(BINARY)-darwin-amd64 - tar -czvf release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION).tar.gz release/$(BINARY)-linux-amd64 - tar -czvf release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION).tar.gz release/$(BINARY)-linux-arm64 - tar -czvf release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION).tar.gz release/$(BINARY)-linux-armv6 - tar -czvf release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION).tar.gz release/$(BINARY)-windows-amd64 + 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) diff --git a/README.md b/README.md index b3a2806..ad88331 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A list of changes can be seen in the [CHANGELOG](CHANGELOG.md). 1. Choose how to deploy: - a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v3.2.0`) + a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v4.0.0`) b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin` @@ -25,7 +25,7 @@ Prebuilt binaries can be validated by extracting the file and verifying it again ``` sha256sum -c sha256sum.txt 2>&1 | grep OK -oauth2_proxy-3.2.0.linux-amd64: OK +oauth2_proxy-4.0.0.linux-amd64: OK ``` 2. [Select a Provider and Register an OAuth Application with a Provider](https://pusher.github.io/oauth2_proxy/auth-configuration) @@ -38,6 +38,10 @@ Read the docs on our [Docs site](https://pusher.github.io/oauth2_proxy). ![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png) +## Getting Involved + +If you would like to reach out to the maintainers, come talk to us in the `#oauth2_proxy` channel in the [Gophers slack](http://gophers.slack.com/). + ## Contributing Please see our [Contributing](CONTRIBUTING.md) guidelines. diff --git a/configure b/configure index 24790ab..26db8fe 100755 --- a/configure +++ b/configure @@ -13,13 +13,9 @@ for arg in "$@"; do "--with-go") desired[go]="${arg##*=}" ;; - "--with-dep") - desired[dep]="${arg##*=}" - ;; "--help") printf "${GREEN}$0${NC}\n" printf " available options:\n" - printf " --with-dep=${BLUE}${NC}\n" printf " --with-go=${BLUE}${NC}\n" exit 0 ;; @@ -81,7 +77,7 @@ check_for() { 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.11 + vercomp $GO_VERSION 1.12 case $? in 0) ;& 1) @@ -91,7 +87,7 @@ check_go_version() { ;; 2) printf "${RED}" - echo "$GO_VERSION < 1.11" + echo "$GO_VERSION < 1.12" exit 1 ;; esac @@ -116,16 +112,14 @@ check_go_env() { cd ${0%/*} -if [ ! -f .env ]; then - rm .env -fi +rm -fv .env check_for make check_for awk check_for go check_go_version check_go_env -check_for dep +check_for golangci-lint echo @@ -133,7 +127,7 @@ cat <<- EOF > .env MAKE := "${tools[make]}" GO := "${tools[go]}" GO_VERSION := ${tools[go_version]} - DEP := "${tools[dep]}" + GOLANGCILINT := "${tools[golangci-lint]}" EOF echo "Environment configuration written to .env" diff --git a/docs/0_index.md b/docs/0_index.md index 30c3555..376bd75 100644 --- a/docs/0_index.md +++ b/docs/0_index.md @@ -12,7 +12,7 @@ to validate accounts by email, domain or group. **Note:** This repository was forked from [bitly/OAuth2_Proxy](https://github.com/bitly/oauth2_proxy) on 27/11/2018. Versions v3.0.0 and up are from this fork and will have diverged from any changes in the original fork. -A list of changes can be seen in the [CHANGELOG](CHANGELOG.md). +A list of changes can be seen in the [CHANGELOG]({{ site.gitweb }}/CHANGELOG.md). [![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy) @@ -20,4 +20,4 @@ A list of changes can be seen in the [CHANGELOG](CHANGELOG.md). ## Architecture -![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png) \ No newline at end of file +![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png) diff --git a/docs/2_auth.md b/docs/2_auth.md index 7a9bebd..e6c5cc6 100644 --- a/docs/2_auth.md +++ b/docs/2_auth.md @@ -103,13 +103,15 @@ If you are using GitHub enterprise, make sure you set the following to the appro ### GitLab Auth Provider -Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](http://doc.gitlab.com/ce/integration/oauth_provider.html) +Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](https://docs.gitlab.com/ce/integration/oauth_provider.html). Make sure to enable at least the `openid`, `profile` and `email` scopes. + +Restricting by group membership is possible with the following option: + + -gitlab-group="": restrict logins to members of any of these groups (slug), separated by a comma If you are using self-hosted GitLab, make sure you set the following to the appropriate URL: - -login-url="/oauth/authorize" - -redeem-url="/oauth/token" - -validate-url="/api/v4/user" + -oidc-issuer-url="" ### LinkedIn Auth Provider @@ -124,7 +126,7 @@ For LinkedIn, the registration steps are: ### Microsoft Azure AD Provider -For adding an application to the Microsoft Azure AD follow [these steps to add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/). +For adding an application to the Microsoft Azure AD follow [these steps to add an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app). Take note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server. @@ -144,6 +146,56 @@ OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many ma -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. @@ -225,7 +277,7 @@ To authorize by email domain use `--email-domain=yourcompany.com`. To authorize ## Adding a new Provider -Follow the examples in the [`providers` package](providers/) to define a new +Follow the examples in the [`providers` package]({{ site.gitweb }}/providers/) to define a new `Provider` instance. Add a new `case` to -[`providers.New()`](providers/providers.go) to allow `oauth2_proxy` to use the +[`providers.New()`]({{ site.gitweb }}/providers/providers.go) to allow `oauth2_proxy` to use the new `Provider`. diff --git a/docs/4_tls.md b/docs/4_tls.md index 706b665..5e54dd9 100644 --- a/docs/4_tls.md +++ b/docs/4_tls.md @@ -9,65 +9,65 @@ nav_order: 4 There are two recommended configurations. -1. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert=/path/to/cert.pem` and `--tls-key=/path/to/cert.key`. +1. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert-file=/path/to/cert.pem` and `--tls-key-file=/path/to/cert.key`. -The command line to run `oauth2_proxy` in this configuration would look like this: + The command line to run `oauth2_proxy` in this configuration would look like this: -```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=... -``` + ```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"`. + 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/`. + 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): + 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; + ``` + 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; + 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: + 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=... -``` + ```bash + ./oauth2_proxy \ + --email-domain="yourcompany.com" \ + --upstream=http://127.0.0.1:8080/ \ + --cookie-secret=... \ + --cookie-secure=true \ + --provider=... \ + --client-id=... \ + --client-secret=... + ``` diff --git a/docs/6_request_signatures.md b/docs/6_request_signatures.md index 9feb961..0aa60aa 100644 --- a/docs/6_request_signatures.md +++ b/docs/6_request_signatures.md @@ -11,7 +11,7 @@ If `signature_key` is defined, proxied requests will be signed with the `GAP-Signature` header, which is a [Hash-based Message Authentication Code (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). +in `oauthproxy.go`]({{ site.gitweb }}/oauthproxy.go). `signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`) diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 8d96e5d..934baab 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -208,7 +208,7 @@ GEM jekyll-seo-tag (~> 2.1) minitest (5.11.3) multipart-post (2.0.0) - nokogiri (1.10.1) + nokogiri (1.10.4) mini_portile2 (~> 2.4.0) octokit (4.13.0) sawyer (~> 0.8.0, >= 0.5.3) diff --git a/docs/_config.yml b/docs/_config.yml index dcc24a1..87f026c 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -18,6 +18,7 @@ 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 diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index fd33d37..05cc299 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -14,89 +14,101 @@ To generate a strong cookie secret use `python -c 'import os,base64; print base6 ### Config File -An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/oauth2_proxy.cfg` +An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/oauth2_proxy.cfg` ### Command Line Options -``` -Usage of oauth2_proxy: - -acr-values string: optional, used by login.gov (default "http://idmanagement.gov/ns/assurance/loa/1") - -approval-prompt string: OAuth approval_prompt (default "force") - -auth-logging: Log authentication attempts (default true) - -auth-logging-format string: Template for authentication log lines (see "Logging Configuration" paragraph below) - -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-path string: an optional cookie path to force cookies to (ie: /poc/)* (default "/") - -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 - -flush-interval: period between flushing response buffers when streaming responses (default "1s") - -footer string: custom footer string. Use "-" to disable default footer. - -gcp-healthchecks: will enable /liveness_check, /readiness_check, and / (with the proper user-agent) endpoints that will make it work well with GCP App Engine and GKE Ingresses (default false) - -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://]: or unix:// to listen on for HTTP clients (default "127.0.0.1:4180") - -https-address string: : to listen on for HTTPS clients (default ":443") - -logging-compress: Should rotated log files be compressed using gzip (default false) - -logging-filename string: File to log requests to, empty for stdout (default to stdout) - -logging-local-time: If the time in log files and backup filenames are local or UTC time (default true) - -logging-max-age int: Maximum number of days to retain old log files (default 7) - -logging-max-backups int: Maximum number of old log files to retain; 0 to disable (default 0) - -logging-max-size int: Maximum size in megabytes of the log file before rotation (default 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 - -oidc-issuer-url: 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: pass OAuth access_token to upstream via X-Forwarded-Access-Token header - -pass-authorization-header: pass OIDC IDToken to upstream via Authorization Bearer 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. //sign_in) (default "/oauth2") - -proxy-websockets: enables WebSocket proxying (default 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" - -request-logging: Log requests to stdout (default true) - -request-logging-format: Template for request log lines (see "Logging Configuration" paragraph below) - -resource string: The resource that is protected (Azure AD only) - -scope string: OAuth scope specification - -session-store-type: Session data storage backend (default: cookie) - -set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) - -set-authorization-header: set Authorization Bearer response header (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-oidc-discovery: bypass OIDC endpoint discovery. login-url, redeem-url and oidc-jwks-url must be configured in this case - -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 - -standard-logging: Log standard runtime information (default true) - -standard-logging-format string: Template for standard log lines (see "Logging Configuration" paragraph below) - -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 - -whitelist-domain: allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com) -``` +| 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://]:` or `unix://` to listen on for HTTP clients | `"127.0.0.1:4180"` | +| `-https-address` | string | `:` 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. /`/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. @@ -112,17 +124,14 @@ Multiple upstreams can either be configured by supplying a comma separated list ### Environment variables -The following environment variables can be used in place of the corresponding command-line arguments: +Every command line argument can be specified as an environment variable by +prefixing it with `OAUTH2_PROXY_`, capitalising it, and replacing hypens (`-`) +with underscores (`_`). 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 `--set-authorization-header` flag becomes `OAUTH2_PROXY_SET_AUTHORIZATION_HEADER`. -- `OAUTH2_PROXY_CLIENT_ID` -- `OAUTH2_PROXY_CLIENT_SECRET` -- `OAUTH2_PROXY_COOKIE_NAME` -- `OAUTH2_PROXY_COOKIE_SECRET` -- `OAUTH2_PROXY_COOKIE_DOMAIN` -- `OAUTH2_PROXY_COOKIE_PATH` -- `OAUTH2_PROXY_COOKIE_EXPIRE` -- `OAUTH2_PROXY_COOKIE_REFRESH` -- `OAUTH2_PROXY_SIGNATURE_KEY` ## Logging Configuration @@ -134,6 +143,8 @@ There are three different types of logging: standard, authentication, and HTTP r Each type of logging has their own configurable format and variables. By default these formats are similar to the Apache Combined Log. +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: @@ -306,3 +317,5 @@ nginx.ingress.kubernetes.io/configuration-snippet: | 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=". diff --git a/docs/configuration/sessions.md b/docs/configuration/sessions.md index 6e9d9d7..4cfb09d 100644 --- a/docs/configuration/sessions.md +++ b/docs/configuration/sessions.md @@ -15,7 +15,8 @@ The OAuth2 Proxy uses a Cookie to track user sessions and will store the session data in one of the available session storage backends. At present the available backends are (as passed to `--session-store-type`): -- [cookie](cookie-storage) (default) +- [cookie](#cookie-storage) (default) +- [redis](#redis-storage) ### Cookie Storage @@ -32,3 +33,35 @@ The following should be known when using this implementation: - Since multiple requests can be made concurrently to the OAuth2 Proxy, this session implementation 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. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7880da9 --- /dev/null +++ b/go.mod @@ -0,0 +1,87 @@ +module github.com/pusher/oauth2_proxy + +go 1.12 + +require ( + cloud.google.com/go v0.41.0 // indirect + github.com/BurntSushi/toml v0.3.1 + github.com/OpenPeeDeeP/depguard v1.0.0 // indirect + github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect + github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5 + github.com/bitly/go-simplejson v0.5.0 + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/coreos/bbolt v1.3.3 // indirect + github.com/coreos/etcd v3.3.13+incompatible // indirect + github.com/coreos/go-oidc v2.0.0+incompatible + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/fatih/color v1.7.0 // indirect + github.com/go-critic/go-critic v0.3.4 // indirect + github.com/go-kit/kit v0.9.0 // indirect + github.com/go-ole/go-ole v1.2.4 // indirect + github.com/go-redis/redis v6.15.2+incompatible + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + github.com/golang/protobuf v1.3.2 // indirect + github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect + github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect + github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect + github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect + github.com/golangci/golangci-lint v1.17.1 // indirect + github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect + github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect + github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect + github.com/gostaticanalysis/analysisutil v0.0.2 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.9.4 // indirect + github.com/kisielk/errcheck v1.2.0 // indirect + github.com/klauspost/compress v1.7.2 // indirect + github.com/klauspost/cpuid v1.2.1 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/kr/pty v1.1.8 // indirect + github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946 // indirect + github.com/magiconair/properties v1.8.1 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e + github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05 + github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect + github.com/onsi/ginkgo v1.8.0 + github.com/onsi/gomega v1.5.0 + github.com/pelletier/go-toml v1.4.0 // indirect + github.com/pkg/errors v0.8.1 // indirect + github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 // indirect + github.com/prometheus/common v0.6.0 // indirect + github.com/prometheus/procfs v0.0.3 // indirect + github.com/rogpeppe/fastuuid v1.2.0 // indirect + github.com/rogpeppe/go-internal v1.3.0 // indirect + github.com/russross/blackfriday v2.0.0+incompatible // indirect + github.com/shirou/gopsutil v2.18.12+incompatible // indirect + github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 // indirect + github.com/sirupsen/logrus v1.4.2 // indirect + github.com/spf13/afero v1.2.2 // indirect + github.com/spf13/cobra v0.0.5 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/viper v1.4.0 // indirect + github.com/stretchr/objx v0.2.0 // indirect + github.com/stretchr/testify v1.3.0 + github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822 // indirect + github.com/ugorji/go v1.1.7 // indirect + github.com/valyala/fasthttp v1.4.0 // indirect + github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 + go.etcd.io/bbolt v1.3.3 // indirect + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 + golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f // indirect + golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 // indirect + golang.org/x/mobile v0.0.0-20190711165009-e47acb2ca7f9 // indirect + golang.org/x/net v0.0.0-20190628185345-da137c7871d7 + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect + golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d // indirect + google.golang.org/api v0.7.0 + google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 // indirect + google.golang.org/grpc v1.22.0 // indirect + gopkg.in/fsnotify/fsnotify.v1 v1.2.11 + gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3 + gopkg.in/square/go-jose.v2 v2.1.3 + mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect + sourcegraph.com/sqs/pbtypes v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c8ce175 --- /dev/null +++ b/go.sum @@ -0,0 +1,531 @@ +cloud.google.com/go v0.16.0 h1:alV/SO2XpH+lrvqjDl94dYez7FfeT8ptayazgWwHPIU= +cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.41.0 h1:NFvqUTDnSNYPX5oReekmB+D+90jrJIcVImxQ3qrBVgM= +cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg= +github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= +github.com/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM= +github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U= +github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5 h1:+xnalaRl7JEs6xynGsLgGilz75ljDYZTFKuCadGquPY= +github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5/go.mod h1:8cBZ4R1fh1lx8l4UVit3jNxyybdDi+rjnukCwTYVQE0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg= +github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= +github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc= +github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= +github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= +github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o= +github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= +github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8= +github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= +github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= +github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= +github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= +github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= +github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= +github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astinfo v1.0.0/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= +github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= +github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= +github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= +github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= +github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA= +github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= +github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= +github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 h1:MRhC9XbUjE6XDOInSJ8pwHuPagqsyO89QDU9IdVhe3o= +github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= +github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= +github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0= +github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= +github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU= +github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME= +github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g= +github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= +github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc= +github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo= +github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc h1:XRFao922N8F3EcIXBSNX8Iywk+GI0dxD/8FicMX2D/c= +github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= +github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= +github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM= +github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/gostaticanalysis/analysisutil v0.0.2 h1:OZ4/Q9Lt9bzdyyjAgAWzJfL5dSwPrbkN+6UOHwYeJDM= +github.com/gostaticanalysis/analysisutil v0.0.2/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.7.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e h1:eMYU396eZUQ/ex49JNVJOEhShOhQe3Lf/opF61nFtlA= +github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05 h1:9cELXrXqZu2sczHBZHRpZ+84SR27+yXSKb1MBiUaPhA= +github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05/go.mod h1:zHtCks/HQvOt8ATyfwVe3JJq2PPuImzXINPRTC03+9w= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 h1:0XM1XL/OFFJjXsYXlG30spTkV/E9+gmd5GD1w2HE8xM= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/prometheus/tsdb v0.9.1/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= +github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs= +github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ= +github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822 h1:uVnVN3IUKAVcB3xG26bThgwXkWaGFc9i5qFHYKy4TKc= +github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/fasthttp v1.4.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 h1:1+FQ4Ns+UZtUiQ4lP0sTCyKSQ0EXoiwAdHZB0Pd5t9Q= +github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997/go.mod h1:DIGbh/f5XMAessMV/uaIik81gkDVjUeQ9ApdaU7wRKE= +github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583 h1:SZPG5w7Qxq7bMcMVl6e3Ht2X7f+AAGQdzjkbyOnNNZ8= +github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3 h1:f4/ZD59VsBOaJmWeI2yqtHvJhmRRPzi73C88ZtfhAIk= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190711165009-e47acb2ca7f9/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24 h1:nP0LlV1P7+z/qtbjHygz+Bba7QsbB4MqdhGJmAyicuI= +golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190506115046-ca7f33d4116e h1:bq5BY1tGuaK8HxuwN6pT6kWgTVLeJ5KwuyBpsl1CZL4= +golang.org/x/sys v0.0.0-20190506115046-ca7f33d4116e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d h1:JZPFnINSinLUdC0BJDoKhrky8niIzXMPIY2oR07+I+E= +golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +google.golang.org/api v0.0.0-20171116170945-8791354e7ab1 h1:g6iAMpIfX2EaDmaU3Nm8KcWAuf9yDiM3uE5a7/9gZao= +google.golang.org/api v0.0.0-20171116170945-8791354e7ab1/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/appengine v1.0.0 h1:dN4LljjBKVChsv0XCSI+zbyzdqrkEwX5LQFUMRSGqOc= +google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 h1:5pOB7se0B2+IssELuQUs6uoBgYJenkU2AQlvopc2sRw= +google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/fsnotify/fsnotify.v1 v1.2.11 h1:m6l7lekIm1I7UEqyXifLaHywMLxwlWea1o3zUGDGQHs= +gopkg.in/fsnotify/fsnotify.v1 v1.2.11/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3 h1:AFxeG48hTWHhDTQDk/m2gorfVHUEa9vo3tp3D7TzwjI= +gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.1.3 h1:/FoFBTvlJN6MTTVCe9plTOG+YydzkjvDGxiSPzIyoDM= +gopkg.in/square/go-jose.v2 v2.1.3/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= +mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g= +mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= +sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o= +sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4= diff --git a/htpasswd.go b/htpasswd.go index 0166e08..b7c8d57 100644 --- a/htpasswd.go +++ b/htpasswd.go @@ -7,7 +7,7 @@ import ( "io" "os" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" "golang.org/x/crypto/bcrypt" ) diff --git a/http.go b/http.go index 8ccc6f6..2cee227 100644 --- a/http.go +++ b/http.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" ) // Server represents an HTTP server diff --git a/http_test.go b/http_test.go index b365d6f..f5ee142 100644 --- a/http_test.go +++ b/http_test.go @@ -8,6 +8,9 @@ import ( "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")) @@ -16,8 +19,8 @@ func TestGCPHealthcheckLiveness(t *testing.T) { h := gcpHealthcheck(http.HandlerFunc(handler)) rw := httptest.NewRecorder() r, _ := http.NewRequest("GET", "/liveness_check", nil) - r.RemoteAddr = "127.0.0.1" - r.Host = "test-server" + r.RemoteAddr = localhost + r.Host = host h.ServeHTTP(rw, r) assert.Equal(t, 200, rw.Code) @@ -32,8 +35,8 @@ func TestGCPHealthcheckReadiness(t *testing.T) { h := gcpHealthcheck(http.HandlerFunc(handler)) rw := httptest.NewRecorder() r, _ := http.NewRequest("GET", "/readiness_check", nil) - r.RemoteAddr = "127.0.0.1" - r.Host = "test-server" + r.RemoteAddr = localhost + r.Host = host h.ServeHTTP(rw, r) assert.Equal(t, 200, rw.Code) @@ -48,8 +51,8 @@ func TestGCPHealthcheckNotHealthcheck(t *testing.T) { h := gcpHealthcheck(http.HandlerFunc(handler)) rw := httptest.NewRecorder() r, _ := http.NewRequest("GET", "/not_any_check", nil) - r.RemoteAddr = "127.0.0.1" - r.Host = "test-server" + r.RemoteAddr = localhost + r.Host = host h.ServeHTTP(rw, r) assert.Equal(t, "test", rw.Body.String()) @@ -63,8 +66,8 @@ func TestGCPHealthcheckIngress(t *testing.T) { h := gcpHealthcheck(http.HandlerFunc(handler)) rw := httptest.NewRecorder() r, _ := http.NewRequest("GET", "/", nil) - r.RemoteAddr = "127.0.0.1" - r.Host = "test-server" + r.RemoteAddr = localhost + r.Host = host r.Header.Set(userAgentHeader, googleHealthCheckUserAgent) h.ServeHTTP(rw, r) @@ -80,8 +83,8 @@ func TestGCPHealthcheckNotIngress(t *testing.T) { h := gcpHealthcheck(http.HandlerFunc(handler)) rw := httptest.NewRecorder() r, _ := http.NewRequest("GET", "/foo", nil) - r.RemoteAddr = "127.0.0.1" - r.Host = "test-server" + r.RemoteAddr = localhost + r.Host = host r.Header.Set(userAgentHeader, googleHealthCheckUserAgent) h.ServeHTTP(rw, r) @@ -96,8 +99,8 @@ func TestGCPHealthcheckNotIngressPut(t *testing.T) { h := gcpHealthcheck(http.HandlerFunc(handler)) rw := httptest.NewRecorder() r, _ := http.NewRequest("PUT", "/", nil) - r.RemoteAddr = "127.0.0.1" - r.Host = "test-server" + r.RemoteAddr = localhost + r.Host = host r.Header.Set(userAgentHeader, googleHealthCheckUserAgent) h.ServeHTTP(rw, r) diff --git a/logging_handler.go b/logging_handler.go index 77c2fca..9915e27 100644 --- a/logging_handler.go +++ b/logging_handler.go @@ -10,7 +10,7 @@ import ( "net/http" "time" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" ) // responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status @@ -75,18 +75,19 @@ func (l *responseLogger) Status() int { return l.status } -// Size returns teh response size +// Size returns the response size func (l *responseLogger) Size() int { return l.size } +// Flush sends any buffered data to the client func (l *responseLogger) Flush() { if flusher, ok := l.w.(http.Flusher); ok { flusher.Flush() } } -// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its friends +// loggingHandler is the http.Handler implementation for LoggingHandler type loggingHandler struct { handler http.Handler } diff --git a/logging_handler_test.go b/logging_handler_test.go index f92c7e0..ddc9778 100644 --- a/logging_handler_test.go +++ b/logging_handler_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" ) func TestLoggingHandler_ServeHTTP(t *testing.T) { @@ -17,10 +17,23 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) { tests := []struct { Format, - ExpectedLogMessage string + ExpectedLogMessage, + Path string + ExcludePaths []string + SilencePingLogging bool }{ - {logger.DefaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", logger.FormatTimestamp(ts))}, - {"{{.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{}, false}, + {logger.DefaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", logger.FormatTimestamp(ts)), "/foo/bar", []string{}, true}, + {logger.DefaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", logger.FormatTimestamp(ts)), "/foo/bar", []string{"/ping"}, false}, + {logger.DefaultRequestLoggingFormat, "", "/foo/bar", []string{"/foo/bar"}, false}, + {logger.DefaultRequestLoggingFormat, "", "/ping", []string{}, true}, + {logger.DefaultRequestLoggingFormat, "", "/ping", []string{"/ping"}, false}, + {logger.DefaultRequestLoggingFormat, "", "/ping", []string{"/ping"}, true}, + {logger.DefaultRequestLoggingFormat, "", "/ping", []string{"/foo/bar", "/ping"}, false}, + {"{{.RequestMethod}}", "GET\n", "/foo/bar", []string{}, true}, + {"{{.RequestMethod}}", "GET\n", "/foo/bar", []string{"/ping"}, false}, + {"{{.RequestMethod}}", "GET\n", "/ping", []string{}, false}, + {"{{.RequestMethod}}", "", "/ping", []string{"/ping"}, true}, } for _, test := range tests { @@ -36,9 +49,13 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) { logger.SetOutput(buf) 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", "/foo/bar", nil) + r, _ := http.NewRequest("GET", test.Path, nil) r.RemoteAddr = "127.0.0.1" r.Host = "test-server" diff --git a/main.go b/main.go index a74c245..a9f1e4a 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( "github.com/BurntSushi/toml" options "github.com/mreiferson/go-options" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" ) func main() { @@ -23,15 +23,17 @@ func main() { whitelistDomains := StringArray{} upstreams := StringArray{} skipAuthRegex := StringArray{} + jwtIssuers := StringArray{} googleGroups := StringArray{} + redisSentinelConnectionURLs := StringArray{} config := flagSet.String("config", "", "path to config file") showVersion := flagSet.Bool("version", false, "print version string") flagSet.String("http-address", "127.0.0.1:4180", "[http://]: or unix:// to listen on for HTTP clients") flagSet.String("https-address", ":443", ": to listen on for HTTPS clients") - flagSet.String("tls-cert", "", "path to certificate file") - flagSet.String("tls-key", "", "path to private key file") + flagSet.String("tls-cert-file", "", "path to certificate file") + flagSet.String("tls-key-file", "", "path to private key file") flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"") flagSet.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") @@ -45,14 +47,20 @@ func main() { 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-auth-preflight", false, "will skip authentication for OPTIONS requests") - flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS") + flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers") + flagSet.Bool("ssl-upstream-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS upstreams") flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses") + flagSet.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(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)") 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-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.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") @@ -62,8 +70,10 @@ func main() { flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption or \"htpasswd -B\" for bcrypt encryption") flagSet.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("banner", "", "custom banner string. Use \"-\" to disable default banner.") 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. //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") @@ -76,6 +86,10 @@ func main() { flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") flagSet.String("session-store-type", "cookie", "the session storage provider to use") + flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])") + flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature") + flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel") + flagSet.Var(&redisSentinelConnectionURLs, "redis-sentinel-connection-urls", "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel") flagSet.String("logging-filename", "", "File to log requests to, empty for stdout") flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation") @@ -89,12 +103,15 @@ func main() { 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("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") @@ -140,7 +157,13 @@ func main() { validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile) oauthproxy := NewOAuthProxy(opts, validator) - if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" { + if len(opts.Banner) >= 1 { + if opts.Banner == "-" { + oauthproxy.SignInMessage = "" + } else { + oauthproxy.SignInMessage = opts.Banner + } + } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" { if len(opts.EmailDomains) > 1 { oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", ")) } else if opts.EmailDomains[0] != "*" { diff --git a/oauthproxy.go b/oauthproxy.go index 389b2a9..2418e73 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -1,6 +1,8 @@ package main import ( + "context" + "crypto/tls" b64 "encoding/base64" "errors" "fmt" @@ -13,10 +15,11 @@ import ( "strings" "time" + "github.com/coreos/go-oidc" "github.com/mbland/hmacauth" - "github.com/pusher/oauth2_proxy/cookie" - "github.com/pusher/oauth2_proxy/logger" 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/providers" "github.com/yhat/wsutil" ) @@ -47,6 +50,11 @@ var SignatureHeaders = []string{ "Gap-Auth", } +var ( + // ErrNeedsLogin means the user should be redirected to the login page + ErrNeedsLogin = errors.New("redirect to login page") +) + // OAuthProxy is the main authentication proxy type OAuthProxy struct { CookieSeed string @@ -87,8 +95,11 @@ type OAuthProxy struct { PassAuthorization bool skipAuthRegex []string skipAuthPreflight bool + skipJwtBearerTokens bool + jwtBearerVerifiers []*oidc.IDTokenVerifier compiledRegex []*regexp.Regexp templates *template.Template + Banner string Footer string } @@ -118,9 +129,14 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // NewReverseProxy creates a new reverse proxy for proxying requests to upstream // servers -func NewReverseProxy(target *url.URL, flushInterval time.Duration) (proxy *httputil.ReverseProxy) { +func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) { proxy = httputil.NewSingleHostReverseProxy(target) - proxy.FlushInterval = flushInterval + proxy.FlushInterval = opts.FlushInterval + if opts.SSLUpstreamInsecureSkipVerify { + proxy.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } return proxy } @@ -151,9 +167,9 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) { } // NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url -func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) (restProxy http.Handler) { +func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler { u.Path = "" - proxy := NewReverseProxy(u, opts.FlushInterval) + proxy := NewReverseProxy(u, opts) if !opts.PassHostHeader { setProxyUpstreamHostHeader(proxy, u) } else { @@ -167,7 +183,12 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma wsURL := &url.URL{Scheme: wsScheme, Host: u.Host} wsProxy = wsutil.NewSingleHostReverseProxy(wsURL) } - return &UpstreamProxy{u.Host, proxy, wsProxy, auth} + return &UpstreamProxy{ + upstream: u.Host, + handler: proxy, + wsHandler: wsProxy, + auth: auth, + } } // NewOAuthProxy creates a new instance of OOuthProxy from the options provided @@ -192,7 +213,13 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { } logger.Printf("mapping path %q => file system %q", path, u.Path) proxy := NewFileServer(path, u.Path) - serveMux.Handle(path, &UpstreamProxy{path, proxy, nil, nil}) + uProxy := UpstreamProxy{ + upstream: path, + handler: proxy, + wsHandler: nil, + auth: nil, + } + serveMux.Handle(path, &uProxy) default: panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme)) } @@ -201,6 +228,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { logger.Printf("compiled skip-auth-regex => %q", u) } + if opts.SkipJwtBearerTokens { + logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL) + for _, issuer := range opts.ExtraJwtIssuers { + logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer) + } + } redirectURL := opts.redirectURL if redirectURL.Path == "" { redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) @@ -227,32 +260,35 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { Validator: validator, RobotsPath: "/robots.txt", - PingPath: "/ping", + PingPath: opts.PingPath, SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix), OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix), OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix), AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix), - ProxyPrefix: opts.ProxyPrefix, - provider: opts.provider, - sessionStore: opts.sessionStore, - serveMux: serveMux, - redirectURL: redirectURL, - whitelistDomains: opts.WhitelistDomains, - skipAuthRegex: opts.SkipAuthRegex, - skipAuthPreflight: opts.SkipAuthPreflight, - compiledRegex: opts.CompiledRegex, - SetXAuthRequest: opts.SetXAuthRequest, - PassBasicAuth: opts.PassBasicAuth, - PassUserHeaders: opts.PassUserHeaders, - BasicAuthPassword: opts.BasicAuthPassword, - PassAccessToken: opts.PassAccessToken, - SetAuthorization: opts.SetAuthorization, - PassAuthorization: opts.PassAuthorization, - SkipProviderButton: opts.SkipProviderButton, - templates: loadTemplates(opts.CustomTemplatesDir), - Footer: opts.Footer, + ProxyPrefix: opts.ProxyPrefix, + provider: opts.provider, + sessionStore: opts.sessionStore, + serveMux: serveMux, + redirectURL: redirectURL, + whitelistDomains: opts.WhitelistDomains, + skipAuthRegex: opts.SkipAuthRegex, + skipAuthPreflight: opts.SkipAuthPreflight, + skipJwtBearerTokens: opts.SkipJwtBearerTokens, + jwtBearerVerifiers: opts.jwtBearerVerifiers, + compiledRegex: opts.CompiledRegex, + SetXAuthRequest: opts.SetXAuthRequest, + PassBasicAuth: opts.PassBasicAuth, + PassUserHeaders: opts.PassUserHeaders, + BasicAuthPassword: opts.BasicAuthPassword, + PassAccessToken: opts.PassAccessToken, + SetAuthorization: opts.SetAuthorization, + PassAuthorization: opts.PassAuthorization, + SkipProviderButton: opts.SkipProviderButton, + templates: loadTemplates(opts.CustomTemplatesDir), + Banner: opts.Banner, + Footer: opts.Footer, } } @@ -477,20 +513,19 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool { } // IsWhitelistedRequest is used to check if auth should be skipped for this request -func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) { +func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool { isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) } // IsWhitelistedPath is used to check if the request path is allowed without auth -func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) { +func (p *OAuthProxy) IsWhitelistedPath(path string) bool { for _, u := range p.compiledRegex { - ok = u.MatchString(path) - if ok { - return + if u.MatchString(path) { + return true } } - return + return false } func getRemoteAddr(req *http.Request) (s string) { @@ -555,7 +590,7 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { // OAuthStart starts the OAuth2 authentication flow func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { - nonce, err := cookie.Nonce() + nonce, err := encryption.Nonce() if err != nil { logger.Printf("Error obtaining nonce: %s", err.Error()) p.ErrorPage(rw, 500, "Internal Error", err.Error()) @@ -634,63 +669,97 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { } http.Redirect(rw, req, redirect, 302) } else { - logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Invalid authentication via OAuth2: unauthorized") + logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized") p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account") } } // AuthenticateOnly checks whether the user is currently logged in func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) { - status := p.Authenticate(rw, req) - if status == http.StatusAccepted { - rw.WriteHeader(http.StatusAccepted) - } else { + session, err := p.getAuthenticatedSession(rw, req) + if err != nil { http.Error(rw, "unauthorized request", http.StatusUnauthorized) + return } + + // we are authenticated + p.addHeadersForProxying(rw, req, session) + rw.WriteHeader(http.StatusAccepted) } // Proxy proxies the user request if the user is authenticated else it prompts // them to authenticate func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { - status := p.Authenticate(rw, req) - if status == http.StatusInternalServerError { - p.ErrorPage(rw, http.StatusInternalServerError, - "Internal Error", "Internal Error") - } else if status == http.StatusForbidden { + session, err := p.getAuthenticatedSession(rw, req) + switch err { + case nil: + // we are authenticated + p.addHeadersForProxying(rw, req, session) + p.serveMux.ServeHTTP(rw, req) + + case ErrNeedsLogin: + // we need to send the user to a login screen + if isAjax(req) { + // no point redirecting an AJAX request + p.ErrorJSON(rw, http.StatusUnauthorized) + return + } + if p.SkipProviderButton { p.OAuthStart(rw, req) } else { p.SignInPage(rw, req, http.StatusForbidden) } - } else if status == http.StatusUnauthorized { - p.ErrorJSON(rw, status) - } else { - p.serveMux.ServeHTTP(rw, req) + + default: + // unknown error + logger.Printf("Unexpected internal error: %s", err) + p.ErrorPage(rw, http.StatusInternalServerError, + "Internal Error", "Internal Error") } + } -// Authenticate checks whether a user is authenticated -func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int { +// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so +// Returns nil, ErrNeedsLogin if user needs to login. +// Set-Cookie headers may be set on the response as a side-effect of calling this method. +func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) { + var session *sessionsapi.SessionState + var err error var saveSession, clearSession, revalidated bool + + if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" { + session, err = p.GetJwtSession(req) + if err != nil { + logger.Printf("Error retrieving session from token in Authorization header: %s", err) + } + if session != nil { + saveSession = false + } + } + remoteAddr := getRemoteAddr(req) + if session == nil { + session, err = p.LoadCookiedSession(req) + if err != nil { + logger.Printf("Error loading cookied session: %s", err) + } - session, err := p.LoadCookiedSession(req) - if err != nil { - logger.Printf("Error loading cookied session: %s", err) - } - if session != nil && session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) { - logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh) - saveSession = true - } + if session != nil { + if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) { + logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh) + saveSession = true + } - var ok bool - if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil { - logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session) - clearSession = true - session = nil - } else if ok { - saveSession = true - revalidated = true + if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil { + logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session) + clearSession = true + session = nil + } else if ok { + saveSession = true + revalidated = true + } + } } if session != nil && session.IsExpired() { @@ -709,18 +778,20 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int } } - if session != nil && session.Email != "" && !p.Validator(session.Email) { - logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session) - session = nil - saveSession = false - clearSession = true + if session != nil && session.Email != "" { + if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) { + logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session) + session = nil + saveSession = false + clearSession = true + } } if saveSession && session != nil { err = p.SaveSession(rw, req, session) if err != nil { logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err) - return http.StatusInternalServerError + return nil, err } } @@ -736,52 +807,78 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int } if session == nil { - // Check if is an ajax request and return unauthorized to avoid a redirect - // to the login page - if p.isAjax(req) { - return http.StatusUnauthorized - } - return http.StatusForbidden + return nil, ErrNeedsLogin } - // At this point, the user is authenticated. proxy normally + return session, nil +} + +// addHeadersForProxying adds the appropriate headers the request / response for proxying +func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) { if p.PassBasicAuth { req.SetBasicAuth(session.User, p.BasicAuthPassword) req.Header["X-Forwarded-User"] = []string{session.User} if session.Email != "" { req.Header["X-Forwarded-Email"] = []string{session.Email} + } else { + req.Header.Del("X-Forwarded-Email") } } + if p.PassUserHeaders { req.Header["X-Forwarded-User"] = []string{session.User} if session.Email != "" { req.Header["X-Forwarded-Email"] = []string{session.Email} + } else { + req.Header.Del("X-Forwarded-Email") } } + if p.SetXAuthRequest { rw.Header().Set("X-Auth-Request-User", session.User) if session.Email != "" { rw.Header().Set("X-Auth-Request-Email", session.Email) + } else { + rw.Header().Del("X-Auth-Request-Email") } - if p.PassAccessToken && session.AccessToken != "" { - rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken) + + if p.PassAccessToken { + if session.AccessToken != "" { + rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken) + } else { + rw.Header().Del("X-Auth-Request-Access-Token") + } } } - if p.PassAccessToken && session.AccessToken != "" { - req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken} + + if p.PassAccessToken { + if session.AccessToken != "" { + req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken} + } else { + req.Header.Del("X-Forwarded-Access-Token") + } } - if p.PassAuthorization && session.IDToken != "" { - req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)} + + if p.PassAuthorization { + if session.IDToken != "" { + req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)} + } else { + req.Header.Del("Authorization") + } } - if p.SetAuthorization && session.IDToken != "" { - rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken)) + if p.SetAuthorization { + if session.IDToken != "" { + rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken)) + } else { + rw.Header().Del("Authorization") + } } + if session.Email == "" { rw.Header().Set("GAP-Auth", session.User) } else { rw.Header().Set("GAP-Auth", session.Email) } - return http.StatusAccepted } // CheckBasicAuth checks the requests Authorization header for basic auth @@ -815,7 +912,7 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionStat } // isAjax checks if a request is an ajax request -func (p *OAuthProxy) isAjax(req *http.Request) bool { +func isAjax(req *http.Request) bool { acceptValues, ok := req.Header["accept"] if !ok { acceptValues = req.Header["Accept"] @@ -829,8 +926,97 @@ func (p *OAuthProxy) isAjax(req *http.Request) bool { return false } -// ErrorJSON returns the error code witht an application/json mime type +// ErrorJSON returns the error code with an application/json mime type func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) { rw.Header().Set("Content-Type", applicationJSON) rw.WriteHeader(code) } + +// GetJwtSession loads a session based on a JWT token in the authorization header. +func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) { + rawBearerToken, err := p.findBearerToken(req) + if err != nil { + return nil, err + } + + ctx := context.Background() + var session *sessionsapi.SessionState + for _, verifier := range p.jwtBearerVerifiers { + bearerToken, err := verifier.Verify(ctx, rawBearerToken) + + if err != nil { + logger.Printf("failed to verify bearer token: %v", err) + continue + } + + var claims struct { + Subject string `json:"sub"` + Email string `json:"email"` + Verified *bool `json:"email_verified"` + } + + if err := bearerToken.Claims(&claims); err != nil { + return nil, fmt.Errorf("failed to parse bearer token claims: %v", err) + } + + if claims.Email == "" { + claims.Email = claims.Subject + } + + if claims.Verified != nil && !*claims.Verified { + return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email) + } + + session = &sessionsapi.SessionState{ + AccessToken: rawBearerToken, + IDToken: rawBearerToken, + RefreshToken: "", + ExpiresOn: bearerToken.Expiry, + Email: claims.Email, + User: claims.Email, + } + return session, nil + } + return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization")) +} + +// findBearerToken finds a valid JWT token from the Authorization header of a given request. +func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) { + auth := req.Header.Get("Authorization") + s := strings.SplitN(auth, " ", 2) + if len(s) != 2 { + return "", fmt.Errorf("invalid authorization header %s", auth) + } + jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`) + var rawBearerToken string + if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) { + rawBearerToken = s[1] + } else if s[0] == "Basic" { + // Check if we have a Bearer token masquerading in Basic + b, err := b64.StdEncoding.DecodeString(s[1]) + if err != nil { + return "", err + } + pair := strings.SplitN(string(b), ":", 2) + if len(pair) != 2 { + return "", fmt.Errorf("invalid format %s", b) + } + user, password := pair[0], pair[1] + + // check user, user+password, or just password for a token + if jwtRegex.MatchString(user) { + // Support blank passwords or magic `x-oauth-basic` passwords - nothing else + if password == "" || password == "x-oauth-basic" { + rawBearerToken = user + } + } else if jwtRegex.MatchString(password) { + // support passwords and ignore user + rawBearerToken = password + } + } + if rawBearerToken == "" { + return "", fmt.Errorf("no valid bearer token found in authorization header") + } + + return rawBearerToken, nil +} diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 1d09bbb..8dd3adf 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -1,8 +1,10 @@ package main import ( + "context" "crypto" "encoding/base64" + "fmt" "io" "io/ioutil" "net" @@ -14,9 +16,10 @@ import ( "testing" "time" + "github.com/coreos/go-oidc" "github.com/mbland/hmacauth" - "github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/logger" "github.com/pusher/oauth2_proxy/pkg/sessions/cookie" "github.com/pusher/oauth2_proxy/providers" "github.com/stretchr/testify/assert" @@ -119,7 +122,7 @@ func TestNewReverseProxy(t *testing.T) { backendHost := net.JoinHostPort(backendHostname, backendPort) proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/") - proxyHandler := NewReverseProxy(proxyURL, time.Second) + proxyHandler := NewReverseProxy(proxyURL, &Options{FlushInterval: time.Second}) setProxyUpstreamHostHeader(proxyHandler, proxyURL) frontend := httptest.NewServer(proxyHandler) defer frontend.Close() @@ -141,7 +144,7 @@ func TestEncodedSlashes(t *testing.T) { defer backend.Close() b, _ := url.Parse(backend.URL) - proxyHandler := NewReverseProxy(b, time.Second) + proxyHandler := NewReverseProxy(b, &Options{FlushInterval: time.Second}) setProxyDirector(proxyHandler) frontend := httptest.NewServer(proxyHandler) defer frontend.Close() @@ -160,9 +163,9 @@ func TestEncodedSlashes(t *testing.T) { func TestRobotsTxt(t *testing.T) { opts := NewOptions() - opts.ClientID = "bazquux" - opts.ClientSecret = "foobar" - opts.CookieSecret = "xyzzyplugh" + opts.ClientID = "asdlkjx" + opts.ClientSecret = "alkgks" + opts.CookieSecret = "asdkugkj" opts.Validate() proxy := NewOAuthProxy(opts, func(string) bool { return true }) @@ -175,9 +178,9 @@ func TestRobotsTxt(t *testing.T) { func TestIsValidRedirect(t *testing.T) { opts := NewOptions() - opts.ClientID = "bazquux" - opts.ClientSecret = "foobar" - opts.CookieSecret = "xyzzyplugh" + opts.ClientID = "skdlfj" + opts.ClientSecret = "fgkdsgj" + opts.CookieSecret = "ljgiogbj" // Should match domains that are exactly foo.bar and any subdomain of bar.foo opts.WhitelistDomains = []string{"foo.bar", ".bar.foo"} opts.Validate() @@ -226,8 +229,9 @@ func TestIsValidRedirect(t *testing.T) { type TestProvider struct { *providers.ProviderData - EmailAddress string - ValidToken bool + EmailAddress string + ValidToken bool + GroupValidator func(string) bool } func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider { @@ -252,6 +256,9 @@ func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider { Scope: "profile.email", }, EmailAddress: emailAddress, + GroupValidator: func(s string) bool { + return true + }, } } @@ -263,6 +270,13 @@ func (tp *TestProvider) ValidateSessionState(session *sessions.SessionState) boo return tp.ValidToken } +func (tp *TestProvider) ValidateGroup(email string) bool { + if tp.GroupValidator != nil { + return tp.GroupValidator(email) + } + return true +} + func TestBasicAuthPassword(t *testing.T) { providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger.Printf("%#v", r) @@ -284,8 +298,8 @@ func TestBasicAuthPassword(t *testing.T) { // The CookieSecret must be 32 bytes in order to create the AES // cipher. opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" - opts.ClientID = "bazquux" - opts.ClientSecret = "foobar" + opts.ClientID = "dlgkj" + opts.ClientSecret = "alkgret" opts.CookieSecure = false opts.PassBasicAuth = true opts.PassUserHeaders = true @@ -378,8 +392,8 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes // The CookieSecret must be 32 bytes in order to create the AES // cipher. t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" - t.opts.ClientID = "bazquux" - t.opts.ClientSecret = "foobar" + t.opts.ClientID = "slgkj" + t.opts.ClientSecret = "gfjgojl" t.opts.CookieSecure = false t.opts.PassAccessToken = opts.PassAccessToken t.opts.Validate() @@ -504,9 +518,9 @@ func NewSignInPageTest(skipProvider bool) *SignInPageTest { var sipTest SignInPageTest sipTest.opts = NewOptions() - sipTest.opts.CookieSecret = "foobar" - sipTest.opts.ClientID = "bazquux" - sipTest.opts.ClientSecret = "xyzzyplugh" + sipTest.opts.CookieSecret = "adklsj2" + sipTest.opts.ClientID = "lkdgj" + sipTest.opts.ClientSecret = "sgiufgoi" sipTest.opts.SkipProviderButton = skipProvider sipTest.opts.Validate() @@ -610,8 +624,8 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi for _, modifier := range modifiers { modifier(pcTest.opts) } - pcTest.opts.ClientID = "bazquux" - pcTest.opts.ClientSecret = "xyzzyplugh" + pcTest.opts.ClientID = "asdfljk" + pcTest.opts.ClientSecret = "lkjfdsig" pcTest.opts.CookieSecret = "0123456789abcdefabcd" // First, set the CookieRefresh option so proxy.AesCipher is created, // needed to encrypt the access_token. @@ -788,6 +802,25 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) { assert.Equal(t, "unauthorized request\n", string(bodyBytes)) } +func TestAuthOnlyEndpointUnauthorizedOnProviderGroupValidationFailure(t *testing.T) { + test := NewAuthOnlyEndpointTest() + startSession := &sessions.SessionState{ + Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()} + test.SaveSession(startSession) + provider := &TestProvider{ + ValidToken: true, + GroupValidator: func(s string) bool { + return false + }, + } + + test.proxy.provider = provider + test.proxy.ServeHTTP(test.rw, test.req) + assert.Equal(t, http.StatusUnauthorized, test.rw.Code) + bodyBytes, _ := ioutil.ReadAll(test.rw.Body) + assert.Equal(t, "unauthorized request\n", string(bodyBytes)) +} + func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { var pcTest ProcessCookieTest @@ -827,9 +860,9 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) { opts := NewOptions() opts.Upstreams = append(opts.Upstreams, upstream.URL) - opts.ClientID = "bazquux" - opts.ClientSecret = "foobar" - opts.CookieSecret = "xyzzyplugh" + opts.ClientID = "aljsal" + opts.ClientSecret = "jglkfsdgj" + opts.CookieSecret = "dkfjgdls" opts.SkipAuthPreflight = true opts.Validate() @@ -966,8 +999,8 @@ func TestNoRequestSignature(t *testing.T) { func TestRequestSignatureGetRequest(t *testing.T) { st := NewSignatureTest() defer st.Close() - st.opts.SignatureKey = "sha1:foobar" - st.MakeRequestWithExpectedKey("GET", "", "foobar") + st.opts.SignatureKey = "sha1:7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d" + st.MakeRequestWithExpectedKey("GET", "", "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d") assert.Equal(t, 200, st.rw.Code) assert.Equal(t, st.rw.Body.String(), "signatures match") } @@ -975,9 +1008,9 @@ func TestRequestSignatureGetRequest(t *testing.T) { func TestRequestSignaturePostRequest(t *testing.T) { st := NewSignatureTest() defer st.Close() - st.opts.SignatureKey = "sha1:foobar" + st.opts.SignatureKey = "sha1:d90df39e2d19282840252612dd7c81421a372f61" payload := `{ "hello": "world!" }` - st.MakeRequestWithExpectedKey("POST", payload, "foobar") + st.MakeRequestWithExpectedKey("POST", payload, "d90df39e2d19282840252612dd7c81421a372f61") assert.Equal(t, 200, st.rw.Code) assert.Equal(t, st.rw.Body.String(), "signatures match") } @@ -1023,9 +1056,9 @@ type ajaxRequestTest struct { func newAjaxRequestTest() *ajaxRequestTest { test := &ajaxRequestTest{} test.opts = NewOptions() - test.opts.CookieSecret = "foobar" - test.opts.ClientID = "bazquux" - test.opts.ClientSecret = "xyzzyplugh" + test.opts.CookieSecret = "sdflsw" + test.opts.ClientID = "gkljfdl" + test.opts.ClientSecret = "sdflkjs" test.opts.Validate() test.proxy = NewOAuthProxy(test.opts, func(email string) bool { return true @@ -1132,3 +1165,173 @@ func TestClearSingleCookie(t *testing.T) { assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries") } + +type NoOpKeySet struct { +} + +func (NoOpKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) { + splitStrings := strings.Split(jwt, ".") + payloadString := splitStrings[1] + jsonString, err := base64.RawURLEncoding.DecodeString(payloadString) + return []byte(jsonString), err +} + +func TestGetJwtSession(t *testing.T) { + /* token payload: + { + "sub": "1234567890", + "aud": "https://test.myapp.com", + "name": "John Doe", + "email": "john@example.com", + "iss": "https://issuer.example.com", + "iat": 1553691215, + "exp": 1912151821 + } + */ + goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" + + "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" + + "E1LCJleHAiOjE5MTIxNTE4MjF9." + + "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" + + "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8" + + keyset := NoOpKeySet{} + verifier := oidc.NewVerifier("https://issuer.example.com", keyset, + &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true}) + + test := NewAuthOnlyEndpointTest(func(opts *Options) { + opts.PassAuthorization = true + opts.SetAuthorization = true + opts.SetXAuthRequest = true + opts.SkipJwtBearerTokens = true + opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier) + }) + tp, _ := test.proxy.provider.(*TestProvider) + tp.GroupValidator = func(s string) bool { + return true + } + + authHeader := fmt.Sprintf("Bearer %s", goodJwt) + test.req.Header = map[string][]string{ + "Authorization": {authHeader}, + } + + // Bearer + session, _ := test.proxy.GetJwtSession(test.req) + assert.Equal(t, session.User, "john@example.com") + assert.Equal(t, session.Email, "john@example.com") + assert.Equal(t, session.ExpiresOn, time.Unix(1912151821, 0)) + assert.Equal(t, session.IDToken, goodJwt) + + test.proxy.ServeHTTP(test.rw, test.req) + if test.rw.Code >= 400 { + t.Fatalf("expected 3xx got %d", test.rw.Code) + } + + // Check PassAuthorization, should overwrite Basic header + assert.Equal(t, test.req.Header.Get("Authorization"), authHeader) + assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "john@example.com") + assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com") + + // SetAuthorization and SetXAuthRequest + assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader) + assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "john@example.com") + assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com") +} + +func TestJwtUnauthorizedOnGroupValidationFailure(t *testing.T) { + goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" + + "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" + + "E1LCJleHAiOjE5MTIxNTE4MjF9." + + "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" + + "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8" + + keyset := NoOpKeySet{} + verifier := oidc.NewVerifier("https://issuer.example.com", keyset, + &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true}) + + test := NewAuthOnlyEndpointTest(func(opts *Options) { + opts.PassAuthorization = true + opts.SetAuthorization = true + opts.SetXAuthRequest = true + opts.SkipJwtBearerTokens = true + opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier) + }) + tp, _ := test.proxy.provider.(*TestProvider) + // Verify ValidateGroup fails JWT authorization + tp.GroupValidator = func(s string) bool { + return false + } + + authHeader := fmt.Sprintf("Bearer %s", goodJwt) + test.req.Header = map[string][]string{ + "Authorization": {authHeader}, + } + test.proxy.ServeHTTP(test.rw, test.req) + if test.rw.Code != http.StatusUnauthorized { + t.Fatalf("expected 401 got %d", test.rw.Code) + } +} + +func TestFindJwtBearerToken(t *testing.T) { + p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"} + getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}} + + validToken := "eyJfoobar.eyJfoobar.12345asdf" + var token string + + // Bearer + getReq.Header = map[string][]string{ + "Authorization": {fmt.Sprintf("Bearer %s", validToken)}, + } + + token, _ = p.findBearerToken(getReq) + assert.Equal(t, validToken, token) + + // Basic - no password + getReq.SetBasicAuth(token, "") + token, _ = p.findBearerToken(getReq) + assert.Equal(t, validToken, token) + + // Basic - sentinel password + getReq.SetBasicAuth(token, "x-oauth-basic") + token, _ = p.findBearerToken(getReq) + assert.Equal(t, validToken, token) + + // Basic - any username, password matching jwt pattern + getReq.SetBasicAuth("any-username-you-could-wish-for", token) + token, _ = p.findBearerToken(getReq) + assert.Equal(t, validToken, token) + + failures := []string{ + // Too many parts + "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA.dGVzdA", + // Not enough parts + "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA", + // Invalid encrypted key + "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.//////.dGVzdA.dGVzdA.dGVzdA", + // Invalid IV + "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.//////.dGVzdA.dGVzdA", + // Invalid ciphertext + "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.//////.dGVzdA", + // Invalid tag + "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.//////", + // Invalid header + "W10.dGVzdA.dGVzdA.dGVzdA.dGVzdA", + // Invalid header + "######.dGVzdA.dGVzdA.dGVzdA.dGVzdA", + // Missing alc/enc params + "e30.dGVzdA.dGVzdA.dGVzdA.dGVzdA", + } + + for _, failure := range failures { + getReq.Header = map[string][]string{ + "Authorization": {fmt.Sprintf("Bearer %s", failure)}, + } + _, err := p.findBearerToken(getReq) + assert.Error(t, err) + } + + fmt.Printf("%s", token) +} diff --git a/options.go b/options.go index 0460bce..706f6d5 100644 --- a/options.go +++ b/options.go @@ -17,10 +17,10 @@ import ( oidc "github.com/coreos/go-oidc" "github.com/dgrijalva/jwt-go" "github.com/mbland/hmacauth" - "github.com/pusher/oauth2_proxy/cookie" - "github.com/pusher/oauth2_proxy/logger" "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" @@ -29,28 +29,33 @@ import ( // Options holds Configuration Options that can be set by Command Line Flag, // or Config File type Options struct { - ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy-prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"` + ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"` + PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"` ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"` HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"` HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"` 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"` ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"` - TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"` - TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` + TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"` + TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"` AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"` + BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"` + BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"` EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"` 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"` GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"` + GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group" env:"OAUTH2_PROXY_GITLAB_GROUP"` GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"` GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"` GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"` HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"` DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"` CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"` + Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"` Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"` // Embed CookieOptions @@ -59,64 +64,70 @@ type Options struct { // Embed SessionOptions options.SessionOptions - Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"` - SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"` - PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"` - BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"` - PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"` - PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"` - SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"` - PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"` - SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_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"` + Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"` + SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"` + SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"` + ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"` + PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"` + BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"` + PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"` + PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"` + SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"` + PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"` + 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 // potential overrides. - Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"` - OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"` - SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_SKIP_OIDC_DISCOVERY"` - OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_OIDC_JWKS_URL"` - LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"` - RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"` - ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"` - ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"` - ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"` - Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"` - ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` + Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"` + OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"` + InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"` + SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_PROXY_SKIP_OIDC_DISCOVERY"` + OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_PROXY_OIDC_JWKS_URL"` + LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"` + RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"` + ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"` + ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"` + ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"` + Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"` + ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` // Configuration values for logging - LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_LOGGING_FILENAME"` - LoggingMaxSize int `flag:"logging-max-size" cfg:"logging_max_size" env:"OAUTH2_LOGGING_MAX_SIZE"` - LoggingMaxAge int `flag:"logging-max-age" cfg:"logging_max_age" env:"OAUTH2_LOGGING_MAX_AGE"` - LoggingMaxBackups int `flag:"logging-max-backups" cfg:"logging_max_backups" env:"OAUTH2_LOGGING_MAX_BACKUPS"` - LoggingLocalTime bool `flag:"logging-local-time" cfg:"logging_local_time" env:"OAUTH2_LOGGING_LOCAL_TIME"` - LoggingCompress bool `flag:"logging-compress" cfg:"logging_compress" env:"OAUTH2_LOGGING_COMPRESS"` - StandardLogging bool `flag:"standard-logging" cfg:"standard_logging" env:"OAUTH2_STANDARD_LOGGING"` - StandardLoggingFormat string `flag:"standard-logging-format" cfg:"standard_logging_format" env:"OAUTH2_STANDARD_LOGGING_FORMAT"` - RequestLogging bool `flag:"request-logging" cfg:"request_logging" env:"OAUTH2_REQUEST_LOGGING"` - RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format" env:"OAUTH2_REQUEST_LOGGING_FORMAT"` - AuthLogging bool `flag:"auth-logging" cfg:"auth_logging" env:"OAUTH2_LOGGING_AUTH_LOGGING"` - AuthLoggingFormat string `flag:"auth-logging-format" cfg:"auth_logging_format" env:"OAUTH2_AUTH_LOGGING_FORMAT"` - - 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"` + 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"` + 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 - redirectURL *url.URL - proxyURLs []*url.URL - CompiledRegex []*regexp.Regexp - provider providers.Provider - sessionStore sessionsapi.SessionStore - signatureData *SignatureData - oidcVerifier *oidc.IDTokenVerifier + redirectURL *url.URL + proxyURLs []*url.URL + CompiledRegex []*regexp.Regexp + provider providers.Provider + sessionStore sessionsapi.SessionStore + signatureData *SignatureData + oidcVerifier *oidc.IDTokenVerifier + jwtBearerVerifiers []*oidc.IDTokenVerifier } // SignatureData holds hmacauth signature hash and key @@ -129,6 +140,7 @@ type SignatureData struct { func NewOptions() *Options { return &Options{ ProxyPrefix: "/oauth2", + PingPath: "/ping", ProxyWebSockets: true, HTTPAddress: "127.0.0.1:4180", HTTPSAddress: ":443", @@ -143,31 +155,40 @@ func NewOptions() *Options { SessionOptions: options.SessionOptions{ Type: "cookie", }, - SetXAuthRequest: false, - SkipAuthPreflight: false, - PassBasicAuth: true, - PassUserHeaders: true, - PassAccessToken: false, - PassHostHeader: true, - SetAuthorization: false, - PassAuthorization: false, - ApprovalPrompt: "force", - SkipOIDCDiscovery: false, - LoggingFilename: "", - LoggingMaxSize: 100, - LoggingMaxAge: 7, - LoggingMaxBackups: 0, - LoggingLocalTime: true, - LoggingCompress: false, - StandardLogging: true, - StandardLoggingFormat: logger.DefaultStandardLoggingFormat, - RequestLogging: true, - RequestLoggingFormat: logger.DefaultRequestLoggingFormat, - AuthLogging: true, - AuthLoggingFormat: logger.DefaultAuthLoggingFormat, + SetXAuthRequest: false, + SkipAuthPreflight: false, + PassBasicAuth: true, + PassUserHeaders: true, + PassAccessToken: false, + PassHostHeader: true, + SetAuthorization: false, + PassAuthorization: false, + 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, + RequestLoggingFormat: logger.DefaultRequestLoggingFormat, + AuthLogging: true, + AuthLoggingFormat: logger.DefaultAuthLoggingFormat, } } +// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier. +type jwtIssuer struct { + issuerURI string + audience string +} + func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) { parsed, err := url.Parse(toParse) if err != nil { @@ -244,6 +265,25 @@ func (o *Options) Validate() error { } } + if o.SkipJwtBearerTokens { + // If we are using an oidc provider, go ahead and add that provider to the list + if o.oidcVerifier != nil { + o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier) + } + // Configure extra issuers + if len(o.ExtraJwtIssuers) > 0 { + var jwtIssuers []jwtIssuer + jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs) + for _, jwtIssuer := range jwtIssuers { + verifier, err := newVerifierFromJwtIssuer(jwtIssuer) + if err != nil { + msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err)) + } + o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier) + } + } + } + o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs) for _, u := range o.Upstreams { @@ -268,7 +308,7 @@ func (o *Options) Validate() error { } msgs = parseProviderInfo(o, msgs) - var cipher *cookie.Cipher + var cipher *encryption.Cipher if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.CookieRefresh != time.Duration(0)) { validCookieSecretSize := false for _, i := range []int{16, 24, 32} { @@ -293,7 +333,7 @@ func (o *Options) Validate() error { len(secretBytes(o.CookieSecret)), suffix)) } else { var err error - cipher, err = cookie.NewCipher(secretBytes(o.CookieSecret)) + cipher, err = encryption.NewCipher(secretBytes(o.CookieSecret)) if err != nil { msgs = append(msgs, fmt.Sprintf("cookie-secret error: %v", err)) } @@ -367,12 +407,39 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file) } } + case *providers.BitbucketProvider: + p.SetTeam(o.BitbucketTeam) + p.SetRepository(o.BitbucketRepository) case *providers.OIDCProvider: + p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail if o.oidcVerifier == nil { msgs = append(msgs, "oidc provider requires an oidc issuer URL") } else { 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) @@ -426,10 +493,49 @@ func parseSignatureKey(o *Options, msgs []string) []string { return append(msgs, "unsupported signature hash algorithm: "+ o.SignatureKey) } - o.signatureData = &SignatureData{hash, secretKey} + 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 { + verifier = provider.Verifier(config) + } + return verifier, nil +} + func validateCookieName(o *Options, msgs []string) []string { cookie := &http.Cookie{Name: o.CookieName} if cookie.String() == "" { @@ -500,6 +606,14 @@ func setupLogger(o *Options, msgs []string) []string { 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) } diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go index 7d33f14..c96d490 100644 --- a/pkg/apis/options/sessions.go +++ b/pkg/apis/options/sessions.go @@ -1,14 +1,13 @@ package options -import ( - "github.com/pusher/oauth2_proxy/cookie" -) +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 *cookie.Cipher + Cipher *encryption.Cipher CookieStoreOptions + RedisStoreOptions } // CookieSessionStoreType is used to indicate the CookieSessionStore should be @@ -17,3 +16,15 @@ 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"` +} diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index e085976..84c0dc9 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/pusher/oauth2_proxy/cookie" + "github.com/pusher/oauth2_proxy/pkg/encryption" ) // SessionState is used to store information about the currently authenticated user session @@ -66,7 +66,7 @@ func (s *SessionState) String() string { } // EncodeSessionState returns string representation of the current session -func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) { +func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) { var ss SessionState if c == nil { // Store only Email and User when cipher is unavailable @@ -133,7 +133,7 @@ func legacyDecodeSessionStatePlain(v string) (*SessionState, error) { // legacyDecodeSessionState attempts to decode the session state string // generated by v3.1.0 or older -func legacyDecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) { +func legacyDecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { chunks := strings.Split(v, "|") if c == nil { @@ -176,7 +176,7 @@ func legacyDecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) } // DecodeSessionState decodes the session cookie string into a SessionState -func DecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) { +func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { var ssj SessionStateJSON var ss *SessionState err := json.Unmarshal([]byte(v), &ssj) @@ -203,14 +203,14 @@ func DecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) { User: ss.User, } } else { - // Backward compatibility with using unecrypted Email + // 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 unecrypted User + // Backward compatibility with using unencrypted User if ss.User != "" { decryptedUser, errUser := c.Decrypt(ss.User) if errUser == nil { diff --git a/pkg/apis/sessions/session_state_test.go b/pkg/apis/sessions/session_state_test.go index a48344e..c8ccff1 100644 --- a/pkg/apis/sessions/session_state_test.go +++ b/pkg/apis/sessions/session_state_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/pusher/oauth2_proxy/cookie" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/encryption" "github.com/stretchr/testify/assert" ) @@ -14,9 +14,9 @@ const secret = "0123456789abcdefghijklmnopqrstuv" const altSecret = "0000000000abcdefghijklmnopqrstuv" func TestSessionStateSerialization(t *testing.T) { - c, err := cookie.NewCipher([]byte(secret)) + c, err := encryption.NewCipher([]byte(secret)) assert.Equal(t, nil, err) - c2, err := cookie.NewCipher([]byte(altSecret)) + c2, err := encryption.NewCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &sessions.SessionState{ Email: "user@domain.com", @@ -54,9 +54,9 @@ func TestSessionStateSerialization(t *testing.T) { } func TestSessionStateSerializationWithUser(t *testing.T) { - c, err := cookie.NewCipher([]byte(secret)) + c, err := encryption.NewCipher([]byte(secret)) assert.Equal(t, nil, err) - c2, err := cookie.NewCipher([]byte(altSecret)) + c2, err := encryption.NewCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &sessions.SessionState{ User: "just-user", @@ -146,7 +146,7 @@ func TestExpired(t *testing.T) { type testCase struct { sessions.SessionState Encoded string - Cipher *cookie.Cipher + Cipher *encryption.Cipher Error bool } @@ -203,7 +203,7 @@ func TestDecodeSessionState(t *testing.T) { eString := string(eJSON) eUnix := e.Unix() - c, err := cookie.NewCipher([]byte(secret)) + c, err := encryption.NewCipher([]byte(secret)) assert.NoError(t, err) testCases := []testCase{ diff --git a/pkg/cookies/cookies.go b/pkg/cookies/cookies.go index 08e6a9b..75b93e4 100644 --- a/pkg/cookies/cookies.go +++ b/pkg/cookies/cookies.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/apis/options" + "github.com/pusher/oauth2_proxy/pkg/logger" ) // MakeCookie constructs a cookie from the given parameters, diff --git a/cookie/cookies.go b/pkg/encryption/cipher.go similarity index 99% rename from cookie/cookies.go rename to pkg/encryption/cipher.go index 0d354e1..c308330 100644 --- a/cookie/cookies.go +++ b/pkg/encryption/cipher.go @@ -1,4 +1,4 @@ -package cookie +package encryption import ( "crypto/aes" diff --git a/cookie/cookies_test.go b/pkg/encryption/cipher_test.go similarity index 98% rename from cookie/cookies_test.go rename to pkg/encryption/cipher_test.go index 500550e..fb6a4aa 100644 --- a/cookie/cookies_test.go +++ b/pkg/encryption/cipher_test.go @@ -1,4 +1,4 @@ -package cookie +package encryption import ( "encoding/base64" diff --git a/cookie/nonce.go b/pkg/encryption/nonce.go similarity index 93% rename from cookie/nonce.go rename to pkg/encryption/nonce.go index 6def148..69850c4 100644 --- a/cookie/nonce.go +++ b/pkg/encryption/nonce.go @@ -1,4 +1,4 @@ -package cookie +package encryption import ( "crypto/rand" diff --git a/logger/logger.go b/pkg/logger/logger.go similarity index 95% rename from logger/logger.go rename to pkg/logger/logger.go index 8e27733..1b6017b 100644 --- a/logger/logger.go +++ b/pkg/logger/logger.go @@ -88,6 +88,7 @@ type Logger struct { stdEnabled bool authEnabled bool reqEnabled bool + excludePaths map[string]struct{} stdLogTemplate *template.Template authTemplate *template.Template reqTemplate *template.Template @@ -101,6 +102,7 @@ func New(flag int) *Logger { 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)), @@ -177,6 +179,10 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url. return } + if _, ok := l.excludePaths[url.Path]; ok { + return + } + duration := float64(time.Now().Sub(ts)) / float64(time.Second) if username == "" { @@ -302,6 +308,16 @@ func (l *Logger) SetReqEnabled(e bool) { 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() @@ -365,6 +381,11 @@ 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) { diff --git a/api/api.go b/pkg/requests/requests.go similarity index 96% rename from api/api.go rename to pkg/requests/requests.go index c5d5623..82d1176 100644 --- a/api/api.go +++ b/pkg/requests/requests.go @@ -1,4 +1,4 @@ -package api +package requests import ( "encoding/json" @@ -7,7 +7,7 @@ import ( "net/http" "github.com/bitly/go-simplejson" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" ) // Request parses the request body into a simplejson.Json object diff --git a/api/api_test.go b/pkg/requests/requests_test.go similarity index 99% rename from api/api_test.go rename to pkg/requests/requests_test.go index 7bdf1b7..99a4c3b 100644 --- a/api/api_test.go +++ b/pkg/requests/requests_test.go @@ -1,4 +1,4 @@ -package api +package requests import ( "io/ioutil" diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go index c40dd23..960be90 100644 --- a/pkg/sessions/cookie/session_store.go +++ b/pkg/sessions/cookie/session_store.go @@ -8,10 +8,10 @@ import ( "strings" "time" - "github.com/pusher/oauth2_proxy/cookie" "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" ) @@ -28,7 +28,7 @@ var _ sessions.SessionStore = &SessionStore{} // interface that stores sessions in client side cookies type SessionStore struct { CookieOptions *options.CookieOptions - CookieCipher *cookie.Cipher + CookieCipher *encryption.Cipher } // Save takes a sessions.SessionState and stores the information from it @@ -53,7 +53,7 @@ func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) { // always http.ErrNoCookie return nil, fmt.Errorf("Cookie %q not present", s.CookieOptions.CookieName) } - val, _, ok := cookie.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire) + val, _, ok := encryption.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire) if !ok { return nil, errors.New("Cookie Signature not valid") } @@ -96,7 +96,7 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques // authentication details func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie { if value != "" { - value = cookie.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now) + 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) { diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go new file mode 100644 index 0000000..ed33d72 --- /dev/null +++ b/pkg/sessions/redis/redis_store.go @@ -0,0 +1,305 @@ +package redis + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/go-redis/redis" + "github.com/pusher/oauth2_proxy/pkg/apis/options" + "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/cookies" + "github.com/pusher/oauth2_proxy/pkg/encryption" +) + +// TicketData is a structure representing the ticket used in server session storage +type TicketData struct { + TicketID string + Secret []byte +} + +// SessionStore is an implementation of the sessions.SessionStore +// interface that stores sessions in redis +type SessionStore struct { + CookieCipher *encryption.Cipher + CookieOptions *options.CookieOptions + Client *redis.Client +} + +// NewRedisSessionStore initialises a new instance of the SessionStore from +// the configuration given +func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) { + client, err := newRedisClient(opts.RedisStoreOptions) + if err != nil { + return nil, fmt.Errorf("error constructing redis client: %v", err) + } + + rs := &SessionStore{ + Client: client, + CookieCipher: opts.Cipher, + CookieOptions: cookieOpts, + } + return rs, nil + +} + +func newRedisClient(opts options.RedisStoreOptions) (*redis.Client, error) { + if opts.UseSentinel { + client := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: opts.SentinelMasterName, + SentinelAddrs: opts.SentinelConnectionURLs, + }) + return client, nil + } + + opt, err := redis.ParseURL(opts.RedisConnectionURL) + if err != nil { + return nil, fmt.Errorf("unable to parse redis url: %s", err) + } + + client := redis.NewClient(opt) + return client, nil +} + +// Save takes a sessions.SessionState and stores the information from it +// to redies, and adds a new ticket cookie on the HTTP response writer +func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error { + if s.CreatedAt.IsZero() { + s.CreatedAt = time.Now() + } + + // Old sessions that we are refreshing would have a request cookie + // New sessions don't, so we ignore the error. storeValue will check requestCookie + requestCookie, _ := req.Cookie(store.CookieOptions.CookieName) + value, err := s.EncodeSessionState(store.CookieCipher) + if err != nil { + return err + } + ticketString, err := store.storeValue(value, store.CookieOptions.CookieExpire, requestCookie) + if err != nil { + return err + } + + ticketCookie := store.makeCookie( + req, + ticketString, + store.CookieOptions.CookieExpire, + s.CreatedAt, + ) + + http.SetCookie(rw, ticketCookie) + return nil +} + +// Load reads sessions.SessionState information from a ticket +// cookie within the HTTP request object +func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) { + requestCookie, err := req.Cookie(store.CookieOptions.CookieName) + if err != nil { + return nil, fmt.Errorf("error loading session: %s", err) + } + + val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) + if !ok { + return nil, fmt.Errorf("Cookie Signature not valid") + } + session, err := store.loadSessionFromString(val) + if err != nil { + return nil, fmt.Errorf("error loading session: %s", err) + } + return session, nil +} + +// loadSessionFromString loads the session based on the ticket value +func (store *SessionStore) loadSessionFromString(value string) (*sessions.SessionState, error) { + ticket, err := decodeTicket(store.CookieOptions.CookieName, value) + if err != nil { + return nil, err + } + + result, err := store.Client.Get(ticket.asHandle(store.CookieOptions.CookieName)).Result() + if err != nil { + return nil, err + } + + resultBytes := []byte(result) + block, err := aes.NewCipher(ticket.Secret) + if err != nil { + return nil, err + } + // Use secret as the IV too, because each entry has it's own key + stream := cipher.NewCFBDecrypter(block, ticket.Secret) + stream.XORKeyStream(resultBytes, resultBytes) + + session, err := sessions.DecodeSessionState(string(resultBytes), store.CookieCipher) + if err != nil { + return nil, err + } + return session, nil +} + +// Clear clears any saved session information for a given ticket cookie +// from redis, and then clears the session +func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error { + // We go ahead and clear the cookie first, always. + clearCookie := store.makeCookie( + req, + "", + time.Hour*-1, + time.Now(), + ) + http.SetCookie(rw, clearCookie) + + // If there was an existing cookie we should clear the session in redis + requestCookie, err := req.Cookie(store.CookieOptions.CookieName) + if err != nil && err == http.ErrNoCookie { + // No existing cookie so can't clear redis + return nil + } else if err != nil { + return fmt.Errorf("error retrieving cookie: %v", err) + } + + val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) + if !ok { + return fmt.Errorf("Cookie Signature not valid") + } + + // We only return an error if we had an issue with redis + // If there's an issue decoding the ticket, ignore it + ticket, _ := decodeTicket(store.CookieOptions.CookieName, val) + if ticket != nil { + _, err := store.Client.Del(ticket.asHandle(store.CookieOptions.CookieName)).Result() + if err != nil { + return fmt.Errorf("error clearing cookie from redis: %s", err) + } + } + return nil +} + +// makeCookie makes a cookie, signing the value if present +func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie { + if value != "" { + value = encryption.SignedValue(store.CookieOptions.CookieSecret, store.CookieOptions.CookieName, value, now) + } + return cookies.MakeCookieFromOptions( + req, + store.CookieOptions.CookieName, + value, + store.CookieOptions, + expires, + now, + ) +} + +func (store *SessionStore) storeValue(value string, expiration time.Duration, requestCookie *http.Cookie) (string, error) { + ticket, err := store.getTicket(requestCookie) + if err != nil { + return "", fmt.Errorf("error getting ticket: %v", err) + } + + ciphertext := make([]byte, len(value)) + block, err := aes.NewCipher(ticket.Secret) + if err != nil { + return "", fmt.Errorf("error initiating cipher block %s", err) + } + + // Use secret as the Initialization Vector too, because each entry has it's own key + stream := cipher.NewCFBEncrypter(block, ticket.Secret) + stream.XORKeyStream(ciphertext, []byte(value)) + + handle := ticket.asHandle(store.CookieOptions.CookieName) + err = store.Client.Set(handle, ciphertext, expiration).Err() + if err != nil { + return "", err + } + return ticket.encodeTicket(store.CookieOptions.CookieName), nil +} + +// getTicket retrieves an existing ticket from the cookie if present, +// or creates a new ticket +func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, error) { + if requestCookie == nil { + return newTicket() + } + + // An existing cookie exists, try to retrieve the ticket + val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) + if !ok { + // Cookie is invalid, create a new ticket + return newTicket() + } + + // Valid cookie, decode the ticket + ticket, err := decodeTicket(store.CookieOptions.CookieName, val) + if err != nil { + // If we can't decode the ticket we have to create a new one + return newTicket() + } + return ticket, nil +} + +func newTicket() (*TicketData, error) { + rawID := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, rawID); err != nil { + return nil, fmt.Errorf("failed to create new ticket ID %s", err) + } + // ticketID is hex encoded + ticketID := fmt.Sprintf("%x", rawID) + + secret := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(rand.Reader, secret); err != nil { + return nil, fmt.Errorf("failed to create initialization vector %s", err) + } + ticket := &TicketData{ + TicketID: ticketID, + Secret: secret, + } + return ticket, nil +} + +func (ticket *TicketData) asHandle(prefix string) string { + return fmt.Sprintf("%s-%s", prefix, ticket.TicketID) +} + +func decodeTicket(cookieName string, ticketString string) (*TicketData, error) { + prefix := cookieName + "-" + if !strings.HasPrefix(ticketString, prefix) { + return nil, fmt.Errorf("failed to decode ticket handle") + } + trimmedTicket := strings.TrimPrefix(ticketString, prefix) + + ticketParts := strings.Split(trimmedTicket, ".") + if len(ticketParts) != 2 { + return nil, fmt.Errorf("failed to decode ticket") + } + ticketID, secretBase64 := ticketParts[0], ticketParts[1] + + // ticketID must be a hexadecimal string + _, err := hex.DecodeString(ticketID) + if err != nil { + return nil, fmt.Errorf("server ticket failed sanity checks") + } + + secret, err := base64.RawURLEncoding.DecodeString(secretBase64) + if err != nil { + return nil, fmt.Errorf("failed to decode initialization vector %s", err) + } + ticketData := &TicketData{ + TicketID: ticketID, + Secret: secret, + } + return ticketData, nil +} + +func (ticket *TicketData) encodeTicket(prefix string) string { + handle := ticket.asHandle(prefix) + ticketString := handle + "." + base64.RawURLEncoding.EncodeToString(ticket.Secret) + return ticketString +} diff --git a/pkg/sessions/session_store.go b/pkg/sessions/session_store.go index ec84dd7..17ef21c 100644 --- a/pkg/sessions/session_store.go +++ b/pkg/sessions/session_store.go @@ -6,6 +6,7 @@ import ( "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 @@ -13,6 +14,8 @@ func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOpt 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) } diff --git a/pkg/sessions/session_store_test.go b/pkg/sessions/session_store_test.go index b407d58..fd0b0e5 100644 --- a/pkg/sessions/session_store_test.go +++ b/pkg/sessions/session_store_test.go @@ -10,14 +10,16 @@ import ( "testing" "time" + "github.com/alicebob/miniredis" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/pusher/oauth2_proxy/cookie" "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" ) @@ -34,6 +36,7 @@ var _ = Describe("NewSessionStore", func() { var response *httptest.ResponseRecorder var session *sessionsapi.SessionState var ss sessionsapi.SessionStore + var mr *miniredis.Miniredis CheckCookieOptions := func() { Context("the cookies returned", func() { @@ -89,19 +92,113 @@ var _ = Describe("NewSessionStore", func() { }) } - SessionStoreInterfaceTests := func() { - Context("when Save is called", func() { + // The following should only be for server stores + PersistentSessionStoreTests := func() { + Context("when Clear is called on a persistent store", func() { + var resultCookies []*http.Cookie + BeforeEach(func() { - err := ss.Save(response, request, session) + 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()) }) - It("sets a `set-cookie` header in the response", func() { - Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty()) + 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()) + }) }) - It("Ensures the session CreatedAt is not zero", func() { - Expect(session.CreatedAt.IsZero()).To(BeFalse()) + 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() @@ -109,18 +206,15 @@ var _ = Describe("NewSessionStore", func() { Context("when Clear is called", func() { BeforeEach(func() { - cookie := cookies.MakeCookie(request, - cookieOpts.CookieName, - "foo", - cookieOpts.CookiePath, - cookieOpts.CookieDomain, - cookieOpts.CookieHTTPOnly, - cookieOpts.CookieSecure, - cookieOpts.CookieExpire, - time.Now(), - ) - request.AddCookie(cookie) - err := ss.Clear(response, request) + 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()) }) @@ -132,7 +226,38 @@ var _ = Describe("NewSessionStore", func() { }) Context("when Load is called", func() { - var loadedSession *sessionsapi.SessionState + 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() @@ -142,36 +267,57 @@ var _ = Describe("NewSessionStore", func() { for _, cookie := range resp.Result().Cookies() { request.AddCookie(cookie) } - 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()) - } + 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() { + RunSessionTests := func(persistent bool) { Context("with default options", func() { BeforeEach(func() { var err error @@ -179,7 +325,7 @@ var _ = Describe("NewSessionStore", func() { Expect(err).ToNot(HaveOccurred()) }) - SessionStoreInterfaceTests() + SessionStoreInterfaceTests(persistent) }) Context("with non-default options", func() { @@ -188,7 +334,7 @@ var _ = Describe("NewSessionStore", func() { CookieName: "_cookie_name", CookiePath: "/path", CookieExpire: time.Duration(72) * time.Hour, - CookieRefresh: time.Duration(3600), + CookieRefresh: time.Duration(2) * time.Hour, CookieSecure: false, CookieHTTPOnly: false, CookieDomain: "example.com", @@ -199,7 +345,7 @@ var _ = Describe("NewSessionStore", func() { Expect(err).ToNot(HaveOccurred()) }) - SessionStoreInterfaceTests() + SessionStoreInterfaceTests(persistent) }) Context("with a cipher", func() { @@ -208,7 +354,7 @@ var _ = Describe("NewSessionStore", func() { _, err := rand.Read(secret) Expect(err).ToNot(HaveOccurred()) cookieOpts.CookieSecret = base64.URLEncoding.EncodeToString(secret) - cipher, err := cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret)) + cipher, err := encryption.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret)) Expect(err).ToNot(HaveOccurred()) Expect(cipher).ToNot(BeNil()) opts.Cipher = cipher @@ -217,7 +363,7 @@ var _ = Describe("NewSessionStore", func() { Expect(err).ToNot(HaveOccurred()) }) - SessionStoreInterfaceTests() + SessionStoreInterfaceTests(persistent) }) } @@ -230,7 +376,7 @@ var _ = Describe("NewSessionStore", func() { CookieName: "_oauth2_proxy", CookiePath: "/", CookieExpire: time.Duration(168) * time.Hour, - CookieRefresh: time.Duration(0), + CookieRefresh: time.Duration(1) * time.Hour, CookieSecure: true, CookieHTTPOnly: true, } @@ -260,7 +406,31 @@ var _ = Describe("NewSessionStore", func() { }) Context("the cookie.SessionStore", func() { - RunSessionTests() + RunSessionTests(false) + }) + }) + + Context("with type 'redis'", func() { + BeforeEach(func() { + var err error + mr, err = miniredis.Run() + Expect(err).ToNot(HaveOccurred()) + opts.Type = options.RedisSessionStoreType + opts.RedisConnectionURL = "redis://" + mr.Addr() + }) + + AfterEach(func() { + mr.Close() + }) + + It("creates a redis.SessionStore", func() { + ss, err := sessions.NewSessionStore(opts, cookieOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(ss).To(BeAssignableToTypeOf(&redis.SessionStore{})) + }) + + Context("the redis.SessionStore", func() { + RunSessionTests(true) }) }) diff --git a/pkg/sessions/utils/utils.go b/pkg/sessions/utils/utils.go index 051e9cc..1fb27f4 100644 --- a/pkg/sessions/utils/utils.go +++ b/pkg/sessions/utils/utils.go @@ -3,17 +3,17 @@ package utils import ( "encoding/base64" - "github.com/pusher/oauth2_proxy/cookie" "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 *cookie.Cipher) (string, error) { +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 *cookie.Cipher) (s *sessions.SessionState, err error) { +func SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) { return sessions.DecodeSessionState(v, c) } diff --git a/providers/azure.go b/providers/azure.go index a7961d2..653090b 100644 --- a/providers/azure.go +++ b/providers/azure.go @@ -7,9 +7,9 @@ import ( "net/url" "github.com/bitly/go-simplejson" - "github.com/pusher/oauth2_proxy/api" - "github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/pusher/oauth2_proxy/pkg/requests" ) // AzureProvider represents an Azure based Identity Provider @@ -102,7 +102,7 @@ func (p *AzureProvider) GetEmailAddress(s *sessions.SessionState) (string, error } req.Header = getAzureHeader(s.AccessToken) - json, err := api.Request(req) + json, err := requests.Request(req) if err != nil { return "", err diff --git a/providers/bitbucket.go b/providers/bitbucket.go new file mode 100644 index 0000000..63c1d0f --- /dev/null +++ b/providers/bitbucket.go @@ -0,0 +1,163 @@ +package providers + +import ( + "net/http" + "net/url" + "strings" + + "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/pusher/oauth2_proxy/pkg/requests" +) + +// BitbucketProvider represents an Bitbucket based Identity Provider +type BitbucketProvider struct { + *ProviderData + Team string + Repository string +} + +// NewBitbucketProvider initiates a new BitbucketProvider +func NewBitbucketProvider(p *ProviderData) *BitbucketProvider { + p.ProviderName = "Bitbucket" + if p.LoginURL == nil || p.LoginURL.String() == "" { + p.LoginURL = &url.URL{ + Scheme: "https", + Host: "bitbucket.org", + Path: "/site/oauth2/authorize", + } + } + if p.RedeemURL == nil || p.RedeemURL.String() == "" { + p.RedeemURL = &url.URL{ + Scheme: "https", + Host: "bitbucket.org", + Path: "/site/oauth2/access_token", + } + } + if p.ValidateURL == nil || p.ValidateURL.String() == "" { + p.ValidateURL = &url.URL{ + Scheme: "https", + Host: "api.bitbucket.org", + Path: "/2.0/user/emails", + } + } + if p.Scope == "" { + p.Scope = "email" + } + return &BitbucketProvider{ProviderData: p} +} + +// SetTeam defines the Bitbucket team the user must be part of +func (p *BitbucketProvider) SetTeam(team string) { + p.Team = team + if !strings.Contains(p.Scope, "team") { + p.Scope += " team" + } +} + +// SetRepository defines the repository the user must have access to +func (p *BitbucketProvider) SetRepository(repository string) { + p.Repository = repository + if !strings.Contains(p.Scope, "repository") { + p.Scope += " repository" + } +} + +// GetEmailAddress returns the email of the authenticated user +func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { + + var emails struct { + Values []struct { + Email string `json:"email"` + Primary bool `json:"is_primary"` + } + } + var teams struct { + Values []struct { + Name string `json:"username"` + } + } + var repositories struct { + Values []struct { + FullName string `json:"full_name"` + } + } + req, err := http.NewRequest("GET", + p.ValidateURL.String()+"?access_token="+s.AccessToken, nil) + if err != nil { + logger.Printf("failed building request %s", err) + return "", err + } + err = requests.RequestJSON(req, &emails) + if err != nil { + logger.Printf("failed making request %s", err) + return "", err + } + + if p.Team != "" { + teamURL := &url.URL{} + *teamURL = *p.ValidateURL + teamURL.Path = "/2.0/teams" + req, err = http.NewRequest("GET", + teamURL.String()+"?role=member&access_token="+s.AccessToken, nil) + if err != nil { + logger.Printf("failed building request %s", err) + return "", err + } + err = requests.RequestJSON(req, &teams) + if err != nil { + logger.Printf("failed requesting teams membership %s", err) + return "", err + } + var found = false + for _, team := range teams.Values { + if p.Team == team.Name { + found = true + break + } + } + if found != true { + logger.Print("team membership test failed, access denied") + return "", nil + } + } + + if p.Repository != "" { + repositoriesURL := &url.URL{} + *repositoriesURL = *p.ValidateURL + repositoriesURL.Path = "/2.0/repositories/" + strings.Split(p.Repository, "/")[0] + req, err = http.NewRequest("GET", + repositoriesURL.String()+"?role=contributor"+ + "&q=full_name="+url.QueryEscape("\""+p.Repository+"\"")+ + "&access_token="+s.AccessToken, + nil) + if err != nil { + logger.Printf("failed building request %s", err) + return "", err + } + err = requests.RequestJSON(req, &repositories) + if err != nil { + logger.Printf("failed checking repository access %s", err) + return "", err + } + var found = false + for _, repository := range repositories.Values { + if p.Repository == repository.FullName { + found = true + break + } + } + if found != true { + logger.Print("repository access test failed, access denied") + return "", nil + } + } + + for _, email := range emails.Values { + if email.Primary { + return email.Email, nil + } + } + + return "", nil +} diff --git a/providers/bitbucket_test.go b/providers/bitbucket_test.go new file mode 100644 index 0000000..585603d --- /dev/null +++ b/providers/bitbucket_test.go @@ -0,0 +1,170 @@ +package providers + +import ( + "log" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/pusher/oauth2_proxy/pkg/apis/sessions" +) + +func testBitbucketProvider(hostname, team string, repository string) *BitbucketProvider { + p := NewBitbucketProvider( + &ProviderData{ + ProviderName: "", + LoginURL: &url.URL{}, + RedeemURL: &url.URL{}, + ProfileURL: &url.URL{}, + ValidateURL: &url.URL{}, + Scope: ""}) + + if team != "" { + p.SetTeam(team) + } + + if repository != "" { + p.SetRepository(repository) + } + + if hostname != "" { + updateURL(p.Data().LoginURL, hostname) + updateURL(p.Data().RedeemURL, hostname) + updateURL(p.Data().ProfileURL, hostname) + updateURL(p.Data().ValidateURL, hostname) + } + return p +} + +func testBitbucketBackend(payload string) *httptest.Server { + paths := map[string]bool{ + "/2.0/user/emails": true, + "/2.0/teams": true, + } + + return httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + url := r.URL + if !paths[url.Path] { + log.Printf("%s not in %+v\n", url.Path, paths) + w.WriteHeader(404) + } else if r.URL.Query().Get("access_token") != "imaginary_access_token" { + w.WriteHeader(403) + } else { + w.WriteHeader(200) + w.Write([]byte(payload)) + } + })) +} + +func TestBitbucketProviderDefaults(t *testing.T) { + p := testBitbucketProvider("", "", "") + assert.NotEqual(t, nil, p) + assert.Equal(t, "Bitbucket", p.Data().ProviderName) + assert.Equal(t, "https://bitbucket.org/site/oauth2/authorize", + p.Data().LoginURL.String()) + assert.Equal(t, "https://bitbucket.org/site/oauth2/access_token", + p.Data().RedeemURL.String()) + assert.Equal(t, "https://api.bitbucket.org/2.0/user/emails", + p.Data().ValidateURL.String()) + assert.Equal(t, "email", p.Data().Scope) +} + +func TestBitbucketProviderScopeAdjustForTeam(t *testing.T) { + p := testBitbucketProvider("", "test-team", "") + assert.NotEqual(t, nil, p) + assert.Equal(t, "email team", p.Data().Scope) +} + +func TestBitbucketProviderScopeAdjustForRepository(t *testing.T) { + p := testBitbucketProvider("", "", "rest-repo") + assert.NotEqual(t, nil, p) + assert.Equal(t, "email repository", p.Data().Scope) +} + +func TestBitbucketProviderOverrides(t *testing.T) { + p := NewBitbucketProvider( + &ProviderData{ + LoginURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/auth"}, + RedeemURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/token"}, + ValidateURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/api/v3/user"}, + Scope: "profile"}) + assert.NotEqual(t, nil, p) + assert.Equal(t, "Bitbucket", p.Data().ProviderName) + assert.Equal(t, "https://example.com/oauth/auth", + p.Data().LoginURL.String()) + assert.Equal(t, "https://example.com/oauth/token", + p.Data().RedeemURL.String()) + assert.Equal(t, "https://example.com/api/v3/user", + p.Data().ValidateURL.String()) + assert.Equal(t, "profile", p.Data().Scope) +} + +func TestBitbucketProviderGetEmailAddress(t *testing.T) { + b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true } ] }") + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testBitbucketProvider(bURL.Host, "", "") + + session := &sessions.SessionState{AccessToken: "imaginary_access_token"} + email, err := p.GetEmailAddress(session) + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +func TestBitbucketProviderGetEmailAddressAndGroup(t *testing.T) { + b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true, \"username\": \"bioinformatics\" } ] }") + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testBitbucketProvider(bURL.Host, "bioinformatics", "") + + session := &sessions.SessionState{AccessToken: "imaginary_access_token"} + email, err := p.GetEmailAddress(session) + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +// Note that trying to trigger the "failed building request" case is not +// practical, since the only way it can fail is if the URL fails to parse. +func TestBitbucketProviderGetEmailAddressFailedRequest(t *testing.T) { + b := testBitbucketBackend("unused payload") + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testBitbucketProvider(bURL.Host, "", "") + + // We'll trigger a request failure by using an unexpected access + // token. Alternatively, we could allow the parsing of the payload as + // JSON to fail. + session := &sessions.SessionState{AccessToken: "unexpected_access_token"} + email, err := p.GetEmailAddress(session) + assert.NotEqual(t, nil, err) + assert.Equal(t, "", email) +} + +func TestBitbucketProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { + b := testBitbucketBackend("{\"foo\": \"bar\"}") + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testBitbucketProvider(bURL.Host, "", "") + + session := &sessions.SessionState{AccessToken: "imaginary_access_token"} + email, err := p.GetEmailAddress(session) + assert.Equal(t, "", email) + assert.Equal(t, nil, err) +} diff --git a/providers/facebook.go b/providers/facebook.go index 9897a1b..abd5382 100644 --- a/providers/facebook.go +++ b/providers/facebook.go @@ -6,8 +6,8 @@ import ( "net/http" "net/url" - "github.com/pusher/oauth2_proxy/api" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/requests" ) // FacebookProvider represents an Facebook based Identity Provider @@ -69,7 +69,7 @@ func (p *FacebookProvider) GetEmailAddress(s *sessions.SessionState) (string, er Email string } var r result - err = api.RequestJSON(req, &r) + err = requests.RequestJSON(req, &r) if err != nil { return "", err } diff --git a/providers/github.go b/providers/github.go index b60ffe1..ba58bb1 100644 --- a/providers/github.go +++ b/providers/github.go @@ -10,8 +10,8 @@ import ( "strconv" "strings" - "github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/logger" ) // GitHubProvider represents an GitHub based Identity Provider diff --git a/providers/gitlab.go b/providers/gitlab.go index af956c4..c32ebe8 100644 --- a/providers/gitlab.go +++ b/providers/gitlab.go @@ -1,62 +1,258 @@ package providers import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" "net/http" - "net/url" + "strings" + "time" - "github.com/pusher/oauth2_proxy/api" - "github.com/pusher/oauth2_proxy/logger" + oidc "github.com/coreos/go-oidc" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "golang.org/x/oauth2" ) -// GitLabProvider represents an GitLab based Identity Provider +// GitLabProvider represents a GitLab based Identity Provider type GitLabProvider struct { *ProviderData + + Group string + EmailDomains []string + + Verifier *oidc.IDTokenVerifier + AllowUnverifiedEmail bool } // NewGitLabProvider initiates a new GitLabProvider func NewGitLabProvider(p *ProviderData) *GitLabProvider { p.ProviderName = "GitLab" - if p.LoginURL == nil || p.LoginURL.String() == "" { - p.LoginURL = &url.URL{ - Scheme: "https", - Host: "gitlab.com", - Path: "/oauth/authorize", - } - } - if p.RedeemURL == nil || p.RedeemURL.String() == "" { - p.RedeemURL = &url.URL{ - Scheme: "https", - Host: "gitlab.com", - Path: "/oauth/token", - } - } - if p.ValidateURL == nil || p.ValidateURL.String() == "" { - p.ValidateURL = &url.URL{ - Scheme: "https", - Host: "gitlab.com", - Path: "/api/v4/user", - } - } + if p.Scope == "" { - p.Scope = "read_user" + p.Scope = "openid email" } + return &GitLabProvider{ProviderData: p} } +// Redeem exchanges the OAuth2 authentication token for an ID token +func (p *GitLabProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { + ctx := context.Background() + c := oauth2.Config{ + ClientID: p.ClientID, + ClientSecret: p.ClientSecret, + Endpoint: oauth2.Endpoint{ + TokenURL: p.RedeemURL.String(), + }, + RedirectURL: redirectURL, + } + token, err := c.Exchange(ctx, code) + if err != nil { + return nil, fmt.Errorf("token exchange: %v", err) + } + s, err = p.createSessionState(ctx, token) + if err != nil { + return nil, fmt.Errorf("unable to update session: %v", err) + } + return +} + +// RefreshSessionIfNeeded checks if the session has expired and uses the +// RefreshToken to fetch a new ID token if required +func (p *GitLabProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) { + if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" { + return false, nil + } + + origExpiration := s.ExpiresOn + + err := p.redeemRefreshToken(s) + if err != nil { + return false, fmt.Errorf("unable to redeem refresh token: %v", err) + } + + fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration) + return true, nil +} + +func (p *GitLabProvider) redeemRefreshToken(s *sessions.SessionState) (err error) { + c := oauth2.Config{ + ClientID: p.ClientID, + ClientSecret: p.ClientSecret, + Endpoint: oauth2.Endpoint{ + TokenURL: p.RedeemURL.String(), + }, + } + ctx := context.Background() + t := &oauth2.Token{ + RefreshToken: s.RefreshToken, + Expiry: time.Now().Add(-time.Hour), + } + token, err := c.TokenSource(ctx, t).Token() + if err != nil { + return fmt.Errorf("failed to get token: %v", err) + } + newSession, err := p.createSessionState(ctx, token) + if err != nil { + return fmt.Errorf("unable to update session: %v", err) + } + s.AccessToken = newSession.AccessToken + s.IDToken = newSession.IDToken + s.RefreshToken = newSession.RefreshToken + s.CreatedAt = newSession.CreatedAt + s.ExpiresOn = newSession.ExpiresOn + s.Email = newSession.Email + return +} + +type gitlabUserInfo struct { + Username string `json:"nickname"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + Groups []string `json:"groups"` +} + +func (p *GitLabProvider) getUserInfo(s *sessions.SessionState) (*gitlabUserInfo, error) { + // Retrieve user info JSON + // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information + + // Build user info url from login url of GitLab instance + userInfoURL := *p.LoginURL + userInfoURL.Path = "/oauth/userinfo" + + req, err := http.NewRequest("GET", userInfoURL.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create user info request: %v", err) + } + req.Header.Set("Authorization", "Bearer "+s.AccessToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to perform user info request: %v", err) + } + var body []byte + body, err = ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("failed to read user info response: %v", err) + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("got %d during user info request: %s", resp.StatusCode, body) + } + + var userInfo gitlabUserInfo + err = json.Unmarshal(body, &userInfo) + if err != nil { + return nil, fmt.Errorf("failed to parse user info: %v", err) + } + + return &userInfo, nil +} + +func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error { + if p.Group == "" { + return nil + } + + // Collect user group memberships + membershipSet := make(map[string]bool) + for _, group := range userInfo.Groups { + membershipSet[group] = true + } + + // Find a valid group that they are a member of + validGroups := strings.Split(p.Group, " ") + for _, validGroup := range validGroups { + if _, ok := membershipSet[validGroup]; ok { + return nil + } + } + + return fmt.Errorf("user is not a member of '%s'", p.Group) +} + +func (p *GitLabProvider) verifyEmailDomain(userInfo *gitlabUserInfo) error { + if len(p.EmailDomains) == 0 || p.EmailDomains[0] == "*" { + return nil + } + + for _, domain := range p.EmailDomains { + if strings.HasSuffix(userInfo.Email, domain) { + return nil + } + } + + return fmt.Errorf("user email is not one of the valid domains '%v'", p.EmailDomains) +} + +func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) { + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + return nil, fmt.Errorf("token response did not contain an id_token") + } + + // Parse and verify ID Token payload. + idToken, err := p.Verifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, fmt.Errorf("could not verify id_token: %v", err) + } + + return &sessions.SessionState{ + AccessToken: token.AccessToken, + IDToken: rawIDToken, + RefreshToken: token.RefreshToken, + CreatedAt: time.Now(), + ExpiresOn: idToken.Expiry, + }, nil +} + +// ValidateSessionState checks that the session's IDToken is still valid +func (p *GitLabProvider) ValidateSessionState(s *sessions.SessionState) bool { + ctx := context.Background() + _, err := p.Verifier.Verify(ctx, s.IDToken) + if err != nil { + return false + } + + return true +} + // GetEmailAddress returns the Account email address func (p *GitLabProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { + // Retrieve user info + userInfo, err := p.getUserInfo(s) + if err != nil { + return "", fmt.Errorf("failed to retrieve user info: %v", err) + } - req, err := http.NewRequest("GET", - p.ValidateURL.String()+"?access_token="+s.AccessToken, nil) - if err != nil { - logger.Printf("failed building request %s", err) - return "", err + // Check if email is verified + if !p.AllowUnverifiedEmail && !userInfo.EmailVerified { + return "", fmt.Errorf("user email is not verified") } - json, err := api.Request(req) + + // Check if email has valid domain + err = p.verifyEmailDomain(userInfo) if err != nil { - logger.Printf("failed making request %s", err) - return "", err + return "", fmt.Errorf("email domain check failed: %v", err) } - return json.Get("email").String() + + // 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 } diff --git a/providers/gitlab_test.go b/providers/gitlab_test.go index 112eb89..f75c4bf 100644 --- a/providers/gitlab_test.go +++ b/providers/gitlab_test.go @@ -25,104 +25,142 @@ func testGitLabProvider(hostname string) *GitLabProvider { updateURL(p.Data().ProfileURL, hostname) updateURL(p.Data().ValidateURL, hostname) } + return p } -func testGitLabBackend(payload string) *httptest.Server { - path := "/api/v4/user" - query := "access_token=imaginary_access_token" +func testGitLabBackend() *httptest.Server { + userInfo := ` + { + "nickname": "FooBar", + "email": "foo@bar.com", + "email_verified": false, + "groups": ["foo", "bar"] + } + ` + authHeader := "Bearer gitlab_access_token" return httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != path || r.URL.RawQuery != query { - w.WriteHeader(404) + if r.URL.Path == "/oauth/userinfo" { + if r.Header["Authorization"][0] == authHeader { + w.WriteHeader(200) + w.Write([]byte(userInfo)) + } else { + w.WriteHeader(401) + } } else { - w.WriteHeader(200) - w.Write([]byte(payload)) + w.WriteHeader(404) } })) } -func TestGitLabProviderDefaults(t *testing.T) { - p := testGitLabProvider("") - assert.NotEqual(t, nil, p) - assert.Equal(t, "GitLab", p.Data().ProviderName) - assert.Equal(t, "https://gitlab.com/oauth/authorize", - p.Data().LoginURL.String()) - assert.Equal(t, "https://gitlab.com/oauth/token", - p.Data().RedeemURL.String()) - assert.Equal(t, "https://gitlab.com/api/v4/user", - p.Data().ValidateURL.String()) - assert.Equal(t, "read_user", p.Data().Scope) -} - -func TestGitLabProviderOverrides(t *testing.T) { - p := NewGitLabProvider( - &ProviderData{ - LoginURL: &url.URL{ - Scheme: "https", - Host: "example.com", - Path: "/oauth/auth"}, - RedeemURL: &url.URL{ - Scheme: "https", - Host: "example.com", - Path: "/oauth/token"}, - ValidateURL: &url.URL{ - Scheme: "https", - Host: "example.com", - Path: "/api/v4/user"}, - Scope: "profile"}) - assert.NotEqual(t, nil, p) - assert.Equal(t, "GitLab", p.Data().ProviderName) - assert.Equal(t, "https://example.com/oauth/auth", - p.Data().LoginURL.String()) - assert.Equal(t, "https://example.com/oauth/token", - p.Data().RedeemURL.String()) - assert.Equal(t, "https://example.com/api/v4/user", - p.Data().ValidateURL.String()) - assert.Equal(t, "profile", p.Data().Scope) -} - -func TestGitLabProviderGetEmailAddress(t *testing.T) { - b := testGitLabBackend("{\"email\": \"michael.bland@gsa.gov\"}") +func TestGitLabProviderBadToken(t *testing.T) { + b := testGitLabBackend() defer b.Close() bURL, _ := url.Parse(b.URL) p := testGitLabProvider(bURL.Host) - session := &sessions.SessionState{AccessToken: "imaginary_access_token"} + session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"} + _, err := p.GetEmailAddress(session) + assert.NotEqual(t, nil, err) +} + +func TestGitLabProviderUnverifiedEmailDenied(t *testing.T) { + b := testGitLabBackend() + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitLabProvider(bURL.Host) + + session := &sessions.SessionState{AccessToken: "gitlab_access_token"} + _, err := p.GetEmailAddress(session) + assert.NotEqual(t, nil, err) +} + +func TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) { + b := testGitLabBackend() + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitLabProvider(bURL.Host) + p.AllowUnverifiedEmail = true + + session := &sessions.SessionState{AccessToken: "gitlab_access_token"} email, err := p.GetEmailAddress(session) assert.Equal(t, nil, err) - assert.Equal(t, "michael.bland@gsa.gov", email) + assert.Equal(t, "foo@bar.com", email) } -// Note that trying to trigger the "failed building request" case is not -// practical, since the only way it can fail is if the URL fails to parse. -func TestGitLabProviderGetEmailAddressFailedRequest(t *testing.T) { - b := testGitLabBackend("unused payload") +func TestGitLabProviderUsername(t *testing.T) { + b := testGitLabBackend() defer b.Close() bURL, _ := url.Parse(b.URL) p := testGitLabProvider(bURL.Host) + p.AllowUnverifiedEmail = true - // 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) + session := &sessions.SessionState{AccessToken: "gitlab_access_token"} + username, err := p.GetUserName(session) + assert.Equal(t, nil, err) + assert.Equal(t, "FooBar", username) } -func TestGitLabProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { - b := testGitLabBackend("{\"foo\": \"bar\"}") +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: "imaginary_access_token"} + session := &sessions.SessionState{AccessToken: "gitlab_access_token"} email, err := p.GetEmailAddress(session) - assert.NotEqual(t, nil, err) - assert.Equal(t, "", email) + 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) +} + +func TestGitLabProviderEmailDomainValid(t *testing.T) { + b := testGitLabBackend() + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitLabProvider(bURL.Host) + p.AllowUnverifiedEmail = true + p.EmailDomains = []string{"bar.com"} + + session := &sessions.SessionState{AccessToken: "gitlab_access_token"} + email, err := p.GetEmailAddress(session) + assert.Equal(t, nil, err) + assert.Equal(t, "foo@bar.com", email) +} + +func TestGitLabProviderEmailDomainInvalid(t *testing.T) { + b := testGitLabBackend() + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitLabProvider(bURL.Host) + p.AllowUnverifiedEmail = true + p.EmailDomains = []string{"baz.com"} + + session := &sessions.SessionState{AccessToken: "gitlab_access_token"} + _, err := p.GetEmailAddress(session) + assert.NotEqual(t, nil, err) } diff --git a/providers/google.go b/providers/google.go index 6f29c2c..4748631 100644 --- a/providers/google.go +++ b/providers/google.go @@ -13,8 +13,8 @@ import ( "strings" "time" - "github.com/pusher/oauth2_proxy/logger" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/logger" "golang.org/x/oauth2" "golang.org/x/oauth2/google" admin "google.golang.org/api/admin/directory/v1" @@ -189,69 +189,44 @@ func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Serv } func userInGroup(service *admin.Service, groups []string, email string) bool { - user, err := fetchUser(service, email) - if err != nil { - logger.Printf("error fetching user: %v", err) - return false - } - id := user.Id - custID := user.CustomerId - for _, group := range groups { - members, err := fetchGroupMembers(service, group) + // Use the HasMember API to checking for the user's presence in each group or nested subgroups + req := service.Members.HasMember(group, email) + r, err := req.Do() if err != nil { - if err, ok := err.(*googleapi.Error); ok && err.Code == 404 { - logger.Printf("error fetching members for group %s: group does not exist", group) - } else { - logger.Printf("error fetching group members: %v", err) - return false - } - } + err, ok := err.(*googleapi.Error) + if ok && err.Code == 404 { + logger.Printf("error checking membership in group %s: group does not exist", group) + } else if ok && err.Code == 400 { + // It is possible for Members.HasMember to return false even if the email is a group member. + // One case that can cause this is if the user email is from a different domain than the group, + // e.g. "member@otherdomain.com" in the group "group@mydomain.com" will result in a 400 error + // from the HasMember API. In that case, attempt to query the member object directly from the group. + req := service.Members.Get(group, email) + r, err := req.Do() - for _, member := range members { - switch member.Type { - case "CUSTOMER": - if member.Id == custID { - return true - } - case "USER": - if member.Id == id { + 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 { + logger.Printf("error checking group membership: %v", err) } + continue + } + if r.IsMember { + return true } } return false } -func fetchUser(service *admin.Service, email string) (*admin.User, error) { - user, err := service.Users.Get(email).Do() - return user, err -} - -func fetchGroupMembers(service *admin.Service, group string) ([]*admin.Member, error) { - members := []*admin.Member{} - pageToken := "" - for { - req := service.Members.List(group) - if pageToken != "" { - req.PageToken(pageToken) - } - r, err := req.Do() - 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 // group(s). func (p *GoogleProvider) ValidateGroup(email string) bool { diff --git a/providers/google_test.go b/providers/google_test.go index 25b375a..37b8326 100644 --- a/providers/google_test.go +++ b/providers/google_test.go @@ -1,14 +1,19 @@ package providers import ( + "context" "encoding/base64" "encoding/json" + "fmt" "net/http" "net/http/httptest" "net/url" "testing" "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) { @@ -179,3 +184,56 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) { } } + +func TestGoogleProviderUserInGroup(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/groups/group@example.com/hasMember/member-in-domain@example.com" { + fmt.Fprintln(w, `{"isMember": true}`) + } else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-in-domain@example.com" { + fmt.Fprintln(w, `{"isMember": false}`) + } else if r.URL.Path == "/groups/group@example.com/hasMember/member-out-of-domain@otherexample.com" { + http.Error( + w, + `{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`, + http.StatusBadRequest, + ) + } else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-out-of-domain@otherexample.com" { + http.Error( + w, + `{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`, + http.StatusBadRequest, + ) + } else if r.URL.Path == "/groups/group@example.com/members/non-member-out-of-domain@otherexample.com" { + // note that the client currently doesn't care what this response text or code is - any error here results in failure to match the group + http.Error( + w, + `{"error": {"errors": [{"domain": "global","reason": "notFound","message": "Resource Not Found: memberKey"}],"code": 404,"message": "Resource Not Found: memberKey"}}`, + http.StatusNotFound, + ) + } else if r.URL.Path == "/groups/group@example.com/members/member-out-of-domain@otherexample.com" { + fmt.Fprintln(w, + `{"kind": "admin#directory#member","etag":"12345","id":"1234567890","email": "member-out-of-domain@otherexample.com","role": "MEMBER","type": "USER","status": "ACTIVE","delivery_settings": "ALL_MAIL"}}`, + ) + } + })) + defer ts.Close() + + client := ts.Client() + ctx := context.Background() + + service, err := admin.NewService(ctx, option.WithHTTPClient(client)) + service.BasePath = ts.URL + assert.Equal(t, nil, err) + + result := userInGroup(service, []string{"group@example.com"}, "member-in-domain@example.com") + assert.True(t, result) + + result = userInGroup(service, []string{"group@example.com"}, "member-out-of-domain@otherexample.com") + assert.True(t, result) + + result = userInGroup(service, []string{"group@example.com"}, "non-member-in-domain@example.com") + assert.False(t, result) + + result = userInGroup(service, []string{"group@example.com"}, "non-member-out-of-domain@otherexample.com") + assert.False(t, result) +} diff --git a/providers/internal_util.go b/providers/internal_util.go index 7144dee..fb33b31 100644 --- a/providers/internal_util.go +++ b/providers/internal_util.go @@ -5,8 +5,8 @@ import ( "net/http" "net/url" - "github.com/pusher/oauth2_proxy/api" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/pusher/oauth2_proxy/pkg/requests" ) // stripToken is a helper function to obfuscate "access_token" @@ -47,7 +47,7 @@ func stripParam(param, endpoint string) string { // validateToken returns true if token is valid func validateToken(p Provider, accessToken string, header http.Header) bool { - if accessToken == "" || p.Data().ValidateURL == nil { + if accessToken == "" || p.Data().ValidateURL == nil || p.Data().ValidateURL.String() == "" { return false } endpoint := p.Data().ValidateURL.String() @@ -55,7 +55,7 @@ func validateToken(p Provider, accessToken string, header http.Header) bool { params := url.Values{"access_token": {accessToken}} endpoint = endpoint + "?" + params.Encode() } - resp, err := api.RequestUnparsedResponse(endpoint, header) + resp, err := requests.RequestUnparsedResponse(endpoint, header) if err != nil { logger.Printf("GET %s", stripToken(endpoint)) logger.Printf("token validation request failed: %s", err) diff --git a/providers/linkedin.go b/providers/linkedin.go index a31b4a1..bca2936 100644 --- a/providers/linkedin.go +++ b/providers/linkedin.go @@ -6,8 +6,8 @@ import ( "net/http" "net/url" - "github.com/pusher/oauth2_proxy/api" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/requests" ) // LinkedInProvider represents an LinkedIn based Identity Provider @@ -61,7 +61,7 @@ func (p *LinkedInProvider) GetEmailAddress(s *sessions.SessionState) (string, er } req.Header = getLinkedInHeader(s.AccessToken) - json, err := api.Request(req) + json, err := requests.Request(req) if err != nil { return "", err } diff --git a/providers/oidc.go b/providers/oidc.go index 08ea082..2abf2ca 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -3,10 +3,13 @@ package providers import ( "context" "fmt" + "net/http" "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" ) @@ -14,7 +17,8 @@ import ( type OIDCProvider struct { *ProviderData - Verifier *oidc.IDTokenVerifier + Verifier *oidc.IDTokenVerifier + AllowUnverifiedEmail bool } // NewOIDCProvider initiates a new OIDCProvider @@ -116,10 +120,33 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok } if claims.Email == "" { - // TODO: Try getting email from /userinfo before falling back to Subject - claims.Email = claims.Subject + if p.ProfileURL.String() == "" { + return nil, fmt.Errorf("id_token did not contain an email") + } + + // 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 claims.Verified != nil && !*claims.Verified { + if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified { return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email) } @@ -128,7 +155,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok IDToken: rawIDToken, RefreshToken: token.RefreshToken, CreatedAt: time.Now(), - ExpiresOn: token.Expiry, + ExpiresOn: idToken.Expiry, Email: claims.Email, User: claims.Subject, }, nil @@ -144,3 +171,10 @@ func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool { 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 +} diff --git a/providers/provider_default.go b/providers/provider_default.go index 4716014..d87b939 100644 --- a/providers/provider_default.go +++ b/providers/provider_default.go @@ -10,8 +10,8 @@ import ( "net/url" "time" - "github.com/pusher/oauth2_proxy/cookie" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/encryption" ) // Redeem provides a default implementation of the OAuth2 token redemption process @@ -96,12 +96,12 @@ func (p *ProviderData) GetLoginURL(redirectURI, state string) string { } // CookieForSession serializes a session state for storage in a cookie -func (p *ProviderData) CookieForSession(s *sessions.SessionState, c *cookie.Cipher) (string, error) { +func (p *ProviderData) CookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) { return s.EncodeSessionState(c) } // SessionFromCookie deserializes a session from a cookie value -func (p *ProviderData) SessionFromCookie(v string, c *cookie.Cipher) (s *sessions.SessionState, err error) { +func (p *ProviderData) SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) { return sessions.DecodeSessionState(v, c) } diff --git a/providers/providers.go b/providers/providers.go index 57ace41..276fab6 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -1,8 +1,8 @@ package providers import ( - "github.com/pusher/oauth2_proxy/cookie" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/pusher/oauth2_proxy/pkg/encryption" ) // Provider represents an upstream identity provider implementation @@ -15,8 +15,8 @@ type Provider interface { ValidateSessionState(*sessions.SessionState) bool GetLoginURL(redirectURI, finalRedirect string) string RefreshSessionIfNeeded(*sessions.SessionState) (bool, error) - SessionFromCookie(string, *cookie.Cipher) (*sessions.SessionState, error) - CookieForSession(*sessions.SessionState, *cookie.Cipher) (string, error) + SessionFromCookie(string, *encryption.Cipher) (*sessions.SessionState, error) + CookieForSession(*sessions.SessionState, *encryption.Cipher) (string, error) } // New provides a new Provider based on the configured provider string @@ -36,6 +36,8 @@ func New(provider string, p *ProviderData) Provider { return NewOIDCProvider(p) case "login.gov": return NewLoginGovProvider(p) + case "bitbucket": + return NewBitbucketProvider(p) default: return NewGoogleProvider(p) } diff --git a/templates.go b/templates.go index ec1ba87..99637ed 100644 --- a/templates.go +++ b/templates.go @@ -4,7 +4,7 @@ import ( "html/template" "path" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" ) func loadTemplates(dir string) *template.Template { diff --git a/validator.go b/validator.go index 1a5c465..a0dc585 100644 --- a/validator.go +++ b/validator.go @@ -8,7 +8,7 @@ import ( "sync/atomic" "unsafe" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" ) // UserMap holds information from the authenticated emails file diff --git a/watcher.go b/watcher.go index 34e98d7..ed2bc0e 100644 --- a/watcher.go +++ b/watcher.go @@ -7,7 +7,7 @@ import ( "path/filepath" "time" - "github.com/pusher/oauth2_proxy/logger" + "github.com/pusher/oauth2_proxy/pkg/logger" fsnotify "gopkg.in/fsnotify/fsnotify.v1" ) diff --git a/watcher_unsupported.go b/watcher_unsupported.go index 1f6e3fc..ff708b7 100644 --- a/watcher_unsupported.go +++ b/watcher_unsupported.go @@ -2,7 +2,7 @@ package main -import "github.com/pusher/oauth2_proxy/logger" +import "github.com/pusher/oauth2_proxy/pkg/logger" func WatchForUpdates(filename string, done <-chan bool, action func()) { logger.Printf("file watching not implemented on this platform")