git.iko.soy/typeable/oauth2-proxy
on proxy-mapping help sign in
typeable/oauth2-proxy/oauthproxy.go
1055 lines 35 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/coreos/go-oidc"
17 "github.com/justinas/alice"
18 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
19 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
21 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
22 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
30 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
31 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
34const (
35 httpScheme = "http"
36 httpsScheme = "https"
38 applicationJSON = "application/json"
41var (
42 // ErrNeedsLogin means the user should be redirected to the login page
43 ErrNeedsLogin = errors.New("redirect to login page")
45 // Used to check final redirects are not susceptible to open redirects.
46 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
47 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
50// allowedRoute manages method + path based allowlists
51type allowedRoute struct {
52 method string
53 pathRegex *regexp.Regexp
56// OAuthProxy is the main authentication proxy
57type OAuthProxy struct {
58 CookieSeed string
59 CookieName string
60 CSRFCookieName string
61 CookieDomains []string
62 CookiePath string
63 CookieSecure bool
64 CookieHTTPOnly bool
65 CookieExpire time.Duration
66 CookieRefresh time.Duration
67 CookieSameSite string
68 Validator func(string) bool
70 RobotsPath string
71 SignInPath string
72 SignOutPath string
73 OAuthStartPath string
74 OAuthCallbackPath string
75 AuthOnlyPath string
76 UserInfoPath string
78 allowedRoutes []allowedRoute
79 redirectURL *url.URL // the url to receive requests at
80 whitelistDomains []string
81 provider providers.Provider
82 providerNameOverride string
83 sessionStore sessionsapi.SessionStore
84 ProxyPrefix string
85 SignInMessage string
86 basicAuthValidator basic.Validator
87 displayHtpasswdForm bool
88 serveMux http.Handler
89 SetXAuthRequest bool
90 PassBasicAuth bool
91 SetBasicAuth bool
92 SkipProviderButton bool
93 PassUserHeaders bool
94 BasicAuthPassword string
95 PassAccessToken bool
96 SetAuthorization bool
97 PassAuthorization bool
98 PreferEmailToUser bool
99 skipAuthPreflight bool
100 skipJwtBearerTokens bool
101 mainJwtBearerVerifier *oidc.IDTokenVerifier
102 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
103 templates *template.Template
104 realClientIPParser ipapi.RealClientIPParser
105 trustedIPs *ip.NetSet
106 Banner string
107 Footer string
108 AllowedGroups []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 provider: opts.GetProvider(),
205 providerNameOverride: opts.ProviderName,
206 sessionStore: sessionStore,
207 serveMux: upstreamProxy,
208 redirectURL: redirectURL,
209 allowedRoutes: allowedRoutes,
210 whitelistDomains: opts.WhitelistDomains,
211 skipAuthPreflight: opts.SkipAuthPreflight,
212 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
213 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
214 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
215 realClientIPParser: opts.GetRealClientIPParser(),
216 SkipProviderButton: opts.SkipProviderButton,
217 templates: templates,
218 trustedIPs: trustedIPs,
219 Banner: opts.Banner,
220 Footer: opts.Footer,
221 SignInMessage: buildSignInMessage(opts),
222 AllowedGroups: opts.AllowedGroups,
224 basicAuthValidator: basicAuthValidator,
225 displayHtpasswdForm: basicAuthValidator != nil && opts.DisplayHtpasswdForm,
226 sessionChain: sessionChain,
227 headersChain: headersChain,
228 preAuthChain: preAuthChain,
229 }, nil
232// buildPreAuthChain constructs a chain that should process every request before
233// the OAuth2 Proxy authentication logic kicks in.
234// For example forcing HTTPS or health checks.
235func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
236 chain := alice.New(middleware.NewScope())
238 if opts.ForceHTTPS {
239 _, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress)
240 if err != nil {
241 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.HTTPAddress, err)
242 }
243 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
244 }
246 healthCheckPaths := []string{opts.PingPath}
247 healthCheckUserAgents := []string{opts.PingUserAgent}
248 if opts.GCPHealthChecks {
249 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
250 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
251 }
253 // To silence logging of health checks, register the health check handler before
254 // the logging handler
255 if opts.Logging.SilencePing {
256 chain = chain.Append(middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), LoggingHandler)
257 } else {
258 chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents))
259 }
261 return chain, nil
264func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
265 chain := alice.New()
267 if opts.SkipJwtBearerTokens {
268 sessionLoaders := []middlewareapi.TokenToSessionLoader{}
269 if opts.GetOIDCVerifier() != nil {
270 sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
271 Verifier: opts.GetOIDCVerifier(),
272 TokenToSession: opts.GetProvider().CreateSessionStateFromBearerToken,
273 })
274 }
276 for _, verifier := range opts.GetJWTBearerVerifiers() {
277 sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
278 Verifier: verifier,
279 })
280 }
282 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
283 }
285 if validator != nil {
286 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
287 }
289 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
290 SessionStore: sessionStore,
291 RefreshPeriod: opts.Cookie.Refresh,
292 RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
293 ValidateSessionState: opts.GetProvider().ValidateSessionState,
294 }))
296 return chain
299func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
300 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
301 if err != nil {
302 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
303 }
305 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
306 if err != nil {
307 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
308 }
310 return alice.New(requestInjector, responseInjector), nil
313func buildSignInMessage(opts *options.Options) string {
314 var msg string
315 if len(opts.Banner) >= 1 {
316 if opts.Banner == "-" {
317 msg = ""
318 } else {
319 msg = opts.Banner
320 }
321 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
322 if len(opts.EmailDomains) > 1 {
323 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
324 } else if opts.EmailDomains[0] != "*" {
325 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
326 }
327 }
328 return msg
331// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
332// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
333// (method=path support)
334func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
335 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
337 for _, path := range opts.SkipAuthRegex {
338 compiledRegex, err := regexp.Compile(path)
339 if err != nil {
340 return nil, err
341 }
342 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
343 routes = append(routes, allowedRoute{
344 method: "",
345 pathRegex: compiledRegex,
346 })
347 }
349 for _, methodPath := range opts.SkipAuthRoutes {
350 var (
351 method string
352 path string
353 )
355 parts := strings.SplitN(methodPath, "=", 2)
356 if len(parts) == 1 {
357 method = ""
358 path = parts[0]
359 } else {
360 method = strings.ToUpper(parts[0])
361 path = parts[1]
362 }
364 compiledRegex, err := regexp.Compile(path)
365 if err != nil {
366 return nil, err
367 }
368 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
369 routes = append(routes, allowedRoute{
370 method: method,
371 pathRegex: compiledRegex,
372 })
373 }
375 return routes, nil
378// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
379// redirect clients to once authenticated
380func (p *OAuthProxy) GetRedirectURI(host string) string {
381 // default to the request Host if not set
382 if p.redirectURL.Host != "" {
383 return p.redirectURL.String()
384 }
385 u := *p.redirectURL
386 if u.Scheme == "" {
387 if p.CookieSecure {
388 u.Scheme = httpsScheme
389 } else {
390 u.Scheme = httpScheme
391 }
392 }
393 u.Host = host
394 return u.String()
397func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
398 if code == "" {
399 return nil, errors.New("missing code")
400 }
401 redirectURI := p.GetRedirectURI(host)
402 s, err := p.provider.Redeem(ctx, redirectURI, code)
403 if err != nil {
404 return nil, err
405 }
406 return s, nil
409func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
410 var err error
411 if s.Email == "" {
412 s.Email, err = p.provider.GetEmailAddress(ctx, s)
413 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
414 return err
415 }
416 }
418 return p.provider.EnrichSessionState(ctx, s)
421// MakeCSRFCookie creates a cookie for CSRF
422func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
423 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
426func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
427 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
429 if cookieDomain != "" {
430 domain := util.GetRequestHost(req)
431 if h, _, err := net.SplitHostPort(domain); err == nil {
432 domain = h
433 }
434 if !strings.HasSuffix(domain, cookieDomain) {
435 logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
436 }
437 }
439 return &http.Cookie{
440 Name: name,
441 Value: value,
442 Path: p.CookiePath,
443 Domain: cookieDomain,
444 HttpOnly: p.CookieHTTPOnly,
445 Secure: p.CookieSecure,
446 Expires: now.Add(expiration),
447 SameSite: cookies.ParseSameSite(p.CookieSameSite),
448 }
451// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
452// session
453func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
454 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
457// SetCSRFCookie adds a CSRF cookie to the response
458func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
459 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
462// ClearSessionCookie creates a cookie to unset the user's authentication cookie
463// stored in the user's session
464func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
465 return p.sessionStore.Clear(rw, req)
468// LoadCookiedSession reads the user's authentication details from the request
469func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
470 return p.sessionStore.Load(req)
473// SaveSession creates a new session cookie value and sets this on the response
474func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
475 return p.sessionStore.Save(rw, req, s)
478// RobotsTxt disallows scraping pages from the OAuthProxy
479func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
480 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
481 if err != nil {
482 logger.Printf("Error writing robots.txt: %v", err)
483 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
484 return
485 }
486 rw.WriteHeader(http.StatusOK)
489// ErrorPage writes an error response
490func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
491 rw.WriteHeader(code)
492 t := struct {
493 Title string
494 Message string
495 ProxyPrefix string
496 }{
497 Title: fmt.Sprintf("%d %s", code, title),
498 Message: message,
499 ProxyPrefix: p.ProxyPrefix,
500 }
501 err := p.templates.ExecuteTemplate(rw, "error.html", t)
502 if err != nil {
503 logger.Printf("Error rendering error.html template: %v", err)
504 http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
505 }
508// SignInPage writes the sing in template to the response
509func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
510 prepareNoCache(rw)
511 err := p.ClearSessionCookie(rw, req)
512 if err != nil {
513 logger.Printf("Error clearing session cookie: %v", err)
514 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
515 return
516 }
517 rw.WriteHeader(code)
519 redirectURL, err := p.GetRedirect(req)
520 if err != nil {
521 logger.Errorf("Error obtaining redirect: %v", err)
522 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
523 return
524 }
526 if redirectURL == p.SignInPath {
527 redirectURL = "/"
528 }
530 // We allow unescaped template.HTML since it is user configured options
531 /* #nosec G203 */
532 t := struct {
533 ProviderName string
534 SignInMessage template.HTML
535 CustomLogin bool
536 Redirect string
537 Version string
538 ProxyPrefix string
539 Footer template.HTML
540 }{
541 ProviderName: p.provider.Data().ProviderName,
542 SignInMessage: template.HTML(p.SignInMessage),
543 CustomLogin: p.displayHtpasswdForm,
544 Redirect: redirectURL,
545 Version: VERSION,
546 ProxyPrefix: p.ProxyPrefix,
547 Footer: template.HTML(p.Footer),
548 }
549 if p.providerNameOverride != "" {
550 t.ProviderName = p.providerNameOverride
551 }
552 err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
553 if err != nil {
554 logger.Printf("Error rendering sign_in.html template: %v", err)
555 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
556 }
559// ManualSignIn handles basic auth logins to the proxy
560func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
561 if req.Method != "POST" || p.basicAuthValidator == nil {
562 return "", false
563 }
564 user := req.FormValue("username")
565 passwd := req.FormValue("password")
566 if user == "" {
567 return "", false
568 }
569 // check auth
570 if p.basicAuthValidator.Validate(user, passwd) {
571 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
572 return user, true
573 }
574 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
575 return "", false
578// GetRedirect reads the query parameter to get the URL to redirect clients to
579// once authenticated with the OAuthProxy
580func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
581 err = req.ParseForm()
582 if err != nil {
583 return
584 }
586 redirect = req.Header.Get("X-Auth-Request-Redirect")
587 if req.Form.Get("rd") != "" {
588 redirect = req.Form.Get("rd")
589 }
590 if !p.IsValidRedirect(redirect) {
591 // Use RequestURI to preserve ?query
592 redirect = req.URL.RequestURI()
593 if strings.HasPrefix(redirect, p.ProxyPrefix) {
594 redirect = "/"
595 }
596 }
598 return
601// splitHostPort separates host and port. If the port is not valid, it returns
602// the entire input as host, and it doesn't check the validity of the host.
603// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
604// *** taken from net/url, modified validOptionalPort() to accept ":*"
605func splitHostPort(hostport string) (host, port string) {
606 host = hostport
608 colon := strings.LastIndexByte(host, ':')
609 if colon != -1 && validOptionalPort(host[colon:]) {
610 host, port = host[:colon], host[colon+1:]
611 }
613 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
614 host = host[1 : len(host)-1]
615 }
617 return
620// validOptionalPort reports whether port is either an empty string
621// or matches /^:\d*$/
622// *** taken from net/url, modified to accept ":*"
623func validOptionalPort(port string) bool {
624 if port == "" || port == ":*" {
625 return true
626 }
627 if port[0] != ':' {
628 return false
629 }
630 for _, b := range port[1:] {
631 if b < '0' || b > '9' {
632 return false
633 }
634 }
635 return true
638// IsValidRedirect checks whether the redirect URL is whitelisted
639func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
640 switch {
641 case redirect == "":
642 // The user didn't specify a redirect, should fallback to `/`
643 return false
644 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
645 return true
646 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
647 redirectURL, err := url.Parse(redirect)
648 if err != nil {
649 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
650 return false
651 }
652 redirectHostname := redirectURL.Hostname()
654 for _, domain := range p.whitelistDomains {
655 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
656 if domainHostname == "" {
657 continue
658 }
660 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
661 // the domain names match, now validate the ports
662 // if the whitelisted domain's port is '*', allow all ports
663 // if the whitelisted domain contains a specific port, only allow that port
664 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
665 redirectPort := redirectURL.Port()
666 if (domainPort == "*") ||
667 (domainPort == redirectPort) ||
668 (domainPort == "" && redirectPort == "") {
669 return true
670 }
671 }
672 }
674 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
675 return false
676 default:
677 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
678 return false
679 }
682// IsAllowedRequest is used to check if auth should be skipped for this request
683func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
684 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
685 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
688// IsAllowedRoute is used to check if the request method & path is allowed without auth
689func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
690 for _, route := range p.allowedRoutes {
691 if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
692 return true
693 }
694 }
695 return false
698// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
699var noCacheHeaders = map[string]string{
700 "Expires": time.Unix(0, 0).Format(time.RFC1123),
701 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
702 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
705// prepareNoCache prepares headers for preventing browser caching.
706func prepareNoCache(w http.ResponseWriter) {
707 // Set NoCache headers
708 for k, v := range noCacheHeaders {
709 w.Header().Set(k, v)
710 }
713// IsTrustedIP is used to check if a request comes from a trusted client IP address.
714func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
715 if p.trustedIPs == nil {
716 return false
717 }
719 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
720 if err != nil {
721 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
722 // Possibly spoofed X-Real-IP header
723 return false
724 }
726 if remoteAddr == nil {
727 return false
728 }
730 return p.trustedIPs.Has(remoteAddr)
733func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
734 p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
737func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
738 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
739 prepareNoCache(rw)
740 }
742 switch path := req.URL.Path; {
743 case path == p.RobotsPath:
744 p.RobotsTxt(rw)
745 case p.IsAllowedRequest(req):
746 p.SkipAuthProxy(rw, req)
747 case path == p.SignInPath:
748 p.SignIn(rw, req)
749 case path == p.SignOutPath:
750 p.SignOut(rw, req)
751 case path == p.OAuthStartPath:
752 p.OAuthStart(rw, req)
753 case path == p.OAuthCallbackPath:
754 p.OAuthCallback(rw, req)
755 case path == p.AuthOnlyPath:
756 p.AuthenticateOnly(rw, req)
757 case path == p.UserInfoPath:
758 p.UserInfo(rw, req)
759 default:
760 p.Proxy(rw, req)
761 }
764// SignIn serves a page prompting users to sign in
765func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
766 redirect, err := p.GetRedirect(req)
767 if err != nil {
768 logger.Errorf("Error obtaining redirect: %v", err)
769 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
770 return
771 }
773 user, ok := p.ManualSignIn(req)
774 if ok {
775 session := &sessionsapi.SessionState{User: user}
776 err = p.SaveSession(rw, req, session)
777 if err != nil {
778 logger.Printf("Error saving session: %v", err)
779 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
780 return
781 }
782 http.Redirect(rw, req, redirect, http.StatusFound)
783 } else {
784 if p.SkipProviderButton {
785 p.OAuthStart(rw, req)
786 } else {
787 p.SignInPage(rw, req, http.StatusOK)
788 }
789 }
792//UserInfo endpoint outputs session email and preferred username in JSON format
793func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
795 session, err := p.getAuthenticatedSession(rw, req)
796 if err != nil {
797 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
798 return
799 }
800 userInfo := struct {
801 Email string `json:"email"`
802 PreferredUsername string `json:"preferredUsername,omitempty"`
803 }{
804 Email: session.Email,
805 PreferredUsername: session.PreferredUsername,
806 }
807 rw.Header().Set("Content-Type", "application/json")
808 rw.WriteHeader(http.StatusOK)
809 err = json.NewEncoder(rw).Encode(userInfo)
810 if err != nil {
811 logger.Printf("Error encoding user info: %v", err)
812 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
813 }
816// SignOut sends a response to clear the authentication cookie
817func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
818 redirect, err := p.GetRedirect(req)
819 if err != nil {
820 logger.Errorf("Error obtaining redirect: %v", err)
821 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
822 return
823 }
824 err = p.ClearSessionCookie(rw, req)
825 if err != nil {
826 logger.Errorf("Error clearing session cookie: %v", err)
827 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
828 return
829 }
830 http.Redirect(rw, req, redirect, http.StatusFound)
833// OAuthStart starts the OAuth2 authentication flow
834func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
835 prepareNoCache(rw)
836 nonce, err := encryption.Nonce()
837 if err != nil {
838 logger.Errorf("Error obtaining nonce: %v", err)
839 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
840 return
841 }
842 p.SetCSRFCookie(rw, req, nonce)
843 redirect, err := p.GetRedirect(req)
844 if err != nil {
845 logger.Errorf("Error obtaining redirect: %v", err)
846 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
847 return
848 }
849 redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
850 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
853// OAuthCallback is the OAuth2 authentication flow callback that finishes the
854// OAuth2 authentication flow
855func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
856 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
858 // finish the oauth cycle
859 err := req.ParseForm()
860 if err != nil {
861 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
862 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
863 return
864 }
865 errorString := req.Form.Get("error")
866 if errorString != "" {
867 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
868 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
869 return
870 }
872 session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
873 if err != nil {
874 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
875 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
876 return
877 }
879 err = p.enrichSessionState(req.Context(), session)
880 if err != nil {
881 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
882 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
883 return
884 }
886 state := strings.SplitN(req.Form.Get("state"), ":", 2)
887 if len(state) != 2 {
888 logger.Error("Error while parsing OAuth2 state: invalid length")
889 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
890 return
891 }
892 nonce := state[0]
893 redirect := state[1]
894 c, err := req.Cookie(p.CSRFCookieName)
895 if err != nil {
896 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
897 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
898 return
899 }
900 p.ClearCSRFCookie(rw, req)
901 if c.Value != nonce {
902 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
903 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
904 return
905 }
907 if !p.IsValidRedirect(redirect) {
908 redirect = "/"
909 }
911 // set cookie, or deny
912 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
913 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
914 err := p.SaveSession(rw, req, session)
915 if err != nil {
916 logger.Printf("Error saving session state for %s: %v", remoteAddr, err)
917 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
918 return
919 }
920 http.Redirect(rw, req, redirect, http.StatusFound)
921 } else {
922 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
923 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
924 }
927// AuthenticateOnly checks whether the user is currently logged in
928func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
929 session, err := p.getAuthenticatedSession(rw, req)
930 if err != nil {
931 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
932 return
933 }
935 // we are authenticated
936 p.addHeadersForProxying(rw, req, session)
937 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
938 rw.WriteHeader(http.StatusAccepted)
939 })).ServeHTTP(rw, req)
942// SkipAuthProxy proxies allowlisted requests and skips authentication
943func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
944 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
947// Proxy proxies the user request if the user is authenticated else it prompts
948// them to authenticate
949func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
950 session, err := p.getAuthenticatedSession(rw, req)
951 switch err {
952 case nil:
953 // we are authenticated
954 p.addHeadersForProxying(rw, req, session)
955 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
956 case ErrNeedsLogin:
957 // we need to send the user to a login screen
958 if isAjax(req) {
959 // no point redirecting an AJAX request
960 p.ErrorJSON(rw, http.StatusUnauthorized)
961 return
962 }
964 if p.SkipProviderButton {
965 p.OAuthStart(rw, req)
966 } else {
967 p.SignInPage(rw, req, http.StatusForbidden)
968 }
970 default:
971 // unknown error
972 logger.Errorf("Unexpected internal error: %v", err)
973 p.ErrorPage(rw, http.StatusInternalServerError,
974 "Internal Error", "Internal Error")
975 }
979// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
980// Returns nil, ErrNeedsLogin if user needs to login.
981// Set-Cookie headers may be set on the response as a side-effect of calling this method.
982func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
983 var session *sessionsapi.SessionState
985 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
986 session = middleware.GetRequestScope(req).Session
987 }))
988 getSession.ServeHTTP(rw, req)
990 if session == nil {
991 return nil, ErrNeedsLogin
992 }
994 invalidEmail := session != nil && session.Email != "" && !p.Validator(session.Email)
995 invalidGroups := session != nil && !p.validateGroups(session.Groups)
997 if invalidEmail || invalidGroups {
998 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
999 // Invalid session, clear it
1000 err := p.ClearSessionCookie(rw, req)
1001 if err != nil {
1002 logger.Printf("Error clearing session cookie: %v", err)
1004 return nil, ErrNeedsLogin
1007 return session, nil
1010// addHeadersForProxying adds the appropriate headers the request / response for proxying
1011func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1012 if session.Email == "" {
1013 rw.Header().Set("GAP-Auth", session.User)
1014 } else {
1015 rw.Header().Set("GAP-Auth", session.Email)
1019// isAjax checks if a request is an ajax request
1020func isAjax(req *http.Request) bool {
1021 acceptValues := req.Header.Values("Accept")
1022 const ajaxReq = applicationJSON
1023 for _, v := range acceptValues {
1024 if v == ajaxReq {
1025 return true
1028 return false
1031// ErrorJSON returns the error code with an application/json mime type
1032func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1033 rw.Header().Set("Content-Type", applicationJSON)
1034 rw.WriteHeader(code)
1037func (p *OAuthProxy) validateGroups(groups []string) bool {
1038 if len(p.AllowedGroups) == 0 {
1039 return true
1042 allowedGroups := map[string]struct{}{}
1044 for _, group := range p.AllowedGroups {
1045 allowedGroups[group] = struct{}{}
1048 for _, group := range groups {
1049 if _, ok := allowedGroups[group]; ok {
1050 return true
1054 return false