Compare commits
No commits in common. "master" and "session-store" have entirely different histories.
master
...
session-st
10
.github/CODEOWNERS
vendored
10
.github/CODEOWNERS
vendored
@ -1,6 +1,6 @@
|
||||
# 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
|
||||
# Default owner should be a Pusher cloud-team member unless overridden by later
|
||||
# rules in this file
|
||||
* @pusher/cloud-team
|
||||
|
||||
# login.gov provider
|
||||
# Note: If @timothy-spencer terms out of his appointment, your best bet
|
||||
@ -10,7 +10,3 @@
|
||||
# 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
|
||||
|
@ -1,13 +0,0 @@
|
||||
run:
|
||||
deadline: 120s
|
||||
linters:
|
||||
enable:
|
||||
- govet
|
||||
- golint
|
||||
- ineffassign
|
||||
- goconst
|
||||
- deadcode
|
||||
- gofmt
|
||||
- goimports
|
||||
enable-all: false
|
||||
disable-all: true
|
10
.travis.yml
10
.travis.yml
@ -1,12 +1,16 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
install:
|
||||
# Fetch dependencies
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.17.1
|
||||
- GO111MODULE=on go mod download
|
||||
- wget -O dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
|
||||
- chmod +x dep
|
||||
- mv dep $GOPATH/bin/dep
|
||||
script:
|
||||
- ./configure && make test
|
||||
- ./configure
|
||||
# Run tests
|
||||
- make test
|
||||
sudo: false
|
||||
notifications:
|
||||
email: false
|
||||
|
88
CHANGELOG.md
88
CHANGELOG.md
@ -1,85 +1,16 @@
|
||||
# Vx.x.x (Pre-release)
|
||||
|
||||
## Changes since v4.0.0
|
||||
|
||||
- [#227](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka)
|
||||
|
||||
# v4.0.0
|
||||
|
||||
## Release Highlights
|
||||
- Documentation is now on a [microsite](https://pusher.github.io/oauth2_proxy/)
|
||||
- Health check logging can now be disabled for quieter logs
|
||||
- Authorization Header JWTs can now be verified by the proxy to skip authentication for machine users
|
||||
- Sessions can now be stored in Redis. This reduces refresh failures and uses smaller cookies (Recommended for those using OIDC refreshing)
|
||||
- Logging overhaul allows customisable logging formats
|
||||
|
||||
## Important Notes
|
||||
- This release includes a number of breaking changes that will require users to
|
||||
reconfigure their proxies. Please read the Breaking Changes below thoroughly.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- [#231](https://github.com/pusher/oauth2_proxy/pull/231) Rework GitLab provider
|
||||
- This PR changes the configuration options for the GitLab provider to use
|
||||
a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than
|
||||
explicit `-login-url`, `-redeem-url` and `-validate-url` parameters.
|
||||
- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent
|
||||
- This PR changes configuration options so that all flags have a config counterpart
|
||||
of the same name but with underscores (`_`) in place of hyphens (`-`).
|
||||
This change affects the following flags:
|
||||
- The `--tls-key` flag is now `--tls-key-file` to be consistent with existing
|
||||
file flags and the existing config and environment settings
|
||||
- The `--tls-cert` flag is now `--tls-cert-file` to be consistent with existing
|
||||
file flags and the existing config and environment settings
|
||||
This change affects the following existing configuration options:
|
||||
- The `proxy-prefix` option is now `proxy_prefix`.
|
||||
This PR changes environment variables so that all flags have an environment
|
||||
counterpart of the same name but capitalised, with underscores (`_`) in place
|
||||
of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`.
|
||||
This change affects the following existing environment variables:
|
||||
- The `OAUTH2_SKIP_OIDC_DISCOVERY` environment variable is now `OAUTH2_PROXY_SKIP_OIDC_DISCOVERY`.
|
||||
- The `OAUTH2_OIDC_JWKS_URL` environment variable is now `OAUTH2_PROXY_OIDC_JWKS_URL`.
|
||||
- [#146](https://github.com/pusher/oauth2_proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field
|
||||
- [#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)
|
||||
- This change modifies the contents of the `X-Forwarded-User` header supplied by the proxy for users where the auth response from the IdP did not contain
|
||||
a username.
|
||||
In that case, this header used to only contain the local part of the user's email address (e.g. `john.doe` for `john.doe@example.com`) but now contains
|
||||
the user's full email address instead.
|
||||
- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Pre-built binary tarballs changed format
|
||||
- The pre-built binary tarballs again match the format of the [bitly](https://github.com/bitly/oauth2_proxy) repository, where the unpacked directory
|
||||
has the same name as the tarball and the binary is always named `oauth2_proxy`. This was done to restore compatibility with third-party automation
|
||||
recipes like https://github.com/jhoblitt/puppet-oauth2_proxy.
|
||||
|
||||
## Changes since v3.2.0
|
||||
|
||||
- [#234](https://github.com/pusher/oauth2_proxy/pull/234) Added option `-ssl-upstream-insecure-skip-validation` to skip validation of upstream SSL certificates (@jansinger)
|
||||
- [#224](https://github.com/pusher/oauth2_proxy/pull/224) Check Google group membership using hasMember to support nested groups and external users (@jpalpant)
|
||||
- [#231](https://github.com/pusher/oauth2_proxy/pull/231) Add optional group membership and email domain checks to the GitLab provider (@Overv)
|
||||
- [#226](https://github.com/pusher/oauth2_proxy/pull/226) Made setting of proxied headers deterministic based on configuration alone (@aeijdenberg)
|
||||
- [#178](https://github.com/pusher/oauth2_proxy/pull/178) Add Silence Ping Logging and Exclude Logging Paths flags (@kskewes)
|
||||
- [#209](https://github.com/pusher/oauth2_proxy/pull/209) Improve docker build caching of layers (@dekimsey)
|
||||
- [#186](https://github.com/pusher/oauth2_proxy/pull/186) Make config consistent (@JoelSpeed)
|
||||
- [#187](https://github.com/pusher/oauth2_proxy/pull/187) Move root packages to pkg folder (@JoelSpeed)
|
||||
- [#65](https://github.com/pusher/oauth2_proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
|
||||
the `-skip-jwt-bearer-token` options. (@brianv0)
|
||||
- Additional verifiers can be configured via the `-extra-jwt-issuers` flag if the JWT issuers is either an OpenID provider or has a JWKS URL
|
||||
(e.g. `https://example.com/.well-known/jwks.json`).
|
||||
- [#180](https://github.com/pusher/oauth2_proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
|
||||
- [#175](https://github.com/pusher/oauth2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
|
||||
- Includes fix for potential signature checking issue when OIDC discovery is skipped.
|
||||
- [#155](https://github.com/pusher/oauth2_proxy/pull/155) Add RedisSessionStore implementation (@brianv0, @JoelSpeed)
|
||||
- Implement flags to configure the redis session store
|
||||
- `-session-store-type=redis` Sets the store type to redis
|
||||
- `-redis-connection-url` Sets the Redis connection URL
|
||||
- `-redis-use-sentinel=true` Enables Redis Sentinel support
|
||||
- `-redis-sentinel-master-name` Sets the Sentinel master name, if sentinel is enabled
|
||||
- `-redis-sentinel-connection-urls` Defines the Redis Sentinel Connection URLs, if sentinel is enabled
|
||||
- Introduces the concept of a session ticket. Tickets are composed of the cookie name, a session ID, and a secret.
|
||||
- Redis Sessions are stored encrypted with a per-session secret
|
||||
- Added tests for server based session stores
|
||||
- [#168](https://github.com/pusher/oauth2_proxy/pull/168) Drop Go 1.11 support in Travis (@JoelSpeed)
|
||||
- [#169](https://github.com/pusher/oauth2_proxy/pull/169) Update Alpine to 3.9 (@kskewes)
|
||||
- [#148](https://github.com/pusher/oauth2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed)
|
||||
- [#147](https://github.com/pusher/oauth2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed)
|
||||
- [#147](https://github.com/pusher/outh2_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
|
||||
@ -101,21 +32,8 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
|
||||
- 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
|
||||
|
||||
|
12
Dockerfile
12
Dockerfile
@ -1,17 +1,15 @@
|
||||
FROM golang:1.12-stretch AS builder
|
||||
|
||||
# Download tools
|
||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
|
||||
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
|
||||
|
||||
# Copy sources
|
||||
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
|
||||
COPY . .
|
||||
|
||||
# Fetch dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN GO111MODULE=on go mod download
|
||||
|
||||
# Now pull in our code
|
||||
COPY . .
|
||||
RUN dep ensure --vendor-only
|
||||
|
||||
# Build binary and make sure there is at least an empty key file.
|
||||
# This is useful for GCP App Engine custom runtime builds, because
|
||||
@ -22,7 +20,7 @@ COPY . .
|
||||
RUN ./configure && make build && touch jwt_signing_key.pem
|
||||
|
||||
# Copy binary to alpine
|
||||
FROM alpine:3.10
|
||||
FROM alpine:3.8
|
||||
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
|
||||
|
@ -1,17 +1,15 @@
|
||||
FROM golang:1.12-stretch AS builder
|
||||
|
||||
# Download tools
|
||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
|
||||
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
|
||||
|
||||
# Copy sources
|
||||
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
|
||||
COPY . .
|
||||
|
||||
# Fetch dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN GO111MODULE=on go mod download
|
||||
|
||||
# Now pull in our code
|
||||
COPY . .
|
||||
RUN dep ensure --vendor-only
|
||||
|
||||
# Build binary and make sure there is at least an empty key file.
|
||||
# This is useful for GCP App Engine custom runtime builds, because
|
||||
@ -22,7 +20,7 @@ COPY . .
|
||||
RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem
|
||||
|
||||
# Copy binary to alpine
|
||||
FROM arm64v8/alpine:3.10
|
||||
FROM arm64v8/alpine:3.8
|
||||
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
|
||||
|
@ -1,17 +1,15 @@
|
||||
FROM golang:1.12-stretch AS builder
|
||||
|
||||
# Download tools
|
||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
|
||||
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
|
||||
|
||||
# Copy sources
|
||||
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
|
||||
COPY . .
|
||||
|
||||
# Fetch dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN GO111MODULE=on go mod download
|
||||
|
||||
# Now pull in our code
|
||||
COPY . .
|
||||
RUN dep ensure --vendor-only
|
||||
|
||||
# Build binary and make sure there is at least an empty key file.
|
||||
# This is useful for GCP App Engine custom runtime builds, because
|
||||
@ -22,7 +20,7 @@ COPY . .
|
||||
RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem
|
||||
|
||||
# Copy binary to alpine
|
||||
FROM arm32v6/alpine:3.10
|
||||
FROM arm32v6/alpine:3.8
|
||||
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
|
||||
|
365
Gopkg.lock
generated
Normal file
365
Gopkg.lock
generated
Normal file
@ -0,0 +1,365 @@
|
||||
# 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
|
48
Gopkg.toml
Normal file
48
Gopkg.toml
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
# 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"
|
@ -1,3 +0,0 @@
|
||||
Joel Speed <joel.speed@hotmail.co.uk> (@JoelSpeed)
|
||||
Dan Bond (@syscll)
|
||||
Henry Jenkins <henry@henryjenkins.name> (@steakunderscore)
|
73
Makefile
73
Makefile
@ -4,7 +4,7 @@ VERSION := $(shell git describe --always --dirty --tags 2>/dev/null || echo "und
|
||||
.NOTPARALLEL:
|
||||
|
||||
.PHONY: all
|
||||
all: lint $(BINARY)
|
||||
all: dep lint $(BINARY)
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@ -15,15 +15,36 @@ 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:
|
||||
GO111MODULE=on $(GOLANGCILINT) run
|
||||
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
|
||||
|
||||
.PHONY: build
|
||||
build: clean $(BINARY)
|
||||
|
||||
$(BINARY):
|
||||
GO111MODULE=on CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/pusher/oauth2_proxy
|
||||
CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/pusher/oauth2_proxy
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
@ -54,34 +75,24 @@ docker-push-all: docker-push
|
||||
docker push quay.io/pusher/oauth2_proxy:${VERSION}-armv6
|
||||
|
||||
.PHONY: test
|
||||
test: lint
|
||||
GO111MODULE=on $(GO) test -v -race ./...
|
||||
test: dep lint
|
||||
$(GO) test -v -race ./...
|
||||
|
||||
.PHONY: release
|
||||
release: lint test
|
||||
mkdir release
|
||||
mkdir release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)
|
||||
mkdir release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)
|
||||
mkdir release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)
|
||||
mkdir release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)
|
||||
mkdir release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)
|
||||
GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
|
||||
-o release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
|
||||
GO111MODULE=on GOOS=linux GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
|
||||
-o release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
|
||||
GO111MODULE=on GOOS=linux GOARCH=arm64 go build -ldflags="-X main.VERSION=${VERSION}" \
|
||||
-o release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
|
||||
GO111MODULE=on GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X main.VERSION=${VERSION}" \
|
||||
-o release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
|
||||
GO111MODULE=on GOOS=windows GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \
|
||||
-o release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy
|
||||
shasum -a 256 release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).darwin-amd64-sha256sum.txt
|
||||
shasum -a 256 release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-amd64-sha256sum.txt
|
||||
shasum -a 256 release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-arm64-sha256sum.txt
|
||||
shasum -a 256 release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-armv6-sha256sum.txt
|
||||
shasum -a 256 release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).windows-amd64-sha256sum.txt
|
||||
tar -C release -czvf release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)
|
||||
tar -C release -czvf release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)
|
||||
tar -C release -czvf release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)
|
||||
tar -C release -czvf release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)
|
||||
tar -C release -czvf release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)
|
||||
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
|
||||
|
@ -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 `v4.0.0`)
|
||||
a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v3.2.0`)
|
||||
|
||||
b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin`
|
||||
|
||||
@ -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-4.0.0.linux-amd64: OK
|
||||
oauth2_proxy-3.2.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,10 +38,6 @@ 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.
|
||||
|
@ -1,4 +1,4 @@
|
||||
package requests
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/bitly/go-simplejson"
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
// Request parses the request body into a simplejson.Json object
|
@ -1,4 +1,4 @@
|
||||
package requests
|
||||
package api
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
16
configure
vendored
16
configure
vendored
@ -13,9 +13,13 @@ 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}<path_to_dep_binary>${NC}\n"
|
||||
printf " --with-go=${BLUE}<path_to_go_binary>${NC}\n"
|
||||
exit 0
|
||||
;;
|
||||
@ -77,7 +81,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.12
|
||||
vercomp $GO_VERSION 1.11
|
||||
case $? in
|
||||
0) ;&
|
||||
1)
|
||||
@ -87,7 +91,7 @@ check_go_version() {
|
||||
;;
|
||||
2)
|
||||
printf "${RED}"
|
||||
echo "$GO_VERSION < 1.12"
|
||||
echo "$GO_VERSION < 1.11"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@ -112,14 +116,16 @@ check_go_env() {
|
||||
|
||||
cd ${0%/*}
|
||||
|
||||
rm -fv .env
|
||||
if [ ! -f .env ]; then
|
||||
rm .env
|
||||
fi
|
||||
|
||||
check_for make
|
||||
check_for awk
|
||||
check_for go
|
||||
check_go_version
|
||||
check_go_env
|
||||
check_for golangci-lint
|
||||
check_for dep
|
||||
|
||||
echo
|
||||
|
||||
@ -127,7 +133,7 @@ cat <<- EOF > .env
|
||||
MAKE := "${tools[make]}"
|
||||
GO := "${tools[go]}"
|
||||
GO_VERSION := ${tools[go_version]}
|
||||
GOLANGCILINT := "${tools[golangci-lint]}"
|
||||
DEP := "${tools[dep]}"
|
||||
EOF
|
||||
|
||||
echo "Environment configuration written to .env"
|
||||
|
@ -1,5 +1,5 @@
|
||||
## OAuth2 Proxy Config File
|
||||
## https://github.com/pusher/oauth2_proxy
|
||||
## https://github.com/bitly/oauth2_proxy
|
||||
|
||||
## <addr>:<port> to listen on for HTTP/HTTPS clients
|
||||
# http_address = "127.0.0.1:4180"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package encryption
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
@ -1,4 +1,4 @@
|
||||
package encryption
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
@ -1,4 +1,4 @@
|
||||
package encryption
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
@ -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]({{ site.gitweb }}/CHANGELOG.md).
|
||||
A list of changes can be seen in the [CHANGELOG](CHANGELOG.md).
|
||||
|
||||
[![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy)
|
||||
|
||||
|
@ -15,7 +15,6 @@ Valid providers are :
|
||||
- [Azure](#azure-auth-provider)
|
||||
- [Facebook](#facebook-auth-provider)
|
||||
- [GitHub](#github-auth-provider)
|
||||
- [Keycloak](#keycloak-auth-provider)
|
||||
- [GitLab](#gitlab-auth-provider)
|
||||
- [LinkedIn](#linkedin-auth-provider)
|
||||
- [login.gov](#logingov-provider)
|
||||
@ -102,31 +101,15 @@ If you are using GitHub enterprise, make sure you set the following to the appro
|
||||
-redeem-url="http(s)://<enterprise github host>/login/oauth/access_token"
|
||||
-validate-url="http(s)://<enterprise github host>/api/v3"
|
||||
|
||||
### Keycloak Auth Provider
|
||||
|
||||
1. Create new client in your Keycloak with **Access Type** 'confidental'.
|
||||
2. Create a mapper with **Mapper Type** 'Group Membership'.
|
||||
|
||||
Make sure you set the following to the appropriate url:
|
||||
|
||||
-provider=keycloak
|
||||
-client-id=<client you have created>
|
||||
-client-secret=<your client's secret>
|
||||
-login-url="http(s)://<keycloak host>/realms/<your realm>/protocol/openid-connect/auth"
|
||||
-redeem-url="http(s)://<keycloak host>/realms/master/<your realm>/openid-connect/auth/token"
|
||||
-validate-url="http(s)://<keycloak host>/realms/master/<your realm>/openid-connect/userinfo"
|
||||
|
||||
### GitLab Auth Provider
|
||||
|
||||
Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](https://docs.gitlab.com/ce/integration/oauth_provider.html). Make sure to enable at least the `openid`, `profile` and `email` scopes.
|
||||
|
||||
Restricting by group membership is possible with the following option:
|
||||
|
||||
-gitlab-group="": restrict logins to members of any of these groups (slug), separated by a comma
|
||||
Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](http://doc.gitlab.com/ce/integration/oauth_provider.html)
|
||||
|
||||
If you are using self-hosted GitLab, make sure you set the following to the appropriate URL:
|
||||
|
||||
-oidc-issuer-url="<your gitlab url>"
|
||||
-login-url="<your gitlab url>/oauth/authorize"
|
||||
-redeem-url="<your gitlab url>/oauth/token"
|
||||
-validate-url="<your gitlab url>/api/v4/user"
|
||||
|
||||
### LinkedIn Auth Provider
|
||||
|
||||
@ -141,7 +124,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://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
|
||||
For adding an application to the Microsoft Azure AD follow [these steps to add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/).
|
||||
|
||||
Take note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server.
|
||||
|
||||
@ -161,56 +144,6 @@ 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.
|
||||
@ -292,7 +225,7 @@ To authorize by email domain use `--email-domain=yourcompany.com`. To authorize
|
||||
|
||||
## Adding a new Provider
|
||||
|
||||
Follow the examples in the [`providers` package]({{ site.gitweb }}/providers/) to define a new
|
||||
Follow the examples in the [`providers` package](providers/) to define a new
|
||||
`Provider` instance. Add a new `case` to
|
||||
[`providers.New()`]({{ site.gitweb }}/providers/providers.go) to allow `oauth2_proxy` to use the
|
||||
[`providers.New()`](providers/providers.go) to allow `oauth2_proxy` to use the
|
||||
new `Provider`.
|
||||
|
@ -9,38 +9,38 @@ nav_order: 4
|
||||
|
||||
There are two recommended configurations.
|
||||
|
||||
1. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert-file=/path/to/cert.pem` and `--tls-key-file=/path/to/cert.key`.
|
||||
1. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert=/path/to/cert.pem` and `--tls-key=/path/to/cert.key`.
|
||||
|
||||
The command line to run `oauth2_proxy` in this configuration would look like this:
|
||||
The command line to run `oauth2_proxy` in this configuration would look like this:
|
||||
|
||||
```bash
|
||||
./oauth2_proxy \
|
||||
```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 \
|
||||
--tls-cert=/path/to/cert.pem \
|
||||
--tls-key=/path/to/cert.key \
|
||||
--cookie-secret=... \
|
||||
--cookie-secure=true \
|
||||
--provider=... \
|
||||
--client-id=... \
|
||||
--client-secret=...
|
||||
```
|
||||
```
|
||||
|
||||
2. Configure SSL Termination with [Nginx](http://nginx.org/) (example config below), Amazon ELB, Google Cloud Platform Load Balancing, or ....
|
||||
|
||||
Because `oauth2_proxy` listens on `127.0.0.1:4180` by default, to listen on all interfaces (needed when using an
|
||||
external load balancer like Amazon ELB or Google Platform Load Balancing) use `--http-address="0.0.0.0:4180"` or
|
||||
`--http-address="http://:4180"`.
|
||||
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 {
|
||||
```
|
||||
server {
|
||||
listen 443 default ssl;
|
||||
server_name internal.yourcompany.com;
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
@ -56,13 +56,13 @@ There are two recommended configurations.
|
||||
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 \
|
||||
```bash
|
||||
./oauth2_proxy \
|
||||
--email-domain="yourcompany.com" \
|
||||
--upstream=http://127.0.0.1:8080/ \
|
||||
--cookie-secret=... \
|
||||
@ -70,4 +70,4 @@ There are two recommended configurations.
|
||||
--provider=... \
|
||||
--client-id=... \
|
||||
--client-secret=...
|
||||
```
|
||||
```
|
||||
|
@ -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`]({{ site.gitweb }}/oauthproxy.go).
|
||||
in `oauthproxy.go`](./oauthproxy.go).
|
||||
|
||||
`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`)
|
||||
|
||||
|
@ -208,7 +208,7 @@ GEM
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
minitest (5.11.3)
|
||||
multipart-post (2.0.0)
|
||||
nokogiri (1.10.4)
|
||||
nokogiri (1.10.1)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
octokit (4.13.0)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
|
@ -18,7 +18,6 @@ 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
|
||||
|
@ -14,101 +14,89 @@ To generate a strong cookie secret use `python -c 'import os,base64; print base6
|
||||
|
||||
### Config File
|
||||
|
||||
An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/oauth2_proxy.cfg`
|
||||
An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/oauth2_proxy.cfg`
|
||||
|
||||
### Command Line Options
|
||||
|
||||
| Option | Type | Description | Default |
|
||||
| ------ | ---- | ----------- | ------- |
|
||||
| `-acr-values` | string | optional, used by login.gov | `"http://idmanagement.gov/ns/assurance/loa/1"` |
|
||||
| `-approval-prompt` | string | OAuth approval_prompt | `"force"` |
|
||||
| `-auth-logging` | bool | Log authentication attempts | true |
|
||||
| `-auth-logging-format` | string | Template for authentication log lines | see [Logging Configuration](#logging-configuration) |
|
||||
| `-authenticated-emails-file` | string | authenticate against emails via file (one per line) | |
|
||||
| `-azure-tenant string` | string | go to a tenant-specific or common (tenant-independent) endpoint. | `"common"` |
|
||||
| `-basic-auth-password` | string | the password to set when passing the HTTP Basic Auth header | |
|
||||
| `-client-id` | string | the OAuth Client ID: ie: `"123456.apps.googleusercontent.com"` | |
|
||||
| `-client-secret` | string | the OAuth Client Secret | |
|
||||
| `-config` | string | path to config file | |
|
||||
| `-cookie-domain` | string | an optional cookie domain to force cookies to (ie: `.yourcompany.com`) | |
|
||||
| `-cookie-expire` | duration | expire timeframe for cookie | 168h0m0s |
|
||||
| `-cookie-httponly` | bool | set HttpOnly cookie flag | true |
|
||||
| `-cookie-name` | string | the name of the cookie that the oauth_proxy creates | `"_oauth2_proxy"` |
|
||||
| `-cookie-path` | string | an optional cookie path to force cookies to (ie: `/poc/`) | `"/"` |
|
||||
| `-cookie-refresh` | duration | refresh the cookie after this duration; `0` to disable | |
|
||||
| `-cookie-secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
|
||||
| `-cookie-secure` | bool | set secure (HTTPS) cookie flag | true |
|
||||
| `-custom-templates-dir` | string | path to custom html templates | |
|
||||
| `-display-htpasswd-form` | bool | display username / password login form if an htpasswd file is provided | true |
|
||||
| `-email-domain` | string | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | |
|
||||
| `-extra-jwt-issuers` | string | if `-skip-jwt-bearer-tokens` is set, a list of extra JWT `issuer=audience` pairs (where the issuer URL has a `.well-known/openid-configuration` or a `.well-known/jwks.json`) | |
|
||||
| `-exclude-logging-paths` | string | comma separated list of paths to exclude from logging, eg: `"/ping,/path2"` |`""` (no paths excluded) |
|
||||
| `-flush-interval` | duration | period between flushing response buffers when streaming responses | `"1s"` |
|
||||
| `-banner` | string | custom banner string. Use `"-"` to disable default banner. | |
|
||||
| `-footer` | string | custom footer string. Use `"-"` to disable default footer. | |
|
||||
| `-gcp-healthchecks` | bool | will enable `/liveness_check`, `/readiness_check`, and `/` (with the proper user-agent) endpoints that will make it work well with GCP App Engine and GKE Ingresses | false |
|
||||
| `-github-org` | string | restrict logins to members of this organisation | |
|
||||
| `-github-team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
|
||||
| `-gitlab-group` | string | restrict logins to members of any of these groups (slug), separated by a comma | |
|
||||
| `-google-admin-email` | string | the google admin to impersonate for api calls | |
|
||||
| `-google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
|
||||
| `-google-service-account-json` | string | the path to the service account json credentials | |
|
||||
| `-htpasswd-file` | string | additionally authenticate against a htpasswd file. Entries must be created with `htpasswd -s` for SHA encryption | |
|
||||
| `-http-address` | string | `[http://]<addr>:<port>` or `unix://<path>` to listen on for HTTP clients | `"127.0.0.1:4180"` |
|
||||
| `-https-address` | string | `<addr>:<port>` to listen on for HTTPS clients | `":443"` |
|
||||
| `-logging-compress` | bool | Should rotated log files be compressed using gzip | false |
|
||||
| `-logging-filename` | string | File to log requests to, empty for `stdout` | `""` (stdout) |
|
||||
| `-logging-local-time` | bool | Use local time in log files and backup filenames instead of UTC | true (local time) |
|
||||
| `-logging-max-age` | int | Maximum number of days to retain old log files | 7 |
|
||||
| `-logging-max-backups` | int | Maximum number of old log files to retain; 0 to disable | 0 |
|
||||
| `-logging-max-size` | int | Maximum size in megabytes of the log file before rotation | 100 |
|
||||
| `-jwt-key` | string | private key in PEM format used to sign JWT, so that you can say something like `-jwt-key="${OAUTH2_PROXY_JWT_KEY}"`: required by login.gov | |
|
||||
| `-jwt-key-file` | string | path to the private key file in PEM format used to sign the JWT so that you can say something like `-jwt-key-file=/etc/ssl/private/jwt_signing_key.pem`: required by login.gov | |
|
||||
| `-login-url` | string | Authentication endpoint | |
|
||||
| `-insecure-oidc-allow-unverified-email` | bool | don't fail if an email address in an id_token is not verified | false |
|
||||
| `-oidc-issuer-url` | string | the OpenID Connect issuer URL. ie: `"https://accounts.google.com"` | |
|
||||
| `-oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
|
||||
| `-pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header | false |
|
||||
| `-pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
|
||||
| `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream | true |
|
||||
| `-pass-host-header` | bool | pass the request Host Header to upstream | true |
|
||||
| `-pass-user-headers` | bool | pass X-Forwarded-User and X-Forwarded-Email information to upstream | true |
|
||||
| `-profile-url` | string | Profile access endpoint | |
|
||||
| `-provider` | string | OAuth provider | google |
|
||||
| `-ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
|
||||
| `-proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
|
||||
| `-proxy-websockets` | bool | enables WebSocket proxying | true |
|
||||
| `-pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | |
|
||||
| `-redeem-url` | string | Token redemption endpoint | |
|
||||
| `-redirect-url` | string | the OAuth Redirect URL. ie: `"https://internalapp.yourcompany.com/oauth2/callback"` | |
|
||||
| `-redis-connection-url` | string | URL of redis server for redis session storage (eg: `redis://HOST[:PORT]`) | |
|
||||
| `-redis-sentinel-master-name` | string | Redis sentinel master name. Used in conjunction with `--redis-use-sentinel` | |
|
||||
| `-redis-sentinel-connection-urls` | string \| list | List of Redis sentinel connection URLs (eg `redis://HOST[:PORT]`). Used in conjunction with `--redis-use-sentinel` | |
|
||||
| `-redis-use-sentinel` | bool | Connect to redis via sentinels. Must set `--redis-sentinel-master-name` and `--redis-sentinel-connection-urls` to use this feature | false |
|
||||
| `-request-logging` | bool | Log requests | true |
|
||||
| `-request-logging-format` | string | Template for request log lines | see [Logging Configuration](#logging-configuration) |
|
||||
| `-resource` | string | The resource that is protected (Azure AD only) | |
|
||||
| `-scope` | string | OAuth scope specification | |
|
||||
| `-session-store-type` | string | Session data storage backend | cookie |
|
||||
| `-set-xauthrequest` | bool | set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) | false |
|
||||
| `-set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
|
||||
| `-signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
|
||||
| `-silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
|
||||
| `-skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
|
||||
| `-skip-auth-regex` | string | bypass authentication for requests paths that match (may be given multiple times) | |
|
||||
| `-skip-jwt-bearer-tokens` | bool | will skip requests that have verified JWT bearer tokens | false |
|
||||
| `-skip-oidc-discovery` | bool | bypass OIDC endpoint discovery. `-login-url`, `-redeem-url` and `-oidc-jwks-url` must be configured in this case | false |
|
||||
| `-skip-provider-button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
|
||||
| `-ssl-insecure-skip-verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
|
||||
| `-ssl-upstream-insecure-skip-verify` | bool | skip validation of certificates presented when using HTTPS upstreams | false |
|
||||
| `-standard-logging` | bool | Log standard runtime information | true |
|
||||
| `-standard-logging-format` | string | Template for standard log lines | see [Logging Configuration](#logging-configuration) |
|
||||
| `-tls-cert-file` | string | path to certificate file | |
|
||||
| `-tls-key-file` | string | path to private key file | |
|
||||
| `-upstream` | string \| list | the http url(s) of the upstream endpoint or `file://` paths for static files. Routing is based on the path | |
|
||||
| `-validate-url` | string | Access token validation endpoint | |
|
||||
| `-version` | n/a | print version string | |
|
||||
| `-whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | |
|
||||
```
|
||||
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://]<addr>:<port> or unix://<path> to listen on for HTTP clients (default "127.0.0.1:4180")
|
||||
-https-address string: <addr>:<port> to listen on for HTTPS clients (default ":443")
|
||||
-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. /<oauth2>/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)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@ -124,16 +112,17 @@ Multiple upstreams can either be configured by supplying a comma separated list
|
||||
|
||||
### Environment variables
|
||||
|
||||
Every command line argument can be specified as an environment variable by
|
||||
prefixing it with `OAUTH2_PROXY_`, capitalising it, and replacing hypens (`-`)
|
||||
with underscores (`_`). If the argument can be specified multiple times, the
|
||||
environment variable should be plural (trailing `S`).
|
||||
The following environment variables can be used in place of the corresponding command-line arguments:
|
||||
|
||||
This is particularly useful for storing secrets outside of a configuration file
|
||||
or the command line.
|
||||
|
||||
For example, the `--cookie-secret` flag becomes `OAUTH2_PROXY_COOKIE_SECRET`,
|
||||
and the `--email-domain` flag becomes `OAUTH2_PROXY_EMAIL_DOMAINS`.
|
||||
- `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
|
||||
|
||||
@ -145,8 +134,6 @@ 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:
|
||||
|
||||
@ -319,5 +306,3 @@ 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=".
|
||||
|
@ -15,8 +15,7 @@ The OAuth2 Proxy uses a Cookie to track user sessions and will store the session
|
||||
data in one of the available session storage backends.
|
||||
|
||||
At present the available backends are (as passed to `--session-store-type`):
|
||||
- [cookie](#cookie-storage) (default)
|
||||
- [redis](#redis-storage)
|
||||
- [cookie](cookie-storage) (deafult)
|
||||
|
||||
### Cookie Storage
|
||||
|
||||
@ -33,35 +32,3 @@ 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.
|
||||
|
87
go.mod
87
go.mod
@ -1,87 +0,0 @@
|
||||
module github.com/pusher/oauth2_proxy
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.41.0 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/OpenPeeDeeP/depguard v1.0.0 // indirect
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||
github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5
|
||||
github.com/bitly/go-simplejson v0.5.0
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
|
||||
github.com/coreos/bbolt v1.3.3 // indirect
|
||||
github.com/coreos/etcd v3.3.13+incompatible // indirect
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/go-critic/go-critic v0.3.4 // indirect
|
||||
github.com/go-kit/kit v0.9.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/go-redis/redis v6.15.2+incompatible
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
|
||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
|
||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
|
||||
github.com/golangci/golangci-lint v1.17.1 // indirect
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.0.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.4 // indirect
|
||||
github.com/kisielk/errcheck v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.7.2 // indirect
|
||||
github.com/klauspost/cpuid v1.2.1 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kr/pty v1.1.8 // indirect
|
||||
github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946 // indirect
|
||||
github.com/magiconair/properties v1.8.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.2 // indirect
|
||||
github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e
|
||||
github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect
|
||||
github.com/onsi/ginkgo v1.8.0
|
||||
github.com/onsi/gomega v1.5.0
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 // indirect
|
||||
github.com/prometheus/common v0.6.0 // indirect
|
||||
github.com/prometheus/procfs v0.0.3 // indirect
|
||||
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.3.0 // indirect
|
||||
github.com/russross/blackfriday v2.0.0+incompatible // indirect
|
||||
github.com/shirou/gopsutil v2.18.12+incompatible // indirect
|
||||
github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cobra v0.0.5 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822 // indirect
|
||||
github.com/ugorji/go v1.1.7 // indirect
|
||||
github.com/valyala/fasthttp v1.4.0 // indirect
|
||||
github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997
|
||||
go.etcd.io/bbolt v1.3.3 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
|
||||
golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f // indirect
|
||||
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190711165009-e47acb2ca7f9 // indirect
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
|
||||
golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d // indirect
|
||||
google.golang.org/api v0.7.0
|
||||
google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 // indirect
|
||||
google.golang.org/grpc v1.22.0 // indirect
|
||||
gopkg.in/fsnotify/fsnotify.v1 v1.2.11
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3
|
||||
gopkg.in/square/go-jose.v2 v2.1.3
|
||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect
|
||||
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect
|
||||
)
|
531
go.sum
531
go.sum
@ -1,531 +0,0 @@
|
||||
cloud.google.com/go v0.16.0 h1:alV/SO2XpH+lrvqjDl94dYez7FfeT8ptayazgWwHPIU=
|
||||
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.41.0 h1:NFvqUTDnSNYPX5oReekmB+D+90jrJIcVImxQ3qrBVgM=
|
||||
cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg=
|
||||
github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
|
||||
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U=
|
||||
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5 h1:+xnalaRl7JEs6xynGsLgGilz75ljDYZTFKuCadGquPY=
|
||||
github.com/alicebob/miniredis v0.0.0-20190417180845-3d7aa1333af5/go.mod h1:8cBZ4R1fh1lx8l4UVit3jNxyybdDi+rjnukCwTYVQE0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
||||
github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc=
|
||||
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
|
||||
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
|
||||
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
|
||||
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
|
||||
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
|
||||
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
||||
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
|
||||
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
|
||||
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
|
||||
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
|
||||
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
||||
github.com/go-toolsmith/astinfo v1.0.0/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
||||
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
|
||||
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
|
||||
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
|
||||
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
|
||||
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
|
||||
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
|
||||
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
|
||||
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w=
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
|
||||
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 h1:MRhC9XbUjE6XDOInSJ8pwHuPagqsyO89QDU9IdVhe3o=
|
||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
|
||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
|
||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
|
||||
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc=
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
|
||||
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc h1:XRFao922N8F3EcIXBSNX8Iywk+GI0dxD/8FicMX2D/c=
|
||||
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
||||
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4=
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk=
|
||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
|
||||
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM=
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.2 h1:OZ4/Q9Lt9bzdyyjAgAWzJfL5dSwPrbkN+6UOHwYeJDM=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.2/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.7.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e h1:eMYU396eZUQ/ex49JNVJOEhShOhQe3Lf/opF61nFtlA=
|
||||
github.com/mbland/hmacauth v0.0.0-20170912224942-107c17adcc5e/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||
github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05 h1:9cELXrXqZu2sczHBZHRpZ+84SR27+yXSKb1MBiUaPhA=
|
||||
github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05/go.mod h1:zHtCks/HQvOt8ATyfwVe3JJq2PPuImzXINPRTC03+9w=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 h1:0XM1XL/OFFJjXsYXlG30spTkV/E9+gmd5GD1w2HE8xM=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/prometheus/tsdb v0.9.1/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4=
|
||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
|
||||
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ=
|
||||
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822 h1:uVnVN3IUKAVcB3xG26bThgwXkWaGFc9i5qFHYKy4TKc=
|
||||
github.com/timakin/bodyclose v0.0.0-20190713050349-d96ec0dee822/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||
github.com/valyala/fasthttp v1.4.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 h1:1+FQ4Ns+UZtUiQ4lP0sTCyKSQ0EXoiwAdHZB0Pd5t9Q=
|
||||
github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997/go.mod h1:DIGbh/f5XMAessMV/uaIik81gkDVjUeQ9ApdaU7wRKE=
|
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583 h1:SZPG5w7Qxq7bMcMVl6e3Ht2X7f+AAGQdzjkbyOnNNZ8=
|
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3 h1:f4/ZD59VsBOaJmWeI2yqtHvJhmRRPzi73C88ZtfhAIk=
|
||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190711165009-e47acb2ca7f9/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24 h1:nP0LlV1P7+z/qtbjHygz+Bba7QsbB4MqdhGJmAyicuI=
|
||||
golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190506115046-ca7f33d4116e h1:bq5BY1tGuaK8HxuwN6pT6kWgTVLeJ5KwuyBpsl1CZL4=
|
||||
golang.org/x/sys v0.0.0-20190506115046-ca7f33d4116e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d h1:JZPFnINSinLUdC0BJDoKhrky8niIzXMPIY2oR07+I+E=
|
||||
golang.org/x/tools v0.0.0-20190712213246-8b927904ee0d/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
google.golang.org/api v0.0.0-20171116170945-8791354e7ab1 h1:g6iAMpIfX2EaDmaU3Nm8KcWAuf9yDiM3uE5a7/9gZao=
|
||||
google.golang.org/api v0.0.0-20171116170945-8791354e7ab1/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/appengine v1.0.0 h1:dN4LljjBKVChsv0XCSI+zbyzdqrkEwX5LQFUMRSGqOc=
|
||||
google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 h1:5pOB7se0B2+IssELuQUs6uoBgYJenkU2AQlvopc2sRw=
|
||||
google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=
|
||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/fsnotify/fsnotify.v1 v1.2.11 h1:m6l7lekIm1I7UEqyXifLaHywMLxwlWea1o3zUGDGQHs=
|
||||
gopkg.in/fsnotify/fsnotify.v1 v1.2.11/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3 h1:AFxeG48hTWHhDTQDk/m2gorfVHUEa9vo3tp3D7TzwjI=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.1.3 h1:/FoFBTvlJN6MTTVCe9plTOG+YydzkjvDGxiSPzIyoDM=
|
||||
gopkg.in/square/go-jose.v2 v2.1.3/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g=
|
||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o=
|
||||
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
@ -7,7 +7,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
|
2
http.go
2
http.go
@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
// Server represents an HTTP server
|
||||
|
27
http_test.go
27
http_test.go
@ -8,9 +8,6 @@ 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"))
|
||||
@ -19,8 +16,8 @@ func TestGCPHealthcheckLiveness(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/liveness_check", nil)
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
assert.Equal(t, 200, rw.Code)
|
||||
@ -35,8 +32,8 @@ func TestGCPHealthcheckReadiness(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/readiness_check", nil)
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
assert.Equal(t, 200, rw.Code)
|
||||
@ -51,8 +48,8 @@ func TestGCPHealthcheckNotHealthcheck(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/not_any_check", nil)
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
assert.Equal(t, "test", rw.Body.String())
|
||||
@ -66,8 +63,8 @@ func TestGCPHealthcheckIngress(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/", nil)
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
@ -83,8 +80,8 @@ func TestGCPHealthcheckNotIngress(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/foo", nil)
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
@ -99,8 +96,8 @@ func TestGCPHealthcheckNotIngressPut(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("PUT", "/", nil)
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
|
@ -88,7 +88,6 @@ type Logger struct {
|
||||
stdEnabled bool
|
||||
authEnabled bool
|
||||
reqEnabled bool
|
||||
excludePaths map[string]struct{}
|
||||
stdLogTemplate *template.Template
|
||||
authTemplate *template.Template
|
||||
reqTemplate *template.Template
|
||||
@ -102,7 +101,6 @@ 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)),
|
||||
@ -179,10 +177,6 @@ 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 == "" {
|
||||
@ -308,16 +302,6 @@ 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()
|
||||
@ -381,11 +365,6 @@ 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) {
|
@ -10,7 +10,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status
|
||||
@ -75,19 +75,18 @@ func (l *responseLogger) Status() int {
|
||||
return l.status
|
||||
}
|
||||
|
||||
// Size returns the response size
|
||||
// Size returns teh 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 LoggingHandler
|
||||
// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its friends
|
||||
type loggingHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
func TestLoggingHandler_ServeHTTP(t *testing.T) {
|
||||
@ -17,23 +17,10 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
Format,
|
||||
ExpectedLogMessage,
|
||||
Path string
|
||||
ExcludePaths []string
|
||||
SilencePingLogging bool
|
||||
ExpectedLogMessage string
|
||||
}{
|
||||
{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},
|
||||
{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"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@ -49,13 +36,9 @@ 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", test.Path, nil)
|
||||
r, _ := http.NewRequest("GET", "/foo/bar", nil)
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
|
||||
|
34
main.go
34
main.go
@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
options "github.com/mreiferson/go-options"
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -23,17 +23,15 @@ 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://]<addr>:<port> or unix://<path> to listen on for HTTP clients")
|
||||
flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
|
||||
flagSet.String("tls-cert-file", "", "path to certificate file")
|
||||
flagSet.String("tls-key-file", "", "path to private key file")
|
||||
flagSet.String("tls-cert", "", "path to certificate file")
|
||||
flagSet.String("tls-key", "", "path to private key file")
|
||||
flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
|
||||
flagSet.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")
|
||||
@ -47,21 +45,14 @@ 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 providers")
|
||||
flagSet.Bool("ssl-upstream-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS upstreams")
|
||||
flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS")
|
||||
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("keycloak-group", "", "restrict login to members of this group.")
|
||||
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
|
||||
flagSet.String("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")
|
||||
@ -71,10 +62,8 @@ 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. /<oauth2>/sign_in)")
|
||||
flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks")
|
||||
flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying")
|
||||
|
||||
flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates")
|
||||
@ -87,10 +76,6 @@ 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")
|
||||
@ -104,15 +89,12 @@ 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")
|
||||
@ -158,13 +140,7 @@ func main() {
|
||||
validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile)
|
||||
oauthproxy := NewOAuthProxy(opts, validator)
|
||||
|
||||
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) != 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] != "*" {
|
||||
|
481
oauthproxy.go
481
oauthproxy.go
@ -1,8 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
b64 "encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -15,11 +13,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/mbland/hmacauth"
|
||||
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/cookie"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
"github.com/pusher/oauth2_proxy/providers"
|
||||
"github.com/yhat/wsutil"
|
||||
)
|
||||
@ -32,6 +29,10 @@ const (
|
||||
httpScheme = "http"
|
||||
httpsScheme = "https"
|
||||
|
||||
// Cookies are limited to 4kb including the length of the cookie name,
|
||||
// the cookie name can be up to 256 bytes
|
||||
maxCookieLength = 3840
|
||||
|
||||
applicationJSON = "application/json"
|
||||
)
|
||||
|
||||
@ -50,11 +51,6 @@ 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
|
||||
@ -79,7 +75,6 @@ type OAuthProxy struct {
|
||||
redirectURL *url.URL // the url to receive requests at
|
||||
whitelistDomains []string
|
||||
provider providers.Provider
|
||||
sessionStore sessionsapi.SessionStore
|
||||
ProxyPrefix string
|
||||
SignInMessage string
|
||||
HtpasswdFile *HtpasswdFile
|
||||
@ -93,13 +88,11 @@ type OAuthProxy struct {
|
||||
PassAccessToken bool
|
||||
SetAuthorization bool
|
||||
PassAuthorization bool
|
||||
CookieCipher *cookie.Cipher
|
||||
skipAuthRegex []string
|
||||
skipAuthPreflight bool
|
||||
skipJwtBearerTokens bool
|
||||
jwtBearerVerifiers []*oidc.IDTokenVerifier
|
||||
compiledRegex []*regexp.Regexp
|
||||
templates *template.Template
|
||||
Banner string
|
||||
Footer string
|
||||
}
|
||||
|
||||
@ -129,14 +122,9 @@ 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, opts *Options) (proxy *httputil.ReverseProxy) {
|
||||
func NewReverseProxy(target *url.URL, flushInterval time.Duration) (proxy *httputil.ReverseProxy) {
|
||||
proxy = httputil.NewSingleHostReverseProxy(target)
|
||||
proxy.FlushInterval = opts.FlushInterval
|
||||
if opts.SSLUpstreamInsecureSkipVerify {
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
}
|
||||
proxy.FlushInterval = flushInterval
|
||||
return proxy
|
||||
}
|
||||
|
||||
@ -167,9 +155,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) http.Handler {
|
||||
func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) (restProxy http.Handler) {
|
||||
u.Path = ""
|
||||
proxy := NewReverseProxy(u, opts)
|
||||
proxy := NewReverseProxy(u, opts.FlushInterval)
|
||||
if !opts.PassHostHeader {
|
||||
setProxyUpstreamHostHeader(proxy, u)
|
||||
} else {
|
||||
@ -183,12 +171,7 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma
|
||||
wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
|
||||
wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
|
||||
}
|
||||
return &UpstreamProxy{
|
||||
upstream: u.Host,
|
||||
handler: proxy,
|
||||
wsHandler: wsProxy,
|
||||
auth: auth,
|
||||
}
|
||||
return &UpstreamProxy{u.Host, proxy, wsProxy, auth}
|
||||
}
|
||||
|
||||
// NewOAuthProxy creates a new instance of OOuthProxy from the options provided
|
||||
@ -213,13 +196,7 @@ 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)
|
||||
uProxy := UpstreamProxy{
|
||||
upstream: path,
|
||||
handler: proxy,
|
||||
wsHandler: nil,
|
||||
auth: nil,
|
||||
}
|
||||
serveMux.Handle(path, &uProxy)
|
||||
serveMux.Handle(path, &UpstreamProxy{path, proxy, nil, nil})
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
|
||||
}
|
||||
@ -228,12 +205,6 @@ 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)
|
||||
@ -247,6 +218,15 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
||||
|
||||
logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, refresh)
|
||||
|
||||
var cipher *cookie.Cipher
|
||||
if opts.PassAccessToken || opts.SetAuthorization || opts.PassAuthorization || (opts.CookieRefresh != time.Duration(0)) {
|
||||
var err error
|
||||
cipher, err = cookie.NewCipher(secretBytes(opts.CookieSecret))
|
||||
if err != nil {
|
||||
logger.Fatal("cookie-secret error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &OAuthProxy{
|
||||
CookieName: opts.CookieName,
|
||||
CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
|
||||
@ -260,7 +240,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
||||
Validator: validator,
|
||||
|
||||
RobotsPath: "/robots.txt",
|
||||
PingPath: opts.PingPath,
|
||||
PingPath: "/ping",
|
||||
SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
|
||||
SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
|
||||
OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
|
||||
@ -269,14 +249,11 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
||||
|
||||
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,
|
||||
@ -286,8 +263,8 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
||||
SetAuthorization: opts.SetAuthorization,
|
||||
PassAuthorization: opts.PassAuthorization,
|
||||
SkipProviderButton: opts.SkipProviderButton,
|
||||
CookieCipher: cipher,
|
||||
templates: loadTemplates(opts.CustomTemplatesDir),
|
||||
Banner: opts.Banner,
|
||||
Footer: opts.Footer,
|
||||
}
|
||||
}
|
||||
@ -316,7 +293,7 @@ func (p *OAuthProxy) displayCustomLoginForm() bool {
|
||||
return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
|
||||
}
|
||||
|
||||
func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState, err error) {
|
||||
func (p *OAuthProxy) redeemCode(host, code string) (s *sessions.SessionState, err error) {
|
||||
if code == "" {
|
||||
return nil, errors.New("missing code")
|
||||
}
|
||||
@ -339,6 +316,104 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState,
|
||||
return
|
||||
}
|
||||
|
||||
// MakeSessionCookie creates an http.Cookie containing the authenticated user's
|
||||
// authentication details
|
||||
func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie {
|
||||
if value != "" {
|
||||
value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
|
||||
}
|
||||
c := p.makeCookie(req, p.CookieName, value, expiration, now)
|
||||
if len(c.Value) > 4096-len(p.CookieName) {
|
||||
return splitCookie(c)
|
||||
}
|
||||
return []*http.Cookie{c}
|
||||
}
|
||||
|
||||
func copyCookie(c *http.Cookie) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: c.Name,
|
||||
Value: c.Value,
|
||||
Path: c.Path,
|
||||
Domain: c.Domain,
|
||||
Expires: c.Expires,
|
||||
RawExpires: c.RawExpires,
|
||||
MaxAge: c.MaxAge,
|
||||
Secure: c.Secure,
|
||||
HttpOnly: c.HttpOnly,
|
||||
Raw: c.Raw,
|
||||
Unparsed: c.Unparsed,
|
||||
}
|
||||
}
|
||||
|
||||
// splitCookie reads the full cookie generated to store the session and splits
|
||||
// it into a slice of cookies which fit within the 4kb cookie limit indexing
|
||||
// the cookies from 0
|
||||
func splitCookie(c *http.Cookie) []*http.Cookie {
|
||||
if len(c.Value) < maxCookieLength {
|
||||
return []*http.Cookie{c}
|
||||
}
|
||||
cookies := []*http.Cookie{}
|
||||
valueBytes := []byte(c.Value)
|
||||
count := 0
|
||||
for len(valueBytes) > 0 {
|
||||
new := copyCookie(c)
|
||||
new.Name = fmt.Sprintf("%s_%d", c.Name, count)
|
||||
count++
|
||||
if len(valueBytes) < maxCookieLength {
|
||||
new.Value = string(valueBytes)
|
||||
valueBytes = []byte{}
|
||||
} else {
|
||||
newValue := valueBytes[:maxCookieLength]
|
||||
valueBytes = valueBytes[maxCookieLength:]
|
||||
new.Value = string(newValue)
|
||||
}
|
||||
cookies = append(cookies, new)
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
|
||||
// joinCookies takes a slice of cookies from the request and reconstructs the
|
||||
// full session cookie
|
||||
func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) {
|
||||
if len(cookies) == 0 {
|
||||
return nil, fmt.Errorf("list of cookies must be > 0")
|
||||
}
|
||||
if len(cookies) == 1 {
|
||||
return cookies[0], nil
|
||||
}
|
||||
c := copyCookie(cookies[0])
|
||||
for i := 1; i < len(cookies); i++ {
|
||||
c.Value += cookies[i].Value
|
||||
}
|
||||
c.Name = strings.TrimRight(c.Name, "_0")
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// loadCookie retreieves the sessions state cookie from the http request.
|
||||
// If a single cookie is present this will be returned, otherwise it attempts
|
||||
// to reconstruct a cookie split up by splitCookie
|
||||
func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) {
|
||||
c, err := req.Cookie(cookieName)
|
||||
if err == nil {
|
||||
return c, nil
|
||||
}
|
||||
cookies := []*http.Cookie{}
|
||||
err = nil
|
||||
count := 0
|
||||
for err == nil {
|
||||
var c *http.Cookie
|
||||
c, err = req.Cookie(fmt.Sprintf("%s_%d", cookieName, count))
|
||||
if err == nil {
|
||||
cookies = append(cookies, c)
|
||||
count++
|
||||
}
|
||||
}
|
||||
if len(cookies) == 0 {
|
||||
return nil, fmt.Errorf("Could not find cookie %s", cookieName)
|
||||
}
|
||||
return joinCookies(cookies)
|
||||
}
|
||||
|
||||
// MakeCSRFCookie creates a cookie for CSRF
|
||||
func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
|
||||
return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
|
||||
@ -379,18 +454,66 @@ func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, va
|
||||
|
||||
// ClearSessionCookie creates a cookie to unset the user's authentication cookie
|
||||
// stored in the user's session
|
||||
func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
|
||||
return p.sessionStore.Clear(rw, req)
|
||||
func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) {
|
||||
var cookies []*http.Cookie
|
||||
|
||||
// matches CookieName, CookieName_<number>
|
||||
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", p.CookieName))
|
||||
|
||||
for _, c := range req.Cookies() {
|
||||
if cookieNameRegex.MatchString(c.Name) {
|
||||
clearCookie := p.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())
|
||||
|
||||
http.SetCookie(rw, clearCookie)
|
||||
cookies = append(cookies, clearCookie)
|
||||
}
|
||||
}
|
||||
|
||||
// ugly hack because default domain changed
|
||||
if p.CookieDomain == "" && len(cookies) > 0 {
|
||||
clr2 := *cookies[0]
|
||||
clr2.Domain = req.Host
|
||||
http.SetCookie(rw, &clr2)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSessionCookie adds the user's session cookie to the response
|
||||
func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
|
||||
for _, c := range p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()) {
|
||||
http.SetCookie(rw, c)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadCookiedSession reads the user's authentication details from the request
|
||||
func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
|
||||
return p.sessionStore.Load(req)
|
||||
func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessions.SessionState, time.Duration, error) {
|
||||
var age time.Duration
|
||||
c, err := loadCookie(req, p.CookieName)
|
||||
if err != nil {
|
||||
// always http.ErrNoCookie
|
||||
return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
|
||||
}
|
||||
val, timestamp, ok := cookie.Validate(c, p.CookieSeed, p.CookieExpire)
|
||||
if !ok {
|
||||
return nil, age, errors.New("Cookie Signature not valid")
|
||||
}
|
||||
|
||||
session, err := p.provider.SessionFromCookie(val, p.CookieCipher)
|
||||
if err != nil {
|
||||
return nil, age, err
|
||||
}
|
||||
|
||||
age = time.Now().Truncate(time.Second).Sub(timestamp)
|
||||
return session, age, nil
|
||||
}
|
||||
|
||||
// SaveSession creates a new session cookie value and sets this on the response
|
||||
func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
|
||||
return p.sessionStore.Save(rw, req, s)
|
||||
func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
|
||||
value, err := p.provider.CookieForSession(s, p.CookieCipher)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.SetSessionCookie(rw, req, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RobotsTxt disallows scraping pages from the OAuthProxy
|
||||
@ -513,19 +636,20 @@ 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) bool {
|
||||
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok 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) bool {
|
||||
func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) {
|
||||
for _, u := range p.compiledRegex {
|
||||
if u.MatchString(path) {
|
||||
return true
|
||||
ok = u.MatchString(path)
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
func getRemoteAddr(req *http.Request) (s string) {
|
||||
@ -570,7 +694,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
user, ok := p.ManualSignIn(rw, req)
|
||||
if ok {
|
||||
session := &sessionsapi.SessionState{User: user}
|
||||
session := &sessions.SessionState{User: user}
|
||||
p.SaveSession(rw, req, session)
|
||||
http.Redirect(rw, req, redirect, 302)
|
||||
} else {
|
||||
@ -590,7 +714,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 := encryption.Nonce()
|
||||
nonce, err := cookie.Nonce()
|
||||
if err != nil {
|
||||
logger.Printf("Error obtaining nonce: %s", err.Error())
|
||||
p.ErrorPage(rw, 500, "Internal Error", err.Error())
|
||||
@ -669,89 +793,57 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
http.Redirect(rw, req, redirect, 302)
|
||||
} else {
|
||||
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
|
||||
logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "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) {
|
||||
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)
|
||||
status := p.Authenticate(rw, req)
|
||||
if status == http.StatusAccepted {
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
} else {
|
||||
http.Error(rw, "unauthorized request", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
|
||||
status := p.Authenticate(rw, req)
|
||||
if status == http.StatusInternalServerError {
|
||||
p.ErrorPage(rw, http.StatusInternalServerError,
|
||||
"Internal Error", "Internal Error")
|
||||
} else if status == http.StatusForbidden {
|
||||
if p.SkipProviderButton {
|
||||
p.OAuthStart(rw, req)
|
||||
} else {
|
||||
p.SignInPage(rw, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
default:
|
||||
// unknown error
|
||||
logger.Printf("Unexpected internal error: %s", err)
|
||||
p.ErrorPage(rw, http.StatusInternalServerError,
|
||||
"Internal Error", "Internal Error")
|
||||
} else if status == http.StatusUnauthorized {
|
||||
p.ErrorJSON(rw, status)
|
||||
} else {
|
||||
p.serveMux.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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
|
||||
// Authenticate checks whether a user is authenticated
|
||||
func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
|
||||
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)
|
||||
|
||||
session, sessionAge, err := p.LoadCookiedSession(req)
|
||||
if err != nil {
|
||||
logger.Printf("Error loading cookied session: %s", err)
|
||||
}
|
||||
|
||||
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)
|
||||
if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
|
||||
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", sessionAge, session, p.CookieRefresh)
|
||||
saveSession = true
|
||||
}
|
||||
|
||||
if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
|
||||
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
|
||||
@ -759,8 +851,6 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
|
||||
saveSession = true
|
||||
revalidated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if session != nil && session.IsExpired() {
|
||||
logger.Printf("Removing session: token expired %s", session)
|
||||
@ -778,20 +868,18 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
|
||||
}
|
||||
}
|
||||
|
||||
if session != nil && session.Email != "" {
|
||||
if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
|
||||
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 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 nil, err
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
@ -807,83 +895,57 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
return nil, ErrNeedsLogin
|
||||
// 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 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) {
|
||||
// At this point, the user is authenticated. proxy normally
|
||||
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 {
|
||||
if session.AccessToken != "" {
|
||||
if p.PassAccessToken && session.AccessToken != "" {
|
||||
rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
|
||||
} else {
|
||||
rw.Header().Del("X-Auth-Request-Access-Token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.PassAccessToken {
|
||||
if session.AccessToken != "" {
|
||||
if p.PassAccessToken && session.AccessToken != "" {
|
||||
req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
|
||||
} else {
|
||||
req.Header.Del("X-Forwarded-Access-Token")
|
||||
}
|
||||
}
|
||||
|
||||
if p.PassAuthorization {
|
||||
if session.IDToken != "" {
|
||||
if p.PassAuthorization && session.IDToken != "" {
|
||||
req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
|
||||
} else {
|
||||
req.Header.Del("Authorization")
|
||||
}
|
||||
}
|
||||
if p.SetAuthorization {
|
||||
if session.IDToken != "" {
|
||||
if p.SetAuthorization && 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
|
||||
// credentials and authenticates these against the proxies HtpasswdFile
|
||||
func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
|
||||
func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessions.SessionState, error) {
|
||||
if p.HtpasswdFile == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@ -905,14 +967,14 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionStat
|
||||
}
|
||||
if p.HtpasswdFile.Validate(pair[0], pair[1]) {
|
||||
logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
|
||||
return &sessionsapi.SessionState{User: pair[0]}, nil
|
||||
return &sessions.SessionState{User: pair[0]}, nil
|
||||
}
|
||||
logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// isAjax checks if a request is an ajax request
|
||||
func isAjax(req *http.Request) bool {
|
||||
func (p *OAuthProxy) isAjax(req *http.Request) bool {
|
||||
acceptValues, ok := req.Header["accept"]
|
||||
if !ok {
|
||||
acceptValues = req.Header["Accept"]
|
||||
@ -926,97 +988,8 @@ func isAjax(req *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ErrorJSON returns the error code with an application/json mime type
|
||||
// ErrorJSON returns the error code witht 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
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@ -16,11 +14,9 @@ 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"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -122,7 +118,7 @@ func TestNewReverseProxy(t *testing.T) {
|
||||
backendHost := net.JoinHostPort(backendHostname, backendPort)
|
||||
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
|
||||
|
||||
proxyHandler := NewReverseProxy(proxyURL, &Options{FlushInterval: time.Second})
|
||||
proxyHandler := NewReverseProxy(proxyURL, time.Second)
|
||||
setProxyUpstreamHostHeader(proxyHandler, proxyURL)
|
||||
frontend := httptest.NewServer(proxyHandler)
|
||||
defer frontend.Close()
|
||||
@ -144,7 +140,7 @@ func TestEncodedSlashes(t *testing.T) {
|
||||
defer backend.Close()
|
||||
|
||||
b, _ := url.Parse(backend.URL)
|
||||
proxyHandler := NewReverseProxy(b, &Options{FlushInterval: time.Second})
|
||||
proxyHandler := NewReverseProxy(b, time.Second)
|
||||
setProxyDirector(proxyHandler)
|
||||
frontend := httptest.NewServer(proxyHandler)
|
||||
defer frontend.Close()
|
||||
@ -163,9 +159,9 @@ func TestEncodedSlashes(t *testing.T) {
|
||||
|
||||
func TestRobotsTxt(t *testing.T) {
|
||||
opts := NewOptions()
|
||||
opts.ClientID = "asdlkjx"
|
||||
opts.ClientSecret = "alkgks"
|
||||
opts.CookieSecret = "asdkugkj"
|
||||
opts.ClientID = "bazquux"
|
||||
opts.ClientSecret = "foobar"
|
||||
opts.CookieSecret = "xyzzyplugh"
|
||||
opts.Validate()
|
||||
|
||||
proxy := NewOAuthProxy(opts, func(string) bool { return true })
|
||||
@ -178,9 +174,9 @@ func TestRobotsTxt(t *testing.T) {
|
||||
|
||||
func TestIsValidRedirect(t *testing.T) {
|
||||
opts := NewOptions()
|
||||
opts.ClientID = "skdlfj"
|
||||
opts.ClientSecret = "fgkdsgj"
|
||||
opts.CookieSecret = "ljgiogbj"
|
||||
opts.ClientID = "bazquux"
|
||||
opts.ClientSecret = "foobar"
|
||||
opts.CookieSecret = "xyzzyplugh"
|
||||
// Should match domains that are exactly foo.bar and any subdomain of bar.foo
|
||||
opts.WhitelistDomains = []string{"foo.bar", ".bar.foo"}
|
||||
opts.Validate()
|
||||
@ -231,7 +227,6 @@ type TestProvider struct {
|
||||
*providers.ProviderData
|
||||
EmailAddress string
|
||||
ValidToken bool
|
||||
GroupValidator func(string) bool
|
||||
}
|
||||
|
||||
func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
|
||||
@ -256,9 +251,6 @@ func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
|
||||
Scope: "profile.email",
|
||||
},
|
||||
EmailAddress: emailAddress,
|
||||
GroupValidator: func(s string) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,13 +262,6 @@ 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)
|
||||
@ -298,8 +283,8 @@ func TestBasicAuthPassword(t *testing.T) {
|
||||
// The CookieSecret must be 32 bytes in order to create the AES
|
||||
// cipher.
|
||||
opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
|
||||
opts.ClientID = "dlgkj"
|
||||
opts.ClientSecret = "alkgret"
|
||||
opts.ClientID = "bazquux"
|
||||
opts.ClientSecret = "foobar"
|
||||
opts.CookieSecure = false
|
||||
opts.PassBasicAuth = true
|
||||
opts.PassUserHeaders = true
|
||||
@ -392,8 +377,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 = "slgkj"
|
||||
t.opts.ClientSecret = "gfjgojl"
|
||||
t.opts.ClientID = "bazquux"
|
||||
t.opts.ClientSecret = "foobar"
|
||||
t.opts.CookieSecure = false
|
||||
t.opts.PassAccessToken = opts.PassAccessToken
|
||||
t.opts.Validate()
|
||||
@ -518,9 +503,9 @@ func NewSignInPageTest(skipProvider bool) *SignInPageTest {
|
||||
var sipTest SignInPageTest
|
||||
|
||||
sipTest.opts = NewOptions()
|
||||
sipTest.opts.CookieSecret = "adklsj2"
|
||||
sipTest.opts.ClientID = "lkdgj"
|
||||
sipTest.opts.ClientSecret = "sgiufgoi"
|
||||
sipTest.opts.CookieSecret = "foobar"
|
||||
sipTest.opts.ClientID = "bazquux"
|
||||
sipTest.opts.ClientSecret = "xyzzyplugh"
|
||||
sipTest.opts.SkipProviderButton = skipProvider
|
||||
sipTest.opts.Validate()
|
||||
|
||||
@ -615,17 +600,12 @@ type ProcessCookieTestOpts struct {
|
||||
providerValidateCookieResponse bool
|
||||
}
|
||||
|
||||
type OptionsModifier func(*Options)
|
||||
|
||||
func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) *ProcessCookieTest {
|
||||
func NewProcessCookieTest(opts ProcessCookieTestOpts) *ProcessCookieTest {
|
||||
var pcTest ProcessCookieTest
|
||||
|
||||
pcTest.opts = NewOptions()
|
||||
for _, modifier := range modifiers {
|
||||
modifier(pcTest.opts)
|
||||
}
|
||||
pcTest.opts.ClientID = "asdfljk"
|
||||
pcTest.opts.ClientSecret = "lkjfdsig"
|
||||
pcTest.opts.ClientID = "bazquux"
|
||||
pcTest.opts.ClientSecret = "xyzzyplugh"
|
||||
pcTest.opts.CookieSecret = "0123456789abcdefabcd"
|
||||
// First, set the CookieRefresh option so proxy.AesCipher is created,
|
||||
// needed to encrypt the access_token.
|
||||
@ -654,34 +634,32 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
|
||||
})
|
||||
}
|
||||
|
||||
func NewProcessCookieTestWithOptionsModifiers(modifiers ...OptionsModifier) *ProcessCookieTest {
|
||||
return NewProcessCookieTest(ProcessCookieTestOpts{
|
||||
providerValidateCookieResponse: true,
|
||||
}, modifiers...)
|
||||
func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) []*http.Cookie {
|
||||
return p.proxy.MakeSessionCookie(p.req, value, p.opts.CookieExpire, ref)
|
||||
}
|
||||
|
||||
func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState) error {
|
||||
err := p.proxy.SaveSession(p.rw, p.req, s)
|
||||
func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState, ref time.Time) error {
|
||||
value, err := p.proxy.provider.CookieForSession(s, p.proxy.CookieCipher)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cookie := range p.rw.Result().Cookies() {
|
||||
p.req.AddCookie(cookie)
|
||||
for _, c := range p.proxy.MakeSessionCookie(p.req, value, p.proxy.CookieExpire, ref) {
|
||||
p.req.AddCookie(c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, error) {
|
||||
func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, time.Duration, error) {
|
||||
return p.proxy.LoadCookiedSession(p.req)
|
||||
}
|
||||
|
||||
func TestLoadCookiedSession(t *testing.T) {
|
||||
pcTest := NewProcessCookieTestWithDefaults()
|
||||
|
||||
startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token", CreatedAt: time.Now()}
|
||||
pcTest.SaveSession(startSession)
|
||||
startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token"}
|
||||
pcTest.SaveSession(startSession, time.Now())
|
||||
|
||||
session, err := pcTest.LoadCookiedSession()
|
||||
session, _, err := pcTest.LoadCookiedSession()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, startSession.Email, session.Email)
|
||||
assert.Equal(t, "john.doe@example.com", session.User)
|
||||
@ -691,7 +669,7 @@ func TestLoadCookiedSession(t *testing.T) {
|
||||
func TestProcessCookieNoCookieError(t *testing.T) {
|
||||
pcTest := NewProcessCookieTestWithDefaults()
|
||||
|
||||
session, err := pcTest.LoadCookiedSession()
|
||||
session, _, err := pcTest.LoadCookiedSession()
|
||||
assert.Equal(t, "Cookie \"_oauth2_proxy\" not present", err.Error())
|
||||
if session != nil {
|
||||
t.Errorf("expected nil session. got %#v", session)
|
||||
@ -699,31 +677,29 @@ func TestProcessCookieNoCookieError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProcessCookieRefreshNotSet(t *testing.T) {
|
||||
pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
|
||||
opts.CookieExpire = time.Duration(23) * time.Hour
|
||||
})
|
||||
pcTest := NewProcessCookieTestWithDefaults()
|
||||
pcTest.proxy.CookieExpire = time.Duration(23) * time.Hour
|
||||
reference := time.Now().Add(time.Duration(-2) * time.Hour)
|
||||
|
||||
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
|
||||
pcTest.SaveSession(startSession)
|
||||
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
|
||||
pcTest.SaveSession(startSession, reference)
|
||||
|
||||
session, err := pcTest.LoadCookiedSession()
|
||||
session, age, err := pcTest.LoadCookiedSession()
|
||||
assert.Equal(t, nil, err)
|
||||
if session.Age() < time.Duration(-2)*time.Hour {
|
||||
t.Errorf("cookie too young %v", session.Age())
|
||||
if age < time.Duration(-2)*time.Hour {
|
||||
t.Errorf("cookie too young %v", age)
|
||||
}
|
||||
assert.Equal(t, startSession.Email, session.Email)
|
||||
}
|
||||
|
||||
func TestProcessCookieFailIfCookieExpired(t *testing.T) {
|
||||
pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
|
||||
opts.CookieExpire = time.Duration(24) * time.Hour
|
||||
})
|
||||
pcTest := NewProcessCookieTestWithDefaults()
|
||||
pcTest.proxy.CookieExpire = time.Duration(24) * time.Hour
|
||||
reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
|
||||
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
|
||||
pcTest.SaveSession(startSession)
|
||||
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
|
||||
pcTest.SaveSession(startSession, reference)
|
||||
|
||||
session, err := pcTest.LoadCookiedSession()
|
||||
session, _, err := pcTest.LoadCookiedSession()
|
||||
assert.NotEqual(t, nil, err)
|
||||
if session != nil {
|
||||
t.Errorf("expected nil session %#v", session)
|
||||
@ -731,23 +707,22 @@ func TestProcessCookieFailIfCookieExpired(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
|
||||
pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
|
||||
opts.CookieExpire = time.Duration(24) * time.Hour
|
||||
})
|
||||
pcTest := NewProcessCookieTestWithDefaults()
|
||||
pcTest.proxy.CookieExpire = time.Duration(24) * time.Hour
|
||||
reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
|
||||
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
|
||||
pcTest.SaveSession(startSession)
|
||||
startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
|
||||
pcTest.SaveSession(startSession, reference)
|
||||
|
||||
pcTest.proxy.CookieRefresh = time.Hour
|
||||
session, err := pcTest.LoadCookiedSession()
|
||||
session, _, err := pcTest.LoadCookiedSession()
|
||||
assert.NotEqual(t, nil, err)
|
||||
if session != nil {
|
||||
t.Errorf("expected nil session %#v", session)
|
||||
}
|
||||
}
|
||||
|
||||
func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) *ProcessCookieTest {
|
||||
pcTest := NewProcessCookieTestWithOptionsModifiers(modifiers...)
|
||||
func NewAuthOnlyEndpointTest() *ProcessCookieTest {
|
||||
pcTest := NewProcessCookieTestWithDefaults()
|
||||
pcTest.req, _ = http.NewRequest("GET",
|
||||
pcTest.opts.ProxyPrefix+"/auth", nil)
|
||||
return pcTest
|
||||
@ -756,8 +731,8 @@ func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) *ProcessCookieTest {
|
||||
func TestAuthOnlyEndpointAccepted(t *testing.T) {
|
||||
test := NewAuthOnlyEndpointTest()
|
||||
startSession := &sessions.SessionState{
|
||||
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
|
||||
test.SaveSession(startSession)
|
||||
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
|
||||
test.SaveSession(startSession, time.Now())
|
||||
|
||||
test.proxy.ServeHTTP(test.rw, test.req)
|
||||
assert.Equal(t, http.StatusAccepted, test.rw.Code)
|
||||
@ -775,13 +750,12 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
|
||||
test := NewAuthOnlyEndpointTest(func(opts *Options) {
|
||||
opts.CookieExpire = time.Duration(24) * time.Hour
|
||||
})
|
||||
test := NewAuthOnlyEndpointTest()
|
||||
test.proxy.CookieExpire = time.Duration(24) * time.Hour
|
||||
reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
|
||||
startSession := &sessions.SessionState{
|
||||
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
|
||||
test.SaveSession(startSession)
|
||||
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
|
||||
test.SaveSession(startSession, reference)
|
||||
|
||||
test.proxy.ServeHTTP(test.rw, test.req)
|
||||
assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
|
||||
@ -792,8 +766,8 @@ func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
|
||||
func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
|
||||
test := NewAuthOnlyEndpointTest()
|
||||
startSession := &sessions.SessionState{
|
||||
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
|
||||
test.SaveSession(startSession)
|
||||
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
|
||||
test.SaveSession(startSession, time.Now())
|
||||
test.validateUser = false
|
||||
|
||||
test.proxy.ServeHTTP(test.rw, test.req)
|
||||
@ -802,25 +776,6 @@ 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
|
||||
|
||||
@ -842,8 +797,8 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
|
||||
pcTest.opts.ProxyPrefix+"/auth", nil)
|
||||
|
||||
startSession := &sessions.SessionState{
|
||||
User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: time.Now()}
|
||||
pcTest.SaveSession(startSession)
|
||||
User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token"}
|
||||
pcTest.SaveSession(startSession, time.Now())
|
||||
|
||||
pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
|
||||
assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
|
||||
@ -860,9 +815,9 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
|
||||
|
||||
opts := NewOptions()
|
||||
opts.Upstreams = append(opts.Upstreams, upstream.URL)
|
||||
opts.ClientID = "aljsal"
|
||||
opts.ClientSecret = "jglkfsdgj"
|
||||
opts.CookieSecret = "dkfjgdls"
|
||||
opts.ClientID = "bazquux"
|
||||
opts.ClientSecret = "foobar"
|
||||
opts.CookieSecret = "xyzzyplugh"
|
||||
opts.SkipAuthPreflight = true
|
||||
opts.Validate()
|
||||
|
||||
@ -975,11 +930,11 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
|
||||
|
||||
state := &sessions.SessionState{
|
||||
Email: "mbland@acm.org", AccessToken: "my_access_token"}
|
||||
err = proxy.SaveSession(st.rw, req, state)
|
||||
value, err := proxy.provider.CookieForSession(state, proxy.CookieCipher)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, c := range st.rw.Result().Cookies() {
|
||||
for _, c := range proxy.MakeSessionCookie(req, value, proxy.CookieExpire, time.Now()) {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
// This is used by the upstream to validate the signature.
|
||||
@ -999,8 +954,8 @@ func TestNoRequestSignature(t *testing.T) {
|
||||
func TestRequestSignatureGetRequest(t *testing.T) {
|
||||
st := NewSignatureTest()
|
||||
defer st.Close()
|
||||
st.opts.SignatureKey = "sha1:7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d"
|
||||
st.MakeRequestWithExpectedKey("GET", "", "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d")
|
||||
st.opts.SignatureKey = "sha1:foobar"
|
||||
st.MakeRequestWithExpectedKey("GET", "", "foobar")
|
||||
assert.Equal(t, 200, st.rw.Code)
|
||||
assert.Equal(t, st.rw.Body.String(), "signatures match")
|
||||
}
|
||||
@ -1008,9 +963,9 @@ func TestRequestSignatureGetRequest(t *testing.T) {
|
||||
func TestRequestSignaturePostRequest(t *testing.T) {
|
||||
st := NewSignatureTest()
|
||||
defer st.Close()
|
||||
st.opts.SignatureKey = "sha1:d90df39e2d19282840252612dd7c81421a372f61"
|
||||
st.opts.SignatureKey = "sha1:foobar"
|
||||
payload := `{ "hello": "world!" }`
|
||||
st.MakeRequestWithExpectedKey("POST", payload, "d90df39e2d19282840252612dd7c81421a372f61")
|
||||
st.MakeRequestWithExpectedKey("POST", payload, "foobar")
|
||||
assert.Equal(t, 200, st.rw.Code)
|
||||
assert.Equal(t, st.rw.Body.String(), "signatures match")
|
||||
}
|
||||
@ -1056,9 +1011,9 @@ type ajaxRequestTest struct {
|
||||
func newAjaxRequestTest() *ajaxRequestTest {
|
||||
test := &ajaxRequestTest{}
|
||||
test.opts = NewOptions()
|
||||
test.opts.CookieSecret = "sdflsw"
|
||||
test.opts.ClientID = "gkljfdl"
|
||||
test.opts.ClientSecret = "sdflkjs"
|
||||
test.opts.CookieSecret = "foobar"
|
||||
test.opts.ClientID = "bazquux"
|
||||
test.opts.ClientSecret = "xyzzyplugh"
|
||||
test.opts.Validate()
|
||||
test.proxy = NewOAuthProxy(test.opts, func(email string) bool {
|
||||
return true
|
||||
@ -1113,12 +1068,7 @@ func TestAjaxForbiddendRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClearSplitCookie(t *testing.T) {
|
||||
opts := NewOptions()
|
||||
opts.CookieName = "oauth2"
|
||||
opts.CookieDomain = "abc"
|
||||
store, err := cookie.NewCookieSessionStore(&opts.SessionOptions, &opts.CookieOptions)
|
||||
assert.Equal(t, err, nil)
|
||||
p := OAuthProxy{CookieName: opts.CookieName, CookieDomain: opts.CookieDomain, sessionStore: store}
|
||||
p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"}
|
||||
var rw = httptest.NewRecorder()
|
||||
req := httptest.NewRequest("get", "/", nil)
|
||||
|
||||
@ -1142,12 +1092,7 @@ func TestClearSplitCookie(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClearSingleCookie(t *testing.T) {
|
||||
opts := NewOptions()
|
||||
opts.CookieName = "oauth2"
|
||||
opts.CookieDomain = "abc"
|
||||
store, err := cookie.NewCookieSessionStore(&opts.SessionOptions, &opts.CookieOptions)
|
||||
assert.Equal(t, err, nil)
|
||||
p := OAuthProxy{CookieName: opts.CookieName, CookieDomain: opts.CookieDomain, sessionStore: store}
|
||||
p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"}
|
||||
var rw = httptest.NewRecorder()
|
||||
req := httptest.NewRequest("get", "/", nil)
|
||||
|
||||
@ -1165,173 +1110,3 @@ 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)
|
||||
}
|
||||
|
181
options.go
181
options.go
@ -17,11 +17,8 @@ import (
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mbland/hmacauth"
|
||||
"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,34 +26,28 @@ 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"`
|
||||
PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"`
|
||||
ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy-prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"`
|
||||
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-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"`
|
||||
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"`
|
||||
|
||||
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
|
||||
KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"`
|
||||
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
|
||||
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
|
||||
@ -67,8 +58,6 @@ type Options struct {
|
||||
|
||||
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"`
|
||||
@ -76,7 +65,6 @@ type Options struct {
|
||||
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"`
|
||||
@ -87,9 +75,8 @@ type Options struct {
|
||||
// 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"`
|
||||
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"`
|
||||
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"`
|
||||
@ -99,20 +86,19 @@ type Options struct {
|
||||
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_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"`
|
||||
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"`
|
||||
@ -125,10 +111,8 @@ type Options struct {
|
||||
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
|
||||
@ -141,7 +125,6 @@ type SignatureData struct {
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
ProxyPrefix: "/oauth2",
|
||||
PingPath: "/ping",
|
||||
ProxyWebSockets: true,
|
||||
HTTPAddress: "127.0.0.1:4180",
|
||||
HTTPSAddress: ":443",
|
||||
@ -153,9 +136,6 @@ func NewOptions() *Options {
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
CookieRefresh: time.Duration(0),
|
||||
},
|
||||
SessionOptions: options.SessionOptions{
|
||||
Type: "cookie",
|
||||
},
|
||||
SetXAuthRequest: false,
|
||||
SkipAuthPreflight: false,
|
||||
PassBasicAuth: true,
|
||||
@ -165,7 +145,6 @@ func NewOptions() *Options {
|
||||
SetAuthorization: false,
|
||||
PassAuthorization: false,
|
||||
ApprovalPrompt: "force",
|
||||
InsecureOIDCAllowUnverifiedEmail: false,
|
||||
SkipOIDCDiscovery: false,
|
||||
LoggingFilename: "",
|
||||
LoggingMaxSize: 100,
|
||||
@ -173,8 +152,6 @@ func NewOptions() *Options {
|
||||
LoggingMaxBackups: 0,
|
||||
LoggingLocalTime: true,
|
||||
LoggingCompress: false,
|
||||
ExcludeLoggingPaths: "",
|
||||
SilencePingLogging: false,
|
||||
StandardLogging: true,
|
||||
StandardLoggingFormat: logger.DefaultStandardLoggingFormat,
|
||||
RequestLogging: true,
|
||||
@ -184,12 +161,6 @@ func NewOptions() *Options {
|
||||
}
|
||||
}
|
||||
|
||||
// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier.
|
||||
type jwtIssuer struct {
|
||||
issuerURI string
|
||||
audience string
|
||||
}
|
||||
|
||||
func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
|
||||
parsed, err := url.Parse(toParse)
|
||||
if err != nil {
|
||||
@ -266,25 +237,6 @@ 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 {
|
||||
@ -309,8 +261,7 @@ func (o *Options) Validate() error {
|
||||
}
|
||||
msgs = parseProviderInfo(o, msgs)
|
||||
|
||||
var cipher *encryption.Cipher
|
||||
if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.CookieRefresh != time.Duration(0)) {
|
||||
if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {
|
||||
validCookieSecretSize := false
|
||||
for _, i := range []int{16, 24, 32} {
|
||||
if len(secretBytes(o.CookieSecret)) == i {
|
||||
@ -332,22 +283,8 @@ func (o *Options) Validate() error {
|
||||
"pass_access_token == true or "+
|
||||
"cookie_refresh != 0, but is %d bytes.%s",
|
||||
len(secretBytes(o.CookieSecret)), suffix))
|
||||
} else {
|
||||
var err error
|
||||
cipher, err = encryption.NewCipher(secretBytes(o.CookieSecret))
|
||||
if err != nil {
|
||||
msgs = append(msgs, fmt.Sprintf("cookie-secret error: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
o.SessionOptions.Cipher = cipher
|
||||
sessionStore, err := sessions.NewSessionStore(&o.SessionOptions, &o.CookieOptions)
|
||||
if err != nil {
|
||||
msgs = append(msgs, fmt.Sprintf("error initialising session storage: %v", err))
|
||||
} else {
|
||||
o.sessionStore = sessionStore
|
||||
}
|
||||
|
||||
if o.CookieRefresh >= o.CookieExpire {
|
||||
msgs = append(msgs, fmt.Sprintf(
|
||||
@ -399,8 +336,6 @@ func parseProviderInfo(o *Options, msgs []string) []string {
|
||||
p.Configure(o.AzureTenant)
|
||||
case *providers.GitHubProvider:
|
||||
p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
|
||||
case *providers.KeycloakProvider:
|
||||
p.SetGroup(o.KeycloakGroup)
|
||||
case *providers.GoogleProvider:
|
||||
if o.GoogleServiceAccountJSON != "" {
|
||||
file, err := os.Open(o.GoogleServiceAccountJSON)
|
||||
@ -410,39 +345,12 @@ 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)
|
||||
@ -496,49 +404,10 @@ func parseSignatureKey(o *Options, msgs []string) []string {
|
||||
return append(msgs, "unsupported signature hash algorithm: "+
|
||||
o.SignatureKey)
|
||||
}
|
||||
o.signatureData = &SignatureData{hash: hash, key: secretKey}
|
||||
o.signatureData = &SignatureData{hash, 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() == "" {
|
||||
@ -609,14 +478,6 @@ 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)
|
||||
}
|
||||
|
@ -1,13 +1,9 @@
|
||||
package options
|
||||
|
||||
import "github.com/pusher/oauth2_proxy/pkg/encryption"
|
||||
|
||||
// SessionOptions contains configuration options for the SessionStore providers.
|
||||
type SessionOptions struct {
|
||||
Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
|
||||
Cipher *encryption.Cipher
|
||||
CookieStoreOptions
|
||||
RedisStoreOptions
|
||||
}
|
||||
|
||||
// CookieSessionStoreType is used to indicate the CookieSessionStore should be
|
||||
@ -16,15 +12,3 @@ 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"`
|
||||
}
|
||||
|
@ -7,14 +7,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/encryption"
|
||||
"github.com/pusher/oauth2_proxy/cookie"
|
||||
)
|
||||
|
||||
// SessionState is used to store information about the currently authenticated user session
|
||||
type SessionState struct {
|
||||
AccessToken string `json:",omitempty"`
|
||||
IDToken string `json:",omitempty"`
|
||||
CreatedAt time.Time `json:"-"`
|
||||
ExpiresOn time.Time `json:"-"`
|
||||
RefreshToken string `json:",omitempty"`
|
||||
Email string `json:",omitempty"`
|
||||
@ -24,7 +23,6 @@ type SessionState struct {
|
||||
// SessionStateJSON is used to encode SessionState into JSON without exposing time.Time zero value
|
||||
type SessionStateJSON struct {
|
||||
*SessionState
|
||||
CreatedAt *time.Time `json:",omitempty"`
|
||||
ExpiresOn *time.Time `json:",omitempty"`
|
||||
}
|
||||
|
||||
@ -36,14 +34,6 @@ func (s *SessionState) IsExpired() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Age returns the age of a session
|
||||
func (s *SessionState) Age() time.Duration {
|
||||
if !s.CreatedAt.IsZero() {
|
||||
return time.Now().Truncate(time.Second).Sub(s.CreatedAt)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// String constructs a summary of the session state
|
||||
func (s *SessionState) String() string {
|
||||
o := fmt.Sprintf("Session{email:%s user:%s", s.Email, s.User)
|
||||
@ -53,9 +43,6 @@ func (s *SessionState) String() string {
|
||||
if s.IDToken != "" {
|
||||
o += " id_token:true"
|
||||
}
|
||||
if !s.CreatedAt.IsZero() {
|
||||
o += fmt.Sprintf(" created:%s", s.CreatedAt)
|
||||
}
|
||||
if !s.ExpiresOn.IsZero() {
|
||||
o += fmt.Sprintf(" expires:%s", s.ExpiresOn)
|
||||
}
|
||||
@ -66,7 +53,7 @@ func (s *SessionState) String() string {
|
||||
}
|
||||
|
||||
// EncodeSessionState returns string representation of the current session
|
||||
func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) {
|
||||
func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {
|
||||
var ss SessionState
|
||||
if c == nil {
|
||||
// Store only Email and User when cipher is unavailable
|
||||
@ -108,9 +95,6 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error)
|
||||
}
|
||||
// Embed SessionState and ExpiresOn pointer into SessionStateJSON
|
||||
ssj := &SessionStateJSON{SessionState: &ss}
|
||||
if !ss.CreatedAt.IsZero() {
|
||||
ssj.CreatedAt = &ss.CreatedAt
|
||||
}
|
||||
if !ss.ExpiresOn.IsZero() {
|
||||
ssj.ExpiresOn = &ss.ExpiresOn
|
||||
}
|
||||
@ -133,7 +117,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 *encryption.Cipher) (*SessionState, error) {
|
||||
func legacyDecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) {
|
||||
chunks := strings.Split(v, "|")
|
||||
|
||||
if c == nil {
|
||||
@ -176,16 +160,13 @@ func legacyDecodeSessionState(v string, c *encryption.Cipher) (*SessionState, er
|
||||
}
|
||||
|
||||
// DecodeSessionState decodes the session cookie string into a SessionState
|
||||
func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
|
||||
func DecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) {
|
||||
var ssj SessionStateJSON
|
||||
var ss *SessionState
|
||||
err := json.Unmarshal([]byte(v), &ssj)
|
||||
if err == nil && ssj.SessionState != nil {
|
||||
// Extract SessionState and CreatedAt,ExpiresOn value from SessionStateJSON
|
||||
// Extract SessionState and ExpiresOn value from SessionStateJSON
|
||||
ss = ssj.SessionState
|
||||
if ssj.CreatedAt != nil {
|
||||
ss.CreatedAt = *ssj.CreatedAt
|
||||
}
|
||||
if ssj.ExpiresOn != nil {
|
||||
ss.ExpiresOn = *ssj.ExpiresOn
|
||||
}
|
||||
@ -203,14 +184,14 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
|
||||
User: ss.User,
|
||||
}
|
||||
} else {
|
||||
// Backward compatibility with using unencrypted Email
|
||||
// Backward compatibility with using unecrypted Email
|
||||
if ss.Email != "" {
|
||||
decryptedEmail, errEmail := c.Decrypt(ss.Email)
|
||||
if errEmail == nil {
|
||||
ss.Email = decryptedEmail
|
||||
}
|
||||
}
|
||||
// Backward compatibility with using unencrypted User
|
||||
// Backward compatibility with using unecrypted User
|
||||
if ss.User != "" {
|
||||
decryptedUser, errUser := c.Decrypt(ss.User)
|
||||
if errUser == nil {
|
||||
|
@ -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,15 +14,14 @@ const secret = "0123456789abcdefghijklmnopqrstuv"
|
||||
const altSecret = "0000000000abcdefghijklmnopqrstuv"
|
||||
|
||||
func TestSessionStateSerialization(t *testing.T) {
|
||||
c, err := encryption.NewCipher([]byte(secret))
|
||||
c, err := cookie.NewCipher([]byte(secret))
|
||||
assert.Equal(t, nil, err)
|
||||
c2, err := encryption.NewCipher([]byte(altSecret))
|
||||
c2, err := cookie.NewCipher([]byte(altSecret))
|
||||
assert.Equal(t, nil, err)
|
||||
s := &sessions.SessionState{
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
IDToken: "rawtoken1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
}
|
||||
@ -36,7 +35,6 @@ func TestSessionStateSerialization(t *testing.T) {
|
||||
assert.Equal(t, s.Email, ss.Email)
|
||||
assert.Equal(t, s.AccessToken, ss.AccessToken)
|
||||
assert.Equal(t, s.IDToken, ss.IDToken)
|
||||
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
|
||||
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
|
||||
assert.Equal(t, s.RefreshToken, ss.RefreshToken)
|
||||
|
||||
@ -46,7 +44,6 @@ func TestSessionStateSerialization(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotEqual(t, "user@domain.com", ss.User)
|
||||
assert.NotEqual(t, s.Email, ss.Email)
|
||||
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
|
||||
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
|
||||
assert.NotEqual(t, s.AccessToken, ss.AccessToken)
|
||||
assert.NotEqual(t, s.IDToken, ss.IDToken)
|
||||
@ -54,15 +51,14 @@ func TestSessionStateSerialization(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSessionStateSerializationWithUser(t *testing.T) {
|
||||
c, err := encryption.NewCipher([]byte(secret))
|
||||
c, err := cookie.NewCipher([]byte(secret))
|
||||
assert.Equal(t, nil, err)
|
||||
c2, err := encryption.NewCipher([]byte(altSecret))
|
||||
c2, err := cookie.NewCipher([]byte(altSecret))
|
||||
assert.Equal(t, nil, err)
|
||||
s := &sessions.SessionState{
|
||||
User: "just-user",
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
}
|
||||
@ -75,7 +71,6 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
|
||||
assert.Equal(t, s.User, ss.User)
|
||||
assert.Equal(t, s.Email, ss.Email)
|
||||
assert.Equal(t, s.AccessToken, ss.AccessToken)
|
||||
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
|
||||
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
|
||||
assert.Equal(t, s.RefreshToken, ss.RefreshToken)
|
||||
|
||||
@ -85,7 +80,6 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotEqual(t, s.User, ss.User)
|
||||
assert.NotEqual(t, s.Email, ss.Email)
|
||||
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
|
||||
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
|
||||
assert.NotEqual(t, s.AccessToken, ss.AccessToken)
|
||||
assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
|
||||
@ -95,7 +89,6 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
|
||||
s := &sessions.SessionState{
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
}
|
||||
@ -116,7 +109,6 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
|
||||
User: "just-user",
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
}
|
||||
@ -146,7 +138,7 @@ func TestExpired(t *testing.T) {
|
||||
type testCase struct {
|
||||
sessions.SessionState
|
||||
Encoded string
|
||||
Cipher *encryption.Cipher
|
||||
Cipher *cookie.Cipher
|
||||
Error bool
|
||||
}
|
||||
|
||||
@ -155,7 +147,6 @@ type testCase struct {
|
||||
// Currently only tests without cipher here because we have no way to mock
|
||||
// the random generator used in EncodeSessionState.
|
||||
func TestEncodeSessionState(t *testing.T) {
|
||||
c := time.Now()
|
||||
e := time.Now().Add(time.Duration(1) * time.Hour)
|
||||
|
||||
testCases := []testCase{
|
||||
@ -172,7 +163,6 @@ func TestEncodeSessionState(t *testing.T) {
|
||||
User: "just-user",
|
||||
AccessToken: "token1234",
|
||||
IDToken: "rawtoken1234",
|
||||
CreatedAt: c,
|
||||
ExpiresOn: e,
|
||||
RefreshToken: "refresh4321",
|
||||
},
|
||||
@ -195,15 +185,12 @@ func TestEncodeSessionState(t *testing.T) {
|
||||
|
||||
// TestDecodeSessionState testssessions.DecodeSessionState with the test vector
|
||||
func TestDecodeSessionState(t *testing.T) {
|
||||
created := time.Now()
|
||||
createdJSON, _ := created.MarshalJSON()
|
||||
createdString := string(createdJSON)
|
||||
e := time.Now().Add(time.Duration(1) * time.Hour)
|
||||
eJSON, _ := e.MarshalJSON()
|
||||
eString := string(eJSON)
|
||||
eUnix := e.Unix()
|
||||
|
||||
c, err := encryption.NewCipher([]byte(secret))
|
||||
c, err := cookie.NewCipher([]byte(secret))
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCases := []testCase{
|
||||
@ -232,7 +219,7 @@ func TestDecodeSessionState(t *testing.T) {
|
||||
Email: "user@domain.com",
|
||||
User: "just-user",
|
||||
},
|
||||
Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
|
||||
Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","ExpiresOn":%s}`, eString),
|
||||
},
|
||||
{
|
||||
SessionState: sessions.SessionState{
|
||||
@ -240,11 +227,10 @@ func TestDecodeSessionState(t *testing.T) {
|
||||
User: "just-user",
|
||||
AccessToken: "token1234",
|
||||
IDToken: "rawtoken1234",
|
||||
CreatedAt: created,
|
||||
ExpiresOn: e,
|
||||
RefreshToken: "refresh4321",
|
||||
},
|
||||
Encoded: fmt.Sprintf(`{"Email":"FsKKYrTWZWrxSOAqA/fTNAUZS5QWCqOBjuAbBlbVOw==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw==","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
|
||||
Encoded: fmt.Sprintf(`{"Email":"FsKKYrTWZWrxSOAqA/fTNAUZS5QWCqOBjuAbBlbVOw==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw==","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","ExpiresOn":%s}`, eString),
|
||||
Cipher: c,
|
||||
},
|
||||
{
|
||||
@ -330,14 +316,3 @@ func TestDecodeSessionState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionStateAge(t *testing.T) {
|
||||
ss := &sessions.SessionState{}
|
||||
|
||||
// Created at unset so should be 0
|
||||
assert.Equal(t, time.Duration(0), ss.Age())
|
||||
|
||||
// Set CreatedAt to 1 hour ago
|
||||
ss.CreatedAt = time.Now().Add(-1 * time.Hour)
|
||||
assert.Equal(t, time.Hour, ss.Age().Round(time.Minute))
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/options"
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
// MakeCookie constructs a cookie from the given parameters,
|
||||
@ -33,9 +32,3 @@ func MakeCookie(req *http.Request, name string, value string, path string, domai
|
||||
Expires: now.Add(expiration),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeCookieFromOptions constructs a cookie based on the givemn *options.CookieOptions,
|
||||
// value and creation time
|
||||
func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.CookieOptions, expiration time.Duration, now time.Time) *http.Cookie {
|
||||
return MakeCookie(req, name, value, opts.CookiePath, opts.CookieDomain, opts.CookieHTTPOnly, opts.CookieSecure, expiration, now)
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
@ -27,33 +27,36 @@ var _ sessions.SessionStore = &SessionStore{}
|
||||
// SessionStore is an implementation of the sessions.SessionStore
|
||||
// interface that stores sessions in client side cookies
|
||||
type SessionStore struct {
|
||||
CookieOptions *options.CookieOptions
|
||||
CookieCipher *encryption.Cipher
|
||||
CookieCipher *cookie.Cipher
|
||||
CookieDomain string
|
||||
CookieExpire time.Duration
|
||||
CookieHTTPOnly bool
|
||||
CookieName string
|
||||
CookiePath string
|
||||
CookieSecret string
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
// Save takes a sessions.SessionState and stores the information from it
|
||||
// within Cookies set on the HTTP response writer
|
||||
func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error {
|
||||
if ss.CreatedAt.IsZero() {
|
||||
ss.CreatedAt = time.Now()
|
||||
}
|
||||
value, err := utils.CookieForSession(ss, s.CookieCipher)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.setSessionCookie(rw, req, value, ss.CreatedAt)
|
||||
s.setSessionCookie(rw, req, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load reads sessions.SessionState information from Cookies within the
|
||||
// HTTP request object
|
||||
func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
|
||||
c, err := loadCookie(req, s.CookieOptions.CookieName)
|
||||
c, err := loadCookie(req, s.CookieName)
|
||||
if err != nil {
|
||||
// always http.ErrNoCookie
|
||||
return nil, fmt.Errorf("Cookie %q not present", s.CookieOptions.CookieName)
|
||||
return nil, fmt.Errorf("Cookie %q not present", s.CookieName)
|
||||
}
|
||||
val, _, ok := encryption.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire)
|
||||
val, _, ok := cookie.Validate(c, s.CookieSecret, s.CookieExpire)
|
||||
if !ok {
|
||||
return nil, errors.New("Cookie Signature not valid")
|
||||
}
|
||||
@ -71,11 +74,11 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
|
||||
var cookies []*http.Cookie
|
||||
|
||||
// matches CookieName, CookieName_<number>
|
||||
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieOptions.CookieName))
|
||||
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieName))
|
||||
|
||||
for _, c := range req.Cookies() {
|
||||
if cookieNameRegex.MatchString(c.Name) {
|
||||
clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())
|
||||
clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1)
|
||||
|
||||
http.SetCookie(rw, clearCookie)
|
||||
cookies = append(cookies, clearCookie)
|
||||
@ -86,42 +89,60 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
|
||||
}
|
||||
|
||||
// setSessionCookie adds the user's session cookie to the response
|
||||
func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string, created time.Time) {
|
||||
for _, c := range s.makeSessionCookie(req, val, created) {
|
||||
func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
|
||||
for _, c := range s.makeSessionCookie(req, val, s.CookieExpire, time.Now()) {
|
||||
http.SetCookie(rw, c)
|
||||
}
|
||||
}
|
||||
|
||||
// makeSessionCookie creates an http.Cookie containing the authenticated user's
|
||||
// authentication details
|
||||
func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie {
|
||||
func (s *SessionStore) makeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie {
|
||||
if value != "" {
|
||||
value = encryption.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now)
|
||||
value = cookie.SignedValue(s.CookieSecret, s.CookieName, value, now)
|
||||
}
|
||||
c := s.makeCookie(req, s.CookieOptions.CookieName, value, s.CookieOptions.CookieExpire, now)
|
||||
if len(c.Value) > 4096-len(s.CookieOptions.CookieName) {
|
||||
c := s.makeCookie(req, s.CookieName, value, expiration)
|
||||
if len(c.Value) > 4096-len(s.CookieName) {
|
||||
return splitCookie(c)
|
||||
}
|
||||
return []*http.Cookie{c}
|
||||
}
|
||||
|
||||
func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
|
||||
return cookies.MakeCookieFromOptions(
|
||||
func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie {
|
||||
return cookies.MakeCookie(
|
||||
req,
|
||||
name,
|
||||
value,
|
||||
s.CookieOptions,
|
||||
s.CookiePath,
|
||||
s.CookieDomain,
|
||||
s.CookieHTTPOnly,
|
||||
s.CookieSecure,
|
||||
expiration,
|
||||
now,
|
||||
time.Now(),
|
||||
)
|
||||
}
|
||||
|
||||
// NewCookieSessionStore initialises a new instance of the SessionStore from
|
||||
// the configuration given
|
||||
func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
|
||||
func NewCookieSessionStore(opts options.CookieStoreOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
|
||||
var cipher *cookie.Cipher
|
||||
if len(cookieOpts.CookieSecret) > 0 {
|
||||
var err error
|
||||
cipher, err = cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create cipher: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &SessionStore{
|
||||
CookieCipher: opts.Cipher,
|
||||
CookieOptions: cookieOpts,
|
||||
CookieCipher: cipher,
|
||||
CookieDomain: cookieOpts.CookieDomain,
|
||||
CookieExpire: cookieOpts.CookieExpire,
|
||||
CookieHTTPOnly: cookieOpts.CookieHTTPOnly,
|
||||
CookieName: cookieOpts.CookieName,
|
||||
CookiePath: cookieOpts.CookiePath,
|
||||
CookieSecret: cookieOpts.CookieSecret,
|
||||
CookieSecure: cookieOpts.CookieSecure,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -1,305 +0,0 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/options"
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
"github.com/pusher/oauth2_proxy/pkg/cookies"
|
||||
"github.com/pusher/oauth2_proxy/pkg/encryption"
|
||||
)
|
||||
|
||||
// TicketData is a structure representing the ticket used in server session storage
|
||||
type TicketData struct {
|
||||
TicketID string
|
||||
Secret []byte
|
||||
}
|
||||
|
||||
// SessionStore is an implementation of the sessions.SessionStore
|
||||
// interface that stores sessions in redis
|
||||
type SessionStore struct {
|
||||
CookieCipher *encryption.Cipher
|
||||
CookieOptions *options.CookieOptions
|
||||
Client *redis.Client
|
||||
}
|
||||
|
||||
// NewRedisSessionStore initialises a new instance of the SessionStore from
|
||||
// the configuration given
|
||||
func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
|
||||
client, err := newRedisClient(opts.RedisStoreOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error constructing redis client: %v", err)
|
||||
}
|
||||
|
||||
rs := &SessionStore{
|
||||
Client: client,
|
||||
CookieCipher: opts.Cipher,
|
||||
CookieOptions: cookieOpts,
|
||||
}
|
||||
return rs, nil
|
||||
|
||||
}
|
||||
|
||||
func newRedisClient(opts options.RedisStoreOptions) (*redis.Client, error) {
|
||||
if opts.UseSentinel {
|
||||
client := redis.NewFailoverClient(&redis.FailoverOptions{
|
||||
MasterName: opts.SentinelMasterName,
|
||||
SentinelAddrs: opts.SentinelConnectionURLs,
|
||||
})
|
||||
return client, nil
|
||||
}
|
||||
|
||||
opt, err := redis.ParseURL(opts.RedisConnectionURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse redis url: %s", err)
|
||||
}
|
||||
|
||||
client := redis.NewClient(opt)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Save takes a sessions.SessionState and stores the information from it
|
||||
// to redies, and adds a new ticket cookie on the HTTP response writer
|
||||
func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
|
||||
if s.CreatedAt.IsZero() {
|
||||
s.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
// Old sessions that we are refreshing would have a request cookie
|
||||
// New sessions don't, so we ignore the error. storeValue will check requestCookie
|
||||
requestCookie, _ := req.Cookie(store.CookieOptions.CookieName)
|
||||
value, err := s.EncodeSessionState(store.CookieCipher)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ticketString, err := store.storeValue(value, store.CookieOptions.CookieExpire, requestCookie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticketCookie := store.makeCookie(
|
||||
req,
|
||||
ticketString,
|
||||
store.CookieOptions.CookieExpire,
|
||||
s.CreatedAt,
|
||||
)
|
||||
|
||||
http.SetCookie(rw, ticketCookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load reads sessions.SessionState information from a ticket
|
||||
// cookie within the HTTP request object
|
||||
func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
|
||||
requestCookie, err := req.Cookie(store.CookieOptions.CookieName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading session: %s", err)
|
||||
}
|
||||
|
||||
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Cookie Signature not valid")
|
||||
}
|
||||
session, err := store.loadSessionFromString(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading session: %s", err)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// loadSessionFromString loads the session based on the ticket value
|
||||
func (store *SessionStore) loadSessionFromString(value string) (*sessions.SessionState, error) {
|
||||
ticket, err := decodeTicket(store.CookieOptions.CookieName, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := store.Client.Get(ticket.asHandle(store.CookieOptions.CookieName)).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultBytes := []byte(result)
|
||||
block, err := aes.NewCipher(ticket.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Use secret as the IV too, because each entry has it's own key
|
||||
stream := cipher.NewCFBDecrypter(block, ticket.Secret)
|
||||
stream.XORKeyStream(resultBytes, resultBytes)
|
||||
|
||||
session, err := sessions.DecodeSessionState(string(resultBytes), store.CookieCipher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// Clear clears any saved session information for a given ticket cookie
|
||||
// from redis, and then clears the session
|
||||
func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
|
||||
// We go ahead and clear the cookie first, always.
|
||||
clearCookie := store.makeCookie(
|
||||
req,
|
||||
"",
|
||||
time.Hour*-1,
|
||||
time.Now(),
|
||||
)
|
||||
http.SetCookie(rw, clearCookie)
|
||||
|
||||
// If there was an existing cookie we should clear the session in redis
|
||||
requestCookie, err := req.Cookie(store.CookieOptions.CookieName)
|
||||
if err != nil && err == http.ErrNoCookie {
|
||||
// No existing cookie so can't clear redis
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error retrieving cookie: %v", err)
|
||||
}
|
||||
|
||||
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
|
||||
if !ok {
|
||||
return fmt.Errorf("Cookie Signature not valid")
|
||||
}
|
||||
|
||||
// We only return an error if we had an issue with redis
|
||||
// If there's an issue decoding the ticket, ignore it
|
||||
ticket, _ := decodeTicket(store.CookieOptions.CookieName, val)
|
||||
if ticket != nil {
|
||||
_, err := store.Client.Del(ticket.asHandle(store.CookieOptions.CookieName)).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error clearing cookie from redis: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeCookie makes a cookie, signing the value if present
|
||||
func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
|
||||
if value != "" {
|
||||
value = encryption.SignedValue(store.CookieOptions.CookieSecret, store.CookieOptions.CookieName, value, now)
|
||||
}
|
||||
return cookies.MakeCookieFromOptions(
|
||||
req,
|
||||
store.CookieOptions.CookieName,
|
||||
value,
|
||||
store.CookieOptions,
|
||||
expires,
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
func (store *SessionStore) storeValue(value string, expiration time.Duration, requestCookie *http.Cookie) (string, error) {
|
||||
ticket, err := store.getTicket(requestCookie)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting ticket: %v", err)
|
||||
}
|
||||
|
||||
ciphertext := make([]byte, len(value))
|
||||
block, err := aes.NewCipher(ticket.Secret)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error initiating cipher block %s", err)
|
||||
}
|
||||
|
||||
// Use secret as the Initialization Vector too, because each entry has it's own key
|
||||
stream := cipher.NewCFBEncrypter(block, ticket.Secret)
|
||||
stream.XORKeyStream(ciphertext, []byte(value))
|
||||
|
||||
handle := ticket.asHandle(store.CookieOptions.CookieName)
|
||||
err = store.Client.Set(handle, ciphertext, expiration).Err()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ticket.encodeTicket(store.CookieOptions.CookieName), nil
|
||||
}
|
||||
|
||||
// getTicket retrieves an existing ticket from the cookie if present,
|
||||
// or creates a new ticket
|
||||
func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, error) {
|
||||
if requestCookie == nil {
|
||||
return newTicket()
|
||||
}
|
||||
|
||||
// An existing cookie exists, try to retrieve the ticket
|
||||
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
|
||||
if !ok {
|
||||
// Cookie is invalid, create a new ticket
|
||||
return newTicket()
|
||||
}
|
||||
|
||||
// Valid cookie, decode the ticket
|
||||
ticket, err := decodeTicket(store.CookieOptions.CookieName, val)
|
||||
if err != nil {
|
||||
// If we can't decode the ticket we have to create a new one
|
||||
return newTicket()
|
||||
}
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func newTicket() (*TicketData, error) {
|
||||
rawID := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, rawID); err != nil {
|
||||
return nil, fmt.Errorf("failed to create new ticket ID %s", err)
|
||||
}
|
||||
// ticketID is hex encoded
|
||||
ticketID := fmt.Sprintf("%x", rawID)
|
||||
|
||||
secret := make([]byte, aes.BlockSize)
|
||||
if _, err := io.ReadFull(rand.Reader, secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to create initialization vector %s", err)
|
||||
}
|
||||
ticket := &TicketData{
|
||||
TicketID: ticketID,
|
||||
Secret: secret,
|
||||
}
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func (ticket *TicketData) asHandle(prefix string) string {
|
||||
return fmt.Sprintf("%s-%s", prefix, ticket.TicketID)
|
||||
}
|
||||
|
||||
func decodeTicket(cookieName string, ticketString string) (*TicketData, error) {
|
||||
prefix := cookieName + "-"
|
||||
if !strings.HasPrefix(ticketString, prefix) {
|
||||
return nil, fmt.Errorf("failed to decode ticket handle")
|
||||
}
|
||||
trimmedTicket := strings.TrimPrefix(ticketString, prefix)
|
||||
|
||||
ticketParts := strings.Split(trimmedTicket, ".")
|
||||
if len(ticketParts) != 2 {
|
||||
return nil, fmt.Errorf("failed to decode ticket")
|
||||
}
|
||||
ticketID, secretBase64 := ticketParts[0], ticketParts[1]
|
||||
|
||||
// ticketID must be a hexadecimal string
|
||||
_, err := hex.DecodeString(ticketID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server ticket failed sanity checks")
|
||||
}
|
||||
|
||||
secret, err := base64.RawURLEncoding.DecodeString(secretBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode initialization vector %s", err)
|
||||
}
|
||||
ticketData := &TicketData{
|
||||
TicketID: ticketID,
|
||||
Secret: secret,
|
||||
}
|
||||
return ticketData, nil
|
||||
}
|
||||
|
||||
func (ticket *TicketData) encodeTicket(prefix string) string {
|
||||
handle := ticket.asHandle(prefix)
|
||||
ticketString := handle + "." + base64.RawURLEncoding.EncodeToString(ticket.Secret)
|
||||
return ticketString
|
||||
}
|
@ -6,16 +6,13 @@ 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
|
||||
func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
|
||||
switch opts.Type {
|
||||
case options.CookieSessionStoreType:
|
||||
return cookie.NewCookieSessionStore(opts, cookieOpts)
|
||||
case options.RedisSessionStoreType:
|
||||
return redis.NewRedisSessionStore(opts, cookieOpts)
|
||||
return cookie.NewCookieSessionStore(opts.CookieStoreOptions, cookieOpts)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown session store type '%s'", opts.Type)
|
||||
}
|
||||
|
@ -5,22 +5,16 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/options"
|
||||
sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
"github.com/pusher/oauth2_proxy/pkg/cookies"
|
||||
"github.com/pusher/oauth2_proxy/pkg/encryption"
|
||||
"github.com/pusher/oauth2_proxy/pkg/sessions"
|
||||
sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
|
||||
"github.com/pusher/oauth2_proxy/pkg/sessions/redis"
|
||||
"github.com/pusher/oauth2_proxy/pkg/sessions/utils"
|
||||
"github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
|
||||
)
|
||||
|
||||
func TestSessionStore(t *testing.T) {
|
||||
@ -36,7 +30,6 @@ 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() {
|
||||
@ -79,67 +72,11 @@ var _ = Describe("NewSessionStore", func() {
|
||||
}
|
||||
})
|
||||
|
||||
It("have a signature timestamp matching session.CreatedAt", func() {
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Value != "" {
|
||||
parts := strings.Split(cookie.Value, "|")
|
||||
Expect(parts).To(HaveLen(3))
|
||||
Expect(parts[1]).To(Equal(strconv.Itoa(int(session.CreatedAt.Unix()))))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// The following should only be for server stores
|
||||
PersistentSessionStoreTests := func() {
|
||||
Context("when Clear is called on a persistent store", func() {
|
||||
var resultCookies []*http.Cookie
|
||||
|
||||
BeforeEach(func() {
|
||||
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||
saveResp := httptest.NewRecorder()
|
||||
err := ss.Save(saveResp, req, session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
resultCookies = saveResp.Result().Cookies()
|
||||
for _, c := range resultCookies {
|
||||
request.AddCookie(c)
|
||||
}
|
||||
err = ss.Clear(response, request)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("attempting to Load", func() {
|
||||
var loadedAfterClear *sessionsapi.SessionState
|
||||
var loadErr error
|
||||
|
||||
BeforeEach(func() {
|
||||
loadReq := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||
for _, c := range resultCookies {
|
||||
loadReq.AddCookie(c)
|
||||
}
|
||||
|
||||
loadedAfterClear, loadErr = ss.Load(loadReq)
|
||||
})
|
||||
|
||||
It("returns an empty session", func() {
|
||||
Expect(loadedAfterClear).To(BeNil())
|
||||
})
|
||||
|
||||
It("returns an error", func() {
|
||||
Expect(loadErr).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
CheckCookieOptions()
|
||||
})
|
||||
}
|
||||
|
||||
SessionStoreInterfaceTests := func(persistent bool) {
|
||||
SessionStoreInterfaceTests := func() {
|
||||
Context("when Save is called", func() {
|
||||
Context("with no existing session", func() {
|
||||
BeforeEach(func() {
|
||||
err := ss.Save(response, request, session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@ -149,72 +86,23 @@ var _ = Describe("NewSessionStore", func() {
|
||||
Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("Ensures the session CreatedAt is not zero", func() {
|
||||
Expect(session.CreatedAt.IsZero()).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Context("with a broken session", func() {
|
||||
BeforeEach(func() {
|
||||
By("Using a valid cookie with a different providers session encoding")
|
||||
broken := "BrokenSessionFromADifferentSessionImplementation"
|
||||
value := encryption.SignedValue(cookieOpts.CookieSecret, cookieOpts.CookieName, broken, time.Now())
|
||||
cookie := cookies.MakeCookieFromOptions(request, cookieOpts.CookieName, value, cookieOpts, cookieOpts.CookieExpire, time.Now())
|
||||
request.AddCookie(cookie)
|
||||
|
||||
err := ss.Save(response, request, session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("sets a `set-cookie` header in the response", func() {
|
||||
Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("Ensures the session CreatedAt is not zero", func() {
|
||||
Expect(session.CreatedAt.IsZero()).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Context("with an expired saved session", func() {
|
||||
var err error
|
||||
BeforeEach(func() {
|
||||
By("saving a session")
|
||||
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||
saveResp := httptest.NewRecorder()
|
||||
err = ss.Save(saveResp, req, session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
By("and clearing the session")
|
||||
for _, c := range saveResp.Result().Cookies() {
|
||||
request.AddCookie(c)
|
||||
}
|
||||
clearResp := httptest.NewRecorder()
|
||||
err = ss.Clear(clearResp, request)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
By("then saving a request with the cleared session")
|
||||
err = ss.Save(response, request, session)
|
||||
})
|
||||
|
||||
It("no error should occur", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
CheckCookieOptions()
|
||||
})
|
||||
|
||||
Context("when Clear is called", func() {
|
||||
BeforeEach(func() {
|
||||
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||
saveResp := httptest.NewRecorder()
|
||||
err := ss.Save(saveResp, req, session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for _, c := range saveResp.Result().Cookies() {
|
||||
request.AddCookie(c)
|
||||
}
|
||||
err = ss.Clear(response, request)
|
||||
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)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
@ -226,10 +114,16 @@ var _ = Describe("NewSessionStore", func() {
|
||||
})
|
||||
|
||||
Context("when Load is called", func() {
|
||||
LoadSessionTests := func() {
|
||||
var loadedSession *sessionsapi.SessionState
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
err := ss.Save(resp, req, session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for _, cookie := range resp.Result().Cookies() {
|
||||
request.AddCookie(cookie)
|
||||
}
|
||||
loadedSession, err = ss.Load(request)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
@ -244,80 +138,19 @@ var _ = Describe("NewSessionStore", func() {
|
||||
|
||||
// Can't compare time.Time using Equal() so remove ExpiresOn from sessions
|
||||
l := *loadedSession
|
||||
l.CreatedAt = time.Time{}
|
||||
l.ExpiresOn = time.Time{}
|
||||
s := *session
|
||||
s.CreatedAt = time.Time{}
|
||||
s.ExpiresOn = time.Time{}
|
||||
Expect(l).To(Equal(s))
|
||||
|
||||
// Compare time.Time separately
|
||||
Expect(loadedSession.CreatedAt.Equal(session.CreatedAt)).To(BeTrue())
|
||||
Expect(loadedSession.ExpiresOn.Equal(session.ExpiresOn)).To(BeTrue())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
err := ss.Save(resp, req, session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for _, cookie := range resp.Result().Cookies() {
|
||||
request.AddCookie(cookie)
|
||||
}
|
||||
})
|
||||
|
||||
Context("before the refresh period", func() {
|
||||
LoadSessionTests()
|
||||
})
|
||||
|
||||
// Test TTLs and cleanup of persistent session storage
|
||||
// For non-persistent we rely on the browser cookie lifecycle
|
||||
if persistent {
|
||||
Context("after the refresh period, but before the cookie expire period", func() {
|
||||
BeforeEach(func() {
|
||||
switch ss.(type) {
|
||||
case *redis.SessionStore:
|
||||
mr.FastForward(cookieOpts.CookieRefresh + time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
LoadSessionTests()
|
||||
})
|
||||
|
||||
Context("after the cookie expire period", func() {
|
||||
var loadedSession *sessionsapi.SessionState
|
||||
var err error
|
||||
|
||||
BeforeEach(func() {
|
||||
switch ss.(type) {
|
||||
case *redis.SessionStore:
|
||||
mr.FastForward(cookieOpts.CookieExpire + time.Minute)
|
||||
}
|
||||
|
||||
loadedSession, err = ss.Load(request)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error loading the session", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an empty session", func() {
|
||||
Expect(loadedSession).To(BeNil())
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if persistent {
|
||||
PersistentSessionStoreTests()
|
||||
}
|
||||
}
|
||||
|
||||
RunSessionTests := func(persistent bool) {
|
||||
RunSessionTests := func() {
|
||||
Context("with default options", func() {
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
@ -325,7 +158,7 @@ var _ = Describe("NewSessionStore", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
SessionStoreInterfaceTests(persistent)
|
||||
SessionStoreInterfaceTests()
|
||||
})
|
||||
|
||||
Context("with non-default options", func() {
|
||||
@ -334,7 +167,7 @@ var _ = Describe("NewSessionStore", func() {
|
||||
CookieName: "_cookie_name",
|
||||
CookiePath: "/path",
|
||||
CookieExpire: time.Duration(72) * time.Hour,
|
||||
CookieRefresh: time.Duration(2) * time.Hour,
|
||||
CookieRefresh: time.Duration(3600),
|
||||
CookieSecure: false,
|
||||
CookieHTTPOnly: false,
|
||||
CookieDomain: "example.com",
|
||||
@ -345,25 +178,21 @@ var _ = Describe("NewSessionStore", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
SessionStoreInterfaceTests(persistent)
|
||||
SessionStoreInterfaceTests()
|
||||
})
|
||||
|
||||
Context("with a cipher", func() {
|
||||
Context("with a cookie-secret set", func() {
|
||||
BeforeEach(func() {
|
||||
secret := make([]byte, 32)
|
||||
_, err := rand.Read(secret)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cookieOpts.CookieSecret = base64.URLEncoding.EncodeToString(secret)
|
||||
cipher, err := encryption.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cipher).ToNot(BeNil())
|
||||
opts.Cipher = cipher
|
||||
|
||||
ss, err = sessions.NewSessionStore(opts, cookieOpts)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
SessionStoreInterfaceTests(persistent)
|
||||
SessionStoreInterfaceTests()
|
||||
})
|
||||
}
|
||||
|
||||
@ -376,7 +205,7 @@ var _ = Describe("NewSessionStore", func() {
|
||||
CookieName: "_oauth2_proxy",
|
||||
CookiePath: "/",
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
CookieRefresh: time.Duration(1) * time.Hour,
|
||||
CookieRefresh: time.Duration(0),
|
||||
CookieSecure: true,
|
||||
CookieHTTPOnly: true,
|
||||
}
|
||||
@ -402,35 +231,11 @@ var _ = Describe("NewSessionStore", func() {
|
||||
It("creates a cookie.SessionStore", func() {
|
||||
ss, err := sessions.NewSessionStore(opts, cookieOpts)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ss).To(BeAssignableToTypeOf(&sessionscookie.SessionStore{}))
|
||||
Expect(ss).To(BeAssignableToTypeOf(&cookie.SessionStore{}))
|
||||
})
|
||||
|
||||
Context("the cookie.SessionStore", func() {
|
||||
RunSessionTests(false)
|
||||
})
|
||||
})
|
||||
|
||||
Context("with type 'redis'", func() {
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
mr, err = miniredis.Run()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
opts.Type = options.RedisSessionStoreType
|
||||
opts.RedisConnectionURL = "redis://" + mr.Addr()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
mr.Close()
|
||||
})
|
||||
|
||||
It("creates a redis.SessionStore", func() {
|
||||
ss, err := sessions.NewSessionStore(opts, cookieOpts)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ss).To(BeAssignableToTypeOf(&redis.SessionStore{}))
|
||||
})
|
||||
|
||||
Context("the redis.SessionStore", func() {
|
||||
RunSessionTests(true)
|
||||
RunSessionTests()
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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 *encryption.Cipher) (string, error) {
|
||||
func CookieForSession(s *sessions.SessionState, c *cookie.Cipher) (string, error) {
|
||||
return s.EncodeSessionState(c)
|
||||
}
|
||||
|
||||
// SessionFromCookie deserializes a session from a cookie value
|
||||
func SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) {
|
||||
func SessionFromCookie(v string, c *cookie.Cipher) (s *sessions.SessionState, err error) {
|
||||
return sessions.DecodeSessionState(v, c)
|
||||
}
|
||||
|
||||
|
@ -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 := requests.Request(req)
|
||||
json, err := api.Request(req)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -1,163 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/pkg/requests"
|
||||
)
|
||||
|
||||
// BitbucketProvider represents an Bitbucket based Identity Provider
|
||||
type BitbucketProvider struct {
|
||||
*ProviderData
|
||||
Team string
|
||||
Repository string
|
||||
}
|
||||
|
||||
// NewBitbucketProvider initiates a new BitbucketProvider
|
||||
func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
|
||||
p.ProviderName = "Bitbucket"
|
||||
if p.LoginURL == nil || p.LoginURL.String() == "" {
|
||||
p.LoginURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "bitbucket.org",
|
||||
Path: "/site/oauth2/authorize",
|
||||
}
|
||||
}
|
||||
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
|
||||
p.RedeemURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "bitbucket.org",
|
||||
Path: "/site/oauth2/access_token",
|
||||
}
|
||||
}
|
||||
if p.ValidateURL == nil || p.ValidateURL.String() == "" {
|
||||
p.ValidateURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.bitbucket.org",
|
||||
Path: "/2.0/user/emails",
|
||||
}
|
||||
}
|
||||
if p.Scope == "" {
|
||||
p.Scope = "email"
|
||||
}
|
||||
return &BitbucketProvider{ProviderData: p}
|
||||
}
|
||||
|
||||
// SetTeam defines the Bitbucket team the user must be part of
|
||||
func (p *BitbucketProvider) SetTeam(team string) {
|
||||
p.Team = team
|
||||
if !strings.Contains(p.Scope, "team") {
|
||||
p.Scope += " team"
|
||||
}
|
||||
}
|
||||
|
||||
// SetRepository defines the repository the user must have access to
|
||||
func (p *BitbucketProvider) SetRepository(repository string) {
|
||||
p.Repository = repository
|
||||
if !strings.Contains(p.Scope, "repository") {
|
||||
p.Scope += " repository"
|
||||
}
|
||||
}
|
||||
|
||||
// GetEmailAddress returns the email of the authenticated user
|
||||
func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
|
||||
|
||||
var emails struct {
|
||||
Values []struct {
|
||||
Email string `json:"email"`
|
||||
Primary bool `json:"is_primary"`
|
||||
}
|
||||
}
|
||||
var teams struct {
|
||||
Values []struct {
|
||||
Name string `json:"username"`
|
||||
}
|
||||
}
|
||||
var repositories struct {
|
||||
Values []struct {
|
||||
FullName string `json:"full_name"`
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest("GET",
|
||||
p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
|
||||
if err != nil {
|
||||
logger.Printf("failed building request %s", err)
|
||||
return "", err
|
||||
}
|
||||
err = requests.RequestJSON(req, &emails)
|
||||
if err != nil {
|
||||
logger.Printf("failed making request %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if p.Team != "" {
|
||||
teamURL := &url.URL{}
|
||||
*teamURL = *p.ValidateURL
|
||||
teamURL.Path = "/2.0/teams"
|
||||
req, err = http.NewRequest("GET",
|
||||
teamURL.String()+"?role=member&access_token="+s.AccessToken, nil)
|
||||
if err != nil {
|
||||
logger.Printf("failed building request %s", err)
|
||||
return "", err
|
||||
}
|
||||
err = requests.RequestJSON(req, &teams)
|
||||
if err != nil {
|
||||
logger.Printf("failed requesting teams membership %s", err)
|
||||
return "", err
|
||||
}
|
||||
var found = false
|
||||
for _, team := range teams.Values {
|
||||
if p.Team == team.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != true {
|
||||
logger.Print("team membership test failed, access denied")
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
if p.Repository != "" {
|
||||
repositoriesURL := &url.URL{}
|
||||
*repositoriesURL = *p.ValidateURL
|
||||
repositoriesURL.Path = "/2.0/repositories/" + strings.Split(p.Repository, "/")[0]
|
||||
req, err = http.NewRequest("GET",
|
||||
repositoriesURL.String()+"?role=contributor"+
|
||||
"&q=full_name="+url.QueryEscape("\""+p.Repository+"\"")+
|
||||
"&access_token="+s.AccessToken,
|
||||
nil)
|
||||
if err != nil {
|
||||
logger.Printf("failed building request %s", err)
|
||||
return "", err
|
||||
}
|
||||
err = requests.RequestJSON(req, &repositories)
|
||||
if err != nil {
|
||||
logger.Printf("failed checking repository access %s", err)
|
||||
return "", err
|
||||
}
|
||||
var found = false
|
||||
for _, repository := range repositories.Values {
|
||||
if p.Repository == repository.FullName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != true {
|
||||
logger.Print("repository access test failed, access denied")
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, email := range emails.Values {
|
||||
if email.Primary {
|
||||
return email.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
)
|
||||
|
||||
func testBitbucketProvider(hostname, team string, repository string) *BitbucketProvider {
|
||||
p := NewBitbucketProvider(
|
||||
&ProviderData{
|
||||
ProviderName: "",
|
||||
LoginURL: &url.URL{},
|
||||
RedeemURL: &url.URL{},
|
||||
ProfileURL: &url.URL{},
|
||||
ValidateURL: &url.URL{},
|
||||
Scope: ""})
|
||||
|
||||
if team != "" {
|
||||
p.SetTeam(team)
|
||||
}
|
||||
|
||||
if repository != "" {
|
||||
p.SetRepository(repository)
|
||||
}
|
||||
|
||||
if hostname != "" {
|
||||
updateURL(p.Data().LoginURL, hostname)
|
||||
updateURL(p.Data().RedeemURL, hostname)
|
||||
updateURL(p.Data().ProfileURL, hostname)
|
||||
updateURL(p.Data().ValidateURL, hostname)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func testBitbucketBackend(payload string) *httptest.Server {
|
||||
paths := map[string]bool{
|
||||
"/2.0/user/emails": true,
|
||||
"/2.0/teams": true,
|
||||
}
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL
|
||||
if !paths[url.Path] {
|
||||
log.Printf("%s not in %+v\n", url.Path, paths)
|
||||
w.WriteHeader(404)
|
||||
} else if r.URL.Query().Get("access_token") != "imaginary_access_token" {
|
||||
w.WriteHeader(403)
|
||||
} else {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(payload))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func TestBitbucketProviderDefaults(t *testing.T) {
|
||||
p := testBitbucketProvider("", "", "")
|
||||
assert.NotEqual(t, nil, p)
|
||||
assert.Equal(t, "Bitbucket", p.Data().ProviderName)
|
||||
assert.Equal(t, "https://bitbucket.org/site/oauth2/authorize",
|
||||
p.Data().LoginURL.String())
|
||||
assert.Equal(t, "https://bitbucket.org/site/oauth2/access_token",
|
||||
p.Data().RedeemURL.String())
|
||||
assert.Equal(t, "https://api.bitbucket.org/2.0/user/emails",
|
||||
p.Data().ValidateURL.String())
|
||||
assert.Equal(t, "email", p.Data().Scope)
|
||||
}
|
||||
|
||||
func TestBitbucketProviderScopeAdjustForTeam(t *testing.T) {
|
||||
p := testBitbucketProvider("", "test-team", "")
|
||||
assert.NotEqual(t, nil, p)
|
||||
assert.Equal(t, "email team", p.Data().Scope)
|
||||
}
|
||||
|
||||
func TestBitbucketProviderScopeAdjustForRepository(t *testing.T) {
|
||||
p := testBitbucketProvider("", "", "rest-repo")
|
||||
assert.NotEqual(t, nil, p)
|
||||
assert.Equal(t, "email repository", p.Data().Scope)
|
||||
}
|
||||
|
||||
func TestBitbucketProviderOverrides(t *testing.T) {
|
||||
p := NewBitbucketProvider(
|
||||
&ProviderData{
|
||||
LoginURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/oauth/auth"},
|
||||
RedeemURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/oauth/token"},
|
||||
ValidateURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/api/v3/user"},
|
||||
Scope: "profile"})
|
||||
assert.NotEqual(t, nil, p)
|
||||
assert.Equal(t, "Bitbucket", p.Data().ProviderName)
|
||||
assert.Equal(t, "https://example.com/oauth/auth",
|
||||
p.Data().LoginURL.String())
|
||||
assert.Equal(t, "https://example.com/oauth/token",
|
||||
p.Data().RedeemURL.String())
|
||||
assert.Equal(t, "https://example.com/api/v3/user",
|
||||
p.Data().ValidateURL.String())
|
||||
assert.Equal(t, "profile", p.Data().Scope)
|
||||
}
|
||||
|
||||
func TestBitbucketProviderGetEmailAddress(t *testing.T) {
|
||||
b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true } ] }")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testBitbucketProvider(bURL.Host, "", "")
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||
}
|
||||
|
||||
func TestBitbucketProviderGetEmailAddressAndGroup(t *testing.T) {
|
||||
b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true, \"username\": \"bioinformatics\" } ] }")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testBitbucketProvider(bURL.Host, "bioinformatics", "")
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||
}
|
||||
|
||||
// Note that trying to trigger the "failed building request" case is not
|
||||
// practical, since the only way it can fail is if the URL fails to parse.
|
||||
func TestBitbucketProviderGetEmailAddressFailedRequest(t *testing.T) {
|
||||
b := testBitbucketBackend("unused payload")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testBitbucketProvider(bURL.Host, "", "")
|
||||
|
||||
// We'll trigger a request failure by using an unexpected access
|
||||
// token. Alternatively, we could allow the parsing of the payload as
|
||||
// JSON to fail.
|
||||
session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "", email)
|
||||
}
|
||||
|
||||
func TestBitbucketProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
|
||||
b := testBitbucketBackend("{\"foo\": \"bar\"}")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testBitbucketProvider(bURL.Host, "", "")
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.Equal(t, "", email)
|
||||
assert.Equal(t, nil, err)
|
||||
}
|
@ -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 = requests.RequestJSON(req, &r)
|
||||
err = api.RequestJSON(req, &r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,258 +1,62 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"net/url"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"github.com/pusher/oauth2_proxy/api"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// GitLabProvider represents a GitLab based Identity Provider
|
||||
// GitLabProvider represents an 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 = "openid email"
|
||||
p.Scope = "read_user"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
req, err := http.NewRequest("GET",
|
||||
p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve user info: %v", err)
|
||||
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")
|
||||
}
|
||||
|
||||
// Check if email has valid domain
|
||||
err = p.verifyEmailDomain(userInfo)
|
||||
json, err := api.Request(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("email domain check failed: %v", err)
|
||||
logger.Printf("failed making request %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check group membership
|
||||
err = p.verifyGroupMembership(userInfo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("group membership check failed: %v", err)
|
||||
}
|
||||
|
||||
return userInfo.Email, nil
|
||||
}
|
||||
|
||||
// GetUserName returns the Account user name
|
||||
func (p *GitLabProvider) GetUserName(s *sessions.SessionState) (string, error) {
|
||||
userInfo, err := p.getUserInfo(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve user info: %v", err)
|
||||
}
|
||||
|
||||
return userInfo.Username, nil
|
||||
return json.Get("email").String()
|
||||
}
|
||||
|
@ -25,142 +25,104 @@ func testGitLabProvider(hostname string) *GitLabProvider {
|
||||
updateURL(p.Data().ProfileURL, hostname)
|
||||
updateURL(p.Data().ValidateURL, hostname)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func testGitLabBackend() *httptest.Server {
|
||||
userInfo := `
|
||||
{
|
||||
"nickname": "FooBar",
|
||||
"email": "foo@bar.com",
|
||||
"email_verified": false,
|
||||
"groups": ["foo", "bar"]
|
||||
}
|
||||
`
|
||||
authHeader := "Bearer gitlab_access_token"
|
||||
func testGitLabBackend(payload string) *httptest.Server {
|
||||
path := "/api/v4/user"
|
||||
query := "access_token=imaginary_access_token"
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/oauth/userinfo" {
|
||||
if r.Header["Authorization"][0] == authHeader {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(userInfo))
|
||||
} else {
|
||||
w.WriteHeader(401)
|
||||
}
|
||||
} else {
|
||||
if r.URL.Path != path || r.URL.RawQuery != query {
|
||||
w.WriteHeader(404)
|
||||
} else {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(payload))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func TestGitLabProviderBadToken(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
|
||||
_, err := p.GetEmailAddress(session)
|
||||
assert.NotEqual(t, nil, err)
|
||||
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 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 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 TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
func TestGitLabProviderGetEmailAddress(t *testing.T) {
|
||||
b := testGitLabBackend("{\"email\": \"michael.bland@gsa.gov\"}")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
p.AllowUnverifiedEmail = true
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "foo@bar.com", email)
|
||||
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||
}
|
||||
|
||||
func TestGitLabProviderUsername(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
// 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")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
p.AllowUnverifiedEmail = true
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
username, err := p.GetUserName(session)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "FooBar", username)
|
||||
}
|
||||
|
||||
func TestGitLabProviderGroupMembershipValid(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
p.AllowUnverifiedEmail = true
|
||||
p.Group = "foo"
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
// 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.Equal(t, nil, err)
|
||||
assert.Equal(t, "foo@bar.com", email)
|
||||
}
|
||||
|
||||
func TestGitLabProviderGroupMembershipMissing(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
p.AllowUnverifiedEmail = true
|
||||
p.Group = "baz"
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
_, err := p.GetEmailAddress(session)
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "", email)
|
||||
}
|
||||
|
||||
func TestGitLabProviderEmailDomainValid(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
func TestGitLabProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
|
||||
b := testGitLabBackend("{\"foo\": \"bar\"}")
|
||||
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"}
|
||||
session := &sessions.SessionState{AccessToken: "imaginary_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)
|
||||
assert.Equal(t, "", email)
|
||||
}
|
||||
|
@ -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"
|
||||
@ -149,7 +149,6 @@ func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionSt
|
||||
s = &sessions.SessionState{
|
||||
AccessToken: jsonResponse.AccessToken,
|
||||
IDToken: jsonResponse.IDToken,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
|
||||
RefreshToken: jsonResponse.RefreshToken,
|
||||
Email: c.Email,
|
||||
@ -189,44 +188,69 @@ 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 {
|
||||
// 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()
|
||||
members, err := fetchGroupMembers(service, group)
|
||||
if err != nil {
|
||||
err, ok := err.(*googleapi.Error)
|
||||
if ok && err.Code == 404 {
|
||||
logger.Printf("error checking membership in group %s: group does not exist", group)
|
||||
} else if ok && err.Code == 400 {
|
||||
// It is possible for Members.HasMember to return false even if the email is a group member.
|
||||
// One case that can cause this is if the user email is from a different domain than the group,
|
||||
// e.g. "member@otherdomain.com" in the group "group@mydomain.com" will result in a 400 error
|
||||
// from the HasMember API. In that case, attempt to query the member object directly from the group.
|
||||
req := service.Members.Get(group, email)
|
||||
r, err := req.Do()
|
||||
|
||||
if err != nil {
|
||||
logger.Printf("error using get API to check member %s of google group %s: user not in the group", email, group)
|
||||
continue
|
||||
}
|
||||
|
||||
// If the non-domain user is found within the group, still verify that they are "ACTIVE".
|
||||
// Do not count the user as belonging to a group if they have another status ("ARCHIVED", "SUSPENDED", or "UNKNOWN").
|
||||
if r.Status == "ACTIVE" {
|
||||
return true
|
||||
}
|
||||
if 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 checking group membership: %v", err)
|
||||
logger.Printf("error fetching group members: %v", err)
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if r.IsMember {
|
||||
|
||||
for _, member := range members {
|
||||
switch member.Type {
|
||||
case "CUSTOMER":
|
||||
if member.Id == custID {
|
||||
return true
|
||||
}
|
||||
case "USER":
|
||||
if member.Id == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fetchUser(service *admin.Service, email string) (*admin.User, error) {
|
||||
user, err := service.Users.Get(email).Do()
|
||||
return user, err
|
||||
}
|
||||
|
||||
func fetchGroupMembers(service *admin.Service, group string) ([]*admin.Member, error) {
|
||||
members := []*admin.Member{}
|
||||
pageToken := ""
|
||||
for {
|
||||
req := service.Members.List(group)
|
||||
if pageToken != "" {
|
||||
req.PageToken(pageToken)
|
||||
}
|
||||
r, err := req.Do()
|
||||
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 {
|
||||
|
@ -1,19 +1,14 @@
|
||||
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) {
|
||||
@ -184,56 +179,3 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGoogleProviderUserInGroup(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/groups/group@example.com/hasMember/member-in-domain@example.com" {
|
||||
fmt.Fprintln(w, `{"isMember": true}`)
|
||||
} else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-in-domain@example.com" {
|
||||
fmt.Fprintln(w, `{"isMember": false}`)
|
||||
} else if r.URL.Path == "/groups/group@example.com/hasMember/member-out-of-domain@otherexample.com" {
|
||||
http.Error(
|
||||
w,
|
||||
`{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
} else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-out-of-domain@otherexample.com" {
|
||||
http.Error(
|
||||
w,
|
||||
`{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
} else if r.URL.Path == "/groups/group@example.com/members/non-member-out-of-domain@otherexample.com" {
|
||||
// note that the client currently doesn't care what this response text or code is - any error here results in failure to match the group
|
||||
http.Error(
|
||||
w,
|
||||
`{"error": {"errors": [{"domain": "global","reason": "notFound","message": "Resource Not Found: memberKey"}],"code": 404,"message": "Resource Not Found: memberKey"}}`,
|
||||
http.StatusNotFound,
|
||||
)
|
||||
} else if r.URL.Path == "/groups/group@example.com/members/member-out-of-domain@otherexample.com" {
|
||||
fmt.Fprintln(w,
|
||||
`{"kind": "admin#directory#member","etag":"12345","id":"1234567890","email": "member-out-of-domain@otherexample.com","role": "MEMBER","type": "USER","status": "ACTIVE","delivery_settings": "ALL_MAIL"}}`,
|
||||
)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := ts.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
service, err := admin.NewService(ctx, option.WithHTTPClient(client))
|
||||
service.BasePath = ts.URL
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
result := userInGroup(service, []string{"group@example.com"}, "member-in-domain@example.com")
|
||||
assert.True(t, result)
|
||||
|
||||
result = userInGroup(service, []string{"group@example.com"}, "member-out-of-domain@otherexample.com")
|
||||
assert.True(t, result)
|
||||
|
||||
result = userInGroup(service, []string{"group@example.com"}, "non-member-in-domain@example.com")
|
||||
assert.False(t, result)
|
||||
|
||||
result = userInGroup(service, []string{"group@example.com"}, "non-member-out-of-domain@otherexample.com")
|
||||
assert.False(t, result)
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/pkg/requests"
|
||||
"github.com/pusher/oauth2_proxy/api"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
// 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 || p.Data().ValidateURL.String() == "" {
|
||||
if accessToken == "" || p.Data().ValidateURL == nil {
|
||||
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 := requests.RequestUnparsedResponse(endpoint, header)
|
||||
resp, err := api.RequestUnparsedResponse(endpoint, header)
|
||||
if err != nil {
|
||||
logger.Printf("GET %s", stripToken(endpoint))
|
||||
logger.Printf("token validation request failed: %s", err)
|
||||
|
@ -1,86 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/pkg/requests"
|
||||
)
|
||||
|
||||
type KeycloakProvider struct {
|
||||
*ProviderData
|
||||
Group string
|
||||
}
|
||||
|
||||
func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
|
||||
p.ProviderName = "Keycloak"
|
||||
if p.LoginURL == nil || p.LoginURL.String() == "" {
|
||||
p.LoginURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "keycloak.org",
|
||||
Path: "/oauth/authorize",
|
||||
}
|
||||
}
|
||||
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
|
||||
p.RedeemURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "keycloak.org",
|
||||
Path: "/oauth/token",
|
||||
}
|
||||
}
|
||||
if p.ValidateURL == nil || p.ValidateURL.String() == "" {
|
||||
p.ValidateURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "keycloak.org",
|
||||
Path: "/api/v3/user",
|
||||
}
|
||||
}
|
||||
if p.Scope == "" {
|
||||
p.Scope = "api"
|
||||
}
|
||||
return &KeycloakProvider{ProviderData: p}
|
||||
}
|
||||
|
||||
func (p *KeycloakProvider) SetGroup(group string) {
|
||||
p.Group = group
|
||||
}
|
||||
|
||||
func (p *KeycloakProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
|
||||
|
||||
req, err := http.NewRequest("GET", p.ValidateURL.String(), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+s.AccessToken)
|
||||
if err != nil {
|
||||
logger.Printf("failed building request %s", err)
|
||||
return "", err
|
||||
}
|
||||
json, err := requests.Request(req)
|
||||
if err != nil {
|
||||
logger.Printf("failed making request %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if p.Group != "" {
|
||||
var groups, err = json.Get("groups").Array()
|
||||
if err != nil {
|
||||
logger.Printf("groups not found %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
var found = false
|
||||
for i := range groups {
|
||||
if groups[i].(string) == p.Group {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found != true {
|
||||
logger.Printf("group not found, access denied")
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.Get("email").String()
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
)
|
||||
|
||||
const imaginaryAccessToken = "imaginary_access_token"
|
||||
const bearerAccessToken = "Bearer " + imaginaryAccessToken
|
||||
|
||||
func testKeycloakProvider(hostname, group string) *KeycloakProvider {
|
||||
p := NewKeycloakProvider(
|
||||
&ProviderData{
|
||||
ProviderName: "",
|
||||
LoginURL: &url.URL{},
|
||||
RedeemURL: &url.URL{},
|
||||
ProfileURL: &url.URL{},
|
||||
ValidateURL: &url.URL{},
|
||||
Scope: ""})
|
||||
|
||||
if group != "" {
|
||||
p.SetGroup(group)
|
||||
}
|
||||
|
||||
if hostname != "" {
|
||||
updateURL(p.Data().LoginURL, hostname)
|
||||
updateURL(p.Data().RedeemURL, hostname)
|
||||
updateURL(p.Data().ProfileURL, hostname)
|
||||
updateURL(p.Data().ValidateURL, hostname)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func testKeycloakBackend(payload string) *httptest.Server {
|
||||
path := "/api/v3/user"
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL
|
||||
if url.Path != path {
|
||||
w.WriteHeader(404)
|
||||
} else if r.Header.Get("Authorization") != bearerAccessToken {
|
||||
w.WriteHeader(403)
|
||||
} else {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(payload))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func TestKeycloakProviderDefaults(t *testing.T) {
|
||||
p := testKeycloakProvider("", "")
|
||||
assert.NotEqual(t, nil, p)
|
||||
assert.Equal(t, "Keycloak", p.Data().ProviderName)
|
||||
assert.Equal(t, "https://keycloak.org/oauth/authorize",
|
||||
p.Data().LoginURL.String())
|
||||
assert.Equal(t, "https://keycloak.org/oauth/token",
|
||||
p.Data().RedeemURL.String())
|
||||
assert.Equal(t, "https://keycloak.org/api/v3/user",
|
||||
p.Data().ValidateURL.String())
|
||||
assert.Equal(t, "api", p.Data().Scope)
|
||||
}
|
||||
|
||||
func TestKeycloakProviderOverrides(t *testing.T) {
|
||||
p := NewKeycloakProvider(
|
||||
&ProviderData{
|
||||
LoginURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/oauth/auth"},
|
||||
RedeemURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/oauth/token"},
|
||||
ValidateURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/api/v3/user"},
|
||||
Scope: "profile"})
|
||||
assert.NotEqual(t, nil, p)
|
||||
assert.Equal(t, "Keycloak", p.Data().ProviderName)
|
||||
assert.Equal(t, "https://example.com/oauth/auth",
|
||||
p.Data().LoginURL.String())
|
||||
assert.Equal(t, "https://example.com/oauth/token",
|
||||
p.Data().RedeemURL.String())
|
||||
assert.Equal(t, "https://example.com/api/v3/user",
|
||||
p.Data().ValidateURL.String())
|
||||
assert.Equal(t, "profile", p.Data().Scope)
|
||||
}
|
||||
|
||||
func TestKeycloakProviderGetEmailAddress(t *testing.T) {
|
||||
b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testKeycloakProvider(bURL.Host, "")
|
||||
|
||||
session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||
}
|
||||
|
||||
func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) {
|
||||
b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testKeycloakProvider(bURL.Host, "test-grp1")
|
||||
|
||||
session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||
}
|
||||
|
||||
// Note that trying to trigger the "failed building request" case is not
|
||||
// practical, since the only way it can fail is if the URL fails to parse.
|
||||
func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) {
|
||||
b := testKeycloakBackend("unused payload")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testKeycloakProvider(bURL.Host, "")
|
||||
|
||||
// We'll trigger a request failure by using an unexpected access
|
||||
// token. Alternatively, we could allow the parsing of the payload as
|
||||
// JSON to fail.
|
||||
session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "", email)
|
||||
}
|
||||
|
||||
func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
|
||||
b := testKeycloakBackend("{\"foo\": \"bar\"}")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testKeycloakProvider(bURL.Host, "")
|
||||
|
||||
session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
|
||||
email, err := p.GetEmailAddress(session)
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "", email)
|
||||
}
|
@ -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 := requests.Request(req)
|
||||
json, err := api.Request(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -252,7 +252,6 @@ func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.Session
|
||||
s = &sessions.SessionState{
|
||||
AccessToken: jsonResponse.AccessToken,
|
||||
IDToken: jsonResponse.IDToken,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
|
||||
Email: email,
|
||||
}
|
||||
|
@ -3,13 +3,10 @@ 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"
|
||||
)
|
||||
|
||||
@ -18,7 +15,6 @@ type OIDCProvider struct {
|
||||
*ProviderData
|
||||
|
||||
Verifier *oidc.IDTokenVerifier
|
||||
AllowUnverifiedEmail bool
|
||||
}
|
||||
|
||||
// NewOIDCProvider initiates a new OIDCProvider
|
||||
@ -91,7 +87,6 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error)
|
||||
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
|
||||
@ -120,33 +115,10 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
|
||||
}
|
||||
|
||||
if claims.Email == "" {
|
||||
if p.ProfileURL.String() == "" {
|
||||
return nil, fmt.Errorf("id_token did not contain an email")
|
||||
// TODO: Try getting email from /userinfo before falling back to Subject
|
||||
claims.Email = claims.Subject
|
||||
}
|
||||
|
||||
// If the userinfo endpoint profileURL is defined, then there is a chance the userinfo
|
||||
// contents at the profileURL contains the email.
|
||||
// Make a query to the userinfo endpoint, and attempt to locate the email from there.
|
||||
|
||||
req, err := http.NewRequest("GET", p.ProfileURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = getOIDCHeader(token.AccessToken)
|
||||
|
||||
respJSON, err := requests.Request(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
email, err := respJSON.Get("email").String()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Neither id_token nor userinfo endpoint contained an email")
|
||||
}
|
||||
|
||||
claims.Email = email
|
||||
}
|
||||
if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified {
|
||||
if claims.Verified != nil && !*claims.Verified {
|
||||
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
|
||||
}
|
||||
|
||||
@ -154,8 +126,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
|
||||
AccessToken: token.AccessToken,
|
||||
IDToken: rawIDToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: idToken.Expiry,
|
||||
ExpiresOn: token.Expiry,
|
||||
Email: claims.Email,
|
||||
User: claims.Subject,
|
||||
}, nil
|
||||
@ -171,10 +142,3 @@ 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
|
||||
}
|
||||
|
@ -8,10 +8,9 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"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
|
||||
@ -73,7 +72,7 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionStat
|
||||
return
|
||||
}
|
||||
if a := v.Get("access_token"); a != "" {
|
||||
s = &sessions.SessionState{AccessToken: a, CreatedAt: time.Now()}
|
||||
s = &sessions.SessionState{AccessToken: a}
|
||||
} else {
|
||||
err = fmt.Errorf("no access token found %s", body)
|
||||
}
|
||||
@ -96,12 +95,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 *encryption.Cipher) (string, error) {
|
||||
func (p *ProviderData) CookieForSession(s *sessions.SessionState, c *cookie.Cipher) (string, error) {
|
||||
return s.EncodeSessionState(c)
|
||||
}
|
||||
|
||||
// SessionFromCookie deserializes a session from a cookie value
|
||||
func (p *ProviderData) SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) {
|
||||
func (p *ProviderData) SessionFromCookie(v string, c *cookie.Cipher) (s *sessions.SessionState, err error) {
|
||||
return sessions.DecodeSessionState(v, c)
|
||||
}
|
||||
|
||||
|
@ -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, *encryption.Cipher) (*sessions.SessionState, error)
|
||||
CookieForSession(*sessions.SessionState, *encryption.Cipher) (string, error)
|
||||
SessionFromCookie(string, *cookie.Cipher) (*sessions.SessionState, error)
|
||||
CookieForSession(*sessions.SessionState, *cookie.Cipher) (string, error)
|
||||
}
|
||||
|
||||
// New provides a new Provider based on the configured provider string
|
||||
@ -28,8 +28,6 @@ func New(provider string, p *ProviderData) Provider {
|
||||
return NewFacebookProvider(p)
|
||||
case "github":
|
||||
return NewGitHubProvider(p)
|
||||
case "keycloak":
|
||||
return NewKeycloakProvider(p)
|
||||
case "azure":
|
||||
return NewAzureProvider(p)
|
||||
case "gitlab":
|
||||
@ -38,8 +36,6 @@ 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)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"html/template"
|
||||
"path"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
func loadTemplates(dir string) *template.Template {
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
)
|
||||
|
||||
// UserMap holds information from the authenticated emails file
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
"github.com/pusher/oauth2_proxy/logger"
|
||||
fsnotify "gopkg.in/fsnotify/fsnotify.v1"
|
||||
)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
import "github.com/pusher/oauth2_proxy/logger"
|
||||
|
||||
func WatchForUpdates(filename string, done <-chan bool, action func()) {
|
||||
logger.Printf("file watching not implemented on this platform")
|
||||
|
Loading…
Reference in New Issue
Block a user