Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Support AWS style hmac signatures on requests to upstream servers. #310

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions backends/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package backends

import (
"log"
"net/http"
"net/url"

"github.com/smartystreets/go-aws-auth"
)

func registerNewAwsBackend(u *url.URL, opts *Options, serveMux *http.ServeMux) {
path := u.Path
u.Path = ""
log.Printf("mapping path %q => aws-upstream %q", path, u)

proxy := NewReverseProxy(u)
serveMux.Handle(path, &awsProxy{u, proxy, opts})
}

type awsProxy struct {
upstream *url.URL
handler http.Handler
options *Options
}

func (a *awsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.URL.Host = a.upstream.Host
r.URL.Scheme = a.upstream.Scheme
r.Host = a.upstream.Host

awsauth.Sign(r, awsauth.Credentials{
AccessKeyID: a.options.AwsAccessKeyId,
SecretAccessKey: a.options.AwsSecretAccessKey,
})
a.handler.ServeHTTP(w, r)
}
135 changes: 135 additions & 0 deletions backends/backends.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package backends

import (
"crypto"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"

"github.com/18F/hmacauth"
)

const (
BackendTypeAws = "aws"
BackendTypeDefault = "default"
)

func Register(backendType string, u *url.URL, opts *Options, serveMux *http.ServeMux) {
switch backendType {
case BackendTypeDefault:
registerNewDefaultBackend(u, opts, serveMux)
case BackendTypeAws:
registerNewAwsBackend(u, opts, serveMux)
default:
panic(fmt.Errorf("Invalid backendType: %s", backendType))
}
}

// Default Backend Methods

// GAP-Auth Signatures
const GAPSignatureHeader = "GAP-Signature"

var GAPSignatureHeaders []string = []string{
"Content-Length",
"Content-Md5",
"Content-Type",
"Date",
"Authorization",
"X-Forwarded-User",
"X-Forwarded-Email",
"X-Forwarded-Access-Token",
"Cookie",
"Gap-Auth",
}

type GAPSignatureData struct {
Hash crypto.Hash
Key string
}

type Options struct {
SignatureData *GAPSignatureData
PassHostHeader bool
AwsAccessKeyId string
AwsSecretAccessKey string
}

// this is what is used to handle urls in the "upstreams" option flag
func registerNewDefaultBackend(u *url.URL, opts *Options, serveMux *http.ServeMux) {
// handle gap auth
var auth hmacauth.HmacAuth
if sigData := opts.SignatureData; sigData != nil {
auth = hmacauth.NewHmacAuth(sigData.Hash, []byte(sigData.Key),
GAPSignatureHeader, GAPSignatureHeaders)
}
path := u.Path
switch u.Scheme {
case "http", "https":
u.Path = ""
log.Printf("mapping path %q => upstream %q", path, u)
proxy := NewReverseProxy(u)
if !opts.PassHostHeader {
setProxyUpstreamHostHeader(proxy, u)
} else {
setProxyDirector(proxy)
}
serveMux.Handle(path,
&UpstreamProxy{u, proxy, auth})
case "file":
if u.Fragment != "" {
path = u.Fragment
}
log.Printf("mapping path %q => file system %q", path, u.Path)
proxy := NewFileServer(path, u.Path)
serveMux.Handle(path, &UpstreamProxy{u, proxy, nil})
default:
panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
}
}

type UpstreamProxy struct {
upstream *url.URL
handler http.Handler
auth hmacauth.HmacAuth
}

func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("GAP-Upstream-Address", u.upstream.Host)
if u.auth != nil {
r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
u.auth.SignRequest(r)
}
u.handler.ServeHTTP(w, r)
}

func NewReverseProxy(target *url.URL) (proxy *httputil.ReverseProxy) {
return httputil.NewSingleHostReverseProxy(target)
}

func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
// use RequestURI so that we aren't unescaping encoded slashes in the request path
req.Host = target.Host
req.URL.Opaque = req.RequestURI
req.URL.RawQuery = ""
}
}

func setProxyDirector(proxy *httputil.ReverseProxy) {
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
// use RequestURI so that we aren't unescaping encoded slashes in the request path
req.URL.Opaque = req.RequestURI
req.URL.RawQuery = ""
}
}

func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
}
62 changes: 62 additions & 0 deletions backends/backends_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package backends

import (
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)

func TestNewReverseProxy(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
hostname, _, _ := net.SplitHostPort(r.Host)
w.Write([]byte(hostname))
}))
defer backend.Close()

backendURL, _ := url.Parse(backend.URL)
backendHostname, backendPort, _ := net.SplitHostPort(backendURL.Host)
backendHost := net.JoinHostPort(backendHostname, backendPort)
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")

proxyHandler := NewReverseProxy(proxyURL)
setProxyUpstreamHostHeader(proxyHandler, proxyURL)
frontend := httptest.NewServer(proxyHandler)
defer frontend.Close()

getReq, _ := http.NewRequest("GET", frontend.URL, nil)
res, _ := http.DefaultClient.Do(getReq)
bodyBytes, _ := ioutil.ReadAll(res.Body)
if g, e := string(bodyBytes), backendHostname; g != e {
t.Errorf("got body %q; expected %q", g, e)
}
}

func TestEncodedSlashes(t *testing.T) {
var seen string
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
seen = r.RequestURI
}))
defer backend.Close()

b, _ := url.Parse(backend.URL)
proxyHandler := NewReverseProxy(b)
setProxyDirector(proxyHandler)
frontend := httptest.NewServer(proxyHandler)
defer frontend.Close()

f, _ := url.Parse(frontend.URL)
encodedPath := "/a%2Fb/?c=1"
getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: f.Host, Opaque: encodedPath}}
_, err := http.DefaultClient.Do(getReq)
if err != nil {
t.Fatalf("err %s", err)
}
if seen != encodedPath {
t.Errorf("got bad request %q expected %q", seen, encodedPath)
}
}
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func main() {

emailDomains := StringArray{}
upstreams := StringArray{}
awsUpstreams := StringArray{}
skipAuthRegex := StringArray{}
googleGroups := StringArray{}

Expand Down Expand Up @@ -75,6 +76,10 @@ func main() {

flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")

flagSet.Var(&awsUpstreams, "aws-upstream", "the http url(s) of upstream servers that expect requests to be signed with AWS HMAC signatures.")
flagSet.String("aws-access-key-id", "", "the aws access key id to use when generating hmac signatures for aws-upstreams")
flagSet.String("aws-secret-access-key", "", "the aws secret access key to use when generating hmac signatures for aws-upstreams")

flagSet.Parse(os.Args[1:])

if *showVersion {
Expand Down
92 changes: 5 additions & 87 deletions oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,16 @@ import (
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"strings"
"time"

"github.com/18F/hmacauth"
"github.com/bitly/oauth2_proxy/backends"
"github.com/bitly/oauth2_proxy/cookie"
"github.com/bitly/oauth2_proxy/providers"
)

const SignatureHeader = "GAP-Signature"

var SignatureHeaders []string = []string{
"Content-Length",
"Content-Md5",
"Content-Type",
"Date",
"Authorization",
"X-Forwarded-User",
"X-Forwarded-Email",
"X-Forwarded-Access-Token",
"Cookie",
"Gap-Auth",
}

type OAuthProxy struct {
CookieSeed string
CookieName string
Expand Down Expand Up @@ -69,79 +53,13 @@ type OAuthProxy struct {
Footer string
}

type UpstreamProxy struct {
upstream string
handler http.Handler
auth hmacauth.HmacAuth
}

func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("GAP-Upstream-Address", u.upstream)
if u.auth != nil {
r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
u.auth.SignRequest(r)
}
u.handler.ServeHTTP(w, r)
}

func NewReverseProxy(target *url.URL) (proxy *httputil.ReverseProxy) {
return httputil.NewSingleHostReverseProxy(target)
}
func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
// use RequestURI so that we aren't unescaping encoded slashes in the request path
req.Host = target.Host
req.URL.Opaque = req.RequestURI
req.URL.RawQuery = ""
}
}
func setProxyDirector(proxy *httputil.ReverseProxy) {
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
// use RequestURI so that we aren't unescaping encoded slashes in the request path
req.URL.Opaque = req.RequestURI
req.URL.RawQuery = ""
}
}
func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
}

func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
serveMux := http.NewServeMux()
var auth hmacauth.HmacAuth
if sigData := opts.signatureData; sigData != nil {
auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
SignatureHeader, SignatureHeaders)
}
for _, u := range opts.proxyURLs {
path := u.Path
switch u.Scheme {
case "http", "https":
u.Path = ""
log.Printf("mapping path %q => upstream %q", path, u)
proxy := NewReverseProxy(u)
if !opts.PassHostHeader {
setProxyUpstreamHostHeader(proxy, u)
} else {
setProxyDirector(proxy)
}
serveMux.Handle(path,
&UpstreamProxy{u.Host, proxy, auth})
case "file":
if u.Fragment != "" {
path = u.Fragment
}
log.Printf("mapping path %q => file system %q", path, u.Path)
proxy := NewFileServer(path, u.Path)
serveMux.Handle(path, &UpstreamProxy{path, proxy, nil})
default:
panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
}

for _, p := range opts.proxyURLs {
backends.Register(p.BackendType, p.Url, p.Options, serveMux)
}

for _, u := range opts.CompiledRegex {
log.Printf("compiled skip-auth-regex => %q", u)
}
Expand Down
Loading