git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #1128 from oauth2-proxy/proxy-router

Use gorilla mux for OAuth Proxy routing

commit62436dbc02529236aaa84fb4883eb31c7b942aa3 authorJoel Speed <Joel.speed@hotmail.co.uk> dateSat 19 Jun 2021, 11:22 (5 years ago) parentc00d361a8c9b29
4 files changed+88−70
CHANGELOG.md+2 go.mod+1 go.sum+2 oauthproxy.go+83−70
CHANGELOG.md+2
@@ -7,6 +7,8 @@
77## Breaking Changes
88
99## Changes since v7.1.3
10+
11+- [#1128](https://github.com/oauth2-proxy/oauth2-proxy/pull/1128) Use gorilla mux for OAuth Proxy routing (@JoelSpeed)
1012- [#1238](https://github.com/oauth2-proxy/oauth2-proxy/pull/1238) Added ADFS provider (@samirachoadi)
1113- [#1227](https://github.com/oauth2-proxy/oauth2-proxy/pull/1227) Fix Refresh Session not working for multiple cookies (@rishi1111)
1214- [#1063](https://github.com/oauth2-proxy/oauth2-proxy/pull/1063) Add Redis lock feature to lock persistent sessions (@Bibob7)
go.mod+1
@@ -16,6 +16,7 @@ require (
1616 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
1717 github.com/go-redis/redis/v8 v8.2.3
1818 github.com/google/uuid v1.2.0
19+ github.com/gorilla/mux v1.8.0 // indirect
1920 github.com/justinas/alice v1.2.0
2021 github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa
2122 github.com/mitchellh/mapstructure v1.1.2
go.sum+2
@@ -188,6 +188,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
188188github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
189189github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
190190github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
191+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
192+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
191193github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
192194github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
193195github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
oauthproxy.go+83−70
@@ -15,6 +15,7 @@ import (
1515 "syscall"
1616 "time"
1717
18+ "github.com/gorilla/mux"
1819 "github.com/justinas/alice"
1920 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
2021 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
@@ -38,6 +39,14 @@ const (
3839 schemeHTTP = "http"
3940 schemeHTTPS = "https"
4041 applicationJSON = "application/json"
42+
43+ robotsPath = "/robots.txt"
44+ signInPath = "/sign_in"
45+ signOutPath = "/sign_out"
46+ oauthStartPath = "/start"
47+ oauthCallbackPath = "/callback"
48+ authOnlyPath = "/auth"
49+ userInfoPath = "/userinfo"
4150)
4251
4352var (
@@ -63,13 +72,7 @@ type OAuthProxy struct {
6372 CookieOptions *options.Cookie
6473 Validator func(string) bool
6574
66− RobotsPath string
67− SignInPath string
68− SignOutPath string
69− OAuthStartPath string
70− OAuthCallbackPath string
71− AuthOnlyPath string
72− UserInfoPath string
75+ SignInPath string
7376
7477 allowedRoutes []allowedRoute
7578 redirectURL *url.URL // the url to receive requests at
@@ -78,18 +81,19 @@ type OAuthProxy struct {
7881 sessionStore sessionsapi.SessionStore
7982 ProxyPrefix string
8083 basicAuthValidator basic.Validator
81− serveMux http.Handler
8284 SkipProviderButton bool
8385 skipAuthPreflight bool
8486 skipJwtBearerTokens bool
8587 realClientIPParser ipapi.RealClientIPParser
8688 trustedIPs *ip.NetSet
8789
88− sessionChain alice.Chain
89− headersChain alice.Chain
90− preAuthChain alice.Chain
91− pageWriter pagewriter.Writer
92− server proxyhttp.Server
90+ sessionChain alice.Chain
91+ headersChain alice.Chain
92+ preAuthChain alice.Chain
93+ pageWriter pagewriter.Writer
94+ server proxyhttp.Server
95+ upstreamProxy http.Handler
96+ serveMux *mux.Router
9397}
9498
9599// 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
176180 CookieOptions: &opts.Cookie,
177181 Validator: validator,
178182
179− RobotsPath: "/robots.txt",
180− SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
181− SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
182− OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
183− OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
184− AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
185− UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
183+ SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
186184
187185 ProxyPrefix: opts.ProxyPrefix,
188186 provider: opts.GetProvider(),
189187 sessionStore: sessionStore,
190− serveMux: upstreamProxy,
191188 redirectURL: redirectURL,
192189 allowedRoutes: allowedRoutes,
193190 whitelistDomains: opts.WhitelistDomains,
@@ -202,7 +199,9 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
202199 headersChain: headersChain,
203200 preAuthChain: preAuthChain,
204201 pageWriter: pageWriter,
202+ upstreamProxy: upstreamProxy,
205203 }
204+ p.buildServeMux(opts.ProxyPrefix)
206205
207206 if err := p.setupServer(opts); err != nil {
208207 return nil, fmt.Errorf("error setting up server: %v", err)
@@ -258,6 +257,41 @@ func (p *OAuthProxy) setupServer(opts *options.Options) error {
258257 return nil
259258}
260259
260+func (p *OAuthProxy) buildServeMux(proxyPrefix string) {
261+ r := mux.NewRouter()
262+ // Everything served by the router must go through the preAuthChain first.
263+ r.Use(p.preAuthChain.Then)
264+
265+ // Register the robots path writer
266+ r.Path(robotsPath).HandlerFunc(p.pageWriter.WriteRobotsTxt)
267+
268+ // The authonly path should be registered separately to prevent it from getting no-cache headers.
269+ // We do this to allow users to have a short cache (via nginx) of the response to reduce the
270+ // likelihood of multiple reuests trying to referesh sessions simultaneously.
271+ r.Path(proxyPrefix + authOnlyPath).Handler(p.sessionChain.ThenFunc(p.AuthOnly))
272+
273+ // This will register all of the paths under the proxy prefix, except the auth only path so that no cache headers
274+ // are not applied.
275+ p.buildProxySubrouter(r.PathPrefix(proxyPrefix).Subrouter())
276+
277+ // Register serveHTTP last so it catches anything that isn't already caught earlier.
278+ // Anything that got to this point needs to have a session loaded.
279+ r.PathPrefix("/").Handler(p.sessionChain.ThenFunc(p.Proxy))
280+ p.serveMux = r
281+}
282+
283+func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
284+ s.Use(prepareNoCacheMiddleware)
285+
286+ s.Path(signInPath).HandlerFunc(p.SignIn)
287+ s.Path(signOutPath).HandlerFunc(p.SignOut)
288+ s.Path(oauthStartPath).HandlerFunc(p.OAuthStart)
289+ s.Path(oauthCallbackPath).HandlerFunc(p.OAuthCallback)
290+
291+ // The userinfo endpoint needs to load sessions before handling the request
292+ s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo))
293+}
294+
261295// buildPreAuthChain constructs a chain that should process every request before
262296// the OAuth2 Proxy authentication logic kicks in.
263297// For example forcing HTTPS or health checks.
@@ -478,39 +512,7 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
478512}
479513
480514func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
481− p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
482−}
483−
484−func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
485− if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
486− prepareNoCache(rw)
487− }
488−
489− switch path := req.URL.Path; {
490− case path == p.RobotsPath:
491− p.RobotsTxt(rw, req)
492− case p.IsAllowedRequest(req):
493− p.SkipAuthProxy(rw, req)
494− case path == p.SignInPath:
495− p.SignIn(rw, req)
496− case path == p.SignOutPath:
497− p.SignOut(rw, req)
498− case path == p.OAuthStartPath:
499− p.OAuthStart(rw, req)
500− case path == p.OAuthCallbackPath:
501− p.OAuthCallback(rw, req)
502− case path == p.AuthOnlyPath:
503− p.AuthOnly(rw, req)
504− case path == p.UserInfoPath:
505− p.UserInfo(rw, req)
506− default:
507− p.Proxy(rw, req)
508− }
509−}
510−
511−// RobotsTxt disallows scraping pages from the OAuthProxy
512−func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter, req *http.Request) {
513− p.pageWriter.WriteRobotsTxt(rw, req)
515+ p.serveMux.ServeHTTP(rw, req)
514516}
515517
516518// ErrorPage writes an error response
@@ -643,13 +645,22 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
643645
644646// UserInfo endpoint outputs session email and preferred username in JSON format
645647func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
646−
647648 session, err := p.getAuthenticatedSession(rw, req)
648649 if err != nil {
649650 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
650651 return
651652 }
652653
654+ rw.Header().Set("Content-Type", "application/json")
655+ rw.WriteHeader(http.StatusOK)
656+ if session == nil {
657+ if _, err := rw.Write([]byte("{}")); err != nil {
658+ logger.Printf("Error encoding empty user info: %v", err)
659+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
660+ }
661+ return
662+ }
663+
653664 userInfo := struct {
654665 User string `json:"user"`
655666 Email string `json:"email"`
@@ -662,10 +673,7 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
662673 PreferredUsername: session.PreferredUsername,
663674 }
664675
665− rw.Header().Set("Content-Type", "application/json")
666− rw.WriteHeader(http.StatusOK)
667− err = json.NewEncoder(rw).Encode(userInfo)
668− if err != nil {
676+ if err := json.NewEncoder(rw).Encode(userInfo); err != nil {
669677 logger.Printf("Error encoding user info: %v", err)
670678 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
671679 }
@@ -857,11 +865,6 @@ func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
857865 })).ServeHTTP(rw, req)
858866}
859867
860−// SkipAuthProxy proxies allowlisted requests and skips authentication
861−func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
862− p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
863−}
864−
865868// Proxy proxies the user request if the user is authenticated else it prompts
866869// them to authenticate
867870func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
@@ -870,7 +873,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
870873 case nil:
871874 // we are authenticated
872875 p.addHeadersForProxying(rw, session)
873− p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
876+ p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
874877 case ErrNeedsLogin:
875878 // we need to send the user to a login screen
876879 if isAjax(req) {
@@ -910,6 +913,13 @@ func prepareNoCache(w http.ResponseWriter) {
910913 }
911914}
912915
916+func prepareNoCacheMiddleware(next http.Handler) http.Handler {
917+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
918+ prepareNoCache(rw)
919+ next.ServeHTTP(rw, req)
920+ })
921+}
922+
913923// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
914924// redirect clients to once authenticated.
915925// This is usually the OAuthProxy callback URL.
@@ -1095,12 +1105,12 @@ func validOptionalPort(port string) bool {
10951105// - `nil, ErrAccessDenied` if the authenticated user is not authorized
10961106// Set-Cookie headers may be set on the response as a side-effect of calling this method.
10971107func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
1098− var session *sessionsapi.SessionState
1108+ session := middlewareapi.GetRequestScope(req).Session
10991109
1100− getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1101− session = middlewareapi.GetRequestScope(req).Session
1102− }))
1103− getSession.ServeHTTP(rw, req)
1110+ // Check this after loading the session so that if a valid session exists, we can add headers from it
1111+ if p.IsAllowedRequest(req) {
1112+ return session, nil
1113+ }
11041114
11051115 if session == nil {
11061116 return nil, ErrNeedsLogin
@@ -1190,6 +1200,9 @@ func decodeState(req *http.Request) (string, string, error) {
11901200
11911201// addHeadersForProxying adds the appropriate headers the request / response for proxying
11921202func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
1203+ if session == nil {
1204+ return
1205+ }
11931206 if session.Email == "" {
11941207 rw.Header().Set("GAP-Auth", session.User)
11951208 } else {