git.iko.soy/typeable/oauth2-proxy
on 3c06739 help sign in
typeable/oauth2-proxy/oauthproxy.go
1298 lines 42 kB history raw
1package main
3import (
4 "context"
5 "embed"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "net"
11 "net/http"
12 "net/url"
13 "os"
14 "os/signal"
15 "regexp"
16 "strings"
17 "syscall"
18 "time"
20 "github.com/gorilla/mux"
21 "github.com/justinas/alice"
22 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
23 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
25 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/redirect"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
30 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
31 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
32 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
34 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
35 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
36 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
37 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
38 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
39 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
40 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
43const (
44 schemeHTTP = "http"
45 schemeHTTPS = "https"
46 applicationJSON = "application/json"
48 robotsPath = "/robots.txt"
49 signInPath = "/sign_in"
50 signOutPath = "/sign_out"
51 oauthStartPath = "/start"
52 oauthCallbackPath = "/callback"
53 authOnlyPath = "/auth"
54 userInfoPath = "/userinfo"
55 staticPathPrefix = "/static/"
58var (
59 // ErrNeedsLogin means the user should be redirected to the login page
60 ErrNeedsLogin = errors.New("redirect to login page")
62 // ErrAccessDenied means the user should receive a 401 Unauthorized response
63 ErrAccessDenied = errors.New("access denied")
65 //go:embed static/*
66 staticFiles embed.FS
69// allowedRoute manages method + path based allowlists
70type allowedRoute struct {
71 method string
72 negate bool
73 pathRegex *regexp.Regexp
76type apiRoute struct {
77 pathRegex *regexp.Regexp
80// OAuthProxy is the main authentication proxy
81type OAuthProxy struct {
82 CookieOptions *options.Cookie
83 Validator func(string) bool
85 SignInPath string
87 allowedRoutes []allowedRoute
88 apiRoutes []apiRoute
89 redirectURL *url.URL // the url to receive requests at
90 relativeRedirectURL bool
91 whitelistDomains []string
92 provider providers.Provider
93 sessionStore sessionsapi.SessionStore
94 ProxyPrefix string
95 basicAuthValidator basic.Validator
96 basicAuthGroups []string
97 SkipProviderButton bool
98 skipAuthPreflight bool
99 skipJwtBearerTokens bool
100 forceJSONErrors bool
101 allowQuerySemicolons 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
110 upstreamProxy http.Handler
111 serveMux *mux.Router
112 redirectValidator redirect.Validator
113 appDirector redirect.AppDirector
115 encodeState bool
118// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
119func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
120 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
121 if err != nil {
122 return nil, fmt.Errorf("error initialising session store: %v", err)
123 }
125 var basicAuthValidator basic.Validator
126 if opts.HtpasswdFile != "" {
127 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
128 var err error
129 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
130 if err != nil {
131 return nil, fmt.Errorf("could not validate htpasswd: %v", err)
132 }
133 }
135 provider, err := providers.NewProvider(opts.Providers[0])
136 if err != nil {
137 return nil, fmt.Errorf("error initialising provider: %v", err)
138 }
140 pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
141 TemplatesPath: opts.Templates.Path,
142 CustomLogo: opts.Templates.CustomLogo,
143 ProxyPrefix: opts.ProxyPrefix,
144 Footer: opts.Templates.Footer,
145 Version: VERSION,
146 Debug: opts.Templates.Debug,
147 ProviderName: buildProviderName(provider, opts.Providers[0].Name),
148 SignInMessage: buildSignInMessage(opts),
149 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
150 })
151 if err != nil {
152 return nil, fmt.Errorf("error initialising page writer: %v", err)
153 }
155 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter)
156 if err != nil {
157 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
158 }
160 if opts.SkipJwtBearerTokens {
161 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL)
162 for _, issuer := range opts.ExtraJwtIssuers {
163 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
164 }
165 }
166 redirectURL := opts.GetRedirectURL()
167 if redirectURL.Path == "" {
168 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
169 }
171 logger.Printf("OAuthProxy configured for %s Client ID: %s", provider.Data().ProviderName, opts.Providers[0].ClientID)
172 refresh := "disabled"
173 if opts.Cookie.Refresh != time.Duration(0) {
174 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
175 }
177 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)
179 trustedIPs := ip.NewNetSet()
180 for _, ipStr := range opts.TrustedIPs {
181 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
182 trustedIPs.AddIPNet(*ipNet)
183 } else {
184 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
185 }
186 }
188 allowedRoutes, err := buildRoutesAllowlist(opts)
189 if err != nil {
190 return nil, err
191 }
193 apiRoutes, err := buildAPIRoutes(opts)
194 if err != nil {
195 return nil, err
196 }
198 preAuthChain, err := buildPreAuthChain(opts, sessionStore)
199 if err != nil {
200 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
201 }
202 sessionChain := buildSessionChain(opts, provider, sessionStore, basicAuthValidator)
203 headersChain, err := buildHeadersChain(opts)
204 if err != nil {
205 return nil, fmt.Errorf("could not build headers chain: %v", err)
206 }
208 redirectValidator := redirect.NewValidator(opts.WhitelistDomains)
209 appDirector := redirect.NewAppDirector(redirect.AppDirectorOpts{
210 ProxyPrefix: opts.ProxyPrefix,
211 Validator: redirectValidator,
212 })
214 p := &OAuthProxy{
215 CookieOptions: &opts.Cookie,
216 Validator: validator,
218 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
220 ProxyPrefix: opts.ProxyPrefix,
221 provider: provider,
222 sessionStore: sessionStore,
223 redirectURL: redirectURL,
224 relativeRedirectURL: opts.RelativeRedirectURL,
225 apiRoutes: apiRoutes,
226 allowedRoutes: allowedRoutes,
227 whitelistDomains: opts.WhitelistDomains,
228 skipAuthPreflight: opts.SkipAuthPreflight,
229 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
230 realClientIPParser: opts.GetRealClientIPParser(),
231 SkipProviderButton: opts.SkipProviderButton,
232 forceJSONErrors: opts.ForceJSONErrors,
233 allowQuerySemicolons: opts.AllowQuerySemicolons,
234 trustedIPs: trustedIPs,
236 basicAuthValidator: basicAuthValidator,
237 basicAuthGroups: opts.HtpasswdUserGroups,
238 sessionChain: sessionChain,
239 headersChain: headersChain,
240 preAuthChain: preAuthChain,
241 pageWriter: pageWriter,
242 upstreamProxy: upstreamProxy,
243 redirectValidator: redirectValidator,
244 appDirector: appDirector,
245 encodeState: opts.EncodeState,
246 }
247 p.buildServeMux(opts.ProxyPrefix)
249 if err := p.setupServer(opts); err != nil {
250 return nil, fmt.Errorf("error setting up server: %v", err)
251 }
253 return p, nil
256func (p *OAuthProxy) Start() error {
257 if p.server == nil {
258 // We have to call setupServer before Start is called.
259 // If this doesn't happen it's a programming error.
260 panic("server has not been initialised")
261 }
263 ctx, cancel := context.WithCancel(context.Background())
265 // Observe signals in background goroutine.
266 go func() {
267 sigint := make(chan os.Signal, 1)
268 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
269 <-sigint
270 cancel() // cancel the context
271 }()
273 return p.server.Start(ctx)
276func (p *OAuthProxy) setupServer(opts *options.Options) error {
277 serverOpts := proxyhttp.Opts{
278 Handler: p,
279 BindAddress: opts.Server.BindAddress,
280 SecureBindAddress: opts.Server.SecureBindAddress,
281 TLS: opts.Server.TLS,
282 }
284 // Option: AllowQuerySemicolons
285 if opts.AllowQuerySemicolons {
286 serverOpts.Handler = http.AllowQuerySemicolons(serverOpts.Handler)
287 }
289 appServer, err := proxyhttp.NewServer(serverOpts)
290 if err != nil {
291 return fmt.Errorf("could not build app server: %v", err)
292 }
294 metricsServer, err := proxyhttp.NewServer(proxyhttp.Opts{
295 Handler: middleware.DefaultMetricsHandler,
296 BindAddress: opts.MetricsServer.BindAddress,
297 SecureBindAddress: opts.MetricsServer.SecureBindAddress,
298 TLS: opts.MetricsServer.TLS,
299 })
300 if err != nil {
301 return fmt.Errorf("could not build metrics server: %v", err)
302 }
304 p.server = proxyhttp.NewServerGroup(appServer, metricsServer)
305 return nil
308func (p *OAuthProxy) buildServeMux(proxyPrefix string) {
309 // Use the encoded path here so we can have the option to pass it on in the upstream mux.
310 // Otherwise something like /%2F/ would be redirected to / here already.
311 r := mux.NewRouter().UseEncodedPath()
312 // Everything served by the router must go through the preAuthChain first.
313 r.Use(p.preAuthChain.Then)
315 // Register the robots path writer
316 r.Path(robotsPath).HandlerFunc(p.pageWriter.WriteRobotsTxt)
318 // The authonly path should be registered separately to prevent it from getting no-cache headers.
319 // We do this to allow users to have a short cache (via nginx) of the response to reduce the
320 // likelihood of multiple requests trying to refresh sessions simultaneously.
321 r.Path(proxyPrefix + authOnlyPath).Handler(p.sessionChain.ThenFunc(p.AuthOnly))
323 // This will register all of the paths under the proxy prefix, except the auth only path so that no cache headers
324 // are not applied.
325 p.buildProxySubrouter(r.PathPrefix(proxyPrefix).Subrouter())
327 // Register serveHTTP last so it catches anything that isn't already caught earlier.
328 // Anything that got to this point needs to have a session loaded.
329 r.PathPrefix("/").Handler(p.sessionChain.ThenFunc(p.Proxy))
330 p.serveMux = r
333func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
334 s.Use(prepareNoCacheMiddleware)
336 s.Path(signInPath).HandlerFunc(p.SignIn)
337 s.Path(oauthStartPath).HandlerFunc(p.OAuthStart)
338 s.Path(oauthCallbackPath).HandlerFunc(p.OAuthCallback)
340 // Static file paths
341 s.PathPrefix(staticPathPrefix).Handler(http.StripPrefix(p.ProxyPrefix, http.FileServer(http.FS(staticFiles))))
343 // The userinfo and logout endpoints needs to load sessions before handling the request
344 s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo))
345 s.Path(signOutPath).Handler(p.sessionChain.ThenFunc(p.SignOut))
348// buildPreAuthChain constructs a chain that should process every request before
349// the OAuth2 Proxy authentication logic kicks in.
350// For example forcing HTTPS or health checks.
351func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) {
352 chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
354 if opts.ForceHTTPS {
355 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
356 if err != nil {
357 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err)
358 }
359 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
360 }
362 healthCheckPaths := []string{opts.PingPath}
363 healthCheckUserAgents := []string{opts.PingUserAgent}
364 if opts.GCPHealthChecks {
365 logger.Printf("WARNING: GCP HealthChecks are now deprecated: Reconfigure apps to use the ping path for liveness and readiness checks, set the ping user agent to \"GoogleHC/1.0\" to preserve existing behaviour")
366 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
367 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
368 }
370 // To silence logging of health checks, register the health check handler before
371 // the logging handler
372 if opts.Logging.SilencePing {
373 chain = chain.Append(
374 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
375 middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
376 middleware.NewRequestLogger(),
377 )
378 } else {
379 chain = chain.Append(
380 middleware.NewRequestLogger(),
381 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
382 middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
383 )
384 }
386 chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
388 return chain, nil
391func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
392 chain := alice.New()
394 if opts.SkipJwtBearerTokens {
395 sessionLoaders := []middlewareapi.TokenToSessionFunc{
396 provider.CreateSessionFromToken,
397 }
399 for _, verifier := range opts.GetJWTBearerVerifiers() {
400 sessionLoaders = append(sessionLoaders,
401 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
402 }
404 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
405 }
407 if validator != nil {
408 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups, opts.LegacyPreferEmailToUser))
409 }
411 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
412 SessionStore: sessionStore,
413 RefreshPeriod: opts.Cookie.Refresh,
414 RefreshSession: provider.RefreshSession,
415 ValidateSession: provider.ValidateSession,
416 }))
418 return chain
421func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
422 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
423 if err != nil {
424 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
425 }
427 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
428 if err != nil {
429 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
430 }
432 return alice.New(requestInjector, responseInjector), nil
435func buildSignInMessage(opts *options.Options) string {
436 var msg string
437 if len(opts.Templates.Banner) >= 1 {
438 if opts.Templates.Banner == "-" {
439 msg = ""
440 } else {
441 msg = opts.Templates.Banner
442 }
443 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
444 if len(opts.EmailDomains) > 1 {
445 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
446 } else if opts.EmailDomains[0] != "*" {
447 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
448 }
449 }
450 return msg
453func buildProviderName(p providers.Provider, override string) string {
454 if override != "" {
455 return override
456 }
457 return p.Data().ProviderName
460// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
461// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
462// (method=path support)
463func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
464 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
466 for _, path := range opts.SkipAuthRegex {
467 compiledRegex, err := regexp.Compile(path)
468 if err != nil {
469 return nil, err
470 }
471 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
472 routes = append(routes, allowedRoute{
473 method: "",
474 pathRegex: compiledRegex,
475 })
476 }
478 for _, methodPath := range opts.SkipAuthRoutes {
479 var (
480 method string
481 path string
482 negate = strings.Contains(methodPath, "!=")
483 )
485 parts := regexp.MustCompile("!?=").Split(methodPath, 2)
486 if len(parts) == 1 {
487 method = ""
488 path = parts[0]
489 } else {
490 method = strings.ToUpper(parts[0])
491 path = parts[1]
492 }
494 compiledRegex, err := regexp.Compile(path)
495 if err != nil {
496 return nil, err
497 }
498 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
499 routes = append(routes, allowedRoute{
500 method: method,
501 negate: negate,
502 pathRegex: compiledRegex,
503 })
504 }
506 return routes, nil
509// buildAPIRoutes builds an []apiRoute from ApiRoutes option
510func buildAPIRoutes(opts *options.Options) ([]apiRoute, error) {
511 routes := make([]apiRoute, 0, len(opts.APIRoutes))
513 for _, path := range opts.APIRoutes {
514 compiledRegex, err := regexp.Compile(path)
515 if err != nil {
516 return nil, err
517 }
518 logger.Printf("API route - Path: %s", path)
519 routes = append(routes, apiRoute{
520 pathRegex: compiledRegex,
521 })
522 }
524 return routes, nil
527// ClearSessionCookie creates a cookie to unset the user's authentication cookie
528// stored in the user's session
529func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
530 return p.sessionStore.Clear(rw, req)
533// LoadCookiedSession reads the user's authentication details from the request
534func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
535 return p.sessionStore.Load(req)
538// SaveSession creates a new session cookie value and sets this on the response
539func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
540 return p.sessionStore.Save(rw, req, s)
543func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
544 p.serveMux.ServeHTTP(rw, req)
547// ErrorPage writes an error response
548func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
549 redirectURL, err := p.appDirector.GetRedirect(req)
550 if err != nil {
551 logger.Errorf("Error obtaining redirect: %v", err)
552 }
553 if redirectURL == p.SignInPath || redirectURL == "" {
554 redirectURL = "/"
555 }
557 scope := middlewareapi.GetRequestScope(req)
558 p.pageWriter.WriteErrorPage(rw, pagewriter.ErrorPageOpts{
559 Status: code,
560 RedirectURL: redirectURL,
561 RequestID: scope.RequestID,
562 AppError: appError,
563 Messages: messages,
564 })
567// IsAllowedRequest is used to check if auth should be skipped for this request
568func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
569 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
570 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
573func isAllowedMethod(req *http.Request, route allowedRoute) bool {
574 return route.method == "" || req.Method == route.method
577func isAllowedPath(req *http.Request, route allowedRoute) bool {
578 matches := route.pathRegex.MatchString(requestutil.GetRequestURI(req))
580 if route.negate {
581 return !matches
582 }
584 return matches
587// IsAllowedRoute is used to check if the request method & path is allowed without auth
588func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
589 for _, route := range p.allowedRoutes {
590 if isAllowedMethod(req, route) && isAllowedPath(req, route) {
591 return true
592 }
593 }
594 return false
597func (p *OAuthProxy) isAPIPath(req *http.Request) bool {
598 for _, route := range p.apiRoutes {
599 if route.pathRegex.MatchString(requestutil.GetRequestURI(req)) {
600 return true
601 }
602 }
603 return false
606// isTrustedIP is used to check if a request comes from a trusted client IP address.
607func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
608 if p.trustedIPs == nil {
609 return false
610 }
612 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
613 if err != nil {
614 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
615 // Possibly spoofed X-Real-IP header
616 return false
617 }
619 if remoteAddr == nil {
620 return false
621 }
623 return p.trustedIPs.Has(remoteAddr)
626// SignInPage writes the sign in template to the response
627func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
628 prepareNoCache(rw)
629 err := p.ClearSessionCookie(rw, req)
630 if err != nil {
631 logger.Printf("Error clearing session cookie: %v", err)
632 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
633 return
634 }
635 rw.WriteHeader(code)
637 redirectURL, err := p.appDirector.GetRedirect(req)
638 if err != nil {
639 logger.Errorf("Error obtaining redirect: %v", err)
640 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
641 return
642 }
644 if redirectURL == p.SignInPath {
645 redirectURL = "/"
646 }
648 p.pageWriter.WriteSignInPage(rw, req, redirectURL, code)
651// ManualSignIn handles basic auth logins to the proxy
652func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool, int) {
653 if req.Method != "POST" || p.basicAuthValidator == nil {
654 return "", false, http.StatusOK
655 }
656 user := req.FormValue("username")
657 passwd := req.FormValue("password")
658 if user == "" {
659 return "", false, http.StatusBadRequest
660 }
661 // check auth
662 if p.basicAuthValidator.Validate(user, passwd) {
663 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
664 return user, true, http.StatusOK
665 }
666 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
667 return "", false, http.StatusUnauthorized
670// SignIn serves a page prompting users to sign in
671func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
672 redirect, err := p.appDirector.GetRedirect(req)
673 if err != nil {
674 logger.Errorf("Error obtaining redirect: %v", err)
675 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
676 return
677 }
679 user, ok, statusCode := p.ManualSignIn(req)
680 if ok {
681 session := &sessionsapi.SessionState{User: user, Groups: p.basicAuthGroups}
682 err = p.SaveSession(rw, req, session)
683 if err != nil {
684 logger.Printf("Error saving session: %v", err)
685 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
686 return
687 }
688 http.Redirect(rw, req, redirect, http.StatusFound)
689 } else {
690 if p.SkipProviderButton {
691 p.OAuthStart(rw, req)
692 } else {
693 // TODO - should we pass on /oauth2/sign_in query params to /oauth2/start?
694 p.SignInPage(rw, req, statusCode)
695 }
696 }
699// UserInfo endpoint outputs session email and preferred username in JSON format
700func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
701 session, err := p.getAuthenticatedSession(rw, req)
702 if err != nil {
703 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
704 return
705 }
707 rw.Header().Set("Content-Type", "application/json")
708 rw.WriteHeader(http.StatusOK)
709 if session == nil {
710 if _, err := rw.Write([]byte("{}")); err != nil {
711 logger.Printf("Error encoding empty user info: %v", err)
712 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
713 }
714 return
715 }
717 userInfo := struct {
718 User string `json:"user"`
719 Email string `json:"email"`
720 Groups []string `json:"groups,omitempty"`
721 PreferredUsername string `json:"preferredUsername,omitempty"`
722 }{
723 User: session.User,
724 Email: session.Email,
725 Groups: session.Groups,
726 PreferredUsername: session.PreferredUsername,
727 }
729 if err := json.NewEncoder(rw).Encode(userInfo); err != nil {
730 logger.Printf("Error encoding user info: %v", err)
731 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
732 }
735// SignOut sends a response to clear the authentication cookie
736func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
737 redirect, err := p.appDirector.GetRedirect(req)
738 if err != nil {
739 logger.Errorf("Error obtaining redirect: %v", err)
740 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
741 return
742 }
743 err = p.ClearSessionCookie(rw, req)
744 if err != nil {
745 logger.Errorf("Error clearing session cookie: %v", err)
746 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
747 return
748 }
750 p.backendLogout(rw, req)
752 http.Redirect(rw, req, redirect, http.StatusFound)
755func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request) {
756 session, err := p.getAuthenticatedSession(rw, req)
757 if err != nil {
758 logger.Errorf("error getting authenticated session during backend logout: %v", err)
759 return
760 }
762 if session == nil {
763 return
764 }
766 providerData := p.provider.Data()
767 if providerData.BackendLogoutURL == "" {
768 return
769 }
771 backendLogoutURL := strings.ReplaceAll(providerData.BackendLogoutURL, "{id_token}", session.IDToken)
772 // security exception because URL is dynamic ({id_token} replacement) but
773 // base is not end-user provided but comes from configuration somewhat secure
774 resp, err := http.Get(backendLogoutURL) // #nosec G107
775 if err != nil {
776 logger.Errorf("error while calling backend logout: %v", err)
777 return
778 }
780 defer resp.Body.Close()
781 if resp.StatusCode != 200 {
782 logger.Errorf("error while calling backend logout url, returned error code %v", resp.StatusCode)
783 }
786// OAuthStart starts the OAuth2 authentication flow
787func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
788 // start the flow permitting login URL query parameters to be overridden from the request URL
789 p.doOAuthStart(rw, req, req.URL.Query())
792func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, overrides url.Values) {
793 extraParams := p.provider.Data().LoginURLParams(overrides)
794 prepareNoCache(rw)
796 var (
797 err error
798 codeChallenge, codeVerifier, codeChallengeMethod string
799 )
800 if p.provider.Data().CodeChallengeMethod != "" {
801 codeChallengeMethod = p.provider.Data().CodeChallengeMethod
802 codeVerifier, err = encryption.GenerateRandomASCIIString(96)
803 if err != nil {
804 logger.Errorf("Unable to build random ASCII string for code verifier: %v", err)
805 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
806 return
807 }
809 codeChallenge, err = encryption.GenerateCodeChallenge(p.provider.Data().CodeChallengeMethod, codeVerifier)
810 if err != nil {
811 logger.Errorf("Error creating code challenge: %v", err)
812 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
813 return
814 }
816 extraParams.Add("code_challenge", codeChallenge)
817 extraParams.Add("code_challenge_method", codeChallengeMethod)
818 }
820 csrf, err := cookies.NewCSRF(p.CookieOptions, codeVerifier)
821 if err != nil {
822 logger.Errorf("Error creating CSRF nonce: %v", err)
823 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
824 return
825 }
827 appRedirect, err := p.appDirector.GetRedirect(req)
828 if err != nil {
829 logger.Errorf("Error obtaining application redirect: %v", err)
830 p.ErrorPage(rw, req, http.StatusBadRequest, err.Error())
831 return
832 }
834 callbackRedirect := p.getOAuthRedirectURI(req)
835 loginURL := p.provider.GetLoginURL(
836 callbackRedirect,
837 encodeState(csrf.HashOAuthState(), appRedirect, p.encodeState),
838 csrf.HashOIDCNonce(),
839 extraParams,
840 )
842 if _, err := csrf.SetCookie(rw, req); err != nil {
843 logger.Errorf("Error setting CSRF cookie: %v", err)
844 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
845 return
846 }
848 http.Redirect(rw, req, loginURL, http.StatusFound)
851// OAuthCallback is the OAuth2 authentication flow callback that finishes the
852// OAuth2 authentication flow
853func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
854 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
856 // finish the oauth cycle
857 err := req.ParseForm()
858 if err != nil {
859 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
860 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
861 return
862 }
863 errorString := req.Form.Get("error")
864 if errorString != "" {
865 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
866 message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
867 // Set the debug message and override the non debug message to be the same for this case
868 p.ErrorPage(rw, req, http.StatusForbidden, message, message)
869 return
870 }
872 csrf, err := cookies.LoadCSRFCookie(req, p.CookieOptions)
873 if err != nil {
874 logger.Println(req, logger.AuthFailure, "Invalid authentication via OAuth2. Error while loading CSRF cookie:", err.Error())
875 p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
876 return
877 }
879 session, err := p.redeemCode(req, csrf.GetCodeVerifier())
880 if err != nil {
881 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
882 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
883 return
884 }
886 err = p.enrichSessionState(req.Context(), session)
887 if err != nil {
888 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
889 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
890 return
891 }
893 csrf.ClearCookie(rw, req)
895 nonce, appRedirect, err := decodeState(req.Form.Get("state"), p.encodeState)
896 if err != nil {
897 logger.Errorf("Error while parsing OAuth2 state: %v", err)
898 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
899 return
900 }
902 if !csrf.CheckOAuthState(nonce) {
903 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
904 p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
905 return
906 }
908 csrf.SetSessionNonce(session)
909 if !p.provider.ValidateSession(req.Context(), session) {
910 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Session validation failed: %s", session)
911 p.ErrorPage(rw, req, http.StatusForbidden, "Session validation failed")
912 return
913 }
915 if !p.redirectValidator.IsValidRedirect(appRedirect) {
916 appRedirect = "/"
917 }
919 // set cookie, or deny
920 authorized, err := p.provider.Authorize(req.Context(), session)
921 if err != nil {
922 logger.Errorf("Error with authorization: %v", err)
923 }
924 if p.Validator(session.Email) && authorized {
925 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
926 err := p.SaveSession(rw, req, session)
927 if err != nil {
928 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
929 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
930 return
931 }
932 http.Redirect(rw, req, appRedirect, http.StatusFound)
933 } else {
934 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
935 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
936 }
939func (p *OAuthProxy) redeemCode(req *http.Request, codeVerifier string) (*sessionsapi.SessionState, error) {
940 code := req.Form.Get("code")
941 if code == "" {
942 return nil, providers.ErrMissingCode
943 }
945 redirectURI := p.getOAuthRedirectURI(req)
946 s, err := p.provider.Redeem(req.Context(), redirectURI, code, codeVerifier)
947 if err != nil {
948 return nil, err
949 }
951 // Force setting these in case the Provider didn't
952 if s.CreatedAt == nil {
953 s.CreatedAtNow()
954 }
955 if s.ExpiresOn == nil {
956 s.ExpiresIn(p.CookieOptions.Expire)
957 }
959 return s, nil
962func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
963 var err error
964 if s.Email == "" {
965 // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
966 // nolint:staticcheck
967 s.Email, err = p.provider.GetEmailAddress(ctx, s)
968 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
969 return err
970 }
971 }
973 return p.provider.EnrichSession(ctx, s)
976// AuthOnly checks whether the user is currently logged in (both authentication
977// and optional authorization).
978func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
979 session, err := p.getAuthenticatedSession(rw, req)
980 if err != nil {
981 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
982 return
983 }
985 // Unauthorized cases need to return 403 to prevent infinite redirects with
986 // subrequest architectures
987 if !authOnlyAuthorize(req, session) {
988 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
989 return
990 }
992 // we are authenticated
993 p.addHeadersForProxying(rw, session)
994 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
995 rw.WriteHeader(http.StatusAccepted)
996 })).ServeHTTP(rw, req)
999// Proxy proxies the user request if the user is authenticated else it prompts
1000// them to authenticate
1001func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
1002 session, err := p.getAuthenticatedSession(rw, req)
1003 switch err {
1004 case nil:
1005 // we are authenticated
1006 p.addHeadersForProxying(rw, session)
1007 p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
1008 case ErrNeedsLogin:
1009 // we need to send the user to a login screen
1010 if p.forceJSONErrors || isAjax(req) || p.isAPIPath(req) {
1011 logger.Printf("No valid authentication in request. Access Denied.")
1012 // no point redirecting an AJAX request
1013 p.errorJSON(rw, http.StatusUnauthorized)
1014 return
1017 logger.Printf("No valid authentication in request. Initiating login.")
1018 if p.SkipProviderButton {
1019 // start OAuth flow, but only with the default login URL params - do not
1020 // consider this request's query params as potential overrides, since
1021 // the user did not explicitly start the login flow
1022 p.doOAuthStart(rw, req, nil)
1023 } else {
1024 p.SignInPage(rw, req, http.StatusForbidden)
1027 case ErrAccessDenied:
1028 if p.forceJSONErrors {
1029 p.errorJSON(rw, http.StatusForbidden)
1030 } else {
1031 p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
1034 default:
1035 // unknown error
1036 logger.Errorf("Unexpected internal error: %v", err)
1037 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
1041// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
1042var noCacheHeaders = map[string]string{
1043 "Expires": time.Unix(0, 0).Format(time.RFC1123),
1044 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
1045 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
1048// prepareNoCache prepares headers for preventing browser caching.
1049func prepareNoCache(w http.ResponseWriter) {
1050 // Set NoCache headers
1051 for k, v := range noCacheHeaders {
1052 w.Header().Set(k, v)
1056func prepareNoCacheMiddleware(next http.Handler) http.Handler {
1057 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1058 prepareNoCache(rw)
1059 next.ServeHTTP(rw, req)
1060 })
1063// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
1064// redirect clients to once authenticated.
1065// This is usually the OAuthProxy callback URL.
1066func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
1067 // if `p.redirectURL` already has a host, return it
1068 if p.relativeRedirectURL || p.redirectURL.Host != "" {
1069 return p.redirectURL.String()
1072 // Otherwise figure out the scheme + host from the request
1073 rd := *p.redirectURL
1074 rd.Host = requestutil.GetRequestHost(req)
1075 rd.Scheme = requestutil.GetRequestProto(req)
1077 // If there's no scheme in the request, we should still include one
1078 if rd.Scheme == "" {
1079 rd.Scheme = schemeHTTP
1082 // If CookieSecure is true, return `https` no matter what
1083 // Not all reverse proxies set X-Forwarded-Proto
1084 if p.CookieOptions.Secure {
1085 rd.Scheme = schemeHTTPS
1087 return rd.String()
1090// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
1091// Returns:
1092// - `nil, ErrNeedsLogin` if user needs to login.
1093// - `nil, ErrAccessDenied` if the authenticated user is not authorized
1094// Set-Cookie headers may be set on the response as a side-effect of calling this method.
1095func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
1096 session := middlewareapi.GetRequestScope(req).Session
1098 // Check this after loading the session so that if a valid session exists, we can add headers from it
1099 if p.IsAllowedRequest(req) {
1100 return session, nil
1103 if session == nil {
1104 return nil, ErrNeedsLogin
1107 invalidEmail := session.Email != "" && !p.Validator(session.Email)
1108 authorized, err := p.provider.Authorize(req.Context(), session)
1109 if err != nil {
1110 logger.Errorf("Error with authorization: %v", err)
1113 if invalidEmail || !authorized {
1114 cause := "unauthorized"
1115 if invalidEmail {
1116 cause = "invalid email"
1119 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session (%s): removing session %s", cause, session)
1120 // Invalid session, clear it
1121 err := p.ClearSessionCookie(rw, req)
1122 if err != nil {
1123 logger.Errorf("Error clearing session cookie: %v", err)
1125 return nil, ErrAccessDenied
1128 return session, nil
1131// authOnlyAuthorize handles special authorization logic that is only done
1132// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1133func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1134 // Allow requests previously allowed to be bypassed
1135 if s == nil {
1136 return true
1139 constraints := []func(*http.Request, *sessionsapi.SessionState) bool{
1140 checkAllowedGroups,
1141 checkAllowedEmailDomains,
1142 checkAllowedEmails,
1145 for _, constraint := range constraints {
1146 if !constraint(req, s) {
1147 return false
1151 return true
1154// extractAllowedEntities aims to extract and split allowed entities linked by a key,
1155// from an HTTP request query. Output is a map[string]struct{} where keys are valuable,
1156// the goal is to avoid time complexity O(N^2) while finding matches during membership checks.
1157func extractAllowedEntities(req *http.Request, key string) map[string]struct{} {
1158 entities := map[string]struct{}{}
1160 query := req.URL.Query()
1161 for _, allowedEntities := range query[key] {
1162 for _, entity := range strings.Split(allowedEntities, ",") {
1163 if entity != "" {
1164 entities[entity] = struct{}{}
1169 return entities
1172// checkAllowedEmailDomains allow email domain restrictions based on the `allowed_email_domains`
1173// querystring parameter
1174func checkAllowedEmailDomains(req *http.Request, s *sessionsapi.SessionState) bool {
1175 allowedEmailDomains := extractAllowedEntities(req, "allowed_email_domains")
1176 if len(allowedEmailDomains) == 0 {
1177 return true
1180 splitEmail := strings.Split(s.Email, "@")
1181 if len(splitEmail) != 2 {
1182 return false
1185 endpoint, _ := url.Parse("")
1186 endpoint.Host = splitEmail[1]
1188 allowedEmailDomainsList := []string{}
1189 for ed := range allowedEmailDomains {
1190 allowedEmailDomainsList = append(allowedEmailDomainsList, ed)
1193 return util.IsEndpointAllowed(endpoint, allowedEmailDomainsList)
1196// checkAllowedGroups allow secondary group restrictions based on the `allowed_groups`
1197// querystring parameter
1198func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1199 allowedGroups := extractAllowedEntities(req, "allowed_groups")
1200 if len(allowedGroups) == 0 {
1201 return true
1204 for _, group := range s.Groups {
1205 if _, ok := allowedGroups[group]; ok {
1206 return true
1210 return false
1213// checkAllowedEmails allow email restrictions based on the `allowed_emails`
1214// querystring parameter
1215func checkAllowedEmails(req *http.Request, s *sessionsapi.SessionState) bool {
1216 allowedEmails := extractAllowedEntities(req, "allowed_emails")
1217 if len(allowedEmails) == 0 {
1218 return true
1221 allowed := false
1223 for email := range allowedEmails {
1224 if email == s.Email {
1225 allowed = true
1226 break
1230 return allowed
1233// encodedState builds the OAuth state param out of our nonce and
1234// original application redirect
1235func encodeState(nonce string, redirect string, encode bool) string {
1236 rawString := fmt.Sprintf("%v:%v", nonce, redirect)
1237 if encode {
1238 return base64.RawURLEncoding.EncodeToString([]byte(rawString))
1240 return rawString
1243// decodeState splits the reflected OAuth state response back into
1244// the nonce and original application redirect
1245func decodeState(state string, encode bool) (string, string, error) {
1246 toParse := state
1247 if encode {
1248 decoded, _ := base64.RawURLEncoding.DecodeString(state)
1249 toParse = string(decoded)
1252 parsedState := strings.SplitN(toParse, ":", 2)
1253 if len(parsedState) != 2 {
1254 return "", "", errors.New("invalid length")
1256 return parsedState[0], parsedState[1], nil
1259// addHeadersForProxying adds the appropriate headers the request / response for proxying
1260func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
1261 if session == nil {
1262 return
1264 if session.Email == "" {
1265 rw.Header().Set("GAP-Auth", session.User)
1266 } else {
1267 rw.Header().Set("GAP-Auth", session.Email)
1271// isAjax checks if a request is an ajax request
1272func isAjax(req *http.Request) bool {
1273 acceptValues := req.Header.Values("Accept")
1274 const ajaxReq = applicationJSON
1275 // Iterate over multiple Accept headers, i.e.
1276 // Accept: application/json
1277 // Accept: text/plain
1278 for _, mimeTypes := range acceptValues {
1279 // Iterate over multiple mimetypes in a single header, i.e.
1280 // Accept: application/json, text/plain, */*
1281 for _, mimeType := range strings.Split(mimeTypes, ",") {
1282 mimeType = strings.TrimSpace(mimeType)
1283 if mimeType == ajaxReq {
1284 return true
1288 return false
1291// errorJSON returns the error code with an application/json mime type
1292func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
1293 rw.Header().Set("Content-Type", applicationJSON)
1294 rw.WriteHeader(code)
1295 // we need to send some JSON response because we set the Content-Type to
1296 // application/json
1297 rw.Write([]byte("{}"))