diff --git a/.gitignore b/.gitignore index a5f59b4e..aff7b5b3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ release # Folders _obj _test +.idea/ # Architecture specific extensions/prefixes *.[568vq] diff --git a/.travis.yml b/.travis.yml index 7fb4bce2..ef8aa3ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - 1.12.x - 1.13.x install: # Fetch dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 7687dd3d..a5657c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ # Vx.x.x (Pre-release) ## 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 + - 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) +- [#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) - [#265](https://github.com/pusher/oauth2_proxy/pull/265) Add upstream with static response (@cgroschupp) # v4.0.0 diff --git a/Makefile b/Makefile index 18283f94..f0f83b44 100644 --- a/Makefile +++ b/Makefile @@ -61,29 +61,4 @@ test: lint .PHONY: release release: lint test - mkdir release - mkdir release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION) - mkdir release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION) - mkdir release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION) - mkdir release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION) - mkdir release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION) - GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \ - -o release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy - GO111MODULE=on GOOS=linux GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \ - -o release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy - GO111MODULE=on GOOS=linux GOARCH=arm64 go build -ldflags="-X main.VERSION=${VERSION}" \ - -o release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy - GO111MODULE=on GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X main.VERSION=${VERSION}" \ - -o release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy - GO111MODULE=on GOOS=windows GOARCH=amd64 go build -ldflags="-X main.VERSION=${VERSION}" \ - -o release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)/$(BINARY) github.com/pusher/oauth2_proxy - shasum -a 256 release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).darwin-amd64-sha256sum.txt - shasum -a 256 release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-amd64-sha256sum.txt - shasum -a 256 release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-arm64-sha256sum.txt - shasum -a 256 release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).linux-armv6-sha256sum.txt - shasum -a 256 release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION)/$(BINARY) > release/$(BINARY)-$(VERSION).windows-amd64-sha256sum.txt - tar -C release -czvf release/$(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).darwin-amd64.$(GO_VERSION) - tar -C release -czvf release/$(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-amd64.$(GO_VERSION) - tar -C release -czvf release/$(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-arm64.$(GO_VERSION) - tar -C release -czvf release/$(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).linux-armv6.$(GO_VERSION) - tar -C release -czvf release/$(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION).tar.gz $(BINARY)-$(VERSION).windows-amd64.$(GO_VERSION) + BINARY=${BINARY} VERSION=${VERSION} ./dist.sh diff --git a/README.md b/README.md index ad88331c..daae04cc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# oauth2_proxy +![OAuth2 Proxy](/docs/logos/OAuth2_Proxy_horizontal.svg) + +[![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy) +[![Go Report Card](https://goreportcard.com/badge/github.com/pusher/oauth2_proxy)](https://goreportcard.com/report/github.com/pusher/oauth2_proxy) +[![GoDoc](https://godoc.org/github.com/pusher/oauth2_proxy?status.svg)](https://godoc.org/github.com/pusher/oauth2_proxy) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others) to validate accounts by email, domain or group. @@ -7,8 +12,6 @@ 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). -[![Build Status](https://secure.travis-ci.org/pusher/oauth2_proxy.svg?branch=master)](http://travis-ci.org/pusher/oauth2_proxy) - ![Sign In Page](https://cloud.githubusercontent.com/assets/45028/4970624/7feb7dd8-6886-11e4-93e0-c9904af44ea8.png) ## Installation diff --git a/configure b/configure index 26db8fea..10af15e6 100755 --- a/configure +++ b/configure @@ -5,6 +5,10 @@ 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=() diff --git a/dist.sh b/dist.sh index a00318bb..e5be8180 100755 --- a/dist.sh +++ b/dist.sh @@ -1,45 +1,46 @@ -#!/bin/bash -# build binary distributions for linux/amd64 and darwin/amd64 -set -e +#!/usr/bin/env bash -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -echo "working dir $DIR" -mkdir -p $DIR/dist -dep ensure || exit 1 +set -o errexit -os=$(go env GOOS) -arch=$(go env GOARCH) -version=$(cat $DIR/version.go | grep "const VERSION" | awk '{print $NF}' | sed 's/"//g') -goversion=$(go version | awk '{print $3}') -sha256sum=() - -echo "... running tests" -./test.sh - -for os in windows linux darwin; do - echo "... building v$version for $os/$arch" - EXT= - if [ $os = windows ]; then - EXT=".exe" - fi - BUILD=$(mktemp -d ${TMPDIR:-/tmp}/oauth2_proxy.XXXXXX) - TARGET="oauth2_proxy-$version.$os-$arch.$goversion" - FILENAME="oauth2_proxy-$version.$os-$arch$EXT" - GOOS=$os GOARCH=$arch CGO_ENABLED=0 \ - go build -ldflags="-s -w" -o $BUILD/$TARGET/$FILENAME || exit 1 - pushd $BUILD/$TARGET - sha256sum+=("$(shasum -a 256 $FILENAME || exit 1)") - cd .. && tar czvf $TARGET.tar.gz $TARGET - mv $TARGET.tar.gz $DIR/dist - popd -done - -checksum_file="sha256sum.txt" -cd $DIR/dist -if [ -f $checksum_file ]; then - rm $checksum_file +if [[ -z ${BINARY} ]] || [[ -z ${VERSION} ]]; then + echo "Missing required env var: BINARY=X VERSION=X $(basename $0)" + exit 1 fi -touch $checksum_file -for checksum in "${sha256sum[@]}"; do - echo "$checksum" >> $checksum_file + +# Check for Go version 1.13.* +GO_VERSION=$(go version | awk '{print $3}') +if [[ ! "${GO_VERSION}" =~ ^go1.13.* ]]; then + echo "Go version must be >= go1.13" + exit 1 +fi + +ARCHS=(darwin-amd64 linux-amd64 linux-arm64 linux-armv6 windows-amd64) + +mkdir -p release + +# Create architecture specific release dirs +for ARCH in "${ARCHS[@]}"; do + mkdir -p release/${BINARY}-${VERSION}.${ARCH}.${GO_VERSION} + + GO_OS=$(echo $ARCH | awk -F- '{print $1}') + GO_ARCH=$(echo $ARCH | awk -F- '{print $2}') + + # Create architecture specific binaries + if [[ ${GO_ARCH} == "armv6" ]]; then + GO111MODULE=on GOOS=${GO_OS} GOARCH=arm GOARM=6 go build -ldflags="-X main.VERSION=${VERSION}" \ + -o release/${BINARY}-${VERSION}.${ARCH}.${GO_VERSION}/${BINARY} github.com/pusher/oauth2_proxy + else + GO111MODULE=on GOOS=${GO_OS} GOARCH=${GO_ARCH} go build -ldflags="-X main.VERSION=${VERSION}" \ + -o release/${BINARY}-${VERSION}.${ARCH}.${GO_VERSION}/${BINARY} github.com/pusher/oauth2_proxy + fi + + cd release + + # Create sha256sum for architecture specific binary + shasum -a 256 ${BINARY}-${VERSION}.${ARCH}.${GO_VERSION}/${BINARY} > ${BINARY}-${VERSION}.${ARCH}-sha256sum.txt + + # Create tar file for architecture specific binary + tar -czvf ${BINARY}-${VERSION}.${ARCH}.${GO_VERSION}.tar.gz ${BINARY}-${VERSION}.${ARCH}.${GO_VERSION} + + cd .. done diff --git a/docs/0_index.md b/docs/0_index.md index 376bd754..e0e3227d 100644 --- a/docs/0_index.md +++ b/docs/0_index.md @@ -5,7 +5,7 @@ permalink: / nav_order: 0 --- -# oauth2_proxy +![OAuth2 Proxy](/logos/OAuth2_Proxy_horizontal.svg) A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others) to validate accounts by email, domain or group. diff --git a/docs/1_installation.md b/docs/1_installation.md index 9eb3939f..8ed72b81 100644 --- a/docs/1_installation.md +++ b/docs/1_installation.md @@ -9,7 +9,7 @@ nav_order: 1 1. Choose how to deploy: - a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v3.2.0`) + a. Download [Prebuilt Binary](https://github.com/pusher/oauth2_proxy/releases) (current release is `v4.0.0`) b. Build with `$ go get github.com/pusher/oauth2_proxy` which will put the binary in `$GOROOT/bin` @@ -18,8 +18,8 @@ nav_order: 1 Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. ``` -sha256sum -c sha256sum.txt 2>&1 | grep OK -oauth2_proxy-3.2.0.linux-amd64: OK +$ sha256sum -c sha256sum.txt 2>&1 | grep OK +oauth2_proxy-4.0.0.linux-amd64: OK ``` 2. [Select a Provider and Register an OAuth Application with a Provider](auth-configuration) diff --git a/docs/2_auth.md b/docs/2_auth.md index 04e63292..991bbae2 100644 --- a/docs/2_auth.md +++ b/docs/2_auth.md @@ -81,6 +81,8 @@ Note: The user is checked against the group members list on initial authenticati --client-secret= ``` +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. + ### Facebook Auth Provider 1. Create a new FB App from diff --git a/docs/_config.yml b/docs/_config.yml index 87f026c8..a53e9e14 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -14,6 +14,7 @@ # You can create any custom variable you would like, and they will be accessible # in the templates via {{ site.myvariable }}. 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 diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index ffe1be42..158ad3bd 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -44,6 +44,7 @@ An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example | `-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 | @@ -116,7 +117,7 @@ See below for provider specific options ### Upstreams Configuration -`oauth2_proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter, that will forward all authenticated requests to be forwarded to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream. +`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/`. diff --git a/docs/logos/OAuth2_Proxy_horizontal.png b/docs/logos/OAuth2_Proxy_horizontal.png new file mode 100644 index 00000000..1020c52d Binary files /dev/null and b/docs/logos/OAuth2_Proxy_horizontal.png differ diff --git a/docs/logos/OAuth2_Proxy_horizontal.svg b/docs/logos/OAuth2_Proxy_horizontal.svg new file mode 100644 index 00000000..4df740f6 --- /dev/null +++ b/docs/logos/OAuth2_Proxy_horizontal.svg @@ -0,0 +1 @@ +OAuth2_Proxy_logo_v3 \ No newline at end of file diff --git a/docs/logos/OAuth2_Proxy_icon.png b/docs/logos/OAuth2_Proxy_icon.png new file mode 100644 index 00000000..720f4ce3 Binary files /dev/null and b/docs/logos/OAuth2_Proxy_icon.png differ diff --git a/docs/logos/OAuth2_Proxy_icon.svg b/docs/logos/OAuth2_Proxy_icon.svg new file mode 100644 index 00000000..5b0837f9 --- /dev/null +++ b/docs/logos/OAuth2_Proxy_icon.svg @@ -0,0 +1 @@ +OAuth2_Proxy_logo_v3 \ No newline at end of file diff --git a/docs/logos/OAuth2_Proxy_vertical.png b/docs/logos/OAuth2_Proxy_vertical.png new file mode 100644 index 00000000..994cd0b7 Binary files /dev/null and b/docs/logos/OAuth2_Proxy_vertical.png differ diff --git a/docs/logos/OAuth2_Proxy_vertical.svg b/docs/logos/OAuth2_Proxy_vertical.svg new file mode 100644 index 00000000..bf48ab0f --- /dev/null +++ b/docs/logos/OAuth2_Proxy_vertical.svg @@ -0,0 +1 @@ +OAuth2_Proxy_logo_v3 \ No newline at end of file diff --git a/http.go b/http.go index 2cee227b..88280c44 100644 --- a/http.go +++ b/http.go @@ -152,3 +152,14 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc.SetKeepAlivePeriod(3 * time.Minute) return tc, nil } + +func redirectToHTTPS(opts *Options, h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + proto := r.Header.Get("X-Forwarded-Proto") + if opts.ForceHTTPS && (r.TLS == nil || (proto != "" && strings.ToLower(proto) != "https")) { + http.Redirect(w, r, opts.HTTPSAddress, http.StatusPermanentRedirect) + } + + h.ServeHTTP(w, r) + }) +} diff --git a/http_test.go b/http_test.go index f5ee1421..400213a0 100644 --- a/http_test.go +++ b/http_test.go @@ -106,3 +106,53 @@ func TestGCPHealthcheckNotIngressPut(t *testing.T) { assert.Equal(t, "test", rw.Body.String()) } + +func TestRedirectToHTTPSTrue(t *testing.T) { + opts := NewOptions() + opts.ForceHTTPS = true + handler := func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte("test")) + } + + h := redirectToHTTPS(opts, http.HandlerFunc(handler)) + rw := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/", nil) + h.ServeHTTP(rw, r) + + assert.Equal(t, http.StatusPermanentRedirect, rw.Code, "status code should be %d, got: %d", http.StatusPermanentRedirect, rw.Code) +} + +func TestRedirectToHTTPSFalse(t *testing.T) { + opts := NewOptions() + handler := func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte("test")) + } + + h := redirectToHTTPS(opts, http.HandlerFunc(handler)) + rw := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/", nil) + h.ServeHTTP(rw, r) + + assert.Equal(t, http.StatusOK, rw.Code, "status code should be %d, got: %d", http.StatusOK, rw.Code) +} + +func TestRedirectNotWhenHTTPS(t *testing.T) { + opts := NewOptions() + opts.ForceHTTPS = true + handler := func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte("test")) + } + + h := redirectToHTTPS(opts, http.HandlerFunc(handler)) + s := httptest.NewTLSServer(h) + defer s.Close() + + opts.HTTPSAddress = s.URL + client := s.Client() + res, err := client.Get(s.URL) + if err != nil { + t.Fatalf("request to test server failed with error: %v", err) + } + + assert.Equal(t, http.StatusOK, res.StatusCode, "status code should be %d, got: %d", http.StatusOK, res.StatusCode) +} diff --git a/main.go b/main.go index d30d6bda..b0e327bd 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ func main() { flagSet.String("http-address", "127.0.0.1:4180", "[http://]: or unix:// to listen on for HTTP clients") flagSet.String("https-address", ":443", ": to listen on for HTTPS clients") + flagSet.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\"") @@ -185,9 +186,9 @@ func main() { var handler http.Handler if opts.GCPHealthChecks { - handler = gcpHealthcheck(LoggingHandler(oauthproxy)) + handler = redirectToHTTPS(opts, gcpHealthcheck(LoggingHandler(oauthproxy))) } else { - handler = LoggingHandler(oauthproxy) + handler = redirectToHTTPS(opts, LoggingHandler(oauthproxy)) } s := &Server{ Handler: handler, diff --git a/options.go b/options.go index 37bbb0b9..ddc10cfd 100644 --- a/options.go +++ b/options.go @@ -34,6 +34,7 @@ type Options struct { 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"` + 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"` @@ -145,6 +146,7 @@ func NewOptions() *Options { ProxyWebSockets: true, HTTPAddress: "127.0.0.1:4180", HTTPSAddress: ":443", + ForceHTTPS: false, DisplayHtpasswdForm: true, CookieOptions: options.CookieOptions{ CookieName: "_oauth2_proxy", diff --git a/pkg/requests/requests.go b/pkg/requests/requests.go index 82d1176a..9083b2d4 100644 --- a/pkg/requests/requests.go +++ b/pkg/requests/requests.go @@ -18,17 +18,23 @@ func Request(req *http.Request) (*simplejson.Json, error) { return nil, err } body, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - logger.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body) - if err != nil { - return nil, err + if body != nil { + defer resp.Body.Close() } + + logger.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body) + + if err != nil { + return nil, fmt.Errorf("problem reading http request body: %w", err) + } + if resp.StatusCode != 200 { return nil, fmt.Errorf("got %d %s", resp.StatusCode, body) } + data, err := simplejson.NewJson(body) if err != nil { - return nil, err + return nil, fmt.Errorf("error unmarshalling json: %w", err) } return data, nil } @@ -41,10 +47,13 @@ func RequestJSON(req *http.Request, v interface{}) error { return err } body, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() + if body != nil { + defer resp.Body.Close() + } + logger.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body) if err != nil { - return err + return fmt.Errorf("error reading body from http response: %w", err) } if resp.StatusCode != 200 { return fmt.Errorf("got %d %s", resp.StatusCode, body) @@ -56,7 +65,7 @@ func RequestJSON(req *http.Request, v interface{}) error { func RequestUnparsedResponse(url string, header http.Header) (resp *http.Response, err error) { req, err := http.NewRequest("GET", url, nil) if err != nil { - return nil, err + return nil, fmt.Errorf("error performing get request: %w", err) } req.Header = header diff --git a/pkg/requests/requests_test.go b/pkg/requests/requests_test.go index 99a4c3b6..c9ec4e88 100644 --- a/pkg/requests/requests_test.go +++ b/pkg/requests/requests_test.go @@ -8,20 +8,21 @@ import ( "testing" "github.com/bitly/go-simplejson" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func testBackend(responseCode int, payload string) *httptest.Server { +func testBackend(t *testing.T, responseCode int, payload string) *httptest.Server { return httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(responseCode) - w.Write([]byte(payload)) + _, err := w.Write([]byte(payload)) + require.NoError(t, err) })) } func TestRequest(t *testing.T) { - backend := testBackend(200, "{\"foo\": \"bar\"}") + backend := testBackend(t, 200, "{\"foo\": \"bar\"}") defer backend.Close() req, _ := http.NewRequest("GET", backend.URL, nil) @@ -35,7 +36,7 @@ func TestRequest(t *testing.T) { func TestRequestFailure(t *testing.T) { // Create a backend to generate a test URL, then close it to cause a // connection error. - backend := testBackend(200, "{\"foo\": \"bar\"}") + backend := testBackend(t, 200, "{\"foo\": \"bar\"}") backend.Close() req, err := http.NewRequest("GET", backend.URL, nil) @@ -49,7 +50,7 @@ func TestRequestFailure(t *testing.T) { } func TestHttpErrorCode(t *testing.T) { - backend := testBackend(404, "{\"foo\": \"bar\"}") + backend := testBackend(t, 404, "{\"foo\": \"bar\"}") defer backend.Close() req, err := http.NewRequest("GET", backend.URL, nil) @@ -60,7 +61,7 @@ func TestHttpErrorCode(t *testing.T) { } func TestJsonParsingError(t *testing.T) { - backend := testBackend(200, "not well-formed JSON") + backend := testBackend(t, 200, "not well-formed JSON") defer backend.Close() req, err := http.NewRequest("GET", backend.URL, nil) @@ -77,7 +78,8 @@ func TestRequestUnparsedResponseUsingAccessTokenParameter(t *testing.T) { token := r.FormValue("access_token") if r.URL.Path == "/" && token == "my_token" { w.WriteHeader(200) - w.Write([]byte("some payload")) + _, err := w.Write([]byte("some payload")) + require.NoError(t, err) } else { w.WriteHeader(403) } @@ -86,16 +88,17 @@ func TestRequestUnparsedResponseUsingAccessTokenParameter(t *testing.T) { response, err := RequestUnparsedResponse( backend.URL+"?access_token=my_token", nil) + 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) - response.Body.Close() assert.Equal(t, "some payload", string(body)) } func TestRequestUnparsedResponseUsingAccessTokenParameterFailedResponse(t *testing.T) { - backend := testBackend(200, "some payload") + backend := testBackend(t, 200, "some payload") // Close the backend now to force a request failure. backend.Close() @@ -110,7 +113,8 @@ func TestRequestUnparsedResponseUsingHeaders(t *testing.T) { func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" && r.Header["Auth"][0] == "my_token" { w.WriteHeader(200) - w.Write([]byte("some payload")) + _, err := w.Write([]byte("some payload")) + require.NoError(t, err) } else { w.WriteHeader(403) } @@ -120,10 +124,12 @@ func TestRequestUnparsedResponseUsingHeaders(t *testing.T) { headers := make(http.Header) headers.Set("Auth", "my_token") response, err := RequestUnparsedResponse(backend.URL, headers) + 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) - response.Body.Close() + assert.Equal(t, "some payload", string(body)) } diff --git a/providers/azure.go b/providers/azure.go index 653090b0..48cea846 100644 --- a/providers/azure.go +++ b/providers/azure.go @@ -1,10 +1,14 @@ package providers import ( + "bytes" + "encoding/json" "errors" "fmt" + "io/ioutil" "net/http" "net/url" + "time" "github.com/bitly/go-simplejson" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" @@ -65,6 +69,67 @@ func (p *AzureProvider) Configure(tenant string) { } } +func (p *AzureProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { + if code == "" { + err = errors.New("missing code") + return + } + + params := url.Values{} + params.Add("redirect_uri", redirectURL) + params.Add("client_id", p.ClientID) + params.Add("client_secret", p.ClientSecret) + params.Add("code", code) + params.Add("grant_type", "authorization_code") + if p.ProtectedResource != nil && p.ProtectedResource.String() != "" { + params.Add("resource", p.ProtectedResource.String()) + } + + var req *http.Request + req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) + if err != nil { + return + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + var resp *http.Response + resp, err = http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + var body []byte + body, err = ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return + } + + if resp.StatusCode != 200 { + err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body) + return + } + + var jsonResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresOn int64 `json:"expires_on,string"` + IDToken string `json:"id_token"` + } + err = json.Unmarshal(body, &jsonResponse) + if err != nil { + return + } + + s = &sessions.SessionState{ + AccessToken: jsonResponse.AccessToken, + IDToken: jsonResponse.IDToken, + CreatedAt: time.Now(), + ExpiresOn: time.Unix(jsonResponse.ExpiresOn, 0), + RefreshToken: jsonResponse.RefreshToken, + } + return +} + func getAzureHeader(accessToken string) http.Header { header := make(http.Header) header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) diff --git a/providers/azure_test.go b/providers/azure_test.go index 8d34bdc8..24745f3a 100644 --- a/providers/azure_test.go +++ b/providers/azure_test.go @@ -5,6 +5,7 @@ import ( "net/http/httptest" "net/url" "testing" + "time" "github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" @@ -20,6 +21,7 @@ func testAzureProvider(hostname string) *AzureProvider { ValidateURL: &url.URL{}, ProtectedResource: &url.URL{}, Scope: ""}) + if hostname != "" { updateURL(p.Data().LoginURL, hostname) updateURL(p.Data().RedeemURL, hostname) @@ -111,8 +113,11 @@ func testAzureBackend(payload string) *httptest.Server { return httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != path || r.URL.RawQuery != query { + if (r.URL.Path != path || r.URL.RawQuery != query) && r.Method != "POST" { w.WriteHeader(404) + } else if r.Method == "POST" && r.Body != nil { + w.WriteHeader(200) + w.Write([]byte(payload)) } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" { w.WriteHeader(403) } else { @@ -199,3 +204,19 @@ func TestAzureProviderGetEmailAddressIncorrectOtherMails(t *testing.T) { assert.Equal(t, "type assertion to string failed", err.Error()) assert.Equal(t, "", email) } + +func TestAzureProviderRedeemReturnsIdToken(t *testing.T) { + b := testAzureBackend(`{ "id_token": "testtoken1234", "expires_on": "1136239445", "refresh_token": "refresh1234" }`) + defer b.Close() + timestamp, err := time.Parse(time.RFC3339, "2006-01-02T22:04:05Z") + assert.Equal(t, nil, err) + + bURL, _ := url.Parse(b.URL) + p := testAzureProvider(bURL.Host) + p.Data().RedeemURL.Path = "/common/oauth2/token" + s, err := p.Redeem("https://localhost", "1234") + assert.Equal(t, nil, err) + assert.Equal(t, "testtoken1234", s.IDToken) + assert.Equal(t, timestamp, s.ExpiresOn.UTC()) + assert.Equal(t, "refresh1234", s.RefreshToken) +} diff --git a/validator_test.go b/validator_test.go index 6e72cdbe..58253e8e 100644 --- a/validator_test.go +++ b/validator_test.go @@ -8,30 +8,34 @@ import ( ) type ValidatorTest struct { - authEmailFile *os.File - done chan bool - updateSeen bool + authEmailFileName string + done chan bool + updateSeen bool } func NewValidatorTest(t *testing.T) *ValidatorTest { vt := &ValidatorTest{} var err error - vt.authEmailFile, err = ioutil.TempFile("", "test_auth_emails_") + f, err := ioutil.TempFile("", "test_auth_emails_") if err != nil { - t.Fatal("failed to create temp file: " + err.Error()) + t.Fatalf("failed to create temp file: %v", err) } + if err := f.Close(); err != nil { + t.Fatalf("failed to close temp file: %v", err) + } + vt.authEmailFileName = f.Name() vt.done = make(chan bool, 1) return vt } func (vt *ValidatorTest) TearDown() { vt.done <- true - os.Remove(vt.authEmailFile.Name()) + os.Remove(vt.authEmailFileName) } func (vt *ValidatorTest) NewValidator(domains []string, updated chan<- bool) func(string) bool { - return newValidatorImpl(domains, vt.authEmailFile.Name(), + return newValidatorImpl(domains, vt.authEmailFileName, vt.done, func() { if vt.updateSeen == false { updated <- true @@ -40,13 +44,18 @@ func (vt *ValidatorTest) NewValidator(domains []string, }) } -// This will close vt.authEmailFile. func (vt *ValidatorTest) WriteEmails(t *testing.T, emails []string) { - defer vt.authEmailFile.Close() - vt.authEmailFile.WriteString(strings.Join(emails, "\n")) - if err := vt.authEmailFile.Close(); err != nil { - t.Fatal("failed to close temp file " + - vt.authEmailFile.Name() + ": " + err.Error()) + f, err := os.OpenFile(vt.authEmailFileName, os.O_WRONLY, 0600) + if err != nil { + t.Fatalf("failed to open auth email file: %v", err) + } + + if _, err := f.WriteString(strings.Join(emails, "\n")); err != nil { + t.Fatalf("failed to write emails to auth email file: %v", err) + } + + if err := f.Close(); err != nil { + t.Fatalf("failed to close auth email file: %v", err) } } @@ -160,3 +169,43 @@ func TestValidatorIgnoreSpacesInAuthEmails(t *testing.T) { t.Error("email should validate") } } + +func TestValidatorOverwriteEmailListDirectly(t *testing.T) { + vt := NewValidatorTest(t) + defer vt.TearDown() + + vt.WriteEmails(t, []string{ + "xyzzy@example.com", + "plugh@example.com", + }) + domains := []string(nil) + updated := make(chan bool) + validator := vt.NewValidator(domains, updated) + + if !validator("xyzzy@example.com") { + t.Error("first email in list should validate") + } + if !validator("plugh@example.com") { + t.Error("second email in list should validate") + } + if validator("xyzzy.plugh@example.com") { + t.Error("email not in list that matches no domains " + + "should not validate") + } + + vt.WriteEmails(t, []string{ + "xyzzy.plugh@example.com", + "plugh@example.com", + }) + <-updated + + if validator("xyzzy@example.com") { + t.Error("email removed from list should not validate") + } + if !validator("plugh@example.com") { + t.Error("email retained in list should validate") + } + if !validator("xyzzy.plugh@example.com") { + t.Error("email added to list should validate") + } +} diff --git a/validator_watcher_copy_test.go b/validator_watcher_copy_test.go deleted file mode 100644 index 15ed6faf..00000000 --- a/validator_watcher_copy_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// +build go1.3,!plan9,!solaris,!windows - -// Turns out you can't copy over an existing file on Windows. - -package main - -import ( - "io/ioutil" - "os" - "testing" -) - -func (vt *ValidatorTest) UpdateEmailFileViaCopyingOver( - t *testing.T, emails []string) { - origFile := vt.authEmailFile - var err error - vt.authEmailFile, err = ioutil.TempFile("", "test_auth_emails_") - if err != nil { - t.Fatal("failed to create temp file for copy: " + err.Error()) - } - vt.WriteEmails(t, emails) - err = os.Rename(vt.authEmailFile.Name(), origFile.Name()) - if err != nil { - t.Fatal("failed to copy over temp file: " + err.Error()) - } - vt.authEmailFile = origFile -} - -func TestValidatorOverwriteEmailListViaCopyingOver(t *testing.T) { - vt := NewValidatorTest(t) - defer vt.TearDown() - - vt.WriteEmails(t, []string{"xyzzy@example.com"}) - domains := []string(nil) - updated := make(chan bool) - validator := vt.NewValidator(domains, updated) - - if !validator("xyzzy@example.com") { - t.Error("email in list should validate") - } - - vt.UpdateEmailFileViaCopyingOver(t, []string{"plugh@example.com"}) - <-updated - - if validator("xyzzy@example.com") { - t.Error("email removed from list should not validate") - } -} diff --git a/validator_watcher_test.go b/validator_watcher_test.go deleted file mode 100644 index b022d68f..00000000 --- a/validator_watcher_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// +build go1.3,!plan9,!solaris - -package main - -import ( - "io/ioutil" - "os" - "testing" -) - -func (vt *ValidatorTest) UpdateEmailFile(t *testing.T, emails []string) { - var err error - vt.authEmailFile, err = os.OpenFile( - vt.authEmailFile.Name(), os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - t.Fatal("failed to re-open temp file for updates") - } - vt.WriteEmails(t, emails) -} - -func (vt *ValidatorTest) UpdateEmailFileViaRenameAndReplace( - t *testing.T, emails []string) { - origFile := vt.authEmailFile - var err error - vt.authEmailFile, err = ioutil.TempFile("", "test_auth_emails_") - if err != nil { - t.Fatal("failed to create temp file for rename and replace: " + - err.Error()) - } - vt.WriteEmails(t, emails) - - movedName := origFile.Name() + "-moved" - err = os.Rename(origFile.Name(), movedName) - err = os.Rename(vt.authEmailFile.Name(), origFile.Name()) - if err != nil { - t.Fatal("failed to rename and replace temp file: " + - err.Error()) - } - vt.authEmailFile = origFile - os.Remove(movedName) -} - -func TestValidatorOverwriteEmailListDirectly(t *testing.T) { - vt := NewValidatorTest(t) - defer vt.TearDown() - - vt.WriteEmails(t, []string{ - "xyzzy@example.com", - "plugh@example.com", - }) - domains := []string(nil) - updated := make(chan bool) - validator := vt.NewValidator(domains, updated) - - if !validator("xyzzy@example.com") { - t.Error("first email in list should validate") - } - if !validator("plugh@example.com") { - t.Error("second email in list should validate") - } - if validator("xyzzy.plugh@example.com") { - t.Error("email not in list that matches no domains " + - "should not validate") - } - - vt.UpdateEmailFile(t, []string{ - "xyzzy.plugh@example.com", - "plugh@example.com", - }) - <-updated - - if validator("xyzzy@example.com") { - t.Error("email removed from list should not validate") - } - if !validator("plugh@example.com") { - t.Error("email retained in list should validate") - } - if !validator("xyzzy.plugh@example.com") { - t.Error("email added to list should validate") - } -} - -func TestValidatorOverwriteEmailListViaRenameAndReplace(t *testing.T) { - vt := NewValidatorTest(t) - defer vt.TearDown() - - vt.WriteEmails(t, []string{"xyzzy@example.com"}) - domains := []string(nil) - updated := make(chan bool, 1) - validator := vt.NewValidator(domains, updated) - - if !validator("xyzzy@example.com") { - t.Error("email in list should validate") - } - - vt.UpdateEmailFileViaRenameAndReplace(t, []string{"plugh@example.com"}) - <-updated - - if validator("xyzzy@example.com") { - t.Error("email removed from list should not validate") - } -}