git.iko.soy/typeable/oauth2-proxy
on master help sign in
Improve handler vs helper organization in oauthproxy.go

Additionally, convert a lot of helper methods to be private

commitfa6a785eafa29ecb32764c52ca75807f501cf090 authorNick Meves <nick.meves@greenhouse.io> dateSat 2 Jan 2021, 14:20 (6 years ago) parent73fc770
2 files changed+388−385
oauthproxy.go+383−381 oauthproxy_test.go+5−4
oauthproxy.go+383−381
@@ -31,9 +31,7 @@ import (
3131)
3232
3333const (
34− httpScheme = "http"
35− httpsScheme = "https"
36−
34+ schemeHTTPS = "https"
3735 applicationJSON = "application/json"
3836)
3937
@@ -366,49 +364,6 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
366364 return routes, nil
367365}
368366
369−// GetOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
370−// redirect clients to once authenticated
371−func (p *OAuthProxy) GetOAuthRedirectURI(host string) string {
372− // default to the request Host if not set
373− if p.redirectURL.Host != "" {
374− return p.redirectURL.String()
375− }
376− u := *p.redirectURL
377− if u.Scheme == "" {
378− if p.CookieSecure {
379− u.Scheme = httpsScheme
380− } else {
381− u.Scheme = httpScheme
382− }
383− }
384− u.Host = host
385− return u.String()
386−}
387−
388−func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
389− if code == "" {
390− return nil, providers.ErrMissingCode
391− }
392− redirectURI := p.GetOAuthRedirectURI(host)
393− s, err := p.provider.Redeem(ctx, redirectURI, code)
394− if err != nil {
395− return nil, err
396− }
397− return s, nil
398−}
399−
400−func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
401− var err error
402− if s.Email == "" {
403− s.Email, err = p.provider.GetEmailAddress(ctx, s)
404− if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
405− return err
406− }
407− }
408−
409− return p.provider.EnrichSession(ctx, s)
410−}
411−
412367// MakeCSRFCookie creates a cookie for CSRF
413368func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
414369 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
@@ -466,6 +421,81 @@ func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *s
466421 return p.sessionStore.Save(rw, req, s)
467422}
468423
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+
469499// RobotsTxt disallows scraping pages from the OAuthProxy
470500func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
471501 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
@@ -496,6 +526,42 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m
496526 }
497527}
498528
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+
499565// SignInPage writes the sing in template to the response
500566func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
501567 prepareNoCache(rw)
@@ -507,7 +573,7 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
507573 }
508574 rw.WriteHeader(code)
509575
510− redirectURL, err := p.GetAppRedirect(req)
576+ redirectURL, err := p.getAppRedirect(req)
511577 if err != nil {
512578 logger.Errorf("Error obtaining redirect: %v", err)
513579 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
@@ -566,347 +632,80 @@ func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
566632 return "", false
567633}
568634
569−// GetAppRedirect determines the full URL or URI path to redirect clients to
570−// once authenticated with the OAuthProxy
571−// Strategy priority (first legal result is used):
572−// - `rd` querysting parameter
573−// - `X-Auth-Request-Redirect` header
574−// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
575−// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
576−// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
577−// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
578−// - `/`
579−func (p *OAuthProxy) GetAppRedirect(req *http.Request) (string, error) {
580− err := req.ParseForm()
635+// SignIn serves a page prompting users to sign in
636+func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
637+ redirect, err := p.getAppRedirect(req)
581638 if err != nil {
582− return "", err
639+ logger.Errorf("Error obtaining redirect: %v", err)
640+ p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
641+ return
583642 }
584643
585− // These redirect getter functions are strategies ordered by priority
586− // for figuring out the redirect URL.
587− type redirectGetter func(req *http.Request) string
588− for _, rdGetter := range []redirectGetter{
589− p.getRdQuerystringRedirect,
590− p.getXAuthRequestRedirect,
591− p.getXForwardedHeadersRedirect,
592− p.getURIRedirect,
593− } {
594− if redirect := rdGetter(req); redirect != "" {
595− return redirect, nil
644+ user, ok := p.ManualSignIn(req)
645+ if ok {
646+ session := &sessionsapi.SessionState{User: user}
647+ err = p.SaveSession(rw, req, session)
648+ if err != nil {
649+ logger.Printf("Error saving session: %v", err)
650+ p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
651+ return
652+ }
653+ http.Redirect(rw, req, redirect, http.StatusFound)
654+ } else {
655+ if p.SkipProviderButton {
656+ p.OAuthStart(rw, req)
657+ } else {
658+ p.SignInPage(rw, req, http.StatusOK)
596659 }
597660 }
598−
599− return "/", nil
600661}
601662
602−func isForwardedRequest(req *http.Request) bool {
603− return requestutil.IsProxied(req) &&
604− req.Host != requestutil.GetRequestHost(req)
605−}
663+//UserInfo endpoint outputs session email and preferred username in JSON format
664+func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
606665
607−func (p *OAuthProxy) hasProxyPrefix(path string) bool {
608− return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
609−}
666+ session, err := p.getAuthenticatedSession(rw, req)
667+ if err != nil {
668+ http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
669+ return
670+ }
610671
611−// getRdQuerystringRedirect handles this GetAppRedirect strategy:
612−// - `rd` querysting parameter
613−func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
614− redirect := req.Form.Get("rd")
615− if p.IsValidRedirect(redirect) {
616− return redirect
672+ userInfo := struct {
673+ User string `json:"user"`
674+ Email string `json:"email"`
675+ Groups []string `json:"groups,omitempty"`
676+ PreferredUsername string `json:"preferredUsername,omitempty"`
677+ }{
678+ User: session.User,
679+ Email: session.Email,
680+ Groups: session.Groups,
681+ PreferredUsername: session.PreferredUsername,
617682 }
618− return ""
619−}
620683
621−// getXAuthRequestRedirect handles this GetAppRedirect strategy:
622−// - `X-Auth-Request-Redirect` Header
623−func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
624− redirect := req.Header.Get("X-Auth-Request-Redirect")
625− if p.IsValidRedirect(redirect) {
626− return redirect
684+ rw.Header().Set("Content-Type", "application/json")
685+ rw.WriteHeader(http.StatusOK)
686+ err = json.NewEncoder(rw).Encode(userInfo)
687+ if err != nil {
688+ logger.Printf("Error encoding user info: %v", err)
689+ p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
627690 }
628− return ""
629691}
630692
631−// getXForwardedHeadersRedirect handles these GetAppRedirect strategies:
632−// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
633−// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
634−func (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
635− if !isForwardedRequest(req) {
636− return ""
693+// SignOut sends a response to clear the authentication cookie
694+func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
695+ redirect, err := p.getAppRedirect(req)
696+ if err != nil {
697+ logger.Errorf("Error obtaining redirect: %v", err)
698+ p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
699+ return
637700 }
638−
639− uri := requestutil.GetRequestURI(req)
640− if p.hasProxyPrefix(uri) {
641− uri = "/"
701+ err = p.ClearSessionCookie(rw, req)
702+ if err != nil {
703+ logger.Errorf("Error clearing session cookie: %v", err)
704+ p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
705+ return
642706 }
643−
644− redirect := fmt.Sprintf(
645− "%s://%s%s",
646− requestutil.GetRequestProto(req),
647− requestutil.GetRequestHost(req),
648− uri,
649− )
650−
651− if p.IsValidRedirect(redirect) {
652− return redirect
653− }
654− return ""
655−}
656−
657−// getURIRedirect handles these GetAppRedirect strategies:
658−// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
659−// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
660−// - `/`
661−func (p *OAuthProxy) getURIRedirect(req *http.Request) string {
662− redirect := requestutil.GetRequestURI(req)
663− if !p.IsValidRedirect(redirect) {
664− redirect = req.URL.RequestURI()
665− }
666−
667− if p.hasProxyPrefix(redirect) {
668− return "/"
669− }
670− return redirect
671−}
672−
673−// splitHostPort separates host and port. If the port is not valid, it returns
674−// the entire input as host, and it doesn't check the validity of the host.
675−// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
676−// *** taken from net/url, modified validOptionalPort() to accept ":*"
677−func splitHostPort(hostport string) (host, port string) {
678− host = hostport
679−
680− colon := strings.LastIndexByte(host, ':')
681− if colon != -1 && validOptionalPort(host[colon:]) {
682− host, port = host[:colon], host[colon+1:]
683− }
684−
685− if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
686− host = host[1 : len(host)-1]
687− }
688−
689− return
690−}
691−
692−// validOptionalPort reports whether port is either an empty string
693−// or matches /^:\d*$/
694−// *** taken from net/url, modified to accept ":*"
695−func validOptionalPort(port string) bool {
696− if port == "" || port == ":*" {
697− return true
698− }
699− if port[0] != ':' {
700− return false
701− }
702− for _, b := range port[1:] {
703− if b < '0' || b > '9' {
704− return false
705− }
706− }
707− return true
708−}
709−
710−// IsValidRedirect checks whether the redirect URL is whitelisted
711−func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
712− switch {
713− case redirect == "":
714− // The user didn't specify a redirect, should fallback to `/`
715− return false
716− case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
717− return true
718− case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
719− redirectURL, err := url.Parse(redirect)
720− if err != nil {
721− logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
722− return false
723− }
724− redirectHostname := redirectURL.Hostname()
725−
726− for _, domain := range p.whitelistDomains {
727− domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
728− if domainHostname == "" {
729− continue
730− }
731−
732− if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
733− // the domain names match, now validate the ports
734− // if the whitelisted domain's port is '*', allow all ports
735− // if the whitelisted domain contains a specific port, only allow that port
736− // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
737− redirectPort := redirectURL.Port()
738− if (domainPort == "*") ||
739− (domainPort == redirectPort) ||
740− (domainPort == "" && redirectPort == "") {
741− return true
742− }
743− }
744− }
745−
746− logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
747− return false
748− default:
749− logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
750− return false
751− }
752−}
753−
754−// IsAllowedRequest is used to check if auth should be skipped for this request
755−func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
756− isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
757− return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
758−}
759−
760−// IsAllowedRoute is used to check if the request method & path is allowed without auth
761−func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
762− for _, route := range p.allowedRoutes {
763− if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
764− return true
765− }
766− }
767− return false
768−}
769−
770−// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
771−var noCacheHeaders = map[string]string{
772− "Expires": time.Unix(0, 0).Format(time.RFC1123),
773− "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
774− "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
775−}
776−
777−// prepareNoCache prepares headers for preventing browser caching.
778−func prepareNoCache(w http.ResponseWriter) {
779− // Set NoCache headers
780− for k, v := range noCacheHeaders {
781− w.Header().Set(k, v)
782− }
783−}
784−
785−// IsTrustedIP is used to check if a request comes from a trusted client IP address.
786−func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
787− if p.trustedIPs == nil {
788− return false
789− }
790−
791− remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
792− if err != nil {
793− logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
794− // Possibly spoofed X-Real-IP header
795− return false
796− }
797−
798− if remoteAddr == nil {
799− return false
800− }
801−
802− return p.trustedIPs.Has(remoteAddr)
803−}
804−
805−func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
806− p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
807−}
808−
809−func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
810− if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
811− prepareNoCache(rw)
812− }
813−
814− switch path := req.URL.Path; {
815− case path == p.RobotsPath:
816− p.RobotsTxt(rw)
817− case p.IsAllowedRequest(req):
818− p.SkipAuthProxy(rw, req)
819− case path == p.SignInPath:
820− p.SignIn(rw, req)
821− case path == p.SignOutPath:
822− p.SignOut(rw, req)
823− case path == p.OAuthStartPath:
824− p.OAuthStart(rw, req)
825− case path == p.OAuthCallbackPath:
826− p.OAuthCallback(rw, req)
827− case path == p.AuthOnlyPath:
828− p.AuthOnly(rw, req)
829− case path == p.UserInfoPath:
830− p.UserInfo(rw, req)
831− default:
832− p.Proxy(rw, req)
833− }
834−}
835−
836−// SignIn serves a page prompting users to sign in
837−func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
838− redirect, err := p.GetAppRedirect(req)
839− if err != nil {
840− logger.Errorf("Error obtaining redirect: %v", err)
841− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
842− return
843− }
844−
845− user, ok := p.ManualSignIn(req)
846− if ok {
847− session := &sessionsapi.SessionState{User: user}
848− err = p.SaveSession(rw, req, session)
849− if err != nil {
850− logger.Printf("Error saving session: %v", err)
851− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
852− return
853− }
854− http.Redirect(rw, req, redirect, http.StatusFound)
855− } else {
856− if p.SkipProviderButton {
857− p.OAuthStart(rw, req)
858− } else {
859− p.SignInPage(rw, req, http.StatusOK)
860− }
861− }
862−}
863−
864−//UserInfo endpoint outputs session email and preferred username in JSON format
865−func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
866−
867− session, err := p.getAuthenticatedSession(rw, req)
868− if err != nil {
869− http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
870− return
871− }
872−
873− userInfo := struct {
874− User string `json:"user"`
875− Email string `json:"email"`
876− Groups []string `json:"groups,omitempty"`
877− PreferredUsername string `json:"preferredUsername,omitempty"`
878− }{
879− User: session.User,
880− Email: session.Email,
881− Groups: session.Groups,
882− PreferredUsername: session.PreferredUsername,
883− }
884−
885− rw.Header().Set("Content-Type", "application/json")
886− rw.WriteHeader(http.StatusOK)
887− err = json.NewEncoder(rw).Encode(userInfo)
888− if err != nil {
889− logger.Printf("Error encoding user info: %v", err)
890− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
891− }
892−}
893−
894−// SignOut sends a response to clear the authentication cookie
895−func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
896− redirect, err := p.GetAppRedirect(req)
897− if err != nil {
898− logger.Errorf("Error obtaining redirect: %v", err)
899− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
900− return
901− }
902− err = p.ClearSessionCookie(rw, req)
903− if err != nil {
904− logger.Errorf("Error clearing session cookie: %v", err)
905− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
906− return
907− }
908− http.Redirect(rw, req, redirect, http.StatusFound)
909−}
707+ http.Redirect(rw, req, redirect, http.StatusFound)
708+}
910709
911710// OAuthStart starts the OAuth2 authentication flow
912711func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
@@ -918,13 +717,13 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
918717 return
919718 }
920719 p.SetCSRFCookie(rw, req, nonce)
921− redirect, err := p.GetAppRedirect(req)
720+ redirect, err := p.getAppRedirect(req)
922721 if err != nil {
923722 logger.Errorf("Error obtaining redirect: %v", err)
924723 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
925724 return
926725 }
927− redirectURI := p.GetOAuthRedirectURI(requestutil.GetRequestHost(req))
726+ redirectURI := p.getOAuthRedirectURI(req)
928727 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
929728}
930729
@@ -947,7 +746,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
947746 return
948747 }
949748
950− session, err := p.redeemCode(req.Context(), requestutil.GetRequestHost(req), req.Form.Get("code"))
749+ session, err := p.redeemCode(req)
951750 if err != nil {
952751 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
953752 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
@@ -1006,6 +805,32 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
1006805 }
1007806}
1008807
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+
1009834// AuthOnly checks whether the user is currently logged in (both authentication
1010835// and optional authorization).
1011836func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
@@ -1023,7 +848,7 @@ func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
1023848 }
1024849
1025850 // we are authenticated
1026− p.addHeadersForProxying(rw, req, session)
851+ p.addHeadersForProxying(rw, session)
1027852 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1028853 rw.WriteHeader(http.StatusAccepted)
1029854 })).ServeHTTP(rw, req)
@@ -1041,13 +866,13 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
1041866 switch err {
1042867 case nil:
1043868 // we are authenticated
1044− p.addHeadersForProxying(rw, req, session)
869+ p.addHeadersForProxying(rw, session)
1045870 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
1046871 case ErrNeedsLogin:
1047872 // we need to send the user to a login screen
1048873 if isAjax(req) {
1049874 // no point redirecting an AJAX request
1050− p.ErrorJSON(rw, http.StatusUnauthorized)
875+ p.errorJSON(rw, http.StatusUnauthorized)
1051876 return
1052877 }
1053878
@@ -1066,7 +891,184 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
1066891 p.ErrorPage(rw, http.StatusInternalServerError,
1067892 "Internal Error", "Internal Error")
1068893 }
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+}
902+
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+}
1069932
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+ if redirect := rdGetter(req); redirect != "" {
959+ return redirect, nil
960+ }
961+ }
962+
963+ return "/", nil
964+}
965+
966+func isForwardedRequest(req *http.Request) bool {
967+ return requestutil.IsProxied(req) &&
968+ req.Host != requestutil.GetRequestHost(req)
969+}
970+
971+func (p *OAuthProxy) hasProxyPrefix(path string) bool {
972+ return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
973+}
974+
975+// getRdQuerystringRedirect handles this getAppRedirect strategy:
976+// - `rd` querysting parameter
977+func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
978+ redirect := req.Form.Get("rd")
979+ if p.IsValidRedirect(redirect) {
980+ return redirect
981+ }
982+ return ""
983+}
984+
985+// getXAuthRequestRedirect handles this getAppRedirect strategy:
986+// - `X-Auth-Request-Redirect` Header
987+func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
988+ redirect := req.Header.Get("X-Auth-Request-Redirect")
989+ if p.IsValidRedirect(redirect) {
990+ return redirect
991+ }
992+ return ""
993+}
994+
995+// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
996+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
997+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
998+func (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
999+ if !isForwardedRequest(req) {
1000+ return ""
1001+ }
1002+
1003+ uri := requestutil.GetRequestURI(req)
1004+ if p.hasProxyPrefix(uri) {
1005+ uri = "/"
1006+ }
1007+
1008+ redirect := fmt.Sprintf(
1009+ "%s://%s%s",
1010+ requestutil.GetRequestProto(req),
1011+ requestutil.GetRequestHost(req),
1012+ uri,
1013+ )
1014+
1015+ if p.IsValidRedirect(redirect) {
1016+ return redirect
1017+ }
1018+ return ""
1019+}
1020+
1021+// getURIRedirect handles these getAppRedirect strategies:
1022+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
1023+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
1024+// - `/`
1025+func (p *OAuthProxy) getURIRedirect(req *http.Request) string {
1026+ redirect := requestutil.GetRequestURI(req)
1027+ if !p.IsValidRedirect(redirect) {
1028+ redirect = req.URL.RequestURI()
1029+ }
1030+
1031+ if p.hasProxyPrefix(redirect) {
1032+ return "/"
1033+ }
1034+ return redirect
1035+}
1036+
1037+// splitHostPort separates host and port. If the port is not valid, it returns
1038+// the entire input as host, and it doesn't check the validity of the host.
1039+// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
1040+// *** taken from net/url, modified validOptionalPort() to accept ":*"
1041+func splitHostPort(hostport string) (host, port string) {
1042+ host = hostport
1043+
1044+ colon := strings.LastIndexByte(host, ':')
1045+ if colon != -1 && validOptionalPort(host[colon:]) {
1046+ host, port = host[:colon], host[colon+1:]
1047+ }
1048+
1049+ if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
1050+ host = host[1 : len(host)-1]
1051+ }
1052+
1053+ return
1054+}
1055+
1056+// validOptionalPort reports whether port is either an empty string
1057+// or matches /^:\d*$/
1058+// *** taken from net/url, modified to accept ":*"
1059+func validOptionalPort(port string) bool {
1060+ if port == "" || port == ":*" {
1061+ return true
1062+ }
1063+ if port[0] != ':' {
1064+ return false
1065+ }
1066+ for _, b := range port[1:] {
1067+ if b < '0' || b > '9' {
1068+ return false
1069+ }
1070+ }
1071+ return true
10701072}
10711073
10721074// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
@@ -1153,7 +1155,7 @@ func extractAllowedGroups(req *http.Request) map[string]struct{} {
11531155}
11541156
11551157// addHeadersForProxying adds the appropriate headers the request / response for proxying
1156−func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1158+func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
11571159 if session.Email == "" {
11581160 rw.Header().Set("GAP-Auth", session.User)
11591161 } else {
@@ -1181,8 +1183,8 @@ func isAjax(req *http.Request) bool {
11811183 return false
11821184}
11831185
1184−// ErrorJSON returns the error code with an application/json mime type
1185−func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1186+// errorJSON returns the error code with an application/json mime type
1187+func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
11861188 rw.Header().Set("Content-Type", applicationJSON)
11871189 rw.WriteHeader(code)
11881190}
oauthproxy_test.go+5−4
@@ -415,8 +415,9 @@ func Test_redeemCode(t *testing.T) {
415415 t.Fatal(err)
416416 }
417417
418− _, err = proxy.redeemCode(context.Background(), "www.example.com", "")
419− assert.Error(t, err)
418+ req := httptest.NewRequest(http.MethodGet, "/", nil)
419+ _, err = proxy.redeemCode(req)
420+ assert.Equal(t, providers.ErrMissingCode, err)
420421}
421422
422423func Test_enrichSession(t *testing.T) {
@@ -1749,7 +1750,7 @@ func TestRequestSignature(t *testing.T) {
17491750 }
17501751}
17511752
1752−func TestGetRedirect(t *testing.T) {
1753+func Test_getAppRedirect(t *testing.T) {
17531754 opts := baseTestOptions()
17541755 opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com", ".example.com:8443")
17551756 err := validation.Validate(opts)
@@ -1900,7 +1901,7 @@ func TestGetRedirect(t *testing.T) {
19001901 req = middleware.AddRequestScope(req, &middleware.RequestScope{
19011902 ReverseProxy: tt.reverseProxy,
19021903 })
1903− redirect, err := proxy.GetAppRedirect(req)
1904+ redirect, err := proxy.getAppRedirect(req)
19041905
19051906 assert.NoError(t, err)
19061907 assert.Equal(t, tt.expectedRedirect, redirect)