diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ad700e..8daf2107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ## Breaking Changes ## Changes since v7.1.3 + +- [#1128](https://github.com/oauth2-proxy/oauth2-proxy/pull/1128) Use gorilla mux for OAuth Proxy routing (@JoelSpeed) - [#1238](https://github.com/oauth2-proxy/oauth2-proxy/pull/1238) Added ADFS provider (@samirachoadi) - [#1227](https://github.com/oauth2-proxy/oauth2-proxy/pull/1227) Fix Refresh Session not working for multiple cookies (@rishi1111) - [#1063](https://github.com/oauth2-proxy/oauth2-proxy/pull/1063) Add Redis lock feature to lock persistent sessions (@Bibob7) diff --git a/go.mod b/go.mod index 174484df..df007c82 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-redis/redis/v8 v8.2.3 github.com/google/uuid v1.2.0 + github.com/gorilla/mux v1.8.0 // indirect github.com/justinas/alice v1.2.0 github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa github.com/mitchellh/mapstructure v1.1.2 diff --git a/go.sum b/go.sum index bc2f7038..5344792d 100644 --- a/go.sum +++ b/go.sum @@ -188,6 +188,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= diff --git a/oauthproxy.go b/oauthproxy.go index 5902b9af..ea94194e 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -15,6 +15,7 @@ import ( "syscall" "time" + "github.com/gorilla/mux" "github.com/justinas/alice" ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip" middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" @@ -38,6 +39,14 @@ const ( schemeHTTP = "http" schemeHTTPS = "https" applicationJSON = "application/json" + + robotsPath = "/robots.txt" + signInPath = "/sign_in" + signOutPath = "/sign_out" + oauthStartPath = "/start" + oauthCallbackPath = "/callback" + authOnlyPath = "/auth" + userInfoPath = "/userinfo" ) var ( @@ -63,13 +72,7 @@ type OAuthProxy struct { CookieOptions *options.Cookie Validator func(string) bool - RobotsPath string - SignInPath string - SignOutPath string - OAuthStartPath string - OAuthCallbackPath string - AuthOnlyPath string - UserInfoPath string + SignInPath string allowedRoutes []allowedRoute redirectURL *url.URL // the url to receive requests at @@ -78,18 +81,19 @@ type OAuthProxy struct { sessionStore sessionsapi.SessionStore ProxyPrefix string basicAuthValidator basic.Validator - serveMux http.Handler SkipProviderButton bool skipAuthPreflight bool skipJwtBearerTokens bool realClientIPParser ipapi.RealClientIPParser trustedIPs *ip.NetSet - sessionChain alice.Chain - headersChain alice.Chain - preAuthChain alice.Chain - pageWriter pagewriter.Writer - server proxyhttp.Server + sessionChain alice.Chain + headersChain alice.Chain + preAuthChain alice.Chain + pageWriter pagewriter.Writer + server proxyhttp.Server + upstreamProxy http.Handler + serveMux *mux.Router } // NewOAuthProxy creates a new instance of OAuthProxy from the options provided @@ -176,18 +180,11 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr CookieOptions: &opts.Cookie, Validator: validator, - RobotsPath: "/robots.txt", - SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), - SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix), - OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix), - OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix), - AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix), - UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix), + SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), ProxyPrefix: opts.ProxyPrefix, provider: opts.GetProvider(), sessionStore: sessionStore, - serveMux: upstreamProxy, redirectURL: redirectURL, allowedRoutes: allowedRoutes, whitelistDomains: opts.WhitelistDomains, @@ -202,7 +199,9 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr headersChain: headersChain, preAuthChain: preAuthChain, pageWriter: pageWriter, + upstreamProxy: upstreamProxy, } + p.buildServeMux(opts.ProxyPrefix) if err := p.setupServer(opts); err != nil { return nil, fmt.Errorf("error setting up server: %v", err) @@ -258,6 +257,41 @@ func (p *OAuthProxy) setupServer(opts *options.Options) error { return nil } +func (p *OAuthProxy) buildServeMux(proxyPrefix string) { + r := mux.NewRouter() + // Everything served by the router must go through the preAuthChain first. + r.Use(p.preAuthChain.Then) + + // Register the robots path writer + r.Path(robotsPath).HandlerFunc(p.pageWriter.WriteRobotsTxt) + + // The authonly path should be registered separately to prevent it from getting no-cache headers. + // We do this to allow users to have a short cache (via nginx) of the response to reduce the + // likelihood of multiple reuests trying to referesh sessions simultaneously. + r.Path(proxyPrefix + authOnlyPath).Handler(p.sessionChain.ThenFunc(p.AuthOnly)) + + // This will register all of the paths under the proxy prefix, except the auth only path so that no cache headers + // are not applied. + p.buildProxySubrouter(r.PathPrefix(proxyPrefix).Subrouter()) + + // Register serveHTTP last so it catches anything that isn't already caught earlier. + // Anything that got to this point needs to have a session loaded. + r.PathPrefix("/").Handler(p.sessionChain.ThenFunc(p.Proxy)) + p.serveMux = r +} + +func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) { + s.Use(prepareNoCacheMiddleware) + + s.Path(signInPath).HandlerFunc(p.SignIn) + s.Path(signOutPath).HandlerFunc(p.SignOut) + s.Path(oauthStartPath).HandlerFunc(p.OAuthStart) + s.Path(oauthCallbackPath).HandlerFunc(p.OAuthCallback) + + // The userinfo endpoint needs to load sessions before handling the request + s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo)) +} + // buildPreAuthChain constructs a chain that should process every request before // the OAuth2 Proxy authentication logic kicks in. // For example forcing HTTPS or health checks. @@ -478,39 +512,7 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool { } func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req) -} - -func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { - if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) { - prepareNoCache(rw) - } - - switch path := req.URL.Path; { - case path == p.RobotsPath: - p.RobotsTxt(rw, req) - case p.IsAllowedRequest(req): - p.SkipAuthProxy(rw, req) - case path == p.SignInPath: - p.SignIn(rw, req) - case path == p.SignOutPath: - p.SignOut(rw, req) - case path == p.OAuthStartPath: - p.OAuthStart(rw, req) - case path == p.OAuthCallbackPath: - p.OAuthCallback(rw, req) - case path == p.AuthOnlyPath: - p.AuthOnly(rw, req) - case path == p.UserInfoPath: - p.UserInfo(rw, req) - default: - p.Proxy(rw, req) - } -} - -// RobotsTxt disallows scraping pages from the OAuthProxy -func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter, req *http.Request) { - p.pageWriter.WriteRobotsTxt(rw, req) + p.serveMux.ServeHTTP(rw, req) } // ErrorPage writes an error response @@ -643,13 +645,22 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) { // UserInfo endpoint outputs session email and preferred username in JSON format func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { - session, err := p.getAuthenticatedSession(rw, req) if err != nil { http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + if session == nil { + if _, err := rw.Write([]byte("{}")); err != nil { + logger.Printf("Error encoding empty user info: %v", err) + p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + } + return + } + userInfo := struct { User string `json:"user"` Email string `json:"email"` @@ -662,10 +673,7 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { PreferredUsername: session.PreferredUsername, } - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(http.StatusOK) - err = json.NewEncoder(rw).Encode(userInfo) - if err != nil { + if err := json.NewEncoder(rw).Encode(userInfo); err != nil { logger.Printf("Error encoding user info: %v", err) p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) } @@ -857,11 +865,6 @@ func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) { })).ServeHTTP(rw, req) } -// SkipAuthProxy proxies allowlisted requests and skips authentication -func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) { - p.headersChain.Then(p.serveMux).ServeHTTP(rw, req) -} - // Proxy proxies the user request if the user is authenticated else it prompts // them to authenticate func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { @@ -870,7 +873,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { case nil: // we are authenticated p.addHeadersForProxying(rw, session) - p.headersChain.Then(p.serveMux).ServeHTTP(rw, req) + p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req) case ErrNeedsLogin: // we need to send the user to a login screen if isAjax(req) { @@ -910,6 +913,13 @@ func prepareNoCache(w http.ResponseWriter) { } } +func prepareNoCacheMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + prepareNoCache(rw) + next.ServeHTTP(rw, req) + }) +} + // getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will // redirect clients to once authenticated. // This is usually the OAuthProxy callback URL. @@ -1095,12 +1105,12 @@ func validOptionalPort(port string) bool { // - `nil, ErrAccessDenied` if the authenticated user is not authorized // Set-Cookie headers may be set on the response as a side-effect of calling this method. func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) { - var session *sessionsapi.SessionState + session := middlewareapi.GetRequestScope(req).Session - getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - session = middlewareapi.GetRequestScope(req).Session - })) - getSession.ServeHTTP(rw, req) + // Check this after loading the session so that if a valid session exists, we can add headers from it + if p.IsAllowedRequest(req) { + return session, nil + } if session == nil { return nil, ErrNeedsLogin @@ -1190,6 +1200,9 @@ func decodeState(req *http.Request) (string, string, error) { // addHeadersForProxying adds the appropriate headers the request / response for proxying func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) { + if session == nil { + return + } if session.Email == "" { rw.Header().Set("GAP-Auth", session.User) } else {