Sign Upstream requests with HMAC. closes #147
This commit is contained in:
		
							parent
							
								
									7c241ec1fe
								
							
						
					
					
						commit
						e4626c1360
					
				
							
								
								
									
										1
									
								
								Godeps
								
								
								
								
							
							
						
						
									
										1
									
								
								Godeps
								
								
								
								
							|  | @ -1,3 +1,4 @@ | |||
| github.com/18F/hmacauth                  1.0.1 | ||||
| github.com/BurntSushi/toml               3883ac1ce943878302255f538fce319d23226223 | ||||
| github.com/bitly/go-simplejson           3378bdcb5cebedcbf8b5750edee28010f128fe24 | ||||
| github.com/mreiferson/go-options         ee94b57f2fbf116075426f853e5abbcdfeca8b3d | ||||
|  |  | |||
							
								
								
									
										29
									
								
								README.md
								
								
								
								
							
							
						
						
									
										29
									
								
								README.md
								
								
								
								
							|  | @ -113,15 +113,16 @@ An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is i | |||
| 
 | ||||
| ``` | ||||
| Usage of oauth2_proxy: | ||||
|   -approval_prompt="force": Oauth approval_prompt | ||||
|   -approval-prompt="force": Oauth approval_prompt | ||||
|   -authenticated-emails-file="": authenticate against emails via file (one per line) | ||||
|   -basic-auth-password="": the password to set when passing the HTTP Basic Auth header | ||||
|   -client-id="": the OAuth Client ID: ie: "123456.apps.googleusercontent.com" | ||||
|   -client-secret="": the OAuth Client Secret | ||||
|   -config="": path to config file | ||||
|   -cookie-domain="": an optional cookie domain to force cookies to (ie: .yourcompany.com)* | ||||
|   -cookie-expire=168h0m0s: expire timeframe for cookie | ||||
|   -cookie-httponly=true: set HttpOnly cookie flag | ||||
|   -cookie-key="_oauth2_proxy": the name of the cookie that the oauth_proxy creates | ||||
|   -cookie-name="_oauth2_proxy": the name of the cookie that the oauth_proxy creates | ||||
|   -cookie-refresh=0: refresh the cookie after this duration; 0 to disable | ||||
|   -cookie-secret="": the seed string for secure cookies | ||||
|   -cookie-secure=true: set secure (HTTPS) cookie flag | ||||
|  | @ -130,17 +131,15 @@ Usage of oauth2_proxy: | |||
|   -email-domain=: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email | ||||
|   -github-org="": restrict logins to members of this organisation | ||||
|   -github-team="": restrict logins to members of this team | ||||
|   -google-group="": restrict logins to members of this google group | ||||
|   -google-admin-email="": the google admin to impersonate for api calls | ||||
|   -google-group=: restrict logins to members of this google group (may be given multiple times). | ||||
|   -google-service-account-json="": the path to the service account json credentials | ||||
| 
 | ||||
|   -htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption | ||||
|   -http-address="127.0.0.1:4180": [http://]<addr>:<port> or unix://<path> to listen on for HTTP clients | ||||
|   -https-address=":443": <addr>:<port> to listen on for HTTPS clients | ||||
|   -login-url="": Authentication endpoint | ||||
|   -pass-access-token=false: pass OAuth access_token to upstream via X-Forwarded-Access-Token header | ||||
|   -pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream | ||||
|   -basic-auth-password="": the password to set when passing the HTTP Basic Auth header | ||||
|   -pass-host-header=true: pass the request Host Header to upstream | ||||
|   -profile-url="": Profile access endpoint | ||||
|   -provider="google": OAuth provider | ||||
|  | @ -149,6 +148,7 @@ Usage of oauth2_proxy: | |||
|   -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" | ||||
|   -request-logging=true: Log requests to stdout | ||||
|   -scope="": Oauth scope specification | ||||
|   -signature-key="": GAP-Signature request signature key (algorithm:secretkey) | ||||
|   -skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times) | ||||
|   -tls-cert="": path to certificate file | ||||
|   -tls-key="": path to private key file | ||||
|  | @ -250,6 +250,24 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w | |||
| * /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url. | ||||
| * /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request) | ||||
| 
 | ||||
| ## Request signatures | ||||
| 
 | ||||
| If `signature_key` is defined, proxied requests will be signed with the | ||||
| `GAP-Signature` header, which is a [Hash-based Message Authentication Code | ||||
| (HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) | ||||
| of selected request information and the request body [see `SIGNATURE_HEADERS` | ||||
| in `oauthproxy.go`](./oauthproxy.go). | ||||
| 
 | ||||
| `signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`) | ||||
| 
 | ||||
| For more information about HMAC request signature validation, read the | ||||
| following: | ||||
| 
 | ||||
| * [Amazon Web Services: Signing and Authenticating REST | ||||
|   Requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html) | ||||
| * [rc3.org: Using HMAC to authenticate Web service | ||||
|   requests](http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/) | ||||
| 
 | ||||
| ## Logging Format | ||||
| 
 | ||||
| OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log. | ||||
|  | @ -258,7 +276,6 @@ OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log. | |||
| <REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION> | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| ## Adding a new Provider | ||||
| 
 | ||||
| Follow the examples in the [`providers` package](providers/) to define a new | ||||
|  |  | |||
							
								
								
									
										2
									
								
								main.go
								
								
								
								
							
							
						
						
									
										2
									
								
								main.go
								
								
								
								
							|  | @ -69,6 +69,8 @@ func main() { | |||
| 	flagSet.String("scope", "", "OAuth scope specification") | ||||
| 	flagSet.String("approval-prompt", "force", "OAuth approval_prompt") | ||||
| 
 | ||||
| 	flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") | ||||
| 
 | ||||
| 	flagSet.Parse(os.Args[1:]) | ||||
| 
 | ||||
| 	if *showVersion { | ||||
|  |  | |||
|  | @ -14,10 +14,26 @@ import ( | |||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/18F/hmacauth" | ||||
| 	"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 | ||||
|  | @ -54,10 +70,15 @@ type OAuthProxy struct { | |||
| 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) | ||||
| } | ||||
| 
 | ||||
|  | @ -89,6 +110,11 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) { | |||
| 
 | ||||
| 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 { | ||||
|  | @ -101,14 +127,15 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { | |||
| 			} else { | ||||
| 				setProxyDirector(proxy) | ||||
| 			} | ||||
| 			serveMux.Handle(path, &UpstreamProxy{u.Host, 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}) | ||||
| 			serveMux.Handle(path, &UpstreamProxy{path, proxy, nil}) | ||||
| 		default: | ||||
| 			panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme)) | ||||
| 		} | ||||
|  |  | |||
|  | @ -1,9 +1,12 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"encoding/base64" | ||||
| 	"github.com/18F/hmacauth" | ||||
| 	"github.com/bitly/oauth2_proxy/providers" | ||||
| 	"github.com/bmizerany/assert" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"net" | ||||
|  | @ -88,6 +91,45 @@ func TestRobotsTxt(t *testing.T) { | |||
| 	assert.Equal(t, "User-agent: *\nDisallow: /", rw.Body.String()) | ||||
| } | ||||
| 
 | ||||
| type TestProvider struct { | ||||
| 	*providers.ProviderData | ||||
| 	EmailAddress string | ||||
| 	ValidToken   bool | ||||
| } | ||||
| 
 | ||||
| func NewTestProvider(provider_url *url.URL, email_address string) *TestProvider { | ||||
| 	return &TestProvider{ | ||||
| 		ProviderData: &providers.ProviderData{ | ||||
| 			ProviderName: "Test Provider", | ||||
| 			LoginURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/oauth/authorize", | ||||
| 			}, | ||||
| 			RedeemURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/oauth/token", | ||||
| 			}, | ||||
| 			ProfileURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/api/v1/profile", | ||||
| 			}, | ||||
| 			Scope: "profile.email", | ||||
| 		}, | ||||
| 		EmailAddress: email_address, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) { | ||||
| 	return tp.EmailAddress, nil | ||||
| } | ||||
| 
 | ||||
| func (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool { | ||||
| 	return tp.ValidToken | ||||
| } | ||||
| 
 | ||||
| func TestBasicAuthPassword(t *testing.T) { | ||||
| 	provider_server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		log.Printf("%#v", r) | ||||
|  | @ -121,29 +163,7 @@ func TestBasicAuthPassword(t *testing.T) { | |||
| 	const email_address = "michael.bland@gsa.gov" | ||||
| 	const user_name = "michael.bland" | ||||
| 
 | ||||
| 	opts.provider = &TestProvider{ | ||||
| 		ProviderData: &providers.ProviderData{ | ||||
| 			ProviderName: "Test Provider", | ||||
| 			LoginURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/oauth/authorize", | ||||
| 			}, | ||||
| 			RedeemURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/oauth/token", | ||||
| 			}, | ||||
| 			ProfileURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/api/v1/profile", | ||||
| 			}, | ||||
| 			Scope: "profile.email", | ||||
| 		}, | ||||
| 		EmailAddress: email_address, | ||||
| 	} | ||||
| 
 | ||||
| 	opts.provider = NewTestProvider(provider_url, email_address) | ||||
| 	proxy := NewOAuthProxy(opts, func(email string) bool { | ||||
| 		return email == email_address | ||||
| 	}) | ||||
|  | @ -183,20 +203,6 @@ func TestBasicAuthPassword(t *testing.T) { | |||
| 	provider_server.Close() | ||||
| } | ||||
| 
 | ||||
| type TestProvider struct { | ||||
| 	*providers.ProviderData | ||||
| 	EmailAddress string | ||||
| 	ValidToken   bool | ||||
| } | ||||
| 
 | ||||
| func (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) { | ||||
| 	return tp.EmailAddress, nil | ||||
| } | ||||
| 
 | ||||
| func (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool { | ||||
| 	return tp.ValidToken | ||||
| } | ||||
| 
 | ||||
| type PassAccessTokenTest struct { | ||||
| 	provider_server *httptest.Server | ||||
| 	proxy           *OAuthProxy | ||||
|  | @ -242,29 +248,7 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes | |||
| 	provider_url, _ := url.Parse(t.provider_server.URL) | ||||
| 	const email_address = "michael.bland@gsa.gov" | ||||
| 
 | ||||
| 	t.opts.provider = &TestProvider{ | ||||
| 		ProviderData: &providers.ProviderData{ | ||||
| 			ProviderName: "Test Provider", | ||||
| 			LoginURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/oauth/authorize", | ||||
| 			}, | ||||
| 			RedeemURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/oauth/token", | ||||
| 			}, | ||||
| 			ProfileURL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   provider_url.Host, | ||||
| 				Path:   "/api/v1/profile", | ||||
| 			}, | ||||
| 			Scope: "profile.email", | ||||
| 		}, | ||||
| 		EmailAddress: email_address, | ||||
| 	} | ||||
| 
 | ||||
| 	t.opts.provider = NewTestProvider(provider_url, email_address) | ||||
| 	t.proxy = NewOAuthProxy(t.opts, func(email string) bool { | ||||
| 		return email == email_address | ||||
| 	}) | ||||
|  | @ -559,7 +543,7 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) { | |||
| func NewAuthOnlyEndpointTest() *ProcessCookieTest { | ||||
| 	pc_test := NewProcessCookieTestWithDefaults() | ||||
| 	pc_test.req, _ = http.NewRequest("GET", | ||||
| 		pc_test.opts.ProxyPrefix + "/auth", nil) | ||||
| 		pc_test.opts.ProxyPrefix+"/auth", nil) | ||||
| 	return pc_test | ||||
| } | ||||
| 
 | ||||
|  | @ -610,3 +594,143 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) { | |||
| 	bodyBytes, _ := ioutil.ReadAll(test.rw.Body) | ||||
| 	assert.Equal(t, "unauthorized request\n", string(bodyBytes)) | ||||
| } | ||||
| 
 | ||||
| type SignatureAuthenticator struct { | ||||
| 	auth hmacauth.HmacAuth | ||||
| } | ||||
| 
 | ||||
| func (v *SignatureAuthenticator) Authenticate( | ||||
| 	w http.ResponseWriter, r *http.Request) { | ||||
| 	result, headerSig, computedSig := v.auth.AuthenticateRequest(r) | ||||
| 	if result == hmacauth.ResultNoSignature { | ||||
| 		w.Write([]byte("no signature received")) | ||||
| 	} else if result == hmacauth.ResultMatch { | ||||
| 		w.Write([]byte("signatures match")) | ||||
| 	} else if result == hmacauth.ResultMismatch { | ||||
| 		w.Write([]byte("signatures do not match:" + | ||||
| 			"\n  received: " + headerSig + | ||||
| 			"\n  computed: " + computedSig)) | ||||
| 	} else { | ||||
| 		panic("Unknown result value: " + result.String()) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type SignatureTest struct { | ||||
| 	opts          *Options | ||||
| 	upstream      *httptest.Server | ||||
| 	upstream_host string | ||||
| 	provider      *httptest.Server | ||||
| 	header        http.Header | ||||
| 	rw            *httptest.ResponseRecorder | ||||
| 	authenticator *SignatureAuthenticator | ||||
| } | ||||
| 
 | ||||
| func NewSignatureTest() *SignatureTest { | ||||
| 	opts := NewOptions() | ||||
| 	opts.CookieSecret = "cookie secret" | ||||
| 	opts.ClientID = "client ID" | ||||
| 	opts.ClientSecret = "client secret" | ||||
| 	opts.EmailDomains = []string{"acm.org"} | ||||
| 
 | ||||
| 	authenticator := &SignatureAuthenticator{} | ||||
| 	upstream := httptest.NewServer( | ||||
| 		http.HandlerFunc(authenticator.Authenticate)) | ||||
| 	upstream_url, _ := url.Parse(upstream.URL) | ||||
| 	opts.Upstreams = append(opts.Upstreams, upstream.URL) | ||||
| 
 | ||||
| 	providerHandler := func(w http.ResponseWriter, r *http.Request) { | ||||
| 		w.Write([]byte(`{"access_token": "my_auth_token"}`)) | ||||
| 	} | ||||
| 	provider := httptest.NewServer(http.HandlerFunc(providerHandler)) | ||||
| 	provider_url, _ := url.Parse(provider.URL) | ||||
| 	opts.provider = NewTestProvider(provider_url, "mbland@acm.org") | ||||
| 
 | ||||
| 	return &SignatureTest{ | ||||
| 		opts, | ||||
| 		upstream, | ||||
| 		upstream_url.Host, | ||||
| 		provider, | ||||
| 		make(http.Header), | ||||
| 		httptest.NewRecorder(), | ||||
| 		authenticator, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (st *SignatureTest) Close() { | ||||
| 	st.provider.Close() | ||||
| 	st.upstream.Close() | ||||
| } | ||||
| 
 | ||||
| // fakeNetConn simulates an http.Request.Body buffer that will be consumed
 | ||||
| // when it is read by the hmacauth.HmacAuth if not handled properly. See:
 | ||||
| //   https://github.com/18F/hmacauth/pull/4
 | ||||
| type fakeNetConn struct { | ||||
| 	reqBody string | ||||
| } | ||||
| 
 | ||||
| func (fnc *fakeNetConn) Read(p []byte) (n int, err error) { | ||||
| 	if bodyLen := len(fnc.reqBody); bodyLen != 0 { | ||||
| 		copy(p, fnc.reqBody) | ||||
| 		fnc.reqBody = "" | ||||
| 		return bodyLen, io.EOF | ||||
| 	} | ||||
| 	return 0, io.EOF | ||||
| } | ||||
| 
 | ||||
| func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) { | ||||
| 	err := st.opts.Validate() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	proxy := NewOAuthProxy(st.opts, func(email string) bool { return true }) | ||||
| 
 | ||||
| 	var bodyBuf io.ReadCloser | ||||
| 	if body != "" { | ||||
| 		bodyBuf = ioutil.NopCloser(&fakeNetConn{reqBody: body}) | ||||
| 	} | ||||
| 	req, err := http.NewRequest(method, "/foo/bar", bodyBuf) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	req.Header = st.header | ||||
| 
 | ||||
| 	state := &providers.SessionState{ | ||||
| 		Email: "mbland@acm.org", AccessToken: "my_access_token"} | ||||
| 	value, err := proxy.provider.CookieForSession(state, proxy.CookieCipher) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	cookie := proxy.MakeCookie(req, value, proxy.CookieExpire, time.Now()) | ||||
| 	req.AddCookie(cookie) | ||||
| 	// This is used by the upstream to validate the signature.
 | ||||
| 	st.authenticator.auth = hmacauth.NewHmacAuth( | ||||
| 		crypto.SHA1, []byte(key), SignatureHeader, SignatureHeaders) | ||||
| 	proxy.ServeHTTP(st.rw, req) | ||||
| } | ||||
| 
 | ||||
| func TestNoRequestSignature(t *testing.T) { | ||||
| 	st := NewSignatureTest() | ||||
| 	defer st.Close() | ||||
| 	st.MakeRequestWithExpectedKey("GET", "", "") | ||||
| 	assert.Equal(t, 200, st.rw.Code) | ||||
| 	assert.Equal(t, st.rw.Body.String(), "no signature received") | ||||
| } | ||||
| 
 | ||||
| func TestRequestSignatureGetRequest(t *testing.T) { | ||||
| 	st := NewSignatureTest() | ||||
| 	defer st.Close() | ||||
| 	st.opts.SignatureKey = "sha1:foobar" | ||||
| 	st.MakeRequestWithExpectedKey("GET", "", "foobar") | ||||
| 	assert.Equal(t, 200, st.rw.Code) | ||||
| 	assert.Equal(t, st.rw.Body.String(), "signatures match") | ||||
| } | ||||
| 
 | ||||
| func TestRequestSignaturePostRequest(t *testing.T) { | ||||
| 	st := NewSignatureTest() | ||||
| 	defer st.Close() | ||||
| 	st.opts.SignatureKey = "sha1:foobar" | ||||
| 	payload := `{ "hello": "world!" }` | ||||
| 	st.MakeRequestWithExpectedKey("POST", payload, "foobar") | ||||
| 	assert.Equal(t, 200, st.rw.Code) | ||||
| 	assert.Equal(t, st.rw.Body.String(), "signatures match") | ||||
| } | ||||
|  |  | |||
							
								
								
									
										33
									
								
								options.go
								
								
								
								
							
							
						
						
									
										33
									
								
								options.go
								
								
								
								
							|  | @ -1,6 +1,7 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
|  | @ -8,6 +9,7 @@ import ( | |||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/18F/hmacauth" | ||||
| 	"github.com/bitly/oauth2_proxy/providers" | ||||
| ) | ||||
| 
 | ||||
|  | @ -60,11 +62,19 @@ type Options struct { | |||
| 
 | ||||
| 	RequestLogging bool `flag:"request-logging" cfg:"request_logging"` | ||||
| 
 | ||||
| 	SignatureKey string `flag:"signature-key" cfg:"signature_key"` | ||||
| 
 | ||||
| 	// internal values that are set after config validation
 | ||||
| 	redirectURL   *url.URL | ||||
| 	proxyURLs     []*url.URL | ||||
| 	CompiledRegex []*regexp.Regexp | ||||
| 	provider      providers.Provider | ||||
| 	signatureData *SignatureData | ||||
| } | ||||
| 
 | ||||
| type SignatureData struct { | ||||
| 	hash crypto.Hash | ||||
| 	key  string | ||||
| } | ||||
| 
 | ||||
| func NewOptions() *Options { | ||||
|  | @ -175,6 +185,8 @@ func (o *Options) Validate() error { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	msgs = parseSignatureKey(o, msgs) | ||||
| 
 | ||||
| 	if len(msgs) != 0 { | ||||
| 		return fmt.Errorf("Invalid configuration:\n  %s", | ||||
| 			strings.Join(msgs, "\n  ")) | ||||
|  | @ -210,3 +222,24 @@ func parseProviderInfo(o *Options, msgs []string) []string { | |||
| 	} | ||||
| 	return msgs | ||||
| } | ||||
| 
 | ||||
| func parseSignatureKey(o *Options, msgs []string) []string { | ||||
| 	if o.SignatureKey == "" { | ||||
| 		return msgs | ||||
| 	} | ||||
| 
 | ||||
| 	components := strings.Split(o.SignatureKey, ":") | ||||
| 	if len(components) != 2 { | ||||
| 		return append(msgs, "invalid signature hash:key spec: "+ | ||||
| 			o.SignatureKey) | ||||
| 	} | ||||
| 
 | ||||
| 	algorithm, secretKey := components[0], components[1] | ||||
| 	if hash, err := hmacauth.DigestNameToCryptoHash(algorithm); err != nil { | ||||
| 		return append(msgs, "unsupported signature hash algorithm: "+ | ||||
| 			o.SignatureKey) | ||||
| 	} else { | ||||
| 		o.signatureData = &SignatureData{hash, secretKey} | ||||
| 	} | ||||
| 	return msgs | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | @ -166,3 +167,27 @@ func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) { | |||
| 	o.CookieRefresh -= time.Duration(1) | ||||
| 	assert.Equal(t, nil, o.Validate()) | ||||
| } | ||||
| 
 | ||||
| func TestValidateSignatureKey(t *testing.T) { | ||||
| 	o := testOptions() | ||||
| 	o.SignatureKey = "sha1:secret" | ||||
| 	assert.Equal(t, nil, o.Validate()) | ||||
| 	assert.Equal(t, o.signatureData.hash, crypto.SHA1) | ||||
| 	assert.Equal(t, o.signatureData.key, "secret") | ||||
| } | ||||
| 
 | ||||
| func TestValidateSignatureKeyInvalidSpec(t *testing.T) { | ||||
| 	o := testOptions() | ||||
| 	o.SignatureKey = "invalid spec" | ||||
| 	err := o.Validate() | ||||
| 	assert.Equal(t, err.Error(), "Invalid configuration:\n"+ | ||||
| 		"  invalid signature hash:key spec: "+o.SignatureKey) | ||||
| } | ||||
| 
 | ||||
| func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) { | ||||
| 	o := testOptions() | ||||
| 	o.SignatureKey = "unsupported:default secret" | ||||
| 	err := o.Validate() | ||||
| 	assert.Equal(t, err.Error(), "Invalid configuration:\n"+ | ||||
| 		"  unsupported signature hash algorithm: "+o.SignatureKey) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue