git.iko.soy/typeable/oauth2-proxy
on master help sign in
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.

commit2c851fcd4fc4921617e1eb69afc14f78146fac26 authorChristopher Kohnert <58753523+chkohner@users.noreply.github.com> dateFri 12 Jun 2020, 06:56 (6 years ago) parent160bbaf
6 files changed+89−3
CHANGELOG.md+1 docs/configuration/configuration.md+2−1 logging_handler.go+4−1 logging_handler_test.go+59 oauthproxy.go+21−1 pkg/apis/options/options.go+2
CHANGELOG.md+1
@@ -58,6 +58,7 @@
5858- [#560](https://github.com/oauth2-proxy/oauth2-proxy/pull/560) Fallback to UserInfo is User ID claim not present (@JoelSpeed)
5959- [#598](https://github.com/oauth2-proxy/oauth2-proxy/pull/598) acr_values no longer sent to IdP when empty (@ScottGuymer)
6060- [#548](https://github.com/oauth2-proxy/oauth2-proxy/pull/548) Separate logging options out of main options structure (@JoelSpeed)
61+- [#567](https://github.com/oauth2-proxy/oauth2-proxy/pull/567) Allow health/ping request to be identified via User-Agent (@chkohner)
6162- [#536](https://github.com/oauth2-proxy/oauth2-proxy/pull/536) Improvements to Session State code (@JoelSpeed)
6263- [#573](https://github.com/oauth2-proxy/oauth2-proxy/pull/573) Properly parse redis urls for cluster and sentinel connections (@amnay-mo)
6364- [#574](https://github.com/oauth2-proxy/oauth2-proxy/pull/574) render error page on 502 proxy status (@amnay-mo)
docs/configuration/configuration.md+2−1
@@ -88,6 +88,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
8888| `--provider` | string | OAuth provider | google |
8989| `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
9090| `--ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
91+| `--ping-user-agent` | string | a User-Agent that can be used for basic health checks | `""` (don't check user agent) |
9192| `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
9293| `--proxy-websockets` | bool | enables WebSocket proxying | true |
9394| `--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
163164
164165Each type of logging has their own configurable format and variables. By default these formats are similar to the Apache Combined Log.
165166
166−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`.
167+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`.
167168
168169### Auth Log Format
169170Authentication 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:
logging_handler.go+4−1
@@ -21,6 +21,7 @@ type responseLogger struct {
2121 size int
2222 upstream string
2323 authInfo string
24+ silent bool
2425}
2526
2627// Header returns the ResponseWriter's Header
@@ -104,5 +105,7 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
104105 url := *req.URL
105106 responseLogger := &responseLogger{w: w}
106107 h.handler.ServeHTTP(responseLogger, req)
107− logger.PrintReq(responseLogger.authInfo, responseLogger.upstream, req, url, t, responseLogger.Status(), responseLogger.Size())
108+ if !responseLogger.silent {
109+ logger.PrintReq(responseLogger.authInfo, responseLogger.upstream, req, url, t, responseLogger.Status(), responseLogger.Size())
110+ }
108111}
logging_handler_test.go+59
@@ -5,11 +5,14 @@ import (
55 "fmt"
66 "net/http"
77 "net/http/httptest"
8+ "net/url"
89 "strings"
910 "testing"
1011 "time"
1112
13+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
1214 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
15+ "github.com/oauth2-proxy/oauth2-proxy/pkg/validation"
1316)
1417
1518func TestLoggingHandler_ServeHTTP(t *testing.T) {
@@ -67,3 +70,59 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) {
6770 }
6871 }
6972}
73+
74+func TestLoggingHandler_PingUserAgent(t *testing.T) {
75+ tests := []struct {
76+ ExpectedLogMessage string
77+ Path string
78+ SilencePingLogging bool
79+ WithUserAgent string
80+ }{
81+ {"444\n", "/foo", true, "Blah"},
82+ {"444\n", "/foo", false, "Blah"},
83+ {"", "/ping", true, "Blah"},
84+ {"200\n", "/ping", false, "Blah"},
85+ {"", "/ping", true, "PingMe!"},
86+ {"", "/ping", false, "PingMe!"},
87+ {"", "/foo", true, "PingMe!"},
88+ {"", "/foo", false, "PingMe!"},
89+ }
90+
91+ for idx, test := range tests {
92+ t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
93+ opts := options.NewOptions()
94+ opts.PingUserAgent = "PingMe!"
95+ opts.SkipAuthRegex = []string{"/foo"}
96+ opts.Upstreams = []string{"static://444/foo"}
97+ opts.Logging.SilencePing = test.SilencePingLogging
98+ if test.SilencePingLogging {
99+ opts.Logging.ExcludePaths = []string{"/ping"}
100+ }
101+ opts.RawRedirectURL = "localhost"
102+ validation.Validate(opts)
103+
104+ p := NewOAuthProxy(opts, func(email string) bool {
105+ return true
106+ })
107+ p.provider = NewTestProvider(&url.URL{Host: "localhost"}, "")
108+
109+ buf := bytes.NewBuffer(nil)
110+ logger.SetOutput(buf)
111+ logger.SetReqEnabled(true)
112+ logger.SetReqTemplate("{{.StatusCode}}")
113+
114+ r, _ := http.NewRequest("GET", test.Path, nil)
115+ if test.WithUserAgent != "" {
116+ r.Header.Set("User-Agent", test.WithUserAgent)
117+ }
118+
119+ h := LoggingHandler(p)
120+ h.ServeHTTP(httptest.NewRecorder(), r)
121+
122+ actual := buf.String()
123+ if !strings.Contains(actual, test.ExpectedLogMessage) {
124+ t.Errorf("Log message was\n%s\ninstead of matching \n%s", actual, test.ExpectedLogMessage)
125+ }
126+ })
127+ }
128+}
oauthproxy.go+21−1
@@ -82,6 +82,8 @@ type OAuthProxy struct {
8282
8383 RobotsPath string
8484 PingPath string
85+ PingUserAgent string
86+ SilencePings bool
8587 SignInPath string
8688 SignOutPath string
8789 OAuthStartPath string
@@ -312,6 +314,8 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) *OAuthPro
312314
313315 RobotsPath: "/robots.txt",
314316 PingPath: opts.PingPath,
317+ PingUserAgent: opts.PingUserAgent,
318+ SilencePings: opts.Logging.SilencePing,
315319 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
316320 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
317321 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
@@ -466,6 +470,11 @@ func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
466470
467471// PingPage responds 200 OK to requests
468472func (p *OAuthProxy) PingPage(rw http.ResponseWriter) {
473+ if p.SilencePings {
474+ if rl, ok := rw.(*responseLogger); ok {
475+ rl.silent = true
476+ }
477+ }
469478 rw.WriteHeader(http.StatusOK)
470479 fmt.Fprintf(rw, "OK")
471480}
@@ -675,6 +684,17 @@ func prepareNoCache(w http.ResponseWriter) {
675684 }
676685}
677686
687+// IsPingRequest will check if the request appears to be performing a health check
688+// either via the path it's requesting or by a special User-Agent configuration.
689+func (p *OAuthProxy) IsPingRequest(req *http.Request) bool {
690+
691+ if req.URL.EscapedPath() == p.PingPath {
692+ return true
693+ }
694+
695+ return p.PingUserAgent != "" && req.Header.Get("User-Agent") == p.PingUserAgent
696+}
697+
678698func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
679699 if strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
680700 prepareNoCache(rw)
@@ -683,7 +703,7 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
683703 switch path := req.URL.Path; {
684704 case path == p.RobotsPath:
685705 p.RobotsTxt(rw)
686− case path == p.PingPath:
706+ case p.IsPingRequest(req):
687707 p.PingPage(rw)
688708 case p.IsWhitelistedRequest(req):
689709 p.serveMux.ServeHTTP(rw, req)
pkg/apis/options/options.go+2
@@ -24,6 +24,7 @@ type SignatureData struct {
2424type Options struct {
2525 ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
2626 PingPath string `flag:"ping-path" cfg:"ping_path"`
27+ PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
2728 ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets"`
2829 HTTPAddress string `flag:"http-address" cfg:"http_address"`
2930 HTTPSAddress string `flag:"https-address" cfg:"https_address"`
@@ -245,6 +246,7 @@ func NewFlagSet() *pflag.FlagSet {
245246 flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
246247 flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
247248 flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks")
249+ flagSet.String("ping-user-agent", "", "special User-Agent that will be used for basic health checks")
248250 flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying")
249251
250252 flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates")