diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6af708f88719ddf020e4df8f7f11be999a36599e..6f31d85fdd9efcff902efdc25b2798c8749be9e6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,10 +1,9 @@ -# 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 @JoelSpeed +# Default owner should be a core maintainer unless overridden by later rules in this file +* @steakunderscore @JoelSpeed # login.gov provider # Note: If @timothy-spencer terms out of his appointment, your best bet -# for finding somebody who can test the oauth2_proxy would be to ask somebody +# for finding somebody who can test the oauth2-proxy would be to ask somebody # in the login.gov team (https://login.gov/developers/), the cloud.gov team # (https://cloud.gov/docs/help/), or the 18F org (https://18f.gsa.gov/contact/ # or the public devops channel at https://chat.18f.gov/). diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000000000000000000000000000000000..0a1df293a85f1a4997b12be60abdb4a535aad680 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: "Code scanning - action" + +on: + push: + schedule: + - cron: '0 0 * * 0' + +jobs: + CodeQL-Build: + + strategy: + fail-fast: false + + # CodeQL runs on ubuntu-latest and windows-latest + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: go + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # âšī¸ Command-line programs to run using the OS shell. + # đ https://git.io/JvXDl + + # âī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a21b5449c1d345d9f6778e37ec5f3d1ec9782c1 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,17 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.' + stale-pr-message: 'This pull request has been inactive for 60 days. If the pull request is still relevant please comment to re-activate the pull request. If no action is taken within 7 days, the pull request will be marked closed.' diff --git a/.gitignore b/.gitignore index aff7b5b342b981bb78975d077631a24d03977747..7f352b8a4c2c6889b14ac33837f04d7665608d32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -oauth2_proxy +oauth2-proxy vendor dist release @@ -6,6 +6,7 @@ release *.exe .env .bundle +c.out # Go.gitignore # Compiled Object files, Static and Dynamic libs (Shared Objects) diff --git a/.golangci.yml b/.golangci.yml index a0658e1c424b21272a97dd867819654a8c8dbe6e..9ee860a15e346aa83f25c21e0cc1b5eda9e11fe2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,5 +9,27 @@ linters: - deadcode - gofmt - goimports - enable-all: false + - gosimple + - staticcheck + - structcheck + - typecheck + - unused + - varcheck + - bodyclose + - dogsled + - goprintffuncname + - misspell + - prealloc + - scopelint + - stylecheck + - unconvert + - gocritic disable-all: true +issues: + exclude-rules: + - path: _test\.go + linters: + - scopelint + - bodyclose + - unconvert + - gocritic diff --git a/.travis.yml b/.travis.yml index ef8aa3ec5d5067743ad52bb4821ae2970ba22e44..8b39182ad8187db375cb08738dcb14de4e3eada8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,20 @@ language: go go: - - 1.13.x + - 1.14.x +env: + - COVER=true install: # Fetch dependencies - - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.17.1 + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.24.0 - GO111MODULE=on go mod download + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter +before_script: + - ./cc-test-reporter before-build script: - - ./configure && make test + - make test +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT -t gocov sudo: false notifications: email: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 462045080bcd31cb269bf961347a7512be28cf55..d0952d388bf7b44fe4bf147784475031cea54855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,47 +1,160 @@ # Vx.x.x (Pre-release) -## Release Hightlights +## Release Highlights ## Important Notes -- [#335] The session expiry for the OIDC provider is now taken from the Token Response (expires_in) rather than from the id_token (exp) + +- [#453](https://github.com/oauth2-proxy/oauth2-proxy/pull/453) Responses to endpoints with a proxy prefix will now return headers for preventing browser caching. ## Breaking Changes +- [#464](https://github.com/oauth2-proxy/oauth2-proxy/pull/464) Migration from Pusher to independent org may have introduced breaking changes for your environment. + - See the changes listed below for PR [#464](https://github.com/oauth2-proxy/oauth2-proxy/pull/464) for full details + - Binaries renamed from `oauth2_proxy` to `oauth2-proxy` +- [#440](https://github.com/oauth2-proxy/oauth2-proxy/pull/440) Switch Azure AD Graph API to Microsoft Graph API + - The Azure AD Graph API has been [deprecated](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api) and is being replaced by the Microsoft Graph API. + If your application relies on the access token being passed to it to access the Azure AD Graph API, you should migrate your application to use the Microsoft Graph API. + Existing behaviour can be retained by setting `-resource=https://graph.windows.net`. +- [#484](https://github.com/oauth2-proxy/oauth2-proxy/pull/484) Configuration loading has been replaced with Viper and PFlag + - Flags now require a `--` prefix before the option + - Previously flags allowed either `-` or `--` to prefix the option name + - Eg `-provider` must now be `--provider` +- [#487](https://github.com/oauth2-proxy/oauth2-proxy/pull/487) Switch flags to StringSlice instead of StringArray + - Options that take multiple arguments now split strings on commas if present + - Eg `--foo=a,b,c,d` would result in the values `a`, `b`, `c` and `d` instead of a single `a,b,c,d` value as before +- [#535](https://github.com/oauth2-proxy/oauth2-proxy/pull/535) Drop support for pre v3.1 cookies + - The encoding for session cookies was changed starting in v3.1.0, support for the previous encoding is now dropped + - If you are upgrading from a version earlier than this, please upgrade via a version between v3.1.0 and v5.1.1 +- [#537](https://github.com/oauth2-proxy/oauth2-proxy/pull/537) Drop Fallback to Email if User not set + - Previously, when a session was loaded, if the User was not set, it would be replaced by the Email. + This behaviour was inconsistent as it required the session to be stored and then loaded to function properly. + - This behaviour has now been removed and the User field will remain empty if it was not set when the session was saved. + - In some scenarios `X-Forwarded-User` will now be empty. Use `X-Forwarded-Email` instead. + - In some scenarios, this may break setting Basic Auth on upstream or responses. + Use `--prefer-email-to-user` to restore falling back to the Email in these cases. +- [#556](https://github.com/oauth2-proxy/oauth2-proxy/pull/556) Remove unintentional auto-padding of secrets that were too short + - Previously, after cookie-secrets were opportunistically base64 decoded to raw bytes, + they were padded to have a length divisible by 4. + - This led to wrong sized secrets being valid AES lengths of 16, 24, or 32 bytes. Or it led to confusing errors + reporting an invalid length of 20 or 28 when the user input cookie-secret was not that length. + - Now we will only base64 decode a cookie-secret to raw bytes if it is 16, 24, or 32 bytes long. Otherwise, we will convert + the direct cookie-secret to bytes without silent padding added. + +## Changes since v5.1.1 +- [#556](https://github.com/oauth2-proxy/oauth2-proxy/pull/556) Remove unintentional auto-padding of secrets that were too short (@NickMeves) +- [#538](https://github.com/oauth2-proxy/oauth2-proxy/pull/538) Refactor sessions/utils.go functionality to other areas (@NickMeves) +- [#503](https://github.com/oauth2-proxy/oauth2-proxy/pull/503) Implements --real-client-ip-header option to select the header from which to obtain a proxied client's IP (@Izzette) +- [#529](https://github.com/oauth2-proxy/oauth2-proxy/pull/529) Add local test environments for testing changes and new features (@JoelSpeed) +- [#537](https://github.com/oauth2-proxy/oauth2-proxy/pull/537) Drop Fallback to Email if User not set (@JoelSpeed) +- [#535](https://github.com/oauth2-proxy/oauth2-proxy/pull/535) Drop support for pre v3.1 cookies (@JoelSpeed) +- [#533](https://github.com/oauth2-proxy/oauth2-proxy/pull/487) Set up code coverage within Travis for Code Climate (@JoelSpeed) +- [#514](https://github.com/oauth2-proxy/oauth2-proxy/pull/514) Add basic string functions to templates +- [#524](https://github.com/oauth2-proxy/oauth2-proxy/pull/524) Sign cookies with SHA256 (@NickMeves) +- [#515](https://github.com/oauth2-proxy/oauth2-proxy/pull/515) Drop configure script in favour of native Makefile env and checks (@JoelSpeed) +- [#519](https://github.com/oauth2-proxy/oauth2-proxy/pull/519) Support context in providers (@johejo) +- [#487](https://github.com/oauth2-proxy/oauth2-proxy/pull/487) Switch flags to PFlag to remove StringArray (@JoelSpeed) +- [#484](https://github.com/oauth2-proxy/oauth2-proxy/pull/484) Replace configuration loading with Viper (@JoelSpeed) +- [#499](https://github.com/oauth2-proxy/oauth2-proxy/pull/499) Add `-user-id-claim` to support generic claims in addition to email (@holyjak) +- [#486](https://github.com/oauth2-proxy/oauth2-proxy/pull/486) Add new linters (@johejo) +- [#440](https://github.com/oauth2-proxy/oauth2-proxy/pull/440) Switch Azure AD Graph API to Microsoft Graph API (@johejo) +- [#453](https://github.com/oauth2-proxy/oauth2-proxy/pull/453) Prevent browser caching during auth flow (@johejo) +- [#467](https://github.com/oauth2-proxy/oauth2-proxy/pull/467) Allow OIDC issuer verification to be skipped (@chkohner) +- [#481](https://github.com/oauth2-proxy/oauth2-proxy/pull/481) Update Okta docs (@trevorbox) +- [#474](https://github.com/oauth2-proxy/oauth2-proxy/pull/474) Always log hasMember request error object (@jbielick) +- [#468](https://github.com/oauth2-proxy/oauth2-proxy/pull/468) Implement graceful shutdown and propagate request context (@johejo) +- [#464](https://github.com/oauth2-proxy/oauth2-proxy/pull/464) Migrate to oauth2-proxy/oauth2-proxy (@JoelSpeed) + - Project renamed from `pusher/oauth2_proxy` to `oauth2-proxy` + - Move Go import path from `github.com/pusher/oauth2_proxy` to `github.com/oauth2-proxy/oauth2-proxy` + - Remove Pusher Cloud Team from CODEOWNERS + - Release images moved to `quay.io/oauth2-proxy/oauth2-proxy` + - Binaries renamed from `oauth2_proxy` to `oauth2-proxy` +- [#432](https://github.com/oauth2-proxy/oauth2-proxy/pull/432) Update ruby dependencies for documentation (@theobarberbany) +- [#471](https://github.com/oauth2-proxy/oauth2-proxy/pull/471) Add logging in case of invalid redirects (@gargath) +- [#462](https://github.com/oauth2-proxy/oauth2-proxy/pull/462) Allow HTML in banner message (@eritikass) +- [#412](https://github.com/oauth2-proxy/oauth2-proxy/pull/412) Allow multiple cookie domains to be specified (@edahlseng) +- [#413](https://github.com/oauth2-proxy/oauth2-proxy/pull/413) Add -set-basic-auth param to set the Basic Authorization header for upstreams (@morarucostel) +- [#483](https://github.com/oauth2-proxy/oauth2-proxy/pull/483) Warn users when session cookies are split (@JoelSpeed) +- [#488](https://github.com/oauth2-proxy/oauth2-proxy/pull/488) Set-Basic-Auth should default to false (@JoelSpeed) +- [#494](https://github.com/oauth2-proxy/oauth2-proxy/pull/494) Upstream websockets TLS certificate validation now depends on ssl-upstream-insecure-skip-verify (@yaroslavros) +- [#497](https://github.com/oauth2-proxy/oauth2-proxy/pull/497) Restrict access using Github collaborators (@jsclayton) + +# v5.1.1 + +## Release Highlights + +N/A + +## Important Notes + +- (Security) Fix for [open redirect vulnerability](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-j7px-6hwj-hpjg). + - A bad actor using encoded whitespace in redirect URIs can redirect a session to another domain + +## Breaking Changes + +N/A + +## Changes since v5.1.0 + +- [GHSA-j7px-6hwj-hpjg](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-j7px-6hwj-hpjg) Fix Open Redirect Vulnerability with encoded Whitespace characters (@JoelSpeed) + +# v5.1.0 + +## Release Highlights +- Bump to Go 1.14 +- Reduced number of Google API requests for group validation +- Support for Redis Cluster +- Support for overriding hosts in hosts file + +## Important Notes +- [#335] The session expiry for the OIDC provider is now taken from the Token Response (expires_in) rather than from the id_token (exp) + +## Breaking Changes +N/A + ## Changes since v5.0.0 -- [#385](https://github.com/pusher/oauth2_proxy/pull/385) Use the `Authorization` header instead of `access_token` for refreshing GitHub Provider sessions (@ibuclaw) -- [#372](https://github.com/pusher/oauth2_proxy/pull/372) Allow fallback to secondary verified email address in GitHub provider (@dmnemec) -- [#335](https://github.com/pusher/oauth2_proxy/pull/335) OIDC Provider support for empty id_tokens in the access token refresh response (@howzat) -- [#363](https://github.com/pusher/oauth2_proxy/pull/363) Extension of Redis Session Store to Support Redis Cluster (@yan-dblinf) -- [#353](https://github.com/pusher/oauth2_proxy/pull/353) Fix login page fragment handling after soft reload on Firefox (@ffdybuster) -- [#355](https://github.com/pusher/oauth2_proxy/pull/355) Add Client Secret File support for providers that rotate client secret via file system (@pasha-r) +- [#450](https://github.com/oauth2-proxy/oauth2-proxy/pull/450) Fix http.Cookie SameSite is not copied (@johejo) +- [#445](https://github.com/oauth2-proxy/oauth2-proxy/pull/445) Expose `acr_values` to all providers (@holyjak) +- [#419](https://github.com/oauth2-proxy/oauth2-proxy/pull/419) Support Go 1.14, upgrade dependencies, upgrade golangci-lint to 1.23.6 (@johejo) +- [#444](https://github.com/oauth2-proxy/oauth2-proxy/pull/444) Support prompt in addition to approval-prompt (@holyjak) +- [#435](https://github.com/oauth2-proxy/oauth2-proxy/pull/435) Fix issue with group validation calling google directory API on every HTTP request (@ericofusco) +- [#400](https://github.com/oauth2-proxy/oauth2-proxy/pull/400) Add `nsswitch.conf` to Docker image to allow hosts file to work (@luketainton) +- [#385](https://github.com/oauth2-proxy/oauth2-proxy/pull/385) Use the `Authorization` header instead of `access_token` for refreshing GitHub Provider sessions (@ibuclaw) +- [#372](https://github.com/oauth2-proxy/oauth2-proxy/pull/372) Allow fallback to secondary verified email address in GitHub provider (@dmnemec) +- [#335](https://github.com/oauth2-proxy/oauth2-proxy/pull/335) OIDC Provider support for empty id_tokens in the access token refresh response (@howzat) +- [#363](https://github.com/oauth2-proxy/oauth2-proxy/pull/363) Extension of Redis Session Store to Support Redis Cluster (@yan-dblinf) +- [#353](https://github.com/oauth2-proxy/oauth2-proxy/pull/353) Fix login page fragment handling after soft reload on Firefox (@ffdybuster) +- [#355](https://github.com/oauth2-proxy/oauth2-proxy/pull/355) Add Client Secret File support for providers that rotate client secret via file system (@pasha-r) +- [#401](https://github.com/oauth2-proxy/oauth2-proxy/pull/401) Give the option to pass email address in the Basic auth header instead of upstream usernames. (@Spindel) +- [#405](https://github.com/oauth2-proxy/oauth2-proxy/pull/405) The `/sign_in` page now honors the `rd` query parameter, fixing the redirect after a successful authentication (@ti-mo) +- [#434](https://github.com/oauth2-proxy/oauth2-proxy/pull/434) Give the option to prefer email address in the username header when using the -pass-user-headers option (@jordancrawfordnz) # v5.0.0 -## Release Hightlights +## Release Highlights - Disabled CGO (binaries will work regardless og glibc/musl) - Allow whitelisted redirect ports - Nextcloud provider support added - DigitalOcean provider support added ## Important Notes -- (Security) Fix for [open redirect vulnerability](https://github.com/pusher/oauth2_proxy/security/advisories/GHSA-qqxw-m5fj-f7gv).. a bad actor using `/\` in redirect URIs can redirect a session to another domain +- (Security) Fix for [open redirect vulnerability](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-qqxw-m5fj-f7gv).. a bad actor using `/\` in redirect URIs can redirect a session to another domain ## Breaking Changes -- [#321](https://github.com/pusher/oauth2_proxy/pull/331) Add reverse proxy boolean flag to control whether headers like `X-Real-Ip` are accepted. +- [#321](https://github.com/oauth2-proxy/oauth2-proxy/pull/331) Add reverse proxy boolean flag to control whether headers like `X-Real-Ip` are accepted. This defaults to false. **Usage behind a reverse proxy will require this flag to be set to avoid logging the reverse proxy IP address**. ## Changes since v4.1.0 -- [#331](https://github.com/pusher/oauth2_proxy/pull/331) Add reverse proxy setting (@martin-css) -- [#365](https://github.com/pusher/oauth2_proxy/pull/365) Build with CGO=0 (@tomelliff) -- [#339](https://github.com/pusher/oauth2_proxy/pull/339) Add configuration for cookie 'SameSite' value. (@pgroudas) -- [#347](https://github.com/pusher/oauth2_proxy/pull/347) Update keycloak provider configuration documentation. (@sushiMix) -- [#325](https://github.com/pusher/oauth2_proxy/pull/325) dist.sh: use sha256sum (@syscll) -- [#179](https://github.com/pusher/oauth2_proxy/pull/179) Add Nextcloud provider (@Ramblurr) -- [#280](https://github.com/pusher/oauth2_proxy/pull/280) whitelisted redirect domains: add support for whitelisting specific ports or allowing wildcard ports (@kamaln7) -- [#351](https://github.com/pusher/oauth2_proxy/pull/351) Add DigitalOcean Auth provider (@kamaln7) +- [#331](https://github.com/oauth2-proxy/oauth2-proxy/pull/331) Add reverse proxy setting (@martin-css) +- [#365](https://github.com/oauth2-proxy/oauth2-proxy/pull/365) Build with CGO=0 (@tomelliff) +- [#339](https://github.com/oauth2-proxy/oauth2-proxy/pull/339) Add configuration for cookie 'SameSite' value. (@pgroudas) +- [#347](https://github.com/oauth2-proxy/oauth2-proxy/pull/347) Update keycloak provider configuration documentation. (@sushiMix) +- [#325](https://github.com/oauth2-proxy/oauth2-proxy/pull/325) dist.sh: use sha256sum (@syscll) +- [#179](https://github.com/oauth2-proxy/oauth2-proxy/pull/179) Add Nextcloud provider (@Ramblurr) +- [#280](https://github.com/oauth2-proxy/oauth2-proxy/pull/280) whitelisted redirect domains: add support for whitelisting specific ports or allowing wildcard ports (@kamaln7) +- [#351](https://github.com/oauth2-proxy/oauth2-proxy/pull/351) Add DigitalOcean Auth provider (@kamaln7) # v4.1.0 @@ -60,29 +173,29 @@ N/A N/A ## Changes since v4.0.0 -- [#292](https://github.com/pusher/oauth2_proxy/pull/292) Added bash >= 4.0 dependency to configure script (@jmfrank63) -- [#227](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka) -- [#259](https://github.com/pusher/oauth2_proxy/pull/259) Redirect to HTTPS (@jmickey) -- [#273](https://github.com/pusher/oauth2_proxy/pull/273) Support Go 1.13 (@dio) -- [#275](https://github.com/pusher/oauth2_proxy/pull/275) docker: build from debian buster (@syscll) -- [#258](https://github.com/pusher/oauth2_proxy/pull/258) Add IDToken for Azure provider (@leyshon) - - This PR adds the IDToken into the session for the Azure provider allowing requests to a backend to be identified as a specific user. As a consequence, if you are using a cookie to store the session the cookie will now exceed the 4kb size limit and be split into multiple cookies. This can cause problems when using nginx as a proxy, resulting in no cookie being passed at all. Either increase the proxy_buffer_size in nginx or implement the redis session storage (see https://pusher.github.io/oauth2_proxy/configuration#redis-storage) -- [#286](https://github.com/pusher/oauth2_proxy/pull/286) Requests.go updated with useful error messages (@biotom) -- [#274](https://github.com/pusher/oauth2_proxy/pull/274) Supports many github teams with api pagination support (@toshi-miura, @apratina) -- [#302](https://github.com/pusher/oauth2_proxy/pull/302) Rewrite dist script (@syscll) -- [#304](https://github.com/pusher/oauth2_proxy/pull/304) Add new Logo! :tada: (@JoelSpeed) -- [#300](https://github.com/pusher/oauth2_proxy/pull/300) Added userinfo endpoint (@kbabuadze) -- [#309](https://github.com/pusher/oauth2_proxy/pull/309) Added support for custom CA when connecting to Redis cache (@lleszczu) -- [#248](https://github.com/pusher/oauth2_proxy/pull/248) Fix issue with X-Auth-Request-Redirect header being ignored (@webnard) -- [#314](https://github.com/pusher/oauth2_proxy/pull/314) Add redirect capability to sign_out (@costelmoraru) -- [#265](https://github.com/pusher/oauth2_proxy/pull/265) Add upstream with static response (@cgroschupp) -- [#317](https://github.com/pusher/oauth2_proxy/pull/317) Add build for FreeBSD (@fnkr) -- [#296](https://github.com/pusher/oauth2_proxy/pull/296) Allow to override provider's name for sign-in page (@ffdybuster) +- [#292](https://github.com/oauth2-proxy/oauth2-proxy/pull/292) Added bash >= 4.0 dependency to configure script (@jmfrank63) +- [#227](https://github.com/oauth2-proxy/oauth2-proxy/pull/227) Add Keycloak provider (@Ofinka) +- [#259](https://github.com/oauth2-proxy/oauth2-proxy/pull/259) Redirect to HTTPS (@jmickey) +- [#273](https://github.com/oauth2-proxy/oauth2-proxy/pull/273) Support Go 1.13 (@dio) +- [#275](https://github.com/oauth2-proxy/oauth2-proxy/pull/275) docker: build from debian buster (@syscll) +- [#258](https://github.com/oauth2-proxy/oauth2-proxy/pull/258) Add IDToken for Azure provider (@leyshon) + - This PR adds the IDToken into the session for the Azure provider allowing requests to a backend to be identified as a specific user. As a consequence, if you are using a cookie to store the session the cookie will now exceed the 4kb size limit and be split into multiple cookies. This can cause problems when using nginx as a proxy, resulting in no cookie being passed at all. Either increase the proxy_buffer_size in nginx or implement the redis session storage (see https://oauth2-proxy.github.io/oauth2-proxy/configuration#redis-storage) +- [#286](https://github.com/oauth2-proxy/oauth2-proxy/pull/286) Requests.go updated with useful error messages (@biotom) +- [#274](https://github.com/oauth2-proxy/oauth2-proxy/pull/274) Supports many github teams with api pagination support (@toshi-miura, @apratina) +- [#302](https://github.com/oauth2-proxy/oauth2-proxy/pull/302) Rewrite dist script (@syscll) +- [#304](https://github.com/oauth2-proxy/oauth2-proxy/pull/304) Add new Logo! :tada: (@JoelSpeed) +- [#300](https://github.com/oauth2-proxy/oauth2-proxy/pull/300) Added userinfo endpoint (@kbabuadze) +- [#309](https://github.com/oauth2-proxy/oauth2-proxy/pull/309) Added support for custom CA when connecting to Redis cache (@lleszczu) +- [#248](https://github.com/oauth2-proxy/oauth2-proxy/pull/248) Fix issue with X-Auth-Request-Redirect header being ignored (@webnard) +- [#314](https://github.com/oauth2-proxy/oauth2-proxy/pull/314) Add redirect capability to sign_out (@costelmoraru) +- [#265](https://github.com/oauth2-proxy/oauth2-proxy/pull/265) Add upstream with static response (@cgroschupp) +- [#317](https://github.com/oauth2-proxy/oauth2-proxy/pull/317) Add build for FreeBSD (@fnkr) +- [#296](https://github.com/oauth2-proxy/oauth2-proxy/pull/296) Allow to override provider's name for sign-in page (@ffdybuster) # v4.0.0 ## Release Highlights -- Documentation is now on a [microsite](https://pusher.github.io/oauth2_proxy/) +- Documentation is now on a [microsite](https://oauth2-proxy.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) @@ -94,11 +207,11 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly. ## Breaking Changes -- [#231](https://github.com/pusher/oauth2_proxy/pull/231) Rework GitLab provider +- [#231](https://github.com/oauth2-proxy/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 +- [#186](https://github.com/oauth2-proxy/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: @@ -114,34 +227,34 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly. 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/oauth2-proxy/oauth2-proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field - This change modifies the contents of the `X-Forwarded-User` header supplied by the proxy for users where the auth response from the IdP did not contain a username. In that case, this header used to only contain the local part of the user's email address (e.g. `john.doe` for `john.doe@example.com`) but now contains the user's full email address instead. -- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Pre-built binary tarballs changed format +- [#170](https://github.com/oauth2-proxy/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 +- [#234](https://github.com/oauth2-proxy/oauth2-proxy/pull/234) Added option `-ssl-upstream-insecure-skip-validation` to skip validation of upstream SSL certificates (@jansinger) +- [#224](https://github.com/oauth2-proxy/oauth2-proxy/pull/224) Check Google group membership using hasMember to support nested groups and external users (@jpalpant) +- [#231](https://github.com/oauth2-proxy/oauth2-proxy/pull/231) Add optional group membership and email domain checks to the GitLab provider (@Overv) +- [#226](https://github.com/oauth2-proxy/oauth2-proxy/pull/226) Made setting of proxied headers deterministic based on configuration alone (@aeijdenberg) +- [#178](https://github.com/oauth2-proxy/oauth2-proxy/pull/178) Add Silence Ping Logging and Exclude Logging Paths flags (@kskewes) +- [#209](https://github.com/oauth2-proxy/oauth2-proxy/pull/209) Improve docker build caching of layers (@dekimsey) +- [#186](https://github.com/oauth2-proxy/oauth2-proxy/pull/186) Make config consistent (@JoelSpeed) +- [#187](https://github.com/oauth2-proxy/oauth2-proxy/pull/187) Move root packages to pkg folder (@JoelSpeed) +- [#65](https://github.com/oauth2-proxy/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). +- [#180](https://github.com/oauth2-proxy/oauth2-proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg). +- [#175](https://github.com/oauth2-proxy/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) +- [#155](https://github.com/oauth2-proxy/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 @@ -151,18 +264,18 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly. - 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) +- [#168](https://github.com/oauth2-proxy/oauth2-proxy/pull/168) Drop Go 1.11 support in Travis (@JoelSpeed) +- [#169](https://github.com/oauth2-proxy/oauth2-proxy/pull/169) Update Alpine to 3.9 (@kskewes) +- [#148](https://github.com/oauth2-proxy/oauth2-proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed) +- [#147](https://github.com/oauth2-proxy/oauth2-proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed) - Allows for multiple different session storage implementations including client and server side - Adds tests suite for interface to ensure consistency across implementations - Refactor some configuration options (around cookies) into packages -- [#114](https://github.com/pusher/oauth2_proxy/pull/114), [#154](https://github.com/pusher/oauth2_proxy/pull/154) Documentation is now available live at our [docs website](https://pusher.github.io/oauth2_proxy/) (@JoelSpeed, @icelynjennings) -- [#146](https://github.com/pusher/oauth2_proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field (@gargath) -- [#144](https://github.com/pusher/oauth2_proxy/pull/144) Use GO 1.12 for ARM builds (@kskewes) -- [#142](https://github.com/pusher/oauth2_proxy/pull/142) ARM Docker USER fix (@kskewes) -- [#52](https://github.com/pusher/oauth2_proxy/pull/52) Logging Improvements (@MisterWil) +- [#114](https://github.com/oauth2-proxy/oauth2-proxy/pull/114), [#154](https://github.com/oauth2-proxy/oauth2-proxy/pull/154) Documentation is now available live at our [docs website](https://oauth2-proxy.github.io/oauth2-proxy/) (@JoelSpeed, @icelynjennings) +- [#146](https://github.com/oauth2-proxy/oauth2-proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field (@gargath) +- [#144](https://github.com/oauth2-proxy/oauth2-proxy/pull/144) Use GO 1.12 for ARM builds (@kskewes) +- [#142](https://github.com/oauth2-proxy/oauth2-proxy/pull/142) ARM Docker USER fix (@kskewes) +- [#52](https://github.com/oauth2-proxy/oauth2-proxy/pull/52) Logging Improvements (@MisterWil) - Implement flags to configure file logging - `-logging-filename` Defines the filename to log to - `-logging-max-size` Defines the maximum @@ -176,21 +289,21 @@ 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) +- [#111](https://github.com/oauth2-proxy/oauth2-proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer) +- [#170](https://github.com/oauth2-proxy/oauth2-proxy/pull/170) Restore binary tarball contents to be compatible with bitlys original tarballs (@zeha) +- [#185](https://github.com/oauth2-proxy/oauth2-proxy/pull/185) Fix an unsupported protocol scheme error during token validation when using the Azure provider (@jonas) +- [#141](https://github.com/oauth2-proxy/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) +- [#195](https://github.com/oauth2-proxy/oauth2-proxy/pull/195) Add `-banner` flag for overriding the banner line that is displayed (@steakunderscore) +- [#198](https://github.com/oauth2-proxy/oauth2-proxy/pull/198) Switch from gometalinter to golangci-lint (@steakunderscore) +- [#159](https://github.com/oauth2-proxy/oauth2-proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email` (@djfinlay) +- [#210](https://github.com/oauth2-proxy/oauth2-proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore) +- [#201](https://github.com/oauth2-proxy/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) +- [#211](https://github.com/oauth2-proxy/oauth2-proxy/pull/211) Switch from dep to go modules (@steakunderscore) +- [#145](https://github.com/oauth2-proxy/oauth2-proxy/pull/145) Add support for OIDC UserInfo endpoint email verification (@rtluckie) # v3.2.0 @@ -210,25 +323,25 @@ instead of returning an error ## Changes since v3.1.0 - [#96](https://github.com/bitly/oauth2_proxy/pull/96) Check if email is verified on GitHub (@caarlos0) -- [#110](https://github.com/pusher/oauth2_proxy/pull/110) Added GCP healthcheck option (@timothy-spencer) -- [#112](https://github.com/pusher/oauth2_proxy/pull/112) Improve websocket support (@gyson) -- [#63](https://github.com/pusher/oauth2_proxy/pull/63) Use encoding/json for SessionState serialization (@yaegashi) +- [#110](https://github.com/oauth2-proxy/oauth2-proxy/pull/110) Added GCP healthcheck option (@timothy-spencer) +- [#112](https://github.com/oauth2-proxy/oauth2-proxy/pull/112) Improve websocket support (@gyson) +- [#63](https://github.com/oauth2-proxy/oauth2-proxy/pull/63) Use encoding/json for SessionState serialization (@yaegashi) - Use JSON to encode session state to be stored in browser cookies - Implement legacy decode function to support existing cookies generated by older versions - Add detailed table driven tests in session_state_test.go -- [#120](https://github.com/pusher/oauth2_proxy/pull/120) Encrypting user/email from cookie (@costelmoraru) -- [#55](https://github.com/pusher/oauth2_proxy/pull/55) Added login.gov provider (@timothy-spencer) -- [#55](https://github.com/pusher/oauth2_proxy/pull/55) Added environment variables for all config options (@timothy-spencer) -- [#70](https://github.com/pusher/oauth2_proxy/pull/70) Fix handling of splitted cookies (@einfachchr) -- [#92](https://github.com/pusher/oauth2_proxy/pull/92) Merge websocket proxy feature from openshift/oauth-proxy (@butzist) -- [#57](https://github.com/pusher/oauth2_proxy/pull/57) Fall back to using OIDC Subject instead of Email (@aigarius) -- [#85](https://github.com/pusher/oauth2_proxy/pull/85) Use non-root user in docker images (@kskewes) -- [#68](https://github.com/pusher/oauth2_proxy/pull/68) forward X-Auth-Access-Token header (@davidholsgrove) -- [#41](https://github.com/pusher/oauth2_proxy/pull/41) Added option to manually specify OIDC endpoints instead of relying on discovery -- [#83](https://github.com/pusher/oauth2_proxy/pull/83) Add `id_token` refresh to Google provider (@leki75) -- [#10](https://github.com/pusher/oauth2_proxy/pull/10) fix redirect url param handling (@dt-rush) -- [#122](https://github.com/pusher/oauth2_proxy/pull/122) Expose -cookie-path as configuration parameter (@costelmoraru) -- [#124](https://github.com/pusher/oauth2_proxy/pull/124) Use Go 1.12 for testing and build environments (@syscll) +- [#120](https://github.com/oauth2-proxy/oauth2-proxy/pull/120) Encrypting user/email from cookie (@costelmoraru) +- [#55](https://github.com/oauth2-proxy/oauth2-proxy/pull/55) Added login.gov provider (@timothy-spencer) +- [#55](https://github.com/oauth2-proxy/oauth2-proxy/pull/55) Added environment variables for all config options (@timothy-spencer) +- [#70](https://github.com/oauth2-proxy/oauth2-proxy/pull/70) Fix handling of splitted cookies (@einfachchr) +- [#92](https://github.com/oauth2-proxy/oauth2-proxy/pull/92) Merge websocket proxy feature from openshift/oauth-proxy (@butzist) +- [#57](https://github.com/oauth2-proxy/oauth2-proxy/pull/57) Fall back to using OIDC Subject instead of Email (@aigarius) +- [#85](https://github.com/oauth2-proxy/oauth2-proxy/pull/85) Use non-root user in docker images (@kskewes) +- [#68](https://github.com/oauth2-proxy/oauth2-proxy/pull/68) forward X-Auth-Access-Token header (@davidholsgrove) +- [#41](https://github.com/oauth2-proxy/oauth2-proxy/pull/41) Added option to manually specify OIDC endpoints instead of relying on discovery +- [#83](https://github.com/oauth2-proxy/oauth2-proxy/pull/83) Add `id_token` refresh to Google provider (@leki75) +- [#10](https://github.com/oauth2-proxy/oauth2-proxy/pull/10) fix redirect url param handling (@dt-rush) +- [#122](https://github.com/oauth2-proxy/oauth2-proxy/pull/122) Expose -cookie-path as configuration parameter (@costelmoraru) +- [#124](https://github.com/oauth2-proxy/oauth2-proxy/pull/124) Use Go 1.12 for testing and build environments (@syscll) # v3.1.0 @@ -249,27 +362,27 @@ instead of returning an error - Streamed responses will now be flushed every 1 second by default. Previously streamed responses were flushed only when the buffer was full. To retain the old behaviour set `--flush-interval=0`. - See [#23](https://github.com/pusher/oauth2_proxy/pull/23) for further details. + See [#23](https://github.com/oauth2-proxy/oauth2-proxy/pull/23) for further details. ## Changes since v3.0.0 -- [#14](https://github.com/pusher/oauth2_proxy/pull/14) OIDC ID Token, Authorization Headers, Refreshing and Verification (@joelspeed) +- [#14](https://github.com/oauth2-proxy/oauth2-proxy/pull/14) OIDC ID Token, Authorization Headers, Refreshing and Verification (@joelspeed) - Implement `pass-authorization-header` and `set-authorization-header` flags - Implement token refreshing in OIDC provider - Split cookies larger than 4k limit into multiple cookies - Implement token validation in OIDC provider -- [#15](https://github.com/pusher/oauth2_proxy/pull/15) WhitelistDomains (@joelspeed) +- [#15](https://github.com/oauth2-proxy/oauth2-proxy/pull/15) WhitelistDomains (@joelspeed) - Add `--whitelist-domain` flag to allow redirection to approved domains after OAuth flow -- [#21](https://github.com/pusher/oauth2_proxy/pull/21) Docker Improvement (@yaegashi) +- [#21](https://github.com/oauth2-proxy/oauth2-proxy/pull/21) Docker Improvement (@yaegashi) - Move Docker base image from debian to alpine - Install ca-certificates in docker image -- [#23](https://github.com/pusher/oauth2_proxy/pull/23) Flushed streaming responses +- [#23](https://github.com/oauth2-proxy/oauth2-proxy/pull/23) Flushed streaming responses - Long-running upstream responses will get flushed every <timeperiod> (1 second by default) -- [#24](https://github.com/pusher/oauth2_proxy/pull/24) Redirect fix (@agentgonzo) +- [#24](https://github.com/oauth2-proxy/oauth2-proxy/pull/24) Redirect fix (@agentgonzo) - After a successful login, you will be redirected to your original URL rather than / -- [#35](https://github.com/pusher/oauth2_proxy/pull/35) arm and arm64 binary releases (@kskewes) +- [#35](https://github.com/oauth2-proxy/oauth2-proxy/pull/35) arm and arm64 binary releases (@kskewes) - Add armv6 and arm64 to Makefile `release` target -- [#37](https://github.com/pusher/oauth2_proxy/pull/37) cross build arm and arm64 docker images (@kskewes) +- [#37](https://github.com/oauth2-proxy/oauth2-proxy/pull/37) cross build arm and arm64 docker images (@kskewes) # v3.0.0 @@ -279,7 +392,7 @@ v2.2 as released by Bitly. ## Changes since v2.2: -- [#7](https://github.com/pusher/oauth2_proxy/pull/7) Migration to Pusher (@joelspeed) +- [#7](https://github.com/oauth2-proxy/oauth2-proxy/pull/7) Migration to Pusher (@joelspeed) - Move automated build to debian base image - Add Makefile - Update CI to run `make test` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14828c66e1def4361a4328d7cda99fa1c86f9bc3..ae051a18b4fcd69306a78db927b85a76aeb4eaf2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,9 +7,8 @@ Download the dependencies using `go mod download`. ```bash cd $GOPATH/src/github.com # Create this directory if it doesn't exist -git clone git@github.com:<YOUR_FORK>/oauth2_proxy pusher/oauth2_proxy -cd pusher/oauth2_proxy -./configure # Setup your environment variables +git clone git@github.com:<YOUR_FORK>/oauth2-proxy oauth2-proxy/oauth2-proxy +cd oauth2-proxy/oauth2-proxy go mod download ``` diff --git a/Dockerfile b/Dockerfile index 2a29bf0bf9cd8b87d759e907f6113de67cba0e21..dd286a7f6f8d9fa0f44636ba70ea0aba4cd741c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -FROM golang:1.13-buster AS builder +FROM golang:1.14-buster 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 curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 # Copy sources -WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy +WORKDIR $GOPATH/src/github.com/oauth2-proxy/oauth2-proxy # Fetch dependencies COPY go.mod go.sum ./ @@ -19,14 +19,15 @@ COPY . . # build the key into the container and then tell it where it is # by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem # in app.yaml instead. -RUN ./configure && make build && touch jwt_signing_key.pem +RUN make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM alpine:3.10 +FROM alpine:3.11 +COPY nsswitch.conf /etc/nsswitch.conf 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 +COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy +COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem USER 2000:2000 -ENTRYPOINT ["/bin/oauth2_proxy"] +ENTRYPOINT ["/bin/oauth2-proxy"] diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index df4fc825bf8d2ffa3e8fb48554f1aec4e8768461..6b494b7fa92d6b916a3449bcd07a78e1aa343894 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -1,10 +1,10 @@ -FROM golang:1.13-buster AS builder +FROM golang:1.14-buster 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 curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 # Copy sources -WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy +WORKDIR $GOPATH/src/github.com/oauth2-proxy/oauth2-proxy # Fetch dependencies COPY go.mod go.sum ./ @@ -19,14 +19,15 @@ COPY . . # build the key into the container and then tell it where it is # by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem # in app.yaml instead. -RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem +RUN GOARCH=arm64 make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM arm64v8/alpine:3.10 +FROM arm64v8/alpine:3.11 +COPY nsswitch.conf /etc/nsswitch.conf 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 +COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy +COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem USER 2000:2000 -ENTRYPOINT ["/bin/oauth2_proxy"] +ENTRYPOINT ["/bin/oauth2-proxy"] diff --git a/Dockerfile.armv6 b/Dockerfile.armv6 index 908d2d95d46ff79852e885820b9054a9cbee6e7f..1f8680c277430ad10104cf2e3c9fcaabb5866bce 100644 --- a/Dockerfile.armv6 +++ b/Dockerfile.armv6 @@ -1,10 +1,10 @@ -FROM golang:1.13-buster AS builder +FROM golang:1.14-buster 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 curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 # Copy sources -WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy +WORKDIR $GOPATH/src/github.com/oauth2-proxy/oauth2-proxy # Fetch dependencies COPY go.mod go.sum ./ @@ -19,14 +19,15 @@ COPY . . # build the key into the container and then tell it where it is # by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem # in app.yaml instead. -RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem +RUN GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM arm32v6/alpine:3.10 +FROM arm32v6/alpine:3.11 +COPY nsswitch.conf /etc/nsswitch.conf 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 +COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy +COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem USER 2000:2000 -ENTRYPOINT ["/bin/oauth2_proxy"] +ENTRYPOINT ["/bin/oauth2-proxy"] diff --git a/MAINTAINERS b/MAINTAINERS index 25fb47507b7c17b8bb19eb309245accc2d335b3d..1642e7412f910fc764c98a2c3b07911633d27d21 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,3 +1,2 @@ Joel Speed <joel.speed@hotmail.co.uk> (@JoelSpeed) -Dan Bond (@syscll) Henry Jenkins <henry@henryjenkins.name> (@steakunderscore) diff --git a/Makefile b/Makefile index f0f83b447917ddd5de90b22e18da9a0a9e047390..ba58b354b53ee43285fa7d88543b1d131392f0d9 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,22 @@ -include .env -BINARY := oauth2_proxy +GO ?= go +GOLANGCILINT ?= golangci-lint + +BINARY := oauth2-proxy VERSION := $(shell git describe --always --dirty --tags 2>/dev/null || echo "undefined") # Allow to override image registry. -REGISTRY ?= quay.io/pusher +REGISTRY ?= quay.io/oauth2-proxy .NOTPARALLEL: +GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1) +GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2) +MINIMUM_SUPPORTED_GO_MAJOR_VERSION = 1 +MINIMUM_SUPPORTED_GO_MINOR_VERSION = 14 +GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION).$(MINIMUM_SUPPORTED_GO_MINOR_VERSION) + +ifeq ($(COVER),true) +TESTCOVER ?= -coverprofile c.out +endif + .PHONY: all all: lint $(BINARY) @@ -18,47 +30,69 @@ distclean: clean rm -rf vendor .PHONY: lint -lint: +lint: validate-go-version GO111MODULE=on $(GOLANGCILINT) run .PHONY: build -build: clean $(BINARY) +build: validate-go-version clean $(BINARY) $(BINARY): - GO111MODULE=on CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/pusher/oauth2_proxy + GO111MODULE=on CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/oauth2-proxy/oauth2-proxy .PHONY: docker docker: - docker build -f Dockerfile -t $(REGISTRY)/oauth2_proxy:latest . + docker build -f Dockerfile -t $(REGISTRY)/oauth2-proxy:latest . .PHONY: docker-all docker-all: docker - docker build -f Dockerfile -t $(REGISTRY)/oauth2_proxy:latest-amd64 . - docker build -f Dockerfile -t $(REGISTRY)/oauth2_proxy:${VERSION} . - docker build -f Dockerfile -t $(REGISTRY)/oauth2_proxy:${VERSION}-amd64 . - docker build -f Dockerfile.arm64 -t $(REGISTRY)/oauth2_proxy:latest-arm64 . - docker build -f Dockerfile.arm64 -t $(REGISTRY)/oauth2_proxy:${VERSION}-arm64 . - docker build -f Dockerfile.armv6 -t $(REGISTRY)/oauth2_proxy:latest-armv6 . - docker build -f Dockerfile.armv6 -t $(REGISTRY)/oauth2_proxy:${VERSION}-armv6 . + docker build -f Dockerfile -t $(REGISTRY)/oauth2-proxy:latest-amd64 . + docker build -f Dockerfile -t $(REGISTRY)/oauth2-proxy:${VERSION} . + docker build -f Dockerfile -t $(REGISTRY)/oauth2-proxy:${VERSION}-amd64 . + docker build -f Dockerfile.arm64 -t $(REGISTRY)/oauth2-proxy:latest-arm64 . + docker build -f Dockerfile.arm64 -t $(REGISTRY)/oauth2-proxy:${VERSION}-arm64 . + docker build -f Dockerfile.armv6 -t $(REGISTRY)/oauth2-proxy:latest-armv6 . + docker build -f Dockerfile.armv6 -t $(REGISTRY)/oauth2-proxy:${VERSION}-armv6 . .PHONY: docker-push docker-push: - docker push $(REGISTRY)/oauth2_proxy:latest + docker push $(REGISTRY)/oauth2-proxy:latest .PHONY: docker-push-all docker-push-all: docker-push - docker push $(REGISTRY)/oauth2_proxy:latest-amd64 - docker push $(REGISTRY)/oauth2_proxy:${VERSION} - docker push $(REGISTRY)/oauth2_proxy:${VERSION}-amd64 - docker push $(REGISTRY)/oauth2_proxy:latest-arm64 - docker push $(REGISTRY)/oauth2_proxy:${VERSION}-arm64 - docker push $(REGISTRY)/oauth2_proxy:latest-armv6 - docker push $(REGISTRY)/oauth2_proxy:${VERSION}-armv6 + docker push $(REGISTRY)/oauth2-proxy:latest-amd64 + docker push $(REGISTRY)/oauth2-proxy:${VERSION} + docker push $(REGISTRY)/oauth2-proxy:${VERSION}-amd64 + docker push $(REGISTRY)/oauth2-proxy:latest-arm64 + docker push $(REGISTRY)/oauth2-proxy:${VERSION}-arm64 + docker push $(REGISTRY)/oauth2-proxy:latest-armv6 + docker push $(REGISTRY)/oauth2-proxy:${VERSION}-armv6 .PHONY: test test: lint - GO111MODULE=on $(GO) test -v -race ./... + GO111MODULE=on $(GO) test $(TESTCOVER) -v -race ./... .PHONY: release release: lint test BINARY=${BINARY} VERSION=${VERSION} ./dist.sh + +.PHONY: validate-go-version +validate-go-version: + @if [ $(GO_MAJOR_VERSION) -gt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \ + exit 0 ;\ + elif [ $(GO_MAJOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \ + echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\ + exit 1; \ + elif [ $(GO_MINOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MINOR_VERSION) ] ; then \ + echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\ + exit 1; \ + fi + +# local-env can be used to interact with the local development environment +# eg: +# make local-env-up # Bring up a basic test environment +# make local-env-down # Tear down the basic test environment +# make local-env-nginx-up # Bring up an nginx based test environment +# make local-env-nginx-down # Tead down the nginx based test environment +.PHONY: local-env-% +local-env-%: + make -C contrib/local-environment $* diff --git a/README.md b/README.md index c7ae9e60f740759c1d7c88ac854670f43516ae54..d912fa7e6564704809870655f32cb1c50038f6da 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@  -[](http://travis-ci.org/pusher/oauth2_proxy) -[](https://goreportcard.com/report/github.com/pusher/oauth2_proxy) -[](https://godoc.org/github.com/pusher/oauth2_proxy) +[](http://travis-ci.org/oauth2-proxy/oauth2-proxy) +[](https://goreportcard.com/report/github.com/oauth2-proxy/oauth2-proxy) +[](https://godoc.org/github.com/oauth2-proxy/oauth2-proxy) [](./LICENSE) +[](https://codeclimate.com/github/oauth2-proxy/oauth2-proxy/maintainability) +[](https://codeclimate.com/github/oauth2-proxy/oauth2-proxy/test_coverage) A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others) to validate accounts by email, domain or group. @@ -12,37 +14,41 @@ to validate accounts by email, domain or group. Versions v3.0.0 and up are from this fork and will have diverged from any changes in the original fork. A list of changes can be seen in the [CHANGELOG](CHANGELOG.md). +**Note:** This project was formerly hosted as `pusher/oauth2_proxy` but has been renamed as of 29/03/2020 to `oauth2-proxy/oauth2-proxy`. +Going forward, all images shall be available at `quay.io/oauth2-proxy/oauth2-proxy` and binaries wiil been named `oauth2-proxy`. +  ## Installation 1. Choose how to deploy: - a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v5.0.0`) + a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v5.1.1`) - b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin` + b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy` which will put the binary in `$GOROOT/bin` - c. Using the prebuilt docker image [quay.io/pusher/oauth2_proxy](https://quay.io/pusher/oauth2_proxy) (AMD64, ARMv6 and ARM64 tags available) + c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available) Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. ``` sha256sum -c sha256sum.txt 2>&1 | grep OK -oauth2_proxy-4.0.0.linux-amd64: OK +oauth2-proxy-x.y.z.linux-amd64: OK ``` -2. [Select a Provider and Register an OAuth Application with a Provider](https://pusher.github.io/oauth2_proxy/auth-configuration) -3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](https://pusher.github.io/oauth2_proxy/configuration) -4. [Configure SSL or Deploy behind a SSL endpoint](https://pusher.github.io/oauth2_proxy/tls-configuration) (example provided for Nginx) +2. [Select a Provider and Register an OAuth Application with a Provider](https://oauth2-proxy.github.io/oauth2-proxy/auth-configuration) +3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](https://oauth2-proxy.github.io/oauth2-proxy/configuration) +4. [Configure SSL or Deploy behind a SSL endpoint](https://oauth2-proxy.github.io/oauth2-proxy/tls-configuration) (example provided for Nginx) ## Security -If you are running a version older than v5.0.0 we **strongly recommend you please update** to a current version. RE: [open redirect vulnverability](https://github.com/pusher/oauth2_proxy/security/advisories/GHSA-qqxw-m5fj-f7gv) +If you are running a version older than v5.1.0 we **strongly recommend you please update** to a current version. +See [open redirect vulnverability](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-j7px-6hwj-hpjg) for details. ## Docs -Read the docs on our [Docs site](https://pusher.github.io/oauth2_proxy). +Read the docs on our [Docs site](https://oauth2-proxy.github.io/oauth2-proxy).  diff --git a/RELEASE.md b/RELEASE.md index 1dc9b888c641b0a87bc4446b2707c5da82c3429b..fa5c03046a8efcab9e4b72d5dc3a08217ba8dd44 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,6 +1,6 @@ # Release -Here's how OAuth2_Proxy releases are created. +Here's how OAuth2 Proxy releases are created. ## Schedule @@ -44,4 +44,4 @@ Note this uses `v4.1.0` as an example release number. ``` Note: Ensure the docker tags don't include `-dirty`. This means you have uncommitted changes. -12. Verify everything looks good at [quay](https://quay.io/repository/pusher/oauth2_proxy?tag=latest&tab=tags) and [github](https://github.com/pusher/oauth2_proxy/releases) +12. Verify everything looks good at [quay](https://quay.io/repository/oauth2-proxy/oauth2-proxy?tag=latest&tab=tags) and [github](https://github.com/oauth2-proxy/oauth2-proxy/releases) diff --git a/configure b/configure deleted file mode 100755 index 10af15e6ef6b9fb30a2e641ed7d50367463d6237..0000000000000000000000000000000000000000 --- a/configure +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -NC='\033[0m' - -if [ -z "${BASH_VERSINFO}" ] || [ -z "${BASH_VERSINFO[0]}" ] || [ ${BASH_VERSINFO[0]} -lt 4 ]; then - echo "This script requires Bash version >= 4"; exit 1; -fi - -declare -A tools=() -declare -A desired=() - -for arg in "$@"; do - case ${arg%%=*} in - "--with-go") - desired[go]="${arg##*=}" - ;; - "--help") - printf "${GREEN}$0${NC}\n" - printf " available options:\n" - printf " --with-go=${BLUE}<path_to_go_binary>${NC}\n" - exit 0 - ;; - *) - echo "Unknown option: $arg" - exit 2 - ;; - esac -done - -vercomp () { - if [[ $1 == $2 ]] - then - return 0 - fi - local IFS=. - local i ver1=($1) ver2=($2) - # fill empty fields in ver1 with zeros - for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)) - do - ver1[i]=0 - done - for ((i=0; i<${#ver1[@]}; i++)) - do - if [[ -z ${ver2[i]} ]] - then - # fill empty fields in ver2 with zeros - ver2[i]=0 - fi - if ((10#${ver1[i]} > 10#${ver2[i]})) - then - return 1 - fi - if ((10#${ver1[i]} < 10#${ver2[i]})) - then - return 2 - fi - done - return 0 -} - -check_for() { - echo -n "Checking for $1... " - if ! [ -z "${desired[$1]}" ]; then - TOOL_PATH="${desired[$1]}" - else - TOOL_PATH=$(command -v $1) - fi - if ! [ -x "$TOOL_PATH" -a -f "$TOOL_PATH" ]; then - printf "${RED}not found${NC}\n" - cd - - exit 1 - else - printf "${GREEN}found${NC}\n" - tools[$1]=$TOOL_PATH - fi -} - -check_go_version() { - echo -n "Checking go version... " - GO_VERSION=$(${tools[go]} version | ${tools[awk]} '{where = match($0, /[0-9]\.[0-9]+\.[0-9]*/); if (where != 0) print substr($0, RSTART, RLENGTH)}') - vercomp $GO_VERSION 1.12 - case $? in - 0) ;& - 1) - printf "${GREEN}" - echo $GO_VERSION - printf "${NC}" - ;; - 2) - printf "${RED}" - echo "$GO_VERSION < 1.12" - exit 1 - ;; - esac - VERSION=$(${tools[go]} version | ${tools[awk]} '{print $3}') - tools["go_version"]="${VERSION}" -} - -check_docker_version() { - echo -n "Checking docker version... " - DOCKER_VERSION=$(${tools[docker]} version | ${tools[awk]}) -} - -check_go_env() { - echo -n "Checking \$GOPATH... " - GOPATH="$(go env GOPATH)" - if [ -z "$GOPATH" ]; then - printf "${RED}invalid${NC} - GOPATH not set\n" - exit 1 - fi - printf "${GREEN}valid${NC} - $GOPATH\n" -} - -cd ${0%/*} - -rm -fv .env - -check_for make -check_for awk -check_for go -check_go_version -check_go_env -check_for golangci-lint - -echo - -cat <<- EOF > .env - MAKE := "${tools[make]}" - GO := "${tools[go]}" - GO_VERSION := ${tools[go_version]} - GOLANGCILINT := "${tools[golangci-lint]}" -EOF - -echo "Environment configuration written to .env" - -cd - > /dev/null diff --git a/contrib/local-environment/Makefile b/contrib/local-environment/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..0cfeaa6630cfab0a84b03cb0b25384d3502a2ebb --- /dev/null +++ b/contrib/local-environment/Makefile @@ -0,0 +1,15 @@ +.PHONY: up +up: + docker-compose up -d + +.PHONY: % +%: + docker-compose $* + +.PHONY: nginx-up +nginx-up: + docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml up -d + +.PHONY: nginx-% +nginx-%: + docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml $* diff --git a/contrib/local-environment/dex.yaml b/contrib/local-environment/dex.yaml new file mode 100644 index 0000000000000000000000000000000000000000..40a8bed0fb90904301e9e646c8e60ae33dd68069 --- /dev/null +++ b/contrib/local-environment/dex.yaml @@ -0,0 +1,32 @@ +# This configuration is intended to be used with the docker-compose testing +# environment. +# This should configure Dex to run on port 4190 and provides a static login +issuer: http://dex.localhost:4190/dex +storage: + type: etcd + config: + endpoints: + - http://etcd:2379 + namespace: dex/ +web: + http: 0.0.0.0:4190 +oauth2: + skipApprovalScreen: true +expiry: + signingKeys: "4h" + idTokens: "1h" +staticClients: +- id: oauth2-proxy + redirectURIs: + # These redirect URIs point to the `--redirect-url` for OAuth2 proxy. + - 'http://localhost:4180/oauth2/callback' # For basic proxy example. + - 'http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback' # For nginx example. + name: 'OAuth2 Proxy' + secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK +enablePasswordDB: true +staticPasswords: +- email: "admin@example.com" + # bcrypt hash of the string "password" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/contrib/local-environment/docker-compose-nginx.yaml b/contrib/local-environment/docker-compose-nginx.yaml new file mode 100644 index 0000000000000000000000000000000000000000..af6c587b396f3754cb5c2bdb544714abfa800578 --- /dev/null +++ b/contrib/local-environment/docker-compose-nginx.yaml @@ -0,0 +1,43 @@ +# This docker-compose file can be used to bring up an example instance of oauth2-proxy +# for manual testing and exploration of features. +# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider, +# etcd for storage for Dex, nginx as a reverse proxy and other http services for upstreams +# +# This file is an extension of the main compose file and must be used with it +# docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml <command> +# Alternatively: +# make nginx-<command> (eg make nginx-up, make nginx-down) +# +# Access one of the following URLs to initiate a login flow: +# - http://oauth2-proxy.localhost +# - http://httpbin.oauth2-proxy.localhost +# +# The OAuth2 Proxy itself is hosted at http://oauth2-proxy.oauth2-proxy.localhost +# +# Note, the above URLs should work with Chrome, but you may need to add hosts +# entries for other browsers +# 127.0.0.1 oauth2-proxy.localhost +# 127.0.0.1 httpbin.oauth2-proxy.localhost +# 127.0.0.1 oauth2-proxy.oauth2-proxy.localhost +version: '3.0' +services: + oauth2-proxy: + ports: [] + hostname: oauth2-proxy + volumes: + - "./oauth2-proxy-nginx.cfg:/oauth2-proxy.cfg" + networks: + oauth2-proxy: {} + nginx: + container_name: nginx + image: nginx:1.18 + ports: + - 80:80/tcp + hostname: nginx + volumes: + - "./nginx.conf:/etc/nginx/conf.d/default.conf" + networks: + oauth2-proxy: {} + httpbin: {} +networks: + oauth2-proxy: {} diff --git a/contrib/local-environment/docker-compose.yaml b/contrib/local-environment/docker-compose.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6f57720a2c99b41d5a84f26a28987bd169949270 --- /dev/null +++ b/contrib/local-environment/docker-compose.yaml @@ -0,0 +1,64 @@ +# This docker-compose file can be used to bring up an example instance of oauth2-proxy +# for manual testing and exploration of features. +# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider, +# etcd for storage for Dex and HTTPBin as an example upstream. +# +# This can either be created using docker-compose +# docker-compose -f docker-compose.yaml <command> +# Or: +# make <command> (eg. make up, make down) +# +# Access http://localhost:4180 to initiate a login cycle +version: '3.0' +services: + oauth2-proxy: + container_name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v5.1.1 + command: --config /oauth2-proxy.cfg + ports: + - 4180:4180/tcp + hostname: oauth2-proxy + volumes: + - "./oauth2-proxy.cfg:/oauth2-proxy.cfg" + restart: unless-stopped + networks: + dex: {} + httpbin: {} + depends_on: + - dex + - httpbin + dex: + container_name: dex + image: quay.io/dexidp/dex:v2.23.0 + command: serve /dex.yaml + ports: + - 4190:4190/tcp + hostname: dex + volumes: + - "./dex.yaml:/dex.yaml" + restart: unless-stopped + networks: + dex: + aliases: + - dex.localhost + etcd: {} + depends_on: + - etcd + httpbin: + container_name: httpbin + image: kennethreitz/httpbin + networks: + httpbin: {} + etcd: + container_name: etcd + image: gcr.io/etcd-development/etcd:v3.4.7 + entrypoint: /usr/local/bin/etcd + command: + - --listen-client-urls=http://0.0.0.0:2379 + - --advertise-client-urls=http://etcd:2379 + networks: + etcd: {} +networks: + dex: {} + etcd: {} + httpbin: {} diff --git a/contrib/local-environment/nginx.conf b/contrib/local-environment/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..877c213ea4e229a27fe72157175079ffe101eb61 --- /dev/null +++ b/contrib/local-environment/nginx.conf @@ -0,0 +1,84 @@ +# Reverse proxy to oauth2-proxy +server { + listen 80; + server_name oauth2-proxy.oauth2-proxy.localhost; + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass http://oauth2-proxy:4180/; + } +} + +# Reverse proxy to httpbin +server { + listen 80; + server_name httpbin.oauth2-proxy.localhost; + + auth_request /internal-auth/oauth2/auth; + + # If the auth_request denies the request (401), redirect to the sign_in page + # and include the final rd URL back to the user's original request. + error_page 401 = http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri; + + # Alternatively send the request to `start` to skip the provider button + # error_page 401 = http://oauth2-proxy.oauth2-proxy.localhost/oauth2/start?rd=$scheme://$host$request_uri; + + location / { + proxy_pass http://httpbin/; + } + + # auth_request must be a URI so this allows an internal path to then proxy to + # the real auth_request path. + # The trailing /'s are required so that nginx strips the prefix before proxying. + location /internal-auth/ { + internal; # Ensure external users can't access this path + + # Make sure the OAuth2 Proxy knows where the original request came from. + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass http://oauth2-proxy:4180/; + } +} + +# Statically serve the nginx welcome +server { + listen 80; + server_name oauth2-proxy.localhost; + + location / { + auth_request /internal-auth/oauth2/auth; + + # If the auth_request denies the request (401), redirect to the sign_in page + # and include the final rd URL back to the user's original request. + error_page 401 = http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri; + + # Alternatively send the request to `start` to skip the provider button + # error_page 401 = http://oauth2-proxy.oauth2-proxy.localhost/oauth2/start?rd=$scheme://$host$request_uri; + + + root /usr/share/nginx/html; + index index.html index.htm; + } + + # redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + # auth_request must be a URI so this allows an internal path to then proxy to + # the real auth_request path. + # The trailing /'s are required so that nginx strips the prefix before proxying. + location /internal-auth/ { + internal; # Ensure external users can't access this path + + # Make sure the OAuth2 Proxy knows where the original request came from. + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass http://oauth2-proxy:4180/; + } +} diff --git a/contrib/local-environment/oauth2-proxy-nginx.cfg b/contrib/local-environment/oauth2-proxy-nginx.cfg new file mode 100644 index 0000000000000000000000000000000000000000..6ba5623aa71498c3bd7a7e9686611f762cc70016 --- /dev/null +++ b/contrib/local-environment/oauth2-proxy-nginx.cfg @@ -0,0 +1,12 @@ +http_address="0.0.0.0:4180" +cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" +provider="oidc" +email_domains="example.com" +oidc_issuer_url="http://dex.localhost:4190/dex" +client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" +client_id="oauth2-proxy" +cookie_secure="false" + +redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback" +cookie_domain=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains. +whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target. diff --git a/contrib/local-environment/oauth2-proxy.cfg b/contrib/local-environment/oauth2-proxy.cfg new file mode 100644 index 0000000000000000000000000000000000000000..7ee55d8ff09eb86dad6c1a56ea850cde93524d3e --- /dev/null +++ b/contrib/local-environment/oauth2-proxy.cfg @@ -0,0 +1,11 @@ +http_address="0.0.0.0:4180" +cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" +provider="oidc" +email_domains="example.com" +oidc_issuer_url="http://dex.localhost:4190/dex" +client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" +client_id="oauth2-proxy" +cookie_secure="false" + +redirect_url="http://localhost:4180/oauth2/callback" +upstreams="http://httpbin" diff --git a/contrib/oauth2_proxy.cfg.example b/contrib/oauth2-proxy.cfg.example similarity index 98% rename from contrib/oauth2_proxy.cfg.example rename to contrib/oauth2-proxy.cfg.example index 24dfb5a8ace025809d084ac94d72796170b2c61c..f521e0009ffd8d54fd795973f3887ab58ee1fa4b 100644 --- a/contrib/oauth2_proxy.cfg.example +++ b/contrib/oauth2-proxy.cfg.example @@ -1,5 +1,5 @@ ## OAuth2 Proxy Config File -## https://github.com/pusher/oauth2_proxy +## https://github.com/oauth2-proxy/oauth2-proxy ## <addr>:<port> to listen on for HTTP/HTTPS clients # http_address = "127.0.0.1:4180" diff --git a/contrib/oauth2_proxy.service.example b/contrib/oauth2-proxy.service.example similarity index 68% rename from contrib/oauth2_proxy.service.example rename to contrib/oauth2-proxy.service.example index a5bf1f0df553f0f34309135ebbbf5be6684d75fa..7bfc79b58af5b5c6482d748b2487d0a0eee0906a 100644 --- a/contrib/oauth2_proxy.service.example +++ b/contrib/oauth2-proxy.service.example @@ -1,10 +1,10 @@ -# Systemd service file for oauth2_proxy daemon +# Systemd service file for oauth2-proxy daemon # # Date: Feb 9, 2016 # Author: Srdjan Grubor <sgnn7@sgnn7.org> [Unit] -Description=oauth2_proxy daemon service +Description=oauth2-proxy daemon service After=syslog.target network.target [Service] @@ -12,7 +12,7 @@ After=syslog.target network.target User=www-data Group=www-data -ExecStart=/usr/local/bin/oauth2_proxy -config=/etc/oauth2_proxy.cfg +ExecStart=/usr/local/bin/oauth2-proxy -config=/etc/oauth2-proxy.cfg ExecReload=/bin/kill -HUP $MAINPID KillMode=process diff --git a/contrib/oauth2-proxy_autocomplete.sh b/contrib/oauth2-proxy_autocomplete.sh new file mode 100644 index 0000000000000000000000000000000000000000..d4eaa1b1e76c7575db02675f525bf5a8eedc1ecb --- /dev/null +++ b/contrib/oauth2-proxy_autocomplete.sh @@ -0,0 +1,34 @@ +# +# Autocompletion for oauth2-proxy +# +# To install this, copy/move this file to /etc/bash.completion.d/ +# or add a line to your ~/.bashrc | ~/.bash_profile that says ". /path/to/oauth2-proxy/contrib/oauth2-proxy_autocomplete.sh" +# + +_oauth2_proxy() { + _oauth2_proxy_commands=$(oauth2-proxy -h 2>&1 | sed -n '/^\s*-/s/ \+/ /gp' | awk '{print $1}' | tr '\n' ' ') + local cur prev + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + case "$prev" in + -@(config|tls-cert-file|tls-key-file|authenticated-emails-file|htpasswd-file|custom-templates-dir|logging-filename|jwt-key-file)) + _filedir + return 0 + ;; + -provider) + COMPREPLY=( $(compgen -W "google azure facebook github keycloak gitlab linkedin login.gov digitalocean" -- ${cur}) ) + return 0 + ;; + --real-client-ip-header) + COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) ) + return 0 + ;; + -@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url)) + return 0 + ;; + esac + COMPREPLY=( $(compgen -W "${_oauth2_proxy_commands}" -- ${cur}) ) + return 0; +} +complete -F _oauth2_proxy oauth2-proxy diff --git a/contrib/oauth2_proxy_autocomplete.sh b/contrib/oauth2_proxy_autocomplete.sh deleted file mode 100644 index 0d2f395de1312541add0ebc7a3c1efe9dcd4ede1..0000000000000000000000000000000000000000 --- a/contrib/oauth2_proxy_autocomplete.sh +++ /dev/null @@ -1,30 +0,0 @@ -# -# Autocompletion for oauth2_proxy -# -# To install this, copy/move this file to /etc/bash.completion.d/ -# or add a line to your ~/.bashrc | ~/.bash_profile that says ". /path/to/oauth2_proxy/contrib/oauth2_proxy_autocomplete.sh" -# - -_oauth2_proxy() { - _oauth2_proxy_commands=$(oauth2_proxy -h 2>&1 | sed -n '/^\s*-/s/ \+/ /gp' | awk '{print $1}' | tr '\n' ' ') - local cur prev - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - case "$prev" in - -@(config|tls-cert-file|tls-key-file|authenticated-emails-file|htpasswd-file|custom-templates-dir|logging-filename|jwt-key-file)) - _filedir - return 0 - ;; - -provider) - COMPREPLY=( $(compgen -W "google azure facebook github keycloak gitlab linkedin login.gov digitalocean" -- ${cur}) ) - return 0 - ;; - -@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|gitlab-group|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url)) - return 0 - ;; - esac - COMPREPLY=( $(compgen -W "${_oauth2_proxy_commands}" -- ${cur}) ) - return 0; -} -complete -F _oauth2_proxy oauth2_proxy diff --git a/dist.sh b/dist.sh index 37052125b87b103130b3e59ee2e89f634df91fb2..4d9c58441230cb67e9660b8d9b57026a79394ed9 100755 --- a/dist.sh +++ b/dist.sh @@ -7,10 +7,10 @@ if [[ -z ${BINARY} ]] || [[ -z ${VERSION} ]]; then exit 1 fi -# Check for Go version 1.13.* +# Check for Go version 1.14.* GO_VERSION=$(go version | awk '{print $3}') -if [[ ! "${GO_VERSION}" =~ ^go1.13.* ]]; then - echo "Go version must be >= go1.13" +if [[ ! "${GO_VERSION}" =~ ^go1.14.* ]]; then + echo "Go version must be >= go1.14" exit 1 fi @@ -28,10 +28,10 @@ for ARCH in "${ARCHS[@]}"; do # Create architecture specific binaries if [[ ${GO_ARCH} == "armv6" ]]; then GO111MODULE=on GOOS=${GO_OS} GOARCH=arm GOARM=6 CGO_ENABLED=0 go build -ldflags="-X main.VERSION=${VERSION}" \ - -o release/${BINARY}-${VERSION}.${ARCH}.${GO_VERSION}/${BINARY} github.com/pusher/oauth2_proxy + -o release/${BINARY}-${VERSION}.${ARCH}.${GO_VERSION}/${BINARY} github.com/oauth2-proxy/oauth2-proxy else GO111MODULE=on GOOS=${GO_OS} GOARCH=${GO_ARCH} CGO_ENABLED=0 go build -ldflags="-X main.VERSION=${VERSION}" \ - -o release/${BINARY}-${VERSION}.${ARCH}.${GO_VERSION}/${BINARY} github.com/pusher/oauth2_proxy + -o release/${BINARY}-${VERSION}.${ARCH}.${GO_VERSION}/${BINARY} github.com/oauth2-proxy/oauth2-proxy fi cd release diff --git a/docs/0_index.md b/docs/0_index.md index e0e3227d0a96e19dfb8c0c2dbbf928434656a67f..e724e0f886e713ade6b3216825e70ea0a50caab9 100644 --- a/docs/0_index.md +++ b/docs/0_index.md @@ -14,7 +14,7 @@ to validate accounts by email, domain or group. 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). -[](http://travis-ci.org/pusher/oauth2_proxy) +[](http://travis-ci.org/oauth2-proxy/oauth2-proxy)  diff --git a/docs/1_installation.md b/docs/1_installation.md index 8ed72b817574635b98ce9a769f50285aa2132fcb..fdb206aa6a457d3a451e27dfba90fb2c719afbd4 100644 --- a/docs/1_installation.md +++ b/docs/1_installation.md @@ -9,17 +9,17 @@ nav_order: 1 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/oauth2-proxy/oauth2-proxy/releases) (current release is `v5.1.1`) - b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin` + b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy` which will put the binary in `$GOROOT/bin` - c. Using the prebuilt docker image [quay.io/pusher/oauth2_proxy](https://quay.io/pusher/oauth2_proxy) (AMD64, ARMv6 and ARM64 tags available) + c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available) Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. ``` $ sha256sum -c sha256sum.txt 2>&1 | grep OK -oauth2_proxy-4.0.0.linux-amd64: OK +oauth2-proxy-x.y.z.linux-amd64: OK ``` 2. [Select a Provider and Register an OAuth Application with a Provider](auth-configuration) diff --git a/docs/2_auth.md b/docs/2_auth.md index d715c6cc4bcfa97604650d901c35c77181f6857c..851bde5eacea3ab47ec91256ea5f528c1ff73d86 100644 --- a/docs/2_auth.md +++ b/docs/2_auth.md @@ -7,7 +7,7 @@ nav_order: 2 ## OAuth Provider Configuration -You will need to register an OAuth application with a Provider (Google, GitHub or another provider), and configure it with Redirect URI(s) for the domain you intend to run `oauth2_proxy` on. +You will need to register an OAuth application with a Provider (Google, GitHub or another provider), and configure it with Redirect URI(s) for the domain you intend to run `oauth2-proxy` on. Valid providers are : @@ -18,12 +18,18 @@ Valid providers are : - [Keycloak](#keycloak-auth-provider) - [GitLab](#gitlab-auth-provider) - [LinkedIn](#linkedin-auth-provider) +- [Microsoft Azure AD](#microsoft-azure-ad-provider) +- [OpenID Connect](#openid-connect-provider) - [login.gov](#logingov-provider) - [Nextcloud](#nextcloud-provider) - [DigitalOcean](#digitalocean-auth-provider) +- [Bitbucket](#bitbucket-auth-provider) +- [Gitea](#gitea-auth-provider) The provider can be selected using the `provider` configuration value. +Please note that not all providers support all claims. The `preferred_username` claim is currently only supported by the OpenID Connect provider. + ### Google Auth Provider For Google, the registration steps are: @@ -62,8 +68,8 @@ https://www.googleapis.com/auth/admin.directory.user.readonly 7. Create or choose an existing administrative email address on the Gmail domain to assign to the `google-admin-email` flag. This email will be impersonated by this client to make calls to the Admin SDK. See the note on the link from step 5 for the reason why. 8. Create or choose an existing email group and set that email to the `google-group` flag. You can pass multiple instances of this flag with different groups and the user will be checked against all the provided groups. -9. Lock down the permissions on the json file downloaded from step 1 so only oauth2_proxy is able to read the file and set the path to the file in the `google-service-account-json` flag. -10. Restart oauth2_proxy. +9. Lock down the permissions on the json file downloaded from step 1 so only oauth2-proxy is able to read the file and set the path to the file in the `google-service-account-json` flag. +10. Restart oauth2-proxy. Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ). @@ -83,7 +89,7 @@ Note: The user is checked against the group members list on initial authenticati --client-secret=<value from step 6> ``` -Note: When using the Azure Auth provider with nginx and the cookie session store you may find the cookie is too large and doesn't get passed through correctly. Increasing the proxy_buffer_size in nginx or implementing the [redis session storage](configuration#redis-storage) should resolve this. +Note: When using the Azure Auth provider with nginx and the cookie session store you may find the cookie is too large and doesn't get passed through correctly. Increasing the proxy_buffer_size in nginx or implementing the [redis session storage](configuration/sessions#redis-storage) should resolve this. ### Facebook Auth Provider @@ -95,11 +101,24 @@ Note: When using the Azure Auth provider with nginx and the cookie session store 1. Create a new project: https://github.com/settings/developers 2. Under `Authorization callback URL` enter the correct url ie `https://internal.yourcompany.com/oauth2/callback` -The GitHub auth provider supports two additional parameters to restrict authentication to Organization or Team level access. Restricting by org and team is normally accompanied with `--email-domain=*` +The GitHub auth provider supports two additional ways to restrict authentication to either organization and optional team level access, or to collaborators of a repository. Restricting by these options is normally accompanied with `--email-domain=*` + +To restrict by organization only, include the following flag: -github-org="": restrict logins to members of this organisation + +To restrict within an organization to specific teams, include the following flag in addition to `-github-org`: + -github-team="": restrict logins to members of any of these teams (slug), separated by a comma +If you would rather restrict access to collaborators of a repository, those users must either have push access to a public repository or any access to a private repository: + + -github-repo="": restrict logins to collaborators of this repository formatted as orgname/repo + +If you'd like to allow access to users with **read only** access to a **public** repository you will need to provide a [token](https://github.com/settings/tokens) for a user that has write access to the repository. The token must be created with at least the `public_repo` scope: + + -github-token="": the token to use when verifying repository collaborators + If you are using GitHub enterprise, make sure you set the following to the appropriate url: -login-url="http(s)://<enterprise github host>/login/oauth/authorize" @@ -158,12 +177,12 @@ Take note of your `TenantId` if applicable for your situation. The `TenantId` ca OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects. This provider was originally built against CoreOS Dex and we will use it as an example. 1. Launch a Dex instance using the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md). -2. Setup oauth2_proxy with the correct provider and using the default ports and callbacks. -3. Login with the fixture use in the dex guide and run the oauth2_proxy with the following args: +2. Setup oauth2-proxy with the correct provider and using the default ports and callbacks. +3. Login with the fixture use in the dex guide and run the oauth2-proxy with the following args: -provider oidc -provider-display-name "My OIDC Provider" - -client-id oauth2_proxy + -client-id oauth2-proxy -client-secret proxy -redirect-url http://127.0.0.1:4180/oauth2/callback -oidc-issuer-url http://127.0.0.1:5556 @@ -198,7 +217,7 @@ you may wish to configure an authorization server for each application. Otherwis ``` provider = "oidc" -redirect_url = "https://example.corp.com" +redirect_url = "https://example.corp.com/oauth2/callback" oidc_issuer_url = "https://corp.okta.com/oauth2/abCd1234" upstreams = [ "https://example.corp.com" @@ -217,8 +236,41 @@ The `oidc_issuer_url` is based on URL from your **Authorization Server**'s **Iss 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` - +Then you can start the oauth2-proxy with `./oauth2-proxy -config /etc/example.cfg` + +#### Configuring the OIDC Provider with Okta - localhost +1. Signup for developer account: https://developer.okta.com/signup/ +2. Create New `Web` Application: https://${your-okta-domain}/dev/console/apps/new +3. Example Application Settings for localhost: + * **Name:** My Web App + * **Base URIs:** http://localhost:4180/ + * **Login redirect URIs:** http://localhost:4180/oauth2/callback + * **Logout redirect URIs:** http://localhost:4180/ + * **Group assignments:** `Everyone` + * **Grant type allowed:** `Authorization Code` and `Refresh Token` +4. Make note of the `Client ID` and `Client secret`, they are needed in a future step +5. Make note of the **default** Authorization Server Issuer URI from: https://${your-okta-domain}/admin/oauth2/as +6. Example config file `/etc/localhost.cfg` + ``` + provider = "oidc" + redirect_url = "http://localhost:4180/oauth2/callback" + oidc_issuer_url = "https://${your-okta-domain}/oauth2/default" + upstreams = [ + "http://0.0.0.0:8080" + ] + email_domains = [ + "*" + ] + client_id = "XXX" + client_secret = "YYY" + pass_access_token = true + cookie_secret = "ZZZ" + cookie_secure = false + skip_provider_button = true + # Note: use the following for testing within a container + # http_address = "0.0.0.0:4180" + ``` +7. Then you can start the oauth2-proxy with `./oauth2-proxy -config /etc/localhost.cfg` ### login.gov Provider @@ -246,7 +298,7 @@ First, register your application in the dashboard. The important bits are: Now start the proxy up with the following options: ``` -./oauth2_proxy -provider login.gov \ +./oauth2-proxy -provider login.gov \ -client-id=${LOGINGOV_ISSUER} \ -redirect-url=http://localhost:4180/oauth2/callback \ -oidc-issuer-url=https://idp.int.identitysandbox.gov/ \ @@ -267,7 +319,7 @@ If you encounter this, then you can create a `jwt_signing_key.pem` file in the t directory of the repo which contains the key in PEM format and then do your docker build. The docker build process will copy that file into your image which you can then access by setting the `OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem` -environment variable, or by setting `-jwt-key-file=/etc/ssl/private/jwt_signing_key.pem` on the commandline. +environment variable, or by setting `--jwt-key-file=/etc/ssl/private/jwt_signing_key.pem` on the commandline. Once it is running, you should be able to go to `http://localhost:4180/` in your browser, get authenticated by the login.gov integration server, and then get proxied on to your @@ -277,13 +329,13 @@ proxy, and you would use real hostnames everywhere. #### Skip OIDC discovery -Some providers do not support OIDC discovery via their issuer URL, so oauth2_proxy cannot simply grab the authorization, token and jwks URI endpoints from the provider's metadata. +Some providers do not support OIDC discovery via their issuer URL, so oauth2-proxy cannot simply grab the authorization, token and jwks URI endpoints from the provider's metadata. -In this case, you can set the `-skip-oidc-discovery` option, and supply those required endpoints manually: +In this case, you can set the `--skip-oidc-discovery` option, and supply those required endpoints manually: ``` -provider oidc - -client-id oauth2_proxy + -client-id oauth2-proxy -client-secret proxy -redirect-url http://127.0.0.1:4180/oauth2/callback -oidc-issuer-url http://127.0.0.1:5556 @@ -325,7 +377,7 @@ Note: in *all* cases the validate-url will *not* have the `index.php`. 1. [Create a new OAuth application](https://cloud.digitalocean.com/account/api/applications) * You can fill in the name, homepage, and description however you wish. - * In the "Application callback URL" field, enter: `https://oauth-proxy/oauth2/callback`, substituting `oauth2-proxy` with the actual hostname that oauth2_proxy is running on. The URL must match oauth2_proxy's configured redirect URL. + * In the "Application callback URL" field, enter: `https://oauth-proxy/oauth2/callback`, substituting `oauth2-proxy` with the actual hostname that oauth2-proxy is running on. The URL must match oauth2-proxy's configured redirect URL. 2. Note the Client ID and Client Secret. To use the provider, pass the following options: @@ -338,6 +390,46 @@ To use the provider, pass the following options: Alternatively, set the equivalent options in the config file. The redirect URL defaults to `https://<requested host header>/oauth2/callback`. If you need to change it, you can use the `--redirect-url` command-line option. +### Bitbucket Auth Provider + +1. [Add a new OAuth consumer](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html) + * In "Callback URL" use `https://<oauth2-proxy>/oauth2/callback`, substituting `<oauth2-proxy>` with the actual hostname that oauth2-proxy is running on. + * In Permissions section select: + * Account -> Email + * Team membership -> Read + * Repositories -> Read +2. Note the Client ID and Client Secret. + +To use the provider, pass the following options: + +``` + --provider=bitbucket + --client-id=<Client ID> + --client-secret=<Client Secret> +``` + +The default configuration allows everyone with Bitbucket account to authenticate. To restrict the access to the team members use additional configuration option: `--bitbucket-team=<Team name>`. To restrict the access to only these users who has access to one selected repository use `--bitbucket-repository=<Repository name>`. + + +### Gitea Auth Provider + +1. Create a new application: `https://< your gitea host >/user/settings/applications` +2. Under `Redirect URI` enter the correct URL i.e. `https://<proxied host>/oauth2/callback` +3. Note the Client ID and Client Secret. +4. Pass the following options to the proxy: + +``` + --provider="github" + --redirect-url="https://<proxied host>/oauth2/callback" + --provider-display-name="Gitea" + --client-id="< client_id as generated by Gitea >" + --client-secret="< client_secret as generated by Gitea >" + --login-url="https://< your gitea host >/login/oauth/authorize" + --redeem-url="https://< your gitea host >/login/oauth/access_token" + --validate-url="https://< your gitea host >/api/v1" +``` + + ## Email Authentication To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`. @@ -346,5 +438,5 @@ To authorize by email domain use `--email-domain=yourcompany.com`. To authorize Follow the examples in the [`providers` package]({{ site.gitweb }}/providers/) to define a new `Provider` instance. Add a new `case` to -[`providers.New()`]({{ site.gitweb }}/providers/providers.go) to allow `oauth2_proxy` to use the +[`providers.New()`]({{ site.gitweb }}/providers/providers.go) to allow `oauth2-proxy` to use the new `Provider`. diff --git a/docs/4_tls.md b/docs/4_tls.md index c6d40d746e9bfb98656ff8369076c2fc53c95c06..fcbdc780c19d252bc0c2ba8e2ea0bdcdf643c36c 100644 --- a/docs/4_tls.md +++ b/docs/4_tls.md @@ -11,10 +11,10 @@ There are two recommended configurations. 1. Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert-file=/path/to/cert.pem` and `--tls-key-file=/path/to/cert.key`. - The command line to run `oauth2_proxy` in this configuration would look like this: + The command line to run `oauth2-proxy` in this configuration would look like this: ```bash - ./oauth2_proxy \ + ./oauth2-proxy \ --email-domain="yourcompany.com" \ --upstream=http://127.0.0.1:8080/ \ --tls-cert-file=/path/to/cert.pem \ @@ -28,12 +28,12 @@ There are two recommended configurations. 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 + 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 + 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 @@ -59,10 +59,10 @@ There are two recommended configurations. } ``` - 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 \ + ./oauth2-proxy \ --email-domain="yourcompany.com" \ --upstream=http://127.0.0.1:8080/ \ --cookie-secret=... \ diff --git a/docs/5_endpoints.md b/docs/5_endpoints.md index f4132d98da4651cd58c768e55291cdcfa970343d..3f9761f3f5a86397f1a46bee20b06bbe7973db1c 100644 --- a/docs/5_endpoints.md +++ b/docs/5_endpoints.md @@ -17,3 +17,23 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w - /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url. - /oauth2/userinfo - the URL is used to return user's email from the session in JSON format. - /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request) + +### Sign out + +To sign the user out, redirect them to `/oauth2/sign_out`. This endpoint only removes oauth2-proxy's own cookies, i.e. the user is still logged in with the authentication provider and may automatically re-login when accessing the application again. You will also need to redirect the user to the authentication provider's sign out page afterwards using the `rd` query parameter, i.e. redirect the user to something like (notice the url-encoding!): + +``` +/oauth2/sign_out?rd=https%3A%2F%2Fmy-oidc-provider.example.com%2Fsign_out_page +``` + +Alternatively, include the redirect URL in the `X-Auth-Request-Redirect` header: + +``` +GET /oauth2/sign_out HTTP/1.1 +X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page +... +``` + +(The "sign_out_page" should be the [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0.html#rfc.section.2.1) from [the metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) if your OIDC provider supports Session Management and Discovery.) + +BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](configuration) configuration option otherwise the redirect will be ignored. diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 781ebd405cc261c24946e2176ae7549ac9a2fdc4..3d711df0970e82e0d26d6bcdde947313b1327e58 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,13 +1,14 @@ GEM remote: https://rubygems.org/ specs: - activesupport (4.2.10) - i18n (~> 0.7) + activesupport (6.0.2.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) + zeitwerk (~> 2.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) coffee-script (2.4.1) coffee-script-source execjs @@ -15,8 +16,8 @@ GEM colorator (1.1.0) commonmarker (0.17.13) ruby-enum (~> 0.5) - concurrent-ruby (1.1.4) - dnsruby (1.61.2) + concurrent-ruby (1.1.6) + dnsruby (1.61.3) addressable (~> 2.5) em-websocket (0.5.1) eventmachine (>= 0.12.9) @@ -25,33 +26,32 @@ GEM ffi (>= 1.3.0) eventmachine (1.2.7) execjs (2.7.0) - faraday (0.15.4) + faraday (1.0.0) multipart-post (>= 1.2, < 3) - ffi (1.10.0) + ffi (1.12.2) forwardable-extended (2.6.0) - gemoji (3.0.0) - github-pages (193) - activesupport (= 4.2.10) - github-pages-health-check (= 1.8.1) - jekyll (= 3.7.4) - jekyll-avatar (= 0.6.0) + gemoji (3.0.1) + github-pages (204) + github-pages-health-check (= 1.16.1) + jekyll (= 3.8.5) + jekyll-avatar (= 0.7.0) jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.1.5) + jekyll-commonmark-ghpages (= 0.1.6) jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.11.0) + jekyll-feed (= 0.13.0) jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.9.4) - jekyll-mentions (= 1.4.1) - jekyll-optional-front-matter (= 0.3.0) + jekyll-github-metadata (= 2.13.0) + jekyll-mentions (= 1.5.1) + jekyll-optional-front-matter (= 0.3.2) jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.2.0) - jekyll-redirect-from (= 0.14.0) - jekyll-relative-links (= 0.5.3) - jekyll-remote-theme (= 0.3.1) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.15.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.1) jekyll-sass-converter (= 1.5.2) - jekyll-seo-tag (= 2.5.0) - jekyll-sitemap (= 1.2.0) - jekyll-swiss (= 0.4.0) + jekyll-seo-tag (= 2.6.1) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) jekyll-theme-architect (= 0.1.1) jekyll-theme-cayman (= 0.1.1) jekyll-theme-dinky (= 0.1.1) @@ -61,33 +61,32 @@ GEM jekyll-theme-midnight (= 0.1.1) jekyll-theme-minimal (= 0.1.1) jekyll-theme-modernist (= 0.1.1) - jekyll-theme-primer (= 0.5.3) + jekyll-theme-primer (= 0.5.4) jekyll-theme-slate (= 0.1.1) jekyll-theme-tactile (= 0.1.1) jekyll-theme-time-machine (= 0.1.1) - jekyll-titles-from-headings (= 0.5.1) - jemoji (= 0.10.1) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.11.1) kramdown (= 1.17.0) - liquid (= 4.0.0) - listen (= 3.1.5) + liquid (= 4.0.3) mercenary (~> 0.3) - minima (= 2.5.0) - nokogiri (>= 1.8.2, < 2.0) - rouge (= 2.2.1) + minima (= 2.5.1) + nokogiri (>= 1.10.4, < 2.0) + rouge (= 3.13.0) terminal-table (~> 1.4) - github-pages-health-check (1.8.1) + github-pages-health-check (1.16.1) addressable (~> 2.3) dnsruby (~> 1.60) octokit (~> 4.0) - public_suffix (~> 2.0) + public_suffix (~> 3.0) typhoeus (~> 1.3) - html-pipeline (2.10.0) + html-pipeline (2.12.3) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.6.0) i18n (0.9.5) concurrent-ruby (~> 1.0) - jekyll (3.7.4) + jekyll (3.8.5) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) @@ -100,49 +99,50 @@ GEM pathutil (~> 0.9) rouge (>= 1.7, < 4) safe_yaml (~> 1.0) - jekyll-avatar (0.6.0) - jekyll (~> 3.0) + jekyll-avatar (0.7.0) + jekyll (>= 3.0, < 5.0) jekyll-coffeescript (1.1.1) coffee-script (~> 2.2) coffee-script-source (~> 1.11.1) - jekyll-commonmark (1.2.0) + jekyll-commonmark (1.3.1) commonmarker (~> 0.14) - jekyll (>= 3.0, < 4.0) - jekyll-commonmark-ghpages (0.1.5) + jekyll (>= 3.7, < 5.0) + jekyll-commonmark-ghpages (0.1.6) commonmarker (~> 0.17.6) - jekyll-commonmark (~> 1) - rouge (~> 2) + jekyll-commonmark (~> 1.2) + rouge (>= 2.0, < 4.0) jekyll-default-layout (0.1.4) jekyll (~> 3.0) - jekyll-feed (0.11.0) - jekyll (~> 3.3) + jekyll-feed (0.13.0) + jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) - jekyll-github-metadata (2.9.4) - jekyll (~> 3.1) + jekyll-github-metadata (2.13.0) + jekyll (>= 3.4, < 5.0) octokit (~> 4.0, != 4.4.0) - jekyll-mentions (1.4.1) + jekyll-mentions (1.5.1) html-pipeline (~> 2.3) - jekyll (~> 3.0) - jekyll-optional-front-matter (0.3.0) - jekyll (~> 3.0) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) jekyll-paginate (1.1.0) - jekyll-readme-index (0.2.0) - jekyll (~> 3.0) - jekyll-redirect-from (0.14.0) - jekyll (~> 3.3) - jekyll-relative-links (0.5.3) - jekyll (~> 3.3) - jekyll-remote-theme (0.3.1) - jekyll (~> 3.5) - rubyzip (>= 1.2.1, < 3.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.15.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.1) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + rubyzip (>= 1.3.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) - jekyll-seo-tag (2.5.0) - jekyll (~> 3.3) - jekyll-sitemap (1.2.0) - jekyll (~> 3.3) - jekyll-swiss (0.4.0) + jekyll-seo-tag (2.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) jekyll-theme-architect (0.1.1) jekyll (~> 3.5) jekyll-seo-tag (~> 2.0) @@ -170,8 +170,8 @@ GEM jekyll-theme-modernist (0.1.1) jekyll (~> 3.5) jekyll-seo-tag (~> 2.0) - jekyll-theme-primer (0.5.3) - jekyll (~> 3.5) + jekyll-theme-primer (0.5.4) + jekyll (> 3.5, < 5.0) jekyll-github-metadata (~> 2.9) jekyll-seo-tag (~> 2.0) jekyll-theme-slate (0.1.1) @@ -183,64 +183,65 @@ GEM jekyll-theme-time-machine (0.1.1) jekyll (~> 3.5) jekyll-seo-tag (~> 2.0) - jekyll-titles-from-headings (0.5.1) - jekyll (~> 3.3) - jekyll-watch (2.1.2) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.10.1) + jemoji (0.11.1) gemoji (~> 3.0) html-pipeline (~> 2.2) - jekyll (~> 3.0) - just-the-docs (0.1.6) - jekyll (~> 3.3) - rake (~> 10.0) + jekyll (>= 3.0, < 5.0) + just-the-docs (0.2.7) + jekyll (~> 3.8.5) + jekyll-seo-tag (~> 2.0) + rake (~> 12.3.1) kramdown (1.17.0) - liquid (4.0.0) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) + liquid (4.0.3) + listen (3.2.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) mini_portile2 (2.4.0) - minima (2.5.0) - jekyll (~> 3.5) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.11.3) - multipart-post (2.0.0) - nokogiri (1.10.4) + minitest (5.14.0) + multipart-post (2.1.1) + nokogiri (1.10.9) mini_portile2 (~> 2.4.0) - octokit (4.13.0) + octokit (4.16.0) + faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (2.0.5) - rake (10.5.0) + public_suffix (3.1.1) + rake (12.3.3) rb-fsevent (0.10.3) - rb-inotify (0.10.0) + rb-inotify (0.10.1) ffi (~> 1.0) - rouge (2.2.1) + rouge (3.13.0) ruby-enum (0.7.2) i18n - ruby_dep (1.5.0) - rubyzip (2.0.0) - safe_yaml (1.0.4) - sass (3.7.3) + rubyzip (2.2.0) + safe_yaml (1.0.5) + sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.8.1) - addressable (>= 2.3.5, < 2.6) - faraday (~> 0.8, < 1.0) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) thread_safe (0.3.6) typhoeus (1.3.1) ethon (>= 0.9.0) - tzinfo (1.2.5) + tzinfo (1.2.6) thread_safe (~> 0.1) - unicode-display_width (1.4.1) + unicode-display_width (1.6.1) + zeitwerk (2.2.2) PLATFORMS ruby @@ -251,4 +252,4 @@ DEPENDENCIES tzinfo-data BUNDLED WITH - 2.0.1 + 2.1.2 diff --git a/docs/README.md b/docs/README.md index ad5ef46b68a1dc57c31234811d14d56b8700b960..cb133d3fe3dadc364a8ebcec7f986df3fac233f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ # Docs This folder contains our Jekyll based docs site which is hosted at -https://pusher.github.io/oauth2_proxy. +https://oauth2-proxy.github.io/oauth2-proxy. When making changes to this docs site, please test your changes locally: diff --git a/docs/_config.yml b/docs/_config.yml index a53e9e14e58d13085d3d388ea3b48d5f7d3e0b3e..ea063cc275df67e120003a543a11eb524c4016b0 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -13,13 +13,13 @@ # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. # You can create any custom variable you would like, and they will be accessible # in the templates via {{ site.myvariable }}. -title: OAuth2_Proxy +title: OAuth2 Proxy logo: /logos/OAuth2_Proxy_horizontal.svg 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" + OAuth2-Proxy documentation site +baseurl: "/oauth2-proxy" # the subpath of your site, e.g. /blog +url: "https://oauth2-proxy.github.io" # the base hostname & protocol for your site, e.g. http://example.com +gitweb: "https://github.com/oauth2-proxy/oauth2-proxy/blob/master" # Build settings markdown: kramdown @@ -28,8 +28,8 @@ search_enabled: true # Aux links for the upper right navigation aux_links: - "OAuth2_Proxy on GitHub": - - "https://github.com/pusher/oauth2_proxy" + "OAuth2 Proxy on GitHub": + - "https://github.com/oauth2-proxy/oauth2-proxy" # Exclude from processing. # The following items will not be processed, by default. Create a custom list diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index aa9164ce2dea17afd956edfcd9f58f148f1c117f..69b3cfa018a54bc69cede1a3b2033d5aa95e5113 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -8,114 +8,124 @@ nav_order: 3 ## Configuration -`oauth2_proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables). +`oauth2-proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables). -To generate a strong cookie secret use `python -c 'import os,base64; print base64.urlsafe_b64encode(os.urandom(16))'` +To generate a strong cookie secret use `python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(16)).decode())'` ### 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` +Every command line argument can be specified in a config file by replacing hypens (-) with underscores (\_). If the argument can be specified multiple times, the config option should be plural (trailing s). + +An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `--config=/etc/oauth2-proxy.cfg` ### Command Line Options | Option | Type | Description | Default | | ------ | ---- | ----------- | ------- | -| `-acr-values` | string | optional, used by login.gov | `"http://idmanagement.gov/ns/assurance/loa/1"` | -| `-approval-prompt` | string | OAuth approval_prompt | `"force"` | -| `-auth-logging` | bool | Log authentication attempts | true | -| `-auth-logging-format` | string | Template for authentication log lines | see [Logging Configuration](#logging-configuration) | -| `-authenticated-emails-file` | string | authenticate against emails via file (one per line) | | -| `-azure-tenant string` | string | go to a tenant-specific or common (tenant-independent) endpoint. | `"common"` | -| `-basic-auth-password` | string | the password to set when passing the HTTP Basic Auth header | | -| `-client-id` | string | the OAuth Client ID: ie: `"123456.apps.googleusercontent.com"` | | -| `-client-secret` | string | the OAuth Client Secret | | -| `-client-secret-file` | string | the file with 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 only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true | -| `-cookie-samesite` | string | set SameSite cookie attribute (ie: `"lax"`, `"strict"`, `"none"`, or `""`). | `""` | -| `-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"` | -| `-force-https` | bool | enforce https redirect | `false` | -| `-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 | -| `-provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) | -| `-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-cluster-connection-urls` | string \| list | List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with `--redis-use-cluster` | | -| `-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-cluster` | bool | Connect to redis cluster. Must set `--redis-cluster-connection-urls` to use this feature | false | -| `-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) | | -| `-reverse-proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted | false | -| `-scope` | string | OAuth scope specification | | -| `-session-store-type` | string | [Session data storage backend](configuration/sessions); redis or cookie | 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, file:// paths for static files or `static://<status_code>` for static response. 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`) | | +| `--acr-values` | string | optional, see [docs](https://openid.net/specs/openid-connect-eap-acr-values-1_0.html#acrValues) | `""` | +| `--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 | 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 | | +| `--client-secret-file` | string | the file with OAuth Client Secret | | +| `--config` | string | path to config file | | +| `--cookie-domain` | string \| list | Optional cookie domains to force cookies to (ie: `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match). | | +| `--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 only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true | +| `--cookie-samesite` | string | set SameSite cookie attribute (ie: `"lax"`, `"strict"`, `"none"`, or `""`). | `""` | +| `--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"` | +| `--force-https` | bool | enforce https redirect | `false` | +| `--banner` | string | custom (html) banner string. Use `"-"` to disable default banner. | | +| `--footer` | string | custom (html) 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 | | +| `--github-repo` | string | restrict logins to collaborators of this repository formatted as `orgname/repo` | | +| `--github-token` | string | the token to use when verifying repository collaborators (must have push access to the repository) | | +| `--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 | +| `--insecure-oidc-skip-issuer-verification` | bool | allow the OIDC issuer URL to differ from the expected (currently required for Azure multi-tenant compatibility) | 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, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true | +| `--prefer-email-to-user` | bool | Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, eg. htaccess authentication. Used in conjunction with `--pass-basic-auth` and `--pass-user-headers` | false | +| `--pass-host-header` | bool | pass the request Host Header to upstream | true | +| `--pass-user-headers` | bool | pass X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true | +| `--profile-url` | string | Profile access endpoint | | +| `--prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` | +| `--provider` | string | OAuth provider | google | +| `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) | +| `--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 | | +| `--real-client-ip-header` | string | Header used to determine the real IP of the client, requires `--reverse-proxy` to be set (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP) | X-Real-IP | +| `--redeem-url` | string | Token redemption endpoint | | +| `--redirect-url` | string | the OAuth Redirect URL. ie: `"https://internalapp.yourcompany.com/oauth2/callback"` | | +| `--redis-cluster-connection-urls` | string \| list | List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with `--redis-use-cluster` | | +| `--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-cluster` | bool | Connect to redis cluster. Must set `--redis-cluster-connection-urls` to use this feature | false | +| `--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) | | +| `--reverse-proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted | false | +| `--scope` | string | OAuth scope specification | | +| `--session-store-type` | string | [Session data storage backend](configuration/sessions); redis or cookie | cookie | +| `--set-xauthrequest` | bool | set X-Auth-Request-User, X-Auth-Request-Email and X-Auth-Request-Preferred-Username 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 | +| `--set-basic-auth` | bool | set HTTP Basic Auth information in response (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, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | | +| `--user-id-claim` | string | which claim contains the user ID | \["email"\] | +| `--validate-url` | string | Access token validation endpoint | | +| `--version` | n/a | print version string | | +| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | | Note: when using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URL's protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`. @@ -123,11 +133,11 @@ See below for provider specific options ### Upstreams Configuration -`oauth2_proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter, this will forward all authenticated requests to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream. +`oauth2-proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter, this will forward all authenticated requests to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream. -Static file paths are configured as a file:// URL. `file:///var/www/static/` will serve the files from that directory at `http://[oauth2_proxy url]/var/www/static/`, which may not be what you want. You can provide the path to where the files should be available by adding a fragment to the configured URL. The value of the fragment will then be used to specify which path the files are available at. `file:///var/www/static/#/static/` will ie. make `/var/www/static/` available at `http://[oauth2_proxy url]/static/`. +Static file paths are configured as a file:// URL. `file:///var/www/static/` will serve the files from that directory at `http://[oauth2-proxy url]/var/www/static/`, which may not be what you want. You can provide the path to where the files should be available by adding a fragment to the configured URL. The value of the fragment will then be used to specify which path the files are available at. `file:///var/www/static/#/static/` will ie. make `/var/www/static/` available at `http://[oauth2-proxy url]/static/`. -Multiple upstreams can either be configured by supplying a comma separated list to the `-upstream` parameter, supplying the parameter multiple times or provinding a list in the [config file](#config-file). When multiple upstreams are used routing to them will be based on the path they are set up with. +Multiple upstreams can either be configured by supplying a comma separated list to the `--upstream` parameter, supplying the parameter multiple times or provinding a list in the [config file](#config-file). When multiple upstreams are used routing to them will be based on the path they are set up with. ### Environment variables @@ -144,15 +154,15 @@ and the `--email-domain` flag becomes `OAUTH2_PROXY_EMAIL_DOMAINS`. ## Logging Configuration -By default, OAuth2 Proxy logs all output to stdout. Logging can be configured to output to a rotating log file using the `-logging-filename` command. +By default, OAuth2 Proxy logs all output to stdout. Logging can be configured to output to a rotating log file using the `--logging-filename` command. -If logging to a file you can also configure the maximum file size (`-logging-max-size`), age (`-logging-max-age`), max backup logs (`-logging-max-backups`), and if backup logs should be compressed (`-logging-compress`). +If logging to a file you can also configure the maximum file size (`--logging-max-size`), age (`--logging-max-age`), max backup logs (`--logging-max-backups`), and if backup logs should be compressed (`--logging-compress`). -There are three different types of logging: standard, authentication, and HTTP requests. These can each be enabled or disabled with `-standard-logging`, `-auth-logging`, and `-request-logging`. +There are three different types of logging: standard, authentication, and HTTP requests. These can each be enabled or disabled with `--standard-logging`, `--auth-logging`, and `--request-logging`. Each type of logging has their own configurable format and variables. By default these formats are similar to the Apache Combined Log. -Logging of requests to the `/ping` endpoint can be disabled with `-silence-ping-logging` reducing log volume. This flag appends the `-ping-path` to `-exclude-logging-paths`. +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: @@ -167,7 +177,7 @@ The status block will contain one of the below strings: - `AuthFailure` If the user failed to authenticate explicitly - `AuthError` If there was an unexpected error during authentication -If you require a different format than that, you can configure it with the `-auth-logging-format` flag. +If you require a different format than that, you can configure it with the `--auth-logging-format` flag. The default format is configured as follows: ``` @@ -195,7 +205,7 @@ HTTP request logs will output by default in the below format: <REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION> ``` -If you require a different format than that, you can configure it with the `-request-logging-format` flag. +If you require a different format than that, you can configure it with the `--request-logging-format` flag. The default format is configured as follows: ``` @@ -226,7 +236,7 @@ All other logging that is not covered by the above two types of logging will be [19/Mar/2015:17:20:19 -0400] [main.go:40] <MESSAGE> ``` -If you require a different format than that, you can configure it with the `-standard-logging-format` flag. The default format is configured as follows: +If you require a different format than that, you can configure it with the `--standard-logging-format` flag. The default format is configured as follows: ``` {% raw %}[{{.Timestamp}}] [{{.File}}] {{.Message}}{% endraw %} @@ -242,7 +252,7 @@ Available variables for standard logging: ## <a name="nginx-auth-request"></a>Configuring for use with the Nginx `auth_request` directive -The [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) allows Nginx to authenticate requests via the oauth2_proxy's `/auth` endpoint, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the request through. For example: +The [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) allows Nginx to authenticate requests via the oauth2-proxy's `/auth` endpoint, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the request through. For example: ```nginx server { @@ -313,7 +323,9 @@ server { } ``` -If you use ingress-nginx in Kubernetes (which includes the Lua module), you also can use the following configuration snippet for your Ingress: +When you use ingress-nginx in Kubernetes , you MUST use `kubernetes/ingress-nginx` (which includes the Lua module) and the following configuration snippet for your `Ingress`. +Variables set with `auth_request_set` are not `set`-able in plain nginx config when the location is processed via `proxy_pass` and then may only be processed by Lua. +Note that `nginxinc/kubernetes-ingress` does not include the Lua module. ```yaml nginx.ingress.kubernetes.io/auth-response-headers: Authorization @@ -328,6 +340,7 @@ nginx.ingress.kubernetes.io/configuration-snippet: | end } ``` +It is recommended to use `--session-store-type=redis` when expecting large sessions/OIDC tokens (_e.g._ with MS Azure). You have to substitute *name* with the actual cookie name you configured via --cookie-name parameter. If you don't set a custom cookie name the variable should be "$upstream_cookie__oauth2_proxy_1" instead of "$upstream_cookie_name_1" and the new cookie-name should be "_oauth2_proxy_1=" instead of "name_1=". diff --git a/env_options.go b/env_options.go deleted file mode 100644 index 058b90c6ce4538ec3697c4c7c3faf87d0f7ebf10..0000000000000000000000000000000000000000 --- a/env_options.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "os" - "reflect" - "strings" -) - -// EnvOptions holds program options loaded from the process environment -type EnvOptions map[string]interface{} - -// LoadEnvForStruct loads environment variables for each field in an options -// struct passed into it. -// -// Fields in the options struct must have an `env` and `cfg` tag to be read -// from the environment -func (cfg EnvOptions) LoadEnvForStruct(options interface{}) { - val := reflect.ValueOf(options) - var typ reflect.Type - if val.Kind() == reflect.Ptr { - typ = val.Elem().Type() - } else { - typ = val.Type() - } - - for i := 0; i < typ.NumField(); i++ { - // pull out the struct tags: - // flag - the name of the command line flag - // deprecated - (optional) the name of the deprecated command line flag - // cfg - (optional, defaults to underscored flag) the name of the config file option - field := typ.Field(i) - fieldV := reflect.Indirect(val).Field(i) - - if field.Type.Kind() == reflect.Struct && field.Anonymous { - cfg.LoadEnvForStruct(fieldV.Interface()) - continue - } - - flagName := field.Tag.Get("flag") - envName := field.Tag.Get("env") - cfgName := field.Tag.Get("cfg") - if cfgName == "" && flagName != "" { - cfgName = strings.ReplaceAll(flagName, "-", "_") - } - if envName == "" || cfgName == "" { - // resolvable fields must have the `env` and `cfg` struct tag - continue - } - v := os.Getenv(envName) - if v != "" { - cfg[cfgName] = v - } - } -} diff --git a/env_options_test.go b/env_options_test.go deleted file mode 100644 index c1937e63e6d45d6a155028f3afcba83ac518a9c2..0000000000000000000000000000000000000000 --- a/env_options_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package main_test - -import ( - "os" - "testing" - - proxy "github.com/pusher/oauth2_proxy" - "github.com/stretchr/testify/assert" -) - -type EnvTest struct { - TestField string `cfg:"target_field" env:"TEST_ENV_FIELD"` - EnvTestEmbed -} - -type EnvTestEmbed struct { - TestFieldEmbed string `cfg:"target_field_embed" env:"TEST_ENV_FIELD_EMBED"` -} - -func TestLoadEnvForStruct(t *testing.T) { - - cfg := make(proxy.EnvOptions) - cfg.LoadEnvForStruct(&EnvTest{}) - - _, ok := cfg["target_field"] - assert.Equal(t, ok, false) - - os.Setenv("TEST_ENV_FIELD", "1234abcd") - cfg.LoadEnvForStruct(&EnvTest{}) - v := cfg["target_field"] - assert.Equal(t, v, "1234abcd") -} - -func TestLoadEnvForStructWithEmbeddedFields(t *testing.T) { - - cfg := make(proxy.EnvOptions) - cfg.LoadEnvForStruct(&EnvTest{}) - - _, ok := cfg["target_field_embed"] - assert.Equal(t, ok, false) - - os.Setenv("TEST_ENV_FIELD_EMBED", "1234abcd") - cfg.LoadEnvForStruct(&EnvTest{}) - v := cfg["target_field_embed"] - assert.Equal(t, v, "1234abcd") -} diff --git a/go.mod b/go.mod index 8d883ee84d7d536e03508328292b2efae1fae751..618c7c7393ec07a432c6ca541557dd3b4a2156b9 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,29 @@ -module github.com/pusher/oauth2_proxy +module github.com/oauth2-proxy/oauth2-proxy -go 1.13 +go 1.14 require ( - github.com/BurntSushi/toml v0.3.1 - github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect - github.com/alicebob/miniredis v2.5.0+incompatible + github.com/alicebob/miniredis/v2 v2.11.2 github.com/bitly/go-simplejson v0.5.0 - github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 - github.com/coreos/go-oidc v2.1.0+incompatible + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/coreos/go-oidc v2.2.1+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/go-redis/redis v6.15.5+incompatible - github.com/gomodule/redigo v2.0.0+incompatible // indirect - github.com/kr/pretty v0.1.0 // indirect + github.com/fsnotify/fsnotify v1.4.9 + github.com/go-redis/redis/v7 v7.2.0 + github.com/kr/pretty v0.2.0 // indirect github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa - github.com/mreiferson/go-options v0.0.0-20190302064952-20ba7d382d05 - github.com/onsi/ginkgo v1.10.2 - github.com/onsi/gomega v1.7.0 + github.com/mitchellh/mapstructure v1.1.2 + github.com/onsi/ginkgo v1.12.0 + github.com/onsi/gomega v1.9.0 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect - github.com/stretchr/testify v1.4.0 + github.com/spf13/pflag v1.0.3 + github.com/spf13/viper v1.6.3 + github.com/stretchr/testify v1.5.1 github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 - github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036 // indirect - golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc - golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c - golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 - google.golang.org/api v0.10.0 - gopkg.in/fsnotify/fsnotify.v1 v1.4.7 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 + golang.org/x/net v0.0.0-20190923162816-aa69164e4478 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + google.golang.org/api v0.20.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 - gopkg.in/square/go-jose.v2 v2.3.1 + gopkg.in/square/go-jose.v2 v2.4.1 ) diff --git a/go.sum b/go.sum index 480f92a6b57739c329782aa3f235b3f01fd660ec..45e2be1b3a86e7fab01a98506932a191fc020ee8 100644 --- a/go.sum +++ b/go.sum @@ -4,82 +4,185 @@ cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +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 v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= -github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/alicebob/miniredis/v2 v2.11.2 h1:OtWO7akm5otuhssnE/sNfsWxG4gZ8DbGhShDtRrByJs= +github.com/alicebob/miniredis/v2 v2.11.2/go.mod h1:VL3UDEfAH59bSa7MuHMuFToxkqyHh69s/WUbYlOAuyg= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +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/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.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/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 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/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk= -github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +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-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs= +github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 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/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/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -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/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3 h1:6amM4HsNPOvMLVc2ZnyqrjeQ92YAVWn7T4WBKK87inY= +github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/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 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +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/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 v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +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/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa h1:hI1uC2A3vJFjwvBn0G0a7QBRdBUp6Y048BtLAHRTKPo= github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s= -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/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94= -github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/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-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/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_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/client_model v0.0.0-20190812154241-14fe0d1b01d4/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/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/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= +github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +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-20190514113301-1cd887cd7036 h1:1b6PAtenNyhsmo/NKXVe34h7JEZKva1YB/ne7K7mqKM= -github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4= -golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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= @@ -89,42 +192,59 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl 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/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 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-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 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-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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-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-20190114222345-bf090417da8b/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-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.10.0 h1:7tmAxx3oKE98VMZ+SBZzvYYWRQ9HODBxmC8mXUsraSQ= -google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 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 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= @@ -132,26 +252,36 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= -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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= -gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= +gopkg.in/square/go-jose.v2 v2.4.1/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= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/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= diff --git a/htpasswd.go b/htpasswd.go index b7c8d57904df1c9906161c966eb9182dd02ab168..670aa72906c2cd42c1d1a959289ec872e91c17c4 100644 --- a/htpasswd.go +++ b/htpasswd.go @@ -7,7 +7,7 @@ import ( "io" "os" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" "golang.org/x/crypto/bcrypt" ) diff --git a/http.go b/http.go index 88280c440f0bfafe6f7f639da77dab64ce151a23..228364e37dd81b49c7e621dd6c307d7c94a8da18 100644 --- a/http.go +++ b/http.go @@ -1,19 +1,22 @@ package main import ( + "context" "crypto/tls" + "errors" "net" "net/http" "strings" "time" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // Server represents an HTTP server type Server struct { Handler http.Handler Opts *Options + stop chan struct{} // channel for waiting shutdown } // ListenAndServe will serve traffic on HTTP or HTTPS depending on TLS options @@ -90,13 +93,7 @@ func (s *Server) ServeHTTP() { logger.Fatalf("FATAL: listen (%s, %s) failed - %s", networkType, listenAddr, err) } logger.Printf("HTTP: listening on %s", listenAddr) - - server := &http.Server{Handler: s.Handler} - err = server.Serve(listener) - if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { - logger.Printf("ERROR: http.Serve() - %s", err) - } - + s.serve(listener) logger.Printf("HTTP: closing %s", listener.Addr()) } @@ -125,14 +122,31 @@ func (s *Server) ServeHTTPS() { logger.Printf("HTTPS: listening on %s", ln.Addr()) tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) + s.serve(tlsListener) + logger.Printf("HTTPS: closing %s", tlsListener.Addr()) +} + +func (s *Server) serve(listener net.Listener) { srv := &http.Server{Handler: s.Handler} - err = srv.Serve(tlsListener) - if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { - logger.Printf("ERROR: https.Serve() - %s", err) - } + // See https://golang.org/pkg/net/http/#Server.Shutdown + idleConnsClosed := make(chan struct{}) + go func() { + <-s.stop // wait notification for stopping server - logger.Printf("HTTPS: closing %s", tlsListener.Addr()) + // We received an interrupt signal, shut down. + if err := srv.Shutdown(context.Background()); err != nil { + // Error from closing listeners, or context timeout: + logger.Printf("HTTP server Shutdown: %v", err) + } + close(idleConnsClosed) + }() + + err := srv.Serve(listener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Printf("ERROR: http.Serve() - %s", err) + } + <-idleConnsClosed } // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted diff --git a/http_test.go b/http_test.go index 400213a01bc47aeb49655403161acd3750028e3a..f5165c5e2dc53a191b557ac1bb956c512d21d0d8 100644 --- a/http_test.go +++ b/http_test.go @@ -3,7 +3,9 @@ package main import ( "net/http" "net/http/httptest" + "sync" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -156,3 +158,31 @@ func TestRedirectNotWhenHTTPS(t *testing.T) { assert.Equal(t, http.StatusOK, res.StatusCode, "status code should be %d, got: %d", http.StatusOK, res.StatusCode) } + +func TestGracefulShutdown(t *testing.T) { + opts := NewOptions() + stop := make(chan struct{}, 1) + srv := Server{Handler: http.DefaultServeMux, Opts: opts, stop: stop} + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + srv.ServeHTTP() + }() + + stop <- struct{}{} // emulate catching signals + + // An idiomatic for sync.WaitGroup with timeout + c := make(chan struct{}) + go func() { + defer close(c) + wg.Wait() + }() + select { + case <-c: + case <-time.After(1 * time.Second): + t.Fatal("Server should return gracefully but timeout has occurred") + } + + assert.Len(t, stop, 0) // check if stop chan is empty +} diff --git a/logging_handler.go b/logging_handler.go index 9915e277ea15d3228d9f95a78d5689f6d0b85802..1c8574135caa8b7b41748f6525c6461643950219 100644 --- a/logging_handler.go +++ b/logging_handler.go @@ -10,7 +10,7 @@ import ( "net/http" "time" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status diff --git a/logging_handler_test.go b/logging_handler_test.go index ddc977836ceffd6e32332d3c9b4214c13d215766..756329fcbd4488a33db12a85220a1d71bdd97254 100644 --- a/logging_handler_test.go +++ b/logging_handler_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) func TestLoggingHandler_ServeHTTP(t *testing.T) { diff --git a/main.go b/main.go index dcd2d6b1208942a5c657ad5642e6e00c78a0d0b8..d72f14e96dcc92ad8b5cdbbd07d2d9ba597d355b 100644 --- a/main.go +++ b/main.go @@ -1,32 +1,24 @@ package main import ( - "flag" "fmt" "math/rand" "net/http" "os" + "os/signal" "runtime" "strings" + "syscall" "time" - "github.com/BurntSushi/toml" - options "github.com/mreiferson/go-options" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/spf13/pflag" ) func main() { logger.SetFlags(logger.Lshortfile) - flagSet := flag.NewFlagSet("oauth2_proxy", flag.ExitOnError) - - emailDomains := StringArray{} - whitelistDomains := StringArray{} - upstreams := StringArray{} - skipAuthRegex := StringArray{} - jwtIssuers := StringArray{} - googleGroups := StringArray{} - redisSentinelConnectionURLs := StringArray{} - redisClusterConnectionURLs := StringArray{} + flagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ExitOnError) config := flagSet.String("config", "", "path to config file") showVersion := flagSet.Bool("version", false, "print version string") @@ -34,39 +26,44 @@ func main() { 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.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted") + flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP)") flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests") flagSet.String("tls-cert-file", "", "path to certificate file") flagSet.String("tls-key-file", "", "path to private key file") flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"") flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)") - flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint, file:// paths for static files or static://<status_code> for static response. Routing is based on the path") + flagSet.StringSlice("upstream", []string{}, "the http url(s) of the upstream endpoint, file:// paths for static files or static://<status_code> for static response. Routing is based on the path") flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream") + flagSet.Bool("set-basic-auth", false, "set HTTP Basic Auth information in response (useful in Nginx auth_request mode)") + flagSet.Bool("prefer-email-to-user", false, "Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, eg. htaccess authentication. Used in conjunction with -pass-basic-auth and -pass-user-headers") flagSet.Bool("pass-user-headers", true, "pass X-Forwarded-User and X-Forwarded-Email information to upstream") flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header") flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header") flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream") flagSet.Bool("pass-authorization-header", false, "pass the Authorization Header to upstream") flagSet.Bool("set-authorization-header", false, "set Authorization response headers (useful in Nginx auth_request mode)") - flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)") + flagSet.StringSlice("skip-auth-regex", []string{}, "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.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.StringSlice("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)") - 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.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") + flagSet.StringSlice("whitelist-domain", []string{}, "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("github-repo", "", "restrict logins to collaborators of this repository") + flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)") flagSet.String("gitlab-group", "", "restrict logins to members of this group") flagSet.String("authsch-group", "", "restrict logins to members of this AD group") - flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).") + flagSet.StringSlice("google-group", []string{}, "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") flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"") @@ -84,7 +81,7 @@ func main() { flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates") flagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)") - flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*") + flagSet.StringSlice("cookie-domain", []string{}, "Optional cookie domains to force cookies to (ie: `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match).") flagSet.String("cookie-path", "/", "an optional cookie path to force cookies to (ie: /poc/)*") flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable") @@ -98,9 +95,9 @@ func main() { flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel") flagSet.String("redis-ca-path", "", "Redis custom CA path") flagSet.Bool("redis-insecure-skip-tls-verify", false, "Use insecure TLS connection to redis") - 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.StringSlice("redis-sentinel-connection-urls", []string{}, "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel") flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature") - flagSet.Var(&redisClusterConnectionURLs, "redis-cluster-connection-urls", "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster") + flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster") 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") @@ -124,6 +121,7 @@ func main() { flagSet.String("provider-display-name", "", "Provider display name") 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("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL") 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") @@ -132,35 +130,33 @@ func main() { flagSet.String("resource", "", "The resource that is protected (Azure AD only)") flagSet.String("validate-url", "", "Access token validation endpoint") flagSet.String("scope", "", "OAuth scope specification") + flagSet.String("prompt", "", "OIDC prompt") flagSet.String("approval-prompt", "force", "OAuth approval_prompt") flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") - flagSet.String("acr-values", "http://idmanagement.gov/ns/assurance/loa/1", "acr values string: optional, used by login.gov") + flagSet.String("acr-values", "", "acr values string: optional") flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov") flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov") flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov") flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints") + flagSet.String("user-id-claim", "email", "which claim contains the user ID") + flagSet.Parse(os.Args[1:]) if *showVersion { - fmt.Printf("oauth2_proxy %s (built with %s)\n", VERSION, runtime.Version()) + fmt.Printf("oauth2-proxy %s (built with %s)\n", VERSION, runtime.Version()) return } opts := NewOptions() - - cfg := make(EnvOptions) - if *config != "" { - _, err := toml.DecodeFile(*config, &cfg) - if err != nil { - logger.Fatalf("ERROR: failed to load config file %s - %s", *config, err) - } + err := options.Load(*config, flagSet, opts) + if err != nil { + logger.Printf("ERROR: Failed to load config: %v", err) + os.Exit(1) } - cfg.LoadEnvForStruct(opts) - options.Resolve(opts, flagSet, cfg) - err := opts.Validate() + err = opts.Validate() if err != nil { logger.Printf("%s", err) os.Exit(1) @@ -203,6 +199,14 @@ func main() { s := &Server{ Handler: handler, Opts: opts, + stop: make(chan struct{}, 1), } + // Observe signals in background goroutine. + go func() { + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt, syscall.SIGTERM) + <-sigint + s.stop <- struct{}{} // notify having caught signal + }() s.ListenAndServe() } diff --git a/nsswitch.conf b/nsswitch.conf new file mode 100644 index 0000000000000000000000000000000000000000..21fae7381fc941035607abfe669fd3d1eaad8edd --- /dev/null +++ b/nsswitch.conf @@ -0,0 +1 @@ +hosts: files dns diff --git a/oauthproxy.go b/oauthproxy.go index 0ac18092f317839bb093eaaad61802305170a051..761d9de2601dd3d236cdd861ec9e2d97f6fd7421 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -19,11 +19,11 @@ import ( "github.com/coreos/go-oidc" "github.com/mbland/hmacauth" - 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/logger" - "github.com/pusher/oauth2_proxy/providers" + sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies" + "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/providers" "github.com/yhat/wsutil" ) @@ -48,6 +48,7 @@ var SignatureHeaders = []string{ "Authorization", "X-Forwarded-User", "X-Forwarded-Email", + "X-Forwarded-Preferred-User", "X-Forwarded-Access-Token", "Cookie", "Gap-Auth", @@ -56,6 +57,10 @@ var SignatureHeaders = []string{ var ( // ErrNeedsLogin means the user should be redirected to the login page ErrNeedsLogin = errors.New("redirect to login page") + + // Used to check final redirects are not susceptible to open redirects. + // Matches //, /\ and both of these with whitespace in between (eg / / or / \). + invalidRedirectRegex = regexp.MustCompile(`^/(\s|\v)?(/|\\)`) ) // OAuthProxy is the main authentication proxy @@ -63,7 +68,7 @@ type OAuthProxy struct { CookieSeed string CookieName string CSRFCookieName string - CookieDomain string + CookieDomains []string CookiePath string CookieSecure bool CookieHTTPOnly bool @@ -93,18 +98,21 @@ type OAuthProxy struct { serveMux http.Handler SetXAuthRequest bool PassBasicAuth bool + SetBasicAuth bool SkipProviderButton bool PassUserHeaders bool BasicAuthPassword string PassAccessToken bool SetAuthorization bool PassAuthorization bool + PreferEmailToUser bool skipAuthRegex []string skipAuthPreflight bool skipJwtBearerTokens bool jwtBearerVerifiers []*oidc.IDTokenVerifier compiledRegex []*regexp.Regexp templates *template.Template + realClientIPParser realClientIPParser Banner string Footer string } @@ -188,6 +196,9 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http") wsURL := &url.URL{Scheme: wsScheme, Host: u.Host} wsProxy = wsutil.NewSingleHostReverseProxy(wsURL) + if opts.SSLUpstreamInsecureSkipVerify { + wsProxy.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } } return &UpstreamProxy{ upstream: u.Host, @@ -241,7 +252,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme)) } } - for _, u := range opts.CompiledRegex { + for _, u := range opts.compiledRegex { logger.Printf("compiled skip-auth-regex => %q", u) } @@ -258,23 +269,23 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID) refresh := "disabled" - if opts.CookieRefresh != time.Duration(0) { - refresh = fmt.Sprintf("after %s", opts.CookieRefresh) + if opts.Cookie.Refresh != time.Duration(0) { + refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh) } - logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s samesite:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, opts.CookieSameSite, refresh) + logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh) return &OAuthProxy{ - CookieName: opts.CookieName, - CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"), - CookieSeed: opts.CookieSecret, - CookieDomain: opts.CookieDomain, - CookiePath: opts.CookiePath, - CookieSecure: opts.CookieSecure, - CookieHTTPOnly: opts.CookieHTTPOnly, - CookieExpire: opts.CookieExpire, - CookieRefresh: opts.CookieRefresh, - CookieSameSite: opts.CookieSameSite, + CookieName: opts.Cookie.Name, + CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"), + CookieSeed: opts.Cookie.Secret, + CookieDomains: opts.Cookie.Domains, + CookiePath: opts.Cookie.Path, + CookieSecure: opts.Cookie.Secure, + CookieHTTPOnly: opts.Cookie.HTTPOnly, + CookieExpire: opts.Cookie.Expire, + CookieRefresh: opts.Cookie.Refresh, + CookieSameSite: opts.Cookie.SameSite, Validator: validator, RobotsPath: "/robots.txt", @@ -297,14 +308,17 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { skipAuthPreflight: opts.SkipAuthPreflight, skipJwtBearerTokens: opts.SkipJwtBearerTokens, jwtBearerVerifiers: opts.jwtBearerVerifiers, - compiledRegex: opts.CompiledRegex, + compiledRegex: opts.compiledRegex, + realClientIPParser: opts.realClientIPParser, SetXAuthRequest: opts.SetXAuthRequest, PassBasicAuth: opts.PassBasicAuth, + SetBasicAuth: opts.SetBasicAuth, PassUserHeaders: opts.PassUserHeaders, BasicAuthPassword: opts.BasicAuthPassword, PassAccessToken: opts.PassAccessToken, SetAuthorization: opts.SetAuthorization, PassAuthorization: opts.PassAuthorization, + PreferEmailToUser: opts.PreferEmailToUser, SkipProviderButton: opts.SkipProviderButton, templates: loadTemplates(opts.CustomTemplatesDir), Banner: opts.Banner, @@ -319,8 +333,7 @@ func (p *OAuthProxy) GetRedirectURI(host string) string { if p.redirectURL.Host != "" { return p.redirectURL.String() } - var u url.URL - u = *p.redirectURL + u := *p.redirectURL if u.Scheme == "" { if p.CookieSecure { u.Scheme = httpsScheme @@ -336,22 +349,29 @@ 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(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) { if code == "" { return nil, errors.New("missing code") } redirectURI := p.GetRedirectURI(host) - s, err = p.provider.Redeem(redirectURI, code) + s, err = p.provider.Redeem(ctx, redirectURI, code) if err != nil { return } if s.Email == "" { - s.Email, err = p.provider.GetEmailAddress(s) + s.Email, err = p.provider.GetEmailAddress(ctx, s) + } + + if s.PreferredUsername == "" { + s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s) + if err != nil && err.Error() == "not implemented" { + err = nil + } } if s.User == "" { - s.User, err = p.provider.GetUserName(s) + s.User, err = p.provider.GetUserName(ctx, s) if err != nil && err.Error() == "not implemented" { err = nil } @@ -366,13 +386,15 @@ func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration } func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie { - if p.CookieDomain != "" { - domain := req.Host + cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains) + + if cookieDomain != "" { + domain := cookies.GetRequestHost(req) if h, _, err := net.SplitHostPort(domain); err == nil { domain = h } - if !strings.HasSuffix(domain, p.CookieDomain) { - logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain) + if !strings.HasSuffix(domain, cookieDomain) { + logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain) } } @@ -380,7 +402,7 @@ func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, ex Name: name, Value: value, Path: p.CookiePath, - Domain: p.CookieDomain, + Domain: cookieDomain, HttpOnly: p.CookieHTTPOnly, Secure: p.CookieSecure, Expires: now.Add(expiration), @@ -446,20 +468,24 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m // SignInPage writes the sing in template to the response func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) { + prepareNoCache(rw) p.ClearSessionCookie(rw, req) rw.WriteHeader(code) - redirecURL := req.URL.RequestURI() - if req.Header.Get("X-Auth-Request-Redirect") != "" { - redirecURL = req.Header.Get("X-Auth-Request-Redirect") + redirectURL, err := p.GetRedirect(req) + if err != nil { + logger.Printf("Error obtaining redirect: %s", err.Error()) + p.ErrorPage(rw, 500, "Internal Error", err.Error()) + return } - if redirecURL == p.SignInPath { - redirecURL = "/" + + if redirectURL == p.SignInPath { + redirectURL = "/" } t := struct { ProviderName string - SignInMessage string + SignInMessage template.HTML CustomLogin bool Redirect string Version string @@ -467,9 +493,9 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code Footer template.HTML }{ ProviderName: p.provider.Data().ProviderName, - SignInMessage: p.SignInMessage, + SignInMessage: template.HTML(p.SignInMessage), CustomLogin: p.displayCustomLoginForm(), - Redirect: redirecURL, + Redirect: redirectURL, Version: VERSION, ProxyPrefix: p.ProxyPrefix, Footer: template.HTML(p.Footer), @@ -561,11 +587,12 @@ func validOptionalPort(port string) bool { // IsValidRedirect checks whether the redirect URL is whitelisted func (p *OAuthProxy) IsValidRedirect(redirect string) bool { switch { - case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !strings.HasPrefix(redirect, "/\\"): + case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect): return true case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"): redirectURL, err := url.Parse(redirect) if err != nil { + logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect) return false } redirectHostname := redirectURL.Hostname() @@ -590,8 +617,10 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool { } } + logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect) return false default: + logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect) return false } } @@ -612,15 +641,26 @@ func (p *OAuthProxy) IsWhitelistedPath(path string) bool { return false } -func getRemoteAddr(req *http.Request) (s string) { - s = req.RemoteAddr - if req.Header.Get("X-Real-IP") != "" { - s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP")) +// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en +var noCacheHeaders = map[string]string{ + "Expires": time.Unix(0, 0).Format(time.RFC1123), + "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0", + "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ +} + +// prepareNoCache prepares headers for preventing browser caching. +func prepareNoCache(w http.ResponseWriter) { + // Set NoCache headers + for k, v := range noCacheHeaders { + w.Header().Set(k, v) } - return } func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if strings.HasPrefix(req.URL.Path, p.ProxyPrefix) { + prepareNoCache(rw) + } + switch path := req.URL.Path; { case path == p.RobotsPath: p.RobotsTxt(rw) @@ -658,7 +698,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) { if ok { session := &sessionsapi.SessionState{User: user} p.SaveSession(rw, req, session) - http.Redirect(rw, req, redirect, 302) + http.Redirect(rw, req, redirect, http.StatusFound) } else { if p.SkipProviderButton { p.OAuthStart(rw, req) @@ -668,7 +708,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) { } } -//UserInfo endpoint outputs session email in JSON format +//UserInfo endpoint outputs session email and preferred username in JSON format func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { session, err := p.getAuthenticatedSession(rw, req) @@ -677,8 +717,12 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { return } userInfo := struct { - Email string `json:"email"` - }{session.Email} + Email string `json:"email"` + PreferredUsername string `json:"preferredUsername,omitempty"` + }{ + Email: session.Email, + PreferredUsername: session.PreferredUsername, + } rw.Header().Set("Content-Type", "application/json") rw.WriteHeader(http.StatusOK) json.NewEncoder(rw).Encode(userInfo) @@ -693,11 +737,12 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { return } p.ClearSessionCookie(rw, req) - http.Redirect(rw, req, redirect, 302) + http.Redirect(rw, req, redirect, http.StatusFound) } // OAuthStart starts the OAuth2 authentication flow func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { + prepareNoCache(rw) nonce, err := encryption.Nonce() if err != nil { logger.Printf("Error obtaining nonce: %s", err.Error()) @@ -712,13 +757,13 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { return } redirectURI := p.GetRedirectURI(req.Host) - http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302) + http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound) } // OAuthCallback is the OAuth2 authentication flow callback that finishes the // OAuth2 authentication flow func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { - remoteAddr := getRemoteAddr(req) + remoteAddr := getClientString(p.realClientIPParser, req, true) // finish the oauth cycle err := req.ParseForm() @@ -734,7 +779,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { return } - session, err := p.redeemCode(req.Host, req.Form.Get("code")) + session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code")) if err != nil { logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error()) p.ErrorPage(rw, 500, "Internal Error", "Internal Error") @@ -775,7 +820,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { p.ErrorPage(rw, 500, "Internal Error", "Internal Error") return } - http.Redirect(rw, req, redirect, 302) + http.Redirect(rw, req, redirect, http.StatusFound) } else { logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized") p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account") @@ -846,7 +891,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R } } - remoteAddr := getRemoteAddr(req) + remoteAddr := getClientString(p.realClientIPParser, req, true) if session == nil { session, err = p.LoadCookiedSession(req) if err != nil { @@ -859,7 +904,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R saveSession = true } - if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil { + if ok, err := p.provider.RefreshSessionIfNeeded(req.Context(), session); err != nil { logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session) clearSession = true session = nil @@ -878,7 +923,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R } if saveSession && !revalidated && session != nil && session.AccessToken != "" { - if !p.provider.ValidateSessionState(session) { + if !p.provider.ValidateSessionState(req.Context(), session) { logger.Printf("Removing session: error validating %s", session) saveSession = false session = nil @@ -886,13 +931,11 @@ 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) { - logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session) - session = nil - saveSession = false - clearSession = true - } + if session != nil && session.Email != "" && !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 { @@ -924,21 +967,43 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R // addHeadersForProxying adds the appropriate headers the request / response for proxying func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) { if p.PassBasicAuth { - req.SetBasicAuth(session.User, p.BasicAuthPassword) - req.Header["X-Forwarded-User"] = []string{session.User} - if session.Email != "" { - req.Header["X-Forwarded-Email"] = []string{session.Email} - } else { + if p.PreferEmailToUser && session.Email != "" { + req.SetBasicAuth(session.Email, p.BasicAuthPassword) + req.Header["X-Forwarded-User"] = []string{session.Email} req.Header.Del("X-Forwarded-Email") + } else { + 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 session.PreferredUsername != "" { + req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername} + } else { + req.Header.Del("X-Forwarded-Preferred-Username") } } if p.PassUserHeaders { - req.Header["X-Forwarded-User"] = []string{session.User} - if session.Email != "" { - req.Header["X-Forwarded-Email"] = []string{session.Email} - } else { + if p.PreferEmailToUser && session.Email != "" { + req.Header["X-Forwarded-User"] = []string{session.Email} req.Header.Del("X-Forwarded-Email") + } else { + 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 session.PreferredUsername != "" { + req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername} + } else { + req.Header.Del("X-Forwarded-Preferred-Username") } } @@ -949,6 +1014,11 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req } else { rw.Header().Del("X-Auth-Request-Email") } + if session.PreferredUsername != "" { + rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername) + } else { + rw.Header().Del("X-Auth-Request-Preferred-Username") + } if p.PassAccessToken { if session.AccessToken != "" { @@ -974,6 +1044,18 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req req.Header.Del("Authorization") } } + if p.SetBasicAuth { + switch { + case p.PreferEmailToUser && session.Email != "": + authVal := b64.StdEncoding.EncodeToString([]byte(session.Email + ":" + p.BasicAuthPassword)) + rw.Header().Set("Authorization", "Basic "+authVal) + case session.User != "": + authVal := b64.StdEncoding.EncodeToString([]byte(session.User + ":" + p.BasicAuthPassword)) + rw.Header().Set("Authorization", "Basic "+authVal) + default: + rw.Header().Del("Authorization") + } + } if p.SetAuthorization { if session.IDToken != "" { rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken)) @@ -1021,10 +1103,7 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionStat // isAjax checks if a request is an ajax request func isAjax(req *http.Request) bool { - acceptValues, ok := req.Header["accept"] - if !ok { - acceptValues = req.Header["Accept"] - } + acceptValues := req.Header.Values("Accept") const ajaxReq = applicationJSON for _, v := range acceptValues { if v == ajaxReq { @@ -1041,49 +1120,22 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) { } // GetJwtSession loads a session based on a JWT token in the authorization header. +// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers) 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) + bearerToken, err := verifier.Verify(req.Context(), 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 p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken) } return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization")) } diff --git a/oauthproxy_test.go b/oauthproxy_test.go index abe449d9737b46dfeac3add803a900452a5a15c9..a08b1640003e9c05fb3063b63c1e91edfabbda6c 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -18,10 +18,10 @@ import ( "github.com/coreos/go-oidc" "github.com/mbland/hmacauth" - "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/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/cookie" + "github.com/oauth2-proxy/oauth2-proxy/providers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/websocket" @@ -64,7 +64,6 @@ func TestWebSocketProxy(t *testing.T) { if err != nil { t.Fatalf("err %s", err) } - return }), } backend := httptest.NewServer(&handler) @@ -165,7 +164,7 @@ func TestRobotsTxt(t *testing.T) { opts := NewOptions() opts.ClientID = "asdlkjx" opts.ClientSecret = "alkgks" - opts.CookieSecret = "asdkugkj" + opts.Cookie.Secret = "asdkugkj" opts.Validate() proxy := NewOAuthProxy(opts, func(string) bool { return true }) @@ -180,7 +179,7 @@ func TestIsValidRedirect(t *testing.T) { opts := NewOptions() opts.ClientID = "skdlfj" opts.ClientSecret = "fgkdsgj" - opts.CookieSecret = "ljgiogbj" + opts.Cookie.Secret = "ljgiogbj" // Should match domains that are exactly foo.bar and any subdomain of bar.foo opts.WhitelistDomains = []string{ "foo.bar", @@ -323,6 +322,61 @@ func TestIsValidRedirect(t *testing.T) { Redirect: "http://a.sub.anyport.bar:8081/redirect", ExpectedResult: true, }, + { + Desc: "openRedirect1", + Redirect: "/\\evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectSpace1", + Redirect: "/ /evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectSpace2", + Redirect: "/ \\evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectTab1", + Redirect: "/\t/evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectTab2", + Redirect: "/\t\\evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectVerticalTab1", + Redirect: "/\v/evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectVerticalTab2", + Redirect: "/\v\\evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectNewLine1", + Redirect: "/\n/evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectNewLine2", + Redirect: "/\n\\evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectCarriageReturn1", + Redirect: "/\r/evil.com", + ExpectedResult: false, + }, + { + Desc: "openRedirectCarriageReturn2", + Redirect: "/\r\\evil.com", + ExpectedResult: false, + }, } for _, tc := range testCases { @@ -343,6 +397,8 @@ type TestProvider struct { GroupValidator func(string) bool } +var _ providers.Provider = (*TestProvider)(nil) + func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider { return &TestProvider{ ProviderData: &providers.ProviderData{ @@ -371,21 +427,14 @@ func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider { } } -func (tp *TestProvider) GetEmailAddress(session *sessions.SessionState) (string, error) { +func (tp *TestProvider) GetEmailAddress(ctx context.Context, session *sessions.SessionState) (string, error) { return tp.EmailAddress, nil } -func (tp *TestProvider) ValidateSessionState(session *sessions.SessionState) bool { +func (tp *TestProvider) ValidateSessionState(ctx context.Context, session *sessions.SessionState) bool { 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) @@ -406,12 +455,14 @@ func TestBasicAuthPassword(t *testing.T) { opts.Upstreams = append(opts.Upstreams, providerServer.URL) // The CookieSecret must be 32 bytes in order to create the AES // cipher. - opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" + opts.Cookie.Secret = "xyzzyplughxyzzyplughxyzzyplughxp" opts.ClientID = "dlgkj" opts.ClientSecret = "alkgret" - opts.CookieSecure = false + opts.Cookie.Secure = false opts.PassBasicAuth = true + opts.SetBasicAuth = true opts.PassUserHeaders = true + opts.PreferEmailToUser = true opts.BasicAuthPassword = "This is a secure password" opts.Validate() @@ -431,7 +482,7 @@ func TestBasicAuthPassword(t *testing.T) { if rw.Code >= 400 { t.Fatalf("expected 3xx got %d", rw.Code) } - cookie := rw.HeaderMap["Set-Cookie"][1] + cookie := rw.Header().Values("Set-Cookie")[1] cookieName := proxy.CookieName var value string @@ -466,6 +517,91 @@ func TestBasicAuthPassword(t *testing.T) { providerServer.Close() } +func TestBasicAuthWithEmail(t *testing.T) { + opts := NewOptions() + opts.PassBasicAuth = true + opts.PassUserHeaders = false + opts.PreferEmailToUser = false + opts.BasicAuthPassword = "This is a secure password" + opts.Validate() + + const emailAddress = "john.doe@example.com" + const userName = "9fcab5c9b889a557" + + // The username in the basic auth credentials is expected to be equal to the email address from the + expectedEmailHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(emailAddress+":"+opts.BasicAuthPassword)) + expectedUserHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(userName+":"+opts.BasicAuthPassword)) + + session := &sessions.SessionState{ + User: userName, + Email: emailAddress, + AccessToken: "oauth_token", + CreatedAt: time.Now(), + } + { + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", opts.ProxyPrefix+"/testCase0", nil) + proxy := NewOAuthProxy(opts, func(email string) bool { + return email == emailAddress + }) + proxy.addHeadersForProxying(rw, req, session) + assert.Equal(t, expectedUserHeader, req.Header["Authorization"][0]) + assert.Equal(t, userName, req.Header["X-Forwarded-User"][0]) + } + + opts.PreferEmailToUser = true + { + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", opts.ProxyPrefix+"/testCase1", nil) + + proxy := NewOAuthProxy(opts, func(email string) bool { + return email == emailAddress + }) + proxy.addHeadersForProxying(rw, req, session) + assert.Equal(t, expectedEmailHeader, req.Header["Authorization"][0]) + assert.Equal(t, emailAddress, req.Header["X-Forwarded-User"][0]) + } +} + +func TestPassUserHeadersWithEmail(t *testing.T) { + opts := NewOptions() + opts.PassBasicAuth = false + opts.PassUserHeaders = true + opts.PreferEmailToUser = false + opts.Validate() + + const emailAddress = "john.doe@example.com" + const userName = "9fcab5c9b889a557" + + session := &sessions.SessionState{ + User: userName, + Email: emailAddress, + AccessToken: "oauth_token", + CreatedAt: time.Now(), + } + { + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", opts.ProxyPrefix+"/testCase0", nil) + proxy := NewOAuthProxy(opts, func(email string) bool { + return email == emailAddress + }) + proxy.addHeadersForProxying(rw, req, session) + assert.Equal(t, userName, req.Header["X-Forwarded-User"][0]) + } + + opts.PreferEmailToUser = true + { + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", opts.ProxyPrefix+"/testCase1", nil) + + proxy := NewOAuthProxy(opts, func(email string) bool { + return email == emailAddress + }) + proxy.addHeadersForProxying(rw, req, session) + assert.Equal(t, emailAddress, req.Header["X-Forwarded-User"][0]) + } +} + type PassAccessTokenTest struct { providerServer *httptest.Server proxy *OAuthProxy @@ -503,10 +639,10 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes } // The CookieSecret must be 32 bytes in order to create the AES // cipher. - t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" + t.opts.Cookie.Secret = "xyzzyplughxyzzyplughxyzzyplughxp" t.opts.ClientID = "slgkj" t.opts.ClientSecret = "gfjgojl" - t.opts.CookieSecure = false + t.opts.Cookie.Secure = false t.opts.PassAccessToken = opts.PassAccessToken t.opts.Validate() @@ -534,7 +670,7 @@ func (patTest *PassAccessTokenTest) getCallbackEndpoint() (httpCode int, } req.AddCookie(patTest.proxy.MakeCSRFCookie(req, "nonce", time.Hour, time.Now())) patTest.proxy.ServeHTTP(rw, req) - return rw.Code, rw.HeaderMap["Set-Cookie"][1] + return rw.Code, rw.Header().Values("Set-Cookie")[1] } // getEndpointWithCookie makes a requests againt the oauthproxy with passed requestPath @@ -611,7 +747,7 @@ func TestStaticProxyUpstream(t *testing.T) { } assert.NotEqual(t, nil, cookie) - // Now we make a regular request againts the upstream proxy; And validate + // Now we make a regular request against the upstream proxy; And validate // the returned status code through the static proxy. code, payload := patTest.getEndpointWithCookie(cookie, "/static-proxy") if code != 200 { @@ -656,7 +792,7 @@ func NewSignInPageTest(skipProvider bool) *SignInPageTest { var sipTest SignInPageTest sipTest.opts = NewOptions() - sipTest.opts.CookieSecret = "adklsj2" + sipTest.opts.Cookie.Secret = "adklsj2" sipTest.opts.ClientID = "lkdgj" sipTest.opts.ClientSecret = "sgiufgoi" sipTest.opts.SkipProviderButton = skipProvider @@ -744,8 +880,6 @@ type ProcessCookieTest struct { proxy *OAuthProxy rw *httptest.ResponseRecorder req *http.Request - provider TestProvider - responseCode int validateUser bool } @@ -764,10 +898,10 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi } pcTest.opts.ClientID = "asdfljk" pcTest.opts.ClientSecret = "lkjfdsig" - pcTest.opts.CookieSecret = "0123456789abcdefabcd" + pcTest.opts.Cookie.Secret = "0123456789abcdef0123456789abcdef" // First, set the CookieRefresh option so proxy.AesCipher is created, // needed to encrypt the access_token. - pcTest.opts.CookieRefresh = time.Hour + pcTest.opts.Cookie.Refresh = time.Hour pcTest.opts.Validate() pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool { @@ -822,7 +956,7 @@ func TestLoadCookiedSession(t *testing.T) { 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) + assert.Equal(t, "", session.User) assert.Equal(t, startSession.AccessToken, session.AccessToken) } @@ -830,7 +964,7 @@ func TestProcessCookieNoCookieError(t *testing.T) { pcTest := NewProcessCookieTestWithDefaults() session, err := pcTest.LoadCookiedSession() - assert.Equal(t, "Cookie \"_oauth2_proxy\" not present", err.Error()) + assert.Equal(t, "cookie \"_oauth2_proxy\" not present", err.Error()) if session != nil { t.Errorf("expected nil session. got %#v", session) } @@ -838,7 +972,7 @@ func TestProcessCookieNoCookieError(t *testing.T) { func TestProcessCookieRefreshNotSet(t *testing.T) { pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) { - opts.CookieExpire = time.Duration(23) * time.Hour + opts.Cookie.Expire = time.Duration(23) * time.Hour }) reference := time.Now().Add(time.Duration(-2) * time.Hour) @@ -855,7 +989,7 @@ func TestProcessCookieRefreshNotSet(t *testing.T) { func TestProcessCookieFailIfCookieExpired(t *testing.T) { pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) { - opts.CookieExpire = time.Duration(24) * time.Hour + opts.Cookie.Expire = 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} @@ -870,7 +1004,7 @@ func TestProcessCookieFailIfCookieExpired(t *testing.T) { func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) { pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) { - opts.CookieExpire = time.Duration(24) * time.Hour + opts.Cookie.Expire = 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} @@ -940,7 +1074,7 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) { func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) { test := NewAuthOnlyEndpointTest(func(opts *Options) { - opts.CookieExpire = time.Duration(24) * time.Hour + opts.Cookie.Expire = time.Duration(24) * time.Hour }) reference := time.Now().Add(time.Duration(25) * time.Hour * -1) startSession := &sessions.SessionState{ @@ -966,30 +1100,75 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) { assert.Equal(t, "unauthorized request\n", string(bodyBytes)) } -func TestAuthOnlyEndpointUnauthorizedOnProviderGroupValidationFailure(t *testing.T) { - test := NewAuthOnlyEndpointTest() +func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { + var pcTest ProcessCookieTest + + pcTest.opts = NewOptions() + pcTest.opts.SetXAuthRequest = true + pcTest.opts.Validate() + + pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool { + return pcTest.validateUser + }) + pcTest.proxy.provider = &TestProvider{ + ValidToken: true, + } + + pcTest.validateUser = true + + pcTest.rw = httptest.NewRecorder() + pcTest.req, _ = http.NewRequest("GET", + pcTest.opts.ProxyPrefix+"/auth", nil) + startSession := &sessions.SessionState{ - Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()} - test.SaveSession(startSession) - provider := &TestProvider{ + User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: time.Now()} + pcTest.SaveSession(startSession) + + pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req) + assert.Equal(t, http.StatusAccepted, pcTest.rw.Code) + assert.Equal(t, "oauth_user", pcTest.rw.Header().Get("X-Auth-Request-User")) + assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Get("X-Auth-Request-Email")) +} + +func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) { + var pcTest ProcessCookieTest + + pcTest.opts = NewOptions() + pcTest.opts.SetXAuthRequest = true + pcTest.opts.SetBasicAuth = true + pcTest.opts.Validate() + + pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool { + return pcTest.validateUser + }) + pcTest.proxy.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)) + pcTest.validateUser = true + + pcTest.rw = httptest.NewRecorder() + pcTest.req, _ = http.NewRequest("GET", + 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) + + pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req) + assert.Equal(t, http.StatusAccepted, pcTest.rw.Code) + assert.Equal(t, "oauth_user", pcTest.rw.Header().Values("X-Auth-Request-User")[0]) + assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Values("X-Auth-Request-Email")[0]) + expectedHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte("oauth_user:"+pcTest.opts.BasicAuthPassword)) + assert.Equal(t, expectedHeader, pcTest.rw.Header().Values("Authorization")[0]) } -func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { +func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) { var pcTest ProcessCookieTest pcTest.opts = NewOptions() pcTest.opts.SetXAuthRequest = true + pcTest.opts.SetBasicAuth = false pcTest.opts.Validate() pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool { @@ -1011,8 +1190,9 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req) assert.Equal(t, http.StatusAccepted, pcTest.rw.Code) - assert.Equal(t, "oauth_user", pcTest.rw.HeaderMap["X-Auth-Request-User"][0]) - assert.Equal(t, "oauth_user@example.com", pcTest.rw.HeaderMap["X-Auth-Request-Email"][0]) + assert.Equal(t, "oauth_user", pcTest.rw.Header().Values("X-Auth-Request-User")[0]) + assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Values("X-Auth-Request-Email")[0]) + assert.Equal(t, 0, len(pcTest.rw.Header().Values("Authorization")), "should not have Authorization header entries") } func TestAuthSkippedForPreflightRequests(t *testing.T) { @@ -1026,7 +1206,7 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) { opts.Upstreams = append(opts.Upstreams, upstream.URL) opts.ClientID = "aljsal" opts.ClientSecret = "jglkfsdgj" - opts.CookieSecret = "dkfjgdls" + opts.Cookie.Secret = "dkfjgdls" opts.SkipAuthPreflight = true opts.Validate() @@ -1073,7 +1253,7 @@ type SignatureTest struct { func NewSignatureTest() *SignatureTest { opts := NewOptions() - opts.CookieSecret = "cookie secret" + opts.Cookie.Secret = "cookie secret" opts.ClientID = "client ID" opts.ClientSecret = "client secret" opts.EmailDomains = []string{"acm.org"} @@ -1220,7 +1400,7 @@ type ajaxRequestTest struct { func newAjaxRequestTest() *ajaxRequestTest { test := &ajaxRequestTest{} test.opts = NewOptions() - test.opts.CookieSecret = "sdflsw" + test.opts.Cookie.Secret = "sdflsw" test.opts.ClientID = "gkljfdl" test.opts.ClientSecret = "sdflkjs" test.opts.Validate() @@ -1278,11 +1458,11 @@ 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) + opts.Cookie.Name = "oauth2" + opts.Cookie.Domains = []string{"abc"} + store, err := cookie.NewCookieSessionStore(&opts.Session, &opts.Cookie) assert.Equal(t, err, nil) - p := OAuthProxy{CookieName: opts.CookieName, CookieDomain: opts.CookieDomain, sessionStore: store} + p := OAuthProxy{CookieName: opts.Cookie.Name, CookieDomains: opts.Cookie.Domains, sessionStore: store} var rw = httptest.NewRecorder() req := httptest.NewRequest("get", "/", nil) @@ -1307,11 +1487,11 @@ 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) + opts.Cookie.Name = "oauth2" + opts.Cookie.Domains = []string{"abc"} + store, err := cookie.NewCookieSessionStore(&opts.Session, &opts.Cookie) assert.Equal(t, err, nil) - p := OAuthProxy{CookieName: opts.CookieName, CookieDomain: opts.CookieDomain, sessionStore: store} + p := OAuthProxy{CookieName: opts.Cookie.Name, CookieDomains: opts.Cookie.Domains, sessionStore: store} var rw = httptest.NewRecorder() req := httptest.NewRequest("get", "/", nil) @@ -1336,8 +1516,7 @@ 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 + return base64.RawURLEncoding.DecodeString(payloadString) } func TestGetJwtSession(t *testing.T) { @@ -1403,43 +1582,8 @@ func TestGetJwtSession(t *testing.T) { 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"} + p := OAuthProxy{CookieName: "oauth2", CookieDomains: []string{"abc"}} getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}} validToken := "eyJfoobar.eyJfoobar.12345asdf" @@ -1499,3 +1643,46 @@ func TestFindJwtBearerToken(t *testing.T) { fmt.Printf("%s", token) } + +func Test_prepareNoCache(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + prepareNoCache(w) + }) + mux := http.NewServeMux() + mux.Handle("/", handler) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + mux.ServeHTTP(rec, req) + + for k, v := range noCacheHeaders { + assert.Equal(t, rec.Header().Get(k), v) + } +} + +func Test_noCacheHeadersDoesNotExistsInResponseHeadersFromUpstream(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("upstream")) + })) + t.Cleanup(upstream.Close) + + opts := NewOptions() + opts.Upstreams = []string{upstream.URL} + opts.SkipAuthRegex = []string{".*"} + _ = opts.Validate() + proxy := NewOAuthProxy(opts, func(email string) bool { + return true + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/upstream", nil) + proxy.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "upstream", rec.Body.String()) + + // checking noCacheHeaders does not exists in response headers from upstream + for k := range noCacheHeaders { + assert.Equal(t, "", rec.Header().Get(k)) + } +} diff --git a/options.go b/options.go index 4ec8ca6ea756c95b3875106a2463bfb2e1d4761a..fc3882ae3053475c06d7a69facd225203574d246 100644 --- a/options.go +++ b/options.go @@ -4,44 +4,46 @@ import ( "context" "crypto" "crypto/tls" - "encoding/base64" "fmt" "io/ioutil" "net/http" "net/url" "os" "regexp" + "sort" "strings" "time" oidc "github.com/coreos/go-oidc" "github.com/dgrijalva/jwt-go" "github.com/mbland/hmacauth" - "github.com/pusher/oauth2_proxy/pkg/apis/options" - sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/encryption" - "github.com/pusher/oauth2_proxy/pkg/logger" - "github.com/pusher/oauth2_proxy/pkg/sessions" - "github.com/pusher/oauth2_proxy/providers" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" + "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions" + "github.com/oauth2-proxy/oauth2-proxy/providers" "gopkg.in/natefinch/lumberjack.v2" ) // 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"` - 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"` - ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy" env:"OAUTH2_PROXY_REVERSE_PROXY"` - ForceHTTPS bool `flag:"force-https" cfg:"force_https" env:"OAUTH2_PROXY_FORCE_HTTPS"` - 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"` - ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file" env:"OAUTH2_PROXY_CLIENT_SECRET_FILE"` - TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"` - TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` + ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"` + PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"` + ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"` + HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"` + HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"` + ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy" env:"OAUTH2_PROXY_REVERSE_PROXY"` + RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header" env:"OAUTH2_PROXY_REAL_CLIENT_IP_HEADER"` + ForceHTTPS bool `flag:"force-https" cfg:"force_https" env:"OAUTH2_PROXY_FORCE_HTTPS"` + 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"` + ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file" env:"OAUTH2_PROXY_CLIENT_SECRET_FILE"` + TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"` + TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"` KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"` @@ -52,6 +54,8 @@ type Options struct { 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"` + GitHubRepo string `flag:"github-repo" cfg:"github_repo" env:"OAUTH2_PROXY_GITHUB_REPO"` + GitHubToken string `flag:"github-token" cfg:"github_token" env:"OAUTH2_PROXY_GITHUB_TOKEN"` GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group" env:"OAUTH2_PROXY_GITLAB_GROUP"` AuthSCHGroup string `flag:"authsch-group" cfg:"authsch_group" env:"OAUTH2_PROXY_AUTHSCH_GROUP"` GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"` @@ -63,17 +67,16 @@ type Options struct { Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"` Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"` - // Embed CookieOptions - options.CookieOptions - - // Embed SessionOptions - options.SessionOptions + Cookie options.CookieOptions `cfg:",squash"` + Session options.SessionOptions `cfg:",squash"` 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"` + SetBasicAuth bool `flag:"set-basic-auth" cfg:"set_basic_auth" env:"OAUTH2_PROXY_SET_BASIC_AUTH"` + PreferEmailToUser bool `flag:"prefer-email-to-user" cfg:"prefer_email_to_user" env:"OAUTH2_PROXY_PREFER_EMAIL_TO_USER"` BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"` PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"` PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"` @@ -89,19 +92,22 @@ type Options struct { // These options allow for other providers besides Google, with // potential overrides. - Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"` - ProviderName string `flag:"provider-display-name" cfg:"provider_display_name" env:"OAUTH2_PROXY_PROVIDER_DISPLAY_NAME"` - OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"` - InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"` - SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_PROXY_SKIP_OIDC_DISCOVERY"` - OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_PROXY_OIDC_JWKS_URL"` - LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"` - RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"` - ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"` - ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"` - ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"` - Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"` - ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` + Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"` + ProviderName string `flag:"provider-display-name" cfg:"provider_display_name" env:"OAUTH2_PROXY_PROVIDER_DISPLAY_NAME"` + 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"` + InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification" env:"OAUTH2_PROXY_INSECURE_OIDC_SKIP_ISSUER_VERIFICATION"` + SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_PROXY_SKIP_OIDC_DISCOVERY"` + OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_PROXY_OIDC_JWKS_URL"` + LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"` + RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"` + ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"` + ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"` + ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"` + Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"` + Prompt string `flag:"prompt" cfg:"prompt" env:"OAUTH2_PROXY_PROMPT"` + ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` // Deprecated by OIDC 1.0 + UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim" env:"OAUTH2_PROXY_USER_ID_CLAIM"` // Configuration values for logging LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_PROXY_LOGGING_FILENAME"` @@ -128,12 +134,13 @@ type Options struct { // internal values that are set after config validation redirectURL *url.URL proxyURLs []*url.URL - CompiledRegex []*regexp.Regexp + compiledRegex []*regexp.Regexp provider providers.Provider sessionStore sessionsapi.SessionStore signatureData *SignatureData oidcVerifier *oidc.IDTokenVerifier jwtBearerVerifiers []*oidc.IDTokenVerifier + realClientIPParser realClientIPParser } // SignatureData holds hmacauth signature hash and key @@ -152,25 +159,29 @@ func NewOptions() *Options { HTTPSAddress: ":443", ForceHTTPS: false, DisplayHtpasswdForm: true, - CookieOptions: options.CookieOptions{ - CookieName: "_oauth2_proxy", - CookieSecure: true, - CookieHTTPOnly: true, - CookieExpire: time.Duration(168) * time.Hour, - CookieRefresh: time.Duration(0), + Cookie: options.CookieOptions{ + Name: "_oauth2_proxy", + Secure: true, + HTTPOnly: true, + Expire: time.Duration(168) * time.Hour, + Refresh: time.Duration(0), }, - SessionOptions: options.SessionOptions{ + Session: options.SessionOptions{ Type: "cookie", }, SetXAuthRequest: false, SkipAuthPreflight: false, PassBasicAuth: true, + SetBasicAuth: false, PassUserHeaders: true, PassAccessToken: false, PassHostHeader: true, SetAuthorization: false, PassAuthorization: false, + PreferEmailToUser: false, + Prompt: "", // Change to "login" when ApprovalPrompt officially deprecated ApprovalPrompt: "force", + UserIDClaim: "email", InsecureOIDCAllowUnverifiedEmail: false, SkipOIDCDiscovery: false, LoggingFilename: "", @@ -217,7 +228,7 @@ func (o *Options) Validate() error { } msgs := make([]string, 0) - if o.CookieSecret == "" { + if o.Cookie.Secret == "" { msgs = append(msgs, "missing setting: cookie-secret") } if o.ClientID == "" { @@ -240,10 +251,52 @@ func (o *Options) Validate() error { "\n use email-domain=* to authorize all email addresses") } + if o.SetBasicAuth && o.SetAuthorization { + msgs = append(msgs, "mutually exclusive: set-basic-auth and set-authorization-header can not both be true") + } + if o.OIDCIssuerURL != "" { ctx := context.Background() + if o.InsecureOIDCSkipIssuerVerification && !o.SkipOIDCDiscovery { + // go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call + // (which uses discovery to get the URLs), so we'll do a quick check ourselves and if + // we get the URLs, we'll just use the non-discovery path. + + logger.Printf("Performing OIDC Discovery...") + + if req, err := http.NewRequest("GET", strings.TrimSuffix(o.OIDCIssuerURL, "/")+"/.well-known/openid-configuration", nil); err == nil { + if body, err := requests.Request(req); err == nil { + + // Prefer manually configured URLs. It's a bit unclear + // why you'd be doing discovery and also providing the URLs + // explicitly though... + if o.LoginURL == "" { + o.LoginURL = body.Get("authorization_endpoint").MustString() + } + + if o.RedeemURL == "" { + o.RedeemURL = body.Get("token_endpoint").MustString() + } + + if o.OIDCJwksURL == "" { + o.OIDCJwksURL = body.Get("jwks_uri").MustString() + } + + if o.ProfileURL == "" { + o.ProfileURL = body.Get("userinfo_endpoint").MustString() + } + + o.SkipOIDCDiscovery = true + } else { + logger.Printf("error: failed to discover OIDC configuration: %v", err) + } + } else { + logger.Printf("error: failed parsing OIDC discovery URL: %v", err) + } + } + // Construct a manual IDTokenVerifier from issuer URL & JWKS URI // instead of metadata discovery if we enable -skip-oidc-discovery. // In this case we need to make sure the required endpoints for @@ -260,7 +313,8 @@ func (o *Options) Validate() error { } keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL) o.oidcVerifier = oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{ - ClientID: o.ClientID, + ClientID: o.ClientID, + SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification, }) } else { // Configure discoverable provider data. @@ -269,7 +323,8 @@ func (o *Options) Validate() error { return err } o.oidcVerifier = provider.Verifier(&oidc.Config{ - ClientID: o.ClientID, + ClientID: o.ClientID, + SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification, }) o.LoginURL = provider.Endpoint().AuthURL @@ -280,6 +335,10 @@ func (o *Options) Validate() error { } } + if o.PreferEmailToUser && !o.PassBasicAuth && !o.PassUserHeaders { + msgs = append(msgs, "PreferEmailToUser should only be used with PassBasicAuth or PassUserHeaders") + } + if o.SkipJwtBearerTokens { // If we are using an oidc provider, go ahead and add that provider to the list if o.oidcVerifier != nil { @@ -314,61 +373,61 @@ func (o *Options) Validate() error { } for _, u := range o.SkipAuthRegex { - CompiledRegex, err := regexp.Compile(u) + compiledRegex, err := regexp.Compile(u) if err != nil { msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err)) continue } - o.CompiledRegex = append(o.CompiledRegex, CompiledRegex) + o.compiledRegex = append(o.compiledRegex, compiledRegex) } msgs = parseProviderInfo(o, msgs) var cipher *encryption.Cipher - if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.CookieRefresh != time.Duration(0)) { + if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.Cookie.Refresh != time.Duration(0)) { validCookieSecretSize := false for _, i := range []int{16, 24, 32} { - if len(secretBytes(o.CookieSecret)) == i { + if len(encryption.SecretBytes(o.Cookie.Secret)) == i { validCookieSecretSize = true } } var decoded bool - if string(secretBytes(o.CookieSecret)) != o.CookieSecret { + if string(encryption.SecretBytes(o.Cookie.Secret)) != o.Cookie.Secret { decoded = true } - if validCookieSecretSize == false { + if !validCookieSecretSize { var suffix string if decoded { - suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret) + suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.Cookie.Secret) } msgs = append(msgs, fmt.Sprintf( "cookie_secret must be 16, 24, or 32 bytes "+ "to create an AES cipher when "+ "pass_access_token == true or "+ "cookie_refresh != 0, but is %d bytes.%s", - len(secretBytes(o.CookieSecret)), suffix)) + len(encryption.SecretBytes(o.Cookie.Secret)), suffix)) } else { var err error - cipher, err = encryption.NewCipher(secretBytes(o.CookieSecret)) + cipher, err = encryption.NewCipher(encryption.SecretBytes(o.Cookie.Secret)) 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) + o.Session.Cipher = cipher + sessionStore, err := sessions.NewSessionStore(&o.Session, &o.Cookie) if err != nil { msgs = append(msgs, fmt.Sprintf("error initialising session storage: %v", err)) } else { o.sessionStore = sessionStore } - if o.CookieRefresh >= o.CookieExpire { + if o.Cookie.Refresh >= o.Cookie.Expire { msgs = append(msgs, fmt.Sprintf( "cookie_refresh (%s) must be less than "+ "cookie_expire (%s)", - o.CookieRefresh.String(), - o.CookieExpire.String())) + o.Cookie.Refresh.String(), + o.Cookie.Expire.String())) } if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" { @@ -383,18 +442,31 @@ func (o *Options) Validate() error { } } - switch o.CookieSameSite { + switch o.Cookie.SameSite { case "", "none", "lax", "strict": default: - msgs = append(msgs, fmt.Sprintf("cookie_samesite (%s) must be one of ['', 'lax', 'strict', 'none']", o.CookieSameSite)) + msgs = append(msgs, fmt.Sprintf("cookie_samesite (%s) must be one of ['', 'lax', 'strict', 'none']", o.Cookie.SameSite)) } + // Sort cookie domains by length, so that we try longer (and more specific) + // domains first + sort.Slice(o.Cookie.Domains, func(i, j int) bool { + return len(o.Cookie.Domains[i]) > len(o.Cookie.Domains[j]) + }) + msgs = parseSignatureKey(o, msgs) msgs = validateCookieName(o, msgs) msgs = setupLogger(o, msgs) + if o.ReverseProxy { + o.realClientIPParser, err = getRealClientIPParser(o.RealClientIPHeader) + if err != nil { + msgs = append(msgs, fmt.Sprintf("real_client_ip_header (%s) not accepted parameter value: %v", o.RealClientIPHeader, err)) + } + } + if len(msgs) != 0 { - return fmt.Errorf("Invalid configuration:\n %s", + return fmt.Errorf("invalid configuration:\n %s", strings.Join(msgs, "\n ")) } return nil @@ -406,7 +478,9 @@ func parseProviderInfo(o *Options, msgs []string) []string { ClientID: o.ClientID, ClientSecret: o.ClientSecret, ClientSecretFile: o.ClientSecretFile, + Prompt: o.Prompt, ApprovalPrompt: o.ApprovalPrompt, + AcrValues: o.AcrValues, } p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs) p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs) @@ -420,6 +494,7 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.Configure(o.AzureTenant) case *providers.GitHubProvider: p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) + p.SetRepo(o.GitHubRepo, o.GitHubToken) case *providers.KeycloakProvider: p.SetGroup(o.KeycloakGroup) case *providers.GoogleProvider: @@ -436,6 +511,7 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.SetRepository(o.BitbucketRepository) case *providers.OIDCProvider: p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail + p.UserIDClaim = o.UserIDClaim if o.oidcVerifier == nil { msgs = append(msgs, "oidc provider requires an oidc issuer URL") } else { @@ -467,7 +543,6 @@ func parseProviderInfo(o *Options, msgs []string) []string { case *providers.AuthSCHProvider: p.Group = o.AuthSCHGroup case *providers.LoginGovProvider: - p.AcrValues = o.AcrValues p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs) // JWT key can be supplied via env variable or file in the filesystem, but not both. @@ -526,7 +601,7 @@ func parseSignatureKey(o *Options, msgs []string) []string { // 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 + parsedIssuers := make([]jwtIssuer, 0, len(issuers)) for _, jwtVerifier := range issuers { components := strings.Split(jwtVerifier, "=") if len(components) < 2 { @@ -563,36 +638,13 @@ func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error } func validateCookieName(o *Options, msgs []string) []string { - cookie := &http.Cookie{Name: o.CookieName} + cookie := &http.Cookie{Name: o.Cookie.Name} if cookie.String() == "" { - return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.CookieName)) + return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.Cookie.Name)) } return msgs } -func addPadding(secret string) string { - padding := len(secret) % 4 - switch padding { - case 1: - return secret + "===" - case 2: - return secret + "==" - case 3: - return secret + "=" - default: - return secret - } -} - -// secretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary -func secretBytes(secret string) []byte { - b, err := base64.URLEncoding.DecodeString(addPadding(secret)) - if err == nil { - return []byte(addPadding(string(b))) - } - return []byte(secret) -} - func setupLogger(o *Options, msgs []string) []string { // Setup the log file if len(o.LoggingFilename) > 0 { @@ -631,7 +683,9 @@ func setupLogger(o *Options, msgs []string) []string { logger.SetStandardTemplate(o.StandardLoggingFormat) logger.SetAuthTemplate(o.AuthLoggingFormat) logger.SetReqTemplate(o.RequestLoggingFormat) - logger.SetReverseProxy(o.ReverseProxy) + logger.SetGetClientFunc(func(r *http.Request) string { + return getClientString(o.realClientIPParser, r, false) + }) excludePaths := make([]string, 0) excludePaths = append(excludePaths, strings.Split(o.ExcludeLoggingPaths, ",")...) diff --git a/options_test.go b/options_test.go index 996acaea68e57fba522f99ca5520380af47f91d2..c6c2d3b55c156b73cd828c4504a469253c6e2376 100644 --- a/options_test.go +++ b/options_test.go @@ -13,19 +13,25 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + cookieSecret = "foobar" + clientID = "bazquux" + clientSecret = "xyzzyplugh" +) + func testOptions() *Options { o := NewOptions() o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8080/") - o.CookieSecret = "foobar" - o.ClientID = "bazquux" - o.ClientSecret = "xyzzyplugh" + o.Cookie.Secret = cookieSecret + o.ClientID = clientID + o.ClientSecret = clientSecret o.EmailDomains = []string{"*"} return o } func errorMsg(msgs []string) string { result := make([]string, 0) - result = append(result, "Invalid configuration:") + result = append(result, "invalid configuration:") result = append(result, msgs...) return strings.Join(result, "\n ") } @@ -45,15 +51,15 @@ func TestNewOptions(t *testing.T) { func TestClientSecretFileOptionFails(t *testing.T) { o := NewOptions() - o.CookieSecret = "foobar" - o.ClientID = "bazquux" - o.ClientSecretFile = "xyzzyplugh" + o.Cookie.Secret = cookieSecret + o.ClientID = clientID + o.ClientSecretFile = clientSecret o.EmailDomains = []string{"*"} err := o.Validate() assert.NotEqual(t, nil, err) p := o.provider.Data() - assert.Equal(t, "xyzzyplugh", p.ClientSecretFile) + assert.Equal(t, clientSecret, p.ClientSecretFile) assert.Equal(t, "", p.ClientSecret) s, err := p.GetClientSecret() @@ -75,8 +81,8 @@ func TestClientSecretFileOption(t *testing.T) { defer os.Remove(clientSecretFileName) o := NewOptions() - o.CookieSecret = "foobar" - o.ClientID = "bazquux" + o.Cookie.Secret = cookieSecret + o.ClientID = clientID o.ClientSecretFile = clientSecretFileName o.EmailDomains = []string{"*"} err = o.Validate() @@ -150,11 +156,7 @@ func TestProxyURLsError(t *testing.T) { o.Upstreams = append(o.Upstreams, "127.0.0.1:8081") err := o.Validate() assert.NotEqual(t, nil, err) - - expected := errorMsg([]string{ - "error parsing upstream: parse 127.0.0.1:8081: " + - "first path segment in URL cannot contain colon"}) - assert.Equal(t, expected, err.Error()) + assert.Contains(t, err.Error(), "error parsing upstream") } func TestCompiledRegex(t *testing.T) { @@ -163,7 +165,7 @@ func TestCompiledRegex(t *testing.T) { o.SkipAuthRegex = regexps assert.Equal(t, nil, o.Validate()) actual := make([]string, 0) - for _, regex := range o.CompiledRegex { + for _, regex := range o.compiledRegex { actual = append(actual, regex.String()) } assert.Equal(t, regexps, actual) @@ -210,20 +212,20 @@ func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) { assert.Equal(t, false, o.PassAccessToken) o.PassAccessToken = true - o.CookieSecret = "cookie of invalid length-" + o.Cookie.Secret = "cookie of invalid length-" assert.NotEqual(t, nil, o.Validate()) o.PassAccessToken = false - o.CookieRefresh = time.Duration(24) * time.Hour + o.Cookie.Refresh = time.Duration(24) * time.Hour assert.NotEqual(t, nil, o.Validate()) - o.CookieSecret = "16 bytes AES-128" + o.Cookie.Secret = "16 bytes AES-128" assert.Equal(t, nil, o.Validate()) - o.CookieSecret = "24 byte secret AES-192--" + o.Cookie.Secret = "24 byte secret AES-192--" assert.Equal(t, nil, o.Validate()) - o.CookieSecret = "32 byte secret for AES-256------" + o.Cookie.Secret = "32 byte secret for AES-256------" assert.Equal(t, nil, o.Validate()) } @@ -231,11 +233,11 @@ func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) { o := testOptions() assert.Equal(t, nil, o.Validate()) - o.CookieSecret = "0123456789abcdefabcd" - o.CookieRefresh = o.CookieExpire + o.Cookie.Secret = "0123456789abcdef" + o.Cookie.Refresh = o.Cookie.Expire assert.NotEqual(t, nil, o.Validate()) - o.CookieRefresh -= time.Duration(1) + o.Cookie.Refresh -= time.Duration(1) assert.Equal(t, nil, o.Validate()) } @@ -244,23 +246,23 @@ func TestBase64CookieSecret(t *testing.T) { assert.Equal(t, nil, o.Validate()) // 32 byte, base64 (urlsafe) encoded key - o.CookieSecret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ=" + o.Cookie.Secret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ=" assert.Equal(t, nil, o.Validate()) // 32 byte, base64 (urlsafe) encoded key, w/o padding - o.CookieSecret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ" + o.Cookie.Secret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ" assert.Equal(t, nil, o.Validate()) // 24 byte, base64 (urlsafe) encoded key - o.CookieSecret = "Kp33Gj-GQmYtz4zZUyUDdqQKx5_Hgkv3" + o.Cookie.Secret = "Kp33Gj-GQmYtz4zZUyUDdqQKx5_Hgkv3" assert.Equal(t, nil, o.Validate()) // 16 byte, base64 (urlsafe) encoded key - o.CookieSecret = "LFEqZYvYUwKwzn0tEuTpLA==" + o.Cookie.Secret = "LFEqZYvYUwKwzn0tEuTpLA==" assert.Equal(t, nil, o.Validate()) // 16 byte, base64 (urlsafe) encoded key, w/o padding - o.CookieSecret = "LFEqZYvYUwKwzn0tEuTpLA" + o.Cookie.Secret = "LFEqZYvYUwKwzn0tEuTpLA" assert.Equal(t, nil, o.Validate()) } @@ -276,7 +278,7 @@ func TestValidateSignatureKeyInvalidSpec(t *testing.T) { o := testOptions() o.SignatureKey = "invalid spec" err := o.Validate() - assert.Equal(t, err.Error(), "Invalid configuration:\n"+ + assert.Equal(t, err.Error(), "invalid configuration:\n"+ " invalid signature hash:key spec: "+o.SignatureKey) } @@ -284,22 +286,22 @@ func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) { o := testOptions() o.SignatureKey = "unsupported:default secret" err := o.Validate() - assert.Equal(t, err.Error(), "Invalid configuration:\n"+ + assert.Equal(t, err.Error(), "invalid configuration:\n"+ " unsupported signature hash algorithm: "+o.SignatureKey) } func TestValidateCookie(t *testing.T) { o := testOptions() - o.CookieName = "_valid_cookie_name" + o.Cookie.Name = "_valid_cookie_name" assert.Equal(t, nil, o.Validate()) } func TestValidateCookieBadName(t *testing.T) { o := testOptions() - o.CookieName = "_bad_cookie_name{}" + o.Cookie.Name = "_bad_cookie_name{}" err := o.Validate() - assert.Equal(t, err.Error(), "Invalid configuration:\n"+ - fmt.Sprintf(" invalid cookie name: %q", o.CookieName)) + assert.Equal(t, err.Error(), "invalid configuration:\n"+ + fmt.Sprintf(" invalid cookie name: %q", o.Cookie.Name)) } func TestSkipOIDCDiscovery(t *testing.T) { @@ -309,8 +311,8 @@ func TestSkipOIDCDiscovery(t *testing.T) { o.SkipOIDCDiscovery = true err := o.Validate() - assert.Equal(t, "Invalid configuration:\n"+ - fmt.Sprintf(" missing setting: login-url\n missing setting: redeem-url\n missing setting: oidc-jwks-url"), err.Error()) + assert.Equal(t, "invalid configuration:\n"+ + " missing setting: login-url\n missing setting: redeem-url\n missing setting: oidc-jwks-url", err.Error()) o.LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in" o.RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in" @@ -324,3 +326,46 @@ func TestGCPHealthcheck(t *testing.T) { o.GCPHealthChecks = true assert.Equal(t, nil, o.Validate()) } + +func TestRealClientIPHeader(t *testing.T) { + var o *Options + var err error + var expected string + + // Ensure nil if ReverseProxy not set. + o = testOptions() + o.RealClientIPHeader = "X-Real-IP" + assert.Equal(t, nil, o.Validate()) + assert.Nil(t, o.realClientIPParser) + + // Ensure simple use case works. + o = testOptions() + o.ReverseProxy = true + o.RealClientIPHeader = "X-Forwarded-For" + assert.Equal(t, nil, o.Validate()) + assert.NotNil(t, o.realClientIPParser) + + // Ensure unknown header format process an error. + o = testOptions() + o.ReverseProxy = true + o.RealClientIPHeader = "Forwarded" + err = o.Validate() + assert.NotEqual(t, nil, err) + expected = errorMsg([]string{ + "real_client_ip_header (Forwarded) not accepted parameter value: the http header key (Forwarded) is either invalid or unsupported", + }) + assert.Equal(t, expected, err.Error()) + assert.Nil(t, o.realClientIPParser) + + // Ensure invalid header format produces an error. + o = testOptions() + o.ReverseProxy = true + o.RealClientIPHeader = "!934invalidheader-23:" + err = o.Validate() + assert.NotEqual(t, nil, err) + expected = errorMsg([]string{ + "real_client_ip_header (!934invalidheader-23:) not accepted parameter value: the http header key (!934invalidheader-23:) is either invalid or unsupported", + }) + assert.Equal(t, expected, err.Error()) + assert.Nil(t, o.realClientIPParser) +} diff --git a/pkg/apis/options/cookie.go b/pkg/apis/options/cookie.go index dcb6c75a64529bdb7e6b5017fa0eec4068578f93..2cb77eb61afffd80fb1269660201f6f6d7ff4f6f 100644 --- a/pkg/apis/options/cookie.go +++ b/pkg/apis/options/cookie.go @@ -4,13 +4,13 @@ import "time" // CookieOptions contains configuration options relating to Cookie configuration type CookieOptions struct { - CookieName string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"` - CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"` - CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"` - CookiePath string `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"` - CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"` - CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"` - CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"` - CookieHTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"` - CookieSameSite string `flag:"cookie-samesite" cfg:"cookie_samesite" env:"OAUTH2_PROXY_COOKIE_SAMESITE"` + Name string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"` + Secret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"` + Domains []string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"` + Path string `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"` + Expire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"` + Refresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"` + Secure bool `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"` + HTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"` + SameSite string `flag:"cookie-samesite" cfg:"cookie_samesite" env:"OAUTH2_PROXY_COOKIE_SAMESITE"` } diff --git a/pkg/apis/options/load.go b/pkg/apis/options/load.go new file mode 100644 index 0000000000000000000000000000000000000000..aeb39b9ae0b5fd0216894ceaa581129be1471825 --- /dev/null +++ b/pkg/apis/options/load.go @@ -0,0 +1,134 @@ +package options + +import ( + "fmt" + "reflect" + "strings" + + "github.com/mitchellh/mapstructure" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +// Load reads in the config file at the path given, then merges in environment +// variables (prefixed with `OAUTH2_PROXY`) and finally merges in flags from the flagSet. +// If a config value is unset and the flag has a non-zero value default, this default will be used. +// Eg. A field defined: +// FooBar `cfg:"foo_bar" flag:"foo-bar"` +// Can be set in the config file as `foo_bar="baz"`, in the environment as `OAUTH2_PROXY_FOO_BAR=baz`, +// or via the command line flag `--foo-bar=baz`. +func Load(configFileName string, flagSet *pflag.FlagSet, into interface{}) error { + v := viper.New() + v.SetConfigFile(configFileName) + v.SetConfigType("toml") // Config is in toml format + v.SetEnvPrefix("OAUTH2_PROXY") + v.AutomaticEnv() + v.SetTypeByDefaultValue(true) + + if configFileName != "" { + err := v.ReadInConfig() + if err != nil { + return fmt.Errorf("unable to load config file: %w", err) + } + } + + err := registerFlags(v, "", flagSet, into) + if err != nil { + // This should only happen if there is a programming error + return fmt.Errorf("unable to register flags: %w", err) + } + + // UnmarhsalExact will return an error if the config includes options that are + // not mapped to felds of the into struct + err = v.UnmarshalExact(into, decodeFromCfgTag) + if err != nil { + return fmt.Errorf("error unmarshalling config: %w", err) + } + + return nil +} + +// registerFlags uses `cfg` and `flag` tags to associate flags in the flagSet +// to the fields in the options interface provided. +// Each exported field in the options must have a `cfg` tag otherwise an error will occur. +// - For fields, set `cfg` and `flag` so that `flag` is the name of the flag associated to this config option +// - For exported fields that are not user facing, set the `cfg` to `,internal` +// - For structs containing user facing fields, set the `cfg` to `,squash` +func registerFlags(v *viper.Viper, prefix string, flagSet *pflag.FlagSet, options interface{}) error { + val := reflect.ValueOf(options) + var typ reflect.Type + if val.Kind() == reflect.Ptr { + typ = val.Elem().Type() + } else { + typ = val.Type() + } + + for i := 0; i < typ.NumField(); i++ { + // pull out the struct tags: + // flag - the name of the command line flag + // cfg - the name of the config file option + field := typ.Field(i) + fieldV := reflect.Indirect(val).Field(i) + fieldName := strings.Join([]string{prefix, field.Name}, ".") + + cfgName := field.Tag.Get("cfg") + if cfgName == ",internal" { + // Public but internal types that should not be exposed to users, skip them + continue + } + + if isUnexported(field.Name) { + // Unexported fields cannot be set by a user, so won't have tags or flags, skip them + continue + } + + if field.Type.Kind() == reflect.Struct { + if cfgName != ",squash" { + return fmt.Errorf("field %q does not have required cfg tag: `,squash`", fieldName) + } + err := registerFlags(v, fieldName, flagSet, fieldV.Interface()) + if err != nil { + return err + } + continue + } + + flagName := field.Tag.Get("flag") + if flagName == "" || cfgName == "" { + return fmt.Errorf("field %q does not have required tags (cfg, flag)", fieldName) + } + + if flagSet == nil { + return fmt.Errorf("flagset cannot be nil") + } + + f := flagSet.Lookup(flagName) + if f == nil { + return fmt.Errorf("field %q does not have a registered flag", flagName) + } + err := v.BindPFlag(cfgName, f) + if err != nil { + return fmt.Errorf("error binding flag for field %q: %w", fieldName, err) + } + } + + return nil +} + +// decodeFromCfgTag sets the Viper decoder to read the names from the `cfg` tag +// on each struct entry. +func decodeFromCfgTag(c *mapstructure.DecoderConfig) { + c.TagName = "cfg" +} + +// isUnexported checks if a field name starts with a lowercase letter and therefore +// if it is unexported. +func isUnexported(name string) bool { + if len(name) == 0 { + // This should never happen + panic("field name has len 0") + } + + first := string(name[0]) + return first == strings.ToLower(first) +} diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go new file mode 100644 index 0000000000000000000000000000000000000000..492680187bc8480054fc5de1de26b9a7ac29cfc2 --- /dev/null +++ b/pkg/apis/options/load_test.go @@ -0,0 +1,300 @@ +package options + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + "github.com/spf13/pflag" +) + +func TestOptionsSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Options Suite") +} + +var _ = Describe("Load", func() { + Context("with a testOptions structure", func() { + type TestOptionSubStruct struct { + StringSliceOption []string `flag:"string-slice-option" cfg:"string_slice_option"` + } + + type TestOptions struct { + StringOption string `flag:"string-option" cfg:"string_option"` + Sub TestOptionSubStruct `cfg:",squash"` + // Check exported but internal fields do not break loading + Internal *string `cfg:",internal"` + // Check unexported fields do not break loading + unexported string + } + + type MissingSquashTestOptions struct { + StringOption string `flag:"string-option" cfg:"string_option"` + Sub TestOptionSubStruct + } + + type MissingCfgTestOptions struct { + StringOption string `flag:"string-option"` + Sub TestOptionSubStruct `cfg:",squash"` + } + + type MissingFlagTestOptions struct { + StringOption string `cfg:"string_option"` + Sub TestOptionSubStruct `cfg:",squash"` + } + + var testOptionsConfigBytes = []byte(` + string_option="foo" + string_slice_option="a,b,c,d" + `) + + var testOptionsFlagSet *pflag.FlagSet + + type testOptionsTableInput struct { + env map[string]string + args []string + configFile []byte + flagSet func() *pflag.FlagSet + expectedErr error + input interface{} + expectedOutput interface{} + } + + BeforeEach(func() { + testOptionsFlagSet = pflag.NewFlagSet("testFlagSet", pflag.ExitOnError) + testOptionsFlagSet.String("string-option", "default", "") + testOptionsFlagSet.StringSlice("string-slice-option", []string{"a", "b"}, "") + }) + + DescribeTable("Load", + func(o *testOptionsTableInput) { + var configFileName string + + if o.configFile != nil { + By("Creating a config file") + configFile, err := ioutil.TempFile("", "oauth2-proxy-test-legacy-config-file") + Expect(err).ToNot(HaveOccurred()) + defer configFile.Close() + + _, err = configFile.Write(o.configFile) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(configFile.Name()) + + configFileName = configFile.Name() + } + + if len(o.env) > 0 { + By("Setting environment variables") + for k, v := range o.env { + os.Setenv(k, v) + defer os.Unsetenv(k) + } + } + + Expect(o.flagSet).ToNot(BeNil()) + flagSet := o.flagSet() + Expect(flagSet).ToNot(BeNil()) + + if len(o.args) > 0 { + By("Parsing flag arguments") + Expect(flagSet.Parse(o.args)).To(Succeed()) + } + + var input interface{} + if o.input != nil { + input = o.input + } else { + input = &TestOptions{} + } + err := Load(configFileName, flagSet, input) + if o.expectedErr != nil { + Expect(err).To(MatchError(o.expectedErr.Error())) + } else { + Expect(err).ToNot(HaveOccurred()) + } + Expect(input).To(Equal(o.expectedOutput)) + }, + Entry("with just a config file", &testOptionsTableInput{ + configFile: testOptionsConfigBytes, + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedOutput: &TestOptions{ + StringOption: "foo", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b", "c", "d"}, + }, + }, + }), + Entry("when setting env variables", &testOptionsTableInput{ + configFile: testOptionsConfigBytes, + env: map[string]string{ + "OAUTH2_PROXY_STRING_OPTION": "bar", + "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", + }, + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedOutput: &TestOptions{ + StringOption: "bar", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b", "c"}, + }, + }, + }), + Entry("when setting flags", &testOptionsTableInput{ + configFile: testOptionsConfigBytes, + env: map[string]string{ + "OAUTH2_PROXY_STRING_OPTION": "bar", + "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", + }, + args: []string{ + "--string-option", "baz", + "--string-slice-option", "a,b,c,d,e", + }, + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedOutput: &TestOptions{ + StringOption: "baz", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b", "c", "d", "e"}, + }, + }, + }), + Entry("when setting flags multiple times", &testOptionsTableInput{ + configFile: testOptionsConfigBytes, + env: map[string]string{ + "OAUTH2_PROXY_STRING_OPTION": "bar", + "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", + }, + args: []string{ + "--string-option", "baz", + "--string-slice-option", "x", + "--string-slice-option", "y", + "--string-slice-option", "z", + }, + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedOutput: &TestOptions{ + StringOption: "baz", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"x", "y", "z"}, + }, + }, + }), + Entry("when setting env variables without a config file", &testOptionsTableInput{ + env: map[string]string{ + "OAUTH2_PROXY_STRING_OPTION": "bar", + "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", + }, + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedOutput: &TestOptions{ + StringOption: "bar", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b", "c"}, + }, + }, + }), + Entry("when setting flags without a config file", &testOptionsTableInput{ + env: map[string]string{ + "OAUTH2_PROXY_STRING_OPTION": "bar", + "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", + }, + args: []string{ + "--string-option", "baz", + "--string-slice-option", "a,b,c,d,e", + }, + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedOutput: &TestOptions{ + StringOption: "baz", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b", "c", "d", "e"}, + }, + }, + }), + Entry("when setting flags without a config file", &testOptionsTableInput{ + env: map[string]string{ + "OAUTH2_PROXY_STRING_OPTION": "bar", + "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", + }, + args: []string{ + "--string-option", "baz", + "--string-slice-option", "a,b,c,d,e", + }, + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedOutput: &TestOptions{ + StringOption: "baz", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b", "c", "d", "e"}, + }, + }, + }), + Entry("when nothing is set it should use flag defaults", &testOptionsTableInput{ + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedOutput: &TestOptions{ + StringOption: "default", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b"}, + }, + }, + }), + Entry("with an invalid config file", &testOptionsTableInput{ + configFile: []byte(`slice_option = foo`), + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedErr: fmt.Errorf("unable to load config file: While parsing config: (1, 16): never reached"), + expectedOutput: &TestOptions{}, + }), + Entry("with an invalid flagset", &testOptionsTableInput{ + flagSet: func() *pflag.FlagSet { + // Missing a flag + f := pflag.NewFlagSet("testFlagSet", pflag.ExitOnError) + f.String("string-option", "default", "") + return f + }, + expectedErr: fmt.Errorf("unable to register flags: field \"string-slice-option\" does not have a registered flag"), + expectedOutput: &TestOptions{}, + }), + Entry("with an struct is missing the squash tag", &testOptionsTableInput{ + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedErr: fmt.Errorf("unable to register flags: field \".Sub\" does not have required cfg tag: `,squash`"), + input: &MissingSquashTestOptions{}, + expectedOutput: &MissingSquashTestOptions{}, + }), + Entry("with a field is missing the cfg tag", &testOptionsTableInput{ + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedErr: fmt.Errorf("unable to register flags: field \".StringOption\" does not have required tags (cfg, flag)"), + input: &MissingCfgTestOptions{}, + expectedOutput: &MissingCfgTestOptions{}, + }), + Entry("with a field is missing the flag tag", &testOptionsTableInput{ + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedErr: fmt.Errorf("unable to register flags: field \".StringOption\" does not have required tags (cfg, flag)"), + input: &MissingFlagTestOptions{}, + expectedOutput: &MissingFlagTestOptions{}, + }), + Entry("with existing unexported fields", &testOptionsTableInput{ + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + input: &TestOptions{ + unexported: "unexported", + }, + expectedOutput: &TestOptions{ + StringOption: "default", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b"}, + }, + unexported: "unexported", + }, + }), + Entry("with an unknown option in the config file", &testOptionsTableInput{ + configFile: []byte(`unknown_option="foo"`), + flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, + expectedErr: fmt.Errorf("error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_option"), + // Viper will unmarshal before returning the error, so this is the default output + expectedOutput: &TestOptions{ + StringOption: "default", + Sub: TestOptionSubStruct{ + StringSliceOption: []string{"a", "b"}, + }, + }, + }), + ) + }) +}) diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go index 9a2abd77deed1c919e6dd631f41507e9546ba7dc..fd69d6095932a4accfb563071d9bc85f86db7dab 100644 --- a/pkg/apis/options/sessions.go +++ b/pkg/apis/options/sessions.go @@ -1,34 +1,30 @@ package options -import "github.com/pusher/oauth2_proxy/pkg/encryption" +import "github.com/oauth2-proxy/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 + Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"` + Cipher *encryption.Cipher `cfg:",internal"` + Redis RedisStoreOptions `cfg:",squash"` } // CookieSessionStoreType is used to indicate the CookieSessionStore should be // used for storing sessions. var CookieSessionStoreType = "cookie" -// CookieStoreOptions contains configuration options for the CookieSessionStore. -type CookieStoreOptions struct{} - // RedisSessionStoreType is used to indicate the RedisSessionStore should be // used for storing sessions. var RedisSessionStoreType = "redis" // RedisStoreOptions contains configuration options for the RedisSessionStore. type RedisStoreOptions struct { - RedisConnectionURL string `flag:"redis-connection-url" cfg:"redis_connection_url" env:"OAUTH2_PROXY_REDIS_CONNECTION_URL"` + ConnectionURL 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"` UseCluster bool `flag:"redis-use-cluster" cfg:"redis_use_cluster" env:"OAUTH2_PROXY_REDIS_USE_CLUSTER"` ClusterConnectionURLs []string `flag:"redis-cluster-connection-urls" cfg:"redis_cluster_connection_urls" env:"OAUTH2_PROXY_REDIS_CLUSTER_CONNECTION_URLS"` - RedisCAPath string `flag:"redis-ca-path" cfg:"redis_ca_path" env:"OAUTH2_PROXY_REDIS_CA_PATH"` - RedisInsecureTLS bool `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify" env:"OAUTH2_PROXY_REDIS_INSECURE_SKIP_TLS_VERIFY"` + CAPath string `flag:"redis-ca-path" cfg:"redis_ca_path" env:"OAUTH2_PROXY_REDIS_CA_PATH"` + InsecureSkipTLSVerify bool `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify" env:"OAUTH2_PROXY_REDIS_INSECURE_SKIP_TLS_VERIFY"` } diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index 84c0dc908e505b434651bb362a0cc1479fd9d8d9..a09f01c132c3e3c6a7109e83cb5f29cf53aa9331 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -2,23 +2,23 @@ package sessions import ( "encoding/json" + "errors" "fmt" - "strconv" - "strings" "time" - "github.com/pusher/oauth2_proxy/pkg/encryption" + "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" ) // SessionState is used to store information about the currently authenticated user session type SessionState struct { - AccessToken string `json:",omitempty"` - IDToken string `json:",omitempty"` - CreatedAt time.Time `json:"-"` - ExpiresOn time.Time `json:"-"` - RefreshToken string `json:",omitempty"` - Email string `json:",omitempty"` - User string `json:",omitempty"` + AccessToken string `json:",omitempty"` + IDToken string `json:",omitempty"` + CreatedAt time.Time `json:"-"` + ExpiresOn time.Time `json:"-"` + RefreshToken string `json:",omitempty"` + Email string `json:",omitempty"` + User string `json:",omitempty"` + PreferredUsername string `json:",omitempty"` } // SessionStateJSON is used to encode SessionState into JSON without exposing time.Time zero value @@ -46,7 +46,7 @@ func (s *SessionState) Age() time.Duration { // 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) + o := fmt.Sprintf("Session{email:%s user:%s PreferredUsername:%s", s.Email, s.User, s.PreferredUsername) if s.AccessToken != "" { o += " token:true" } @@ -72,6 +72,7 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) // Store only Email and User when cipher is unavailable ss.Email = s.Email ss.User = s.User + ss.PreferredUsername = s.PreferredUsername } else { ss = *s var err error @@ -87,6 +88,12 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) return "", err } } + if ss.PreferredUsername != "" { + ss.PreferredUsername, err = c.Encrypt(ss.PreferredUsername) + if err != nil { + return "", err + } + } if ss.AccessToken != "" { ss.AccessToken, err = c.Encrypt(ss.AccessToken) if err != nil { @@ -118,89 +125,33 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) return string(b), err } -// legacyDecodeSessionStatePlain decodes older plain session state string -func legacyDecodeSessionStatePlain(v string) (*SessionState, error) { - chunks := strings.Split(v, " ") - if len(chunks) != 2 { - return nil, fmt.Errorf("invalid session state (legacy: expected 2 chunks for user/email got %d)", len(chunks)) - } - - user := strings.TrimPrefix(chunks[1], "user:") - email := strings.TrimPrefix(chunks[0], "email:") - - return &SessionState{User: user, Email: email}, nil -} - -// legacyDecodeSessionState attempts to decode the session state string -// generated by v3.1.0 or older -func legacyDecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { - chunks := strings.Split(v, "|") - - if c == nil { - if len(chunks) != 1 { - return nil, fmt.Errorf("invalid session state (legacy: expected 1 chunk for plain got %d)", len(chunks)) - } - return legacyDecodeSessionStatePlain(chunks[0]) - } - - if len(chunks) != 4 && len(chunks) != 5 { - return nil, fmt.Errorf("invalid session state (legacy: expected 4 or 5 chunks for full got %d)", len(chunks)) - } - - i := 0 - ss, err := legacyDecodeSessionStatePlain(chunks[i]) - if err != nil { - return nil, err - } - - i++ - ss.AccessToken = chunks[i] - - if len(chunks) == 5 { - // SessionState with IDToken in v3.1.0 - i++ - ss.IDToken = chunks[i] - } - - i++ - ts, err := strconv.Atoi(chunks[i]) - if err != nil { - return nil, fmt.Errorf("invalid session state (legacy: wrong expiration time: %s)", err) - } - ss.ExpiresOn = time.Unix(int64(ts), 0) - - i++ - ss.RefreshToken = chunks[i] - - return ss, nil -} - // DecodeSessionState decodes the session cookie string into a SessionState func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { var ssj SessionStateJSON var ss *SessionState err := json.Unmarshal([]byte(v), &ssj) - if err == nil && ssj.SessionState != nil { - // Extract SessionState and CreatedAt,ExpiresOn value from SessionStateJSON - ss = ssj.SessionState - if ssj.CreatedAt != nil { - ss.CreatedAt = *ssj.CreatedAt - } - if ssj.ExpiresOn != nil { - ss.ExpiresOn = *ssj.ExpiresOn - } - } else { - // Try to decode a legacy string when json.Unmarshal failed - ss, err = legacyDecodeSessionState(v, c) - if err != nil { - return nil, err - } + if err != nil { + return nil, fmt.Errorf("error unmarshalling session: %w", err) + } + if ssj.SessionState == nil { + return nil, errors.New("expected session state to not be nil") + } + + // Extract SessionState and CreatedAt,ExpiresOn value from SessionStateJSON + ss = ssj.SessionState + if ssj.CreatedAt != nil { + ss.CreatedAt = *ssj.CreatedAt + } + if ssj.ExpiresOn != nil { + ss.ExpiresOn = *ssj.ExpiresOn } + if c == nil { // Load only Email and User when cipher is unavailable ss = &SessionState{ - Email: ss.Email, - User: ss.User, + Email: ss.Email, + User: ss.User, + PreferredUsername: ss.PreferredUsername, } } else { // Backward compatibility with using unencrypted Email @@ -217,6 +168,12 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { ss.User = decryptedUser } } + if ss.PreferredUsername != "" { + ss.PreferredUsername, err = c.Decrypt(ss.PreferredUsername) + if err != nil { + return nil, err + } + } if ss.AccessToken != "" { ss.AccessToken, err = c.Decrypt(ss.AccessToken) if err != nil { @@ -236,8 +193,5 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { } } } - if ss.User == "" { - ss.User = ss.Email - } return ss, nil } diff --git a/pkg/apis/sessions/session_state_test.go b/pkg/apis/sessions/session_state_test.go index c8ccff10ae68bea57b648358963b0057da9d1ba8..94c624bfa2960300622e1f74fb0977039582aa53 100644 --- a/pkg/apis/sessions/session_state_test.go +++ b/pkg/apis/sessions/session_state_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/encryption" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" "github.com/stretchr/testify/assert" ) @@ -19,12 +19,13 @@ func TestSessionStateSerialization(t *testing.T) { c2, err := encryption.NewCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &sessions.SessionState{ - Email: "user@domain.com", - AccessToken: "token1234", - IDToken: "rawtoken1234", - CreatedAt: time.Now(), - ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), - RefreshToken: "refresh4321", + Email: "user@domain.com", + PreferredUsername: "user", + AccessToken: "token1234", + IDToken: "rawtoken1234", + CreatedAt: time.Now(), + ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), + RefreshToken: "refresh4321", } encoded, err := s.EncodeSessionState(c) assert.Equal(t, nil, err) @@ -32,8 +33,9 @@ func TestSessionStateSerialization(t *testing.T) { ss, err := sessions.DecodeSessionState(encoded, c) t.Logf("%#v", ss) assert.Equal(t, nil, err) - assert.Equal(t, "user@domain.com", ss.User) + assert.Equal(t, "", ss.User) assert.Equal(t, s.Email, ss.Email) + assert.Equal(t, s.PreferredUsername, ss.PreferredUsername) assert.Equal(t, s.AccessToken, ss.AccessToken) assert.Equal(t, s.IDToken, ss.IDToken) assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix()) @@ -46,6 +48,7 @@ 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.NotEqual(t, s.PreferredUsername, ss.PreferredUsername) 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) @@ -59,12 +62,13 @@ func TestSessionStateSerializationWithUser(t *testing.T) { c2, err := encryption.NewCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &sessions.SessionState{ - User: "just-user", - Email: "user@domain.com", - AccessToken: "token1234", - CreatedAt: time.Now(), - ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), - RefreshToken: "refresh4321", + User: "just-user", + PreferredUsername: "ju", + Email: "user@domain.com", + AccessToken: "token1234", + CreatedAt: time.Now(), + ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), + RefreshToken: "refresh4321", } encoded, err := s.EncodeSessionState(c) assert.Equal(t, nil, err) @@ -74,6 +78,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) { assert.Equal(t, nil, err) assert.Equal(t, s.User, ss.User) assert.Equal(t, s.Email, ss.Email) + assert.Equal(t, s.PreferredUsername, ss.PreferredUsername) 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()) @@ -85,6 +90,7 @@ 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.NotEqual(t, s.PreferredUsername, ss.PreferredUsername) 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) @@ -93,11 +99,12 @@ func TestSessionStateSerializationWithUser(t *testing.T) { 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", + Email: "user@domain.com", + PreferredUsername: "user", + AccessToken: "token1234", + CreatedAt: time.Now(), + ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), + RefreshToken: "refresh4321", } encoded, err := s.EncodeSessionState(nil) assert.Equal(t, nil, err) @@ -105,20 +112,22 @@ func TestSessionStateSerializationNoCipher(t *testing.T) { // only email should have been serialized ss, err := sessions.DecodeSessionState(encoded, nil) assert.Equal(t, nil, err) - assert.Equal(t, "user@domain.com", ss.User) + assert.Equal(t, "", ss.User) assert.Equal(t, s.Email, ss.Email) + assert.Equal(t, s.PreferredUsername, ss.PreferredUsername) assert.Equal(t, "", ss.AccessToken) assert.Equal(t, "", ss.RefreshToken) } func TestSessionStateSerializationNoCipherWithUser(t *testing.T) { s := &sessions.SessionState{ - User: "just-user", - Email: "user@domain.com", - AccessToken: "token1234", - CreatedAt: time.Now(), - ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), - RefreshToken: "refresh4321", + User: "just-user", + Email: "user@domain.com", + PreferredUsername: "user", + AccessToken: "token1234", + CreatedAt: time.Now(), + ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour), + RefreshToken: "refresh4321", } encoded, err := s.EncodeSessionState(nil) assert.Equal(t, nil, err) @@ -128,6 +137,7 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) { assert.Equal(t, nil, err) assert.Equal(t, s.User, ss.User) assert.Equal(t, s.Email, ss.Email) + assert.Equal(t, s.PreferredUsername, ss.PreferredUsername) assert.Equal(t, "", ss.AccessToken) assert.Equal(t, "", ss.RefreshToken) } @@ -201,7 +211,6 @@ func TestDecodeSessionState(t *testing.T) { e := time.Now().Add(time.Duration(1) * time.Hour) eJSON, _ := e.MarshalJSON() eString := string(eJSON) - eUnix := e.Unix() c, err := encryption.NewCipher([]byte(secret)) assert.NoError(t, err) @@ -217,7 +226,7 @@ func TestDecodeSessionState(t *testing.T) { { SessionState: sessions.SessionState{ Email: "user@domain.com", - User: "user@domain.com", + User: "", }, Encoded: `{"Email":"user@domain.com"}`, }, @@ -265,50 +274,6 @@ func TestDecodeSessionState(t *testing.T) { Cipher: c, Error: true, }, - { - SessionState: sessions.SessionState{ - User: "just-user", - Email: "user@domain.com", - }, - Encoded: "email:user@domain.com user:just-user", - }, - { - Encoded: "email:user@domain.com user:just-user||||", - Error: true, - }, - { - Encoded: "email:user@domain.com user:just-user", - Cipher: c, - Error: true, - }, - { - Encoded: "email:user@domain.com user:just-user|||99999999999999999999|", - Cipher: c, - Error: true, - }, - { - SessionState: sessions.SessionState{ - Email: "user@domain.com", - User: "just-user", - AccessToken: "token1234", - ExpiresOn: e, - RefreshToken: "refresh4321", - }, - Encoded: fmt.Sprintf("email:user@domain.com user:just-user|I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==|%d|qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K", eUnix), - Cipher: c, - }, - { - SessionState: sessions.SessionState{ - Email: "user@domain.com", - User: "just-user", - AccessToken: "token1234", - IDToken: "rawtoken1234", - ExpiresOn: e, - RefreshToken: "refresh4321", - }, - Encoded: fmt.Sprintf("email:user@domain.com user:just-user|I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==|xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==|%d|qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K", eUnix), - Cipher: c, - }, } for i, tc := range testCases { diff --git a/pkg/cookies/cookies.go b/pkg/cookies/cookies.go index b2a02a129a38a03f584fa0ce9920733ad57581f0..5235bb66ced784918a34415a4b61c1ee6fbed870 100644 --- a/pkg/cookies/cookies.go +++ b/pkg/cookies/cookies.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/pusher/oauth2_proxy/pkg/apis/options" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // MakeCookie constructs a cookie from the given parameters, @@ -38,8 +38,40 @@ func MakeCookie(req *http.Request, name string, value string, path string, domai // MakeCookieFromOptions constructs a cookie based on the given *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, ParseSameSite(opts.CookieSameSite)) +func MakeCookieFromOptions(req *http.Request, name string, value string, cookieOpts *options.CookieOptions, expiration time.Duration, now time.Time) *http.Cookie { + domain := GetCookieDomain(req, cookieOpts.Domains) + + if domain != "" { + return MakeCookie(req, name, value, cookieOpts.Path, domain, cookieOpts.HTTPOnly, cookieOpts.Secure, expiration, now, ParseSameSite(cookieOpts.SameSite)) + } + // If nothing matches, create the cookie with the shortest domain + logger.Printf("Warning: request host %q did not match any of the specific cookie domains of %q", GetRequestHost(req), strings.Join(cookieOpts.Domains, ",")) + defaultDomain := "" + if len(cookieOpts.Domains) > 0 { + defaultDomain = cookieOpts.Domains[len(cookieOpts.Domains)-1] + } + return MakeCookie(req, name, value, cookieOpts.Path, defaultDomain, cookieOpts.HTTPOnly, cookieOpts.Secure, expiration, now, ParseSameSite(cookieOpts.SameSite)) +} + +// GetCookieDomain returns the correct cookie domain given a list of domains +// by checking the X-Fowarded-Host and host header of an an http request +func GetCookieDomain(req *http.Request, cookieDomains []string) string { + host := GetRequestHost(req) + for _, domain := range cookieDomains { + if strings.HasSuffix(host, domain) { + return domain + } + } + return "" +} + +// GetRequestHost return the request host header or X-Forwarded-Host if present +func GetRequestHost(req *http.Request) string { + host := req.Header.Get("X-Forwarded-Host") + if host == "" { + host = req.Host + } + return host } // Parse a valid http.SameSite value from a user supplied string for use of making cookies. diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index c308330f92e233f3dc41d83c097eea8b20a67db1..2dcbee6bf205fb6d696c9fa71ed91d636bde9386 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -6,8 +6,10 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha1" + "crypto/sha256" "encoding/base64" "fmt" + "hash" "io" "net/http" "strconv" @@ -15,6 +17,24 @@ import ( "time" ) +// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary +func SecretBytes(secret string) []byte { + b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(secret, "=")) + if err == nil { + // Only return decoded form if a valid AES length + // Don't want unintentional decoding resulting in invalid lengths confusing a user + // that thought they used a 16, 24, 32 length string + for _, i := range []int{16, 24, 32} { + if len(b) == i { + return b + } + } + } + // If decoding didn't work or resulted in non-AES compliant length, + // assume the raw string was the intended secret + return []byte(secret) +} + // cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set. // additionally, the 'value' is encrypted so it's opaque to the browser @@ -25,8 +45,7 @@ func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value if len(parts) != 3 { return } - sig := cookieSignature(seed, cookie.Name, parts[0], parts[1]) - if checkHmac(parts[2], sig) { + if checkSignature(parts[2], seed, cookie.Name, parts[0], parts[1]) { ts, err := strconv.Atoi(parts[1]) if err != nil { return @@ -53,13 +72,13 @@ func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value func SignedValue(seed string, key string, value string, now time.Time) string { encodedValue := base64.URLEncoding.EncodeToString([]byte(value)) timeStr := fmt.Sprintf("%d", now.Unix()) - sig := cookieSignature(seed, key, encodedValue, timeStr) + sig := cookieSignature(sha256.New, seed, key, encodedValue, timeStr) cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig) return cookieVal } -func cookieSignature(args ...string) string { - h := hmac.New(sha1.New, []byte(args[0])) +func cookieSignature(signer func() hash.Hash, args ...string) string { + h := hmac.New(signer, []byte(args[0])) for _, arg := range args[1:] { h.Write([]byte(arg)) } @@ -68,6 +87,17 @@ func cookieSignature(args ...string) string { return base64.URLEncoding.EncodeToString(b) } +func checkSignature(signature string, args ...string) bool { + checkSig := cookieSignature(sha256.New, args...) + if checkHmac(signature, checkSig) { + return true + } + + // TODO: After appropriate rollout window, remove support for SHA1 + legacySig := cookieSignature(sha1.New, args...) + return checkHmac(signature, legacySig) +} + func checkHmac(input, expected string) bool { inputMAC, err1 := base64.URLEncoding.DecodeString(input) if err1 == nil { diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index fb6a4aa720a8e744d038c8530232c58f3145e896..76bfc1bc314bc3ad0ff3832e8b9beb0aab58c6a6 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -1,12 +1,104 @@ package encryption import ( + "crypto/rand" + "crypto/sha1" + "crypto/sha256" "encoding/base64" + "fmt" + "io" "testing" "github.com/stretchr/testify/assert" ) +func TestSecretBytesEncoded(t *testing.T) { + for _, secretSize := range []int{16, 24, 32} { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + // We test both padded & raw Base64 to ensure we handle both + // potential user input routes for Base64 + base64Padded := base64.URLEncoding.EncodeToString(secret) + sb := SecretBytes(base64Padded) + assert.Equal(t, secret, sb) + assert.Equal(t, len(sb), secretSize) + + base64Raw := base64.RawURLEncoding.EncodeToString(secret) + sb = SecretBytes(base64Raw) + assert.Equal(t, secret, sb) + assert.Equal(t, len(sb), secretSize) + }) + } +} + +// A string that isn't intended as Base64 and still decodes (but to unintended length) +// will return the original secret as bytes +func TestSecretBytesEncodedWrongSize(t *testing.T) { + for _, secretSize := range []int{15, 20, 28, 33, 44} { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + // We test both padded & raw Base64 to ensure we handle both + // potential user input routes for Base64 + base64Padded := base64.URLEncoding.EncodeToString(secret) + sb := SecretBytes(base64Padded) + assert.NotEqual(t, secret, sb) + assert.NotEqual(t, len(sb), secretSize) + // The given secret is returned as []byte + assert.Equal(t, base64Padded, string(sb)) + + base64Raw := base64.RawURLEncoding.EncodeToString(secret) + sb = SecretBytes(base64Raw) + assert.NotEqual(t, secret, sb) + assert.NotEqual(t, len(sb), secretSize) + // The given secret is returned as []byte + assert.Equal(t, base64Raw, string(sb)) + }) + } +} + +func TestSecretBytesNonBase64(t *testing.T) { + trailer := "equals==========" + assert.Equal(t, trailer, string(SecretBytes(trailer))) + + raw16 := "asdflkjhqwer)(*&" + sb16 := SecretBytes(raw16) + assert.Equal(t, raw16, string(sb16)) + assert.Equal(t, 16, len(sb16)) + + raw24 := "asdflkjhqwer)(*&CJEN#$%^" + sb24 := SecretBytes(raw24) + assert.Equal(t, raw24, string(sb24)) + assert.Equal(t, 24, len(sb24)) + + raw32 := "asdflkjhqwer)(*&1234lkjhqwer)(*&" + sb32 := SecretBytes(raw32) + assert.Equal(t, raw32, string(sb32)) + assert.Equal(t, 32, len(sb32)) +} + +func TestSignAndValidate(t *testing.T) { + seed := "0123456789abcdef" + key := "cookie-name" + value := base64.URLEncoding.EncodeToString([]byte("I am soooo encoded")) + epoch := "123456789" + + sha256sig := cookieSignature(sha256.New, seed, key, value, epoch) + sha1sig := cookieSignature(sha1.New, seed, key, value, epoch) + + assert.True(t, checkSignature(sha256sig, seed, key, value, epoch)) + // This should be switched to False after fully deprecating SHA1 + assert.True(t, checkSignature(sha1sig, seed, key, value, epoch)) + + assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch)) + assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch)) +} + func TestEncodeAndDecodeAccessToken(t *testing.T) { const secret = "0123456789abcdefghijklmnopqrstuv" const token = "my access token" diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 962a2c93b9545f8767a32b64b40337086304b10c..9bfc2b3a6a7351f43cf43edd1f828903f7073907 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -3,7 +3,6 @@ package logger import ( "fmt" "io" - "net" "net/http" "net/url" "os" @@ -76,6 +75,9 @@ type reqLogMessageData struct { Username string } +// Returns the apparent "real client IP" as a string. +type GetClientFunc = func(r *http.Request) string + // A Logger represents an active logging object that generates lines of // output to an io.Writer passed through a formatter. Each logging // operation makes a single call to the Writer's Write method. A Logger @@ -88,7 +90,7 @@ type Logger struct { stdEnabled bool authEnabled bool reqEnabled bool - reverseProxy bool + getClientFunc GetClientFunc excludePaths map[string]struct{} stdLogTemplate *template.Template authTemplate *template.Template @@ -103,7 +105,7 @@ func New(flag int) *Logger { stdEnabled: true, authEnabled: true, reqEnabled: true, - reverseProxy: false, + getClientFunc: func(r *http.Request) string { return r.RemoteAddr }, excludePaths: nil, stdLogTemplate: template.Must(template.New("std-log").Parse(DefaultStandardLoggingFormat)), authTemplate: template.Must(template.New("auth-log").Parse(DefaultAuthLoggingFormat)), @@ -139,10 +141,10 @@ func (l *Logger) Output(calldepth int, message string) { l.writer.Write([]byte("\n")) } -// PrintAuth writes auth info to the logger. Requires an http.Request to +// PrintAuthf writes auth info to the logger. Requires an http.Request to // log request details. Remaining arguments are handled in the manner of // fmt.Sprintf. Writes a final newline to the end of every message. -func (l *Logger) PrintAuth(username string, req *http.Request, status AuthStatus, format string, a ...interface{}) { +func (l *Logger) PrintAuthf(username string, req *http.Request, status AuthStatus, format string, a ...interface{}) { if !l.authEnabled { return } @@ -153,7 +155,7 @@ func (l *Logger) PrintAuth(username string, req *http.Request, status AuthStatus username = "-" } - client := GetClient(req, l.reverseProxy) + client := l.getClientFunc(req) l.mu.Lock() defer l.mu.Unlock() @@ -166,7 +168,7 @@ func (l *Logger) PrintAuth(username string, req *http.Request, status AuthStatus Timestamp: FormatTimestamp(now), UserAgent: fmt.Sprintf("%q", req.UserAgent()), Username: username, - Status: fmt.Sprintf("%s", status), + Status: string(status), Message: fmt.Sprintf(format, a...), }) @@ -185,7 +187,7 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url. return } - duration := float64(time.Now().Sub(ts)) / float64(time.Second) + duration := float64(time.Since(ts)) / float64(time.Second) if username == "" { username = "-" @@ -201,7 +203,7 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url. } } - client := GetClient(req, l.reverseProxy) + client := l.getClientFunc(req) l.mu.Lock() defer l.mu.Unlock() @@ -252,22 +254,6 @@ func (l *Logger) GetFileLineString(calldepth int) string { return fmt.Sprintf("%s:%d", file, line) } -// GetClient parses an HTTP request for the client/remote IP address. -func GetClient(req *http.Request, reverseProxy bool) string { - client := req.RemoteAddr - if reverseProxy { - if ip := req.Header.Get("X-Real-IP"); ip != "" { - client = ip - } - } - - if c, _, err := net.SplitHostPort(client); err == nil { - client = c - } - - return client -} - // FormatTimestamp returns a formatted timestamp. func (l *Logger) FormatTimestamp(ts time.Time) string { if l.flag&LUTC != 0 { @@ -312,11 +298,11 @@ func (l *Logger) SetReqEnabled(e bool) { l.reqEnabled = e } -// SetReverseProxy controls whether logging will trust headers that can be set by a reverse proxy. -func (l *Logger) SetReverseProxy(e bool) { +// SetGetClientFunc sets the function which determines the apparent "real client IP". +func (l *Logger) SetGetClientFunc(f GetClientFunc) { l.mu.Lock() defer l.mu.Unlock() - l.reverseProxy = e + l.getClientFunc = f } // SetExcludePaths sets the paths to exclude from logging. @@ -392,10 +378,10 @@ func SetReqEnabled(e bool) { std.SetReqEnabled(e) } -// SetReverseProxy controls whether logging will trust headers that can be set -// by a reverse proxy for the standard logger. -func SetReverseProxy(e bool) { - std.SetReverseProxy(e) +// SetGetClientFunc sets the function which determines the apparent IP address +// set by a reverse proxy for the standard logger. +func SetGetClientFunc(f GetClientFunc) { + std.SetGetClientFunc(f) } // SetExcludePaths sets the path to exclude from logging, eg: health checks @@ -481,7 +467,7 @@ func Panicln(v ...interface{}) { // PrintAuthf writes authentication details to the standard logger. // Arguments are handled in the manner of fmt.Printf. func PrintAuthf(username string, req *http.Request, status AuthStatus, format string, a ...interface{}) { - std.PrintAuth(username, req, status, format, a...) + std.PrintAuthf(username, req, status, format, a...) } // PrintReq writes request details to the standard logger. diff --git a/pkg/requests/requests.go b/pkg/requests/requests.go index 9083b2d4d273ca328b7fa2bc781e8e2a8a2b8b70..64cacaa934d2a68d2de8e323e703b861dc1cdd66 100644 --- a/pkg/requests/requests.go +++ b/pkg/requests/requests.go @@ -1,13 +1,14 @@ package requests import ( + "context" "encoding/json" "fmt" "io/ioutil" "net/http" "github.com/bitly/go-simplejson" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // Request parses the request body into a simplejson.Json object @@ -62,8 +63,8 @@ func RequestJSON(req *http.Request, v interface{}) error { } // RequestUnparsedResponse performs a GET and returns the raw response object -func RequestUnparsedResponse(url string, header http.Header) (resp *http.Response, err error) { - req, err := http.NewRequest("GET", url, nil) +func RequestUnparsedResponse(ctx context.Context, url string, header http.Header) (resp *http.Response, err error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("error performing get request: %w", err) } diff --git a/pkg/requests/requests_test.go b/pkg/requests/requests_test.go index c9ec4e88d6b4dd90c54036c1875fecd38a05fe19..0c3e4152da9d2ec64dc0504d4395e747c4d7e190 100644 --- a/pkg/requests/requests_test.go +++ b/pkg/requests/requests_test.go @@ -1,6 +1,7 @@ package requests import ( + "context" "io/ioutil" "net/http" "net/http/httptest" @@ -87,10 +88,10 @@ func TestRequestUnparsedResponseUsingAccessTokenParameter(t *testing.T) { defer backend.Close() response, err := RequestUnparsedResponse( - backend.URL+"?access_token=my_token", nil) + context.Background(), backend.URL+"?access_token=my_token", nil) + assert.Equal(t, nil, err) defer response.Body.Close() - assert.Equal(t, nil, err) assert.Equal(t, 200, response.StatusCode) body, err := ioutil.ReadAll(response.Body) assert.Equal(t, nil, err) @@ -103,7 +104,7 @@ func TestRequestUnparsedResponseUsingAccessTokenParameterFailedResponse(t *testi backend.Close() response, err := RequestUnparsedResponse( - backend.URL+"?access_token=my_token", nil) + context.Background(), backend.URL+"?access_token=my_token", nil) assert.NotEqual(t, nil, err) assert.Equal(t, (*http.Response)(nil), response) } @@ -123,10 +124,10 @@ func TestRequestUnparsedResponseUsingHeaders(t *testing.T) { headers := make(http.Header) headers.Set("Auth", "my_token") - response, err := RequestUnparsedResponse(backend.URL, headers) + response, err := RequestUnparsedResponse(context.Background(), backend.URL, headers) + assert.Equal(t, nil, err) defer response.Body.Close() - assert.Equal(t, nil, err) assert.Equal(t, 200, response.StatusCode) body, err := ioutil.ReadAll(response.Body) assert.Equal(t, nil, err) diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go index ac9cfaf26e20da06342246e5c58dd7b81d6cc5a4..d97125c3e2eae81373c8ffec22a25763609b4d63 100644 --- a/pkg/sessions/cookie/session_store.go +++ b/pkg/sessions/cookie/session_store.go @@ -8,11 +8,11 @@ import ( "strings" "time" - "github.com/pusher/oauth2_proxy/pkg/apis/options" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/cookies" - "github.com/pusher/oauth2_proxy/pkg/encryption" - "github.com/pusher/oauth2_proxy/pkg/sessions/utils" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies" + "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) const ( @@ -37,7 +37,7 @@ func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessi if ss.CreatedAt.IsZero() { ss.CreatedAt = time.Now() } - value, err := utils.CookieForSession(ss, s.CookieCipher) + value, err := cookieForSession(ss, s.CookieCipher) if err != nil { return err } @@ -48,17 +48,17 @@ func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessi // 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.CookieOptions.Name) 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.CookieOptions.Name) } - val, _, ok := encryption.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire) + val, _, ok := encryption.Validate(c, s.CookieOptions.Secret, s.CookieOptions.Expire) if !ok { - return nil, errors.New("Cookie Signature not valid") + return nil, errors.New("cookie signature not valid") } - session, err := utils.SessionFromCookie(val, s.CookieCipher) + session, err := sessionFromCookie(val, s.CookieCipher) if err != nil { return nil, err } @@ -68,23 +68,30 @@ func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) { // Clear clears any saved session information by writing a cookie to // clear the session func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error { - var cookies []*http.Cookie - // matches CookieName, CookieName_<number> - var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieOptions.CookieName)) + var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieOptions.Name)) for _, c := range req.Cookies() { if cookieNameRegex.MatchString(c.Name) { clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1, time.Now()) http.SetCookie(rw, clearCookie) - cookies = append(cookies, clearCookie) } } return nil } +// cookieForSession serializes a session state for storage in a cookie +func cookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) { + return s.EncodeSessionState(c) +} + +// sessionFromCookie deserializes a session from a cookie value +func sessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) { + return sessions.DecodeSessionState(v, c) +} + // 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) { @@ -96,10 +103,10 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques // authentication details func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie { if value != "" { - value = encryption.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now) + value = encryption.SignedValue(s.CookieOptions.Secret, s.CookieOptions.Name, 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.CookieOptions.Name, value, s.CookieOptions.Expire, now) + if len(c.Value) > 4096-len(s.CookieOptions.Name) { return splitCookie(c) } return []*http.Cookie{c} @@ -129,6 +136,7 @@ func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.Coo // 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 { + logger.Printf("WARNING: Multiple cookies are required for this session as it exceeds the 4kb cookie limit. Please use server side session storage (eg. Redis) instead.") if len(c.Value) < maxCookieLength { return []*http.Cookie{c} } @@ -172,7 +180,7 @@ func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) { } } if len(cookies) == 0 { - return nil, fmt.Errorf("Could not find cookie %s", cookieName) + return nil, fmt.Errorf("could not find cookie %s", cookieName) } return joinCookies(cookies) } @@ -207,5 +215,6 @@ func copyCookie(c *http.Cookie) *http.Cookie { HttpOnly: c.HttpOnly, Raw: c.Raw, Unparsed: c.Unparsed, + SameSite: c.SameSite, } } diff --git a/pkg/sessions/cookie/session_store_test.go b/pkg/sessions/cookie/session_store_test.go new file mode 100644 index 0000000000000000000000000000000000000000..de8d1b57fd71ac6b06d43b49d42eb180aa0d9823 --- /dev/null +++ b/pkg/sessions/cookie/session_store_test.go @@ -0,0 +1,30 @@ +package cookie + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_copyCookie(t *testing.T) { + expire, _ := time.Parse(time.RFC3339, "2020-03-17T00:00:00Z") + c := &http.Cookie{ + Name: "name", + Value: "value", + Path: "/path", + Domain: "x.y.z", + Expires: expire, + RawExpires: "rawExpire", + MaxAge: 1, + Secure: true, + HttpOnly: true, + Raw: "raw", + Unparsed: []string{"unparsed"}, + SameSite: http.SameSiteLaxMode, + } + + got := copyCookie(c) + assert.Equal(t, c, got) +} diff --git a/pkg/sessions/redis/client.go b/pkg/sessions/redis/client.go new file mode 100644 index 0000000000000000000000000000000000000000..6037ce470d2e81d47566c740109fe340c09c0656 --- /dev/null +++ b/pkg/sessions/redis/client.go @@ -0,0 +1,59 @@ +package redis + +import ( + "context" + "time" + + "github.com/go-redis/redis/v7" +) + +// Client is wrapper interface for redis.Client and redis.ClusterClient. +type Client interface { + Get(ctx context.Context, key string) ([]byte, error) + Set(ctx context.Context, key string, value []byte, expiration time.Duration) error + Del(ctx context.Context, key string) error +} + +var _ Client = (*client)(nil) + +type client struct { + *redis.Client +} + +func newClient(c *redis.Client) Client { + return &client{Client: c} +} + +func (c *client) Get(ctx context.Context, key string) ([]byte, error) { + return c.WithContext(ctx).Get(key).Bytes() +} + +func (c *client) Set(ctx context.Context, key string, value []byte, expiration time.Duration) error { + return c.WithContext(ctx).Set(key, value, expiration).Err() +} + +func (c *client) Del(ctx context.Context, key string) error { + return c.WithContext(ctx).Del(key).Err() +} + +var _ Client = (*clusterClient)(nil) + +type clusterClient struct { + *redis.ClusterClient +} + +func newClusterClient(c *redis.ClusterClient) Client { + return &clusterClient{ClusterClient: c} +} + +func (c *clusterClient) Get(ctx context.Context, key string) ([]byte, error) { + return c.WithContext(ctx).Get(key).Bytes() +} + +func (c *clusterClient) Set(ctx context.Context, key string, value []byte, expiration time.Duration) error { + return c.WithContext(ctx).Set(key, value, expiration).Err() +} + +func (c *clusterClient) Del(ctx context.Context, key string) error { + return c.WithContext(ctx).Del(key).Err() +} diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go index 0d3d361ccd167f93935d5ba34bb0f5cd33389b50..7737b960b4289c8bfe39802a8326b8924fb99f95 100644 --- a/pkg/sessions/redis/redis_store.go +++ b/pkg/sessions/redis/redis_store.go @@ -1,6 +1,7 @@ package redis import ( + "context" "crypto/aes" "crypto/cipher" "crypto/rand" @@ -14,12 +15,12 @@ import ( "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" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/go-redis/redis/v7" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies" + "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // TicketData is a structure representing the ticket used in server session storage @@ -33,19 +34,19 @@ type TicketData struct { type SessionStore struct { CookieCipher *encryption.Cipher CookieOptions *options.CookieOptions - Cmdable redis.Cmdable + Client Client } // NewRedisSessionStore initialises a new instance of the SessionStore from // the configuration given func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) { - cmdable, err := newRedisCmdable(opts.RedisStoreOptions) + client, err := newRedisCmdable(opts.Redis) if err != nil { return nil, fmt.Errorf("error constructing redis client: %v", err) } rs := &SessionStore{ - Cmdable: cmdable, + Client: client, CookieCipher: opts.Cipher, CookieOptions: cookieOpts, } @@ -53,7 +54,7 @@ func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.Cook } -func newRedisCmdable(opts options.RedisStoreOptions) (redis.Cmdable, error) { +func newRedisCmdable(opts options.RedisStoreOptions) (Client, error) { if opts.UseSentinel && opts.UseCluster { return nil, fmt.Errorf("options redis-use-sentinel and redis-use-cluster are mutually exclusive") } @@ -63,26 +64,26 @@ func newRedisCmdable(opts options.RedisStoreOptions) (redis.Cmdable, error) { MasterName: opts.SentinelMasterName, SentinelAddrs: opts.SentinelConnectionURLs, }) - return client, nil + return newClient(client), nil } if opts.UseCluster { client := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: opts.ClusterConnectionURLs, }) - return client, nil + return newClusterClient(client), nil } - opt, err := redis.ParseURL(opts.RedisConnectionURL) + opt, err := redis.ParseURL(opts.ConnectionURL) if err != nil { return nil, fmt.Errorf("unable to parse redis url: %s", err) } - if opts.RedisInsecureTLS != false { + if opts.InsecureSkipTLSVerify { opt.TLSConfig.InsecureSkipVerify = true } - if opts.RedisCAPath != "" { + if opts.CAPath != "" { rootCAs, err := x509.SystemCertPool() if err != nil { logger.Printf("failed to load system cert pool for redis connection, falling back to empty cert pool") @@ -90,9 +91,9 @@ func newRedisCmdable(opts options.RedisStoreOptions) (redis.Cmdable, error) { if rootCAs == nil { rootCAs = x509.NewCertPool() } - certs, err := ioutil.ReadFile(opts.RedisCAPath) + certs, err := ioutil.ReadFile(opts.CAPath) if err != nil { - return nil, fmt.Errorf("failed to load %q, %v", opts.RedisCAPath, err) + return nil, fmt.Errorf("failed to load %q, %v", opts.CAPath, err) } // Append our cert to the system pool @@ -104,7 +105,7 @@ func newRedisCmdable(opts options.RedisStoreOptions) (redis.Cmdable, error) { } client := redis.NewClient(opt) - return client, nil + return newClient(client), nil } // Save takes a sessions.SessionState and stores the information from it @@ -116,12 +117,13 @@ func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *se // 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) + requestCookie, _ := req.Cookie(store.CookieOptions.Name) value, err := s.EncodeSessionState(store.CookieCipher) if err != nil { return err } - ticketString, err := store.storeValue(value, store.CookieOptions.CookieExpire, requestCookie) + ctx := req.Context() + ticketString, err := store.storeValue(ctx, value, store.CookieOptions.Expire, requestCookie) if err != nil { return err } @@ -129,7 +131,7 @@ func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *se ticketCookie := store.makeCookie( req, ticketString, - store.CookieOptions.CookieExpire, + store.CookieOptions.Expire, s.CreatedAt, ) @@ -140,16 +142,17 @@ func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *se // 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) + requestCookie, err := req.Cookie(store.CookieOptions.Name) if err != nil { return nil, fmt.Errorf("error loading session: %s", err) } - val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) + val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.Secret, store.CookieOptions.Expire) if !ok { - return nil, fmt.Errorf("Cookie Signature not valid") + return nil, fmt.Errorf("cookie signature not valid") } - session, err := store.loadSessionFromString(val) + ctx := req.Context() + session, err := store.loadSessionFromString(ctx, val) if err != nil { return nil, fmt.Errorf("error loading session: %s", err) } @@ -157,18 +160,17 @@ func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, erro } // 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) +func (store *SessionStore) loadSessionFromString(ctx context.Context, value string) (*sessions.SessionState, error) { + ticket, err := decodeTicket(store.CookieOptions.Name, value) if err != nil { return nil, err } - result, err := store.Cmdable.Get(ticket.asHandle(store.CookieOptions.CookieName)).Result() + resultBytes, err := store.Client.Get(ctx, ticket.asHandle(store.CookieOptions.Name)) if err != nil { return nil, err } - resultBytes := []byte(result) block, err := aes.NewCipher(ticket.Secret) if err != nil { return nil, err @@ -197,7 +199,7 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro http.SetCookie(rw, clearCookie) // If there was an existing cookie we should clear the session in redis - requestCookie, err := req.Cookie(store.CookieOptions.CookieName) + requestCookie, err := req.Cookie(store.CookieOptions.Name) if err != nil && err == http.ErrNoCookie { // No existing cookie so can't clear redis return nil @@ -205,16 +207,17 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro return fmt.Errorf("error retrieving cookie: %v", err) } - val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) + val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.Secret, store.CookieOptions.Expire) if !ok { - return fmt.Errorf("Cookie Signature not valid") + 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) + ticket, _ := decodeTicket(store.CookieOptions.Name, val) if ticket != nil { - _, err := store.Cmdable.Del(ticket.asHandle(store.CookieOptions.CookieName)).Result() + ctx := req.Context() + err := store.Client.Del(ctx, ticket.asHandle(store.CookieOptions.Name)) if err != nil { return fmt.Errorf("error clearing cookie from redis: %s", err) } @@ -225,11 +228,11 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro // 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) + value = encryption.SignedValue(store.CookieOptions.Secret, store.CookieOptions.Name, value, now) } return cookies.MakeCookieFromOptions( req, - store.CookieOptions.CookieName, + store.CookieOptions.Name, value, store.CookieOptions, expires, @@ -237,7 +240,7 @@ func (store *SessionStore) makeCookie(req *http.Request, value string, expires t ) } -func (store *SessionStore) storeValue(value string, expiration time.Duration, requestCookie *http.Cookie) (string, error) { +func (store *SessionStore) storeValue(ctx context.Context, 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) @@ -253,12 +256,12 @@ func (store *SessionStore) storeValue(value string, expiration time.Duration, re stream := cipher.NewCFBEncrypter(block, ticket.Secret) stream.XORKeyStream(ciphertext, []byte(value)) - handle := ticket.asHandle(store.CookieOptions.CookieName) - err = store.Cmdable.Set(handle, ciphertext, expiration).Err() + handle := ticket.asHandle(store.CookieOptions.Name) + err = store.Client.Set(ctx, handle, ciphertext, expiration) if err != nil { return "", err } - return ticket.encodeTicket(store.CookieOptions.CookieName), nil + return ticket.encodeTicket(store.CookieOptions.Name), nil } // getTicket retrieves an existing ticket from the cookie if present, @@ -269,14 +272,14 @@ func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, e } // An existing cookie exists, try to retrieve the ticket - val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) + val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.Secret, store.CookieOptions.Expire) if !ok { // Cookie is invalid, create a new ticket return newTicket() } // Valid cookie, decode the ticket - ticket, err := decodeTicket(store.CookieOptions.CookieName, val) + ticket, err := decodeTicket(store.CookieOptions.Name, val) if err != nil { // If we can't decode the ticket we have to create a new one return newTicket() @@ -290,7 +293,7 @@ func newTicket() (*TicketData, error) { return nil, fmt.Errorf("failed to create new ticket ID %s", err) } // ticketID is hex encoded - ticketID := fmt.Sprintf("%x", rawID) + ticketID := hex.EncodeToString(rawID) secret := make([]byte, aes.BlockSize) if _, err := io.ReadFull(rand.Reader, secret); err != nil { diff --git a/pkg/sessions/session_store.go b/pkg/sessions/session_store.go index 17ef21c24ea0ad8460cf0057fa25ff58a717c7cc..992d6ded22487d9d4f4afc752b35d7c884c7b3be 100644 --- a/pkg/sessions/session_store.go +++ b/pkg/sessions/session_store.go @@ -3,10 +3,10 @@ package sessions import ( "fmt" - "github.com/pusher/oauth2_proxy/pkg/apis/options" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/sessions/cookie" - "github.com/pusher/oauth2_proxy/pkg/sessions/redis" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/cookie" + "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/redis" ) // NewSessionStore creates a SessionStore from the provided configuration diff --git a/pkg/sessions/session_store_test.go b/pkg/sessions/session_store_test.go index f8162c1b0368f6f5e4d26974681d65a9de6e60f4..68cfd125c11361483af4c93f35997a3fccfaa42e 100644 --- a/pkg/sessions/session_store_test.go +++ b/pkg/sessions/session_store_test.go @@ -10,17 +10,16 @@ import ( "testing" "time" - "github.com/alicebob/miniredis" + miniredis "github.com/alicebob/miniredis/v2" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + cookiesapi "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies" + "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" + "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions" + sessionscookie "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/cookie" + "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/redis" . "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" - cookiesapi "github.com/pusher/oauth2_proxy/pkg/cookies" - "github.com/pusher/oauth2_proxy/pkg/encryption" - "github.com/pusher/oauth2_proxy/pkg/sessions" - sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie" - "github.com/pusher/oauth2_proxy/pkg/sessions/redis" - "github.com/pusher/oauth2_proxy/pkg/sessions/utils" ) func TestSessionStore(t *testing.T) { @@ -47,41 +46,45 @@ var _ = Describe("NewSessionStore", func() { It("have the correct name set", func() { if len(cookies) == 1 { - Expect(cookies[0].Name).To(Equal(cookieOpts.CookieName)) + Expect(cookies[0].Name).To(Equal(cookieOpts.Name)) } else { for _, cookie := range cookies { - Expect(cookie.Name).To(ContainSubstring(cookieOpts.CookieName)) + Expect(cookie.Name).To(ContainSubstring(cookieOpts.Name)) } } }) It("have the correct path set", func() { for _, cookie := range cookies { - Expect(cookie.Path).To(Equal(cookieOpts.CookiePath)) + Expect(cookie.Path).To(Equal(cookieOpts.Path)) } }) It("have the correct domain set", func() { for _, cookie := range cookies { - Expect(cookie.Domain).To(Equal(cookieOpts.CookieDomain)) + specifiedDomain := "" + if len(cookieOpts.Domains) > 0 { + specifiedDomain = cookieOpts.Domains[0] + } + Expect(cookie.Domain).To(Equal(specifiedDomain)) } }) It("have the correct HTTPOnly set", func() { for _, cookie := range cookies { - Expect(cookie.HttpOnly).To(Equal(cookieOpts.CookieHTTPOnly)) + Expect(cookie.HttpOnly).To(Equal(cookieOpts.HTTPOnly)) } }) It("have the correct secure set", func() { for _, cookie := range cookies { - Expect(cookie.Secure).To(Equal(cookieOpts.CookieSecure)) + Expect(cookie.Secure).To(Equal(cookieOpts.Secure)) } }) It("have the correct SameSite set", func() { for _, cookie := range cookies { - Expect(cookie.SameSite).To(Equal(cookiesapi.ParseSameSite(cookieOpts.CookieSameSite))) + Expect(cookie.SameSite).To(Equal(cookiesapi.ParseSameSite(cookieOpts.SameSite))) } }) @@ -164,8 +167,8 @@ var _ = Describe("NewSessionStore", 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 := cookiesapi.MakeCookieFromOptions(request, cookieOpts.CookieName, value, cookieOpts, cookieOpts.CookieExpire, time.Now()) + value := encryption.SignedValue(cookieOpts.Secret, cookieOpts.Name, broken, time.Now()) + cookie := cookiesapi.MakeCookieFromOptions(request, cookieOpts.Name, value, cookieOpts, cookieOpts.Expire, time.Now()) request.AddCookie(cookie) err := ss.Save(response, request, session) @@ -241,7 +244,7 @@ var _ = Describe("NewSessionStore", func() { }) It("loads a session equal to the original session", func() { - if cookieOpts.CookieSecret == "" { + if cookieOpts.Secret == "" { // Only Email and User stored in session when encrypted Expect(loadedSession.Email).To(Equal(session.Email)) Expect(loadedSession.User).To(Equal(session.User)) @@ -286,7 +289,7 @@ var _ = Describe("NewSessionStore", func() { BeforeEach(func() { switch ss.(type) { case *redis.SessionStore: - mr.FastForward(cookieOpts.CookieRefresh + time.Minute) + mr.FastForward(cookieOpts.Refresh + time.Minute) } }) @@ -300,7 +303,7 @@ var _ = Describe("NewSessionStore", func() { BeforeEach(func() { switch ss.(type) { case *redis.SessionStore: - mr.FastForward(cookieOpts.CookieExpire + time.Minute) + mr.FastForward(cookieOpts.Expire + time.Minute) } loadedSession, err = ss.Load(request) @@ -337,14 +340,14 @@ var _ = Describe("NewSessionStore", func() { Context("with non-default options", func() { BeforeEach(func() { cookieOpts = &options.CookieOptions{ - CookieName: "_cookie_name", - CookiePath: "/path", - CookieExpire: time.Duration(72) * time.Hour, - CookieRefresh: time.Duration(2) * time.Hour, - CookieSecure: false, - CookieHTTPOnly: false, - CookieDomain: "example.com", - CookieSameSite: "strict", + Name: "_cookie_name", + Path: "/path", + Expire: time.Duration(72) * time.Hour, + Refresh: time.Duration(2) * time.Hour, + Secure: false, + HTTPOnly: false, + Domains: []string{"example.com"}, + SameSite: "strict", } var err error @@ -360,8 +363,8 @@ var _ = Describe("NewSessionStore", 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)) + cookieOpts.Secret = base64.URLEncoding.EncodeToString(secret) + cipher, err := encryption.NewCipher(encryption.SecretBytes(cookieOpts.Secret)) Expect(err).ToNot(HaveOccurred()) Expect(cipher).ToNot(BeNil()) opts.Cipher = cipher @@ -380,13 +383,13 @@ var _ = Describe("NewSessionStore", func() { // Set default options in CookieOptions cookieOpts = &options.CookieOptions{ - CookieName: "_oauth2_proxy", - CookiePath: "/", - CookieExpire: time.Duration(168) * time.Hour, - CookieRefresh: time.Duration(1) * time.Hour, - CookieSecure: true, - CookieHTTPOnly: true, - CookieSameSite: "", + Name: "_oauth2_proxy", + Path: "/", + Expire: time.Duration(168) * time.Hour, + Refresh: time.Duration(1) * time.Hour, + Secure: true, + HTTPOnly: true, + SameSite: "", } session = &sessionsapi.SessionState{ @@ -424,7 +427,7 @@ var _ = Describe("NewSessionStore", func() { mr, err = miniredis.Run() Expect(err).ToNot(HaveOccurred()) opts.Type = options.RedisSessionStoreType - opts.RedisConnectionURL = "redis://" + mr.Addr() + opts.Redis.ConnectionURL = "redis://" + mr.Addr() }) AfterEach(func() { diff --git a/pkg/sessions/utils/utils.go b/pkg/sessions/utils/utils.go deleted file mode 100644 index 1fb27f4ddffa718b30524368dec8772dbfe2673f..0000000000000000000000000000000000000000 --- a/pkg/sessions/utils/utils.go +++ /dev/null @@ -1,41 +0,0 @@ -package utils - -import ( - "encoding/base64" - - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/encryption" -) - -// CookieForSession serializes a session state for storage in a cookie -func CookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) { - return s.EncodeSessionState(c) -} - -// SessionFromCookie deserializes a session from a cookie value -func SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) { - return sessions.DecodeSessionState(v, c) -} - -// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary -func SecretBytes(secret string) []byte { - b, err := base64.URLEncoding.DecodeString(addPadding(secret)) - if err == nil { - return []byte(addPadding(string(b))) - } - return []byte(secret) -} - -func addPadding(secret string) string { - padding := len(secret) % 4 - switch padding { - case 1: - return secret + "===" - case 2: - return secret + "==" - case 3: - return secret + "=" - default: - return secret - } -} diff --git a/providers/auth_test.go b/providers/auth_test.go index e17ad8d7466769f9eeb3c9520dcb364215898f5f..55b98a0fb79a93848148b33a862b4f2192b7733c 100644 --- a/providers/auth_test.go +++ b/providers/auth_test.go @@ -5,7 +5,7 @@ import ( "net/http" "net/url" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" ) var authorizedAccessToken = "imaginary_access_token" diff --git a/providers/authsch.go b/providers/authsch.go index fc19357bdef0a1a886679bad11c9fc1e61d973b2..5bda011f9f9d4bca404c6c07f49acc80ae584bec 100644 --- a/providers/authsch.go +++ b/providers/authsch.go @@ -1,6 +1,7 @@ package providers import ( + "context" "bytes" "encoding/json" "errors" @@ -11,7 +12,7 @@ import ( "time" "net/url" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" ) // AuthSCHProvider represents an AuthSCH based Identity Provider @@ -20,6 +21,8 @@ type AuthSCHProvider struct { Group string } +var _ Provider = (*AuthSCHProvider)(nil) + type authschLinkedAccounts struct { Name string `json:"schacc"` } @@ -63,7 +66,7 @@ func NewAuthSCHProvider(p *ProviderData) *AuthSCHProvider { } // Redeem provides a default implementation of the OAuth2 token redemption process -func (p *AuthSCHProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { +func (p *AuthSCHProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) { if code == "" { err = errors.New("missing code") @@ -80,7 +83,7 @@ func (p *AuthSCHProvider) Redeem(redirectURL, code string) (s *sessions.SessionS params.Add("client_id", p.ClientID) params.Add("client_secret", clientSecret) var req *http.Request - req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) + req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) if err != nil { return } @@ -123,7 +126,7 @@ func (p *AuthSCHProvider) Redeem(redirectURL, code string) (s *sessions.SessionS } // GetEmailAddress returns the Account email address -func (p *AuthSCHProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *AuthSCHProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { userInfo, err := p.getUserInfo(s) if err != nil { return "", fmt.Errorf("failed to retrieve user info: %v", err) @@ -139,7 +142,7 @@ func (p *AuthSCHProvider) GetEmailAddress(s *sessions.SessionState) (string, err } // GetUserName returns the Account name -func (p *AuthSCHProvider) GetUserName(s *sessions.SessionState) (string, error) { +func (p *AuthSCHProvider) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) { userInfo, err := p.getUserInfo(s) if err != nil { return "", fmt.Errorf("failed to retrieve user info: %v", err) diff --git a/providers/azure.go b/providers/azure.go index b619e6fe64a6c8da70d5c840a8848afd543a3079..961ff908c9452e7d8cddb7035d1dbf5adf913db8 100644 --- a/providers/azure.go +++ b/providers/azure.go @@ -2,6 +2,7 @@ package providers import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -11,9 +12,9 @@ import ( "time" "github.com/bitly/go-simplejson" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/logger" - "github.com/pusher/oauth2_proxy/pkg/requests" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) // AzureProvider represents an Azure based Identity Provider @@ -22,22 +23,23 @@ type AzureProvider struct { Tenant string } +var _ Provider = (*AzureProvider)(nil) + // NewAzureProvider initiates a new AzureProvider func NewAzureProvider(p *ProviderData) *AzureProvider { p.ProviderName = "Azure" if p.ProfileURL == nil || p.ProfileURL.String() == "" { p.ProfileURL = &url.URL{ - Scheme: "https", - Host: "graph.windows.net", - Path: "/me", - RawQuery: "api-version=1.6", + Scheme: "https", + Host: "graph.microsoft.com", + Path: "/v1.0/me", } } if p.ProtectedResource == nil || p.ProtectedResource.String() == "" { p.ProtectedResource = &url.URL{ Scheme: "https", - Host: "graph.windows.net", + Host: "graph.microsoft.com", } } if p.Scope == "" { @@ -69,7 +71,7 @@ func (p *AzureProvider) Configure(tenant string) { } } -func (p *AzureProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { +func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) { if code == "" { err = errors.New("missing code") return @@ -79,7 +81,6 @@ func (p *AzureProvider) Redeem(redirectURL, code string) (s *sessions.SessionSta return } - params := url.Values{} params.Add("redirect_uri", redirectURL) params.Add("client_id", p.ClientID) @@ -91,7 +92,7 @@ func (p *AzureProvider) Redeem(redirectURL, code string) (s *sessions.SessionSta } var req *http.Request - req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) + req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) if err != nil { return } @@ -159,14 +160,14 @@ func getEmailFromJSON(json *simplejson.Json) (string, error) { } // GetEmailAddress returns the Account email address -func (p *AzureProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *AzureProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { var email string var err error if s.AccessToken == "" { return "", errors.New("missing access token") } - req, err := http.NewRequest("GET", p.ProfileURL.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", p.ProfileURL.String(), nil) if err != nil { return "", err } diff --git a/providers/azure_test.go b/providers/azure_test.go index 2fa7a0c869db3ab5bc28520bf6349506e48ed211..af364b7773538808253ce7b2c110b508ad5ff4e9 100644 --- a/providers/azure_test.go +++ b/providers/azure_test.go @@ -1,6 +1,7 @@ package providers import ( + "context" "net/http" "net/http/httptest" "net/url" @@ -41,9 +42,9 @@ func TestAzureProviderDefaults(t *testing.T) { p.Data().LoginURL.String()) assert.Equal(t, "https://login.microsoftonline.com/common/oauth2/token", p.Data().RedeemURL.String()) - assert.Equal(t, "https://graph.windows.net/me?api-version=1.6", + assert.Equal(t, "https://graph.microsoft.com/v1.0/me", p.Data().ProfileURL.String()) - assert.Equal(t, "https://graph.windows.net", + assert.Equal(t, "https://graph.microsoft.com", p.Data().ProtectedResource.String()) assert.Equal(t, "", p.Data().ValidateURL.String()) @@ -97,9 +98,9 @@ func TestAzureSetTenant(t *testing.T) { p.Data().LoginURL.String()) assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/token", p.Data().RedeemURL.String()) - assert.Equal(t, "https://graph.windows.net/me?api-version=1.6", + assert.Equal(t, "https://graph.microsoft.com/v1.0/me", p.Data().ProfileURL.String()) - assert.Equal(t, "https://graph.windows.net", + assert.Equal(t, "https://graph.microsoft.com", p.Data().ProtectedResource.String()) assert.Equal(t, "", p.Data().ValidateURL.String()) @@ -107,14 +108,13 @@ func TestAzureSetTenant(t *testing.T) { } func testAzureBackend(payload string) *httptest.Server { - path := "/me" - query := "api-version=1.6" + path := "/v1.0/me" return httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - if (r.URL.Path != path || r.URL.RawQuery != query) && r.Method != "POST" { + if (r.URL.Path != path) && r.Method != http.MethodPost { w.WriteHeader(404) - } else if r.Method == "POST" && r.Body != nil { + } else if r.Method == http.MethodPost && r.Body != nil { w.WriteHeader(200) w.Write([]byte(payload)) } else if !IsAuthorizedInHeader(r.Header) { @@ -134,7 +134,7 @@ func TestAzureProviderGetEmailAddress(t *testing.T) { p := testAzureProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "user@windows.net", email) } @@ -147,7 +147,7 @@ func TestAzureProviderGetEmailAddressMailNull(t *testing.T) { p := testAzureProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "user@windows.net", email) } @@ -160,7 +160,7 @@ func TestAzureProviderGetEmailAddressGetUserPrincipalName(t *testing.T) { p := testAzureProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "user@windows.net", email) } @@ -173,7 +173,7 @@ func TestAzureProviderGetEmailAddressFailToGetEmailAddress(t *testing.T) { p := testAzureProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, "type assertion to string failed", err.Error()) assert.Equal(t, "", email) } @@ -186,7 +186,7 @@ func TestAzureProviderGetEmailAddressEmptyUserPrincipalName(t *testing.T) { p := testAzureProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "", email) } @@ -199,7 +199,7 @@ func TestAzureProviderGetEmailAddressIncorrectOtherMails(t *testing.T) { p := testAzureProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, "type assertion to string failed", err.Error()) assert.Equal(t, "", email) } @@ -213,7 +213,7 @@ func TestAzureProviderRedeemReturnsIdToken(t *testing.T) { bURL, _ := url.Parse(b.URL) p := testAzureProvider(bURL.Host) p.Data().RedeemURL.Path = "/common/oauth2/token" - s, err := p.Redeem("https://localhost", "1234") + s, err := p.Redeem(context.Background(), "https://localhost", "1234") assert.Equal(t, nil, err) assert.Equal(t, "testtoken1234", s.IDToken) assert.Equal(t, timestamp, s.ExpiresOn.UTC()) diff --git a/providers/bitbucket.go b/providers/bitbucket.go index 63c1d0fa567effbd09851dd823fd04c77096ca3a..2bb876cbf70ce1a4c7273a769f62586d5cc20ff9 100644 --- a/providers/bitbucket.go +++ b/providers/bitbucket.go @@ -1,13 +1,14 @@ package providers import ( + "context" "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" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) // BitbucketProvider represents an Bitbucket based Identity Provider @@ -17,6 +18,8 @@ type BitbucketProvider struct { Repository string } +var _ Provider = (*BitbucketProvider)(nil) + // NewBitbucketProvider initiates a new BitbucketProvider func NewBitbucketProvider(p *ProviderData) *BitbucketProvider { p.ProviderName = "Bitbucket" @@ -64,7 +67,7 @@ func (p *BitbucketProvider) SetRepository(repository string) { } // GetEmailAddress returns the email of the authenticated user -func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *BitbucketProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { var emails struct { Values []struct { @@ -82,7 +85,7 @@ func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, e FullName string `json:"full_name"` } } - req, err := http.NewRequest("GET", + req, err := http.NewRequestWithContext(ctx, "GET", p.ValidateURL.String()+"?access_token="+s.AccessToken, nil) if err != nil { logger.Printf("failed building request %s", err) @@ -98,7 +101,7 @@ func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, e teamURL := &url.URL{} *teamURL = *p.ValidateURL teamURL.Path = "/2.0/teams" - req, err = http.NewRequest("GET", + req, err = http.NewRequestWithContext(ctx, "GET", teamURL.String()+"?role=member&access_token="+s.AccessToken, nil) if err != nil { logger.Printf("failed building request %s", err) @@ -116,7 +119,7 @@ func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, e break } } - if found != true { + if !found { logger.Print("team membership test failed, access denied") return "", nil } @@ -126,7 +129,7 @@ func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, e repositoriesURL := &url.URL{} *repositoriesURL = *p.ValidateURL repositoriesURL.Path = "/2.0/repositories/" + strings.Split(p.Repository, "/")[0] - req, err = http.NewRequest("GET", + req, err = http.NewRequestWithContext(ctx, "GET", repositoriesURL.String()+"?role=contributor"+ "&q=full_name="+url.QueryEscape("\""+p.Repository+"\"")+ "&access_token="+s.AccessToken, @@ -147,7 +150,7 @@ func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, e break } } - if found != true { + if !found { logger.Print("repository access test failed, access denied") return "", nil } diff --git a/providers/bitbucket_test.go b/providers/bitbucket_test.go index d6d2cdc6e0c5afaa04094a12424658fc7f7f1d9f..e788b81e5d0c6f82496489aad0c143956f74625c 100644 --- a/providers/bitbucket_test.go +++ b/providers/bitbucket_test.go @@ -1,6 +1,7 @@ package providers import ( + "context" "log" "net/http" "net/http/httptest" @@ -9,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" ) func testBitbucketProvider(hostname, team string, repository string) *BitbucketProvider { @@ -120,7 +121,7 @@ func TestBitbucketProviderGetEmailAddress(t *testing.T) { p := testBitbucketProvider(bURL.Host, "", "") session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "michael.bland@gsa.gov", email) } @@ -133,7 +134,7 @@ func TestBitbucketProviderGetEmailAddressAndGroup(t *testing.T) { p := testBitbucketProvider(bURL.Host, "bioinformatics", "") session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "michael.bland@gsa.gov", email) } @@ -151,7 +152,7 @@ func TestBitbucketProviderGetEmailAddressFailedRequest(t *testing.T) { // 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) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } @@ -164,7 +165,7 @@ func TestBitbucketProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) p := testBitbucketProvider(bURL.Host, "", "") session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, "", email) assert.Equal(t, nil, err) } diff --git a/providers/digitalocean.go b/providers/digitalocean.go index f4d9ce570dda1e8aaa6025332a7c3e404b7b9dd8..25d37af98ac52865ad6c2428b28601e5e6701dc0 100644 --- a/providers/digitalocean.go +++ b/providers/digitalocean.go @@ -1,13 +1,14 @@ package providers import ( + "context" "errors" "fmt" "net/http" "net/url" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/requests" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) // DigitalOceanProvider represents a DigitalOcean based Identity Provider @@ -15,6 +16,8 @@ type DigitalOceanProvider struct { *ProviderData } +var _ Provider = (*DigitalOceanProvider)(nil) + // NewDigitalOceanProvider initiates a new DigitalOceanProvider func NewDigitalOceanProvider(p *ProviderData) *DigitalOceanProvider { p.ProviderName = "DigitalOcean" @@ -53,11 +56,11 @@ func getDigitalOceanHeader(accessToken string) http.Header { } // GetEmailAddress returns the Account email address -func (p *DigitalOceanProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *DigitalOceanProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { if s.AccessToken == "" { return "", errors.New("missing access token") } - req, err := http.NewRequest("GET", p.ProfileURL.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", p.ProfileURL.String(), nil) if err != nil { return "", err } @@ -76,6 +79,6 @@ func (p *DigitalOceanProvider) GetEmailAddress(s *sessions.SessionState) (string } // ValidateSessionState validates the AccessToken -func (p *DigitalOceanProvider) ValidateSessionState(s *sessions.SessionState) bool { - return validateToken(p, s.AccessToken, getDigitalOceanHeader(s.AccessToken)) +func (p *DigitalOceanProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool { + return validateToken(ctx, p, s.AccessToken, getDigitalOceanHeader(s.AccessToken)) } diff --git a/providers/digitalocean_test.go b/providers/digitalocean_test.go index 3ef0f2aeabbecd55a3d8b3176151e37e9e3d3b68..e7907ebaffd3722ff5cb0ee1a64cd2c5ee4a7795 100644 --- a/providers/digitalocean_test.go +++ b/providers/digitalocean_test.go @@ -1,12 +1,13 @@ package providers import ( + "context" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" ) @@ -99,7 +100,7 @@ func TestDigitalOceanProviderGetEmailAddress(t *testing.T) { p := testDigitalOceanProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "user@example.com", email) } @@ -115,7 +116,7 @@ func TestDigitalOceanProviderGetEmailAddressFailedRequest(t *testing.T) { // 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) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } @@ -128,7 +129,7 @@ func TestDigitalOceanProviderGetEmailAddressEmailNotPresentInPayload(t *testing. p := testDigitalOceanProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } diff --git a/providers/facebook.go b/providers/facebook.go index abd538280787f1e19afd874cf77eccf4b5b467ea..0f9cc624b435c2b246a36c8e94916da7cff62399 100644 --- a/providers/facebook.go +++ b/providers/facebook.go @@ -1,13 +1,14 @@ package providers import ( + "context" "errors" "fmt" "net/http" "net/url" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/requests" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) // FacebookProvider represents an Facebook based Identity Provider @@ -15,6 +16,8 @@ type FacebookProvider struct { *ProviderData } +var _ Provider = (*FacebookProvider)(nil) + // NewFacebookProvider initiates a new FacebookProvider func NewFacebookProvider(p *ProviderData) *FacebookProvider { p.ProviderName = "Facebook" @@ -55,11 +58,11 @@ func getFacebookHeader(accessToken string) http.Header { } // GetEmailAddress returns the Account email address -func (p *FacebookProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *FacebookProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { if s.AccessToken == "" { return "", errors.New("missing access token") } - req, err := http.NewRequest("GET", p.ProfileURL.String()+"?fields=name,email", nil) + req, err := http.NewRequestWithContext(ctx, "GET", p.ProfileURL.String()+"?fields=name,email", nil) if err != nil { return "", err } @@ -80,6 +83,6 @@ func (p *FacebookProvider) GetEmailAddress(s *sessions.SessionState) (string, er } // ValidateSessionState validates the AccessToken -func (p *FacebookProvider) ValidateSessionState(s *sessions.SessionState) bool { - return validateToken(p, s.AccessToken, getFacebookHeader(s.AccessToken)) +func (p *FacebookProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool { + return validateToken(ctx, p, s.AccessToken, getFacebookHeader(s.AccessToken)) } diff --git a/providers/github.go b/providers/github.go index 40ec78d3b9850ff541dea562970948519d9b891f..1dc1e5d349e9a32f51bd2a57af66098575f04ed5 100644 --- a/providers/github.go +++ b/providers/github.go @@ -1,6 +1,7 @@ package providers import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -11,17 +12,21 @@ import ( "strconv" "strings" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // GitHubProvider represents an GitHub based Identity Provider type GitHubProvider struct { *ProviderData - Org string - Team string + Org string + Team string + Repo string + Token string } +var _ Provider = (*GitHubProvider)(nil) + // NewGitHubProvider initiates a new GitHubProvider func NewGitHubProvider(p *ProviderData) *GitHubProvider { p.ProviderName = "GitHub" @@ -69,7 +74,13 @@ func (p *GitHubProvider) SetOrgTeam(org, team string) { } } -func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) { +// SetRepo configures the target repository and optional token to use +func (p *GitHubProvider) SetRepo(repo, token string) { + p.Repo = repo + p.Token = token +} + +func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool, error) { // https://developer.github.com/v3/orgs/#list-your-organizations var orgs []struct { @@ -93,7 +104,7 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) { Path: path.Join(p.ValidateURL.Path, "/user/orgs"), RawQuery: params.Encode(), } - req, _ := http.NewRequest("GET", endpoint.String(), nil) + req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil) req.Header = getGitHubHeader(accessToken) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -122,7 +133,7 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) { pn++ } - var presentOrgs []string + presentOrgs := make([]string, 0, len(orgs)) for _, org := range orgs { if p.Org == org.Login { logger.Printf("Found Github Organization: %q", org.Login) @@ -135,7 +146,7 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) { return false, nil } -func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) { +func (p *GitHubProvider) hasOrgAndTeam(ctx context.Context, accessToken string) (bool, error) { // https://developer.github.com/v3/orgs/teams/#list-user-teams var teams []struct { @@ -169,7 +180,7 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) { RawQuery: params.Encode(), } - req, _ := http.NewRequest("GET", endpoint.String(), nil) + req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil) req.Header = getGitHubHeader(accessToken) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -260,8 +271,84 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) { return false, nil } +func (p *GitHubProvider) hasRepo(ctx context.Context, accessToken string) (bool, error) { + // https://developer.github.com/v3/repos/#get-a-repository + + type permissions struct { + Pull bool `json:"pull"` + Push bool `json:"push"` + } + + type repository struct { + Permissions permissions `json:"permissions"` + Private bool `json:"private"` + } + + endpoint := &url.URL{ + Scheme: p.ValidateURL.Scheme, + Host: p.ValidateURL.Host, + Path: path.Join(p.ValidateURL.Path, "/repo/", p.Repo), + } + + req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil) + req.Header = getGitHubHeader(accessToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, err + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return false, err + } + if resp.StatusCode != 200 { + return false, fmt.Errorf( + "got %d from %q %s", resp.StatusCode, endpoint.String(), body) + } + + var repo repository + if err := json.Unmarshal(body, &repo); err != nil { + return false, err + } + + // Every user can implicitly pull from a public repo, so only grant access + // if they have push access or the repo is private and they can pull + return repo.Permissions.Push || (repo.Private && repo.Permissions.Pull), nil +} + +func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) { + //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator + + endpoint := &url.URL{ + Scheme: p.ValidateURL.Scheme, + Host: p.ValidateURL.Host, + Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo, "/collaborators/", username), + } + req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil) + req.Header = getGitHubHeader(accessToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, err + } + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return false, err + } + + if resp.StatusCode != 204 { + return false, fmt.Errorf("got %d from %q %s", + resp.StatusCode, endpoint.String(), body) + } + + logger.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body) + + return true, nil +} + // GetEmailAddress returns the Account email address -func (p *GitHubProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *GitHubProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { var emails []struct { Email string `json:"email"` @@ -272,14 +359,18 @@ func (p *GitHubProvider) GetEmailAddress(s *sessions.SessionState) (string, erro // if we require an Org or Team, check that first if p.Org != "" { if p.Team != "" { - if ok, err := p.hasOrgAndTeam(s.AccessToken); err != nil || !ok { + if ok, err := p.hasOrgAndTeam(ctx, s.AccessToken); err != nil || !ok { return "", err } } else { - if ok, err := p.hasOrg(s.AccessToken); err != nil || !ok { + if ok, err := p.hasOrg(ctx, s.AccessToken); err != nil || !ok { return "", err } } + } else if p.Repo != "" && p.Token == "" { // If we have a token we'll do the collaborator check in GetUserName + if ok, err := p.hasRepo(ctx, s.AccessToken); err != nil || !ok { + return "", err + } } endpoint := &url.URL{ @@ -287,7 +378,7 @@ func (p *GitHubProvider) GetEmailAddress(s *sessions.SessionState) (string, erro Host: p.ValidateURL.Host, Path: path.Join(p.ValidateURL.Path, "/user/emails"), } - req, _ := http.NewRequest("GET", endpoint.String(), nil) + req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil) req.Header = getGitHubHeader(s.AccessToken) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -324,7 +415,7 @@ func (p *GitHubProvider) GetEmailAddress(s *sessions.SessionState) (string, erro } // GetUserName returns the Account user name -func (p *GitHubProvider) GetUserName(s *sessions.SessionState) (string, error) { +func (p *GitHubProvider) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) { var user struct { Login string `json:"login"` Email string `json:"email"` @@ -336,7 +427,7 @@ func (p *GitHubProvider) GetUserName(s *sessions.SessionState) (string, error) { Path: path.Join(p.ValidateURL.Path, "/user"), } - req, err := http.NewRequest("GET", endpoint.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil) if err != nil { return "", fmt.Errorf("could not create new GET request: %v", err) } @@ -364,10 +455,17 @@ func (p *GitHubProvider) GetUserName(s *sessions.SessionState) (string, error) { return "", fmt.Errorf("%s unmarshaling %s", err, body) } + // Now that we have the username we can check collaborator status + if p.Org == "" && p.Repo != "" && p.Token != "" { + if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok { + return "", err + } + } + return user.Login, nil } // ValidateSessionState validates the AccessToken -func (p *GitHubProvider) ValidateSessionState(s *sessions.SessionState) bool { - return validateToken(p, s.AccessToken, getGitHubHeader(s.AccessToken)) +func (p *GitHubProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool { + return validateToken(ctx, p, s.AccessToken, getGitHubHeader(s.AccessToken)) } diff --git a/providers/github_test.go b/providers/github_test.go index e73ea40b4fc088fe551e911c4187faa76188d99f..ddf0ccc7480998bc4711e9a231a70569af1e8579 100644 --- a/providers/github_test.go +++ b/providers/github_test.go @@ -1,12 +1,13 @@ package providers import ( + "context" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" ) @@ -28,8 +29,10 @@ func testGitHubProvider(hostname string) *GitHubProvider { return p } -func testGitHubBackend(payload []string) *httptest.Server { +func testGitHubBackend(payloads map[string][]string) *httptest.Server { pathToQueryMap := map[string][]string{ + "/repo/oauth2-proxy/oauth2-proxy": {""}, + "/repos/oauth2-proxy/oauth2-proxy/collaborators/mbland": {""}, "/user": {""}, "/user/emails": {""}, "/user/orgs": {"page=1&per_page=100", "page=2&per_page=100", "page=3&per_page=100"}, @@ -46,10 +49,16 @@ func testGitHubBackend(payload []string) *httptest.Server { index = i } } + payload := []string{} + if ok && validQuery { + payload, ok = payloads[r.URL.Path] + } if !ok { w.WriteHeader(404) } else if !validQuery { w.WriteHeader(404) + } else if payload[index] == "" { + w.WriteHeader(204) } else { w.WriteHeader(200) w.Write([]byte(payload[index])) @@ -98,36 +107,43 @@ func TestGitHubProviderOverrides(t *testing.T) { } func TestGitHubProviderGetEmailAddress(t *testing.T) { - b := testGitHubBackend([]string{`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}) + b := testGitHubBackend(map[string][]string{ + "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + }) defer b.Close() bURL, _ := url.Parse(b.URL) p := testGitHubProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "michael.bland@gsa.gov", email) } func TestGitHubProviderGetEmailAddressNotVerified(t *testing.T) { - b := testGitHubBackend([]string{`[ {"email": "michael.bland@gsa.gov", "verified": false, "primary": true} ]`}) + b := testGitHubBackend(map[string][]string{ + "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + }) defer b.Close() bURL, _ := url.Parse(b.URL) p := testGitHubProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Empty(t, "", email) } func TestGitHubProviderGetEmailAddressWithOrg(t *testing.T) { - b := testGitHubBackend([]string{ - `[ {"email": "michael.bland@gsa.gov", "primary": true, "verified": true, "login":"testorg"} ]`, - `[ {"email": "michael.bland1@gsa.gov", "primary": true, "verified": true, "login":"testorg1"} ]`, - `[ ]`, + b := testGitHubBackend(map[string][]string{ + "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + "/user/orgs": { + `[ {"login":"testorg"} ]`, + `[ {"login":"testorg1"} ]`, + `[ ]`, + }, }) defer b.Close() @@ -136,7 +152,90 @@ func TestGitHubProviderGetEmailAddressWithOrg(t *testing.T) { p.Org = "testorg1" session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +func TestGitHubProviderGetEmailAddressWithWriteAccessToPublicRepo(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/repo/oauth2-proxy/oauth2-proxy": {`{"permissions": {"pull": true, "push": true}, "private": false}`}, + "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host) + p.SetRepo("oauth2-proxy/oauth2-proxy", "") + + session := CreateAuthorizedSession() + email, err := p.GetEmailAddress(context.Background(), session) + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +func TestGitHubProviderGetEmailAddressWithReadOnlyAccessToPrivateRepo(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/repo/oauth2-proxy/oauth2-proxy": {`{"permissions": {"pull": true, "push": false}, "private": true}`}, + "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host) + p.SetRepo("oauth2-proxy/oauth2-proxy", "") + + session := CreateAuthorizedSession() + email, err := p.GetEmailAddress(context.Background(), session) + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +func TestGitHubProviderGetEmailAddressWithWriteAccessToPrivateRepo(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/repo/oauth2-proxy/oauth2-proxy": {`{"permissions": {"pull": true, "push": true}, "private": true}`}, + "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host) + p.SetRepo("oauth2-proxy/oauth2-proxy", "") + + session := CreateAuthorizedSession() + email, err := p.GetEmailAddress(context.Background(), session) + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +func TestGitHubProviderGetEmailAddressWithNoAccessToPrivateRepo(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/repo/oauth2-proxy/oauth2-proxy": {}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host) + p.SetRepo("oauth2-proxy/oauth2-proxy", "") + + session := CreateAuthorizedSession() + email, err := p.GetEmailAddress(context.Background(), session) + assert.NotEqual(t, nil, err) + assert.Equal(t, "", email) +} + +func TestGitHubProviderGetEmailAddressWithToken(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host) + p.SetRepo("oauth2-proxy/oauth2-proxy", "token") + + session := CreateAuthorizedSession() + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "michael.bland@gsa.gov", email) } @@ -144,7 +243,7 @@ func TestGitHubProviderGetEmailAddressWithOrg(t *testing.T) { // 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 TestGitHubProviderGetEmailAddressFailedRequest(t *testing.T) { - b := testGitHubBackend([]string{"unused payload"}) + b := testGitHubBackend(map[string][]string{}) defer b.Close() bURL, _ := url.Parse(b.URL) @@ -154,33 +253,68 @@ func TestGitHubProviderGetEmailAddressFailedRequest(t *testing.T) { // 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) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } func TestGitHubProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { - b := testGitHubBackend([]string{"{\"foo\": \"bar\"}"}) + b := testGitHubBackend(map[string][]string{ + "/user/emails": {`{"foo": "bar"}`}, + }) defer b.Close() bURL, _ := url.Parse(b.URL) p := testGitHubProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } func TestGitHubProviderGetUserName(t *testing.T) { - b := testGitHubBackend([]string{`{"email": "michael.bland@gsa.gov", "login": "mbland"}`}) + b := testGitHubBackend(map[string][]string{ + "/user": {`{"email": "michael.bland@gsa.gov", "login": "mbland"}`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host) + + session := CreateAuthorizedSession() + email, err := p.GetUserName(context.Background(), session) + assert.Equal(t, nil, err) + assert.Equal(t, "mbland", email) +} + +func TestGitHubProviderGetUserNameWithRepoAndToken(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/user": {`{"email": "michael.bland@gsa.gov", "login": "mbland"}`}, + "/repos/oauth2-proxy/oauth2-proxy/collaborators/mbland": {""}, + }) defer b.Close() bURL, _ := url.Parse(b.URL) p := testGitHubProvider(bURL.Host) + p.SetRepo("oauth2-proxy/oauth2-proxy", "token") session := CreateAuthorizedSession() - email, err := p.GetUserName(session) + email, err := p.GetUserName(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "mbland", email) } + +func TestGitHubProviderGetUserNameWithRepoAndTokenWithoutPushAccess(t *testing.T) { + b := testGitHubBackend(map[string][]string{}) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host) + p.SetRepo("oauth2-proxy/oauth2-proxy", "token") + + session := CreateAuthorizedSession() + email, err := p.GetUserName(context.Background(), session) + assert.NotEqual(t, nil, err) + assert.Equal(t, "", email) +} diff --git a/providers/gitlab.go b/providers/gitlab.go index 396d41fced540e1bfc008f24be708404d2e16784..beeb6b9810961c27a2cce7f571a5a11ce3c0fc82 100644 --- a/providers/gitlab.go +++ b/providers/gitlab.go @@ -10,7 +10,7 @@ import ( "time" oidc "github.com/coreos/go-oidc" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "golang.org/x/oauth2" ) @@ -25,6 +25,8 @@ type GitLabProvider struct { AllowUnverifiedEmail bool } +var _ Provider = (*GitLabProvider)(nil) + // NewGitLabProvider initiates a new GitLabProvider func NewGitLabProvider(p *ProviderData) *GitLabProvider { p.ProviderName = "GitLab" @@ -37,13 +39,12 @@ func NewGitLabProvider(p *ProviderData) *GitLabProvider { } // Redeem exchanges the OAuth2 authentication token for an ID token -func (p *GitLabProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { +func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) { clientSecret, err := p.GetClientSecret() if err != nil { return } - ctx := context.Background() c := oauth2.Config{ ClientID: p.ClientID, ClientSecret: clientSecret, @@ -65,14 +66,14 @@ func (p *GitLabProvider) Redeem(redirectURL, code string) (s *sessions.SessionSt // 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) { +func (p *GitLabProvider) RefreshSessionIfNeeded(ctx context.Context, 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) + err := p.redeemRefreshToken(ctx, s) if err != nil { return false, fmt.Errorf("unable to redeem refresh token: %v", err) } @@ -81,7 +82,7 @@ func (p *GitLabProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, return true, nil } -func (p *GitLabProvider) redeemRefreshToken(s *sessions.SessionState) (err error) { +func (p *GitLabProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) { clientSecret, err := p.GetClientSecret() if err != nil { return @@ -94,7 +95,6 @@ func (p *GitLabProvider) redeemRefreshToken(s *sessions.SessionState) (err error TokenURL: p.RedeemURL.String(), }, } - ctx := context.Background() t := &oauth2.Token{ RefreshToken: s.RefreshToken, Expiry: time.Now().Add(-time.Hour), @@ -123,7 +123,7 @@ type gitlabUserInfo struct { Groups []string `json:"groups"` } -func (p *GitLabProvider) getUserInfo(s *sessions.SessionState) (*gitlabUserInfo, error) { +func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserInfo, error) { // Retrieve user info JSON // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information @@ -131,7 +131,7 @@ func (p *GitLabProvider) getUserInfo(s *sessions.SessionState) (*gitlabUserInfo, userInfoURL := *p.LoginURL userInfoURL.Path = "/oauth/userinfo" - req, err := http.NewRequest("GET", userInfoURL.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", userInfoURL.String(), nil) if err != nil { return nil, fmt.Errorf("failed to create user info request: %v", err) } @@ -219,20 +219,15 @@ func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.T } // ValidateSessionState checks that the session's IDToken is still valid -func (p *GitLabProvider) ValidateSessionState(s *sessions.SessionState) bool { - ctx := context.Background() +func (p *GitLabProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool { _, err := p.Verifier.Verify(ctx, s.IDToken) - if err != nil { - return false - } - - return true + return err == nil } // GetEmailAddress returns the Account email address -func (p *GitLabProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *GitLabProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { // Retrieve user info - userInfo, err := p.getUserInfo(s) + userInfo, err := p.getUserInfo(ctx, s) if err != nil { return "", fmt.Errorf("failed to retrieve user info: %v", err) } @@ -258,8 +253,8 @@ func (p *GitLabProvider) GetEmailAddress(s *sessions.SessionState) (string, erro } // GetUserName returns the Account user name -func (p *GitLabProvider) GetUserName(s *sessions.SessionState) (string, error) { - userInfo, err := p.getUserInfo(s) +func (p *GitLabProvider) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) { + userInfo, err := p.getUserInfo(ctx, s) if err != nil { return "", fmt.Errorf("failed to retrieve user info: %v", err) } diff --git a/providers/gitlab_test.go b/providers/gitlab_test.go index f75c4bfde8c0f1cc143eaece342d926bbc04b1c0..4a353ce821532006ddc2e475d2e2b5690ee8c7a8 100644 --- a/providers/gitlab_test.go +++ b/providers/gitlab_test.go @@ -1,12 +1,13 @@ package providers import ( + "context" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" ) @@ -63,7 +64,7 @@ func TestGitLabProviderBadToken(t *testing.T) { p := testGitLabProvider(bURL.Host) session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"} - _, err := p.GetEmailAddress(session) + _, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) } @@ -75,7 +76,7 @@ func TestGitLabProviderUnverifiedEmailDenied(t *testing.T) { p := testGitLabProvider(bURL.Host) session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - _, err := p.GetEmailAddress(session) + _, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) } @@ -88,7 +89,7 @@ func TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) { p.AllowUnverifiedEmail = true session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "foo@bar.com", email) } @@ -102,7 +103,7 @@ func TestGitLabProviderUsername(t *testing.T) { p.AllowUnverifiedEmail = true session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - username, err := p.GetUserName(session) + username, err := p.GetUserName(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "FooBar", username) } @@ -117,7 +118,7 @@ func TestGitLabProviderGroupMembershipValid(t *testing.T) { p.Group = "foo" session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "foo@bar.com", email) } @@ -132,7 +133,7 @@ func TestGitLabProviderGroupMembershipMissing(t *testing.T) { p.Group = "baz" session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - _, err := p.GetEmailAddress(session) + _, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) } @@ -146,7 +147,7 @@ func TestGitLabProviderEmailDomainValid(t *testing.T) { p.EmailDomains = []string{"bar.com"} session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "foo@bar.com", email) } @@ -161,6 +162,6 @@ func TestGitLabProviderEmailDomainInvalid(t *testing.T) { p.EmailDomains = []string{"baz.com"} session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - _, err := p.GetEmailAddress(session) + _, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) } diff --git a/providers/google.go b/providers/google.go index 12216a35d876cab9ec9f2dbdd2211b526889a161..1406855ba0b97ab6bdb18b1de7e439e6becea40e 100644 --- a/providers/google.go +++ b/providers/google.go @@ -2,6 +2,7 @@ package providers import ( "bytes" + "context" "encoding/base64" "encoding/json" "errors" @@ -13,12 +14,12 @@ import ( "strings" "time" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/logger" - "golang.org/x/oauth2" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" "golang.org/x/oauth2/google" admin "google.golang.org/api/admin/directory/v1" "google.golang.org/api/googleapi" + "google.golang.org/api/option" ) // GoogleProvider represents an Google based Identity Provider @@ -30,6 +31,8 @@ type GoogleProvider struct { GroupValidator func(string) bool } +var _ Provider = (*GoogleProvider)(nil) + type claims struct { Subject string `json:"sub"` Email string `json:"email"` @@ -97,7 +100,7 @@ func claimsFromIDToken(idToken string) (*claims, error) { } // Redeem exchanges the OAuth2 authentication token for an ID token -func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { +func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) { if code == "" { err = errors.New("missing code") return @@ -114,7 +117,7 @@ func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionSt params.Add("code", code) params.Add("grant_type", "authorization_code") var req *http.Request - req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) + req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) if err != nil { return } @@ -184,8 +187,9 @@ func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Serv } conf.Subject = adminEmail - client := conf.Client(oauth2.NoContext) - adminService, err := admin.New(client) + ctx := context.Background() + client := conf.Client(ctx) + adminService, err := admin.NewService(ctx, option.WithHTTPClient(client)) if err != nil { logger.Fatal(err) } @@ -198,11 +202,11 @@ func userInGroup(service *admin.Service, groups []string, email string) bool { req := service.Members.HasMember(group, email) r, err := req.Do() if err != nil { - err, ok := err.(*googleapi.Error) + gerr, ok := err.(*googleapi.Error) switch { - case ok && err.Code == 404: + case ok && gerr.Code == 404: logger.Printf("error checking membership in group %s: group does not exist", group) - case ok && err.Code == 400: + case ok && gerr.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 @@ -240,12 +244,12 @@ func (p *GoogleProvider) ValidateGroup(email string) bool { // RefreshSessionIfNeeded checks if the session has expired and uses the // RefreshToken to fetch a new ID token if required -func (p *GoogleProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) { +func (p *GoogleProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) { if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" { return false, nil } - newToken, newIDToken, duration, err := p.redeemRefreshToken(s.RefreshToken) + newToken, newIDToken, duration, err := p.redeemRefreshToken(ctx, s.RefreshToken) if err != nil { return false, err } @@ -263,7 +267,7 @@ func (p *GoogleProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, return true, nil } -func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, idToken string, expires time.Duration, err error) { +func (p *GoogleProvider) redeemRefreshToken(ctx context.Context, refreshToken string) (token string, idToken string, expires time.Duration, err error) { // https://developers.google.com/identity/protocols/OAuth2WebServer#refresh clientSecret, err := p.GetClientSecret() if err != nil { @@ -276,7 +280,7 @@ func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, params.Add("refresh_token", refreshToken) params.Add("grant_type", "refresh_token") var req *http.Request - req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) + req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) if err != nil { return } diff --git a/providers/google_test.go b/providers/google_test.go index 0e1de914250b2bfe2e16eac162a87277b060f2fa..63e5a9a80724708e2cd58ab6cd7fe2b911731d5d 100644 --- a/providers/google_test.go +++ b/providers/google_test.go @@ -102,7 +102,7 @@ func TestGoogleProviderGetEmailAddress(t *testing.T) { p.RedeemURL, server = newRedeemServer(body) defer server.Close() - session, err := p.Redeem("http://redirect/", "code1234") + session, err := p.Redeem(context.Background(), "http://redirect/", "code1234") assert.Equal(t, nil, err) assert.NotEqual(t, session, nil) assert.Equal(t, "michael.bland@gsa.gov", session.Email) @@ -139,7 +139,7 @@ func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) { p.RedeemURL, server = newRedeemServer(body) defer server.Close() - session, err := p.Redeem("http://redirect/", "code1234") + session, err := p.Redeem(context.Background(), "http://redirect/", "code1234") assert.NotEqual(t, nil, err) if session != nil { t.Errorf("expect nill session %#v", session) @@ -150,7 +150,7 @@ func TestGoogleProviderRedeemFailsNoCLientSecret(t *testing.T) { p := newGoogleProvider() p.ProviderData.ClientSecretFile = "srvnoerre" - session, err := p.Redeem("http://redirect/", "code1234") + session, err := p.Redeem(context.Background(), "http://redirect/", "code1234") assert.NotEqual(t, nil, err) if session != nil { t.Errorf("expect nill session %#v", session) @@ -170,7 +170,7 @@ func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) { p.RedeemURL, server = newRedeemServer(body) defer server.Close() - session, err := p.Redeem("http://redirect/", "code1234") + session, err := p.Redeem(context.Background(), "http://redirect/", "code1234") assert.NotEqual(t, nil, err) if session != nil { t.Errorf("expect nill session %#v", session) @@ -189,7 +189,7 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) { p.RedeemURL, server = newRedeemServer(body) defer server.Close() - session, err := p.Redeem("http://redirect/", "code1234") + session, err := p.Redeem(context.Background(), "http://redirect/", "code1234") assert.NotEqual(t, nil, err) if session != nil { t.Errorf("expect nill session %#v", session) diff --git a/providers/internal_util.go b/providers/internal_util.go index fb33b31de7bedd970b5695167feac32544183d73..f9bdc30418237dded04f3eb7dd493af34f3a71f8 100644 --- a/providers/internal_util.go +++ b/providers/internal_util.go @@ -1,12 +1,13 @@ package providers import ( + "context" "io/ioutil" "net/http" "net/url" - "github.com/pusher/oauth2_proxy/pkg/logger" - "github.com/pusher/oauth2_proxy/pkg/requests" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) // stripToken is a helper function to obfuscate "access_token" @@ -46,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 { +func validateToken(ctx context.Context, p Provider, accessToken string, header http.Header) bool { if accessToken == "" || p.Data().ValidateURL == nil || p.Data().ValidateURL.String() == "" { return false } @@ -55,7 +56,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 := requests.RequestUnparsedResponse(ctx, endpoint, header) if err != nil { logger.Printf("GET %s", stripToken(endpoint)) logger.Printf("token validation request failed: %s", err) diff --git a/providers/internal_util_test.go b/providers/internal_util_test.go index ba6d470edd1e13a36edafe745d0b126e9b8d60ba..0f6aa437ba343ce6577bc58811c4af38a65555d2 100644 --- a/providers/internal_util_test.go +++ b/providers/internal_util_test.go @@ -1,13 +1,14 @@ package providers import ( + "context" "errors" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" ) @@ -20,13 +21,15 @@ type ValidateSessionStateTestProvider struct { *ProviderData } -func (tp *ValidateSessionStateTestProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +var _ Provider = (*ValidateSessionStateTestProvider)(nil) + +func (tp *ValidateSessionStateTestProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { return "", errors.New("not implemented") } // Note that we're testing the internal validateToken() used to implement // several Provider's ValidateSessionState() implementations -func (tp *ValidateSessionStateTestProvider) ValidateSessionState(s *sessions.SessionState) bool { +func (tp *ValidateSessionStateTestProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool { return false } @@ -87,7 +90,7 @@ func (vtTest *ValidateSessionStateTest) Close() { func TestValidateSessionStateValidToken(t *testing.T) { vtTest := NewValidateSessionStateTest() defer vtTest.Close() - assert.Equal(t, true, validateToken(vtTest.provider, "foobar", nil)) + assert.Equal(t, true, validateToken(context.Background(), vtTest.provider, "foobar", nil)) } func TestValidateSessionStateValidTokenWithHeaders(t *testing.T) { @@ -96,34 +99,34 @@ func TestValidateSessionStateValidTokenWithHeaders(t *testing.T) { vtTest.header = make(http.Header) vtTest.header.Set("Authorization", "Bearer foobar") assert.Equal(t, true, - validateToken(vtTest.provider, "foobar", vtTest.header)) + validateToken(context.Background(), vtTest.provider, "foobar", vtTest.header)) } func TestValidateSessionStateEmptyToken(t *testing.T) { vtTest := NewValidateSessionStateTest() defer vtTest.Close() - assert.Equal(t, false, validateToken(vtTest.provider, "", nil)) + assert.Equal(t, false, validateToken(context.Background(), vtTest.provider, "", nil)) } func TestValidateSessionStateEmptyValidateURL(t *testing.T) { vtTest := NewValidateSessionStateTest() defer vtTest.Close() vtTest.provider.Data().ValidateURL = nil - assert.Equal(t, false, validateToken(vtTest.provider, "foobar", nil)) + assert.Equal(t, false, validateToken(context.Background(), vtTest.provider, "foobar", nil)) } func TestValidateSessionStateRequestNetworkFailure(t *testing.T) { vtTest := NewValidateSessionStateTest() // Close immediately to simulate a network failure vtTest.Close() - assert.Equal(t, false, validateToken(vtTest.provider, "foobar", nil)) + assert.Equal(t, false, validateToken(context.Background(), vtTest.provider, "foobar", nil)) } func TestValidateSessionStateExpiredToken(t *testing.T) { vtTest := NewValidateSessionStateTest() defer vtTest.Close() vtTest.responseCode = 401 - assert.Equal(t, false, validateToken(vtTest.provider, "foobar", nil)) + assert.Equal(t, false, validateToken(context.Background(), vtTest.provider, "foobar", nil)) } func TestStripTokenNotPresent(t *testing.T) { diff --git a/providers/keycloak.go b/providers/keycloak.go index 271537263d660504b848abe414f5a5acad1fadf8..414c4973c8367a08c36920f261bee977fd31d4bf 100644 --- a/providers/keycloak.go +++ b/providers/keycloak.go @@ -1,12 +1,13 @@ package providers import ( + "context" "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" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) type KeycloakProvider struct { @@ -14,6 +15,8 @@ type KeycloakProvider struct { Group string } +var _ Provider = (*KeycloakProvider)(nil) + func NewKeycloakProvider(p *ProviderData) *KeycloakProvider { p.ProviderName = "Keycloak" if p.LoginURL == nil || p.LoginURL.String() == "" { @@ -47,9 +50,9 @@ func (p *KeycloakProvider) SetGroup(group string) { p.Group = group } -func (p *KeycloakProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *KeycloakProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { - req, err := http.NewRequest("GET", p.ValidateURL.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", p.ValidateURL.String(), nil) req.Header.Set("Authorization", "Bearer "+s.AccessToken) if err != nil { logger.Printf("failed building request %s", err) @@ -76,7 +79,7 @@ func (p *KeycloakProvider) GetEmailAddress(s *sessions.SessionState) (string, er } } - if found != true { + if !found { logger.Printf("group not found, access denied") return "", nil } diff --git a/providers/keycloak_test.go b/providers/keycloak_test.go index 4d1d1005b01ccb328059f1cfbdf166047e7f643d..239d727fa0c49143f071929168e963df92f423ed 100644 --- a/providers/keycloak_test.go +++ b/providers/keycloak_test.go @@ -1,13 +1,15 @@ package providers import ( + "context" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/bmizerany/assert" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/stretchr/testify/assert" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" ) func testKeycloakProvider(hostname, group string) *KeycloakProvider { @@ -98,7 +100,7 @@ func TestKeycloakProviderGetEmailAddress(t *testing.T) { p := testKeycloakProvider(bURL.Host, "") session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "michael.bland@gsa.gov", email) } @@ -111,7 +113,7 @@ func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) { p := testKeycloakProvider(bURL.Host, "test-grp1") session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "michael.bland@gsa.gov", email) } @@ -129,7 +131,7 @@ func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) { // 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) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } @@ -142,7 +144,7 @@ func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { p := testKeycloakProvider(bURL.Host, "") session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } diff --git a/providers/linkedin.go b/providers/linkedin.go index bca293608e6e8d8aa479115f889f29fdfe594172..6cc242390585bd936f03f0e5cc679a5cc6bef895 100644 --- a/providers/linkedin.go +++ b/providers/linkedin.go @@ -1,13 +1,14 @@ package providers import ( + "context" "errors" "fmt" "net/http" "net/url" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/requests" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) // LinkedInProvider represents an LinkedIn based Identity Provider @@ -15,6 +16,8 @@ type LinkedInProvider struct { *ProviderData } +var _ Provider = (*LinkedInProvider)(nil) + // NewLinkedInProvider initiates a new LinkedInProvider func NewLinkedInProvider(p *ProviderData) *LinkedInProvider { p.ProviderName = "LinkedIn" @@ -51,11 +54,11 @@ func getLinkedInHeader(accessToken string) http.Header { } // GetEmailAddress returns the Account email address -func (p *LinkedInProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *LinkedInProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { if s.AccessToken == "" { return "", errors.New("missing access token") } - req, err := http.NewRequest("GET", p.ProfileURL.String()+"?format=json", nil) + req, err := http.NewRequestWithContext(ctx, "GET", p.ProfileURL.String()+"?format=json", nil) if err != nil { return "", err } @@ -74,6 +77,6 @@ func (p *LinkedInProvider) GetEmailAddress(s *sessions.SessionState) (string, er } // ValidateSessionState validates the AccessToken -func (p *LinkedInProvider) ValidateSessionState(s *sessions.SessionState) bool { - return validateToken(p, s.AccessToken, getLinkedInHeader(s.AccessToken)) +func (p *LinkedInProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool { + return validateToken(ctx, p, s.AccessToken, getLinkedInHeader(s.AccessToken)) } diff --git a/providers/linkedin_test.go b/providers/linkedin_test.go index 0b7247ff8201f2c3837b0998ecf3640a0e83aa47..6d70d57c409749632e66e402f66dc3aead88ed1e 100644 --- a/providers/linkedin_test.go +++ b/providers/linkedin_test.go @@ -1,12 +1,13 @@ package providers import ( + "context" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" ) @@ -99,7 +100,7 @@ func TestLinkedInProviderGetEmailAddress(t *testing.T) { p := testLinkedInProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "user@linkedin.com", email) } @@ -115,7 +116,7 @@ func TestLinkedInProviderGetEmailAddressFailedRequest(t *testing.T) { // 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) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } @@ -128,7 +129,7 @@ func TestLinkedInProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { p := testLinkedInProvider(bURL.Host) session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } diff --git a/providers/logingov.go b/providers/logingov.go index 95d7aa952e896b859c344728a6a3045f51f1aee2..6f98d0cca0f23cf944ff56d2e40aa5f9eaf86083 100644 --- a/providers/logingov.go +++ b/providers/logingov.go @@ -2,6 +2,7 @@ package providers import ( "bytes" + "context" "crypto/rsa" "encoding/json" "errors" @@ -13,7 +14,7 @@ import ( "time" "github.com/dgrijalva/jwt-go" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "gopkg.in/square/go-jose.v2" ) @@ -24,11 +25,12 @@ type LoginGovProvider struct { // TODO (@timothy-spencer): Ideally, the nonce would be in the session state, but the session state // is created only upon code redemption, not during the auth, when this must be supplied. Nonce string - AcrValues string JWTKey *rsa.PrivateKey PubJWKURL *url.URL } +var _ Provider = (*LoginGovProvider)(nil) + // For generating a nonce var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -126,10 +128,10 @@ func checkNonce(idToken string, p *LoginGovProvider) (err error) { return } -func emailFromUserInfo(accessToken string, userInfoEndpoint string) (email string, err error) { +func emailFromUserInfo(ctx context.Context, accessToken string, userInfoEndpoint string) (email string, err error) { // query the user info endpoint for user attributes var req *http.Request - req, err = http.NewRequest("GET", userInfoEndpoint, nil) + req, err = http.NewRequestWithContext(ctx, "GET", userInfoEndpoint, nil) if err != nil { return } @@ -174,7 +176,7 @@ func emailFromUserInfo(accessToken string, userInfoEndpoint string) (email strin } // Redeem exchanges the OAuth2 authentication token for an ID token -func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { +func (p *LoginGovProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) { if code == "" { err = errors.New("missing code") return @@ -184,7 +186,7 @@ func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.Session Issuer: p.ClientID, Subject: p.ClientID, Audience: p.RedeemURL.String(), - ExpiresAt: int64(time.Now().Add(time.Duration(5 * time.Minute)).Unix()), + ExpiresAt: time.Now().Add(5 * time.Minute).Unix(), Id: randSeq(32), } token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims) @@ -200,7 +202,7 @@ func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.Session params.Add("grant_type", "authorization_code") var req *http.Request - req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) + req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) if err != nil { return } @@ -243,7 +245,7 @@ func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.Session // Get the email address var email string - email, err = emailFromUserInfo(jsonResponse.AccessToken, p.ProfileURL.String()) + email, err = emailFromUserInfo(ctx, jsonResponse.AccessToken, p.ProfileURL.String()) if err != nil { return } @@ -261,8 +263,7 @@ func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.Session // GetLoginURL overrides GetLoginURL to add login.gov parameters func (p *LoginGovProvider) GetLoginURL(redirectURI, state string) string { - var a url.URL - a = *p.LoginURL + a := *p.LoginURL params, _ := url.ParseQuery(a.RawQuery) params.Set("redirect_uri", redirectURI) params.Set("approval_prompt", p.ApprovalPrompt) @@ -270,7 +271,11 @@ func (p *LoginGovProvider) GetLoginURL(redirectURI, state string) string { params.Set("client_id", p.ClientID) params.Set("response_type", "code") params.Add("state", state) - params.Add("acr_values", p.AcrValues) + acr := p.AcrValues + if acr == "" { + acr = "http://idmanagement.gov/ns/assurance/loa/1" + } + params.Add("acr_values", acr) params.Add("nonce", p.Nonce) a.RawQuery = params.Encode() return a.String() diff --git a/providers/logingov_test.go b/providers/logingov_test.go index 29808d02ffba76a49538422fc02404949c260202..96934c686225ae9457b93416a5dbf0f15067eaca 100644 --- a/providers/logingov_test.go +++ b/providers/logingov_test.go @@ -1,6 +1,7 @@ package providers import ( + "context" "crypto" "crypto/rand" "crypto/rsa" @@ -189,7 +190,7 @@ func TestLoginGovProviderSessionData(t *testing.T) { p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody) defer pubjwkserver.Close() - session, err := p.Redeem("http://redirect/", "code1234") + session, err := p.Redeem(context.Background(), "http://redirect/", "code1234") assert.NoError(t, err) assert.NotEqual(t, session, nil) assert.Equal(t, "timothy.spencer@gsa.gov", session.Email) @@ -283,7 +284,7 @@ func TestLoginGovProviderBadNonce(t *testing.T) { p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody) defer pubjwkserver.Close() - _, err = p.Redeem("http://redirect/", "code1234") + _, err = p.Redeem(context.Background(), "http://redirect/", "code1234") // The "badfakenonce" in the idtoken above should cause this to error out assert.Error(t, err) diff --git a/providers/nextcloud.go b/providers/nextcloud.go index 18855c8cea581a917b17c227b65bc60e1ba23590..d51b71839082ef95e64af717505063e63560cc29 100644 --- a/providers/nextcloud.go +++ b/providers/nextcloud.go @@ -1,12 +1,13 @@ package providers import ( + "context" "fmt" "net/http" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/logger" - "github.com/pusher/oauth2_proxy/pkg/requests" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) // NextcloudProvider represents an Nextcloud based Identity Provider @@ -14,6 +15,8 @@ type NextcloudProvider struct { *ProviderData } +var _ Provider = (*NextcloudProvider)(nil) + // NewNextcloudProvider initiates a new NextcloudProvider func NewNextcloudProvider(p *ProviderData) *NextcloudProvider { p.ProviderName = "Nextcloud" @@ -27,8 +30,8 @@ func getNextcloudHeader(accessToken string) http.Header { } // GetEmailAddress returns the Account email address -func (p *NextcloudProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { - req, err := http.NewRequest("GET", +func (p *NextcloudProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", p.ValidateURL.String(), nil) if err != nil { logger.Printf("failed building request %s", err) diff --git a/providers/nextcloud_test.go b/providers/nextcloud_test.go index c598d7bb4e2874534784b476879b2e9e8d4fb0be..ac93d877659353d6f2da63226077901cdf880c98 100644 --- a/providers/nextcloud_test.go +++ b/providers/nextcloud_test.go @@ -1,12 +1,13 @@ package providers import ( + "context" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" ) @@ -97,7 +98,7 @@ func TestNextcloudProviderGetEmailAddress(t *testing.T) { p.ValidateURL.RawQuery = formatJSON session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.Equal(t, nil, err) assert.Equal(t, "michael.bland@gsa.gov", email) } @@ -117,7 +118,7 @@ func TestNextcloudProviderGetEmailAddressFailedRequest(t *testing.T) { // 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) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } @@ -132,7 +133,7 @@ func TestNextcloudProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) p.ValidateURL.RawQuery = formatJSON session := CreateAuthorizedSession() - email, err := p.GetEmailAddress(session) + email, err := p.GetEmailAddress(context.Background(), session) assert.NotEqual(t, nil, err) assert.Equal(t, "", email) } diff --git a/providers/oidc.go b/providers/oidc.go index 4a994017017401541a025c63f9356719850e6383..1b6758b9d5548f83add7809e8ded51aa35a206ed 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -8,18 +8,21 @@ import ( "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" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" ) +const emailClaim = "email" + // OIDCProvider represents an OIDC based Identity Provider type OIDCProvider struct { *ProviderData Verifier *oidc.IDTokenVerifier AllowUnverifiedEmail bool + UserIDClaim string } // NewOIDCProvider initiates a new OIDCProvider @@ -28,14 +31,15 @@ func NewOIDCProvider(p *ProviderData) *OIDCProvider { return &OIDCProvider{ProviderData: p} } +var _ Provider = (*OIDCProvider)(nil) + // Redeem exchanges the OAuth2 authentication token for an ID token -func (p *OIDCProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { +func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) { clientSecret, err := p.GetClientSecret() if err != nil { return } - ctx := context.Background() c := oauth2.Config{ ClientID: p.ClientID, ClientSecret: clientSecret, @@ -57,7 +61,7 @@ func (p *OIDCProvider) Redeem(redirectURL, code string) (s *sessions.SessionStat return nil, fmt.Errorf("token response did not contain an id_token") } - s, err = p.createSessionState(token, idToken) + s, err = p.createSessionState(ctx, token, idToken) if err != nil { return nil, fmt.Errorf("unable to update session: %v", err) } @@ -67,12 +71,12 @@ func (p *OIDCProvider) Redeem(redirectURL, code string) (s *sessions.SessionStat // RefreshSessionIfNeeded checks if the session has expired and uses the // RefreshToken to fetch a new Access Token (and optional ID token) if required -func (p *OIDCProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) { +func (p *OIDCProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) { if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" { return false, nil } - err := p.redeemRefreshToken(s) + err := p.redeemRefreshToken(ctx, s) if err != nil { return false, fmt.Errorf("unable to redeem refresh token: %v", err) } @@ -81,7 +85,7 @@ func (p *OIDCProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, e return true, nil } -func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error) { +func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) { clientSecret, err := p.GetClientSecret() if err != nil { return @@ -94,7 +98,6 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error) TokenURL: p.RedeemURL.String(), }, } - ctx := context.Background() t := &oauth2.Token{ RefreshToken: s.RefreshToken, Expiry: time.Now().Add(-time.Hour), @@ -110,7 +113,7 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error) return fmt.Errorf("unable to extract id_token from response: %v", err) } - newSession, err := p.createSessionState(token, idToken) + newSession, err := p.createSessionState(ctx, token, idToken) if err != nil { return fmt.Errorf("unable create new session state from response: %v", err) } @@ -121,6 +124,7 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error) s.IDToken = newSession.IDToken s.Email = newSession.Email s.User = newSession.User + s.PreferredUsername = newSession.PreferredUsername } s.AccessToken = newSession.AccessToken @@ -139,32 +143,23 @@ func (p *OIDCProvider) findVerifiedIDToken(ctx context.Context, token *oauth2.To } if rawIDToken, present := getIDToken(); present { - verifiedIdToken, err := p.Verifier.Verify(ctx, rawIDToken) - return verifiedIdToken, err - } else { - return nil, nil + verifiedIDToken, err := p.Verifier.Verify(ctx, rawIDToken) + return verifiedIDToken, err } + return nil, nil } -func (p *OIDCProvider) createSessionState(token *oauth2.Token, idToken *oidc.IDToken) (*sessions.SessionState, error) { +func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Token, idToken *oidc.IDToken) (*sessions.SessionState, error) { - newSession := &sessions.SessionState{} + var newSession *sessions.SessionState - if idToken != nil { - claims, err := findClaimsFromIDToken(idToken, token.AccessToken, p.ProfileURL.String()) + if idToken == nil { + newSession = &sessions.SessionState{} + } else { + var err error + newSession, err = p.createSessionStateInternal(ctx, token.Extra("id_token").(string), idToken, token) if err != nil { - return nil, fmt.Errorf("couldn't extract claims from id_token (%e)", err) - } - - if claims != nil { - - if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified { - return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email) - } - - newSession.IDToken = token.Extra("id_token").(string) - newSession.Email = claims.Email - newSession.User = claims.Subject + return nil, err } } @@ -175,15 +170,56 @@ func (p *OIDCProvider) createSessionState(token *oauth2.Token, idToken *oidc.IDT return newSession, nil } -// ValidateSessionState checks that the session's IDToken is still valid -func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool { - ctx := context.Background() - _, err := p.Verifier.Verify(ctx, s.IDToken) +func (p *OIDCProvider) CreateSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessions.SessionState, error) { + newSession, err := p.createSessionStateInternal(ctx, rawIDToken, idToken, nil) + if err != nil { + return nil, err + } + + newSession.AccessToken = rawIDToken + newSession.IDToken = rawIDToken + newSession.RefreshToken = "" + newSession.ExpiresOn = idToken.Expiry + + return newSession, nil +} + +func (p *OIDCProvider) createSessionStateInternal(ctx context.Context, rawIDToken string, idToken *oidc.IDToken, token *oauth2.Token) (*sessions.SessionState, error) { + + newSession := &sessions.SessionState{} + + if idToken == nil { + return newSession, nil + } + accessToken := "" + if token != nil { + accessToken = token.AccessToken + } + + claims, err := p.findClaimsFromIDToken(ctx, idToken, accessToken, p.ProfileURL.String()) if err != nil { - return false + return nil, fmt.Errorf("couldn't extract claims from id_token (%e)", err) + } + + newSession.IDToken = rawIDToken + + newSession.Email = claims.UserID // TODO Rename SessionState.Email to .UserID in the near future + + newSession.User = claims.Subject + newSession.PreferredUsername = claims.PreferredUsername + + verifyEmail := (p.UserIDClaim == emailClaim) && !p.AllowUnverifiedEmail + if verifyEmail && claims.Verified != nil && !*claims.Verified { + return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.UserID) } - return true + return newSession, nil +} + +// ValidateSessionState checks that the session's IDToken is still valid +func (p *OIDCProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool { + _, err := p.Verifier.Verify(ctx, s.IDToken) + return err == nil } func getOIDCHeader(accessToken string) http.Header { @@ -193,15 +229,25 @@ func getOIDCHeader(accessToken string) http.Header { return header } -func findClaimsFromIDToken(idToken *oidc.IDToken, accessToken string, profileURL string) (*OIDCClaims, error) { +func (p *OIDCProvider) findClaimsFromIDToken(ctx context.Context, idToken *oidc.IDToken, accessToken string, profileURL string) (*OIDCClaims, error) { - // Extract custom claims. claims := &OIDCClaims{} - if err := idToken.Claims(claims); err != nil { - return nil, fmt.Errorf("failed to parse id_token claims: %v", err) + // Extract default claims. + if err := idToken.Claims(&claims); err != nil { + return nil, fmt.Errorf("failed to parse default id_token claims: %v", err) + } + // Extract custom claims. + if err := idToken.Claims(&claims.rawClaims); err != nil { + return nil, fmt.Errorf("failed to parse all id_token claims: %v", err) + } + + userID := claims.rawClaims[p.UserIDClaim] + if userID == nil { + return nil, fmt.Errorf("claims did not contains the required user-id-claim '%s'", p.UserIDClaim) } + claims.UserID = fmt.Sprint(userID) - if claims.Email == "" { + if p.UserIDClaim == emailClaim && claims.UserID == "" { if profileURL == "" { return nil, fmt.Errorf("id_token did not contain an email") } @@ -210,7 +256,7 @@ func findClaimsFromIDToken(idToken *oidc.IDToken, accessToken string, profileURL // 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", profileURL, nil) + req, err := http.NewRequestWithContext(ctx, "GET", profileURL, nil) if err != nil { return nil, err } @@ -226,14 +272,16 @@ func findClaimsFromIDToken(idToken *oidc.IDToken, accessToken string, profileURL return nil, fmt.Errorf("neither id_token nor userinfo endpoint contained an email") } - claims.Email = email + claims.UserID = email } return claims, nil } type OIDCClaims struct { - Subject string `json:"sub"` - Email string `json:"email"` - Verified *bool `json:"email_verified"` + rawClaims map[string]interface{} + UserID string + Subject string `json:"sub"` + Verified *bool `json:"email_verified"` + PreferredUsername string `json:"preferred_username"` } diff --git a/providers/oidc_test.go b/providers/oidc_test.go index 865aac8593b85a4dda8f95c01d8f19127f2505a7..823af30c563c527c2a86773697a9c59c8417224c 100644 --- a/providers/oidc_test.go +++ b/providers/oidc_test.go @@ -8,19 +8,19 @@ import ( "encoding/json" "errors" "fmt" - "golang.org/x/oauth2" - - "github.com/bmizerany/assert" - "github.com/coreos/go-oidc" - "github.com/dgrijalva/jwt-go" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" + + "github.com/coreos/go-oidc" + "github.com/dgrijalva/jwt-go" + "github.com/stretchr/testify/assert" + "golang.org/x/oauth2" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" ) const accessToken = "access_token" @@ -31,6 +31,7 @@ const secret = "secret" type idTokenClaims struct { Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` + Phone string `json:"phone_number,omitempty"` Picture string `json:"picture,omitempty"` jwt.StandardClaims } @@ -46,6 +47,7 @@ type redeemTokenResponse struct { var defaultIDToken idTokenClaims = idTokenClaims{ "Jane Dobbs", "janed@me.com", + "+4798765432", "http://mugbook.com/janed/me.jpg", jwt.StandardClaims{ Audience: "https://test.myapp.com", @@ -58,10 +60,13 @@ var defaultIDToken idTokenClaims = idTokenClaims{ }, } -type fakeKeySetStub struct {} +type fakeKeySetStub struct{} func (fakeKeySetStub) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) { decodeString, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1]) + if err != nil { + return nil, err + } tokenClaims := &idTokenClaims{} err = json.Unmarshal(decodeString, tokenClaims) @@ -98,11 +103,12 @@ func newOIDCProvider(serverURL *url.URL) *OIDCProvider { p := &OIDCProvider{ ProviderData: providerData, - Verifier: oidc.NewVerifier( + Verifier: oidc.NewVerifier( "https://issuer.example.com", fakeKeySetStub{}, &oidc.Config{ClientID: clientID}, ), + UserIDClaim: "email", } return p @@ -153,7 +159,7 @@ func TestOIDCProviderRedeem(t *testing.T) { server, provider := newTestSetup(body) defer server.Close() - session, err := provider.Redeem(provider.RedeemURL.String(), "code1234") + session, err := provider.Redeem(context.Background(), provider.RedeemURL.String(), "code1234") assert.Equal(t, nil, err) assert.Equal(t, defaultIDToken.Email, session.Email) assert.Equal(t, accessToken, session.AccessToken) @@ -162,6 +168,26 @@ func TestOIDCProviderRedeem(t *testing.T) { assert.Equal(t, "123456789", session.User) } +func TestOIDCProviderRedeem_custom_userid(t *testing.T) { + + idToken, _ := newSignedTestIDToken(defaultIDToken) + body, _ := json.Marshal(redeemTokenResponse{ + AccessToken: accessToken, + ExpiresIn: 10, + TokenType: "Bearer", + RefreshToken: refreshToken, + IDToken: idToken, + }) + + server, provider := newTestSetup(body) + provider.UserIDClaim = "phone_number" + defer server.Close() + + session, err := provider.Redeem(context.Background(), provider.RedeemURL.String(), "code1234") + assert.Equal(t, nil, err) + assert.Equal(t, defaultIDToken.Phone, session.Email) +} + func TestOIDCProviderRefreshSessionIfNeededWithoutIdToken(t *testing.T) { idToken, _ := newSignedTestIDToken(defaultIDToken) @@ -185,7 +211,7 @@ func TestOIDCProviderRefreshSessionIfNeededWithoutIdToken(t *testing.T) { User: "11223344", } - refreshed, err := provider.RefreshSessionIfNeeded(existingSession) + refreshed, err := provider.RefreshSessionIfNeeded(context.Background(), existingSession) assert.Equal(t, nil, err) assert.Equal(t, refreshed, true) assert.Equal(t, "janedoe@example.com", existingSession.Email) @@ -218,7 +244,7 @@ func TestOIDCProviderRefreshSessionIfNeededWithIdToken(t *testing.T) { Email: "changeit", User: "changeit", } - refreshed, err := provider.RefreshSessionIfNeeded(existingSession) + refreshed, err := provider.RefreshSessionIfNeeded(context.Background(), existingSession) assert.Equal(t, nil, err) assert.Equal(t, refreshed, true) assert.Equal(t, defaultIDToken.Email, existingSession.Email) @@ -235,30 +261,32 @@ func TestOIDCProvider_findVerifiedIdToken(t *testing.T) { defer server.Close() token := newOauth2Token() - signedIdToken, _ := newSignedTestIDToken(defaultIDToken) - tokenWithIdToken := token.WithExtra(map[string]interface{}{ - "id_token": signedIdToken, + signedIDToken, _ := newSignedTestIDToken(defaultIDToken) + tokenWithIDToken := token.WithExtra(map[string]interface{}{ + "id_token": signedIDToken, }) - verifiedIdToken, err := provider.findVerifiedIDToken(context.Background(), tokenWithIdToken) + verifiedIDToken, err := provider.findVerifiedIDToken(context.Background(), tokenWithIDToken) assert.Equal(t, true, err == nil) - assert.Equal(t, true, verifiedIdToken != nil) - assert.Equal(t, defaultIDToken.Issuer, verifiedIdToken.Issuer) - assert.Equal(t, defaultIDToken.Subject, verifiedIdToken.Subject) + if verifiedIDToken == nil { + t.Fatal("verifiedIDToken is nil") + } + assert.Equal(t, defaultIDToken.Issuer, verifiedIDToken.Issuer) + assert.Equal(t, defaultIDToken.Subject, verifiedIDToken.Subject) // When the validation fails the response should be nil defaultIDToken.Id = "this-id-fails-validation" - signedIdToken, _ = newSignedTestIDToken(defaultIDToken) - tokenWithIdToken = token.WithExtra(map[string]interface{}{ - "id_token": signedIdToken, + signedIDToken, _ = newSignedTestIDToken(defaultIDToken) + tokenWithIDToken = token.WithExtra(map[string]interface{}{ + "id_token": signedIDToken, }) - verifiedIdToken, err = provider.findVerifiedIDToken(context.Background(), tokenWithIdToken) + verifiedIDToken, err = provider.findVerifiedIDToken(context.Background(), tokenWithIDToken) assert.Equal(t, errors.New("failed to verify signature: the validation failed for subject [123456789]"), err) - assert.Equal(t, true, verifiedIdToken == nil) + assert.Equal(t, true, verifiedIDToken == nil) // When there is no id token in the oauth token - verifiedIdToken, err = provider.findVerifiedIDToken(context.Background(), newOauth2Token()) + verifiedIDToken, err = provider.findVerifiedIDToken(context.Background(), newOauth2Token()) assert.Equal(t, nil, err) - assert.Equal(t, true, verifiedIdToken == nil) + assert.Equal(t, true, verifiedIDToken == nil) } diff --git a/providers/provider_data.go b/providers/provider_data.go index b264a0bf6d3c821a0a669954571afabb4d9f0932..de5bc0d34970cab532db630856770a32ddbcee3e 100644 --- a/providers/provider_data.go +++ b/providers/provider_data.go @@ -2,31 +2,36 @@ package providers import ( "errors" - "github.com/pusher/oauth2_proxy/pkg/logger" "io/ioutil" "net/url" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // ProviderData contains information required to configure all implementations // of OAuth2 providers type ProviderData struct { ProviderName string - ClientID string - ClientSecret string - ClientSecretFile string LoginURL *url.URL RedeemURL *url.URL ProfileURL *url.URL ProtectedResource *url.URL ValidateURL *url.URL - Scope string - ApprovalPrompt string + // Auth request params & related, see + //https://openid.net/specs/openid-connect-basic-1_0.html#rfc.section.2.1.1.1 + AcrValues string + ApprovalPrompt string // NOTE: Renamed to "prompt" in OAuth2 + ClientID string + ClientSecret string + ClientSecretFile string + Scope string + Prompt string } // Data returns the ProviderData func (p *ProviderData) Data() *ProviderData { return p } -func (p *ProviderData) GetClientSecret() (ClientSecret string, err error) { +func (p *ProviderData) GetClientSecret() (clientSecret string, err error) { if p.ClientSecret != "" || p.ClientSecretFile == "" { return p.ClientSecret, nil } diff --git a/providers/provider_default.go b/providers/provider_default.go index 707bbceac6a12c6d37aa8a00b6d1f855a2ba528f..141261a16ffe9dcfd7745d2fcf3e49b2912e4f03 100644 --- a/providers/provider_default.go +++ b/providers/provider_default.go @@ -2,6 +2,7 @@ package providers import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -10,12 +11,15 @@ import ( "net/url" "time" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/encryption" + "github.com/coreos/go-oidc" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" ) +var _ Provider = (*ProviderData)(nil) + // Redeem provides a default implementation of the OAuth2 token redemption process -func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { +func (p *ProviderData) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) { if code == "" { err = errors.New("missing code") return @@ -36,7 +40,7 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionStat } var req *http.Request - req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) + req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) if err != nil { return } @@ -86,11 +90,15 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionStat // GetLoginURL with typical oauth parameters func (p *ProviderData) GetLoginURL(redirectURI, state string) string { - var a url.URL - a = *p.LoginURL + a := *p.LoginURL params, _ := url.ParseQuery(a.RawQuery) params.Set("redirect_uri", redirectURI) - params.Set("approval_prompt", p.ApprovalPrompt) + params.Add("acr_values", p.AcrValues) + if p.Prompt != "" { + params.Set("prompt", p.Prompt) + } else { // Legacy variant of the prompt param: + params.Set("approval_prompt", p.ApprovalPrompt) + } params.Add("scope", p.Scope) params.Set("client_id", p.ClientID) params.Set("response_type", "code") @@ -99,23 +107,18 @@ func (p *ProviderData) GetLoginURL(redirectURI, state string) string { return a.String() } -// CookieForSession serializes a session state for storage in a cookie -func (p *ProviderData) CookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) { - return s.EncodeSessionState(c) -} - -// SessionFromCookie deserializes a session from a cookie value -func (p *ProviderData) SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) { - return sessions.DecodeSessionState(v, c) -} - // GetEmailAddress returns the Account email address -func (p *ProviderData) GetEmailAddress(s *sessions.SessionState) (string, error) { +func (p *ProviderData) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { return "", errors.New("not implemented") } // GetUserName returns the Account username -func (p *ProviderData) GetUserName(s *sessions.SessionState) (string, error) { +func (p *ProviderData) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) { + return "", errors.New("not implemented") +} + +// GetPreferredUsername returns the Account preferred username +func (p *ProviderData) GetPreferredUsername(ctx context.Context, s *sessions.SessionState) (string, error) { return "", errors.New("not implemented") } @@ -126,12 +129,46 @@ func (p *ProviderData) ValidateGroup(email string) bool { } // ValidateSessionState validates the AccessToken -func (p *ProviderData) ValidateSessionState(s *sessions.SessionState) bool { - return validateToken(p, s.AccessToken, nil) +func (p *ProviderData) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool { + return validateToken(ctx, p, s.AccessToken, nil) } // RefreshSessionIfNeeded should refresh the user's session if required and // do nothing if a refresh is not required -func (p *ProviderData) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) { +func (p *ProviderData) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) { return false, nil } + +func (p *ProviderData) CreateSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessions.SessionState, error) { + var claims struct { + Subject string `json:"sub"` + Email string `json:"email"` + Verified *bool `json:"email_verified"` + PreferredUsername string `json:"preferred_username"` + } + + if err := idToken.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) + } + + newSession := &sessions.SessionState{ + Email: claims.Email, + User: claims.Email, + PreferredUsername: claims.PreferredUsername, + } + + newSession.AccessToken = rawIDToken + newSession.IDToken = rawIDToken + newSession.RefreshToken = "" + newSession.ExpiresOn = idToken.Expiry + + return newSession, nil +} diff --git a/providers/provider_default_test.go b/providers/provider_default_test.go index ffe4aa74482d67ad477e9f039316c35f84e7d293..4d8a8306d254a770c979b11a76f94bfb390a920c 100644 --- a/providers/provider_default_test.go +++ b/providers/provider_default_test.go @@ -1,16 +1,17 @@ package providers import ( + "context" "testing" "time" - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" ) func TestRefresh(t *testing.T) { p := &ProviderData{} - refreshed, err := p.RefreshSessionIfNeeded(&sessions.SessionState{ + refreshed, err := p.RefreshSessionIfNeeded(context.Background(), &sessions.SessionState{ ExpiresOn: time.Now().Add(time.Duration(-11) * time.Minute), }) assert.Equal(t, false, refreshed) diff --git a/providers/providers.go b/providers/providers.go index f4e3c14341e420f54bc7c7b855adaa033e4dca18..37fa9670961794d705131974b5b4b665a4190bd3 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -1,22 +1,24 @@ package providers import ( - "github.com/pusher/oauth2_proxy/pkg/apis/sessions" - "github.com/pusher/oauth2_proxy/pkg/encryption" + "context" + + "github.com/coreos/go-oidc" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" ) // Provider represents an upstream identity provider implementation type Provider interface { Data() *ProviderData - GetEmailAddress(*sessions.SessionState) (string, error) - GetUserName(*sessions.SessionState) (string, error) - Redeem(string, string) (*sessions.SessionState, error) + GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) + GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) + GetPreferredUsername(ctx context.Context, s *sessions.SessionState) (string, error) + Redeem(ctx context.Context, redirectURI, code string) (*sessions.SessionState, error) ValidateGroup(string) bool - ValidateSessionState(*sessions.SessionState) bool + ValidateSessionState(ctx context.Context, s *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) + RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) + CreateSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessions.SessionState, error) } // New provides a new Provider based on the configured provider string diff --git a/realclientip.go b/realclientip.go new file mode 100644 index 0000000000000000000000000000000000000000..b45ef7c29365e53adcccc8b185877b9c888b3267 --- /dev/null +++ b/realclientip.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "net" + "net/http" + "strings" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" +) + +type realClientIPParser interface { + GetRealClientIP(http.Header) (net.IP, error) +} + +func getRealClientIPParser(headerKey string) (realClientIPParser, error) { + headerKey = http.CanonicalHeaderKey(headerKey) + + switch headerKey { + case http.CanonicalHeaderKey("X-Forwarded-For"), http.CanonicalHeaderKey("X-Real-IP"), http.CanonicalHeaderKey("X-ProxyUser-IP"): + return &xForwardedForClientIPParser{header: headerKey}, nil + } + + // TODO: implement the more standardized but more complex `Forwarded` header. + return nil, fmt.Errorf("the http header key (%s) is either invalid or unsupported", headerKey) +} + +type xForwardedForClientIPParser struct { + header string +} + +// GetRealClientIP obtain the IP address of the end-user (not proxy). +// Parses headers sharing the format as specified by: +// * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For. +// Returns the `<client>` portion specified in the above document. +// Additionally, is capable of parsing IPs with the port included, for v4 in the format "<ip>:<port>" and for v6 in the +// format "[<ip>]:<port>". With-port and without-port formats are seamlessly supported concurrently. +func (p xForwardedForClientIPParser) GetRealClientIP(h http.Header) (net.IP, error) { + var ipStr string + if realIP := h.Get(p.header); realIP != "" { + ipStr = realIP + } else { + return nil, nil + } + + // Each successive proxy may append itself, comma separated, to the end of the X-Forwarded-for header. + // Select only the first IP listed, as it is the client IP recorded by the first proxy. + if commaIndex := strings.IndexRune(ipStr, ','); commaIndex != -1 { + ipStr = ipStr[:commaIndex] + } + ipStr = strings.TrimSpace(ipStr) + + if ipHost, _, err := net.SplitHostPort(ipStr); err == nil { + ipStr = ipHost + } + + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("unable to parse ip (%s) from %s header", ipStr, http.CanonicalHeaderKey(p.header)) + } + + return ip, nil +} + +// getRemoteIP obtains the IP of the low-level connected network host +func getRemoteIP(req *http.Request) (net.IP, error) { + if ipStr, _, err := net.SplitHostPort(req.RemoteAddr); err != nil { + return nil, fmt.Errorf("unable to get ip and port from http.RemoteAddr (%s)", req.RemoteAddr) + } else if ip := net.ParseIP(ipStr); ip != nil { + return ip, nil + } else { + return nil, fmt.Errorf("unable to parse ip (%s)", ipStr) + } +} + +// getClientString obtains the human readable string of the remote IP and optionally the real client IP if available +func getClientString(p realClientIPParser, req *http.Request, full bool) (s string) { + var realClientIPStr string + if p != nil { + if realClientIP, err := p.GetRealClientIP(req.Header); err != nil { + logger.Printf("Unable to get real client IP: %v", err) + } else if realClientIP != nil { + realClientIPStr = realClientIP.String() + } + } + + var remoteIPStr string + if remoteIP, err := getRemoteIP(req); err == nil { + remoteIPStr = remoteIP.String() + } else { + // Should not happen, if it does, likely a bug. + logger.Printf("Unable to get remote IP(?!?!): %v", err) + } + + if !full && realClientIPStr != "" { + return realClientIPStr + } + if full && realClientIPStr != "" { + return fmt.Sprintf("%s (%s)", remoteIPStr, realClientIPStr) + } + return remoteIPStr +} diff --git a/realclientip_test.go b/realclientip_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0271e2e331c344119df37b5ee47a2f6d4ef20196 --- /dev/null +++ b/realclientip_test.go @@ -0,0 +1,176 @@ +package main + +import ( + "net" + "net/http" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetRealClientIPParser(t *testing.T) { + forwardedForType := reflect.TypeOf((*xForwardedForClientIPParser)(nil)) + + tests := []struct { + header string + errString string + parserType reflect.Type + }{ + {"X-Forwarded-For", "", forwardedForType}, + {"X-REAL-IP", "", forwardedForType}, + {"x-proxyuser-ip", "", forwardedForType}, + {"", "the http header key () is either invalid or unsupported", nil}, + {"Forwarded", "the http header key (Forwarded) is either invalid or unsupported", nil}, + {"2#* @##$$:kd", "the http header key (2#* @##$$:kd) is either invalid or unsupported", nil}, + } + + for _, test := range tests { + p, err := getRealClientIPParser(test.header) + + if test.errString == "" { + assert.Nil(t, err) + } else { + assert.NotNil(t, err) + assert.Equal(t, test.errString, err.Error()) + } + + if test.parserType == nil { + assert.Nil(t, p) + } else { + assert.NotNil(t, p) + assert.Equal(t, test.parserType, reflect.TypeOf(p)) + } + + if xp, ok := p.(*xForwardedForClientIPParser); ok { + assert.Equal(t, http.CanonicalHeaderKey(test.header), xp.header) + } + } +} + +func TestXForwardedForClientIPParser(t *testing.T) { + p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")} + + tests := []struct { + headerValue string + errString string + expectedIP net.IP + }{ + {"", "", nil}, + {"1.2.3.4", "", net.ParseIP("1.2.3.4")}, + {"10::23", "", net.ParseIP("10::23")}, + {"::1", "", net.ParseIP("::1")}, + {"[::1]:1234", "", net.ParseIP("::1")}, + {"10.0.10.11:1234", "", net.ParseIP("10.0.10.11")}, + {"192.168.10.50, 10.0.0.1, 1.2.3.4", "", net.ParseIP("192.168.10.50")}, + {"nil", "unable to parse ip (nil) from X-Forwarded-For header", nil}, + {"10000.10000.10000.10000", "unable to parse ip (10000.10000.10000.10000) from X-Forwarded-For header", nil}, + } + + for _, test := range tests { + h := http.Header{} + h.Add("X-Forwarded-For", test.headerValue) + + ip, err := p.GetRealClientIP(h) + + if test.errString == "" { + assert.Nil(t, err) + } else { + assert.NotNil(t, err) + assert.Equal(t, test.errString, err.Error()) + } + + if test.expectedIP == nil { + assert.Nil(t, ip) + } else { + assert.NotNil(t, ip) + assert.Equal(t, test.expectedIP, ip) + } + } +} + +func TestXForwardedForClientIPParserIgnoresOthers(t *testing.T) { + p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")} + + h := http.Header{} + expectedIPString := "192.168.10.50" + h.Add("X-Real-IP", "10.0.0.1") + h.Add("X-ProxyUser-IP", "10.0.0.1") + h.Add("X-Forwarded-For", expectedIPString) + ip, err := p.GetRealClientIP(h) + assert.Nil(t, err) + assert.NotNil(t, ip) + assert.Equal(t, ip, net.ParseIP(expectedIPString)) +} + +func TestGetRemoteIP(t *testing.T) { + tests := []struct { + remoteAddr string + errString string + expectedIP net.IP + }{ + {"", "unable to get ip and port from http.RemoteAddr ()", nil}, + {"nil", "unable to get ip and port from http.RemoteAddr (nil)", nil}, + {"235.28.129.186", "unable to get ip and port from http.RemoteAddr (235.28.129.186)", nil}, + {"90::45", "unable to get ip and port from http.RemoteAddr (90::45)", nil}, + {"192.168.73.165:14976, 10.4.201.15:18453", "unable to get ip and port from http.RemoteAddr (192.168.73.165:14976, 10.4.201.15:18453)", nil}, + {"10000.10000.10000.10000:8080", "unable to parse ip (10000.10000.10000.10000)", nil}, + {"[::1]:48290", "", net.ParseIP("::1")}, + {"10.254.244.165:62750", "", net.ParseIP("10.254.244.165")}, + } + + for _, test := range tests { + req := &http.Request{RemoteAddr: test.remoteAddr} + + ip, err := getRemoteIP(req) + + if test.errString == "" { + assert.Nil(t, err) + } else { + assert.NotNil(t, err) + assert.Equal(t, test.errString, err.Error()) + } + + if test.expectedIP == nil { + assert.Nil(t, ip) + } else { + assert.NotNil(t, ip) + assert.Equal(t, test.expectedIP, ip) + } + } +} + +func TestGetClientString(t *testing.T) { + p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")} + + tests := []struct { + parser realClientIPParser + remoteAddr string + headerValue string + expectedClient string + expectedClientFull string + }{ + // Should fail quietly, only printing warnings to the log + {nil, "", "", "", ""}, + {p, "127.0.0.1:11950", "", "127.0.0.1", "127.0.0.1"}, + {p, "[::1]:28660", "99.103.56.12", "99.103.56.12", "::1 (99.103.56.12)"}, + {nil, "10.254.244.165:62750", "", "10.254.244.165", "10.254.244.165"}, + // Parser is nil, the contents of X-Forwarded-For should be ignored in all cases. + {nil, "[2001:470:26:307:a5a1:1177:2ae3:e9c3]:48290", "127.0.0.1", "2001:470:26:307:a5a1:1177:2ae3:e9c3", "2001:470:26:307:a5a1:1177:2ae3:e9c3"}, + } + + for _, test := range tests { + h := http.Header{} + h.Add("X-Forwarded-For", test.headerValue) + req := &http.Request{ + Header: h, + RemoteAddr: test.remoteAddr, + } + + client := getClientString(test.parser, req, false) + assert.Equal(t, test.expectedClient, client) + + clientFull := getClientString(test.parser, req, true) + assert.Equal(t, test.expectedClientFull, clientFull) + } +} diff --git a/string_array.go b/string_array.go deleted file mode 100644 index a6e1d960d9940dc5995b4d0e2a66fd4e023d724f..0000000000000000000000000000000000000000 --- a/string_array.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "strings" -) - -// StringArray is a type alias for a slice of strings -type StringArray []string - -// Get returns the slice of strings -func (a *StringArray) Get() interface{} { - return []string(*a) -} - -// Set appends a string to the StringArray -func (a *StringArray) Set(s string) error { - *a = append(*a, s) - return nil -} - -// String joins elements of the StringArray into a single comma separated string -func (a *StringArray) String() string { - return strings.Join(*a, ",") -} diff --git a/templates.go b/templates.go index 002e2b46addb493b4bcf583932ac8c222daa032a..39e9e14ef374efe801ce5e22e52b13ced74ffcea 100644 --- a/templates.go +++ b/templates.go @@ -3,8 +3,9 @@ package main import ( "html/template" "path" + "strings" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) func loadTemplates(dir string) *template.Template { @@ -12,7 +13,11 @@ func loadTemplates(dir string) *template.Template { return getTemplates() } logger.Printf("using custom template directory %q", dir) - t, err := template.New("").ParseFiles(path.Join(dir, "sign_in.html"), path.Join(dir, "error.html")) + funcMap := template.FuncMap{ + "ToUpper": strings.ToUpper, + "ToLower": strings.ToLower, + } + t, err := template.New("").Funcs(funcMap).ParseFiles(path.Join(dir, "sign_in.html"), path.Join(dir, "error.html")) if err != nil { logger.Fatalf("failed parsing template %s", err) } @@ -149,7 +154,7 @@ func getTemplates() *template.Template { <footer> {{ if eq .Footer "-" }} {{ else if eq .Footer ""}} - Secured with <a href="https://github.com/pusher/oauth2_proxy#oauth2_proxy">OAuth2 Proxy</a> version {{.Version}} + Secured with <a href="https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy">OAuth2 Proxy</a> version {{.Version}} {{ else }} {{.Footer}} {{ end }} diff --git a/templates_test.go b/templates_test.go index 49e1a9dd04ffc2c147aed3764d9f71d7fb6a3d24..63757a0c258fcf2b6ffee28a074ed04adfa086b6 100644 --- a/templates_test.go +++ b/templates_test.go @@ -1,11 +1,61 @@ package main import ( + "bytes" + "io/ioutil" + "log" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" ) +func TestLoadTemplates(t *testing.T) { + data := struct { + TestString string + }{ + TestString: "Testing", + } + + templates := loadTemplates("") + assert.NotEqual(t, templates, nil) + + var defaultSignin bytes.Buffer + templates.ExecuteTemplate(&defaultSignin, "sign_in.html", data) + assert.Equal(t, "\n<!DOCTYPE html>", defaultSignin.String()[0:16]) + + var defaultError bytes.Buffer + templates.ExecuteTemplate(&defaultError, "error.html", data) + assert.Equal(t, "\n<!DOCTYPE html>", defaultError.String()[0:16]) + + dir, err := ioutil.TempDir("", "templatetest") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) + + templateHTML := `{{.TestString}} {{.TestString | ToLower}} {{.TestString | ToUpper}}` + signInFile := filepath.Join(dir, "sign_in.html") + if err := ioutil.WriteFile(signInFile, []byte(templateHTML), 0666); err != nil { + log.Fatal(err) + } + errorFile := filepath.Join(dir, "error.html") + if err := ioutil.WriteFile(errorFile, []byte(templateHTML), 0666); err != nil { + log.Fatal(err) + } + templates = loadTemplates(dir) + assert.NotEqual(t, templates, nil) + + var sitpl bytes.Buffer + templates.ExecuteTemplate(&sitpl, "sign_in.html", data) + assert.Equal(t, "Testing testing TESTING", sitpl.String()) + + var errtpl bytes.Buffer + templates.ExecuteTemplate(&errtpl, "error.html", data) + assert.Equal(t, "Testing testing TESTING", errtpl.String()) +} + func TestTemplatesCompile(t *testing.T) { templates := getTemplates() assert.NotEqual(t, templates, nil) diff --git a/validator.go b/validator.go index a0dc58500830f9d7a042a590f0ab8007400c235e..e287ef50706c3249eeb1744270e4ce75c962112e 100644 --- a/validator.go +++ b/validator.go @@ -8,7 +8,7 @@ import ( "sync/atomic" "unsafe" - "github.com/pusher/oauth2_proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // UserMap holds information from the authenticated emails file diff --git a/watcher.go b/watcher.go index ed2bc0eda94f871a72640fdc8581f2b3fa403091..7c916235cfaee75debf91fd1540b060e719a7bbc 100644 --- a/watcher.go +++ b/watcher.go @@ -7,8 +7,9 @@ import ( "path/filepath" "time" - "github.com/pusher/oauth2_proxy/pkg/logger" - fsnotify "gopkg.in/fsnotify/fsnotify.v1" + "github.com/fsnotify/fsnotify" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // WaitForReplacement waits for a file to exist on disk and then starts a watch @@ -43,7 +44,7 @@ func WatchForUpdates(filename string, done <-chan bool, action func()) { defer watcher.Close() for { select { - case _ = <-done: + case <-done: logger.Printf("Shutting down watcher for: %s", filename) return case event := <-watcher.Events: diff --git a/watcher_unsupported.go b/watcher_unsupported.go index ff708b727a677d2a34d299fd0820a41a08e991f0..09c564687da534bf9cb297bb0336849c08b06fdb 100644 --- a/watcher_unsupported.go +++ b/watcher_unsupported.go @@ -2,7 +2,7 @@ package main -import "github.com/pusher/oauth2_proxy/pkg/logger" +import "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" func WatchForUpdates(filename string, done <-chan bool, action func()) { logger.Printf("file watching not implemented on this platform")