@@ -5,7 +5,6 @@ import (
55 "encoding/json"
66 "errors"
77 "fmt"
8− "html/template"
98 "net"
109 "net/http"
1110 "net/url"
@@ -18,22 +17,21 @@ import (
1817 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
1918 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
2019 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
20+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
2121 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
2222 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
2323 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
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
@@ -77,38 +75,33 @@ type OAuthProxy struct {
7775 AuthOnlyPath string
7876 UserInfoPath string
7977
80− allowedRoutes []allowedRoute
81− redirectURL *url.URL // the url to receive requests at
82− whitelistDomains []string
83− provider providers.Provider
84− providerNameOverride string
85− sessionStore sessionsapi.SessionStore
86− ProxyPrefix string
87− SignInMessage string
88− basicAuthValidator basic.Validator
89− displayHtpasswdForm bool
90− serveMux http.Handler
91− SetXAuthRequest bool
92− PassBasicAuth bool
93− SetBasicAuth bool
94− SkipProviderButton bool
95− PassUserHeaders bool
96− BasicAuthPassword string
97− PassAccessToken bool
98− SetAuthorization bool
99− PassAuthorization bool
100− PreferEmailToUser bool
101− skipAuthPreflight bool
102− skipJwtBearerTokens bool
103− templates *template.Template
104− realClientIPParser ipapi.RealClientIPParser
105− trustedIPs *ip.NetSet
106− Banner string
107− Footer string
78+ allowedRoutes []allowedRoute
79+ redirectURL *url.URL // the url to receive requests at
80+ whitelistDomains []string
81+ provider providers.Provider
82+ sessionStore sessionsapi.SessionStore
83+ ProxyPrefix string
84+ basicAuthValidator basic.Validator
85+ serveMux http.Handler
86+ SetXAuthRequest bool
87+ PassBasicAuth bool
88+ SetBasicAuth bool
89+ SkipProviderButton bool
90+ PassUserHeaders bool
91+ BasicAuthPassword string
92+ PassAccessToken bool
93+ SetAuthorization bool
94+ PassAuthorization bool
95+ PreferEmailToUser bool
96+ skipAuthPreflight bool
97+ skipJwtBearerTokens bool
98+ realClientIPParser ipapi.RealClientIPParser
99+ trustedIPs *ip.NetSet
108100
109101 sessionChain alice.Chain
110102 headersChain alice.Chain
111103 preAuthChain alice.Chain
104+ pageWriter pagewriter.Writer
112105}
113106
114107// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
@@ -118,9 +111,31 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
118111 return nil, fmt.Errorf("error initialising session store: %v", err)
119112 }
120113
121− templates := loadTemplates(opts.CustomTemplatesDir)
122− proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
123− upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
114+ var basicAuthValidator basic.Validator
115+ if opts.HtpasswdFile != "" {
116+ logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
117+ var err error
118+ basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
119+ if err != nil {
120+ return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
121+ }
122+ }
123+
124+ pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
125+ TemplatesPath: opts.Templates.Path,
126+ ProxyPrefix: opts.ProxyPrefix,
127+ Footer: opts.Templates.Footer,
128+ Version: VERSION,
129+ Debug: opts.Templates.Debug,
130+ ProviderName: buildProviderName(opts.GetProvider(), opts.ProviderName),
131+ SignInMessage: buildSignInMessage(opts),
132+ DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
133+ })
134+ if err != nil {
135+ return nil, fmt.Errorf("error initialising page writer: %v", err)
136+ }
137+
138+ upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter.ProxyErrorHandler)
124139 if err != nil {
125140 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
126141 }
@@ -153,16 +168,6 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
153168 }
154169 }
155170
156− var basicAuthValidator basic.Validator
157− if opts.HtpasswdFile != "" {
158− logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
159− var err error
160− basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
161− if err != nil {
162− return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
163− }
164− }
165−
166171 allowedRoutes, err := buildRoutesAllowlist(opts)
167172 if err != nil {
168173 return nil, err
@@ -199,29 +204,24 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
199204 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
200205 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
201206
202− ProxyPrefix: opts.ProxyPrefix,
203− provider: opts.GetProvider(),
204− providerNameOverride: opts.ProviderName,
205− sessionStore: sessionStore,
206− serveMux: upstreamProxy,
207− redirectURL: redirectURL,
208− allowedRoutes: allowedRoutes,
209− whitelistDomains: opts.WhitelistDomains,
210− skipAuthPreflight: opts.SkipAuthPreflight,
211− skipJwtBearerTokens: opts.SkipJwtBearerTokens,
212− realClientIPParser: opts.GetRealClientIPParser(),
213− SkipProviderButton: opts.SkipProviderButton,
214− templates: templates,
215− trustedIPs: trustedIPs,
216− Banner: opts.Banner,
217− Footer: opts.Footer,
218− SignInMessage: buildSignInMessage(opts),
219−
220− basicAuthValidator: basicAuthValidator,
221− displayHtpasswdForm: basicAuthValidator != nil && opts.DisplayHtpasswdForm,
222− sessionChain: sessionChain,
223− headersChain: headersChain,
224− preAuthChain: preAuthChain,
207+ ProxyPrefix: opts.ProxyPrefix,
208+ provider: opts.GetProvider(),
209+ sessionStore: sessionStore,
210+ serveMux: upstreamProxy,
211+ redirectURL: redirectURL,
212+ allowedRoutes: allowedRoutes,
213+ whitelistDomains: opts.WhitelistDomains,
214+ skipAuthPreflight: opts.SkipAuthPreflight,
215+ skipJwtBearerTokens: opts.SkipJwtBearerTokens,
216+ realClientIPParser: opts.GetRealClientIPParser(),
217+ SkipProviderButton: opts.SkipProviderButton,
218+ trustedIPs: trustedIPs,
219+
220+ basicAuthValidator: basicAuthValidator,
221+ sessionChain: sessionChain,
222+ headersChain: headersChain,
223+ preAuthChain: preAuthChain,
224+ pageWriter: pageWriter,
225225 }, nil
226226}
227227
@@ -229,7 +229,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
229229// the OAuth2 Proxy authentication logic kicks in.
230230// For example forcing HTTPS or health checks.
231231func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
232− chain := alice.New(middleware.NewScope())
232+ chain := alice.New(middleware.NewScope(opts.ReverseProxy))
233233
234234 if opts.ForceHTTPS {
235235 _, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress)
@@ -254,6 +254,8 @@ func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
254254 chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents))
255255 }
256256
257+ chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
258+
257259 return chain, nil
258260}
259261
@@ -303,11 +305,11 @@ func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
303305
304306func buildSignInMessage(opts *options.Options) string {
305307 var msg string
306− if len(opts.Banner) >= 1 {
307− if opts.Banner == "-" {
308+ if len(opts.Templates.Banner) >= 1 {
309+ if opts.Templates.Banner == "-" {
308310 msg = ""
309311 } else {
310− msg = opts.Banner
312+ msg = opts.Templates.Banner
311313 }
312314 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
313315 if len(opts.EmailDomains) > 1 {
@@ -319,6 +321,13 @@ func buildSignInMessage(opts *options.Options) string {
319321 return msg
320322}
321323
324+func buildProviderName(p providers.Provider, override string) string {
325+ if override != "" {
326+ return override
327+ }
328+ return p.Data().ProviderName
329+}
330+
322331// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
323332// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
324333// (method=path support)
@@ -366,49 +375,6 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
366375 return routes, nil
367376}
368377
369−// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
370−// redirect clients to once authenticated
371−func (p *OAuthProxy) GetRedirectURI(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.GetRedirectURI(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−
412378// MakeCSRFCookie creates a cookie for CSRF
413379func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
414380 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
@@ -418,7 +384,7 @@ func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, ex
418384 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
419385
420386 if cookieDomain != "" {
421− domain := util.GetRequestHost(req)
387+ domain := requestutil.GetRequestHost(req)
422388 if h, _, err := net.SplitHostPort(domain); err == nil {
423389 domain = h
424390 }
@@ -466,166 +432,6 @@ func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *s
466432 return p.sessionStore.Save(rw, req, s)
467433}
468434
469−// RobotsTxt disallows scraping pages from the OAuthProxy
470−func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
471− _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
472− if err != nil {
473− logger.Printf("Error writing robots.txt: %v", err)
474− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
475− return
476− }
477− rw.WriteHeader(http.StatusOK)
478−}
479−
480−// ErrorPage writes an error response
481−func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
482− rw.WriteHeader(code)
483− t := struct {
484− Title string
485− Message string
486− ProxyPrefix string
487− }{
488− Title: fmt.Sprintf("%d %s", code, title),
489− Message: message,
490− ProxyPrefix: p.ProxyPrefix,
491− }
492− err := p.templates.ExecuteTemplate(rw, "error.html", t)
493− if err != nil {
494− logger.Printf("Error rendering error.html template: %v", err)
495− http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
496− }
497−}
498−
499−// SignInPage writes the sing in template to the response
500−func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
501− prepareNoCache(rw)
502− err := p.ClearSessionCookie(rw, req)
503− if err != nil {
504− logger.Printf("Error clearing session cookie: %v", err)
505− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
506− return
507− }
508− rw.WriteHeader(code)
509−
510− redirectURL, err := p.GetRedirect(req)
511− if err != nil {
512− logger.Errorf("Error obtaining redirect: %v", err)
513− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
514− return
515− }
516−
517− if redirectURL == p.SignInPath {
518− redirectURL = "/"
519− }
520−
521− // We allow unescaped template.HTML since it is user configured options
522− /* #nosec G203 */
523− t := struct {
524− ProviderName string
525− SignInMessage template.HTML
526− CustomLogin bool
527− Redirect string
528− Version string
529− ProxyPrefix string
530− Footer template.HTML
531− }{
532− ProviderName: p.provider.Data().ProviderName,
533− SignInMessage: template.HTML(p.SignInMessage),
534− CustomLogin: p.displayHtpasswdForm,
535− Redirect: redirectURL,
536− Version: VERSION,
537− ProxyPrefix: p.ProxyPrefix,
538− Footer: template.HTML(p.Footer),
539− }
540− if p.providerNameOverride != "" {
541− t.ProviderName = p.providerNameOverride
542− }
543− err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
544− if err != nil {
545− logger.Printf("Error rendering sign_in.html template: %v", err)
546− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
547− }
548−}
549−
550−// ManualSignIn handles basic auth logins to the proxy
551−func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
552− if req.Method != "POST" || p.basicAuthValidator == nil {
553− return "", false
554− }
555− user := req.FormValue("username")
556− passwd := req.FormValue("password")
557− if user == "" {
558− return "", false
559− }
560− // check auth
561− if p.basicAuthValidator.Validate(user, passwd) {
562− logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
563− return user, true
564− }
565− logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
566− return "", false
567−}
568−
569−// GetRedirect reads the query parameter to get the URL to redirect clients to
570−// once authenticated with the OAuthProxy
571−func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
572− err = req.ParseForm()
573− if err != nil {
574− return
575− }
576−
577− redirect = req.Header.Get("X-Auth-Request-Redirect")
578− if req.Form.Get("rd") != "" {
579− redirect = req.Form.Get("rd")
580− }
581− if !p.IsValidRedirect(redirect) {
582− // Use RequestURI to preserve ?query
583− redirect = req.URL.RequestURI()
584− if strings.HasPrefix(redirect, p.ProxyPrefix) {
585− redirect = "/"
586− }
587− }
588−
589− return
590−}
591−
592−// splitHostPort separates host and port. If the port is not valid, it returns
593−// the entire input as host, and it doesn't check the validity of the host.
594−// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
595−// *** taken from net/url, modified validOptionalPort() to accept ":*"
596−func splitHostPort(hostport string) (host, port string) {
597− host = hostport
598−
599− colon := strings.LastIndexByte(host, ':')
600− if colon != -1 && validOptionalPort(host[colon:]) {
601− host, port = host[:colon], host[colon+1:]
602− }
603−
604− if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
605− host = host[1 : len(host)-1]
606− }
607−
608− return
609−}
610−
611−// validOptionalPort reports whether port is either an empty string
612−// or matches /^:\d*$/
613−// *** taken from net/url, modified to accept ":*"
614−func validOptionalPort(port string) bool {
615− if port == "" || port == ":*" {
616− return true
617− }
618− if port[0] != ':' {
619− return false
620− }
621− for _, b := range port[1:] {
622− if b < '0' || b > '9' {
623− return false
624− }
625− }
626− return true
627−}
628−
629435// IsValidRedirect checks whether the redirect URL is whitelisted
630436func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
631437 switch {
@@ -642,21 +448,23 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
642448 }
643449 redirectHostname := redirectURL.Hostname()
644450
645− for _, domain := range p.whitelistDomains {
646− domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
647− if domainHostname == "" {
451+ for _, allowedDomain := range p.whitelistDomains {
452+ allowedHost, allowedPort := splitHostPort(allowedDomain)
453+ if allowedHost == "" {
648454 continue
649455 }
650456
651− if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
457+ if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
458+ (strings.HasPrefix(allowedHost, ".") &&
459+ strings.HasSuffix(redirectHostname, allowedHost)) {
652460 // the domain names match, now validate the ports
653461 // if the whitelisted domain's port is '*', allow all ports
654462 // if the whitelisted domain contains a specific port, only allow that port
655463 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
656464 redirectPort := redirectURL.Port()
657− if (domainPort == "*") ||
658− (domainPort == redirectPort) ||
659− (domainPort == "" && redirectPort == "") {
465+ if allowedPort == "*" ||
466+ allowedPort == redirectPort ||
467+ (allowedPort == "" && redirectPort == "") {
660468 return true
661469 }
662470 }
@@ -670,10 +478,65 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
670478 }
671479}
672480
481+func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
482+ p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
483+}
484+
485+func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
486+ if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
487+ prepareNoCache(rw)
488+ }
489+
490+ switch path := req.URL.Path; {
491+ case path == p.RobotsPath:
492+ p.RobotsTxt(rw, req)
493+ case p.IsAllowedRequest(req):
494+ p.SkipAuthProxy(rw, req)
495+ case path == p.SignInPath:
496+ p.SignIn(rw, req)
497+ case path == p.SignOutPath:
498+ p.SignOut(rw, req)
499+ case path == p.OAuthStartPath:
500+ p.OAuthStart(rw, req)
501+ case path == p.OAuthCallbackPath:
502+ p.OAuthCallback(rw, req)
503+ case path == p.AuthOnlyPath:
504+ p.AuthOnly(rw, req)
505+ case path == p.UserInfoPath:
506+ p.UserInfo(rw, req)
507+ default:
508+ p.Proxy(rw, req)
509+ }
510+}
511+
512+// RobotsTxt disallows scraping pages from the OAuthProxy
513+func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter, req *http.Request) {
514+ _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
515+ if err != nil {
516+ logger.Printf("Error writing robots.txt: %v", err)
517+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
518+ return
519+ }
520+ rw.WriteHeader(http.StatusOK)
521+}
522+
523+// ErrorPage writes an error response
524+func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
525+ redirectURL, err := p.getAppRedirect(req)
526+ if err != nil {
527+ logger.Errorf("Error obtaining redirect: %v", err)
528+ }
529+ if redirectURL == p.SignInPath || redirectURL == "" {
530+ redirectURL = "/"
531+ }
532+
533+ p.pageWriter.WriteErrorPage(rw, code, redirectURL, appError, messages...)
534+}
535+
673536// IsAllowedRequest is used to check if auth should be skipped for this request
674537func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
675538 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
676− return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
539+ return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
677540}
678541
679542// IsAllowedRoute is used to check if the request method & path is allowed without auth
@@ -686,23 +549,8 @@ func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
686549 return false
687550}
688551
689−// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
690−var noCacheHeaders = map[string]string{
691− "Expires": time.Unix(0, 0).Format(time.RFC1123),
692− "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
693− "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
694−}
695−
696−// prepareNoCache prepares headers for preventing browser caching.
697−func prepareNoCache(w http.ResponseWriter) {
698− // Set NoCache headers
699− for k, v := range noCacheHeaders {
700− w.Header().Set(k, v)
701− }
702−}
703−
704−// IsTrustedIP is used to check if a request comes from a trusted client IP address.
705−func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
552+// isTrustedIP is used to check if a request comes from a trusted client IP address.
553+func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
706554 if p.trustedIPs == nil {
707555 return false
708556 }
@@ -721,43 +569,56 @@ func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
721569 return p.trustedIPs.Has(remoteAddr)
722570}
723571
724−func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
725− p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
726−}
572+// SignInPage writes the sing in template to the response
573+func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
574+ prepareNoCache(rw)
575+ err := p.ClearSessionCookie(rw, req)
576+ if err != nil {
577+ logger.Printf("Error clearing session cookie: %v", err)
578+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
579+ return
580+ }
581+ rw.WriteHeader(code)
727582
728−func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
729− if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
730− prepareNoCache(rw)
583+ redirectURL, err := p.getAppRedirect(req)
584+ if err != nil {
585+ logger.Errorf("Error obtaining redirect: %v", err)
586+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
587+ return
731588 }
732589
733− switch path := req.URL.Path; {
734− case path == p.RobotsPath:
735− p.RobotsTxt(rw)
736− case p.IsAllowedRequest(req):
737− p.SkipAuthProxy(rw, req)
738− case path == p.SignInPath:
739− p.SignIn(rw, req)
740− case path == p.SignOutPath:
741− p.SignOut(rw, req)
742− case path == p.OAuthStartPath:
743− p.OAuthStart(rw, req)
744− case path == p.OAuthCallbackPath:
745− p.OAuthCallback(rw, req)
746− case path == p.AuthOnlyPath:
747− p.AuthenticateOnly(rw, req)
748− case path == p.UserInfoPath:
749− p.UserInfo(rw, req)
750− default:
751− p.Proxy(rw, req)
590+ if redirectURL == p.SignInPath {
591+ redirectURL = "/"
592+ }
593+
594+ p.pageWriter.WriteSignInPage(rw, redirectURL)
595+}
596+
597+// ManualSignIn handles basic auth logins to the proxy
598+func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
599+ if req.Method != "POST" || p.basicAuthValidator == nil {
600+ return "", false
601+ }
602+ user := req.FormValue("username")
603+ passwd := req.FormValue("password")
604+ if user == "" {
605+ return "", false
752606 }
607+ // check auth
608+ if p.basicAuthValidator.Validate(user, passwd) {
609+ logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
610+ return user, true
611+ }
612+ logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
613+ return "", false
753614}
754615
755616// SignIn serves a page prompting users to sign in
756617func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
757− redirect, err := p.GetRedirect(req)
618+ redirect, err := p.getAppRedirect(req)
758619 if err != nil {
759620 logger.Errorf("Error obtaining redirect: %v", err)
760− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
621+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
761622 return
762623 }
763624
@@ -767,7 +628,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
767628 err = p.SaveSession(rw, req, session)
768629 if err != nil {
769630 logger.Printf("Error saving session: %v", err)
770− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
631+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
771632 return
772633 }
773634 http.Redirect(rw, req, redirect, http.StatusFound)
@@ -780,7 +641,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
780641 }
781642}
782643
783−//UserInfo endpoint outputs session email and preferred username in JSON format
644+// UserInfo endpoint outputs session email and preferred username in JSON format
784645func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
785646
786647 session, err := p.getAuthenticatedSession(rw, req)
@@ -806,22 +667,22 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
806667 err = json.NewEncoder(rw).Encode(userInfo)
807668 if err != nil {
808669 logger.Printf("Error encoding user info: %v", err)
809− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
670+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
810671 }
811672}
812673
813674// SignOut sends a response to clear the authentication cookie
814675func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
815− redirect, err := p.GetRedirect(req)
676+ redirect, err := p.getAppRedirect(req)
816677 if err != nil {
817678 logger.Errorf("Error obtaining redirect: %v", err)
818− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
679+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
819680 return
820681 }
821682 err = p.ClearSessionCookie(rw, req)
822683 if err != nil {
823684 logger.Errorf("Error clearing session cookie: %v", err)
824− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
685+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
825686 return
826687 }
827688 http.Redirect(rw, req, redirect, http.StatusFound)
@@ -833,17 +694,17 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
833694 nonce, err := encryption.Nonce()
834695 if err != nil {
835696 logger.Errorf("Error obtaining nonce: %v", err)
836− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
697+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
837698 return
838699 }
839700 p.SetCSRFCookie(rw, req, nonce)
840− redirect, err := p.GetRedirect(req)
701+ redirect, err := p.getAppRedirect(req)
841702 if err != nil {
842703 logger.Errorf("Error obtaining redirect: %v", err)
843− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
704+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
844705 return
845706 }
846− redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
707+ redirectURI := p.getOAuthRedirectURI(req)
847708 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
848709}
849710
@@ -856,34 +717,36 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
856717 err := req.ParseForm()
857718 if err != nil {
858719 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
859− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
720+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
860721 return
861722 }
862723 errorString := req.Form.Get("error")
863724 if errorString != "" {
864725 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
865− p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
726+ message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
727+ // Set the debug message and override the non debug message to be the same for this case
728+ p.ErrorPage(rw, req, http.StatusForbidden, message, message)
866729 return
867730 }
868731
869− session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
732+ session, err := p.redeemCode(req)
870733 if err != nil {
871734 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
872− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
735+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
873736 return
874737 }
875738
876739 err = p.enrichSessionState(req.Context(), session)
877740 if err != nil {
878741 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
879− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
742+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
880743 return
881744 }
882745
883746 state := strings.SplitN(req.Form.Get("state"), ":", 2)
884747 if len(state) != 2 {
885748 logger.Error("Error while parsing OAuth2 state: invalid length")
886− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
749+ p.ErrorPage(rw, req, http.StatusInternalServerError, "State paremeter did not have expected length", "Login Failed: Invalid State after login.")
887750 return
888751 }
889752 nonce := state[0]
@@ -891,13 +754,13 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
891754 c, err := req.Cookie(p.CSRFCookieName)
892755 if err != nil {
893756 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
894− p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
757+ p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
895758 return
896759 }
897760 p.ClearCSRFCookie(rw, req)
898761 if c.Value != nonce {
899762 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
900− p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
763+ p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
901764 return
902765 }
903766
@@ -915,26 +778,62 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
915778 err := p.SaveSession(rw, req, session)
916779 if err != nil {
917780 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
918− p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
781+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
919782 return
920783 }
921784 http.Redirect(rw, req, redirect, http.StatusFound)
922785 } else {
923786 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
924− p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
787+ p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
788+ }
789+}
790+
791+func (p *OAuthProxy) redeemCode(req *http.Request) (*sessionsapi.SessionState, error) {
792+ code := req.Form.Get("code")
793+ if code == "" {
794+ return nil, providers.ErrMissingCode
795+ }
796+
797+ redirectURI := p.getOAuthRedirectURI(req)
798+ s, err := p.provider.Redeem(req.Context(), redirectURI, code)
799+ if err != nil {
800+ return nil, err
925801 }
802+ return s, nil
803+}
804+
805+func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
806+ var err error
807+ if s.Email == "" {
808+ // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
809+ // nolint:staticcheck
810+ s.Email, err = p.provider.GetEmailAddress(ctx, s)
811+ if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
812+ return err
813+ }
814+ }
815+
816+ return p.provider.EnrichSession(ctx, s)
926817}
927818
928−// AuthenticateOnly checks whether the user is currently logged in
929−func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
819+// AuthOnly checks whether the user is currently logged in (both authentication
820+// and optional authorization).
821+func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
930822 session, err := p.getAuthenticatedSession(rw, req)
931823 if err != nil {
932− http.Error(rw, "unauthorized request", http.StatusUnauthorized)
824+ http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
825+ return
826+ }
827+
828+ // Unauthorized cases need to return 403 to prevent infinite redirects with
829+ // subrequest architectures
830+ if !authOnlyAuthorize(req, session) {
831+ http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
933832 return
934833 }
935834
936835 // we are authenticated
937− p.addHeadersForProxying(rw, req, session)
836+ p.addHeadersForProxying(rw, session)
938837 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
939838 rw.WriteHeader(http.StatusAccepted)
940839 })).ServeHTTP(rw, req)
@@ -952,13 +851,13 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
952851 switch err {
953852 case nil:
954853 // we are authenticated
955− p.addHeadersForProxying(rw, req, session)
854+ p.addHeadersForProxying(rw, session)
956855 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
957856 case ErrNeedsLogin:
958857 // we need to send the user to a login screen
959858 if isAjax(req) {
960859 // no point redirecting an AJAX request
961− p.ErrorJSON(rw, http.StatusUnauthorized)
860+ p.errorJSON(rw, http.StatusUnauthorized)
962861 return
963862 }
964863
@@ -969,15 +868,202 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
969868 }
970869
971870 case ErrAccessDenied:
972− p.ErrorPage(rw, http.StatusUnauthorized, "Permission Denied", "Unauthorized")
871+ p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
973872
974873 default:
975874 // unknown error
976875 logger.Errorf("Unexpected internal error: %v", err)
977− p.ErrorPage(rw, http.StatusInternalServerError,
978− "Internal Error", "Internal Error")
876+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
979877 }
878+}
980879
880+// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
881+var noCacheHeaders = map[string]string{
882+ "Expires": time.Unix(0, 0).Format(time.RFC1123),
883+ "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
884+ "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
885+}
886+
887+// prepareNoCache prepares headers for preventing browser caching.
888+func prepareNoCache(w http.ResponseWriter) {
889+ // Set NoCache headers
890+ for k, v := range noCacheHeaders {
891+ w.Header().Set(k, v)
892+ }
893+}
894+
895+// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
896+// redirect clients to once authenticated.
897+// This is usually the OAuthProxy callback URL.
898+func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
899+ // if `p.redirectURL` already has a host, return it
900+ if p.redirectURL.Host != "" {
901+ return p.redirectURL.String()
902+ }
903+
904+ // Otherwise figure out the scheme + host from the request
905+ rd := *p.redirectURL
906+ rd.Host = requestutil.GetRequestHost(req)
907+ rd.Scheme = requestutil.GetRequestProto(req)
908+
909+ // If CookieSecure is true, return `https` no matter what
910+ // Not all reverse proxies set X-Forwarded-Proto
911+ if p.CookieSecure {
912+ rd.Scheme = schemeHTTPS
913+ }
914+ return rd.String()
915+}
916+
917+// getAppRedirect determines the full URL or URI path to redirect clients to
918+// once authenticated with the OAuthProxy
919+// Strategy priority (first legal result is used):
920+// - `rd` querysting parameter
921+// - `X-Auth-Request-Redirect` header
922+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
923+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
924+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
925+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
926+// - `/`
927+func (p *OAuthProxy) getAppRedirect(req *http.Request) (string, error) {
928+ err := req.ParseForm()
929+ if err != nil {
930+ return "", err
931+ }
932+
933+ // These redirect getter functions are strategies ordered by priority
934+ // for figuring out the redirect URL.
935+ type redirectGetter func(req *http.Request) string
936+ for _, rdGetter := range []redirectGetter{
937+ p.getRdQuerystringRedirect,
938+ p.getXAuthRequestRedirect,
939+ p.getXForwardedHeadersRedirect,
940+ p.getURIRedirect,
941+ } {
942+ redirect := rdGetter(req)
943+ // Call `p.IsValidRedirect` again here a final time to be safe
944+ if redirect != "" && p.IsValidRedirect(redirect) {
945+ return redirect, nil
946+ }
947+ }
948+
949+ return "/", nil
950+}
951+
952+func isForwardedRequest(req *http.Request) bool {
953+ return requestutil.IsProxied(req) &&
954+ req.Host != requestutil.GetRequestHost(req)
955+}
956+
957+func (p *OAuthProxy) hasProxyPrefix(path string) bool {
958+ return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
959+}
960+
961+func (p *OAuthProxy) validateRedirect(redirect string, errorFormat string) string {
962+ if p.IsValidRedirect(redirect) {
963+ return redirect
964+ }
965+ if redirect != "" {
966+ logger.Errorf(errorFormat, redirect)
967+ }
968+ return ""
969+}
970+
971+// getRdQuerystringRedirect handles this getAppRedirect strategy:
972+// - `rd` querysting parameter
973+func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
974+ return p.validateRedirect(
975+ req.Form.Get("rd"),
976+ "Invalid redirect provided in rd querystring parameter: %s",
977+ )
978+}
979+
980+// getXAuthRequestRedirect handles this getAppRedirect strategy:
981+// - `X-Auth-Request-Redirect` Header
982+func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
983+ return p.validateRedirect(
984+ req.Header.Get("X-Auth-Request-Redirect"),
985+ "Invalid redirect provided in X-Auth-Request-Redirect header: %s",
986+ )
987+}
988+
989+// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
990+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
991+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
992+func (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
993+ if !isForwardedRequest(req) {
994+ return ""
995+ }
996+
997+ uri := requestutil.GetRequestURI(req)
998+ if p.hasProxyPrefix(uri) {
999+ uri = "/"
1000+ }
1001+
1002+ redirect := fmt.Sprintf(
1003+ "%s://%s%s",
1004+ requestutil.GetRequestProto(req),
1005+ requestutil.GetRequestHost(req),
1006+ uri,
1007+ )
1008+
1009+ return p.validateRedirect(redirect,
1010+ "Invalid redirect generated from X-Forwarded-* headers: %s")
1011+}
1012+
1013+// getURIRedirect handles these getAppRedirect strategies:
1014+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
1015+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
1016+// - `/`
1017+func (p *OAuthProxy) getURIRedirect(req *http.Request) string {
1018+ redirect := p.validateRedirect(
1019+ requestutil.GetRequestURI(req),
1020+ "Invalid redirect generated from X-Forwarded-Uri header: %s",
1021+ )
1022+ if redirect == "" {
1023+ redirect = req.URL.RequestURI()
1024+ }
1025+
1026+ if p.hasProxyPrefix(redirect) {
1027+ return "/"
1028+ }
1029+ return redirect
1030+}
1031+
1032+// splitHostPort separates host and port. If the port is not valid, it returns
1033+// the entire input as host, and it doesn't check the validity of the host.
1034+// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
1035+// *** taken from net/url, modified validOptionalPort() to accept ":*"
1036+func splitHostPort(hostport string) (host, port string) {
1037+ host = hostport
1038+
1039+ colon := strings.LastIndexByte(host, ':')
1040+ if colon != -1 && validOptionalPort(host[colon:]) {
1041+ host, port = host[:colon], host[colon+1:]
1042+ }
1043+
1044+ if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
1045+ host = host[1 : len(host)-1]
1046+ }
1047+
1048+ return
1049+}
1050+
1051+// validOptionalPort reports whether port is either an empty string
1052+// or matches /^:\d*$/
1053+// *** taken from net/url, modified to accept ":*"
1054+func validOptionalPort(port string) bool {
1055+ if port == "" || port == ":*" {
1056+ return true
1057+ }
1058+ if port[0] != ':' {
1059+ return false
1060+ }
1061+ for _, b := range port[1:] {
1062+ if b < '0' || b > '9' {
1063+ return false
1064+ }
1065+ }
1066+ return true
9811067}
9821068
9831069// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
@@ -989,7 +1075,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
9891075 var session *sessionsapi.SessionState
9901076
9911077 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
992− session = middleware.GetRequestScope(req).Session
1078+ session = middlewareapi.GetRequestScope(req).Session
9931079 }))
9941080 getSession.ServeHTTP(rw, req)
9951081
@@ -1016,8 +1102,55 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
10161102 return session, nil
10171103}
10181104
1105+// authOnlyAuthorize handles special authorization logic that is only done
1106+// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1107+//
1108+// TODO (@NickMeves): This method is a placeholder to be extended but currently
1109+// fails the linter. Remove the nolint when functionality expands.
1110+//
1111+//nolint:gosimple
1112+func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1113+ // Allow secondary group restrictions based on the `allowed_groups`
1114+ // querystring parameter
1115+ if !checkAllowedGroups(req, s) {
1116+ return false
1117+ }
1118+
1119+ return true
1120+}
1121+
1122+func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1123+ allowedGroups := extractAllowedGroups(req)
1124+ if len(allowedGroups) == 0 {
1125+ return true
1126+ }
1127+
1128+ for _, group := range s.Groups {
1129+ if _, ok := allowedGroups[group]; ok {
1130+ return true
1131+ }
1132+ }
1133+
1134+ return false
1135+}
1136+
1137+func extractAllowedGroups(req *http.Request) map[string]struct{} {
1138+ groups := map[string]struct{}{}
1139+
1140+ query := req.URL.Query()
1141+ for _, allowedGroups := range query["allowed_groups"] {
1142+ for _, group := range strings.Split(allowedGroups, ",") {
1143+ if group != "" {
1144+ groups[group] = struct{}{}
1145+ }
1146+ }
1147+ }
1148+
1149+ return groups
1150+}
1151+
10191152// addHeadersForProxying adds the appropriate headers the request / response for proxying
1020−func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1153+func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
10211154 if session.Email == "" {
10221155 rw.Header().Set("GAP-Auth", session.User)
10231156 } else {
@@ -1029,16 +1162,24 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
10291162func isAjax(req *http.Request) bool {
10301163 acceptValues := req.Header.Values("Accept")
10311164 const ajaxReq = applicationJSON
1032− for _, v := range acceptValues {
1033− if v == ajaxReq {
1034− return true
1165+ // Iterate over multiple Accept headers, i.e.
1166+ // Accept: application/json
1167+ // Accept: text/plain
1168+ for _, mimeTypes := range acceptValues {
1169+ // Iterate over multiple mimetypes in a single header, i.e.
1170+ // Accept: application/json, text/plain, */*
1171+ for _, mimeType := range strings.Split(mimeTypes, ",") {
1172+ mimeType = strings.TrimSpace(mimeType)
1173+ if mimeType == ajaxReq {
1174+ return true
1175+ }
10351176 }
10361177 }
10371178 return false
10381179}
10391180
1040−// ErrorJSON returns the error code with an application/json mime type
1041−func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1181+// errorJSON returns the error code with an application/json mime type
1182+func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
10421183 rw.Header().Set("Content-Type", applicationJSON)
10431184 rw.WriteHeader(code)
10441185}