@@ -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}