Skip to content

Commit

Permalink
Sign upstream requests with a verifiable digital signature.
Browse files Browse the repository at this point in the history
  • Loading branch information
katzdm committed Nov 12, 2018
1 parent cba4491 commit 3dd156b
Show file tree
Hide file tree
Showing 5 changed files with 502 additions and 17 deletions.
72 changes: 56 additions & 16 deletions internal/proxy/oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (
"github.com/datadog/datadog-go/statsd"
)

// SignatureHeader is the header name where the signed request header is stored.
const SignatureHeader = "Gap-Signature"
// HMACSignatureHeader is the header name where the signed request header is stored.
const HMACSignatureHeader = "Gap-Signature"

// SignatureHeaders are the headers that are valid in the request.
var SignatureHeaders = []string{
Expand Down Expand Up @@ -74,6 +74,9 @@ type OAuthProxy struct {

mux map[string]*route
regexRoutes []*route

requestSigner *RequestSigner
publicCertsJSON string
}

type route struct {
Expand All @@ -93,11 +96,12 @@ type StateParameter struct {

// UpstreamProxy stores information necessary for proxying the request back to the upstream.
type UpstreamProxy struct {
name string
cookieName string
handler http.Handler
auth hmacauth.HmacAuth
statsdClient *statsd.Client
name string
cookieName string
handler http.Handler
auth hmacauth.HmacAuth
requestSigner *RequestSigner
statsdClient *statsd.Client
}

// deleteSSOCookieHeader deletes the session cookie from the request header string.
Expand All @@ -118,6 +122,9 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if u.auth != nil {
u.auth.SignRequest(r)
}
if u.requestSigner != nil {
u.requestSigner.Sign(r)
}

start := time.Now()
u.handler.ServeHTTP(w, r)
Expand Down Expand Up @@ -211,13 +218,14 @@ func NewRewriteReverseProxy(route *RewriteRoute, config *UpstreamConfig) *httput
}

// NewReverseProxyHandler creates a new http.Handler given a httputil.ReverseProxy
func NewReverseProxyHandler(reverseProxy *httputil.ReverseProxy, opts *Options, config *UpstreamConfig) (http.Handler, []string) {
func NewReverseProxyHandler(reverseProxy *httputil.ReverseProxy, opts *Options, config *UpstreamConfig, signer *RequestSigner) (http.Handler, []string) {
upstreamProxy := &UpstreamProxy{
name: config.Service,
handler: reverseProxy,
auth: config.HMACAuth,
cookieName: opts.CookieName,
statsdClient: opts.StatsdClient,
name: config.Service,
handler: reverseProxy,
auth: config.HMACAuth,
cookieName: opts.CookieName,
statsdClient: opts.StatsdClient,
requestSigner: signer,
}
if config.FlushInterval != 0 {
return NewStreamingHandler(upstreamProxy, opts, config), []string{"handler:streaming"}
Expand Down Expand Up @@ -255,7 +263,7 @@ func generateHmacAuth(signatureKey string) (hmacauth.HmacAuth, error) {
if err != nil {
return nil, fmt.Errorf("unsupported signature hash algorithm: %s", algorithm)
}
auth := hmacauth.NewHmacAuth(hash, []byte(secret), SignatureHeader, SignatureHeaders)
auth := hmacauth.NewHmacAuth(hash, []byte(secret), HMACSignatureHeader, SignatureHeaders)
return auth, nil
}

Expand Down Expand Up @@ -283,6 +291,25 @@ func NewOAuthProxy(opts *Options, optFuncs ...func(*OAuthProxy) error) (*OAuthPr
c.Run()
}()

// Configure the RequestSigner (used to sign requests with `Octoboi-Signature` header).
// Also build the `certs` static JSON-string which will be served from a public endpoint.
// The key published at this endpoint allows upstreams to decrypt the `Octoboi-Signature`
// header, and validate the integrity and authenticity of a request.
var certs = map[string]string{}
var requestSigner *RequestSigner
if len(opts.RequestSigningKey) > 0 {
if requestSigner, err = NewRequestSigner(opts.RequestSigningKey); err != nil {
return nil, fmt.Errorf("could not build RequestSigner: %s", err)
}
certs["signing_key"] = requestSigner.PublicKey()
} else {
logger.Warn("Running OAuthProxy without signing key. Requests will not be signed.")
}
certsAsStr, err := json.MarshalIndent(certs, "", " ")
if err != nil {
return nil, fmt.Errorf("could not marshal public certs as JSON: %s", err)
}

p := &OAuthProxy{
CookieCipher: cipher,
CookieDomain: opts.CookieDomain,
Expand All @@ -303,6 +330,9 @@ func NewOAuthProxy(opts *Options, optFuncs ...func(*OAuthProxy) error) (*OAuthPr
redirectURL: &url.URL{Path: "/oauth2/callback"},
skipAuthPreflight: opts.SkipAuthPreflight,
templates: getTemplates(),

requestSigner: requestSigner,
publicCertsJSON: string(certsAsStr),
}

for _, optFunc := range optFuncs {
Expand All @@ -316,11 +346,13 @@ func NewOAuthProxy(opts *Options, optFuncs ...func(*OAuthProxy) error) (*OAuthPr
switch route := upstreamConfig.Route.(type) {
case *SimpleRoute:
reverseProxy := NewReverseProxy(route.ToURL, upstreamConfig)
handler, tags := NewReverseProxyHandler(reverseProxy, opts, upstreamConfig)
handler, tags := NewReverseProxyHandler(
reverseProxy, opts, upstreamConfig, requestSigner)
p.Handle(route.FromURL.Host, handler, tags, upstreamConfig)
case *RewriteRoute:
reverseProxy := NewRewriteReverseProxy(route, upstreamConfig)
handler, tags := NewReverseProxyHandler(reverseProxy, opts, upstreamConfig)
handler, tags := NewReverseProxyHandler(
reverseProxy, opts, upstreamConfig, requestSigner)
p.HandleRegex(route.FromRegex, handler, tags, upstreamConfig)
default:
return nil, fmt.Errorf("unknown route type")
Expand All @@ -335,6 +367,7 @@ func (p *OAuthProxy) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/favicon.ico", p.Favicon)
mux.HandleFunc("/robots.txt", p.RobotsTxt)
mux.HandleFunc("/oauth2/certs", p.Certs)
mux.HandleFunc("/oauth2/sign_out", p.SignOut)
mux.HandleFunc("/oauth2/callback", p.OAuthCallback)
mux.HandleFunc("/oauth2/auth", p.AuthenticateOnly)
Expand Down Expand Up @@ -533,6 +566,13 @@ func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter, _ *http.Request) {
fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
}

// Certs publishes the public key necessary for upstream services to validate the digital signature
// used to sign each request.
func (p *OAuthProxy) Certs(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusOK)
fmt.Fprint(rw, p.publicCertsJSON)
}

// Favicon will proxy the request as usual if the user is already authenticated
// but responds with a 404 otherwise, to avoid spurious and confusing
// authentication attempts when a browser automatically requests the favicon on
Expand Down
69 changes: 68 additions & 1 deletion internal/proxy/oauthproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,73 @@ func TestRobotsTxt(t *testing.T) {
testutil.Equal(t, "User-agent: *\nDisallow: /", rw.Body.String())
}

func TestCerts(t *testing.T) {
opts := NewOptions()
opts.ClientID = "bazquux"
opts.ClientSecret = "foobar"
opts.CookieSecret = testEncodedCookieSecret
opts.ProviderURLString = "https://auth.sso.dev"
opts.upstreamConfigs = generateTestUpstreamConfigs("foo-internal.sso.dev")

// Dummy key; generated only for this test.
opts.RequestSigningKey = string(`-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCy38IQCH8QyeNF
s1zA0XuIyqnTcSfYZg0nPfB+K//pFy7tIOAwmR6th8NykrxFhEQDHKNCmLXt4j8V
FDHQZtGjUBHRmAXZW8NOQ0EI1vc/Dpt09sU40JQlXZZeL+9/7iAxEfSE3TQr1k7P
Xwxpjm9rsLSn7FoLnvXco0mc6+d2jjxf4cMgJIaQLKOd783KUQzLVEvBQJ05JnpI
2xMjS0q33ltMTMGF3QZQN9i4bZKgnItomKxTJbfxftO11FTNLB7og94sWmlThAY5
/UMjZaWYJ1g89+WUJ+KpVYyJsHPBBkaQG+NYazcLDyIowpzJ1WVkInysshpTqwT+
UPV4at+jAgMBAAECggEAX8lxK5LRMJVcLlwRZHQJekRE0yS6WKi1jHkfywEW5qRy
jatYQs4MXpLgN/+Z8IQWw6/XQXdznTLV4xzQXDBjPNhI4ntNTotUOBnNvsUW296f
ou/uxzDy1FuchU2YLGLBPGXIEko+gOcfhu74P6J1yi5zX6UyxxxVvtR2PCEb7yDw
m2881chwMblZ5Z8uyF++ajkK3/rqLk64w29+K4ZTDbTcCp5NtBYx2qSEU7yp12rc
qscUGqxG00Abx+osI3cUn0kOq7356LeR1rfA15yZwOb+s28QYp2WPlVB2hOiYXQv
+ttEOpt0x1QJhBAsFgwY173sD5w2MryRQb1RCwBvqQKBgQDeTdbRzxzAl83h/mAq
5I+pNEz57veAFVO+iby7TbZ/0w6q+QeT+bHF+TjGHiSlbtg3nd9NPrex2UjiN7ej
+DrxhsSLsP1ZfwDNv6f1Ii1HluJclUFSUNU/LntBjqqCJ959lniNp1y5+ZQ/j2Rf
+ZraVsHRB0itilFeAl5+n7CfxwKBgQDN/K+E1TCbp1inU60Lc9zeb8fqTEP6Mp36
qQ0Dp+KMLPJ0xQSXFq9ILr4hTJlBqfmTkfmQUcQuwercZ3LNQPbsuIg96bPW73R1
toXjokd6jUn5sJXCOE0RDumcJrL1VRf9RN1AmM4CgCc/adUMjws3pBc5R4An7UyU
ouRQhN+5RQKBgFOVTrzqM3RSX22mWAAomb9T09FxQQueeTM91IFUMdcTwwMTyP6h
Nm8qSmdrM/ojmBYpPKlteGHdQaMUse5rybXAJywiqs84ilPRyNPJOt8c4xVOZRYP
IG62Ck/W1VNErEnqBn+0OpAOP+g6ANJ5JfkL/6mZJIFjbT58g4z2e9FHAoGBAM3f
uBkd7lgTuLJ8Gh6xLVYQCJHuqZ49ytFE9qHpwK5zGdyFMSJE5OlS9mpXoXEUjkHk
iraoUlidLbwdlIr6XBCaGmku07SFXTNtOoIZpjEhV4c762HTXYsoCWos733uD2zt
z+iJEJVFOnTRtMK5kO+KjD+Oa9L8BCcmauTi+Ku1AoGAZBUzi95THA60hPXI0hm/
o0J5mfLkFPfhpUmDAMaEpv3bM4byA+IGXSZVc1IZO6cGoaeUHD2Yl1m9a5tv5rF+
FS9Ht+IgATvGojah+xxQy+kf6tRB9Hn4scyq+64AesXlDbWDEagomQ0hyV/JKSS6
LQatvnCmBd9omRT2uwYUo+o=
-----END PRIVATE KEY-----`)
opts.Validate()

expectedPublicKey := string(`-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAst/CEAh/EMnjRbNcwNF7iMqp03En2GYNJz3wfiv/6Rcu7SDgMJke
rYfDcpK8RYREAxyjQpi17eI/FRQx0GbRo1AR0ZgF2VvDTkNBCNb3Pw6bdPbFONCU
JV2WXi/vf+4gMRH0hN00K9ZOz18MaY5va7C0p+xaC5713KNJnOvndo48X+HDICSG
kCyjne/NylEMy1RLwUCdOSZ6SNsTI0tKt95bTEzBhd0GUDfYuG2SoJyLaJisUyW3
8X7TtdRUzSwe6IPeLFppU4QGOf1DI2WlmCdYPPfllCfiqVWMibBzwQZGkBvjWGs3
Cw8iKMKcydVlZCJ8rLIaU6sE/lD1eGrfowIDAQAB
-----END RSA PUBLIC KEY-----
`)

proxy, err := NewOAuthProxy(opts)
if err != nil {
t.Errorf("unexpected error %s", err)
return
}
rw := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "https://foo.sso.dev/oauth2/certs", nil)
proxy.Handler().ServeHTTP(rw, req)
testutil.Equal(t, 200, rw.Code)

var certs map[string]string
if err := json.Unmarshal([]byte(rw.Body.String()), &certs); err != nil {
t.Errorf("failed to unmarshal certs from json response: %s", err)
return
}
testutil.Equal(t, expectedPublicKey, certs["signing_key"])
}

func TestFavicon(t *testing.T) {
opts := NewOptions()
opts.ClientID = "bazquux"
Expand Down Expand Up @@ -975,7 +1042,7 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
req.AddCookie(cookie)
// This is used by the upstream to validate the signature.
st.authenticator.auth = hmacauth.NewHmacAuth(
crypto.SHA1, []byte(key), SignatureHeader, SignatureHeaders)
crypto.SHA1, []byte(key), HMACSignatureHeader, SignatureHeaders)
proxy.Handler().ServeHTTP(st.rw, req)
}

Expand Down
2 changes: 2 additions & 0 deletions internal/proxy/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ type Options struct {
StatsdHost string `envconfig:"STATSD_HOST"`
StatsdPort int `envconfig:"STATSD_PORT"`

RequestSigningKey string `envconfig:"REQUEST_SIGNATURE_KEY"`

StatsdClient *statsd.Client

// This is an override for supplying template vars at test time
Expand Down
Loading

0 comments on commit 3dd156b

Please sign in to comment.