git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #964 from grnhse/reverse-proxy-context

Track the ReverseProxy config setting in the request Scope

commit9c126f57403bde9cedf30c433a3749aa8817a830 authorJoel Speed <Joel.speed@hotmail.co.uk> dateSun 17 Jan 2021, 18:32 (6 years ago) parent8e02facda02914
25 files changed+767−477
CHANGELOG.md+8 oauthproxy.go+342−275 oauthproxy_test.go+13−14 pkg/apis/middleware/middleware_suite_test.go+19 pkg/apis/middleware/scope.go+29 pkg/apis/middleware/scope_test.go+56 pkg/cookies/cookies.go+4−4 pkg/logger/logger.go+3−3 pkg/middleware/basic_session.go+2−1 pkg/middleware/basic_session_test.go+2−4 pkg/middleware/headers.go+3−2 pkg/middleware/headers_test.go+2−5 pkg/middleware/jwt_session.go+1−1 pkg/middleware/jwt_session_test.go+2−3 pkg/middleware/redirect_to_https.go+7−6 pkg/middleware/redirect_to_https_test.go+31 pkg/middleware/scope.go+9−28 pkg/middleware/scope_test.go+32−57 pkg/middleware/stored_session.go+2−1 pkg/middleware/stored_session_test.go+2−3 pkg/requests/util/util.go+48 pkg/requests/util/util_suite_test.go+19 pkg/requests/util/util_test.go+131 pkg/util/util.go−29 pkg/util/util_test.go−41
CHANGELOG.md+8
@@ -4,6 +4,8 @@
44
55## Important Notes
66
7+- [#964](https://github.com/oauth2-proxy/oauth2-proxy/pull/964) Redirect URL generation will attempt secondary strategies
8+ in the priority chain if any fail the `IsValidRedirect` security check. Previously any failures fell back to `/`.
79- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) Keycloak will now use `--profile-url` if set for the userinfo endpoint
810 instead of `--validate-url`. `--validate-url` will still work for backwards compatibility.
911- [#957](https://github.com/oauth2-proxy/oauth2-proxy/pull/957) To use X-Forwarded-{Proto,Host,Uri} on redirect detection, `--reverse-proxy` must be `true`.
@@ -36,6 +38,11 @@
3638
3739## Breaking Changes
3840
41+- [#964](https://github.com/oauth2-proxy/oauth2-proxy/pull/964) `--reverse-proxy` must be true to trust `X-Forwarded-*` headers as canonical.
42+ These are used throughout the application in redirect URLs, cookie domains and host logging logic. These are the headers:
43+ - `X-Forwarded-Proto` instead of `req.URL.Scheme`
44+ - `X-Forwarded-Host` instead of `req.Host`
45+ - `X-Forwarded-Uri` instead of `req.URL.RequestURI()`
3946- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) In config files & envvar configs, `keycloak_group` is now the plural `keycloak_groups`.
4047 Flag configs are still `--keycloak-group` but it can be passed multiple times.
4148- [#911](https://github.com/oauth2-proxy/oauth2-proxy/pull/911) Specifying a non-existent provider will cause OAuth2-Proxy to fail on startup instead of defaulting to "google".
@@ -60,6 +67,7 @@
6067## Changes since v6.1.1
6168
6269- [#995](https://github.com/oauth2-proxy/oauth2-proxy/pull/995) Add Security Policy (@JoelSpeed)
70+- [#964](https://github.com/oauth2-proxy/oauth2-proxy/pull/964) Require `--reverse-proxy` true to trust `X-Forwareded-*` type headers (@NickMeves)
6371- [#970](https://github.com/oauth2-proxy/oauth2-proxy/pull/970) Fix joined cookie name for those containing underline in the suffix (@peppered)
6472- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) Migrate Keycloak to EnrichSession & support multiple groups for authorization (@NickMeves)
6573- [#957](https://github.com/oauth2-proxy/oauth2-proxy/pull/957) Use X-Forwarded-{Proto,Host,Uri} on redirect as last resort (@linuxgemini)
oauthproxy.go+342−275
@@ -24,16 +24,14 @@ import (
2424 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
2525 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
2626 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
27+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
2728 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
2829 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
29− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
3030 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
3131)
3232
3333const (
34− httpScheme = "http"
35− httpsScheme = "https"
36−
34+ schemeHTTPS = "https"
3735 applicationJSON = "application/json"
3836)
3937
@@ -98,7 +96,6 @@ type OAuthProxy struct {
9896 SetAuthorization bool
9997 PassAuthorization bool
10098 PreferEmailToUser bool
101− ReverseProxy bool
10299 skipAuthPreflight bool
103100 skipJwtBearerTokens bool
104101 templates *template.Template
@@ -201,7 +198,6 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
201198 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
202199
203200 ProxyPrefix: opts.ProxyPrefix,
204− ReverseProxy: opts.ReverseProxy,
205201 provider: opts.GetProvider(),
206202 providerNameOverride: opts.ProviderName,
207203 sessionStore: sessionStore,
@@ -231,7 +227,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
231227// the OAuth2 Proxy authentication logic kicks in.
232228// For example forcing HTTPS or health checks.
233229func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
234− chain := alice.New(middleware.NewScope())
230+ chain := alice.New(middleware.NewScope(opts.ReverseProxy))
235231
236232 if opts.ForceHTTPS {
237233 _, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress)
@@ -368,49 +364,6 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
368364 return routes, nil
369365}
370366
371−// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
372−// redirect clients to once authenticated
373−func (p *OAuthProxy) GetRedirectURI(host string) string {
374− // default to the request Host if not set
375− if p.redirectURL.Host != "" {
376− return p.redirectURL.String()
377− }
378− u := *p.redirectURL
379− if u.Scheme == "" {
380− if p.CookieSecure {
381− u.Scheme = httpsScheme
382− } else {
383− u.Scheme = httpScheme
384− }
385− }
386− u.Host = host
387− return u.String()
388−}
389−
390−func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
391− if code == "" {
392− return nil, providers.ErrMissingCode
393− }
394− redirectURI := p.GetRedirectURI(host)
395− s, err := p.provider.Redeem(ctx, redirectURI, code)
396− if err != nil {
397− return nil, err
398− }
399− return s, nil
400−}
401−
402−func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
403− var err error
404− if s.Email == "" {
405− s.Email, err = p.provider.GetEmailAddress(ctx, s)
406− if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
407− return err
408− }
409− }
410−
411− return p.provider.EnrichSession(ctx, s)
412−}
413−
414367// MakeCSRFCookie creates a cookie for CSRF
415368func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
416369 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
@@ -420,7 +373,7 @@ func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, ex
420373 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
421374
422375 if cookieDomain != "" {
423− domain := util.GetRequestHost(req)
376+ domain := requestutil.GetRequestHost(req)
424377 if h, _, err := net.SplitHostPort(domain); err == nil {
425378 domain = h
426379 }
@@ -468,6 +421,81 @@ func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *s
468421 return p.sessionStore.Save(rw, req, s)
469422}
470423
424+// IsValidRedirect checks whether the redirect URL is whitelisted
425+func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
426+ switch {
427+ case redirect == "":
428+ // The user didn't specify a redirect, should fallback to `/`
429+ return false
430+ case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
431+ return true
432+ case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
433+ redirectURL, err := url.Parse(redirect)
434+ if err != nil {
435+ logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
436+ return false
437+ }
438+ redirectHostname := redirectURL.Hostname()
439+
440+ for _, domain := range p.whitelistDomains {
441+ domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
442+ if domainHostname == "" {
443+ continue
444+ }
445+
446+ if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
447+ // the domain names match, now validate the ports
448+ // if the whitelisted domain's port is '*', allow all ports
449+ // if the whitelisted domain contains a specific port, only allow that port
450+ // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
451+ redirectPort := redirectURL.Port()
452+ if (domainPort == "*") ||
453+ (domainPort == redirectPort) ||
454+ (domainPort == "" && redirectPort == "") {
455+ return true
456+ }
457+ }
458+ }
459+
460+ logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
461+ return false
462+ default:
463+ logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
464+ return false
465+ }
466+}
467+
468+func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
469+ p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
470+}
471+
472+func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
473+ if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
474+ prepareNoCache(rw)
475+ }
476+
477+ switch path := req.URL.Path; {
478+ case path == p.RobotsPath:
479+ p.RobotsTxt(rw)
480+ case p.IsAllowedRequest(req):
481+ p.SkipAuthProxy(rw, req)
482+ case path == p.SignInPath:
483+ p.SignIn(rw, req)
484+ case path == p.SignOutPath:
485+ p.SignOut(rw, req)
486+ case path == p.OAuthStartPath:
487+ p.OAuthStart(rw, req)
488+ case path == p.OAuthCallbackPath:
489+ p.OAuthCallback(rw, req)
490+ case path == p.AuthOnlyPath:
491+ p.AuthOnly(rw, req)
492+ case path == p.UserInfoPath:
493+ p.UserInfo(rw, req)
494+ default:
495+ p.Proxy(rw, req)
496+ }
497+}
498+
471499// RobotsTxt disallows scraping pages from the OAuthProxy
472500func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
473501 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
@@ -498,6 +526,42 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m
498526 }
499527}
500528
529+// IsAllowedRequest is used to check if auth should be skipped for this request
530+func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
531+ isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
532+ return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
533+}
534+
535+// IsAllowedRoute is used to check if the request method & path is allowed without auth
536+func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
537+ for _, route := range p.allowedRoutes {
538+ if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
539+ return true
540+ }
541+ }
542+ return false
543+}
544+
545+// isTrustedIP is used to check if a request comes from a trusted client IP address.
546+func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
547+ if p.trustedIPs == nil {
548+ return false
549+ }
550+
551+ remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
552+ if err != nil {
553+ logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
554+ // Possibly spoofed X-Real-IP header
555+ return false
556+ }
557+
558+ if remoteAddr == nil {
559+ return false
560+ }
561+
562+ return p.trustedIPs.Has(remoteAddr)
563+}
564+
501565// SignInPage writes the sing in template to the response
502566func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
503567 prepareNoCache(rw)
@@ -509,7 +573,7 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
509573 }
510574 rw.WriteHeader(code)
511575
512− redirectURL, err := p.GetRedirect(req)
576+ redirectURL, err := p.getAppRedirect(req)
513577 if err != nil {
514578 logger.Errorf("Error obtaining redirect: %v", err)
515579 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
@@ -568,220 +632,9 @@ func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
568632 return "", false
569633}
570634
571−// GetRedirect reads the query parameter to get the URL to redirect clients to
572−// once authenticated with the OAuthProxy
573−func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
574− err = req.ParseForm()
575− if err != nil {
576− return
577− }
578−
579− redirect = req.Header.Get("X-Auth-Request-Redirect")
580− if req.Form.Get("rd") != "" {
581− redirect = req.Form.Get("rd")
582− }
583− // Quirk: On reverse proxies that doesn't have support for
584− // "X-Auth-Request-Redirect" header or dynamic header/query string
585− // manipulation (like Traefik v1 and v2), we can try if the header
586− // X-Forwarded-Host exists or not.
587− if redirect == "" && isForwardedRequest(req, p.ReverseProxy) {
588− redirect = p.getRedirectFromForwardHeaders(req)
589− }
590− if !p.IsValidRedirect(redirect) {
591− // Use RequestURI to preserve ?query
592− redirect = req.URL.RequestURI()
593−
594− if strings.HasPrefix(redirect, fmt.Sprintf("%s/", p.ProxyPrefix)) {
595− redirect = "/"
596− }
597− }
598−
599− return
600−}
601−
602−// getRedirectFromForwardHeaders returns the redirect URL based on X-Forwarded-{Proto,Host,Uri} headers
603−func (p *OAuthProxy) getRedirectFromForwardHeaders(req *http.Request) string {
604− uri := util.GetRequestURI(req)
605−
606− if strings.HasPrefix(uri, fmt.Sprintf("%s/", p.ProxyPrefix)) {
607− uri = "/"
608− }
609−
610− return fmt.Sprintf("%s://%s%s", util.GetRequestProto(req), util.GetRequestHost(req), uri)
611−}
612−
613−// splitHostPort separates host and port. If the port is not valid, it returns
614−// the entire input as host, and it doesn't check the validity of the host.
615−// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
616−// *** taken from net/url, modified validOptionalPort() to accept ":*"
617−func splitHostPort(hostport string) (host, port string) {
618− host = hostport
619−
620− colon := strings.LastIndexByte(host, ':')
621− if colon != -1 && validOptionalPort(host[colon:]) {
622− host, port = host[:colon], host[colon+1:]
623− }
624−
625− if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
626− host = host[1 : len(host)-1]
627− }
628−
629− return
630−}
631−
632−// validOptionalPort reports whether port is either an empty string
633−// or matches /^:\d*$/
634−// *** taken from net/url, modified to accept ":*"
635−func validOptionalPort(port string) bool {
636− if port == "" || port == ":*" {
637− return true
638− }
639− if port[0] != ':' {
640− return false
641− }
642− for _, b := range port[1:] {
643− if b < '0' || b > '9' {
644− return false
645− }
646− }
647− return true
648−}
649−
650−// IsValidRedirect checks whether the redirect URL is whitelisted
651−func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
652− switch {
653− case redirect == "":
654− // The user didn't specify a redirect, should fallback to `/`
655− return false
656− case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
657− return true
658− case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
659− redirectURL, err := url.Parse(redirect)
660− if err != nil {
661− logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
662− return false
663− }
664− redirectHostname := redirectURL.Hostname()
665−
666− for _, domain := range p.whitelistDomains {
667− domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
668− if domainHostname == "" {
669− continue
670− }
671−
672− if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
673− // the domain names match, now validate the ports
674− // if the whitelisted domain's port is '*', allow all ports
675− // if the whitelisted domain contains a specific port, only allow that port
676− // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
677− redirectPort := redirectURL.Port()
678− if (domainPort == "*") ||
679− (domainPort == redirectPort) ||
680− (domainPort == "" && redirectPort == "") {
681− return true
682− }
683− }
684− }
685−
686− logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
687− return false
688− default:
689− logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
690− return false
691− }
692−}
693−
694−// IsAllowedRequest is used to check if auth should be skipped for this request
695−func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
696− isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
697− return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
698−}
699−
700−// IsAllowedRoute is used to check if the request method & path is allowed without auth
701−func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
702− for _, route := range p.allowedRoutes {
703− if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
704− return true
705− }
706− }
707− return false
708−}
709−
710−// isForwardedRequest is used to check if X-Forwarded-Host header exists or not
711−func isForwardedRequest(req *http.Request, reverseProxy bool) bool {
712− isForwarded := req.Host != util.GetRequestHost(req)
713− return isForwarded && reverseProxy
714−}
715−
716−// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
717−var noCacheHeaders = map[string]string{
718− "Expires": time.Unix(0, 0).Format(time.RFC1123),
719− "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
720− "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
721−}
722−
723−// prepareNoCache prepares headers for preventing browser caching.
724−func prepareNoCache(w http.ResponseWriter) {
725− // Set NoCache headers
726− for k, v := range noCacheHeaders {
727− w.Header().Set(k, v)
728− }
729−}
730−
731−// IsTrustedIP is used to check if a request comes from a trusted client IP address.
732−func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
733− if p.trustedIPs == nil {
734− return false
735− }
736−
737− remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
738− if err != nil {
739− logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
740− // Possibly spoofed X-Real-IP header
741− return false
742− }
743−
744− if remoteAddr == nil {
745− return false
746− }
747−
748− return p.trustedIPs.Has(remoteAddr)
749−}
750−
751−func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
752− p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
753−}
754−
755−func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
756− if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
757− prepareNoCache(rw)
758− }
759−
760− switch path := req.URL.Path; {
761− case path == p.RobotsPath:
762− p.RobotsTxt(rw)
763− case p.IsAllowedRequest(req):
764− p.SkipAuthProxy(rw, req)
765− case path == p.SignInPath:
766− p.SignIn(rw, req)
767− case path == p.SignOutPath:
768− p.SignOut(rw, req)
769− case path == p.OAuthStartPath:
770− p.OAuthStart(rw, req)
771− case path == p.OAuthCallbackPath:
772− p.OAuthCallback(rw, req)
773− case path == p.AuthOnlyPath:
774− p.AuthOnly(rw, req)
775− case path == p.UserInfoPath:
776− p.UserInfo(rw, req)
777− default:
778− p.Proxy(rw, req)
779− }
780−}
781−
782635// SignIn serves a page prompting users to sign in
783636func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
784− redirect, err := p.GetRedirect(req)
637+ redirect, err := p.getAppRedirect(req)
785638 if err != nil {
786639 logger.Errorf("Error obtaining redirect: %v", err)
787640 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
@@ -839,7 +692,7 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
839692
840693// SignOut sends a response to clear the authentication cookie
841694func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
842− redirect, err := p.GetRedirect(req)
695+ redirect, err := p.getAppRedirect(req)
843696 if err != nil {
844697 logger.Errorf("Error obtaining redirect: %v", err)
845698 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
@@ -864,13 +717,13 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
864717 return
865718 }
866719 p.SetCSRFCookie(rw, req, nonce)
867− redirect, err := p.GetRedirect(req)
720+ redirect, err := p.getAppRedirect(req)
868721 if err != nil {
869722 logger.Errorf("Error obtaining redirect: %v", err)
870723 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
871724 return
872725 }
873− redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
726+ redirectURI := p.getOAuthRedirectURI(req)
874727 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
875728}
876729
@@ -893,7 +746,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
893746 return
894747 }
895748
896− session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
749+ session, err := p.redeemCode(req)
897750 if err != nil {
898751 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
899752 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
@@ -952,6 +805,32 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
952805 }
953806}
954807
808+func (p *OAuthProxy) redeemCode(req *http.Request) (*sessionsapi.SessionState, error) {
809+ code := req.Form.Get("code")
810+ if code == "" {
811+ return nil, providers.ErrMissingCode
812+ }
813+
814+ redirectURI := p.getOAuthRedirectURI(req)
815+ s, err := p.provider.Redeem(req.Context(), redirectURI, code)
816+ if err != nil {
817+ return nil, err
818+ }
819+ return s, nil
820+}
821+
822+func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
823+ var err error
824+ if s.Email == "" {
825+ s.Email, err = p.provider.GetEmailAddress(ctx, s)
826+ if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
827+ return err
828+ }
829+ }
830+
831+ return p.provider.EnrichSession(ctx, s)
832+}
833+
955834// AuthOnly checks whether the user is currently logged in (both authentication
956835// and optional authorization).
957836func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
@@ -969,7 +848,7 @@ func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
969848 }
970849
971850 // we are authenticated
972− p.addHeadersForProxying(rw, req, session)
851+ p.addHeadersForProxying(rw, session)
973852 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
974853 rw.WriteHeader(http.StatusAccepted)
975854 })).ServeHTTP(rw, req)
@@ -987,13 +866,13 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
987866 switch err {
988867 case nil:
989868 // we are authenticated
990− p.addHeadersForProxying(rw, req, session)
869+ p.addHeadersForProxying(rw, session)
991870 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
992871 case ErrNeedsLogin:
993872 // we need to send the user to a login screen
994873 if isAjax(req) {
995874 // no point redirecting an AJAX request
996− p.ErrorJSON(rw, http.StatusUnauthorized)
875+ p.errorJSON(rw, http.StatusUnauthorized)
997876 return
998877 }
999878
@@ -1012,7 +891,195 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
1012891 p.ErrorPage(rw, http.StatusInternalServerError,
1013892 "Internal Error", "Internal Error")
1014893 }
894+}
895+
896+// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
897+var noCacheHeaders = map[string]string{
898+ "Expires": time.Unix(0, 0).Format(time.RFC1123),
899+ "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
900+ "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
901+}
1015902
903+// prepareNoCache prepares headers for preventing browser caching.
904+func prepareNoCache(w http.ResponseWriter) {
905+ // Set NoCache headers
906+ for k, v := range noCacheHeaders {
907+ w.Header().Set(k, v)
908+ }
909+}
910+
911+// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
912+// redirect clients to once authenticated.
913+// This is usually the OAuthProxy callback URL.
914+func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
915+ // if `p.redirectURL` already has a host, return it
916+ if p.redirectURL.Host != "" {
917+ return p.redirectURL.String()
918+ }
919+
920+ // Otherwise figure out the scheme + host from the request
921+ rd := *p.redirectURL
922+ rd.Host = requestutil.GetRequestHost(req)
923+ rd.Scheme = requestutil.GetRequestProto(req)
924+
925+ // If CookieSecure is true, return `https` no matter what
926+ // Not all reverse proxies set X-Forwarded-Proto
927+ if p.CookieSecure {
928+ rd.Scheme = schemeHTTPS
929+ }
930+ return rd.String()
931+}
932+
933+// getAppRedirect determines the full URL or URI path to redirect clients to
934+// once authenticated with the OAuthProxy
935+// Strategy priority (first legal result is used):
936+// - `rd` querysting parameter
937+// - `X-Auth-Request-Redirect` header
938+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
939+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
940+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
941+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
942+// - `/`
943+func (p *OAuthProxy) getAppRedirect(req *http.Request) (string, error) {
944+ err := req.ParseForm()
945+ if err != nil {
946+ return "", err
947+ }
948+
949+ // These redirect getter functions are strategies ordered by priority
950+ // for figuring out the redirect URL.
951+ type redirectGetter func(req *http.Request) string
952+ for _, rdGetter := range []redirectGetter{
953+ p.getRdQuerystringRedirect,
954+ p.getXAuthRequestRedirect,
955+ p.getXForwardedHeadersRedirect,
956+ p.getURIRedirect,
957+ } {
958+ redirect := rdGetter(req)
959+ // Call `p.IsValidRedirect` again here a final time to be safe
960+ if redirect != "" && p.IsValidRedirect(redirect) {
961+ return redirect, nil
962+ }
963+ }
964+
965+ return "/", nil
966+}
967+
968+func isForwardedRequest(req *http.Request) bool {
969+ return requestutil.IsProxied(req) &&
970+ req.Host != requestutil.GetRequestHost(req)
971+}
972+
973+func (p *OAuthProxy) hasProxyPrefix(path string) bool {
974+ return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
975+}
976+
977+func (p *OAuthProxy) validateRedirect(redirect string, errorFormat string) string {
978+ if p.IsValidRedirect(redirect) {
979+ return redirect
980+ }
981+ if redirect != "" {
982+ logger.Errorf(errorFormat, redirect)
983+ }
984+ return ""
985+}
986+
987+// getRdQuerystringRedirect handles this getAppRedirect strategy:
988+// - `rd` querysting parameter
989+func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
990+ return p.validateRedirect(
991+ req.Form.Get("rd"),
992+ "Invalid redirect provided in rd querystring parameter: %s",
993+ )
994+}
995+
996+// getXAuthRequestRedirect handles this getAppRedirect strategy:
997+// - `X-Auth-Request-Redirect` Header
998+func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
999+ return p.validateRedirect(
1000+ req.Header.Get("X-Auth-Request-Redirect"),
1001+ "Invalid redirect provided in X-Auth-Request-Redirect header: %s",
1002+ )
1003+}
1004+
1005+// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
1006+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
1007+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
1008+func (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
1009+ if !isForwardedRequest(req) {
1010+ return ""
1011+ }
1012+
1013+ uri := requestutil.GetRequestURI(req)
1014+ if p.hasProxyPrefix(uri) {
1015+ uri = "/"
1016+ }
1017+
1018+ redirect := fmt.Sprintf(
1019+ "%s://%s%s",
1020+ requestutil.GetRequestProto(req),
1021+ requestutil.GetRequestHost(req),
1022+ uri,
1023+ )
1024+
1025+ return p.validateRedirect(redirect,
1026+ "Invalid redirect generated from X-Forwarded-* headers: %s")
1027+}
1028+
1029+// getURIRedirect handles these getAppRedirect strategies:
1030+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
1031+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
1032+// - `/`
1033+func (p *OAuthProxy) getURIRedirect(req *http.Request) string {
1034+ redirect := p.validateRedirect(
1035+ requestutil.GetRequestURI(req),
1036+ "Invalid redirect generated from X-Forwarded-Uri header: %s",
1037+ )
1038+ if redirect == "" {
1039+ redirect = req.URL.RequestURI()
1040+ }
1041+
1042+ if p.hasProxyPrefix(redirect) {
1043+ return "/"
1044+ }
1045+ return redirect
1046+}
1047+
1048+// splitHostPort separates host and port. If the port is not valid, it returns
1049+// the entire input as host, and it doesn't check the validity of the host.
1050+// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
1051+// *** taken from net/url, modified validOptionalPort() to accept ":*"
1052+func splitHostPort(hostport string) (host, port string) {
1053+ host = hostport
1054+
1055+ colon := strings.LastIndexByte(host, ':')
1056+ if colon != -1 && validOptionalPort(host[colon:]) {
1057+ host, port = host[:colon], host[colon+1:]
1058+ }
1059+
1060+ if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
1061+ host = host[1 : len(host)-1]
1062+ }
1063+
1064+ return
1065+}
1066+
1067+// validOptionalPort reports whether port is either an empty string
1068+// or matches /^:\d*$/
1069+// *** taken from net/url, modified to accept ":*"
1070+func validOptionalPort(port string) bool {
1071+ if port == "" || port == ":*" {
1072+ return true
1073+ }
1074+ if port[0] != ':' {
1075+ return false
1076+ }
1077+ for _, b := range port[1:] {
1078+ if b < '0' || b > '9' {
1079+ return false
1080+ }
1081+ }
1082+ return true
10161083}
10171084
10181085// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
@@ -1024,7 +1091,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
10241091 var session *sessionsapi.SessionState
10251092
10261093 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1027− session = middleware.GetRequestScope(req).Session
1094+ session = middlewareapi.GetRequestScope(req).Session
10281095 }))
10291096 getSession.ServeHTTP(rw, req)
10301097
@@ -1099,7 +1166,7 @@ func extractAllowedGroups(req *http.Request) map[string]struct{} {
10991166}
11001167
11011168// addHeadersForProxying adds the appropriate headers the request / response for proxying
1102−func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1169+func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
11031170 if session.Email == "" {
11041171 rw.Header().Set("GAP-Auth", session.User)
11051172 } else {
@@ -1127,8 +1194,8 @@ func isAjax(req *http.Request) bool {
11271194 return false
11281195}
11291196
1130−// ErrorJSON returns the error code with an application/json mime type
1131−func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1197+// errorJSON returns the error code with an application/json mime type
1198+func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
11321199 rw.Header().Set("Content-Type", applicationJSON)
11331200 rw.WriteHeader(code)
11341201}
oauthproxy_test.go+13−14
@@ -19,6 +19,7 @@ import (
1919
2020 "github.com/coreos/go-oidc"
2121 "github.com/mbland/hmacauth"
22+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
2223 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
2324 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
2425 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@@ -414,8 +415,9 @@ func Test_redeemCode(t *testing.T) {
414415 t.Fatal(err)
415416 }
416417
417− _, err = proxy.redeemCode(context.Background(), "www.example.com", "")
418− assert.Error(t, err)
418+ req := httptest.NewRequest(http.MethodGet, "/", nil)
419+ _, err = proxy.redeemCode(req)
420+ assert.Equal(t, providers.ErrMissingCode, err)
419421}
420422
421423func Test_enrichSession(t *testing.T) {
@@ -1748,10 +1750,9 @@ func TestRequestSignature(t *testing.T) {
17481750 }
17491751}
17501752
1751−func TestGetRedirect(t *testing.T) {
1753+func Test_getAppRedirect(t *testing.T) {
17521754 opts := baseTestOptions()
1753− opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com")
1754− opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com:8443")
1755+ opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com", ".example.com:8443")
17551756 err := validation.Validate(opts)
17561757 assert.NoError(t, err)
17571758 require.NotEmpty(t, opts.ProxyPrefix)
@@ -1854,9 +1855,6 @@ func TestGetRedirect(t *testing.T) {
18541855 url: "https://oauth.example.com/foo/bar",
18551856 headers: map[string]string{
18561857 "X-Auth-Request-Redirect": "https://a-service.example.com/foo/bar",
1857− "X-Forwarded-Proto": "",
1858− "X-Forwarded-Host": "",
1859− "X-Forwarded-Uri": "",
18601858 },
18611859 reverseProxy: true,
18621860 expectedRedirect: "https://a-service.example.com/foo/bar",
@@ -1884,10 +1882,9 @@ func TestGetRedirect(t *testing.T) {
18841882 name: "proxied request with rd query string and some headers set redirects to proxied URL on rd query string",
18851883 url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbaz",
18861884 headers: map[string]string{
1887− "X-Auth-Request-Redirect": "",
1888− "X-Forwarded-Proto": "https",
1889− "X-Forwarded-Host": "another-service.example.com",
1890− "X-Forwarded-Uri": "/seasons/greetings",
1885+ "X-Forwarded-Proto": "https",
1886+ "X-Forwarded-Host": "another-service.example.com",
1887+ "X-Forwarded-Uri": "/seasons/greetings",
18911888 },
18921889 reverseProxy: true,
18931890 expectedRedirect: "https://a-service.example.com/foo/baz",
@@ -1901,8 +1898,10 @@ func TestGetRedirect(t *testing.T) {
19011898 req.Header.Add(header, value)
19021899 }
19031900 }
1904− proxy.ReverseProxy = tt.reverseProxy
1905− redirect, err := proxy.GetRedirect(req)
1901+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
1902+ ReverseProxy: tt.reverseProxy,
1903+ })
1904+ redirect, err := proxy.getAppRedirect(req)
19061905
19071906 assert.NoError(t, err)
19081907 assert.Equal(t, tt.expectedRedirect, redirect)
pkg/apis/middleware/middleware_suite_test.gonew file+19
@@ -0,0 +1,19 @@
1+package middleware_test
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+// TestMiddlewareSuite and related tests are in a *_test package
12+// to prevent circular imports with the `logger` package which uses
13+// this functionality
14+func TestMiddlewareSuite(t *testing.T) {
15+ logger.SetOutput(GinkgoWriter)
16+
17+ RegisterFailHandler(Fail)
18+ RunSpecs(t, "Middleware API")
19+}
pkg/apis/middleware/scope.go+29
@@ -1,13 +1,26 @@
11package middleware
22
33import (
4+ "context"
5+ "net/http"
6+
47 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
58)
69
10+type scopeKey string
11+
12+// RequestScopeKey uses a typed string to reduce likelihood of clashing
13+// with other context keys
14+const RequestScopeKey scopeKey = "request-scope"
15+
716// RequestScope contains information regarding the request that is being made.
817// The RequestScope is used to pass information between different middlewares
918// within the chain.
1019type RequestScope struct {
20+ // ReverseProxy tracks whether OAuth2-Proxy is operating in reverse proxy
21+ // mode and if request `X-Forwarded-*` headers should be trusted
22+ ReverseProxy bool
23+
1124 // Session details the authenticated users information (if it exists).
1225 Session *sessions.SessionState
1326
@@ -22,3 +35,19 @@ type RequestScope struct {
2235 // it was loaded or not.
2336 SessionRevalidated bool
2437}
38+
39+// GetRequestScope returns the current request scope from the given request
40+func GetRequestScope(req *http.Request) *RequestScope {
41+ scope := req.Context().Value(RequestScopeKey)
42+ if scope == nil {
43+ return nil
44+ }
45+
46+ return scope.(*RequestScope)
47+}
48+
49+// AddRequestScope adds a RequestScope to a request
50+func AddRequestScope(req *http.Request, scope *RequestScope) *http.Request {
51+ ctx := context.WithValue(req.Context(), RequestScopeKey, scope)
52+ return req.WithContext(ctx)
53+}
pkg/apis/middleware/scope_test.gonew file+56
@@ -0,0 +1,56 @@
1+package middleware_test
2+
3+import (
4+ "net/http"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+var _ = Describe("Scope Suite", func() {
12+ Context("GetRequestScope", func() {
13+ var request *http.Request
14+
15+ BeforeEach(func() {
16+ var err error
17+ request, err = http.NewRequest("", "http://127.0.0.1/", nil)
18+ Expect(err).ToNot(HaveOccurred())
19+ })
20+
21+ Context("with a scope", func() {
22+ var scope *middleware.RequestScope
23+
24+ BeforeEach(func() {
25+ scope = &middleware.RequestScope{}
26+ request = middleware.AddRequestScope(request, scope)
27+ })
28+
29+ It("returns the scope", func() {
30+ s := middleware.GetRequestScope(request)
31+ Expect(s).ToNot(BeNil())
32+ Expect(s).To(Equal(scope))
33+ })
34+
35+ Context("if the scope is then modified", func() {
36+ BeforeEach(func() {
37+ Expect(scope.SaveSession).To(BeFalse())
38+ scope.SaveSession = true
39+ })
40+
41+ It("returns the updated session", func() {
42+ s := middleware.GetRequestScope(request)
43+ Expect(s).ToNot(BeNil())
44+ Expect(s).To(Equal(scope))
45+ Expect(s.SaveSession).To(BeTrue())
46+ })
47+ })
48+ })
49+
50+ Context("without a scope", func() {
51+ It("returns nil", func() {
52+ Expect(middleware.GetRequestScope(request)).To(BeNil())
53+ })
54+ })
55+ })
56+})
pkg/cookies/cookies.go+4−4
@@ -9,14 +9,14 @@ import (
99
1010 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1111 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
12− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
12+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1313)
1414
1515// MakeCookie constructs a cookie from the given parameters,
1616// discovering the domain from the request if not specified.
1717func MakeCookie(req *http.Request, name string, value string, path string, domain string, httpOnly bool, secure bool, expiration time.Duration, now time.Time, sameSite http.SameSite) *http.Cookie {
1818 if domain != "" {
19− host := util.GetRequestHost(req)
19+ host := requestutil.GetRequestHost(req)
2020 if h, _, err := net.SplitHostPort(host); err == nil {
2121 host = h
2222 }
@@ -48,7 +48,7 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, cookieO
4848 // If nothing matches, create the cookie with the shortest domain
4949 defaultDomain := ""
5050 if len(cookieOpts.Domains) > 0 {
51− logger.Errorf("Warning: request host %q did not match any of the specific cookie domains of %q", util.GetRequestHost(req), strings.Join(cookieOpts.Domains, ","))
51+ logger.Errorf("Warning: request host %q did not match any of the specific cookie domains of %q", requestutil.GetRequestHost(req), strings.Join(cookieOpts.Domains, ","))
5252 defaultDomain = cookieOpts.Domains[len(cookieOpts.Domains)-1]
5353 }
5454 return MakeCookie(req, name, value, cookieOpts.Path, defaultDomain, cookieOpts.HTTPOnly, cookieOpts.Secure, expiration, now, ParseSameSite(cookieOpts.SameSite))
@@ -57,7 +57,7 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, cookieO
5757// GetCookieDomain returns the correct cookie domain given a list of domains
5858// by checking the X-Fowarded-Host and host header of an an http request
5959func GetCookieDomain(req *http.Request, cookieDomains []string) string {
60− host := util.GetRequestHost(req)
60+ host := requestutil.GetRequestHost(req)
6161 for _, domain := range cookieDomains {
6262 if strings.HasSuffix(host, domain) {
6363 return domain
pkg/logger/logger.go+3−3
@@ -12,7 +12,7 @@ import (
1212 "text/template"
1313 "time"
1414
15− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
15+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1616)
1717
1818// AuthStatus defines the different types of auth logging that occur
@@ -197,7 +197,7 @@ func (l *Logger) PrintAuthf(username string, req *http.Request, status AuthStatu
197197
198198 err := l.authTemplate.Execute(l.writer, authLogMessageData{
199199 Client: client,
200− Host: util.GetRequestHost(req),
200+ Host: requestutil.GetRequestHost(req),
201201 Protocol: req.Proto,
202202 RequestMethod: req.Method,
203203 Timestamp: FormatTimestamp(now),
@@ -251,7 +251,7 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url.
251251
252252 err := l.reqTemplate.Execute(l.writer, reqLogMessageData{
253253 Client: client,
254− Host: util.GetRequestHost(req),
254+ Host: requestutil.GetRequestHost(req),
255255 Protocol: req.Proto,
256256 RequestDuration: fmt.Sprintf("%0.3f", duration),
257257 RequestMethod: req.Method,
pkg/middleware/basic_session.go+2−1
@@ -5,6 +5,7 @@ import (
55 "net/http"
66
77 "github.com/justinas/alice"
8+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
89 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@@ -23,7 +24,7 @@ func NewBasicAuthSessionLoader(validator basic.Validator) alice.Constructor {
2324// If a session was loaded by a previous handler, it will not be replaced.
2425func loadBasicAuthSession(validator basic.Validator, next http.Handler) http.Handler {
2526 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
26− scope := GetRequestScope(req)
27+ scope := middlewareapi.GetRequestScope(req)
2728 // If scope is nil, this will panic.
2829 // A scope should always be injected before this handler is called.
2930 if scope.Session != nil {
pkg/middleware/basic_session_test.go+2−4
@@ -1,7 +1,6 @@
11package middleware
22
33import (
4− "context"
54 "fmt"
65 "net/http"
76 "net/http/httptest"
@@ -40,8 +39,7 @@ var _ = Describe("Basic Auth Session Suite", func() {
4039 // Set up the request with the authorization header and a request scope
4140 req := httptest.NewRequest("", "/", nil)
4241 req.Header.Set("Authorization", in.authorizationHeader)
43− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
44− req = req.WithContext(contextWithScope)
42+ req = middlewareapi.AddRequestScope(req, scope)
4543
4644 rw := httptest.NewRecorder()
4745
@@ -57,7 +55,7 @@ var _ = Describe("Basic Auth Session Suite", func() {
5755 // from the scope
5856 var gotSession *sessionsapi.SessionState
5957 handler := NewBasicAuthSessionLoader(validator)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60− gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
58+ gotSession = middlewareapi.GetRequestScope(r).Session
6159 }))
6260 handler.ServeHTTP(rw, req)
6361
pkg/middleware/headers.go+3−2
@@ -5,6 +5,7 @@ import (
55 "net/http"
66
77 "github.com/justinas/alice"
8+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
89 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/header"
1011)
@@ -61,7 +62,7 @@ func newRequestHeaderInjector(headers []options.Header) (alice.Constructor, erro
6162
6263func injectRequestHeaders(injector header.Injector, next http.Handler) http.Handler {
6364 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
64− scope := GetRequestScope(req)
65+ scope := middlewareapi.GetRequestScope(req)
6566
6667 // If scope is nil, this will panic.
6768 // A scope should always be injected before this handler is called.
@@ -92,7 +93,7 @@ func newResponseHeaderInjector(headers []options.Header) (alice.Constructor, err
9293
9394func injectResponseHeaders(injector header.Injector, next http.Handler) http.Handler {
9495 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
95− scope := GetRequestScope(req)
96+ scope := middlewareapi.GetRequestScope(req)
9697
9798 // If scope is nil, this will panic.
9899 // A scope should always be injected before this handler is called.
pkg/middleware/headers_test.go+2−5
@@ -1,7 +1,6 @@
11package middleware
22
33import (
4− "context"
54 "encoding/base64"
65 "net/http"
76 "net/http/httptest"
@@ -31,8 +30,7 @@ var _ = Describe("Headers Suite", func() {
3130
3231 // Set up the request with a request scope
3332 req := httptest.NewRequest("", "/", nil)
34− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
35− req = req.WithContext(contextWithScope)
33+ req = middlewareapi.AddRequestScope(req, scope)
3634 req.Header = in.initialHeaders.Clone()
3735
3836 rw := httptest.NewRecorder()
@@ -218,8 +216,7 @@ var _ = Describe("Headers Suite", func() {
218216
219217 // Set up the request with a request scope
220218 req := httptest.NewRequest("", "/", nil)
221− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
222− req = req.WithContext(contextWithScope)
219+ req = middlewareapi.AddRequestScope(req, scope)
223220
224221 rw := httptest.NewRecorder()
225222 for key, values := range in.initialHeaders {
pkg/middleware/jwt_session.go+1−1
@@ -37,7 +37,7 @@ type jwtSessionLoader struct {
3737// If a session was loaded by a previous handler, it will not be replaced.
3838func (j *jwtSessionLoader) loadSession(next http.Handler) http.Handler {
3939 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
40− scope := GetRequestScope(req)
40+ scope := middlewareapi.GetRequestScope(req)
4141 // If scope is nil, this will panic.
4242 // A scope should always be injected before this handler is called.
4343 if scope.Session != nil {
pkg/middleware/jwt_session_test.go+2−3
@@ -103,8 +103,7 @@ Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
103103 // Set up the request with the authorization header and a request scope
104104 req := httptest.NewRequest("", "/", nil)
105105 req.Header.Set("Authorization", in.authorizationHeader)
106− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
107− req = req.WithContext(contextWithScope)
106+ req = middlewareapi.AddRequestScope(req, scope)
108107
109108 rw := httptest.NewRecorder()
110109
@@ -116,7 +115,7 @@ Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
116115 // from the scope
117116 var gotSession *sessionsapi.SessionState
118117 handler := NewJwtSessionLoader(sessionLoaders)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
119− gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
118+ gotSession = middlewareapi.GetRequestScope(r).Session
120119 }))
121120 handler.ServeHTTP(rw, req)
122121
pkg/middleware/redirect_to_https.go+7−6
@@ -7,7 +7,7 @@ import (
77 "strings"
88
99 "github.com/justinas/alice"
10− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
10+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1111)
1212
1313const httpsScheme = "https"
@@ -26,10 +26,11 @@ func NewRedirectToHTTPS(httpsPort string) alice.Constructor {
2626// to the port from the httpsAddress given.
2727func redirectToHTTPS(httpsPort string, next http.Handler) http.Handler {
2828 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
29− proto := req.Header.Get("X-Forwarded-Proto")
30− if strings.EqualFold(proto, httpsScheme) || (req.TLS != nil && proto == "") {
31− // Only care about the connection to us being HTTPS if the proto is empty,
32− // otherwise the proto is source of truth
29+ proto := requestutil.GetRequestProto(req)
30+ if strings.EqualFold(proto, httpsScheme) || (req.TLS != nil && proto == req.URL.Scheme) {
31+ // Only care about the connection to us being HTTPS if the proto wasn't
32+ // from a trusted `X-Forwarded-Proto` (proto == req.URL.Scheme).
33+ // Otherwise the proto is source of truth
3334 next.ServeHTTP(rw, req)
3435 return
3536 }
@@ -41,7 +42,7 @@ func redirectToHTTPS(httpsPort string, next http.Handler) http.Handler {
4142
4243 // Set the Host in case the targetURL still does not have one
4344 // or it isn't X-Forwarded-Host aware
44− targetURL.Host = util.GetRequestHost(req)
45+ targetURL.Host = requestutil.GetRequestHost(req)
4546
4647 // Overwrite the port if the original request was to a non-standard port
4748 if targetURL.Port() != "" {
pkg/middleware/redirect_to_https_test.go+31
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "net/http/httptest"
77
8+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
89 . "github.com/onsi/ginkgo"
910 . "github.com/onsi/ginkgo/extensions/table"
1011 . "github.com/onsi/gomega"
@@ -21,6 +22,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
2122 requestString string
2223 useTLS bool
2324 headers map[string]string
25+ reverseProxy bool
2426 expectedStatus int
2527 expectedBody string
2628 expectedLocation string
@@ -35,6 +37,10 @@ var _ = Describe("RedirectToHTTPS suite", func() {
3537 if in.useTLS {
3638 req.TLS = &tls.ConnectionState{}
3739 }
40+ scope := &middlewareapi.RequestScope{
41+ ReverseProxy: in.reverseProxy,
42+ }
43+ req = middlewareapi.AddRequestScope(req, scope)
3844
3945 rw := httptest.NewRecorder()
4046
@@ -52,6 +58,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
5258 requestString: "http://example.com",
5359 useTLS: false,
5460 headers: map[string]string{},
61+ reverseProxy: false,
5562 expectedStatus: 308,
5663 expectedBody: permanentRedirectBody("https://example.com"),
5764 expectedLocation: "https://example.com",
@@ -60,6 +67,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
6067 requestString: "https://example.com",
6168 useTLS: true,
6269 headers: map[string]string{},
70+ reverseProxy: false,
6371 expectedStatus: 200,
6472 expectedBody: "test",
6573 }),
@@ -69,15 +77,28 @@ var _ = Describe("RedirectToHTTPS suite", func() {
6977 headers: map[string]string{
7078 "X-Forwarded-Proto": "HTTPS",
7179 },
80+ reverseProxy: true,
7281 expectedStatus: 200,
7382 expectedBody: "test",
7483 }),
84+ Entry("without TLS and X-Forwarded-Proto=HTTPS but ReverseProxy not set", &requestTableInput{
85+ requestString: "http://example.com",
86+ useTLS: false,
87+ headers: map[string]string{
88+ "X-Forwarded-Proto": "HTTPS",
89+ },
90+ reverseProxy: false,
91+ expectedStatus: 308,
92+ expectedBody: permanentRedirectBody("https://example.com"),
93+ expectedLocation: "https://example.com",
94+ }),
7595 Entry("with TLS and X-Forwarded-Proto=HTTPS", &requestTableInput{
7696 requestString: "https://example.com",
7797 useTLS: true,
7898 headers: map[string]string{
7999 "X-Forwarded-Proto": "HTTPS",
80100 },
101+ reverseProxy: true,
81102 expectedStatus: 200,
82103 expectedBody: "test",
83104 }),
@@ -87,6 +108,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
87108 headers: map[string]string{
88109 "X-Forwarded-Proto": "https",
89110 },
111+ reverseProxy: true,
90112 expectedStatus: 200,
91113 expectedBody: "test",
92114 }),
@@ -96,6 +118,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
96118 headers: map[string]string{
97119 "X-Forwarded-Proto": "https",
98120 },
121+ reverseProxy: true,
99122 expectedStatus: 200,
100123 expectedBody: "test",
101124 }),
@@ -105,6 +128,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
105128 headers: map[string]string{
106129 "X-Forwarded-Proto": "HTTP",
107130 },
131+ reverseProxy: true,
108132 expectedStatus: 308,
109133 expectedBody: permanentRedirectBody("https://example.com"),
110134 expectedLocation: "https://example.com",
@@ -115,6 +139,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
115139 headers: map[string]string{
116140 "X-Forwarded-Proto": "HTTP",
117141 },
142+ reverseProxy: true,
118143 expectedStatus: 308,
119144 expectedBody: permanentRedirectBody("https://example.com"),
120145 expectedLocation: "https://example.com",
@@ -125,6 +150,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
125150 headers: map[string]string{
126151 "X-Forwarded-Proto": "http",
127152 },
153+ reverseProxy: true,
128154 expectedStatus: 308,
129155 expectedBody: permanentRedirectBody("https://example.com"),
130156 expectedLocation: "https://example.com",
@@ -135,6 +161,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
135161 headers: map[string]string{
136162 "X-Forwarded-Proto": "http",
137163 },
164+ reverseProxy: true,
138165 expectedStatus: 308,
139166 expectedBody: permanentRedirectBody("https://example.com"),
140167 expectedLocation: "https://example.com",
@@ -143,6 +170,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
143170 requestString: "http://example.com:8080",
144171 useTLS: false,
145172 headers: map[string]string{},
173+ reverseProxy: false,
146174 expectedStatus: 308,
147175 expectedBody: permanentRedirectBody("https://example.com:8443"),
148176 expectedLocation: "https://example.com:8443",
@@ -151,6 +179,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
151179 requestString: "https://example.com:8443",
152180 useTLS: true,
153181 headers: map[string]string{},
182+ reverseProxy: false,
154183 expectedStatus: 200,
155184 expectedBody: "test",
156185 }),
@@ -161,6 +190,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
161190 requestString: "/",
162191 useTLS: false,
163192 expectedStatus: 308,
193+ reverseProxy: false,
164194 expectedBody: permanentRedirectBody("https://example.com/"),
165195 expectedLocation: "https://example.com/",
166196 }),
@@ -171,6 +201,7 @@ var _ = Describe("RedirectToHTTPS suite", func() {
171201 "X-Forwarded-Proto": "HTTP",
172202 "X-Forwarded-Host": "external.example.com",
173203 },
204+ reverseProxy: true,
174205 expectedStatus: 308,
175206 expectedBody: permanentRedirectBody("https://external.example.com"),
176207 expectedLocation: "https://external.example.com",
pkg/middleware/scope.go+9−28
@@ -1,39 +1,20 @@
11package middleware
22
33import (
4− "context"
54 "net/http"
65
76 "github.com/justinas/alice"
87 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
98)
109
11−type scopeKey string
12−
13−// requestScopeKey uses a typed string to reduce likelihood of clasing
14−// with other context keys
15−const requestScopeKey scopeKey = "request-scope"
16−
17−func NewScope() alice.Constructor {
18− return addScope
19−}
20−
21−// addScope injects a new request scope into the request context.
22−func addScope(next http.Handler) http.Handler {
23− return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
24− scope := &middlewareapi.RequestScope{}
25− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
26− requestWithScope := req.WithContext(contextWithScope)
27− next.ServeHTTP(rw, requestWithScope)
28− })
29−}
30−
31−// GetRequestScope returns the current request scope from the given request
32−func GetRequestScope(req *http.Request) *middlewareapi.RequestScope {
33− scope := req.Context().Value(requestScopeKey)
34− if scope == nil {
35− return nil
10+func NewScope(reverseProxy bool) alice.Constructor {
11+ return func(next http.Handler) http.Handler {
12+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
13+ scope := &middlewareapi.RequestScope{
14+ ReverseProxy: reverseProxy,
15+ }
16+ req = middlewareapi.AddRequestScope(req, scope)
17+ next.ServeHTTP(rw, req)
18+ })
3619 }
37−
38− return scope.(*middlewareapi.RequestScope)
3920}
pkg/middleware/scope_test.go+32−57
@@ -1,7 +1,6 @@
11package middleware
22
33import (
4− "context"
54 "net/http"
65 "net/http/httptest"
76
@@ -21,73 +20,49 @@ var _ = Describe("Scope Suite", func() {
2120 Expect(err).ToNot(HaveOccurred())
2221
2322 rw = httptest.NewRecorder()
24−
25− handler := NewScope()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26− nextRequest = r
27− w.WriteHeader(200)
28− }))
29− handler.ServeHTTP(rw, request)
30− })
31−
32− It("does not add a scope to the original request", func() {
33− Expect(request.Context().Value(requestScopeKey)).To(BeNil())
34− })
35−
36− It("cannot load a scope from the original request using GetRequestScope", func() {
37− Expect(GetRequestScope(request)).To(BeNil())
38− })
39−
40− It("adds a scope to the request for the next handler", func() {
41− Expect(nextRequest.Context().Value(requestScopeKey)).ToNot(BeNil())
42− })
43−
44− It("can load a scope from the next handler's request using GetRequestScope", func() {
45− Expect(GetRequestScope(nextRequest)).ToNot(BeNil())
46− })
47− })
48−
49− Context("GetRequestScope", func() {
50− var request *http.Request
51−
52− BeforeEach(func() {
53− var err error
54− request, err = http.NewRequest("", "http://127.0.0.1/", nil)
55− Expect(err).ToNot(HaveOccurred())
5623 })
5724
58− Context("with a scope", func() {
59− var scope *middlewareapi.RequestScope
60−
25+ Context("ReverseProxy is false", func() {
6126 BeforeEach(func() {
62− scope = &middlewareapi.RequestScope{}
63− contextWithScope := context.WithValue(request.Context(), requestScopeKey, scope)
64− request = request.WithContext(contextWithScope)
27+ handler := NewScope(false)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+ nextRequest = r
29+ w.WriteHeader(200)
30+ }))
31+ handler.ServeHTTP(rw, request)
6532 })
6633
67− It("returns the scope", func() {
68− s := GetRequestScope(request)
69− Expect(s).ToNot(BeNil())
70− Expect(s).To(Equal(scope))
34+ It("does not add a scope to the original request", func() {
35+ Expect(request.Context().Value(middlewareapi.RequestScopeKey)).To(BeNil())
7136 })
7237
73− Context("if the scope is then modified", func() {
74− BeforeEach(func() {
75− Expect(scope.SaveSession).To(BeFalse())
76− scope.SaveSession = true
77− })
38+ It("cannot load a scope from the original request using GetRequestScope", func() {
39+ Expect(middlewareapi.GetRequestScope(request)).To(BeNil())
40+ })
41+
42+ It("adds a scope to the request for the next handler", func() {
43+ Expect(nextRequest.Context().Value(middlewareapi.RequestScopeKey)).ToNot(BeNil())
44+ })
7845
79− It("returns the updated session", func() {
80− s := GetRequestScope(request)
81− Expect(s).ToNot(BeNil())
82− Expect(s).To(Equal(scope))
83− Expect(s.SaveSession).To(BeTrue())
84− })
46+ It("can load a scope from the next handler's request using GetRequestScope", func() {
47+ scope := middlewareapi.GetRequestScope(nextRequest)
48+ Expect(scope).ToNot(BeNil())
49+ Expect(scope.ReverseProxy).To(BeFalse())
8550 })
8651 })
8752
88− Context("without a scope", func() {
89− It("returns nil", func() {
90− Expect(GetRequestScope(request)).To(BeNil())
53+ Context("ReverseProxy is true", func() {
54+ BeforeEach(func() {
55+ handler := NewScope(true)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+ nextRequest = r
57+ w.WriteHeader(200)
58+ }))
59+ handler.ServeHTTP(rw, request)
60+ })
61+
62+ It("return a scope where the ReverseProxy field is true", func() {
63+ scope := middlewareapi.GetRequestScope(nextRequest)
64+ Expect(scope).ToNot(BeNil())
65+ Expect(scope.ReverseProxy).To(BeTrue())
9166 })
9267 })
9368 })
pkg/middleware/stored_session.go+2−1
@@ -8,6 +8,7 @@ import (
88 "time"
99
1010 "github.com/justinas/alice"
11+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
1112 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1213 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1314)
@@ -59,7 +60,7 @@ type storedSessionLoader struct {
5960// If a session was loader by a previous handler, it will not be replaced.
6061func (s *storedSessionLoader) loadSession(next http.Handler) http.Handler {
6162 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
62− scope := GetRequestScope(req)
63+ scope := middlewareapi.GetRequestScope(req)
6364 // If scope is nil, this will panic.
6465 // A scope should always be injected before this handler is called.
6566 if scope.Session != nil {
pkg/middleware/stored_session_test.go+2−3
@@ -104,8 +104,7 @@ var _ = Describe("Stored Session Suite", func() {
104104 // Set up the request with the request headesr and a request scope
105105 req := httptest.NewRequest("", "/", nil)
106106 req.Header = in.requestHeaders
107− contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
108− req = req.WithContext(contextWithScope)
107+ req = middlewareapi.AddRequestScope(req, scope)
109108
110109 rw := httptest.NewRecorder()
111110
@@ -120,7 +119,7 @@ var _ = Describe("Stored Session Suite", func() {
120119 // from the scope
121120 var gotSession *sessionsapi.SessionState
122121 handler := NewStoredSessionLoader(opts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
123− gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
122+ gotSession = middlewareapi.GetRequestScope(r).Session
124123 }))
125124 handler.ServeHTTP(rw, req)
126125
pkg/requests/util/util.gonew file+48
@@ -0,0 +1,48 @@
1+package util
2+
3+import (
4+ "net/http"
5+
6+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+)
8+
9+// GetRequestProto returns the request scheme or X-Forwarded-Proto if present
10+// and the request is proxied.
11+func GetRequestProto(req *http.Request) string {
12+ proto := req.Header.Get("X-Forwarded-Proto")
13+ if !IsProxied(req) || proto == "" {
14+ proto = req.URL.Scheme
15+ }
16+ return proto
17+}
18+
19+// GetRequestHost returns the request host header or X-Forwarded-Host if
20+// present and the request is proxied.
21+func GetRequestHost(req *http.Request) string {
22+ host := req.Header.Get("X-Forwarded-Host")
23+ if !IsProxied(req) || host == "" {
24+ host = req.Host
25+ }
26+ return host
27+}
28+
29+// GetRequestURI return the request URI or X-Forwarded-Uri if present and the
30+// request is proxied.
31+func GetRequestURI(req *http.Request) string {
32+ uri := req.Header.Get("X-Forwarded-Uri")
33+ if !IsProxied(req) || uri == "" {
34+ // Use RequestURI to preserve ?query
35+ uri = req.URL.RequestURI()
36+ }
37+ return uri
38+}
39+
40+// IsProxied determines if a request was from a proxy based on the RequestScope
41+// ReverseProxy tracker.
42+func IsProxied(req *http.Request) bool {
43+ scope := middlewareapi.GetRequestScope(req)
44+ if scope == nil {
45+ return false
46+ }
47+ return scope.ReverseProxy
48+}
pkg/requests/util/util_suite_test.gonew file+19
@@ -0,0 +1,19 @@
1+package util_test
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+// TestRequestUtilSuite and related tests are in a *_test package
12+// to prevent circular imports with the `logger` package which uses
13+// this functionality
14+func TestRequestUtilSuite(t *testing.T) {
15+ logger.SetOutput(GinkgoWriter)
16+
17+ RegisterFailHandler(Fail)
18+ RunSpecs(t, "Request Utils")
19+}
pkg/requests/util/util_test.gonew file+131
@@ -0,0 +1,131 @@
1+package util_test
2+
3+import (
4+ "fmt"
5+ "net/http"
6+ "net/http/httptest"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var _ = Describe("Util Suite", func() {
15+ const (
16+ proto = "http"
17+ host = "www.oauth2proxy.test"
18+ uri = "/test/endpoint"
19+ )
20+ var req *http.Request
21+
22+ BeforeEach(func() {
23+ req = httptest.NewRequest(
24+ http.MethodGet,
25+ fmt.Sprintf("%s://%s%s", proto, host, uri),
26+ nil,
27+ )
28+ })
29+
30+ Context("GetRequestHost", func() {
31+ Context("IsProxied is false", func() {
32+ BeforeEach(func() {
33+ req = middleware.AddRequestScope(req, &middleware.RequestScope{})
34+ })
35+
36+ It("returns the host", func() {
37+ Expect(util.GetRequestHost(req)).To(Equal(host))
38+ })
39+
40+ It("ignores X-Forwarded-Host and returns the host", func() {
41+ req.Header.Add("X-Forwarded-Host", "external.oauth2proxy.text")
42+ Expect(util.GetRequestHost(req)).To(Equal(host))
43+ })
44+ })
45+
46+ Context("IsProxied is true", func() {
47+ BeforeEach(func() {
48+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
49+ ReverseProxy: true,
50+ })
51+ })
52+
53+ It("returns the host if X-Forwarded-Host is not present", func() {
54+ Expect(util.GetRequestHost(req)).To(Equal(host))
55+ })
56+
57+ It("returns the X-Forwarded-Host when present", func() {
58+ req.Header.Add("X-Forwarded-Host", "external.oauth2proxy.text")
59+ Expect(util.GetRequestHost(req)).To(Equal("external.oauth2proxy.text"))
60+ })
61+ })
62+ })
63+
64+ Context("GetRequestProto", func() {
65+ Context("IsProxied is false", func() {
66+ BeforeEach(func() {
67+ req = middleware.AddRequestScope(req, &middleware.RequestScope{})
68+ })
69+
70+ It("returns the scheme", func() {
71+ Expect(util.GetRequestProto(req)).To(Equal(proto))
72+ })
73+
74+ It("ignores X-Forwarded-Proto and returns the scheme", func() {
75+ req.Header.Add("X-Forwarded-Proto", "https")
76+ Expect(util.GetRequestProto(req)).To(Equal(proto))
77+ })
78+ })
79+
80+ Context("IsProxied is true", func() {
81+ BeforeEach(func() {
82+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
83+ ReverseProxy: true,
84+ })
85+ })
86+
87+ It("returns the scheme if X-Forwarded-Proto is not present", func() {
88+ Expect(util.GetRequestProto(req)).To(Equal(proto))
89+ })
90+
91+ It("returns the X-Forwarded-Proto when present", func() {
92+ req.Header.Add("X-Forwarded-Proto", "https")
93+ Expect(util.GetRequestProto(req)).To(Equal("https"))
94+ })
95+ })
96+ })
97+
98+ Context("GetRequestURI", func() {
99+ Context("IsProxied is false", func() {
100+ BeforeEach(func() {
101+ req = middleware.AddRequestScope(req, &middleware.RequestScope{})
102+ })
103+
104+ It("returns the URI", func() {
105+ Expect(util.GetRequestURI(req)).To(Equal(uri))
106+ })
107+
108+ It("ignores X-Forwarded-Uri and returns the URI", func() {
109+ req.Header.Add("X-Forwarded-Uri", "/some/other/path")
110+ Expect(util.GetRequestURI(req)).To(Equal(uri))
111+ })
112+ })
113+
114+ Context("IsProxied is true", func() {
115+ BeforeEach(func() {
116+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
117+ ReverseProxy: true,
118+ })
119+ })
120+
121+ It("returns the URI if X-Forwarded-Uri is not present", func() {
122+ Expect(util.GetRequestURI(req)).To(Equal(uri))
123+ })
124+
125+ It("returns the X-Forwarded-Uri when present", func() {
126+ req.Header.Add("X-Forwarded-Uri", "/some/other/path")
127+ Expect(util.GetRequestURI(req)).To(Equal("/some/other/path"))
128+ })
129+ })
130+ })
131+})
pkg/util/util.go−29
@@ -4,7 +4,6 @@ import (
44 "crypto/x509"
55 "fmt"
66 "io/ioutil"
7− "net/http"
87)
98
109func GetCertPool(paths []string) (*x509.CertPool, error) {
@@ -24,31 +23,3 @@ func GetCertPool(paths []string) (*x509.CertPool, error) {
2423 }
2524 return pool, nil
2625}
27−
28−// GetRequestProto return the request host header or X-Forwarded-Proto if present
29−func GetRequestProto(req *http.Request) string {
30− proto := req.Header.Get("X-Forwarded-Proto")
31− if proto == "" {
32− proto = req.URL.Scheme
33− }
34− return proto
35−}
36−
37−// GetRequestHost return the request host header or X-Forwarded-Host if present
38−func GetRequestHost(req *http.Request) string {
39− host := req.Header.Get("X-Forwarded-Host")
40− if host == "" {
41− host = req.Host
42− }
43− return host
44−}
45−
46−// GetRequestURI return the request host header or X-Forwarded-Uri if present
47−func GetRequestURI(req *http.Request) string {
48− uri := req.Header.Get("X-Forwarded-Uri")
49− if uri == "" {
50− // Use RequestURI to preserve ?query
51− uri = req.URL.RequestURI()
52− }
53− return uri
54−}
pkg/util/util_test.go−41
@@ -4,11 +4,9 @@ import (
44 "crypto/x509/pkix"
55 "encoding/asn1"
66 "io/ioutil"
7− "net/http/httptest"
87 "os"
98 "testing"
109
11− . "github.com/onsi/gomega"
1210 "github.com/stretchr/testify/assert"
1311)
1412
@@ -97,42 +95,3 @@ func TestGetCertPool(t *testing.T) {
9795 expectedSubjects := []string{testCA1Subj, testCA2Subj}
9896 assert.Equal(t, expectedSubjects, got)
9997}
100−
101−func TestGetRequestHost(t *testing.T) {
102− g := NewWithT(t)
103−
104− req := httptest.NewRequest("GET", "https://example.com", nil)
105− host := GetRequestHost(req)
106− g.Expect(host).To(Equal("example.com"))
107−
108− proxyReq := httptest.NewRequest("GET", "http://internal.example.com", nil)
109− proxyReq.Header.Add("X-Forwarded-Host", "external.example.com")
110− extHost := GetRequestHost(proxyReq)
111− g.Expect(extHost).To(Equal("external.example.com"))
112−}
113−
114−func TestGetRequestProto(t *testing.T) {
115− g := NewWithT(t)
116−
117− req := httptest.NewRequest("GET", "https://example.com", nil)
118− proto := GetRequestProto(req)
119− g.Expect(proto).To(Equal("https"))
120−
121− proxyReq := httptest.NewRequest("GET", "https://internal.example.com", nil)
122− proxyReq.Header.Add("X-Forwarded-Proto", "http")
123− extProto := GetRequestProto(proxyReq)
124− g.Expect(extProto).To(Equal("http"))
125−}
126−
127−func TestGetRequestURI(t *testing.T) {
128− g := NewWithT(t)
129−
130− req := httptest.NewRequest("GET", "https://example.com/ping", nil)
131− uri := GetRequestURI(req)
132− g.Expect(uri).To(Equal("/ping"))
133−
134− proxyReq := httptest.NewRequest("GET", "http://internal.example.com/bong", nil)
135− proxyReq.Header.Add("X-Forwarded-Uri", "/ping")
136− extURI := GetRequestURI(proxyReq)
137− g.Expect(extURI).To(Equal("/ping"))
138−}