From e9a5f9cea87e986d1122b9c026981d1619d55189 Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Thu, 28 Jun 2018 13:09:59 +0200 Subject: [PATCH 01/13] Add first hacky version --- main.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 main.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..3f78454 --- /dev/null +++ b/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "net/http/cookiejar" + "net/http/httputil" + "net/url" + "time" + + "golang.org/x/net/publicsuffix" +) + +//https://github.com/pkg/browser + +func main() { + proxyAddr := ":8124" + + baseURL, err := url.Parse("https://SETME") + if err != nil { + log.Fatalf("Failed to parse baseURL: %v", err) + } + + jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + if err != nil { + log.Fatalf("Failed to create cookie jar: %v", err) + } + + client := http.Client{ + Jar: jar, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/oauth2/callback", func(w http.ResponseWriter, r *http.Request) { + realCallbackURL, _ := r.URL.Parse(r.URL.String()) + realCallbackURL.Scheme = baseURL.Scheme + realCallbackURL.Host = baseURL.Host + // realCallbackURL.Path = "/oauth2/callback" + + resp, err := client.Get(realCallbackURL.String()) + if err != nil { + log.Fatalf("Failed to fetch callback URL: %v", err) + http.Error(w, "Failed to fetch callback URL", http.StatusInternalServerError) + return + } + + var authCookie *http.Cookie + for _, c := range resp.Cookies() { + if c.Name == "_oauth2_proxy" { + authCookie = c + } + } + + w.Write([]byte(fmt.Sprintf( + "Authentication complete. You can now use the proxy at %s\n\n"+ + "Upstream: %s\n\n"+ + "You can close this window.", + proxyAddr, baseURL))) + log.Printf("Cookie: %+v", authCookie) + log.Printf("Response: %+v", resp) + + reverseProxyDirector := func(r *http.Request) { + log.Printf("Original request: %+v", r) + r.Host = baseURL.Host + r.URL.Scheme = baseURL.Scheme + r.URL.Host = baseURL.Host + + r.AddCookie(authCookie) + + log.Printf("Forwarding request: %+v", r) + log.Printf("Request URI: %v", r.RequestURI) + } + + proxyHandler := &httputil.ReverseProxy{ + Director: reverseProxyDirector, + } + + proxyServer := &http.Server{ + Addr: proxyAddr, + Handler: proxyHandler, + } + go proxyServer.ListenAndServe() + + }) + + server := &http.Server{ + Addr: ":8123", + Handler: serverMux, + } + go server.ListenAndServe() + + resp, err := client.Get(baseURL.String() + "/oauth2/start") + if err != nil { + log.Fatalf("Request failed: %v", err) + } + + identityProviderURL := resp.Header.Get("Location") + if identityProviderURL == "" { + log.Fatalf("Got empty identity provider URL in response: %+v", resp) + } + + log.Printf("Go to %s", identityProviderURL) + time.Sleep(1 * time.Hour) +} From b8666239fbedcefb18f2f4bf6e0f965e61eef06c Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Thu, 28 Jun 2018 13:35:11 +0200 Subject: [PATCH 02/13] Automatically open browser --- Gopkg.lock | 15 +++++ Gopkg.toml | 34 ++++++++++ main.go | 39 +++++++----- vendor/github.com/pkg/browser/LICENSE | 23 +++++++ vendor/github.com/pkg/browser/README.md | 55 ++++++++++++++++ vendor/github.com/pkg/browser/browser.go | 62 +++++++++++++++++++ .../github.com/pkg/browser/browser_darwin.go | 5 ++ .../github.com/pkg/browser/browser_linux.go | 5 ++ .../github.com/pkg/browser/browser_openbsd.go | 14 +++++ .../pkg/browser/browser_unsupported.go | 12 ++++ .../github.com/pkg/browser/browser_windows.go | 10 +++ 11 files changed, 257 insertions(+), 17 deletions(-) create mode 100644 Gopkg.lock create mode 100644 Gopkg.toml create mode 100644 vendor/github.com/pkg/browser/LICENSE create mode 100644 vendor/github.com/pkg/browser/README.md create mode 100644 vendor/github.com/pkg/browser/browser.go create mode 100644 vendor/github.com/pkg/browser/browser_darwin.go create mode 100644 vendor/github.com/pkg/browser/browser_linux.go create mode 100644 vendor/github.com/pkg/browser/browser_openbsd.go create mode 100644 vendor/github.com/pkg/browser/browser_unsupported.go create mode 100644 vendor/github.com/pkg/browser/browser_windows.go diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..fa84bb9 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,15 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/pkg/browser" + packages = ["."] + revision = "c90ca0c84f15f81c982e32665bffd8d7aac8f097" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "68d49c04b6ed1c7c0dedc9884e56f5f5b27a703660e812cade105fa7f36a4d23" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..d0d3875 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,34 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[prune] + go-tests = true + unused-packages = true + +[[constraint]] + branch = "master" + name = "github.com/pkg/browser" diff --git a/main.go b/main.go index 3f78454..0c71a89 100644 --- a/main.go +++ b/main.go @@ -9,11 +9,9 @@ import ( "net/url" "time" - "golang.org/x/net/publicsuffix" + "github.com/pkg/browser" ) -//https://github.com/pkg/browser - func main() { proxyAddr := ":8124" @@ -22,7 +20,10 @@ func main() { log.Fatalf("Failed to parse baseURL: %v", err) } - jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + // We don't need a public suffix list for cookies - we only make requests + // to a single host, so there's no need to prevent cookies from being + // sent across domains. + jar, err := cookiejar.New(nil) if err != nil { log.Fatalf("Failed to create cookie jar: %v", err) } @@ -55,24 +56,14 @@ func main() { } } - w.Write([]byte(fmt.Sprintf( - "Authentication complete. You can now use the proxy at %s\n\n"+ - "Upstream: %s\n\n"+ - "You can close this window.", - proxyAddr, baseURL))) - log.Printf("Cookie: %+v", authCookie) - log.Printf("Response: %+v", resp) - reverseProxyDirector := func(r *http.Request) { - log.Printf("Original request: %+v", r) r.Host = baseURL.Host r.URL.Scheme = baseURL.Scheme r.URL.Host = baseURL.Host r.AddCookie(authCookie) - log.Printf("Forwarding request: %+v", r) - log.Printf("Request URI: %v", r.RequestURI) + log.Printf("Forwarding request: %v", r.RequestURI) } proxyHandler := &httputil.ReverseProxy{ @@ -85,12 +76,20 @@ func main() { } go proxyServer.ListenAndServe() + authCompleteText := fmt.Sprintf("Authentication complete. You can now use the proxy at %s\n\n"+ + "Upstream: %s\n\n", proxyAddr, baseURL) + + w.Write([]byte(authCompleteText + "You can close this window.")) + + log.Printf(authCompleteText + + "The proxy will automatically terminate in 12 hours (reauthentication required)") }) server := &http.Server{ Addr: ":8123", Handler: serverMux, } + // TODO check server is actually running to prevent sending auth code to another app go server.ListenAndServe() resp, err := client.Get(baseURL.String() + "/oauth2/start") @@ -103,6 +102,12 @@ func main() { log.Fatalf("Got empty identity provider URL in response: %+v", resp) } - log.Printf("Go to %s", identityProviderURL) - time.Sleep(1 * time.Hour) + if err := browser.OpenURL(identityProviderURL); err != nil { + log.Printf("Failed to open browser, please go to %s", identityProviderURL) + } + log.Printf("Please authenticate in your browser via our identity provider.") + + // TODO Fetch expiration date from cookie + time.Sleep(12 * time.Hour) + log.Printf("12 hours are up, please reauthenticate.") } diff --git a/vendor/github.com/pkg/browser/LICENSE b/vendor/github.com/pkg/browser/LICENSE new file mode 100644 index 0000000..65f78fb --- /dev/null +++ b/vendor/github.com/pkg/browser/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/browser/README.md b/vendor/github.com/pkg/browser/README.md new file mode 100644 index 0000000..72b1976 --- /dev/null +++ b/vendor/github.com/pkg/browser/README.md @@ -0,0 +1,55 @@ + +# browser + import "github.com/pkg/browser" + +Package browser provides helpers to open files, readers, and urls in a browser window. + +The choice of which browser is started is entirely client dependant. + + + + + +## Variables +``` go +var Stderr io.Writer = os.Stderr +``` +Stderr is the io.Writer to which executed commands write standard error. + +``` go +var Stdout io.Writer = os.Stdout +``` +Stdout is the io.Writer to which executed commands write standard output. + + +## func OpenFile +``` go +func OpenFile(path string) error +``` +OpenFile opens new browser window for the file path. + + +## func OpenReader +``` go +func OpenReader(r io.Reader) error +``` +OpenReader consumes the contents of r and presents the +results in a new browser window. + + +## func OpenURL +``` go +func OpenURL(url string) error +``` +OpenURL opens a new browser window pointing to url. + + + + + + + + + +- - - +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/vendor/github.com/pkg/browser/browser.go b/vendor/github.com/pkg/browser/browser.go new file mode 100644 index 0000000..d92c4cd --- /dev/null +++ b/vendor/github.com/pkg/browser/browser.go @@ -0,0 +1,62 @@ +// Package browser provides helpers to open files, readers, and urls in a browser window. +// +// The choice of which browser is started is entirely client dependant. +package browser + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" +) + +// Stdout is the io.Writer to which executed commands write standard output. +var Stdout io.Writer = os.Stdout + +// Stderr is the io.Writer to which executed commands write standard error. +var Stderr io.Writer = os.Stderr + +// OpenFile opens new browser window for the file path. +func OpenFile(path string) error { + path, err := filepath.Abs(path) + if err != nil { + return err + } + return OpenURL("file://" + path) +} + +// OpenReader consumes the contents of r and presents the +// results in a new browser window. +func OpenReader(r io.Reader) error { + f, err := ioutil.TempFile("", "browser") + if err != nil { + return fmt.Errorf("browser: could not create temporary file: %v", err) + } + if _, err := io.Copy(f, r); err != nil { + f.Close() + return fmt.Errorf("browser: caching temporary file failed: %v", err) + } + if err := f.Close(); err != nil { + return fmt.Errorf("browser: caching temporary file failed: %v", err) + } + oldname := f.Name() + newname := oldname + ".html" + if err := os.Rename(oldname, newname); err != nil { + return fmt.Errorf("browser: renaming temporary file failed: %v", err) + } + return OpenFile(newname) +} + +// OpenURL opens a new browser window pointing to url. +func OpenURL(url string) error { + return openBrowser(url) +} + +func runCmd(prog string, args ...string) error { + cmd := exec.Command(prog, args...) + cmd.Stdout = Stdout + cmd.Stderr = Stderr + return cmd.Run() +} diff --git a/vendor/github.com/pkg/browser/browser_darwin.go b/vendor/github.com/pkg/browser/browser_darwin.go new file mode 100644 index 0000000..8507cf7 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_darwin.go @@ -0,0 +1,5 @@ +package browser + +func openBrowser(url string) error { + return runCmd("open", url) +} diff --git a/vendor/github.com/pkg/browser/browser_linux.go b/vendor/github.com/pkg/browser/browser_linux.go new file mode 100644 index 0000000..bed47dd --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_linux.go @@ -0,0 +1,5 @@ +package browser + +func openBrowser(url string) error { + return runCmd("xdg-open", url) +} diff --git a/vendor/github.com/pkg/browser/browser_openbsd.go b/vendor/github.com/pkg/browser/browser_openbsd.go new file mode 100644 index 0000000..4fc7ff0 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_openbsd.go @@ -0,0 +1,14 @@ +package browser + +import ( + "errors" + "os/exec" +) + +func openBrowser(url string) error { + err := runCmd("xdg-open", url) + if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { + return errors.New("xdg-open: command not found - install xdg-utils from ports(8)") + } + return err +} diff --git a/vendor/github.com/pkg/browser/browser_unsupported.go b/vendor/github.com/pkg/browser/browser_unsupported.go new file mode 100644 index 0000000..e29d220 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux,!windows,!darwin,!openbsd + +package browser + +import ( + "fmt" + "runtime" +) + +func openBrowser(url string) error { + return fmt.Errorf("openBrowser: unsupported operating system: %v", runtime.GOOS) +} diff --git a/vendor/github.com/pkg/browser/browser_windows.go b/vendor/github.com/pkg/browser/browser_windows.go new file mode 100644 index 0000000..f65e0ee --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_windows.go @@ -0,0 +1,10 @@ +package browser + +import ( + "strings" +) + +func openBrowser(url string) error { + r := strings.NewReplacer("&", "^&") + return runCmd("cmd", "/c", "start", r.Replace(url)) +} From acd010f12dd6f5216ed7034e48396569646b338d Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Thu, 28 Jun 2018 13:33:25 +0200 Subject: [PATCH 03/13] Ignore binary --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2503e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/oauth2-dev-proxy From 6f1fd17236b528a476984c65cfcfd1f38170d56f Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Thu, 28 Jun 2018 13:41:32 +0200 Subject: [PATCH 04/13] Listen on localhost only (security) --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 0c71a89..85a2360 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) func main() { - proxyAddr := ":8124" + proxyAddr := "127.0.0.1:8124" baseURL, err := url.Parse("https://SETME") if err != nil { @@ -86,7 +86,7 @@ func main() { }) server := &http.Server{ - Addr: ":8123", + Addr: "127.0.0.1:8123", Handler: serverMux, } // TODO check server is actually running to prevent sending auth code to another app From 787626c02f664c376edd8d1b733329e6cd60a66c Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Thu, 28 Jun 2018 13:44:04 +0200 Subject: [PATCH 05/13] Make upstream URL configurable --- main.go | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index 85a2360..e7a43ea 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "net/http/cookiejar" "net/http/httputil" "net/url" + "os" "time" "github.com/pkg/browser" @@ -15,9 +16,19 @@ import ( func main() { proxyAddr := "127.0.0.1:8124" - baseURL, err := url.Parse("https://SETME") + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, + "Usage: %s UPSTREAM_URL\n"+ + "Example: %s https://proxy-dev.example.com\n\n"+ + "Note: The oauth2_proxy running at UPSTREAM_URL must be configured with a\n"+ + " special redirect URL for this development proxy.\n", + os.Args[0], os.Args[0]) + os.Exit(1) + } + + upstreamURL, err := url.Parse(os.Args[1]) if err != nil { - log.Fatalf("Failed to parse baseURL: %v", err) + log.Fatalf("Failed to parse target URL: %v", err) } // We don't need a public suffix list for cookies - we only make requests @@ -38,8 +49,8 @@ func main() { serverMux := http.NewServeMux() serverMux.HandleFunc("/oauth2/callback", func(w http.ResponseWriter, r *http.Request) { realCallbackURL, _ := r.URL.Parse(r.URL.String()) - realCallbackURL.Scheme = baseURL.Scheme - realCallbackURL.Host = baseURL.Host + realCallbackURL.Scheme = upstreamURL.Scheme + realCallbackURL.Host = upstreamURL.Host // realCallbackURL.Path = "/oauth2/callback" resp, err := client.Get(realCallbackURL.String()) @@ -57,9 +68,9 @@ func main() { } reverseProxyDirector := func(r *http.Request) { - r.Host = baseURL.Host - r.URL.Scheme = baseURL.Scheme - r.URL.Host = baseURL.Host + r.Host = upstreamURL.Host + r.URL.Scheme = upstreamURL.Scheme + r.URL.Host = upstreamURL.Host r.AddCookie(authCookie) @@ -76,8 +87,9 @@ func main() { } go proxyServer.ListenAndServe() - authCompleteText := fmt.Sprintf("Authentication complete. You can now use the proxy at %s\n\n"+ - "Upstream: %s\n\n", proxyAddr, baseURL) + authCompleteText := fmt.Sprintf("Authentication complete. You can now use the proxy at "+ + "http://%s\n\n"+ + "Upstream: %s\n\n", proxyAddr, upstreamURL) w.Write([]byte(authCompleteText + "You can close this window.")) @@ -92,7 +104,7 @@ func main() { // TODO check server is actually running to prevent sending auth code to another app go server.ListenAndServe() - resp, err := client.Get(baseURL.String() + "/oauth2/start") + resp, err := client.Get(upstreamURL.String() + "/oauth2/start") if err != nil { log.Fatalf("Request failed: %v", err) } From dd6d8526d487362104ede6160e26fedc0d22af37 Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Thu, 28 Jun 2018 13:46:08 +0200 Subject: [PATCH 06/13] Improve success message --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index e7a43ea..dafe1df 100644 --- a/main.go +++ b/main.go @@ -87,8 +87,8 @@ func main() { } go proxyServer.ListenAndServe() - authCompleteText := fmt.Sprintf("Authentication complete. You can now use the proxy at "+ - "http://%s\n\n"+ + authCompleteText := fmt.Sprintf("Authentication complete. You can now use the proxy.\n\n"+ + "Proxy URL: http://%s\n"+ "Upstream: %s\n\n", proxyAddr, upstreamURL) w.Write([]byte(authCompleteText + "You can close this window.")) From 0273ddfe123fa08ae1ecaad03e3167cf03d9cada Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Tue, 10 Jul 2018 09:01:24 +0200 Subject: [PATCH 07/13] Add README --- README.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..dea82c2 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# OAuth2 API Proxy for Development + +A reverse proxy to be run on development machines that allows transparent access to APIs and other HTTP resources behind an [oauth2_proxy](https://github.com/bitly/oauth2_proxy) authenticating reverse proxy. + +After authenticating via an OpenID Connect identity provider in the default browser, the dev proxy transparently adds the authentication cookie used by *oauth2_proxy* to requests directed to the dev proxy. + +## Why? + +* No need to modify API clients to add credentials +* No need to store/manage credentials on development machines +* Authentication via identity provider + - No manually managed credentials on proxy (e.g. basic auth htpasswd file) + - Centralized user managemnent via provider +* Convenience: Authentication in default browser to leverage active identity provider session + +## Usage + +1. Start the dev proxy: `oauth2-dev-proxy `, e.g. `oauth2-dev-proxy https://api-staging.example.com` +2. The dev proxy automatically opens the identity provider's page in your default browser. +3. Log in to your identity provider - if you're already logged in there, authentication may happen without manual interaction. +4. The proxy shows a success browser page including the local HTTP endpoint you can use for accessing the API you provided in step 1. + +Example success message in the browser: + +``` +Authentication complete. You can now use the proxy. + +Proxy URL: http://127.0.0.1:8124 +Upstream: https://api-staging.example.com + +You can close this window. +``` + +Example output in the shell: + +``` +2018/07/10 08:29:45 Please authenticate in your browser via our identity provider. +2018/07/10 08:29:46 Authentication complete. You can now use the proxy. + +Proxy URL: http://127.0.0.1:8124 +Upstream: https://api-staging.example.com + +The proxy will automatically terminate in 12 hours (reauthentication required) +``` + +## Installation + +### Binary + +Download the binary from the [releases page](https://github.com/FRI-DAY/oauth2-dev-proxy/releases) and make it executable: + + VERSION="0.0.1-alpha1"; OS="darwin" # or linux or windows + curl --silent --location https://github.com/FRI-DAY/oauth2-dev-proxy/releases/download/v${VERSION}/oauth2-dev-proxy_${VERSION}_${OS}_amd64 \ + > /usr/local/bin/oauth2-dev-proxy && \ + chmod +x /usr/local/bin/oauth2-dev-proxy + +### Source + + go get github.com/FRI-DAY/oauth2-dev-proxy + +## oauth2_proxy Setup + +You deploy [oauth2_proxy](https://github.com/bitly/oauth2_proxy) as ususal, the only difference is to set a special redirect URL: + + -redirect-url=http://127.0.0.1:8123/oauth2/callback + +You also need to allow this redirect URL in your identity provider's configuration. + +## Architecture + +The following diagram shows HTTP requests made by the different components. + +``` + Development machine ++------------------------------------------+ +--------------+ +-----------------+ +| | | | | | +| API Client +-------> oauth2-dev-proxy +----> | oauth2-proxy | +-> | Service (API) | +| (e.g. curl) ^ | | | | | +| | | +------+-------+ +-----------------+ +| | | | +| Browser +-----------------+ | +------v-----------+ +| +-----------------------------------> | | +| | | Identity Provider| ++------------------------------------------+ | | + +------------------+ +``` + +The *dev proxy* uses a trick to allow authentication with the identity provider in the local browser, while making the requests to the *oauth2_proxy* directly, so the *dev proxy* can capture the authentication cookie from *oauth2_proxy*. + +The trick requires a separate *oauth2_proxy* instance to be deployed with a redirect URL of `http://127.0.0.1:8123/oauth2/callback`. + +From the dev proxy's point of view, the process works like this: + +1. Start an HTTP server on port 8123 +2. Request `TARGET/oauth2/start` + - Store the CSRF cookie + - Note the identity provider URL the proxy redirects to +3. Open the identity provider URL in the local default browser +4. The user authenticates to the identity provider +5. The identity provider redirects to `http://127.0.0.1:8123/oauth2/callback` +6. Forward the `/oauth2/callback` request to the *oauth2_proxy*, including the auth code + - From the response, note the `_oauth2_proxy` cookie +7. Start a reverse proxy server to the target URL on another port, configured to transparently add the `_oauth2_proxy` cookie to all requests +8. After the cookie expires, shut down the proxy From 63b64d565e47e3ef8b02b21a8106644f1341c093 Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Tue, 10 Jul 2018 10:17:09 +0200 Subject: [PATCH 08/13] Add Apache 2.0 license --- LICENSE | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 7303a8787bc265bf2f65fb7a94d46f2d63097a9d Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Tue, 10 Jul 2018 10:20:08 +0200 Subject: [PATCH 09/13] Add .travis.yml --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ba73bdb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: go +go: + - "1.10" +os: + - linux From b05fa83c94b80e698d61b2a1ef64cef6dcdd7cb4 Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Tue, 10 Jul 2018 12:24:55 +0200 Subject: [PATCH 10/13] Refactor auth and reverse proxy into separate funcs --- main.go | 99 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/main.go b/main.go index dafe1df..db9f9af 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "log" "net/http" @@ -31,6 +32,22 @@ func main() { log.Fatalf("Failed to parse target URL: %v", err) } + authCompleteNotice := fmt.Sprintf("Authentication complete. You can now use the proxy.\n\n"+ + "Proxy URL: http://%s\n"+ + "Upstream: %s\n\n", proxyAddr, upstreamURL) + + authCookie := authenticate(upstreamURL, authCompleteNotice) + + go runProxy(proxyAddr, upstreamURL, authCookie) + + log.Printf("The proxy will automatically terminate in 12 hours (reauthentication required)") + + // TODO Fetch expiration date from cookie + time.Sleep(12 * time.Hour) + log.Printf("12 hours are up, please reauthenticate.") +} + +func authenticate(upstreamURL *url.URL, authCompleteNotice string) *http.Cookie { // We don't need a public suffix list for cookies - we only make requests // to a single host, so there's no need to prevent cookies from being // sent across domains. @@ -46,12 +63,13 @@ func main() { }, } + authCookieCh := make(chan *http.Cookie) + serverMux := http.NewServeMux() serverMux.HandleFunc("/oauth2/callback", func(w http.ResponseWriter, r *http.Request) { realCallbackURL, _ := r.URL.Parse(r.URL.String()) realCallbackURL.Scheme = upstreamURL.Scheme realCallbackURL.Host = upstreamURL.Host - // realCallbackURL.Path = "/oauth2/callback" resp, err := client.Get(realCallbackURL.String()) if err != nil { @@ -60,49 +78,25 @@ func main() { return } - var authCookie *http.Cookie for _, c := range resp.Cookies() { if c.Name == "_oauth2_proxy" { - authCookie = c + w.Write([]byte(authCompleteNotice + "You can close this window.")) + authCookieCh <- c + break } } - - reverseProxyDirector := func(r *http.Request) { - r.Host = upstreamURL.Host - r.URL.Scheme = upstreamURL.Scheme - r.URL.Host = upstreamURL.Host - - r.AddCookie(authCookie) - - log.Printf("Forwarding request: %v", r.RequestURI) - } - - proxyHandler := &httputil.ReverseProxy{ - Director: reverseProxyDirector, - } - - proxyServer := &http.Server{ - Addr: proxyAddr, - Handler: proxyHandler, - } - go proxyServer.ListenAndServe() - - authCompleteText := fmt.Sprintf("Authentication complete. You can now use the proxy.\n\n"+ - "Proxy URL: http://%s\n"+ - "Upstream: %s\n\n", proxyAddr, upstreamURL) - - w.Write([]byte(authCompleteText + "You can close this window.")) - - log.Printf(authCompleteText + - "The proxy will automatically terminate in 12 hours (reauthentication required)") }) server := &http.Server{ Addr: "127.0.0.1:8123", Handler: serverMux, } - // TODO check server is actually running to prevent sending auth code to another app - go server.ListenAndServe() + + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start auth callback server: %v", err) + } + }() resp, err := client.Get(upstreamURL.String() + "/oauth2/start") if err != nil { @@ -119,7 +113,38 @@ func main() { } log.Printf("Please authenticate in your browser via our identity provider.") - // TODO Fetch expiration date from cookie - time.Sleep(12 * time.Hour) - log.Printf("12 hours are up, please reauthenticate.") + // Wait for authentication callback, see handler above + authCookie := <-authCookieCh + + ctx, ctxCancelFn := context.WithTimeout(context.Background(), 1*time.Second) + _ = server.Shutdown(ctx) + ctxCancelFn() + + log.Printf(authCompleteNotice) + + return authCookie +} + +func runProxy(addr string, upstreamURL *url.URL, authCookie *http.Cookie) { + director := func(r *http.Request) { + r.Host = upstreamURL.Host + r.URL.Scheme = upstreamURL.Scheme + r.URL.Host = upstreamURL.Host + + r.AddCookie(authCookie) + + log.Printf("Forwarding request: %v", r.RequestURI) + } + + handler := &httputil.ReverseProxy{ + Director: director, + } + + proxyServer := &http.Server{ + Addr: addr, + Handler: handler, + } + if err := proxyServer.ListenAndServe(); err != nil { + log.Fatalf("Failed to start reverse proxy: %v", err) + } } From 15c30afef26651b78e36c77f2e85e89c9801132b Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Tue, 10 Jul 2018 12:35:49 +0200 Subject: [PATCH 11/13] Add goreleaser config --- .goreleaser.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .goreleaser.yml diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..d335c1e --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,11 @@ +builds: + - binary: oauth2-dev-proxy + goos: + - darwin + - linux + - windows + goarch: + - amd64 +archive: + # An archive just makes installation harder + format: binary From 269ac3f047cc82d4c220ccf10774bbfa3bfc6855 Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Tue, 17 Jul 2018 13:37:56 +0200 Subject: [PATCH 12/13] Quit if oauth2_proxy doesn't return cookie Better to tell users about the problem than just doing nothing. --- main.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index db9f9af..9d2f5ca 100644 --- a/main.go +++ b/main.go @@ -78,13 +78,20 @@ func authenticate(upstreamURL *url.URL, authCompleteNotice string) *http.Cookie return } + var authCookie *http.Cookie for _, c := range resp.Cookies() { if c.Name == "_oauth2_proxy" { w.Write([]byte(authCompleteNotice + "You can close this window.")) - authCookieCh <- c + authCookie = c break } } + if authCookie == nil { + log.Printf("Response from oauth2_proxy:%v\n\n", resp) + log.Fatalf("Error: Expected _oauth2_proxy cookie not found in response " + + "from oauth2_proxy. Please restart the dev proxy and try again.") + } + authCookieCh <- authCookie }) server := &http.Server{ From a1bc14a7bc7a1ff19308ff99bb538f43cdba3f9b Mon Sep 17 00:00:00 2001 From: Michael Frister Date: Tue, 17 Jul 2018 13:54:38 +0200 Subject: [PATCH 13/13] Make proxy listen address configurable The auth callback address must be fixed, so we can allow the redirect_uri at the identity provider. --- main.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 9d2f5ca..e4f168d 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "flag" "fmt" "log" "net/http" @@ -15,30 +16,38 @@ import ( ) func main() { - proxyAddr := "127.0.0.1:8124" + proxyAddr := flag.String("addr", "127.0.0.1:8124", + "Listen on this address for reverse proxy requests") - if len(os.Args) != 2 { + flag.Usage = func() { fmt.Fprintf(os.Stderr, - "Usage: %s UPSTREAM_URL\n"+ + "Usage: %s [flags] UPSTREAM_URL\n"+ "Example: %s https://proxy-dev.example.com\n\n"+ "Note: The oauth2_proxy running at UPSTREAM_URL must be configured with a\n"+ - " special redirect URL for this development proxy.\n", + " special redirect URL for this development proxy.\n\n", os.Args[0], os.Args[0]) + flag.PrintDefaults() + } + + flag.Parse() + + if len(flag.Args()) != 1 { + flag.Usage() os.Exit(1) } - upstreamURL, err := url.Parse(os.Args[1]) + upstreamURL, err := url.Parse(flag.Args()[0]) if err != nil { log.Fatalf("Failed to parse target URL: %v", err) } authCompleteNotice := fmt.Sprintf("Authentication complete. You can now use the proxy.\n\n"+ "Proxy URL: http://%s\n"+ - "Upstream: %s\n\n", proxyAddr, upstreamURL) + "Upstream: %s\n\n", *proxyAddr, upstreamURL) authCookie := authenticate(upstreamURL, authCompleteNotice) - go runProxy(proxyAddr, upstreamURL, authCookie) + go runProxy(*proxyAddr, upstreamURL, authCookie) log.Printf("The proxy will automatically terminate in 12 hours (reauthentication required)")