git.iko.soy/typeable/oauth2-proxy
on 597ffeb help sign in
typeable/oauth2-proxy/oauthproxy.go
1126 lines 37 kB history raw
1package main
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "html/template"
9 "net"
10 "net/http"
11 "net/url"
12 "regexp"
13 "strings"
14 "time"
16 "github.com/justinas/alice"
17 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
18 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
20 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
22 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
30 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
33const (
34 httpScheme = "http"
35 httpsScheme = "https"
37 applicationJSON = "application/json"
40var (
41 // ErrNeedsLogin means the user should be redirected to the login page
42 ErrNeedsLogin = errors.New("redirect to login page")
44 // ErrAccessDenied means the user should receive a 401 Unauthorized response
45 ErrAccessDenied = errors.New("access denied")
47 // Used to check final redirects are not susceptible to open redirects.
48 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
49 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
52// allowedRoute manages method + path based allowlists
53type allowedRoute struct {
54 method string
55 pathRegex *regexp.Regexp
58// OAuthProxy is the main authentication proxy
59type OAuthProxy struct {
60 CookieSeed string
61 CookieName string
62 CSRFCookieName string
63 CookieDomains []string
64 CookiePath string
65 CookieSecure bool
66 CookieHTTPOnly bool
67 CookieExpire time.Duration
68 CookieRefresh time.Duration
69 CookieSameSite string
70 Validator func(string) bool
72 RobotsPath string
73 SignInPath string
74 SignOutPath string
75 OAuthStartPath string
76 OAuthCallbackPath string
77 AuthOnlyPath string
78 UserInfoPath string
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 ReverseProxy bool
102 skipAuthPreflight bool
103 skipJwtBearerTokens bool
104 templates *template.Template
105 realClientIPParser ipapi.RealClientIPParser
106 trustedIPs *ip.NetSet
107 Banner string
108 Footer string
110 sessionChain alice.Chain
111 headersChain alice.Chain
112 preAuthChain alice.Chain
115// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
116func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
117 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
118 if err != nil {
119 return nil, fmt.Errorf("error initialising session store: %v", err)
120 }
122 templates := loadTemplates(opts.CustomTemplatesDir)
123 proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
124 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
125 if err != nil {
126 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
127 }
129 if opts.SkipJwtBearerTokens {
130 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
131 for _, issuer := range opts.ExtraJwtIssuers {
132 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
133 }
134 }
135 redirectURL := opts.GetRedirectURL()
136 if redirectURL.Path == "" {
137 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
138 }
140 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
141 refresh := "disabled"
142 if opts.Cookie.Refresh != time.Duration(0) {
143 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
144 }
146 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh)
148 trustedIPs := ip.NewNetSet()
149 for _, ipStr := range opts.TrustedIPs {
150 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
151 trustedIPs.AddIPNet(*ipNet)
152 } else {
153 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
154 }
155 }
157 var basicAuthValidator basic.Validator
158 if opts.HtpasswdFile != "" {
159 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
160 var err error
161 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
162 if err != nil {
163 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
164 }
165 }
167 allowedRoutes, err := buildRoutesAllowlist(opts)
168 if err != nil {
169 return nil, err
170 }
172 preAuthChain, err := buildPreAuthChain(opts)
173 if err != nil {
174 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
175 }
176 sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
177 headersChain, err := buildHeadersChain(opts)
178 if err != nil {
179 return nil, fmt.Errorf("could not build headers chain: %v", err)
180 }
182 return &OAuthProxy{
183 CookieName: opts.Cookie.Name,
184 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
185 CookieSeed: opts.Cookie.Secret,
186 CookieDomains: opts.Cookie.Domains,
187 CookiePath: opts.Cookie.Path,
188 CookieSecure: opts.Cookie.Secure,
189 CookieHTTPOnly: opts.Cookie.HTTPOnly,
190 CookieExpire: opts.Cookie.Expire,
191 CookieRefresh: opts.Cookie.Refresh,
192 CookieSameSite: opts.Cookie.SameSite,
193 Validator: validator,
195 RobotsPath: "/robots.txt",
196 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
197 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
198 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
199 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
200 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
201 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
203 ProxyPrefix: opts.ProxyPrefix,
204 ReverseProxy: opts.ReverseProxy,
205 provider: opts.GetProvider(),
206 providerNameOverride: opts.ProviderName,
207 sessionStore: sessionStore,
208 serveMux: upstreamProxy,
209 redirectURL: redirectURL,
210 allowedRoutes: allowedRoutes,
211 whitelistDomains: opts.WhitelistDomains,
212 skipAuthPreflight: opts.SkipAuthPreflight,
213 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
214 realClientIPParser: opts.GetRealClientIPParser(),
215 SkipProviderButton: opts.SkipProviderButton,
216 templates: templates,
217 trustedIPs: trustedIPs,
218 Banner: opts.Banner,
219 Footer: opts.Footer,
220 SignInMessage: buildSignInMessage(opts),
222 basicAuthValidator: basicAuthValidator,
223 displayHtpasswdForm: basicAuthValidator != nil && opts.DisplayHtpasswdForm,
224 sessionChain: sessionChain,
225 headersChain: headersChain,
226 preAuthChain: preAuthChain,
227 }, nil
230// buildPreAuthChain constructs a chain that should process every request before
231// the OAuth2 Proxy authentication logic kicks in.
232// For example forcing HTTPS or health checks.
233func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
234 chain := alice.New(middleware.NewScope())
236 if opts.ForceHTTPS {
237 _, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress)
238 if err != nil {
239 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.HTTPAddress, err)
240 }
241 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
242 }
244 healthCheckPaths := []string{opts.PingPath}
245 healthCheckUserAgents := []string{opts.PingUserAgent}
246 if opts.GCPHealthChecks {
247 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
248 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
249 }
251 // To silence logging of health checks, register the health check handler before
252 // the logging handler
253 if opts.Logging.SilencePing {
254 chain = chain.Append(middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), LoggingHandler)
255 } else {
256 chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents))
257 }
259 return chain, nil
262func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
263 chain := alice.New()
265 if opts.SkipJwtBearerTokens {
266 sessionLoaders := []middlewareapi.TokenToSessionFunc{
267 opts.GetProvider().CreateSessionFromToken,
268 }
270 for _, verifier := range opts.GetJWTBearerVerifiers() {
271 sessionLoaders = append(sessionLoaders,
272 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
273 }
275 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
276 }
278 if validator != nil {
279 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
280 }
282 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
283 SessionStore: sessionStore,
284 RefreshPeriod: opts.Cookie.Refresh,
285 RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
286 ValidateSessionState: opts.GetProvider().ValidateSession,
287 }))
289 return chain
292func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
293 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
294 if err != nil {
295 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
296 }
298 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
299 if err != nil {
300 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
301 }
303 return alice.New(requestInjector, responseInjector), nil
306func buildSignInMessage(opts *options.Options) string {
307 var msg string
308 if len(opts.Banner) >= 1 {
309 if opts.Banner == "-" {
310 msg = ""
311 } else {
312 msg = opts.Banner
313 }
314 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
315 if len(opts.EmailDomains) > 1 {
316 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
317 } else if opts.EmailDomains[0] != "*" {
318 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
319 }
320 }
321 return msg
324// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
325// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
326// (method=path support)
327func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
328 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
330 for _, path := range opts.SkipAuthRegex {
331 compiledRegex, err := regexp.Compile(path)
332 if err != nil {
333 return nil, err
334 }
335 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
336 routes = append(routes, allowedRoute{
337 method: "",
338 pathRegex: compiledRegex,
339 })
340 }
342 for _, methodPath := range opts.SkipAuthRoutes {
343 var (
344 method string
345 path string
346 )
348 parts := strings.SplitN(methodPath, "=", 2)
349 if len(parts) == 1 {
350 method = ""
351 path = parts[0]
352 } else {
353 method = strings.ToUpper(parts[0])
354 path = parts[1]
355 }
357 compiledRegex, err := regexp.Compile(path)
358 if err != nil {
359 return nil, err
360 }
361 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
362 routes = append(routes, allowedRoute{
363 method: method,
364 pathRegex: compiledRegex,
365 })
366 }
368 return routes, nil
371// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
372// redirect clients to once authenticated
373func (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()
390func (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
402func (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 }
411 return p.provider.EnrichSession(ctx, s)
414// MakeCSRFCookie creates a cookie for CSRF
415func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
416 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
419func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
420 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
422 if cookieDomain != "" {
423 domain := util.GetRequestHost(req)
424 if h, _, err := net.SplitHostPort(domain); err == nil {
425 domain = h
426 }
427 if !strings.HasSuffix(domain, cookieDomain) {
428 logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
429 }
430 }
432 return &http.Cookie{
433 Name: name,
434 Value: value,
435 Path: p.CookiePath,
436 Domain: cookieDomain,
437 HttpOnly: p.CookieHTTPOnly,
438 Secure: p.CookieSecure,
439 Expires: now.Add(expiration),
440 SameSite: cookies.ParseSameSite(p.CookieSameSite),
441 }
444// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
445// session
446func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
447 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
450// SetCSRFCookie adds a CSRF cookie to the response
451func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
452 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
455// ClearSessionCookie creates a cookie to unset the user's authentication cookie
456// stored in the user's session
457func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
458 return p.sessionStore.Clear(rw, req)
461// LoadCookiedSession reads the user's authentication details from the request
462func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
463 return p.sessionStore.Load(req)
466// SaveSession creates a new session cookie value and sets this on the response
467func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
468 return p.sessionStore.Save(rw, req, s)
471// RobotsTxt disallows scraping pages from the OAuthProxy
472func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
473 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
474 if err != nil {
475 logger.Printf("Error writing robots.txt: %v", err)
476 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
477 return
478 }
479 rw.WriteHeader(http.StatusOK)
482// ErrorPage writes an error response
483func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
484 rw.WriteHeader(code)
485 t := struct {
486 Title string
487 Message string
488 ProxyPrefix string
489 }{
490 Title: fmt.Sprintf("%d %s", code, title),
491 Message: message,
492 ProxyPrefix: p.ProxyPrefix,
493 }
494 err := p.templates.ExecuteTemplate(rw, "error.html", t)
495 if err != nil {
496 logger.Printf("Error rendering error.html template: %v", err)
497 http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
498 }
501// SignInPage writes the sing in template to the response
502func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
503 prepareNoCache(rw)
504 err := p.ClearSessionCookie(rw, req)
505 if err != nil {
506 logger.Printf("Error clearing session cookie: %v", err)
507 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
508 return
509 }
510 rw.WriteHeader(code)
512 redirectURL, err := p.GetRedirect(req)
513 if err != nil {
514 logger.Errorf("Error obtaining redirect: %v", err)
515 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
516 return
517 }
519 if redirectURL == p.SignInPath {
520 redirectURL = "/"
521 }
523 // We allow unescaped template.HTML since it is user configured options
524 /* #nosec G203 */
525 t := struct {
526 ProviderName string
527 SignInMessage template.HTML
528 CustomLogin bool
529 Redirect string
530 Version string
531 ProxyPrefix string
532 Footer template.HTML
533 }{
534 ProviderName: p.provider.Data().ProviderName,
535 SignInMessage: template.HTML(p.SignInMessage),
536 CustomLogin: p.displayHtpasswdForm,
537 Redirect: redirectURL,
538 Version: VERSION,
539 ProxyPrefix: p.ProxyPrefix,
540 Footer: template.HTML(p.Footer),
541 }
542 if p.providerNameOverride != "" {
543 t.ProviderName = p.providerNameOverride
544 }
545 err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
546 if err != nil {
547 logger.Printf("Error rendering sign_in.html template: %v", err)
548 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
549 }
552// ManualSignIn handles basic auth logins to the proxy
553func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
554 if req.Method != "POST" || p.basicAuthValidator == nil {
555 return "", false
556 }
557 user := req.FormValue("username")
558 passwd := req.FormValue("password")
559 if user == "" {
560 return "", false
561 }
562 // check auth
563 if p.basicAuthValidator.Validate(user, passwd) {
564 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
565 return user, true
566 }
567 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
568 return "", false
571// GetRedirect reads the query parameter to get the URL to redirect clients to
572// once authenticated with the OAuthProxy
573func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
574 err = req.ParseForm()
575 if err != nil {
576 return
577 }
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()
594 if strings.HasPrefix(redirect, fmt.Sprintf("%s/", p.ProxyPrefix)) {
595 redirect = "/"
596 }
597 }
599 return
602// getRedirectFromForwardHeaders returns the redirect URL based on X-Forwarded-{Proto,Host,Uri} headers
603func (p *OAuthProxy) getRedirectFromForwardHeaders(req *http.Request) string {
604 uri := util.GetRequestURI(req)
606 if strings.HasPrefix(uri, fmt.Sprintf("%s/", p.ProxyPrefix)) {
607 uri = "/"
608 }
610 return fmt.Sprintf("%s://%s%s", util.GetRequestProto(req), util.GetRequestHost(req), uri)
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 ":*"
617func splitHostPort(hostport string) (host, port string) {
618 host = hostport
620 colon := strings.LastIndexByte(host, ':')
621 if colon != -1 && validOptionalPort(host[colon:]) {
622 host, port = host[:colon], host[colon+1:]
623 }
625 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
626 host = host[1 : len(host)-1]
627 }
629 return
632// validOptionalPort reports whether port is either an empty string
633// or matches /^:\d*$/
634// *** taken from net/url, modified to accept ":*"
635func 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
650// IsValidRedirect checks whether the redirect URL is whitelisted
651func (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()
666 for _, domain := range p.whitelistDomains {
667 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
668 if domainHostname == "" {
669 continue
670 }
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 }
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 }
694// IsAllowedRequest is used to check if auth should be skipped for this request
695func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
696 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
697 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
700// IsAllowedRoute is used to check if the request method & path is allowed without auth
701func (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
710// isForwardedRequest is used to check if X-Forwarded-Host header exists or not
711func isForwardedRequest(req *http.Request, reverseProxy bool) bool {
712 isForwarded := req.Host != util.GetRequestHost(req)
713 return isForwarded && reverseProxy
716// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
717var 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/
723// prepareNoCache prepares headers for preventing browser caching.
724func prepareNoCache(w http.ResponseWriter) {
725 // Set NoCache headers
726 for k, v := range noCacheHeaders {
727 w.Header().Set(k, v)
728 }
731// IsTrustedIP is used to check if a request comes from a trusted client IP address.
732func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
733 if p.trustedIPs == nil {
734 return false
735 }
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 }
744 if remoteAddr == nil {
745 return false
746 }
748 return p.trustedIPs.Has(remoteAddr)
751func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
752 p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
755func (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 }
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 }
782// SignIn serves a page prompting users to sign in
783func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
784 redirect, err := p.GetRedirect(req)
785 if err != nil {
786 logger.Errorf("Error obtaining redirect: %v", err)
787 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
788 return
789 }
791 user, ok := p.ManualSignIn(req)
792 if ok {
793 session := &sessionsapi.SessionState{User: user}
794 err = p.SaveSession(rw, req, session)
795 if err != nil {
796 logger.Printf("Error saving session: %v", err)
797 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
798 return
799 }
800 http.Redirect(rw, req, redirect, http.StatusFound)
801 } else {
802 if p.SkipProviderButton {
803 p.OAuthStart(rw, req)
804 } else {
805 p.SignInPage(rw, req, http.StatusOK)
806 }
807 }
810//UserInfo endpoint outputs session email and preferred username in JSON format
811func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
813 session, err := p.getAuthenticatedSession(rw, req)
814 if err != nil {
815 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
816 return
817 }
819 userInfo := struct {
820 User string `json:"user"`
821 Email string `json:"email"`
822 Groups []string `json:"groups,omitempty"`
823 PreferredUsername string `json:"preferredUsername,omitempty"`
824 }{
825 User: session.User,
826 Email: session.Email,
827 Groups: session.Groups,
828 PreferredUsername: session.PreferredUsername,
829 }
831 rw.Header().Set("Content-Type", "application/json")
832 rw.WriteHeader(http.StatusOK)
833 err = json.NewEncoder(rw).Encode(userInfo)
834 if err != nil {
835 logger.Printf("Error encoding user info: %v", err)
836 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
837 }
840// SignOut sends a response to clear the authentication cookie
841func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
842 redirect, err := p.GetRedirect(req)
843 if err != nil {
844 logger.Errorf("Error obtaining redirect: %v", err)
845 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
846 return
847 }
848 err = p.ClearSessionCookie(rw, req)
849 if err != nil {
850 logger.Errorf("Error clearing session cookie: %v", err)
851 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
852 return
853 }
854 http.Redirect(rw, req, redirect, http.StatusFound)
857// OAuthStart starts the OAuth2 authentication flow
858func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
859 prepareNoCache(rw)
860 nonce, err := encryption.Nonce()
861 if err != nil {
862 logger.Errorf("Error obtaining nonce: %v", err)
863 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
864 return
865 }
866 p.SetCSRFCookie(rw, req, nonce)
867 redirect, err := p.GetRedirect(req)
868 if err != nil {
869 logger.Errorf("Error obtaining redirect: %v", err)
870 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
871 return
872 }
873 redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
874 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
877// OAuthCallback is the OAuth2 authentication flow callback that finishes the
878// OAuth2 authentication flow
879func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
880 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
882 // finish the oauth cycle
883 err := req.ParseForm()
884 if err != nil {
885 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
886 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
887 return
888 }
889 errorString := req.Form.Get("error")
890 if errorString != "" {
891 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
892 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
893 return
894 }
896 session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
897 if err != nil {
898 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
899 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
900 return
901 }
903 err = p.enrichSessionState(req.Context(), session)
904 if err != nil {
905 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
906 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
907 return
908 }
910 state := strings.SplitN(req.Form.Get("state"), ":", 2)
911 if len(state) != 2 {
912 logger.Error("Error while parsing OAuth2 state: invalid length")
913 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
914 return
915 }
916 nonce := state[0]
917 redirect := state[1]
918 c, err := req.Cookie(p.CSRFCookieName)
919 if err != nil {
920 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
921 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
922 return
923 }
924 p.ClearCSRFCookie(rw, req)
925 if c.Value != nonce {
926 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
927 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
928 return
929 }
931 if !p.IsValidRedirect(redirect) {
932 redirect = "/"
933 }
935 // set cookie, or deny
936 authorized, err := p.provider.Authorize(req.Context(), session)
937 if err != nil {
938 logger.Errorf("Error with authorization: %v", err)
939 }
940 if p.Validator(session.Email) && authorized {
941 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
942 err := p.SaveSession(rw, req, session)
943 if err != nil {
944 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
945 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
946 return
947 }
948 http.Redirect(rw, req, redirect, http.StatusFound)
949 } else {
950 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
951 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
952 }
955// AuthOnly checks whether the user is currently logged in (both authentication
956// and optional authorization).
957func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
958 session, err := p.getAuthenticatedSession(rw, req)
959 if err != nil {
960 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
961 return
962 }
964 // Unauthorized cases need to return 403 to prevent infinite redirects with
965 // subrequest architectures
966 if !authOnlyAuthorize(req, session) {
967 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
968 return
969 }
971 // we are authenticated
972 p.addHeadersForProxying(rw, req, session)
973 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
974 rw.WriteHeader(http.StatusAccepted)
975 })).ServeHTTP(rw, req)
978// SkipAuthProxy proxies allowlisted requests and skips authentication
979func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
980 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
983// Proxy proxies the user request if the user is authenticated else it prompts
984// them to authenticate
985func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
986 session, err := p.getAuthenticatedSession(rw, req)
987 switch err {
988 case nil:
989 // we are authenticated
990 p.addHeadersForProxying(rw, req, session)
991 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
992 case ErrNeedsLogin:
993 // we need to send the user to a login screen
994 if isAjax(req) {
995 // no point redirecting an AJAX request
996 p.ErrorJSON(rw, http.StatusUnauthorized)
997 return
998 }
1000 if p.SkipProviderButton {
1001 p.OAuthStart(rw, req)
1002 } else {
1003 p.SignInPage(rw, req, http.StatusForbidden)
1006 case ErrAccessDenied:
1007 p.ErrorPage(rw, http.StatusUnauthorized, "Permission Denied", "Unauthorized")
1009 default:
1010 // unknown error
1011 logger.Errorf("Unexpected internal error: %v", err)
1012 p.ErrorPage(rw, http.StatusInternalServerError,
1013 "Internal Error", "Internal Error")
1018// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
1019// Returns:
1020// - `nil, ErrNeedsLogin` if user needs to login.
1021// - `nil, ErrAccessDenied` if the authenticated user is not authorized
1022// Set-Cookie headers may be set on the response as a side-effect of calling this method.
1023func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
1024 var session *sessionsapi.SessionState
1026 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1027 session = middleware.GetRequestScope(req).Session
1028 }))
1029 getSession.ServeHTTP(rw, req)
1031 if session == nil {
1032 return nil, ErrNeedsLogin
1035 invalidEmail := session.Email != "" && !p.Validator(session.Email)
1036 authorized, err := p.provider.Authorize(req.Context(), session)
1037 if err != nil {
1038 logger.Errorf("Error with authorization: %v", err)
1041 if invalidEmail || !authorized {
1042 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session: removing session %s", session)
1043 // Invalid session, clear it
1044 err := p.ClearSessionCookie(rw, req)
1045 if err != nil {
1046 logger.Errorf("Error clearing session cookie: %v", err)
1048 return nil, ErrAccessDenied
1051 return session, nil
1054// authOnlyAuthorize handles special authorization logic that is only done
1055// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1057// TODO (@NickMeves): This method is a placeholder to be extended but currently
1058// fails the linter. Remove the nolint when functionality expands.
1060//nolint:S1008
1061func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1062 // Allow secondary group restrictions based on the `allowed_groups`
1063 // querystring parameter
1064 if !checkAllowedGroups(req, s) {
1065 return false
1068 return true
1071func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1072 allowedGroups := extractAllowedGroups(req)
1073 if len(allowedGroups) == 0 {
1074 return true
1077 for _, group := range s.Groups {
1078 if _, ok := allowedGroups[group]; ok {
1079 return true
1083 return false
1086func extractAllowedGroups(req *http.Request) map[string]struct{} {
1087 groups := map[string]struct{}{}
1089 query := req.URL.Query()
1090 for _, allowedGroups := range query["allowed_groups"] {
1091 for _, group := range strings.Split(allowedGroups, ",") {
1092 if group != "" {
1093 groups[group] = struct{}{}
1098 return groups
1101// addHeadersForProxying adds the appropriate headers the request / response for proxying
1102func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1103 if session.Email == "" {
1104 rw.Header().Set("GAP-Auth", session.User)
1105 } else {
1106 rw.Header().Set("GAP-Auth", session.Email)
1110// isAjax checks if a request is an ajax request
1111func isAjax(req *http.Request) bool {
1112 acceptValues := req.Header.Values("Accept")
1113 const ajaxReq = applicationJSON
1114 for _, v := range acceptValues {
1115 if v == ajaxReq {
1116 return true
1119 return false
1122// ErrorJSON returns the error code with an application/json mime type
1123func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1124 rw.Header().Set("Content-Type", applicationJSON)
1125 rw.WriteHeader(code)