1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-06-15 00:15:00 +02:00

Merge pull request #1128 from oauth2-proxy/proxy-router

Use gorilla mux for OAuth Proxy routing
This commit is contained in:
Joel Speed
2021-06-19 11:22:37 +01:00
committed by GitHub
4 changed files with 88 additions and 70 deletions

View File

@ -7,6 +7,8 @@
## Breaking Changes ## Breaking Changes
## Changes since v7.1.3 ## 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) - [#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) - [#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) - [#1063](https://github.com/oauth2-proxy/oauth2-proxy/pull/1063) Add Redis lock feature to lock persistent sessions (@Bibob7)

1
go.mod
View File

@ -16,6 +16,7 @@ require (
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
github.com/go-redis/redis/v8 v8.2.3 github.com/go-redis/redis/v8 v8.2.3
github.com/google/uuid v1.2.0 github.com/google/uuid v1.2.0
github.com/gorilla/mux v1.8.0 // indirect
github.com/justinas/alice v1.2.0 github.com/justinas/alice v1.2.0
github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa
github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/mapstructure v1.1.2

2
go.sum
View File

@ -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/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.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.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 v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/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= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=

View File

@ -15,6 +15,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/gorilla/mux"
"github.com/justinas/alice" "github.com/justinas/alice"
ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip" ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
@ -38,6 +39,14 @@ const (
schemeHTTP = "http" schemeHTTP = "http"
schemeHTTPS = "https" schemeHTTPS = "https"
applicationJSON = "application/json" applicationJSON = "application/json"
robotsPath = "/robots.txt"
signInPath = "/sign_in"
signOutPath = "/sign_out"
oauthStartPath = "/start"
oauthCallbackPath = "/callback"
authOnlyPath = "/auth"
userInfoPath = "/userinfo"
) )
var ( var (
@ -63,13 +72,7 @@ type OAuthProxy struct {
CookieOptions *options.Cookie CookieOptions *options.Cookie
Validator func(string) bool Validator func(string) bool
RobotsPath string SignInPath string
SignInPath string
SignOutPath string
OAuthStartPath string
OAuthCallbackPath string
AuthOnlyPath string
UserInfoPath string
allowedRoutes []allowedRoute allowedRoutes []allowedRoute
redirectURL *url.URL // the url to receive requests at redirectURL *url.URL // the url to receive requests at
@ -78,18 +81,19 @@ type OAuthProxy struct {
sessionStore sessionsapi.SessionStore sessionStore sessionsapi.SessionStore
ProxyPrefix string ProxyPrefix string
basicAuthValidator basic.Validator basicAuthValidator basic.Validator
serveMux http.Handler
SkipProviderButton bool SkipProviderButton bool
skipAuthPreflight bool skipAuthPreflight bool
skipJwtBearerTokens bool skipJwtBearerTokens bool
realClientIPParser ipapi.RealClientIPParser realClientIPParser ipapi.RealClientIPParser
trustedIPs *ip.NetSet trustedIPs *ip.NetSet
sessionChain alice.Chain sessionChain alice.Chain
headersChain alice.Chain headersChain alice.Chain
preAuthChain alice.Chain preAuthChain alice.Chain
pageWriter pagewriter.Writer pageWriter pagewriter.Writer
server proxyhttp.Server server proxyhttp.Server
upstreamProxy http.Handler
serveMux *mux.Router
} }
// NewOAuthProxy creates a new instance of OAuthProxy from the options provided // 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, CookieOptions: &opts.Cookie,
Validator: validator, Validator: validator,
RobotsPath: "/robots.txt", SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
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),
ProxyPrefix: opts.ProxyPrefix, ProxyPrefix: opts.ProxyPrefix,
provider: opts.GetProvider(), provider: opts.GetProvider(),
sessionStore: sessionStore, sessionStore: sessionStore,
serveMux: upstreamProxy,
redirectURL: redirectURL, redirectURL: redirectURL,
allowedRoutes: allowedRoutes, allowedRoutes: allowedRoutes,
whitelistDomains: opts.WhitelistDomains, whitelistDomains: opts.WhitelistDomains,
@ -202,7 +199,9 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
headersChain: headersChain, headersChain: headersChain,
preAuthChain: preAuthChain, preAuthChain: preAuthChain,
pageWriter: pageWriter, pageWriter: pageWriter,
upstreamProxy: upstreamProxy,
} }
p.buildServeMux(opts.ProxyPrefix)
if err := p.setupServer(opts); err != nil { if err := p.setupServer(opts); err != nil {
return nil, fmt.Errorf("error setting up server: %v", err) return nil, fmt.Errorf("error setting up server: %v", err)
@ -258,6 +257,41 @@ func (p *OAuthProxy) setupServer(opts *options.Options) error {
return nil 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 // buildPreAuthChain constructs a chain that should process every request before
// the OAuth2 Proxy authentication logic kicks in. // the OAuth2 Proxy authentication logic kicks in.
// For example forcing HTTPS or health checks. // 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) { func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req) p.serveMux.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)
} }
// ErrorPage writes an error response // 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 // UserInfo endpoint outputs session email and preferred username in JSON format
func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
session, err := p.getAuthenticatedSession(rw, req) session, err := p.getAuthenticatedSession(rw, req)
if err != nil { if err != nil {
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return 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 { userInfo := struct {
User string `json:"user"` User string `json:"user"`
Email string `json:"email"` Email string `json:"email"`
@ -662,10 +673,7 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
PreferredUsername: session.PreferredUsername, PreferredUsername: session.PreferredUsername,
} }
rw.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(rw).Encode(userInfo); err != nil {
rw.WriteHeader(http.StatusOK)
err = json.NewEncoder(rw).Encode(userInfo)
if err != nil {
logger.Printf("Error encoding user info: %v", err) logger.Printf("Error encoding user info: %v", err)
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) 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) })).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 // Proxy proxies the user request if the user is authenticated else it prompts
// them to authenticate // them to authenticate
func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { 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: case nil:
// we are authenticated // we are authenticated
p.addHeadersForProxying(rw, session) p.addHeadersForProxying(rw, session)
p.headersChain.Then(p.serveMux).ServeHTTP(rw, req) p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
case ErrNeedsLogin: case ErrNeedsLogin:
// we need to send the user to a login screen // we need to send the user to a login screen
if isAjax(req) { 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 // getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
// redirect clients to once authenticated. // redirect clients to once authenticated.
// This is usually the OAuthProxy callback URL. // 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 // - `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. // 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) { 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) { // Check this after loading the session so that if a valid session exists, we can add headers from it
session = middlewareapi.GetRequestScope(req).Session if p.IsAllowedRequest(req) {
})) return session, nil
getSession.ServeHTTP(rw, req) }
if session == nil { if session == nil {
return nil, ErrNeedsLogin 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 // addHeadersForProxying adds the appropriate headers the request / response for proxying
func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) { func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
if session == nil {
return
}
if session.Email == "" { if session.Email == "" {
rw.Header().Set("GAP-Auth", session.User) rw.Header().Set("GAP-Auth", session.User)
} else { } else {