commit
						9047920e90
					
				|  | @ -77,6 +77,7 @@ Usage of google_auth_proxy: | |||
|   -cookie-expire=168h0m0s: expire timeframe for cookie | ||||
|   -cookie-httponly=true: set HttpOnly cookie flag | ||||
|   -cookie-https-only=true: set secure (HTTPS) cookies (deprecated. use --cookie-secure setting) | ||||
|   -cookie-refresh=0: refresh the cookie when less than this much time remains before expiration; 0 to disable | ||||
|   -cookie-secret="": the seed string for secure cookies | ||||
|   -cookie-secure=true: set secure (HTTPS) cookie flag | ||||
|   -custom-templates-dir="": path to custom html templates | ||||
|  | @ -96,6 +97,7 @@ Usage of google_auth_proxy: | |||
|   -scope="": Oauth scope specification | ||||
|   -skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times) | ||||
|   -upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path | ||||
|   -validate-url="": Access token validation endpoint | ||||
|   -version=false: print version string | ||||
| ``` | ||||
| 
 | ||||
|  |  | |||
|  | @ -46,12 +46,17 @@ | |||
| 
 | ||||
| 
 | ||||
| ## Cookie Settings | ||||
| ## Secret - the seed string for secure cookies | ||||
| ## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes | ||||
| ##          for use with an AES cipher when cookie_refresh or pass_access_code | ||||
| ##          is set | ||||
| ## Domain - optional cookie domain to force cookies to (ie: .yourcompany.com) | ||||
| ## Expire - expire timeframe for cookie | ||||
| ## Refresh - refresh the cookie when less than this much time remains before | ||||
| ##           expiration; should be less than cookie_expire; set to 0 to disable | ||||
| # cookie_secret = "" | ||||
| # cookie_domain = "" | ||||
| # cookie_expire = "168h" | ||||
| # cookie_refresh = "144h" | ||||
| # cookie_secure = true | ||||
| # cookie_httponly = true | ||||
| 
 | ||||
| # pass_access_code = true | ||||
|  |  | |||
|  | @ -15,11 +15,11 @@ import ( | |||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func validateCookie(cookie *http.Cookie, seed string) (string, bool) { | ||||
| func validateCookie(cookie *http.Cookie, seed string) (string, time.Time, bool) { | ||||
| 	// value, timestamp, sig
 | ||||
| 	parts := strings.Split(cookie.Value, "|") | ||||
| 	if len(parts) != 3 { | ||||
| 		return "", false | ||||
| 		return "", time.Unix(0, 0), false | ||||
| 	} | ||||
| 	sig := cookieSignature(seed, cookie.Name, parts[0], parts[1]) | ||||
| 	if checkHmac(parts[2], sig) { | ||||
|  | @ -28,11 +28,11 @@ func validateCookie(cookie *http.Cookie, seed string) (string, bool) { | |||
| 			// it's a valid cookie. now get the contents
 | ||||
| 			rawValue, err := base64.URLEncoding.DecodeString(parts[0]) | ||||
| 			if err == nil { | ||||
| 				return string(rawValue), true | ||||
| 				return string(rawValue), time.Unix(int64(ts), 0), true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return "", false | ||||
| 	return "", time.Unix(0, 0), false | ||||
| } | ||||
| 
 | ||||
| func signedCookieValue(seed string, key string, value string) string { | ||||
|  |  | |||
							
								
								
									
										2
									
								
								main.go
								
								
								
								
							
							
						
						
									
										2
									
								
								main.go
								
								
								
								
							|  | @ -45,6 +45,7 @@ func main() { | |||
| 	flagSet.String("cookie-secret", "", "the seed string for secure cookies") | ||||
| 	flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*") | ||||
| 	flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") | ||||
| 	flagSet.Duration("cookie-refresh", time.Duration(0)*time.Hour, "refresh the cookie when less than this much time remains before expiration; 0 to disable") | ||||
| 	flagSet.Bool("cookie-https-only", true, "set secure (HTTPS) cookies (deprecated. use --cookie-secure setting)") | ||||
| 	flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") | ||||
| 	flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") | ||||
|  | @ -55,6 +56,7 @@ func main() { | |||
| 	flagSet.String("login-url", "", "Authentication endpoint") | ||||
| 	flagSet.String("redeem-url", "", "Token redemption endpoint") | ||||
| 	flagSet.String("profile-url", "", "Profile access endpoint") | ||||
| 	flagSet.String("validate-url", "", "Access token validation endpoint") | ||||
| 	flagSet.String("scope", "", "Oauth scope specification") | ||||
| 
 | ||||
| 	flagSet.Parse(os.Args[1:]) | ||||
|  |  | |||
							
								
								
									
										103
									
								
								oauthproxy.go
								
								
								
								
							
							
						
						
									
										103
									
								
								oauthproxy.go
								
								
								
								
							|  | @ -34,12 +34,14 @@ type OauthProxy struct { | |||
| 	CookieSecure   bool | ||||
| 	CookieHttpOnly bool | ||||
| 	CookieExpire   time.Duration | ||||
| 	CookieRefresh  time.Duration | ||||
| 	Validator      func(string) bool | ||||
| 
 | ||||
| 	redirectUrl         *url.URL // the url to receive requests at
 | ||||
| 	provider            providers.Provider | ||||
| 	oauthRedemptionUrl  *url.URL // endpoint to redeem the code
 | ||||
| 	oauthLoginUrl       *url.URL // to redirect the user to
 | ||||
| 	oauthValidateUrl    *url.URL // to validate the access token
 | ||||
| 	oauthScope          string | ||||
| 	clientID            string | ||||
| 	clientSecret        string | ||||
|  | @ -48,6 +50,7 @@ type OauthProxy struct { | |||
| 	DisplayHtpasswdForm bool | ||||
| 	serveMux            http.Handler | ||||
| 	PassBasicAuth       bool | ||||
| 	PassAccessToken     bool | ||||
| 	AesCipher           cipher.Block | ||||
| 	skipAuthRegex       []string | ||||
| 	compiledRegex       []*regexp.Regexp | ||||
|  | @ -121,12 +124,12 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { | |||
| 	log.Printf("Cookie settings: secure (https):%v httponly:%v expiry:%s domain:%s", opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain) | ||||
| 
 | ||||
| 	var aes_cipher cipher.Block | ||||
| 	if opts.PassAccessToken { | ||||
| 	if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) { | ||||
| 		var err error | ||||
| 		aes_cipher, err = aes.NewCipher([]byte(opts.CookieSecret)) | ||||
| 		if err != nil { | ||||
| 			log.Fatal("error creating AES cipher with "+ | ||||
| 				"pass_access_token == true: %s", err) | ||||
| 				"cookie-secret ", opts.CookieSecret, ": ", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -137,6 +140,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { | |||
| 		CookieSecure:   opts.CookieSecure, | ||||
| 		CookieHttpOnly: opts.CookieHttpOnly, | ||||
| 		CookieExpire:   opts.CookieExpire, | ||||
| 		CookieRefresh:  opts.CookieRefresh, | ||||
| 		Validator:      validator, | ||||
| 
 | ||||
| 		clientID:           opts.ClientID, | ||||
|  | @ -145,11 +149,13 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { | |||
| 		provider:           opts.provider, | ||||
| 		oauthRedemptionUrl: opts.provider.Data().RedeemUrl, | ||||
| 		oauthLoginUrl:      opts.provider.Data().LoginUrl, | ||||
| 		oauthValidateUrl:   opts.provider.Data().ValidateUrl, | ||||
| 		serveMux:           serveMux, | ||||
| 		redirectUrl:        redirectUrl, | ||||
| 		skipAuthRegex:      opts.SkipAuthRegex, | ||||
| 		compiledRegex:      opts.CompiledRegex, | ||||
| 		PassBasicAuth:      opts.PassBasicAuth, | ||||
| 		PassAccessToken:    opts.PassAccessToken, | ||||
| 		AesCipher:          aes_cipher, | ||||
| 		templates:          loadTemplates(opts.CustomTemplatesDir), | ||||
| 	} | ||||
|  | @ -224,7 +230,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) { | |||
| 	return access_token, email, nil | ||||
| } | ||||
| 
 | ||||
| func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) { | ||||
| func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration) *http.Cookie { | ||||
| 	domain := req.Host | ||||
| 	if h, _, err := net.SplitHostPort(domain); err == nil { | ||||
| 		domain = h | ||||
|  | @ -235,40 +241,76 @@ func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) { | |||
| 		} | ||||
| 		domain = p.CookieDomain | ||||
| 	} | ||||
| 	cookie := &http.Cookie{ | ||||
| 
 | ||||
| 	if value != "" { | ||||
| 		value = signedCookieValue(p.CookieSeed, p.CookieKey, value) | ||||
| 	} | ||||
| 
 | ||||
| 	return &http.Cookie{ | ||||
| 		Name:     p.CookieKey, | ||||
| 		Value:    "", | ||||
| 		Value:    value, | ||||
| 		Path:     "/", | ||||
| 		Domain:   domain, | ||||
| 		HttpOnly: p.CookieHttpOnly, | ||||
| 		Secure:   p.CookieSecure, | ||||
| 		Expires:  time.Now().Add(time.Duration(1) * time.Hour * -1), | ||||
| 		Expires:  time.Now().Add(expiration), | ||||
| 	} | ||||
| 	http.SetCookie(rw, cookie) | ||||
| } | ||||
| 
 | ||||
| func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) { | ||||
| 	http.SetCookie(rw, p.MakeCookie(req, "", time.Duration(1)*time.Hour*-1)) | ||||
| } | ||||
| 
 | ||||
| func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) { | ||||
| 	http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire)) | ||||
| } | ||||
| 
 | ||||
| 	domain := req.Host | ||||
| 	if h, _, err := net.SplitHostPort(domain); err == nil { | ||||
| 		domain = h | ||||
| func (p *OauthProxy) ValidateToken(access_token string) bool { | ||||
| 	if access_token == "" || p.oauthValidateUrl == nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	if p.CookieDomain != "" { | ||||
| 		if !strings.HasSuffix(domain, p.CookieDomain) { | ||||
| 			log.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain) | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", | ||||
| 		p.oauthValidateUrl.String()+"?access_token="+access_token, nil) | ||||
| 	if err != nil { | ||||
| 		log.Printf("failed building token validation request: %s", err) | ||||
| 		return false | ||||
| 	} | ||||
| 		domain = p.CookieDomain | ||||
| 
 | ||||
| 	httpclient := &http.Client{} | ||||
| 	resp, err := httpclient.Do(req) | ||||
| 	if err != nil { | ||||
| 		log.Printf("token validation request failed: %s", err) | ||||
| 		return false | ||||
| 	} | ||||
| 	cookie := &http.Cookie{ | ||||
| 		Name:     p.CookieKey, | ||||
| 		Value:    signedCookieValue(p.CookieSeed, p.CookieKey, val), | ||||
| 		Path:     "/", | ||||
| 		Domain:   domain, | ||||
| 		HttpOnly: p.CookieHttpOnly, | ||||
| 		Secure:   p.CookieSecure, | ||||
| 		Expires:  time.Now().Add(p.CookieExpire), | ||||
| 	return resp.StatusCode == 200 | ||||
| } | ||||
| 
 | ||||
| func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) { | ||||
| 	var value string | ||||
| 	var timestamp time.Time | ||||
| 	cookie, err := req.Cookie(p.CookieKey) | ||||
| 	if err == nil { | ||||
| 		value, timestamp, ok = validateCookie(cookie, p.CookieSeed) | ||||
| 		if ok { | ||||
| 			email, user, access_token, err = parseCookieValue( | ||||
| 				value, p.AesCipher) | ||||
| 		} | ||||
| 	http.SetCookie(rw, cookie) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		log.Printf(err.Error()) | ||||
| 		ok = false | ||||
| 	} else if p.CookieRefresh != time.Duration(0) { | ||||
| 		expires := timestamp.Add(p.CookieExpire) | ||||
| 		refresh_threshold := time.Now().Add(p.CookieRefresh) | ||||
| 		if refresh_threshold.Unix() > expires.Unix() { | ||||
| 			ok = p.Validator(email) && p.ValidateToken(access_token) | ||||
| 			if ok { | ||||
| 				p.SetCookie(rw, req, value) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (p *OauthProxy) RobotsTxt(rw http.ResponseWriter) { | ||||
|  | @ -451,18 +493,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | |||
| 	} | ||||
| 
 | ||||
| 	if !ok { | ||||
| 		cookie, err := req.Cookie(p.CookieKey) | ||||
| 		if err == nil { | ||||
| 			var value string | ||||
| 			value, ok = validateCookie(cookie, p.CookieSeed) | ||||
| 			if ok { | ||||
| 				email, user, access_token, err = parseCookieValue( | ||||
| 					value, p.AesCipher) | ||||
| 				if err != nil { | ||||
| 					log.Printf(err.Error()) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		email, user, access_token, ok = p.ProcessCookie(rw, req) | ||||
| 	} | ||||
| 
 | ||||
| 	if !ok { | ||||
|  | @ -480,7 +511,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | |||
| 		req.Header["X-Forwarded-User"] = []string{user} | ||||
| 		req.Header["X-Forwarded-Email"] = []string{email} | ||||
| 	} | ||||
| 	if access_token != "" { | ||||
| 	if p.PassAccessToken { | ||||
| 		req.Header["X-Forwarded-Access-Token"] = []string{access_token} | ||||
| 	} | ||||
| 	if email == "" { | ||||
|  |  | |||
|  | @ -321,3 +321,260 @@ func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) { | |||
| 		t.Fatal(`expected redirect to "/", but was "` + match[1] + `"`) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type ValidateTokenTest struct { | ||||
| 	opts          *Options | ||||
| 	proxy         *OauthProxy | ||||
| 	backend       *httptest.Server | ||||
| 	response_code int | ||||
| } | ||||
| 
 | ||||
| func NewValidateTokenTest() *ValidateTokenTest { | ||||
| 	var vt_test ValidateTokenTest | ||||
| 
 | ||||
| 	vt_test.opts = NewOptions() | ||||
| 	vt_test.opts.Upstreams = append(vt_test.opts.Upstreams, "unused") | ||||
| 	vt_test.opts.CookieSecret = "foobar" | ||||
| 	vt_test.opts.ClientID = "bazquux" | ||||
| 	vt_test.opts.ClientSecret = "xyzzyplugh" | ||||
| 	vt_test.opts.Validate() | ||||
| 
 | ||||
| 	vt_test.backend = httptest.NewServer( | ||||
| 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 			switch r.URL.Path { | ||||
| 			case "/oauth/tokeninfo": | ||||
| 				w.WriteHeader(vt_test.response_code) | ||||
| 				w.Write([]byte("only code matters; contents disregarded")) | ||||
| 			default: | ||||
| 				w.WriteHeader(500) | ||||
| 				w.Write([]byte("unknown URL")) | ||||
| 			} | ||||
| 		})) | ||||
| 	backend_url, _ := url.Parse(vt_test.backend.URL) | ||||
| 	vt_test.opts.provider.Data().ValidateUrl = &url.URL{ | ||||
| 		Scheme: "http", | ||||
| 		Host:   backend_url.Host, | ||||
| 		Path:   "/oauth/tokeninfo", | ||||
| 	} | ||||
| 	vt_test.response_code = 200 | ||||
| 
 | ||||
| 	vt_test.proxy = NewOauthProxy(vt_test.opts, func(email string) bool { | ||||
| 		return true | ||||
| 	}) | ||||
| 	return &vt_test | ||||
| } | ||||
| 
 | ||||
| func (vt_test *ValidateTokenTest) Close() { | ||||
| 	vt_test.backend.Close() | ||||
| } | ||||
| 
 | ||||
| func TestValidateTokenEmptyToken(t *testing.T) { | ||||
| 	vt_test := NewValidateTokenTest() | ||||
| 	defer vt_test.Close() | ||||
| 
 | ||||
| 	assert.Equal(t, false, vt_test.proxy.ValidateToken("")) | ||||
| } | ||||
| 
 | ||||
| func TestValidateTokenEmptyValidateUrl(t *testing.T) { | ||||
| 	vt_test := NewValidateTokenTest() | ||||
| 	defer vt_test.Close() | ||||
| 
 | ||||
| 	vt_test.proxy.oauthValidateUrl = nil | ||||
| 	assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar")) | ||||
| } | ||||
| 
 | ||||
| func TestValidateTokenRequestNetworkFailure(t *testing.T) { | ||||
| 	vt_test := NewValidateTokenTest() | ||||
| 	// Close immediately to simulate a network failure
 | ||||
| 	vt_test.Close() | ||||
| 
 | ||||
| 	assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar")) | ||||
| } | ||||
| 
 | ||||
| func TestValidateTokenExpiredToken(t *testing.T) { | ||||
| 	vt_test := NewValidateTokenTest() | ||||
| 	defer vt_test.Close() | ||||
| 
 | ||||
| 	vt_test.response_code = 401 | ||||
| 	assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar")) | ||||
| } | ||||
| 
 | ||||
| func TestValidateTokenValidToken(t *testing.T) { | ||||
| 	vt_test := NewValidateTokenTest() | ||||
| 	defer vt_test.Close() | ||||
| 
 | ||||
| 	assert.Equal(t, true, vt_test.proxy.ValidateToken("foobar")) | ||||
| } | ||||
| 
 | ||||
| type ProcessCookieTest struct { | ||||
| 	opts          *Options | ||||
| 	proxy         *OauthProxy | ||||
| 	rw            *httptest.ResponseRecorder | ||||
| 	req           *http.Request | ||||
| 	backend       *httptest.Server | ||||
| 	response_code int | ||||
| 	validate_user bool | ||||
| } | ||||
| 
 | ||||
| func NewProcessCookieTest() *ProcessCookieTest { | ||||
| 	var pc_test ProcessCookieTest | ||||
| 
 | ||||
| 	pc_test.opts = NewOptions() | ||||
| 	pc_test.opts.Upstreams = append(pc_test.opts.Upstreams, "unused") | ||||
| 	pc_test.opts.CookieSecret = "foobar" | ||||
| 	pc_test.opts.ClientID = "bazquux" | ||||
| 	pc_test.opts.ClientSecret = "xyzzyplugh" | ||||
| 	pc_test.opts.CookieSecret = "0123456789abcdef" | ||||
| 	// First, set the CookieRefresh option so proxy.AesCipher is created,
 | ||||
| 	// needed to encrypt the access_token.
 | ||||
| 	pc_test.opts.CookieRefresh = time.Duration(24) * time.Hour | ||||
| 	pc_test.opts.Validate() | ||||
| 
 | ||||
| 	pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool { | ||||
| 		return pc_test.validate_user | ||||
| 	}) | ||||
| 
 | ||||
| 	// Now, zero-out proxy.CookieRefresh for the cases that don't involve
 | ||||
| 	// access_token validation.
 | ||||
| 	pc_test.proxy.CookieRefresh = time.Duration(0) | ||||
| 	pc_test.rw = httptest.NewRecorder() | ||||
| 	pc_test.req, _ = http.NewRequest("GET", "/", strings.NewReader("")) | ||||
| 	pc_test.validate_user = true | ||||
| 	return &pc_test | ||||
| } | ||||
| 
 | ||||
| func (p *ProcessCookieTest) InstantiateBackend() { | ||||
| 	p.backend = httptest.NewServer( | ||||
| 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 			w.WriteHeader(p.response_code) | ||||
| 		})) | ||||
| 	backend_url, _ := url.Parse(p.backend.URL) | ||||
| 	p.proxy.oauthValidateUrl = &url.URL{ | ||||
| 		Scheme: "http", | ||||
| 		Host:   backend_url.Host, | ||||
| 		Path:   "/oauth/tokeninfo", | ||||
| 	} | ||||
| 	p.response_code = 200 | ||||
| } | ||||
| 
 | ||||
| func (p *ProcessCookieTest) Close() { | ||||
| 	p.backend.Close() | ||||
| } | ||||
| 
 | ||||
| func (p *ProcessCookieTest) MakeCookie(value, access_token string) *http.Cookie { | ||||
| 	cookie_value, _ := buildCookieValue( | ||||
| 		value, p.proxy.AesCipher, access_token) | ||||
| 	return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire) | ||||
| } | ||||
| 
 | ||||
| func (p *ProcessCookieTest) AddCookie(value, access_token string) { | ||||
| 	p.req.AddCookie(p.MakeCookie(value, access_token)) | ||||
| } | ||||
| 
 | ||||
| func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) { | ||||
| 	return p.proxy.ProcessCookie(p.rw, p.req) | ||||
| } | ||||
| 
 | ||||
| func TestProcessCookie(t *testing.T) { | ||||
| 	pc_test := NewProcessCookieTest() | ||||
| 
 | ||||
| 	pc_test.AddCookie("michael.bland@gsa.gov", "my_access_token") | ||||
| 	email, user, access_token, ok := pc_test.ProcessCookie() | ||||
| 	assert.Equal(t, true, ok) | ||||
| 	assert.Equal(t, "michael.bland@gsa.gov", email) | ||||
| 	assert.Equal(t, "michael.bland", user) | ||||
| 	assert.Equal(t, "my_access_token", access_token) | ||||
| } | ||||
| 
 | ||||
| func TestProcessCookieNoCookieError(t *testing.T) { | ||||
| 	pc_test := NewProcessCookieTest() | ||||
| 	_, _, _, ok := pc_test.ProcessCookie() | ||||
| 	assert.Equal(t, false, ok) | ||||
| } | ||||
| 
 | ||||
| func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) { | ||||
| 	pc_test := NewProcessCookieTest() | ||||
| 	value, _ := buildCookieValue("michael.bland@gsa.gov", | ||||
| 		pc_test.proxy.AesCipher, "my_access_token") | ||||
| 	pc_test.req.AddCookie(pc_test.proxy.MakeCookie( | ||||
| 		pc_test.req, value+"some bogus bytes", | ||||
| 		pc_test.opts.CookieExpire)) | ||||
| 	_, _, _, ok := pc_test.ProcessCookie() | ||||
| 	assert.Equal(t, false, ok) | ||||
| } | ||||
| 
 | ||||
| func TestProcessCookieRefreshNotSet(t *testing.T) { | ||||
| 	pc_test := NewProcessCookieTest() | ||||
| 	pc_test.InstantiateBackend() | ||||
| 	defer pc_test.Close() | ||||
| 
 | ||||
| 	pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour | ||||
| 	cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "") | ||||
| 	pc_test.req.AddCookie(cookie) | ||||
| 
 | ||||
| 	_, _, _, ok := pc_test.ProcessCookie() | ||||
| 	assert.Equal(t, true, ok) | ||||
| 	assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) | ||||
| } | ||||
| 
 | ||||
| func TestProcessCookieRefresh(t *testing.T) { | ||||
| 	pc_test := NewProcessCookieTest() | ||||
| 	pc_test.InstantiateBackend() | ||||
| 	defer pc_test.Close() | ||||
| 
 | ||||
| 	pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour | ||||
| 	cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") | ||||
| 	pc_test.req.AddCookie(cookie) | ||||
| 
 | ||||
| 	pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour | ||||
| 	_, _, _, ok := pc_test.ProcessCookie() | ||||
| 	assert.Equal(t, true, ok) | ||||
| 	assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) | ||||
| } | ||||
| 
 | ||||
| func TestProcessCookieRefreshThresholdNotCrossed(t *testing.T) { | ||||
| 	pc_test := NewProcessCookieTest() | ||||
| 	pc_test.InstantiateBackend() | ||||
| 	defer pc_test.Close() | ||||
| 
 | ||||
| 	pc_test.proxy.CookieExpire = time.Duration(25) * time.Hour | ||||
| 	cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") | ||||
| 	pc_test.req.AddCookie(cookie) | ||||
| 
 | ||||
| 	pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour | ||||
| 	_, _, _, ok := pc_test.ProcessCookie() | ||||
| 	assert.Equal(t, true, ok) | ||||
| 	assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) | ||||
| } | ||||
| 
 | ||||
| func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) { | ||||
| 	pc_test := NewProcessCookieTest() | ||||
| 	pc_test.InstantiateBackend() | ||||
| 	defer pc_test.Close() | ||||
| 	pc_test.response_code = 401 | ||||
| 
 | ||||
| 	pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour | ||||
| 	cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") | ||||
| 	pc_test.req.AddCookie(cookie) | ||||
| 
 | ||||
| 	pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour | ||||
| 	_, _, _, ok := pc_test.ProcessCookie() | ||||
| 	assert.Equal(t, false, ok) | ||||
| 	assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) | ||||
| } | ||||
| 
 | ||||
| func TestProcessCookieFailIfRefreshSetAndUserNoLongerValid(t *testing.T) { | ||||
| 	pc_test := NewProcessCookieTest() | ||||
| 	pc_test.InstantiateBackend() | ||||
| 	defer pc_test.Close() | ||||
| 	pc_test.validate_user = false | ||||
| 
 | ||||
| 	pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour | ||||
| 	cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") | ||||
| 	pc_test.req.AddCookie(cookie) | ||||
| 
 | ||||
| 	pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour | ||||
| 	_, _, _, ok := pc_test.ProcessCookie() | ||||
| 	assert.Equal(t, false, ok) | ||||
| 	assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										18
									
								
								options.go
								
								
								
								
							
							
						
						
									
										18
									
								
								options.go
								
								
								
								
							|  | @ -26,6 +26,7 @@ type Options struct { | |||
| 	CookieSecret    string        `flag:"cookie-secret" cfg:"cookie_secret" env:"GOOGLE_AUTH_PROXY_COOKIE_SECRET"` | ||||
| 	CookieDomain    string        `flag:"cookie-domain" cfg:"cookie_domain" env:"GOOGLE_AUTH_PROXY_COOKIE_DOMAIN"` | ||||
| 	CookieExpire    time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"GOOGLE_AUTH_PROXY_COOKIE_EXPIRE"` | ||||
| 	CookieRefresh   time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"GOOGLE_AUTH_PROXY_COOKIE_REFRESH"` | ||||
| 	CookieHttpsOnly bool          `flag:"cookie-https-only" cfg:"cookie_https_only"` // deprecated use cookie-secure
 | ||||
| 	CookieSecure    bool          `flag:"cookie-secure" cfg:"cookie_secure"` | ||||
| 	CookieHttpOnly  bool          `flag:"cookie-httponly" cfg:"cookie_httponly"` | ||||
|  | @ -42,6 +43,7 @@ type Options struct { | |||
| 	LoginUrl    string `flag:"login-url" cfg:"login_url"` | ||||
| 	RedeemUrl   string `flag:"redeem-url" cfg:"redeem_url"` | ||||
| 	ProfileUrl  string `flag:"profile-url" cfg:"profile_url"` | ||||
| 	ValidateUrl string `flag:"validate-url" cfg:"validate_url"` | ||||
| 	Scope       string `flag:"scope" cfg:"scope"` | ||||
| 
 | ||||
| 	RequestLogging bool `flag:"request-logging" cfg:"request_logging"` | ||||
|  | @ -61,6 +63,7 @@ func NewOptions() *Options { | |||
| 		CookieSecure:        true, | ||||
| 		CookieHttpOnly:      true, | ||||
| 		CookieExpire:        time.Duration(168) * time.Hour, | ||||
| 		CookieRefresh:       time.Duration(0), | ||||
| 		PassBasicAuth:       true, | ||||
| 		PassAccessToken:     false, | ||||
| 		PassHostHeader:      true, | ||||
|  | @ -117,7 +120,7 @@ func (o *Options) Validate() error { | |||
| 	} | ||||
| 	msgs = parseProviderInfo(o, msgs) | ||||
| 
 | ||||
| 	if o.PassAccessToken { | ||||
| 	if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) { | ||||
| 		valid_cookie_secret_size := false | ||||
| 		for _, i := range []int{16, 24, 32} { | ||||
| 			if len(o.CookieSecret) == i { | ||||
|  | @ -128,12 +131,20 @@ func (o *Options) Validate() error { | |||
| 			msgs = append(msgs, fmt.Sprintf( | ||||
| 				"cookie_secret must be 16, 24, or 32 bytes "+ | ||||
| 					"to create an AES cipher when "+ | ||||
| 					"pass_access_token == true, "+ | ||||
| 					"but is %d bytes", | ||||
| 					"pass_access_token == true or "+ | ||||
| 					"cookie_refresh != 0, but is %d bytes", | ||||
| 				len(o.CookieSecret))) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if o.CookieRefresh >= o.CookieExpire { | ||||
| 		msgs = append(msgs, fmt.Sprintf( | ||||
| 			"cookie_refresh (%s) must be less than "+ | ||||
| 				"cookie_expire (%s)", | ||||
| 			o.CookieRefresh.String(), | ||||
| 			o.CookieExpire.String())) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(msgs) != 0 { | ||||
| 		return fmt.Errorf("Invalid configuration:\n  %s", | ||||
| 			strings.Join(msgs, "\n  ")) | ||||
|  | @ -146,6 +157,7 @@ func parseProviderInfo(o *Options, msgs []string) []string { | |||
| 	p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs) | ||||
| 	p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs) | ||||
| 	p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs) | ||||
| 	p.ValidateUrl, msgs = parseUrl(o.ValidateUrl, "validate", msgs) | ||||
| 	o.provider = providers.New(o.Provider, p) | ||||
| 	return msgs | ||||
| } | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import ( | |||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/bmizerany/assert" | ||||
| ) | ||||
|  | @ -112,6 +113,10 @@ func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) { | |||
| 	o.CookieSecret = "cookie of invalid length-" | ||||
| 	assert.NotEqual(t, nil, o.Validate()) | ||||
| 
 | ||||
| 	o.PassAccessToken = false | ||||
| 	o.CookieRefresh = time.Duration(24) * time.Hour | ||||
| 	assert.NotEqual(t, nil, o.Validate()) | ||||
| 
 | ||||
| 	o.CookieSecret = "16 bytes AES-128" | ||||
| 	assert.Equal(t, nil, o.Validate()) | ||||
| 
 | ||||
|  | @ -121,3 +126,15 @@ func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) { | |||
| 	o.CookieSecret = "32 byte secret for AES-256------" | ||||
| 	assert.Equal(t, nil, o.Validate()) | ||||
| } | ||||
| 
 | ||||
| func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) { | ||||
| 	o := testOptions() | ||||
| 	assert.Equal(t, nil, o.Validate()) | ||||
| 
 | ||||
| 	o.CookieSecret = "0123456789abcdef" | ||||
| 	o.CookieRefresh = o.CookieExpire | ||||
| 	assert.NotEqual(t, nil, o.Validate()) | ||||
| 
 | ||||
| 	o.CookieRefresh -= time.Duration(1) | ||||
| 	assert.Equal(t, nil, o.Validate()) | ||||
| } | ||||
|  |  | |||
|  | @ -24,6 +24,11 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider { | |||
| 			Host: "accounts.google.com", | ||||
| 			Path: "/o/oauth2/token"} | ||||
| 	} | ||||
| 	if p.ValidateUrl.String() == "" { | ||||
| 		p.ValidateUrl = &url.URL{Scheme: "https", | ||||
| 			Host: "www.googleapis.com", | ||||
| 			Path: "/oauth2/v1/tokeninfo"} | ||||
| 	} | ||||
| 	if p.Scope == "" { | ||||
| 		p.Scope = "profile email" | ||||
| 	} | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ func newGoogleProvider() *GoogleProvider { | |||
| 			LoginUrl:     &url.URL{}, | ||||
| 			RedeemUrl:    &url.URL{}, | ||||
| 			ProfileUrl:   &url.URL{}, | ||||
| 			ValidateUrl:  &url.URL{}, | ||||
| 			Scope:        ""}) | ||||
| } | ||||
| 
 | ||||
|  | @ -26,6 +27,8 @@ func TestGoogleProviderDefaults(t *testing.T) { | |||
| 		p.Data().LoginUrl.String()) | ||||
| 	assert.Equal(t, "https://accounts.google.com/o/oauth2/token", | ||||
| 		p.Data().RedeemUrl.String()) | ||||
| 	assert.Equal(t, "https://www.googleapis.com/oauth2/v1/tokeninfo", | ||||
| 		p.Data().ValidateUrl.String()) | ||||
| 	assert.Equal(t, "", p.Data().ProfileUrl.String()) | ||||
| 	assert.Equal(t, "profile email", p.Data().Scope) | ||||
| } | ||||
|  | @ -45,6 +48,10 @@ func TestGoogleProviderOverrides(t *testing.T) { | |||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/profile"}, | ||||
| 			ValidateUrl: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/tokeninfo"}, | ||||
| 			Scope: "profile"}) | ||||
| 	assert.NotEqual(t, nil, p) | ||||
| 	assert.Equal(t, "Google", p.Data().ProviderName) | ||||
|  | @ -54,6 +61,8 @@ func TestGoogleProviderOverrides(t *testing.T) { | |||
| 		p.Data().RedeemUrl.String()) | ||||
| 	assert.Equal(t, "https://example.com/oauth/profile", | ||||
| 		p.Data().ProfileUrl.String()) | ||||
| 	assert.Equal(t, "https://example.com/oauth/tokeninfo", | ||||
| 		p.Data().ValidateUrl.String()) | ||||
| 	assert.Equal(t, "profile", p.Data().Scope) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,6 +32,11 @@ func NewMyUsaProvider(p *ProviderData) *MyUsaProvider { | |||
| 			Host: myUsaHost, | ||||
| 			Path: "/api/v1/profile"} | ||||
| 	} | ||||
| 	if p.ValidateUrl.String() == "" { | ||||
| 		p.ValidateUrl = &url.URL{Scheme: "https", | ||||
| 			Host: myUsaHost, | ||||
| 			Path: "/api/v1/tokeninfo"} | ||||
| 	} | ||||
| 	if p.Scope == "" { | ||||
| 		p.Scope = "profile.email" | ||||
| 	} | ||||
|  |  | |||
|  | @ -21,11 +21,13 @@ func testMyUsaProvider(hostname string) *MyUsaProvider { | |||
| 			LoginUrl:     &url.URL{}, | ||||
| 			RedeemUrl:    &url.URL{}, | ||||
| 			ProfileUrl:   &url.URL{}, | ||||
| 			ValidateUrl:  &url.URL{}, | ||||
| 			Scope:        ""}) | ||||
| 	if hostname != "" { | ||||
| 		updateUrl(p.Data().LoginUrl, hostname) | ||||
| 		updateUrl(p.Data().RedeemUrl, hostname) | ||||
| 		updateUrl(p.Data().ProfileUrl, hostname) | ||||
| 		updateUrl(p.Data().ValidateUrl, hostname) | ||||
| 	} | ||||
| 	return p | ||||
| } | ||||
|  | @ -56,6 +58,8 @@ func TestMyUsaProviderDefaults(t *testing.T) { | |||
| 		p.Data().RedeemUrl.String()) | ||||
| 	assert.Equal(t, "https://alpha.my.usa.gov/api/v1/profile", | ||||
| 		p.Data().ProfileUrl.String()) | ||||
| 	assert.Equal(t, "https://alpha.my.usa.gov/api/v1/tokeninfo", | ||||
| 		p.Data().ValidateUrl.String()) | ||||
| 	assert.Equal(t, "profile.email", p.Data().Scope) | ||||
| } | ||||
| 
 | ||||
|  | @ -74,6 +78,10 @@ func TestMyUsaProviderOverrides(t *testing.T) { | |||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/profile"}, | ||||
| 			ValidateUrl: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/tokeninfo"}, | ||||
| 			Scope: "profile"}) | ||||
| 	assert.NotEqual(t, nil, p) | ||||
| 	assert.Equal(t, "MyUSA", p.Data().ProviderName) | ||||
|  | @ -83,6 +91,8 @@ func TestMyUsaProviderOverrides(t *testing.T) { | |||
| 		p.Data().RedeemUrl.String()) | ||||
| 	assert.Equal(t, "https://example.com/oauth/profile", | ||||
| 		p.Data().ProfileUrl.String()) | ||||
| 	assert.Equal(t, "https://example.com/oauth/tokeninfo", | ||||
| 		p.Data().ValidateUrl.String()) | ||||
| 	assert.Equal(t, "profile", p.Data().Scope) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ type ProviderData struct { | |||
| 	LoginUrl     *url.URL | ||||
| 	RedeemUrl    *url.URL | ||||
| 	ProfileUrl   *url.URL | ||||
| 	ValidateUrl  *url.URL | ||||
| 	Scope        string | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue