git.iko.soy/typeable/oauth2-proxy
on 86e672e help sign in
typeable/oauth2-proxy/oauthproxy.go
1250 lines 41 kB history raw
1package main
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "net"
9 "net/http"
10 "net/url"
11 "os"
12 "os/signal"
13 "regexp"
14 "strings"
15 "syscall"
16 "time"
18 "github.com/justinas/alice"
19 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
20 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
22 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
27 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
30 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
31 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
32 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
33 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
34 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
37const (
38 schemeHTTPS = "https"
39 applicationJSON = "application/json"
42var (
43 // ErrNeedsLogin means the user should be redirected to the login page
44 ErrNeedsLogin = errors.New("redirect to login page")
46 // ErrAccessDenied means the user should receive a 401 Unauthorized response
47 ErrAccessDenied = errors.New("access denied")
49 // Used to check final redirects are not susceptible to open redirects.
50 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
51 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
54// allowedRoute manages method + path based allowlists
55type allowedRoute struct {
56 method string
57 pathRegex *regexp.Regexp
60// OAuthProxy is the main authentication proxy
61type OAuthProxy struct {
62 CookieSeed string
63 CookieName string
64 CSRFCookieName string
65 CookieDomains []string
66 CookiePath string
67 CookieSecure bool
68 CookieHTTPOnly bool
69 CookieExpire time.Duration
70 CookieRefresh time.Duration
71 CookieSameSite string
72 Validator func(string) bool
74 RobotsPath string
75 SignInPath string
76 SignOutPath string
77 OAuthStartPath string
78 OAuthCallbackPath string
79 AuthOnlyPath string
80 UserInfoPath string
82 allowedRoutes []allowedRoute
83 redirectURL *url.URL // the url to receive requests at
84 whitelistDomains []string
85 provider providers.Provider
86 sessionStore sessionsapi.SessionStore
87 ProxyPrefix string
88 basicAuthValidator basic.Validator
89 serveMux http.Handler
90 SetXAuthRequest bool
91 PassBasicAuth bool
92 SetBasicAuth bool
93 SkipProviderButton bool
94 PassUserHeaders bool
95 BasicAuthPassword string
96 PassAccessToken bool
97 SetAuthorization bool
98 PassAuthorization bool
99 PreferEmailToUser bool
100 skipAuthPreflight bool
101 skipJwtBearerTokens bool
102 realClientIPParser ipapi.RealClientIPParser
103 trustedIPs *ip.NetSet
105 sessionChain alice.Chain
106 headersChain alice.Chain
107 preAuthChain alice.Chain
108 pageWriter pagewriter.Writer
109 server proxyhttp.Server
112// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
113func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
114 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
115 if err != nil {
116 return nil, fmt.Errorf("error initialising session store: %v", err)
117 }
119 var basicAuthValidator basic.Validator
120 if opts.HtpasswdFile != "" {
121 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
122 var err error
123 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
124 if err != nil {
125 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
126 }
127 }
129 pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
130 TemplatesPath: opts.Templates.Path,
131 CustomLogo: opts.Templates.CustomLogo,
132 ProxyPrefix: opts.ProxyPrefix,
133 Footer: opts.Templates.Footer,
134 Version: VERSION,
135 Debug: opts.Templates.Debug,
136 ProviderName: buildProviderName(opts.GetProvider(), opts.ProviderName),
137 SignInMessage: buildSignInMessage(opts),
138 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
139 })
140 if err != nil {
141 return nil, fmt.Errorf("error initialising page writer: %v", err)
142 }
144 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter.ProxyErrorHandler)
145 if err != nil {
146 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
147 }
149 if opts.SkipJwtBearerTokens {
150 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
151 for _, issuer := range opts.ExtraJwtIssuers {
152 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
153 }
154 }
155 redirectURL := opts.GetRedirectURL()
156 if redirectURL.Path == "" {
157 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
158 }
160 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
161 refresh := "disabled"
162 if opts.Cookie.Refresh != time.Duration(0) {
163 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
164 }
166 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)
168 trustedIPs := ip.NewNetSet()
169 for _, ipStr := range opts.TrustedIPs {
170 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
171 trustedIPs.AddIPNet(*ipNet)
172 } else {
173 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
174 }
175 }
177 allowedRoutes, err := buildRoutesAllowlist(opts)
178 if err != nil {
179 return nil, err
180 }
182 preAuthChain, err := buildPreAuthChain(opts)
183 if err != nil {
184 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
185 }
186 sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
187 headersChain, err := buildHeadersChain(opts)
188 if err != nil {
189 return nil, fmt.Errorf("could not build headers chain: %v", err)
190 }
192 p := &OAuthProxy{
193 CookieName: opts.Cookie.Name,
194 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
195 CookieSeed: opts.Cookie.Secret,
196 CookieDomains: opts.Cookie.Domains,
197 CookiePath: opts.Cookie.Path,
198 CookieSecure: opts.Cookie.Secure,
199 CookieHTTPOnly: opts.Cookie.HTTPOnly,
200 CookieExpire: opts.Cookie.Expire,
201 CookieRefresh: opts.Cookie.Refresh,
202 CookieSameSite: opts.Cookie.SameSite,
203 Validator: validator,
205 RobotsPath: "/robots.txt",
206 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
207 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
208 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
209 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
210 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
211 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
213 ProxyPrefix: opts.ProxyPrefix,
214 provider: opts.GetProvider(),
215 sessionStore: sessionStore,
216 serveMux: upstreamProxy,
217 redirectURL: redirectURL,
218 allowedRoutes: allowedRoutes,
219 whitelistDomains: opts.WhitelistDomains,
220 skipAuthPreflight: opts.SkipAuthPreflight,
221 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
222 realClientIPParser: opts.GetRealClientIPParser(),
223 SkipProviderButton: opts.SkipProviderButton,
224 trustedIPs: trustedIPs,
226 basicAuthValidator: basicAuthValidator,
227 sessionChain: sessionChain,
228 headersChain: headersChain,
229 preAuthChain: preAuthChain,
230 pageWriter: pageWriter,
231 }
233 if err := p.setupServer(opts); err != nil {
234 return nil, fmt.Errorf("error setting up server: %v", err)
235 }
237 return p, nil
240func (p *OAuthProxy) Start() error {
241 if p.server == nil {
242 // We have to call setupServer before Start is called.
243 // If this doesn't happen it's a programming error.
244 panic("server has not been initialised")
245 }
247 ctx, cancel := context.WithCancel(context.Background())
249 // Observe signals in background goroutine.
250 go func() {
251 sigint := make(chan os.Signal, 1)
252 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
253 <-sigint
254 cancel() // cancel the context
255 }()
257 return p.server.Start(ctx)
260func (p *OAuthProxy) setupServer(opts *options.Options) error {
261 serverOpts := proxyhttp.Opts{
262 Handler: p,
263 BindAddress: opts.Server.BindAddress,
264 SecureBindAddress: opts.Server.SecureBindAddress,
265 TLS: opts.Server.TLS,
266 }
268 appServer, err := proxyhttp.NewServer(serverOpts)
269 if err != nil {
270 return fmt.Errorf("could not build app server: %v", err)
271 }
273 metricsServer, err := proxyhttp.NewServer(proxyhttp.Opts{
274 Handler: middleware.DefaultMetricsHandler,
275 BindAddress: opts.MetricsServer.BindAddress,
276 SecureBindAddress: opts.MetricsServer.BindAddress,
277 TLS: opts.MetricsServer.TLS,
278 })
279 if err != nil {
280 return fmt.Errorf("could not build metrics server: %v", err)
281 }
283 p.server = proxyhttp.NewServerGroup(appServer, metricsServer)
284 return nil
287// buildPreAuthChain constructs a chain that should process every request before
288// the OAuth2 Proxy authentication logic kicks in.
289// For example forcing HTTPS or health checks.
290func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
291 chain := alice.New(middleware.NewScope(opts.ReverseProxy))
293 if opts.ForceHTTPS {
294 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
295 if err != nil {
296 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err)
297 }
298 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
299 }
301 healthCheckPaths := []string{opts.PingPath}
302 healthCheckUserAgents := []string{opts.PingUserAgent}
303 if opts.GCPHealthChecks {
304 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
305 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
306 }
308 // To silence logging of health checks, register the health check handler before
309 // the logging handler
310 if opts.Logging.SilencePing {
311 chain = chain.Append(
312 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
313 middleware.NewRequestLogger(),
314 )
315 } else {
316 chain = chain.Append(
317 middleware.NewRequestLogger(),
318 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
319 )
320 }
322 chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
324 return chain, nil
327func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
328 chain := alice.New()
330 if opts.SkipJwtBearerTokens {
331 sessionLoaders := []middlewareapi.TokenToSessionFunc{
332 opts.GetProvider().CreateSessionFromToken,
333 }
335 for _, verifier := range opts.GetJWTBearerVerifiers() {
336 sessionLoaders = append(sessionLoaders,
337 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
338 }
340 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
341 }
343 if validator != nil {
344 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups))
345 }
347 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
348 SessionStore: sessionStore,
349 RefreshPeriod: opts.Cookie.Refresh,
350 RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
351 ValidateSessionState: opts.GetProvider().ValidateSession,
352 }))
354 return chain
357func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
358 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
359 if err != nil {
360 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
361 }
363 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
364 if err != nil {
365 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
366 }
368 return alice.New(requestInjector, responseInjector), nil
371func buildSignInMessage(opts *options.Options) string {
372 var msg string
373 if len(opts.Templates.Banner) >= 1 {
374 if opts.Templates.Banner == "-" {
375 msg = ""
376 } else {
377 msg = opts.Templates.Banner
378 }
379 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
380 if len(opts.EmailDomains) > 1 {
381 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
382 } else if opts.EmailDomains[0] != "*" {
383 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
384 }
385 }
386 return msg
389func buildProviderName(p providers.Provider, override string) string {
390 if override != "" {
391 return override
392 }
393 return p.Data().ProviderName
396// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
397// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
398// (method=path support)
399func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
400 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
402 for _, path := range opts.SkipAuthRegex {
403 compiledRegex, err := regexp.Compile(path)
404 if err != nil {
405 return nil, err
406 }
407 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
408 routes = append(routes, allowedRoute{
409 method: "",
410 pathRegex: compiledRegex,
411 })
412 }
414 for _, methodPath := range opts.SkipAuthRoutes {
415 var (
416 method string
417 path string
418 )
420 parts := strings.SplitN(methodPath, "=", 2)
421 if len(parts) == 1 {
422 method = ""
423 path = parts[0]
424 } else {
425 method = strings.ToUpper(parts[0])
426 path = parts[1]
427 }
429 compiledRegex, err := regexp.Compile(path)
430 if err != nil {
431 return nil, err
432 }
433 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
434 routes = append(routes, allowedRoute{
435 method: method,
436 pathRegex: compiledRegex,
437 })
438 }
440 return routes, nil
443// MakeCSRFCookie creates a cookie for CSRF
444func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
445 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
448func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
449 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
451 if cookieDomain != "" {
452 domain := requestutil.GetRequestHost(req)
453 if h, _, err := net.SplitHostPort(domain); err == nil {
454 domain = h
455 }
456 if !strings.HasSuffix(domain, cookieDomain) {
457 logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
458 }
459 }
461 return &http.Cookie{
462 Name: name,
463 Value: value,
464 Path: p.CookiePath,
465 Domain: cookieDomain,
466 HttpOnly: p.CookieHTTPOnly,
467 Secure: p.CookieSecure,
468 Expires: now.Add(expiration),
469 SameSite: cookies.ParseSameSite(p.CookieSameSite),
470 }
473// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
474// session
475func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
476 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
479// SetCSRFCookie adds a CSRF cookie to the response
480func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
481 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
484// ClearSessionCookie creates a cookie to unset the user's authentication cookie
485// stored in the user's session
486func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
487 return p.sessionStore.Clear(rw, req)
490// LoadCookiedSession reads the user's authentication details from the request
491func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
492 return p.sessionStore.Load(req)
495// SaveSession creates a new session cookie value and sets this on the response
496func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
497 return p.sessionStore.Save(rw, req, s)
500// IsValidRedirect checks whether the redirect URL is whitelisted
501func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
502 switch {
503 case redirect == "":
504 // The user didn't specify a redirect, should fallback to `/`
505 return false
506 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
507 return true
508 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
509 redirectURL, err := url.Parse(redirect)
510 if err != nil {
511 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
512 return false
513 }
514 redirectHostname := redirectURL.Hostname()
516 for _, allowedDomain := range p.whitelistDomains {
517 allowedHost, allowedPort := splitHostPort(allowedDomain)
518 if allowedHost == "" {
519 continue
520 }
522 if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
523 (strings.HasPrefix(allowedHost, ".") &&
524 strings.HasSuffix(redirectHostname, allowedHost)) {
525 // the domain names match, now validate the ports
526 // if the whitelisted domain's port is '*', allow all ports
527 // if the whitelisted domain contains a specific port, only allow that port
528 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
529 redirectPort := redirectURL.Port()
530 if allowedPort == "*" ||
531 allowedPort == redirectPort ||
532 (allowedPort == "" && redirectPort == "") {
533 return true
534 }
535 }
536 }
538 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
539 return false
540 default:
541 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
542 return false
543 }
546func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
547 p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
550func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
551 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
552 prepareNoCache(rw)
553 }
555 switch path := req.URL.Path; {
556 case path == p.RobotsPath:
557 p.RobotsTxt(rw, req)
558 case p.IsAllowedRequest(req):
559 p.SkipAuthProxy(rw, req)
560 case path == p.SignInPath:
561 p.SignIn(rw, req)
562 case path == p.SignOutPath:
563 p.SignOut(rw, req)
564 case path == p.OAuthStartPath:
565 p.OAuthStart(rw, req)
566 case path == p.OAuthCallbackPath:
567 p.OAuthCallback(rw, req)
568 case path == p.AuthOnlyPath:
569 p.AuthOnly(rw, req)
570 case path == p.UserInfoPath:
571 p.UserInfo(rw, req)
572 default:
573 p.Proxy(rw, req)
574 }
577// RobotsTxt disallows scraping pages from the OAuthProxy
578func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter, req *http.Request) {
579 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
580 if err != nil {
581 logger.Printf("Error writing robots.txt: %v", err)
582 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
583 return
584 }
585 rw.WriteHeader(http.StatusOK)
588// ErrorPage writes an error response
589func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
590 redirectURL, err := p.getAppRedirect(req)
591 if err != nil {
592 logger.Errorf("Error obtaining redirect: %v", err)
593 }
594 if redirectURL == p.SignInPath || redirectURL == "" {
595 redirectURL = "/"
596 }
598 p.pageWriter.WriteErrorPage(rw, code, redirectURL, appError, messages...)
601// IsAllowedRequest is used to check if auth should be skipped for this request
602func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
603 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
604 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
607// IsAllowedRoute is used to check if the request method & path is allowed without auth
608func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
609 for _, route := range p.allowedRoutes {
610 if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
611 return true
612 }
613 }
614 return false
617// isTrustedIP is used to check if a request comes from a trusted client IP address.
618func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
619 if p.trustedIPs == nil {
620 return false
621 }
623 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
624 if err != nil {
625 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
626 // Possibly spoofed X-Real-IP header
627 return false
628 }
630 if remoteAddr == nil {
631 return false
632 }
634 return p.trustedIPs.Has(remoteAddr)
637// SignInPage writes the sing in template to the response
638func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
639 prepareNoCache(rw)
640 err := p.ClearSessionCookie(rw, req)
641 if err != nil {
642 logger.Printf("Error clearing session cookie: %v", err)
643 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
644 return
645 }
646 rw.WriteHeader(code)
648 redirectURL, err := p.getAppRedirect(req)
649 if err != nil {
650 logger.Errorf("Error obtaining redirect: %v", err)
651 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
652 return
653 }
655 if redirectURL == p.SignInPath {
656 redirectURL = "/"
657 }
659 p.pageWriter.WriteSignInPage(rw, redirectURL)
662// ManualSignIn handles basic auth logins to the proxy
663func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
664 if req.Method != "POST" || p.basicAuthValidator == nil {
665 return "", false
666 }
667 user := req.FormValue("username")
668 passwd := req.FormValue("password")
669 if user == "" {
670 return "", false
671 }
672 // check auth
673 if p.basicAuthValidator.Validate(user, passwd) {
674 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
675 return user, true
676 }
677 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
678 return "", false
681// SignIn serves a page prompting users to sign in
682func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
683 redirect, err := p.getAppRedirect(req)
684 if err != nil {
685 logger.Errorf("Error obtaining redirect: %v", err)
686 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
687 return
688 }
690 user, ok := p.ManualSignIn(req)
691 if ok {
692 session := &sessionsapi.SessionState{User: user}
693 err = p.SaveSession(rw, req, session)
694 if err != nil {
695 logger.Printf("Error saving session: %v", err)
696 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
697 return
698 }
699 http.Redirect(rw, req, redirect, http.StatusFound)
700 } else {
701 if p.SkipProviderButton {
702 p.OAuthStart(rw, req)
703 } else {
704 p.SignInPage(rw, req, http.StatusOK)
705 }
706 }
709// UserInfo endpoint outputs session email and preferred username in JSON format
710func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
712 session, err := p.getAuthenticatedSession(rw, req)
713 if err != nil {
714 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
715 return
716 }
718 userInfo := struct {
719 User string `json:"user"`
720 Email string `json:"email"`
721 Groups []string `json:"groups,omitempty"`
722 PreferredUsername string `json:"preferredUsername,omitempty"`
723 }{
724 User: session.User,
725 Email: session.Email,
726 Groups: session.Groups,
727 PreferredUsername: session.PreferredUsername,
728 }
730 rw.Header().Set("Content-Type", "application/json")
731 rw.WriteHeader(http.StatusOK)
732 err = json.NewEncoder(rw).Encode(userInfo)
733 if err != nil {
734 logger.Printf("Error encoding user info: %v", err)
735 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
736 }
739// SignOut sends a response to clear the authentication cookie
740func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
741 redirect, err := p.getAppRedirect(req)
742 if err != nil {
743 logger.Errorf("Error obtaining redirect: %v", err)
744 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
745 return
746 }
747 err = p.ClearSessionCookie(rw, req)
748 if err != nil {
749 logger.Errorf("Error clearing session cookie: %v", err)
750 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
751 return
752 }
753 http.Redirect(rw, req, redirect, http.StatusFound)
756// OAuthStart starts the OAuth2 authentication flow
757func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
758 prepareNoCache(rw)
759 nonce, err := encryption.Nonce()
760 if err != nil {
761 logger.Errorf("Error obtaining nonce: %v", err)
762 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
763 return
764 }
765 p.SetCSRFCookie(rw, req, nonce)
766 redirect, err := p.getAppRedirect(req)
767 if err != nil {
768 logger.Errorf("Error obtaining redirect: %v", err)
769 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
770 return
771 }
772 redirectURI := p.getOAuthRedirectURI(req)
773 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
776// OAuthCallback is the OAuth2 authentication flow callback that finishes the
777// OAuth2 authentication flow
778func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
779 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
781 // finish the oauth cycle
782 err := req.ParseForm()
783 if err != nil {
784 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
785 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
786 return
787 }
788 errorString := req.Form.Get("error")
789 if errorString != "" {
790 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
791 message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
792 // Set the debug message and override the non debug message to be the same for this case
793 p.ErrorPage(rw, req, http.StatusForbidden, message, message)
794 return
795 }
797 session, err := p.redeemCode(req)
798 if err != nil {
799 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
800 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
801 return
802 }
804 err = p.enrichSessionState(req.Context(), session)
805 if err != nil {
806 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
807 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
808 return
809 }
811 state := strings.SplitN(req.Form.Get("state"), ":", 2)
812 if len(state) != 2 {
813 logger.Error("Error while parsing OAuth2 state: invalid length")
814 p.ErrorPage(rw, req, http.StatusInternalServerError, "State paremeter did not have expected length", "Login Failed: Invalid State after login.")
815 return
816 }
817 nonce := state[0]
818 redirect := state[1]
819 c, err := req.Cookie(p.CSRFCookieName)
820 if err != nil {
821 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
822 p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
823 return
824 }
825 p.ClearCSRFCookie(rw, req)
826 if c.Value != nonce {
827 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
828 p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
829 return
830 }
832 if !p.IsValidRedirect(redirect) {
833 redirect = "/"
834 }
836 // set cookie, or deny
837 authorized, err := p.provider.Authorize(req.Context(), session)
838 if err != nil {
839 logger.Errorf("Error with authorization: %v", err)
840 }
841 if p.Validator(session.Email) && authorized {
842 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
843 err := p.SaveSession(rw, req, session)
844 if err != nil {
845 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
846 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
847 return
848 }
849 http.Redirect(rw, req, redirect, http.StatusFound)
850 } else {
851 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
852 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
853 }
856func (p *OAuthProxy) redeemCode(req *http.Request) (*sessionsapi.SessionState, error) {
857 code := req.Form.Get("code")
858 if code == "" {
859 return nil, providers.ErrMissingCode
860 }
862 redirectURI := p.getOAuthRedirectURI(req)
863 s, err := p.provider.Redeem(req.Context(), redirectURI, code)
864 if err != nil {
865 return nil, err
866 }
867 return s, nil
870func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
871 var err error
872 if s.Email == "" {
873 // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
874 // nolint:staticcheck
875 s.Email, err = p.provider.GetEmailAddress(ctx, s)
876 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
877 return err
878 }
879 }
881 return p.provider.EnrichSession(ctx, s)
884// AuthOnly checks whether the user is currently logged in (both authentication
885// and optional authorization).
886func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
887 session, err := p.getAuthenticatedSession(rw, req)
888 if err != nil {
889 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
890 return
891 }
893 // Unauthorized cases need to return 403 to prevent infinite redirects with
894 // subrequest architectures
895 if !authOnlyAuthorize(req, session) {
896 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
897 return
898 }
900 // we are authenticated
901 p.addHeadersForProxying(rw, session)
902 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
903 rw.WriteHeader(http.StatusAccepted)
904 })).ServeHTTP(rw, req)
907// SkipAuthProxy proxies allowlisted requests and skips authentication
908func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
909 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
912// Proxy proxies the user request if the user is authenticated else it prompts
913// them to authenticate
914func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
915 session, err := p.getAuthenticatedSession(rw, req)
916 switch err {
917 case nil:
918 // we are authenticated
919 p.addHeadersForProxying(rw, session)
920 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
921 case ErrNeedsLogin:
922 // we need to send the user to a login screen
923 if isAjax(req) {
924 // no point redirecting an AJAX request
925 p.errorJSON(rw, http.StatusUnauthorized)
926 return
927 }
929 if p.SkipProviderButton {
930 p.OAuthStart(rw, req)
931 } else {
932 p.SignInPage(rw, req, http.StatusForbidden)
933 }
935 case ErrAccessDenied:
936 p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
938 default:
939 // unknown error
940 logger.Errorf("Unexpected internal error: %v", err)
941 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
942 }
945// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
946var noCacheHeaders = map[string]string{
947 "Expires": time.Unix(0, 0).Format(time.RFC1123),
948 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
949 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
952// prepareNoCache prepares headers for preventing browser caching.
953func prepareNoCache(w http.ResponseWriter) {
954 // Set NoCache headers
955 for k, v := range noCacheHeaders {
956 w.Header().Set(k, v)
957 }
960// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
961// redirect clients to once authenticated.
962// This is usually the OAuthProxy callback URL.
963func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
964 // if `p.redirectURL` already has a host, return it
965 if p.redirectURL.Host != "" {
966 return p.redirectURL.String()
967 }
969 // Otherwise figure out the scheme + host from the request
970 rd := *p.redirectURL
971 rd.Host = requestutil.GetRequestHost(req)
972 rd.Scheme = requestutil.GetRequestProto(req)
974 // If CookieSecure is true, return `https` no matter what
975 // Not all reverse proxies set X-Forwarded-Proto
976 if p.CookieSecure {
977 rd.Scheme = schemeHTTPS
978 }
979 return rd.String()
982// getAppRedirect determines the full URL or URI path to redirect clients to
983// once authenticated with the OAuthProxy
984// Strategy priority (first legal result is used):
985// - `rd` querysting parameter
986// - `X-Auth-Request-Redirect` header
987// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
988// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
989// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
990// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
991// - `/`
992func (p *OAuthProxy) getAppRedirect(req *http.Request) (string, error) {
993 err := req.ParseForm()
994 if err != nil {
995 return "", err
996 }
998 // These redirect getter functions are strategies ordered by priority
999 // for figuring out the redirect URL.
1000 type redirectGetter func(req *http.Request) string
1001 for _, rdGetter := range []redirectGetter{
1002 p.getRdQuerystringRedirect,
1003 p.getXAuthRequestRedirect,
1004 p.getXForwardedHeadersRedirect,
1005 p.getURIRedirect,
1006 } {
1007 redirect := rdGetter(req)
1008 // Call `p.IsValidRedirect` again here a final time to be safe
1009 if redirect != "" && p.IsValidRedirect(redirect) {
1010 return redirect, nil
1014 return "/", nil
1017func isForwardedRequest(req *http.Request) bool {
1018 return requestutil.IsProxied(req) &&
1019 req.Host != requestutil.GetRequestHost(req)
1022func (p *OAuthProxy) hasProxyPrefix(path string) bool {
1023 return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
1026func (p *OAuthProxy) validateRedirect(redirect string, errorFormat string) string {
1027 if p.IsValidRedirect(redirect) {
1028 return redirect
1030 if redirect != "" {
1031 logger.Errorf(errorFormat, redirect)
1033 return ""
1036// getRdQuerystringRedirect handles this getAppRedirect strategy:
1037// - `rd` querysting parameter
1038func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
1039 return p.validateRedirect(
1040 req.Form.Get("rd"),
1041 "Invalid redirect provided in rd querystring parameter: %s",
1045// getXAuthRequestRedirect handles this getAppRedirect strategy:
1046// - `X-Auth-Request-Redirect` Header
1047func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
1048 return p.validateRedirect(
1049 req.Header.Get("X-Auth-Request-Redirect"),
1050 "Invalid redirect provided in X-Auth-Request-Redirect header: %s",
1054// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
1055// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
1056// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
1057func (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
1058 if !isForwardedRequest(req) {
1059 return ""
1062 uri := requestutil.GetRequestURI(req)
1063 if p.hasProxyPrefix(uri) {
1064 uri = "/"
1067 redirect := fmt.Sprintf(
1068 "%s://%s%s",
1069 requestutil.GetRequestProto(req),
1070 requestutil.GetRequestHost(req),
1071 uri,
1074 return p.validateRedirect(redirect,
1075 "Invalid redirect generated from X-Forwarded-* headers: %s")
1078// getURIRedirect handles these getAppRedirect strategies:
1079// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
1080// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
1081// - `/`
1082func (p *OAuthProxy) getURIRedirect(req *http.Request) string {
1083 redirect := p.validateRedirect(
1084 requestutil.GetRequestURI(req),
1085 "Invalid redirect generated from X-Forwarded-Uri header: %s",
1087 if redirect == "" {
1088 redirect = req.URL.RequestURI()
1091 if p.hasProxyPrefix(redirect) {
1092 return "/"
1094 return redirect
1097// splitHostPort separates host and port. If the port is not valid, it returns
1098// the entire input as host, and it doesn't check the validity of the host.
1099// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
1100// *** taken from net/url, modified validOptionalPort() to accept ":*"
1101func splitHostPort(hostport string) (host, port string) {
1102 host = hostport
1104 colon := strings.LastIndexByte(host, ':')
1105 if colon != -1 && validOptionalPort(host[colon:]) {
1106 host, port = host[:colon], host[colon+1:]
1109 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
1110 host = host[1 : len(host)-1]
1113 return
1116// validOptionalPort reports whether port is either an empty string
1117// or matches /^:\d*$/
1118// *** taken from net/url, modified to accept ":*"
1119func validOptionalPort(port string) bool {
1120 if port == "" || port == ":*" {
1121 return true
1123 if port[0] != ':' {
1124 return false
1126 for _, b := range port[1:] {
1127 if b < '0' || b > '9' {
1128 return false
1131 return true
1134// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
1135// Returns:
1136// - `nil, ErrNeedsLogin` if user needs to login.
1137// - `nil, ErrAccessDenied` if the authenticated user is not authorized
1138// Set-Cookie headers may be set on the response as a side-effect of calling this method.
1139func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
1140 var session *sessionsapi.SessionState
1142 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1143 session = middlewareapi.GetRequestScope(req).Session
1144 }))
1145 getSession.ServeHTTP(rw, req)
1147 if session == nil {
1148 return nil, ErrNeedsLogin
1151 invalidEmail := session.Email != "" && !p.Validator(session.Email)
1152 authorized, err := p.provider.Authorize(req.Context(), session)
1153 if err != nil {
1154 logger.Errorf("Error with authorization: %v", err)
1157 if invalidEmail || !authorized {
1158 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session: removing session %s", session)
1159 // Invalid session, clear it
1160 err := p.ClearSessionCookie(rw, req)
1161 if err != nil {
1162 logger.Errorf("Error clearing session cookie: %v", err)
1164 return nil, ErrAccessDenied
1167 return session, nil
1170// authOnlyAuthorize handles special authorization logic that is only done
1171// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1173// TODO (@NickMeves): This method is a placeholder to be extended but currently
1174// fails the linter. Remove the nolint when functionality expands.
1176//nolint:gosimple
1177func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1178 // Allow secondary group restrictions based on the `allowed_groups`
1179 // querystring parameter
1180 if !checkAllowedGroups(req, s) {
1181 return false
1184 return true
1187func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1188 allowedGroups := extractAllowedGroups(req)
1189 if len(allowedGroups) == 0 {
1190 return true
1193 for _, group := range s.Groups {
1194 if _, ok := allowedGroups[group]; ok {
1195 return true
1199 return false
1202func extractAllowedGroups(req *http.Request) map[string]struct{} {
1203 groups := map[string]struct{}{}
1205 query := req.URL.Query()
1206 for _, allowedGroups := range query["allowed_groups"] {
1207 for _, group := range strings.Split(allowedGroups, ",") {
1208 if group != "" {
1209 groups[group] = struct{}{}
1214 return groups
1217// addHeadersForProxying adds the appropriate headers the request / response for proxying
1218func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
1219 if session.Email == "" {
1220 rw.Header().Set("GAP-Auth", session.User)
1221 } else {
1222 rw.Header().Set("GAP-Auth", session.Email)
1226// isAjax checks if a request is an ajax request
1227func isAjax(req *http.Request) bool {
1228 acceptValues := req.Header.Values("Accept")
1229 const ajaxReq = applicationJSON
1230 // Iterate over multiple Accept headers, i.e.
1231 // Accept: application/json
1232 // Accept: text/plain
1233 for _, mimeTypes := range acceptValues {
1234 // Iterate over multiple mimetypes in a single header, i.e.
1235 // Accept: application/json, text/plain, */*
1236 for _, mimeType := range strings.Split(mimeTypes, ",") {
1237 mimeType = strings.TrimSpace(mimeType)
1238 if mimeType == ajaxReq {
1239 return true
1243 return false
1246// errorJSON returns the error code with an application/json mime type
1247func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
1248 rw.Header().Set("Content-Type", applicationJSON)
1249 rw.WriteHeader(code)