Allow a health/ping request to be identified by User-Agent (#567)
* Add an option to allow health checks based on User-Agent. * Formatting fix * Rename field and avoid unnecessary interface. * Skip the redirect fix so it can be put into a different PR. * Add CHANGELOG entry * Adding a couple tests for the PingUserAgent option.
This commit is contained in:
		
							parent
							
								
									160bbaf98e
								
							
						
					
					
						commit
						2c851fcd4f
					
				|  | @ -58,6 +58,7 @@ | ||||||
| - [#560](https://github.com/oauth2-proxy/oauth2-proxy/pull/560) Fallback to UserInfo is User ID claim not present (@JoelSpeed) | - [#560](https://github.com/oauth2-proxy/oauth2-proxy/pull/560) Fallback to UserInfo is User ID claim not present (@JoelSpeed) | ||||||
| - [#598](https://github.com/oauth2-proxy/oauth2-proxy/pull/598) acr_values no longer sent to IdP when empty (@ScottGuymer) | - [#598](https://github.com/oauth2-proxy/oauth2-proxy/pull/598) acr_values no longer sent to IdP when empty (@ScottGuymer) | ||||||
| - [#548](https://github.com/oauth2-proxy/oauth2-proxy/pull/548) Separate logging options out of main options structure (@JoelSpeed) | - [#548](https://github.com/oauth2-proxy/oauth2-proxy/pull/548) Separate logging options out of main options structure (@JoelSpeed) | ||||||
|  | - [#567](https://github.com/oauth2-proxy/oauth2-proxy/pull/567) Allow health/ping request to be identified via User-Agent (@chkohner) | ||||||
| - [#536](https://github.com/oauth2-proxy/oauth2-proxy/pull/536) Improvements to Session State code (@JoelSpeed) | - [#536](https://github.com/oauth2-proxy/oauth2-proxy/pull/536) Improvements to Session State code (@JoelSpeed) | ||||||
| - [#573](https://github.com/oauth2-proxy/oauth2-proxy/pull/573) Properly parse redis urls for cluster and sentinel connections (@amnay-mo) | - [#573](https://github.com/oauth2-proxy/oauth2-proxy/pull/573) Properly parse redis urls for cluster and sentinel connections (@amnay-mo) | ||||||
| - [#574](https://github.com/oauth2-proxy/oauth2-proxy/pull/574) render error page on 502 proxy status (@amnay-mo) | - [#574](https://github.com/oauth2-proxy/oauth2-proxy/pull/574) render error page on 502 proxy status (@amnay-mo) | ||||||
|  |  | ||||||
|  | @ -88,6 +88,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example | ||||||
| | `--provider` | string | OAuth provider | google | | | `--provider` | string | OAuth provider | google | | ||||||
| | `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) | | | `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) | | ||||||
| | `--ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` | | | `--ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` | | ||||||
|  | | `--ping-user-agent` | string | a User-Agent that can be used for basic health checks | `""` (don't check user agent) | | ||||||
| | `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` | | | `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` | | ||||||
| | `--proxy-websockets` | bool | enables WebSocket proxying | true | | | `--proxy-websockets` | bool | enables WebSocket proxying | true | | ||||||
| | `--pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | | | | `--pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | | | ||||||
|  | @ -163,7 +164,7 @@ There are three different types of logging: standard, authentication, and HTTP r | ||||||
| 
 | 
 | ||||||
| Each type of logging has their own configurable format and variables. By default these formats are similar to the Apache Combined Log. | Each type of logging has their own configurable format and variables. By default these formats are similar to the Apache Combined Log. | ||||||
| 
 | 
 | ||||||
| Logging of requests to the `/ping` endpoint can be disabled with `--silence-ping-logging` reducing log volume. This flag appends the `--ping-path` to `--exclude-logging-paths`. | Logging of requests to the `/ping` endpoint (or using `--ping-user-agent`) can be disabled with `--silence-ping-logging` reducing log volume. This flag appends the `--ping-path` to `--exclude-logging-paths`. | ||||||
| 
 | 
 | ||||||
| ### Auth Log Format | ### Auth Log Format | ||||||
| Authentication logs are logs which are guaranteed to contain a username or email address of a user attempting to authenticate. These logs are output by default in the below format: | Authentication logs are logs which are guaranteed to contain a username or email address of a user attempting to authenticate. These logs are output by default in the below format: | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ type responseLogger struct { | ||||||
| 	size     int | 	size     int | ||||||
| 	upstream string | 	upstream string | ||||||
| 	authInfo string | 	authInfo string | ||||||
|  | 	silent   bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Header returns the ResponseWriter's Header
 | // Header returns the ResponseWriter's Header
 | ||||||
|  | @ -104,5 +105,7 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||||
| 	url := *req.URL | 	url := *req.URL | ||||||
| 	responseLogger := &responseLogger{w: w} | 	responseLogger := &responseLogger{w: w} | ||||||
| 	h.handler.ServeHTTP(responseLogger, req) | 	h.handler.ServeHTTP(responseLogger, req) | ||||||
| 	logger.PrintReq(responseLogger.authInfo, responseLogger.upstream, req, url, t, responseLogger.Status(), responseLogger.Size()) | 	if !responseLogger.silent { | ||||||
|  | 		logger.PrintReq(responseLogger.authInfo, responseLogger.upstream, req, url, t, responseLogger.Status(), responseLogger.Size()) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,11 +5,14 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
|  | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" | ||||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger" | 	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger" | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/pkg/validation" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestLoggingHandler_ServeHTTP(t *testing.T) { | func TestLoggingHandler_ServeHTTP(t *testing.T) { | ||||||
|  | @ -67,3 +70,59 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestLoggingHandler_PingUserAgent(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		ExpectedLogMessage string | ||||||
|  | 		Path               string | ||||||
|  | 		SilencePingLogging bool | ||||||
|  | 		WithUserAgent      string | ||||||
|  | 	}{ | ||||||
|  | 		{"444\n", "/foo", true, "Blah"}, | ||||||
|  | 		{"444\n", "/foo", false, "Blah"}, | ||||||
|  | 		{"", "/ping", true, "Blah"}, | ||||||
|  | 		{"200\n", "/ping", false, "Blah"}, | ||||||
|  | 		{"", "/ping", true, "PingMe!"}, | ||||||
|  | 		{"", "/ping", false, "PingMe!"}, | ||||||
|  | 		{"", "/foo", true, "PingMe!"}, | ||||||
|  | 		{"", "/foo", false, "PingMe!"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for idx, test := range tests { | ||||||
|  | 		t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) { | ||||||
|  | 			opts := options.NewOptions() | ||||||
|  | 			opts.PingUserAgent = "PingMe!" | ||||||
|  | 			opts.SkipAuthRegex = []string{"/foo"} | ||||||
|  | 			opts.Upstreams = []string{"static://444/foo"} | ||||||
|  | 			opts.Logging.SilencePing = test.SilencePingLogging | ||||||
|  | 			if test.SilencePingLogging { | ||||||
|  | 				opts.Logging.ExcludePaths = []string{"/ping"} | ||||||
|  | 			} | ||||||
|  | 			opts.RawRedirectURL = "localhost" | ||||||
|  | 			validation.Validate(opts) | ||||||
|  | 
 | ||||||
|  | 			p := NewOAuthProxy(opts, func(email string) bool { | ||||||
|  | 				return true | ||||||
|  | 			}) | ||||||
|  | 			p.provider = NewTestProvider(&url.URL{Host: "localhost"}, "") | ||||||
|  | 
 | ||||||
|  | 			buf := bytes.NewBuffer(nil) | ||||||
|  | 			logger.SetOutput(buf) | ||||||
|  | 			logger.SetReqEnabled(true) | ||||||
|  | 			logger.SetReqTemplate("{{.StatusCode}}") | ||||||
|  | 
 | ||||||
|  | 			r, _ := http.NewRequest("GET", test.Path, nil) | ||||||
|  | 			if test.WithUserAgent != "" { | ||||||
|  | 				r.Header.Set("User-Agent", test.WithUserAgent) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			h := LoggingHandler(p) | ||||||
|  | 			h.ServeHTTP(httptest.NewRecorder(), r) | ||||||
|  | 
 | ||||||
|  | 			actual := buf.String() | ||||||
|  | 			if !strings.Contains(actual, test.ExpectedLogMessage) { | ||||||
|  | 				t.Errorf("Log message was\n%s\ninstead of matching \n%s", actual, test.ExpectedLogMessage) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -82,6 +82,8 @@ type OAuthProxy struct { | ||||||
| 
 | 
 | ||||||
| 	RobotsPath        string | 	RobotsPath        string | ||||||
| 	PingPath          string | 	PingPath          string | ||||||
|  | 	PingUserAgent     string | ||||||
|  | 	SilencePings      bool | ||||||
| 	SignInPath        string | 	SignInPath        string | ||||||
| 	SignOutPath       string | 	SignOutPath       string | ||||||
| 	OAuthStartPath    string | 	OAuthStartPath    string | ||||||
|  | @ -312,6 +314,8 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) *OAuthPro | ||||||
| 
 | 
 | ||||||
| 		RobotsPath:        "/robots.txt", | 		RobotsPath:        "/robots.txt", | ||||||
| 		PingPath:          opts.PingPath, | 		PingPath:          opts.PingPath, | ||||||
|  | 		PingUserAgent:     opts.PingUserAgent, | ||||||
|  | 		SilencePings:      opts.Logging.SilencePing, | ||||||
| 		SignInPath:        fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), | 		SignInPath:        fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), | ||||||
| 		SignOutPath:       fmt.Sprintf("%s/sign_out", opts.ProxyPrefix), | 		SignOutPath:       fmt.Sprintf("%s/sign_out", opts.ProxyPrefix), | ||||||
| 		OAuthStartPath:    fmt.Sprintf("%s/start", opts.ProxyPrefix), | 		OAuthStartPath:    fmt.Sprintf("%s/start", opts.ProxyPrefix), | ||||||
|  | @ -466,6 +470,11 @@ func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) { | ||||||
| 
 | 
 | ||||||
| // PingPage responds 200 OK to requests
 | // PingPage responds 200 OK to requests
 | ||||||
| func (p *OAuthProxy) PingPage(rw http.ResponseWriter) { | func (p *OAuthProxy) PingPage(rw http.ResponseWriter) { | ||||||
|  | 	if p.SilencePings { | ||||||
|  | 		if rl, ok := rw.(*responseLogger); ok { | ||||||
|  | 			rl.silent = true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	rw.WriteHeader(http.StatusOK) | 	rw.WriteHeader(http.StatusOK) | ||||||
| 	fmt.Fprintf(rw, "OK") | 	fmt.Fprintf(rw, "OK") | ||||||
| } | } | ||||||
|  | @ -675,6 +684,17 @@ func prepareNoCache(w http.ResponseWriter) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // IsPingRequest will check if the request appears to be performing a health check
 | ||||||
|  | // either via the path it's requesting or by a special User-Agent configuration.
 | ||||||
|  | func (p *OAuthProxy) IsPingRequest(req *http.Request) bool { | ||||||
|  | 
 | ||||||
|  | 	if req.URL.EscapedPath() == p.PingPath { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return p.PingUserAgent != "" && req.Header.Get("User-Agent") == p.PingUserAgent | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | ||||||
| 	if strings.HasPrefix(req.URL.Path, p.ProxyPrefix) { | 	if strings.HasPrefix(req.URL.Path, p.ProxyPrefix) { | ||||||
| 		prepareNoCache(rw) | 		prepareNoCache(rw) | ||||||
|  | @ -683,7 +703,7 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | ||||||
| 	switch path := req.URL.Path; { | 	switch path := req.URL.Path; { | ||||||
| 	case path == p.RobotsPath: | 	case path == p.RobotsPath: | ||||||
| 		p.RobotsTxt(rw) | 		p.RobotsTxt(rw) | ||||||
| 	case path == p.PingPath: | 	case p.IsPingRequest(req): | ||||||
| 		p.PingPage(rw) | 		p.PingPage(rw) | ||||||
| 	case p.IsWhitelistedRequest(req): | 	case p.IsWhitelistedRequest(req): | ||||||
| 		p.serveMux.ServeHTTP(rw, req) | 		p.serveMux.ServeHTTP(rw, req) | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ type SignatureData struct { | ||||||
| type Options struct { | type Options struct { | ||||||
| 	ProxyPrefix        string `flag:"proxy-prefix" cfg:"proxy_prefix"` | 	ProxyPrefix        string `flag:"proxy-prefix" cfg:"proxy_prefix"` | ||||||
| 	PingPath           string `flag:"ping-path" cfg:"ping_path"` | 	PingPath           string `flag:"ping-path" cfg:"ping_path"` | ||||||
|  | 	PingUserAgent      string `flag:"ping-user-agent" cfg:"ping_user_agent"` | ||||||
| 	ProxyWebSockets    bool   `flag:"proxy-websockets" cfg:"proxy_websockets"` | 	ProxyWebSockets    bool   `flag:"proxy-websockets" cfg:"proxy_websockets"` | ||||||
| 	HTTPAddress        string `flag:"http-address" cfg:"http_address"` | 	HTTPAddress        string `flag:"http-address" cfg:"http_address"` | ||||||
| 	HTTPSAddress       string `flag:"https-address" cfg:"https_address"` | 	HTTPSAddress       string `flag:"https-address" cfg:"https_address"` | ||||||
|  | @ -245,6 +246,7 @@ func NewFlagSet() *pflag.FlagSet { | ||||||
| 	flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.") | 	flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.") | ||||||
| 	flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)") | 	flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)") | ||||||
| 	flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks") | 	flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks") | ||||||
|  | 	flagSet.String("ping-user-agent", "", "special User-Agent that will be used for basic health checks") | ||||||
| 	flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying") | 	flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying") | ||||||
| 
 | 
 | ||||||
| 	flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates") | 	flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates") | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue