git.iko.soy/typeable/oauth2-proxy
on ea261ca help sign in
typeable/oauth2-proxy/oauthproxy.go
1070 lines 35 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/gorilla/mux"
19 "github.com/justinas/alice"
20 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
21 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
22 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
23 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/redirect"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
28 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
30 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
31 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
32 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
33 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
34 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
35 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
36 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
39const (
40 schemeHTTP = "http"
41 schemeHTTPS = "https"
42 applicationJSON = "application/json"
44 robotsPath = "/robots.txt"
45 signInPath = "/sign_in"
46 signOutPath = "/sign_out"
47 oauthStartPath = "/start"
48 oauthCallbackPath = "/callback"
49 authOnlyPath = "/auth"
50 userInfoPath = "/userinfo"
53var (
54 // ErrNeedsLogin means the user should be redirected to the login page
55 ErrNeedsLogin = errors.New("redirect to login page")
57 // ErrAccessDenied means the user should receive a 401 Unauthorized response
58 ErrAccessDenied = errors.New("access denied")
61// allowedRoute manages method + path based allowlists
62type allowedRoute struct {
63 method string
64 pathRegex *regexp.Regexp
67// OAuthProxy is the main authentication proxy
68type OAuthProxy struct {
69 CookieOptions *options.Cookie
70 Validator func(string) bool
72 SignInPath string
74 allowedRoutes []allowedRoute
75 redirectURL *url.URL // the url to receive requests at
76 whitelistDomains []string
77 provider providers.Provider
78 sessionStore sessionsapi.SessionStore
79 ProxyPrefix string
80 basicAuthValidator basic.Validator
81 basicAuthGroups []string
82 SkipProviderButton bool
83 skipAuthPreflight bool
84 skipJwtBearerTokens bool
85 forceJSONErrors bool
86 realClientIPParser ipapi.RealClientIPParser
87 trustedIPs *ip.NetSet
89 sessionChain alice.Chain
90 headersChain alice.Chain
91 preAuthChain alice.Chain
92 pageWriter pagewriter.Writer
93 server proxyhttp.Server
94 upstreamProxy http.Handler
95 serveMux *mux.Router
96 redirectValidator redirect.Validator
97 appDirector redirect.AppDirector
100// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
101func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
102 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
103 if err != nil {
104 return nil, fmt.Errorf("error initialising session store: %v", err)
105 }
107 var basicAuthValidator basic.Validator
108 if opts.HtpasswdFile != "" {
109 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
110 var err error
111 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
112 if err != nil {
113 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
114 }
115 }
117 pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
118 TemplatesPath: opts.Templates.Path,
119 CustomLogo: opts.Templates.CustomLogo,
120 ProxyPrefix: opts.ProxyPrefix,
121 Footer: opts.Templates.Footer,
122 Version: VERSION,
123 Debug: opts.Templates.Debug,
124 ProviderName: buildProviderName(opts.GetProvider(), opts.Providers[0].Name),
125 SignInMessage: buildSignInMessage(opts),
126 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
127 })
128 if err != nil {
129 return nil, fmt.Errorf("error initialising page writer: %v", err)
130 }
132 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter)
133 if err != nil {
134 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
135 }
137 if opts.SkipJwtBearerTokens {
138 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL)
139 for _, issuer := range opts.ExtraJwtIssuers {
140 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
141 }
142 }
143 redirectURL := opts.GetRedirectURL()
144 if redirectURL.Path == "" {
145 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
146 }
148 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.Providers[0].ClientID)
149 refresh := "disabled"
150 if opts.Cookie.Refresh != time.Duration(0) {
151 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
152 }
154 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)
156 trustedIPs := ip.NewNetSet()
157 for _, ipStr := range opts.TrustedIPs {
158 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
159 trustedIPs.AddIPNet(*ipNet)
160 } else {
161 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
162 }
163 }
165 allowedRoutes, err := buildRoutesAllowlist(opts)
166 if err != nil {
167 return nil, err
168 }
170 preAuthChain, err := buildPreAuthChain(opts)
171 if err != nil {
172 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
173 }
174 sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
175 headersChain, err := buildHeadersChain(opts)
176 if err != nil {
177 return nil, fmt.Errorf("could not build headers chain: %v", err)
178 }
180 redirectValidator := redirect.NewValidator(opts.WhitelistDomains)
181 appDirector := redirect.NewAppDirector(redirect.AppDirectorOpts{
182 ProxyPrefix: opts.ProxyPrefix,
183 Validator: redirectValidator,
184 })
186 p := &OAuthProxy{
187 CookieOptions: &opts.Cookie,
188 Validator: validator,
190 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
192 ProxyPrefix: opts.ProxyPrefix,
193 provider: opts.GetProvider(),
194 sessionStore: sessionStore,
195 redirectURL: redirectURL,
196 allowedRoutes: allowedRoutes,
197 whitelistDomains: opts.WhitelistDomains,
198 skipAuthPreflight: opts.SkipAuthPreflight,
199 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
200 realClientIPParser: opts.GetRealClientIPParser(),
201 SkipProviderButton: opts.SkipProviderButton,
202 forceJSONErrors: opts.ForceJSONErrors,
203 trustedIPs: trustedIPs,
205 basicAuthValidator: basicAuthValidator,
206 basicAuthGroups: opts.HtpasswdUserGroups,
207 sessionChain: sessionChain,
208 headersChain: headersChain,
209 preAuthChain: preAuthChain,
210 pageWriter: pageWriter,
211 upstreamProxy: upstreamProxy,
212 redirectValidator: redirectValidator,
213 appDirector: appDirector,
214 }
215 p.buildServeMux(opts.ProxyPrefix)
217 if err := p.setupServer(opts); err != nil {
218 return nil, fmt.Errorf("error setting up server: %v", err)
219 }
221 return p, nil
224func (p *OAuthProxy) Start() error {
225 if p.server == nil {
226 // We have to call setupServer before Start is called.
227 // If this doesn't happen it's a programming error.
228 panic("server has not been initialised")
229 }
231 ctx, cancel := context.WithCancel(context.Background())
233 // Observe signals in background goroutine.
234 go func() {
235 sigint := make(chan os.Signal, 1)
236 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
237 <-sigint
238 cancel() // cancel the context
239 }()
241 return p.server.Start(ctx)
244func (p *OAuthProxy) setupServer(opts *options.Options) error {
245 serverOpts := proxyhttp.Opts{
246 Handler: p,
247 BindAddress: opts.Server.BindAddress,
248 SecureBindAddress: opts.Server.SecureBindAddress,
249 TLS: opts.Server.TLS,
250 }
252 appServer, err := proxyhttp.NewServer(serverOpts)
253 if err != nil {
254 return fmt.Errorf("could not build app server: %v", err)
255 }
257 metricsServer, err := proxyhttp.NewServer(proxyhttp.Opts{
258 Handler: middleware.DefaultMetricsHandler,
259 BindAddress: opts.MetricsServer.BindAddress,
260 SecureBindAddress: opts.MetricsServer.SecureBindAddress,
261 TLS: opts.MetricsServer.TLS,
262 })
263 if err != nil {
264 return fmt.Errorf("could not build metrics server: %v", err)
265 }
267 p.server = proxyhttp.NewServerGroup(appServer, metricsServer)
268 return nil
271func (p *OAuthProxy) buildServeMux(proxyPrefix string) {
272 // Use the encoded path here so we can have the option to pass it on in the upstream mux.
273 // Otherwise something like /%2F/ would be redirected to / here already.
274 r := mux.NewRouter().UseEncodedPath()
275 // Everything served by the router must go through the preAuthChain first.
276 r.Use(p.preAuthChain.Then)
278 // Register the robots path writer
279 r.Path(robotsPath).HandlerFunc(p.pageWriter.WriteRobotsTxt)
281 // The authonly path should be registered separately to prevent it from getting no-cache headers.
282 // We do this to allow users to have a short cache (via nginx) of the response to reduce the
283 // likelihood of multiple reuests trying to referesh sessions simultaneously.
284 r.Path(proxyPrefix + authOnlyPath).Handler(p.sessionChain.ThenFunc(p.AuthOnly))
286 // This will register all of the paths under the proxy prefix, except the auth only path so that no cache headers
287 // are not applied.
288 p.buildProxySubrouter(r.PathPrefix(proxyPrefix).Subrouter())
290 // Register serveHTTP last so it catches anything that isn't already caught earlier.
291 // Anything that got to this point needs to have a session loaded.
292 r.PathPrefix("/").Handler(p.sessionChain.ThenFunc(p.Proxy))
293 p.serveMux = r
296func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
297 s.Use(prepareNoCacheMiddleware)
299 s.Path(signInPath).HandlerFunc(p.SignIn)
300 s.Path(signOutPath).HandlerFunc(p.SignOut)
301 s.Path(oauthStartPath).HandlerFunc(p.OAuthStart)
302 s.Path(oauthCallbackPath).HandlerFunc(p.OAuthCallback)
304 // The userinfo endpoint needs to load sessions before handling the request
305 s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo))
308// buildPreAuthChain constructs a chain that should process every request before
309// the OAuth2 Proxy authentication logic kicks in.
310// For example forcing HTTPS or health checks.
311func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
312 chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
314 if opts.ForceHTTPS {
315 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
316 if err != nil {
317 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err)
318 }
319 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
320 }
322 healthCheckPaths := []string{opts.PingPath}
323 healthCheckUserAgents := []string{opts.PingUserAgent}
324 if opts.GCPHealthChecks {
325 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")
326 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
327 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
328 }
330 // To silence logging of health checks, register the health check handler before
331 // the logging handler
332 if opts.Logging.SilencePing {
333 chain = chain.Append(
334 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
335 middleware.NewRequestLogger(),
336 )
337 } else {
338 chain = chain.Append(
339 middleware.NewRequestLogger(),
340 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
341 )
342 }
344 chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
346 return chain, nil
349func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
350 chain := alice.New()
352 if opts.SkipJwtBearerTokens {
353 sessionLoaders := []middlewareapi.TokenToSessionFunc{
354 opts.GetProvider().CreateSessionFromToken,
355 }
357 for _, verifier := range opts.GetJWTBearerVerifiers() {
358 sessionLoaders = append(sessionLoaders,
359 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
360 }
362 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
363 }
365 if validator != nil {
366 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups, opts.LegacyPreferEmailToUser))
367 }
369 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
370 SessionStore: sessionStore,
371 RefreshPeriod: opts.Cookie.Refresh,
372 RefreshSession: opts.GetProvider().RefreshSession,
373 ValidateSession: opts.GetProvider().ValidateSession,
374 }))
376 return chain
379func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
380 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
381 if err != nil {
382 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
383 }
385 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
386 if err != nil {
387 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
388 }
390 return alice.New(requestInjector, responseInjector), nil
393func buildSignInMessage(opts *options.Options) string {
394 var msg string
395 if len(opts.Templates.Banner) >= 1 {
396 if opts.Templates.Banner == "-" {
397 msg = ""
398 } else {
399 msg = opts.Templates.Banner
400 }
401 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
402 if len(opts.EmailDomains) > 1 {
403 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
404 } else if opts.EmailDomains[0] != "*" {
405 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
406 }
407 }
408 return msg
411func buildProviderName(p providers.Provider, override string) string {
412 if override != "" {
413 return override
414 }
415 return p.Data().ProviderName
418// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
419// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
420// (method=path support)
421func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
422 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
424 for _, path := range opts.SkipAuthRegex {
425 compiledRegex, err := regexp.Compile(path)
426 if err != nil {
427 return nil, err
428 }
429 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
430 routes = append(routes, allowedRoute{
431 method: "",
432 pathRegex: compiledRegex,
433 })
434 }
436 for _, methodPath := range opts.SkipAuthRoutes {
437 var (
438 method string
439 path string
440 )
442 parts := strings.SplitN(methodPath, "=", 2)
443 if len(parts) == 1 {
444 method = ""
445 path = parts[0]
446 } else {
447 method = strings.ToUpper(parts[0])
448 path = parts[1]
449 }
451 compiledRegex, err := regexp.Compile(path)
452 if err != nil {
453 return nil, err
454 }
455 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
456 routes = append(routes, allowedRoute{
457 method: method,
458 pathRegex: compiledRegex,
459 })
460 }
462 return routes, nil
465// ClearSessionCookie creates a cookie to unset the user's authentication cookie
466// stored in the user's session
467func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
468 return p.sessionStore.Clear(rw, req)
471// LoadCookiedSession reads the user's authentication details from the request
472func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
473 return p.sessionStore.Load(req)
476// SaveSession creates a new session cookie value and sets this on the response
477func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
478 return p.sessionStore.Save(rw, req, s)
481func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
482 p.serveMux.ServeHTTP(rw, req)
485// ErrorPage writes an error response
486func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
487 redirectURL, err := p.appDirector.GetRedirect(req)
488 if err != nil {
489 logger.Errorf("Error obtaining redirect: %v", err)
490 }
491 if redirectURL == p.SignInPath || redirectURL == "" {
492 redirectURL = "/"
493 }
495 scope := middlewareapi.GetRequestScope(req)
496 p.pageWriter.WriteErrorPage(rw, pagewriter.ErrorPageOpts{
497 Status: code,
498 RedirectURL: redirectURL,
499 RequestID: scope.RequestID,
500 AppError: appError,
501 Messages: messages,
502 })
505// IsAllowedRequest is used to check if auth should be skipped for this request
506func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
507 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
508 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
511// IsAllowedRoute is used to check if the request method & path is allowed without auth
512func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
513 for _, route := range p.allowedRoutes {
514 if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
515 return true
516 }
517 }
518 return false
521// isTrustedIP is used to check if a request comes from a trusted client IP address.
522func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
523 if p.trustedIPs == nil {
524 return false
525 }
527 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
528 if err != nil {
529 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
530 // Possibly spoofed X-Real-IP header
531 return false
532 }
534 if remoteAddr == nil {
535 return false
536 }
538 return p.trustedIPs.Has(remoteAddr)
541// SignInPage writes the sign in template to the response
542func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
543 prepareNoCache(rw)
544 err := p.ClearSessionCookie(rw, req)
545 if err != nil {
546 logger.Printf("Error clearing session cookie: %v", err)
547 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
548 return
549 }
550 rw.WriteHeader(code)
552 redirectURL, err := p.appDirector.GetRedirect(req)
553 if err != nil {
554 logger.Errorf("Error obtaining redirect: %v", err)
555 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
556 return
557 }
559 if redirectURL == p.SignInPath {
560 redirectURL = "/"
561 }
563 p.pageWriter.WriteSignInPage(rw, req, redirectURL)
566// ManualSignIn handles basic auth logins to the proxy
567func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
568 if req.Method != "POST" || p.basicAuthValidator == nil {
569 return "", false
570 }
571 user := req.FormValue("username")
572 passwd := req.FormValue("password")
573 if user == "" {
574 return "", false
575 }
576 // check auth
577 if p.basicAuthValidator.Validate(user, passwd) {
578 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
579 return user, true
580 }
581 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
582 return "", false
585// SignIn serves a page prompting users to sign in
586func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
587 redirect, err := p.appDirector.GetRedirect(req)
588 if err != nil {
589 logger.Errorf("Error obtaining redirect: %v", err)
590 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
591 return
592 }
594 user, ok := p.ManualSignIn(req)
595 if ok {
596 session := &sessionsapi.SessionState{User: user, Groups: p.basicAuthGroups}
597 err = p.SaveSession(rw, req, session)
598 if err != nil {
599 logger.Printf("Error saving session: %v", err)
600 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
601 return
602 }
603 http.Redirect(rw, req, redirect, http.StatusFound)
604 } else {
605 if p.SkipProviderButton {
606 p.OAuthStart(rw, req)
607 } else {
608 p.SignInPage(rw, req, http.StatusOK)
609 }
610 }
613// UserInfo endpoint outputs session email and preferred username in JSON format
614func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
615 session, err := p.getAuthenticatedSession(rw, req)
616 if err != nil {
617 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
618 return
619 }
621 rw.Header().Set("Content-Type", "application/json")
622 rw.WriteHeader(http.StatusOK)
623 if session == nil {
624 if _, err := rw.Write([]byte("{}")); err != nil {
625 logger.Printf("Error encoding empty user info: %v", err)
626 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
627 }
628 return
629 }
631 userInfo := struct {
632 User string `json:"user"`
633 Email string `json:"email"`
634 Groups []string `json:"groups,omitempty"`
635 PreferredUsername string `json:"preferredUsername,omitempty"`
636 }{
637 User: session.User,
638 Email: session.Email,
639 Groups: session.Groups,
640 PreferredUsername: session.PreferredUsername,
641 }
643 if err := json.NewEncoder(rw).Encode(userInfo); err != nil {
644 logger.Printf("Error encoding user info: %v", err)
645 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
646 }
649// SignOut sends a response to clear the authentication cookie
650func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
651 redirect, err := p.appDirector.GetRedirect(req)
652 if err != nil {
653 logger.Errorf("Error obtaining redirect: %v", err)
654 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
655 return
656 }
657 err = p.ClearSessionCookie(rw, req)
658 if err != nil {
659 logger.Errorf("Error clearing session cookie: %v", err)
660 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
661 return
662 }
663 http.Redirect(rw, req, redirect, http.StatusFound)
666// OAuthStart starts the OAuth2 authentication flow
667func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
668 prepareNoCache(rw)
670 csrf, err := cookies.NewCSRF(p.CookieOptions)
671 if err != nil {
672 logger.Errorf("Error creating CSRF nonce: %v", err)
673 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
674 return
675 }
677 appRedirect, err := p.appDirector.GetRedirect(req)
678 if err != nil {
679 logger.Errorf("Error obtaining application redirect: %v", err)
680 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
681 return
682 }
684 callbackRedirect := p.getOAuthRedirectURI(req)
685 loginURL := p.provider.GetLoginURL(
686 callbackRedirect,
687 encodeState(csrf.HashOAuthState(), appRedirect),
688 csrf.HashOIDCNonce(),
689 )
691 if _, err := csrf.SetCookie(rw, req); err != nil {
692 logger.Errorf("Error setting CSRF cookie: %v", err)
693 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
694 return
695 }
697 http.Redirect(rw, req, loginURL, http.StatusFound)
700// OAuthCallback is the OAuth2 authentication flow callback that finishes the
701// OAuth2 authentication flow
702func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
703 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
705 // finish the oauth cycle
706 err := req.ParseForm()
707 if err != nil {
708 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
709 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
710 return
711 }
712 errorString := req.Form.Get("error")
713 if errorString != "" {
714 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
715 message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
716 // Set the debug message and override the non debug message to be the same for this case
717 p.ErrorPage(rw, req, http.StatusForbidden, message, message)
718 return
719 }
721 session, err := p.redeemCode(req)
722 if err != nil {
723 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
724 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
725 return
726 }
728 err = p.enrichSessionState(req.Context(), session)
729 if err != nil {
730 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
731 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
732 return
733 }
735 csrf, err := cookies.LoadCSRFCookie(req, p.CookieOptions)
736 if err != nil {
737 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
738 p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
739 return
740 }
742 csrf.ClearCookie(rw, req)
744 nonce, appRedirect, err := decodeState(req)
745 if err != nil {
746 logger.Errorf("Error while parsing OAuth2 state: %v", err)
747 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
748 return
749 }
751 if !csrf.CheckOAuthState(nonce) {
752 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
753 p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
754 return
755 }
757 csrf.SetSessionNonce(session)
758 p.provider.ValidateSession(req.Context(), session)
760 if !p.redirectValidator.IsValidRedirect(appRedirect) {
761 appRedirect = "/"
762 }
764 // set cookie, or deny
765 authorized, err := p.provider.Authorize(req.Context(), session)
766 if err != nil {
767 logger.Errorf("Error with authorization: %v", err)
768 }
769 if p.Validator(session.Email) && authorized {
770 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
771 err := p.SaveSession(rw, req, session)
772 if err != nil {
773 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
774 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
775 return
776 }
777 http.Redirect(rw, req, appRedirect, http.StatusFound)
778 } else {
779 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
780 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
781 }
784func (p *OAuthProxy) redeemCode(req *http.Request) (*sessionsapi.SessionState, error) {
785 code := req.Form.Get("code")
786 if code == "" {
787 return nil, providers.ErrMissingCode
788 }
790 redirectURI := p.getOAuthRedirectURI(req)
791 s, err := p.provider.Redeem(req.Context(), redirectURI, code)
792 if err != nil {
793 return nil, err
794 }
796 // Force setting these in case the Provider didn't
797 if s.CreatedAt == nil {
798 s.CreatedAtNow()
799 }
800 if s.ExpiresOn == nil {
801 s.ExpiresIn(p.CookieOptions.Expire)
802 }
804 return s, nil
807func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
808 var err error
809 if s.Email == "" {
810 // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
811 // nolint:staticcheck
812 s.Email, err = p.provider.GetEmailAddress(ctx, s)
813 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
814 return err
815 }
816 }
818 return p.provider.EnrichSession(ctx, s)
821// AuthOnly checks whether the user is currently logged in (both authentication
822// and optional authorization).
823func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
824 session, err := p.getAuthenticatedSession(rw, req)
825 if err != nil {
826 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
827 return
828 }
830 // Unauthorized cases need to return 403 to prevent infinite redirects with
831 // subrequest architectures
832 if !authOnlyAuthorize(req, session) {
833 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
834 return
835 }
837 // we are authenticated
838 p.addHeadersForProxying(rw, session)
839 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
840 rw.WriteHeader(http.StatusAccepted)
841 })).ServeHTTP(rw, req)
844// Proxy proxies the user request if the user is authenticated else it prompts
845// them to authenticate
846func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
847 session, err := p.getAuthenticatedSession(rw, req)
848 switch err {
849 case nil:
850 // we are authenticated
851 p.addHeadersForProxying(rw, session)
852 p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
853 case ErrNeedsLogin:
854 // we need to send the user to a login screen
855 if p.forceJSONErrors || isAjax(req) {
856 logger.Printf("No valid authentication in request. Access Denied.")
857 // no point redirecting an AJAX request
858 p.errorJSON(rw, http.StatusUnauthorized)
859 return
860 }
862 logger.Printf("No valid authentication in request. Initiating login.")
863 if p.SkipProviderButton {
864 p.OAuthStart(rw, req)
865 } else {
866 p.SignInPage(rw, req, http.StatusForbidden)
867 }
869 case ErrAccessDenied:
870 if p.forceJSONErrors {
871 p.errorJSON(rw, http.StatusForbidden)
872 } else {
873 p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
874 }
876 default:
877 // unknown error
878 logger.Errorf("Unexpected internal error: %v", err)
879 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
880 }
883// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
884var noCacheHeaders = map[string]string{
885 "Expires": time.Unix(0, 0).Format(time.RFC1123),
886 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
887 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
890// prepareNoCache prepares headers for preventing browser caching.
891func prepareNoCache(w http.ResponseWriter) {
892 // Set NoCache headers
893 for k, v := range noCacheHeaders {
894 w.Header().Set(k, v)
895 }
898func prepareNoCacheMiddleware(next http.Handler) http.Handler {
899 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
900 prepareNoCache(rw)
901 next.ServeHTTP(rw, req)
902 })
905// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
906// redirect clients to once authenticated.
907// This is usually the OAuthProxy callback URL.
908func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
909 // if `p.redirectURL` already has a host, return it
910 if p.redirectURL.Host != "" {
911 return p.redirectURL.String()
912 }
914 // Otherwise figure out the scheme + host from the request
915 rd := *p.redirectURL
916 rd.Host = requestutil.GetRequestHost(req)
917 rd.Scheme = requestutil.GetRequestProto(req)
919 // If there's no scheme in the request, we should still include one
920 if rd.Scheme == "" {
921 rd.Scheme = schemeHTTP
922 }
924 // If CookieSecure is true, return `https` no matter what
925 // Not all reverse proxies set X-Forwarded-Proto
926 if p.CookieOptions.Secure {
927 rd.Scheme = schemeHTTPS
928 }
929 return rd.String()
932// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
933// Returns:
934// - `nil, ErrNeedsLogin` if user needs to login.
935// - `nil, ErrAccessDenied` if the authenticated user is not authorized
936// Set-Cookie headers may be set on the response as a side-effect of calling this method.
937func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
938 session := middlewareapi.GetRequestScope(req).Session
940 // Check this after loading the session so that if a valid session exists, we can add headers from it
941 if p.IsAllowedRequest(req) {
942 return session, nil
943 }
945 if session == nil {
946 return nil, ErrNeedsLogin
947 }
949 invalidEmail := session.Email != "" && !p.Validator(session.Email)
950 authorized, err := p.provider.Authorize(req.Context(), session)
951 if err != nil {
952 logger.Errorf("Error with authorization: %v", err)
953 }
955 if invalidEmail || !authorized {
956 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session: removing session %s", session)
957 // Invalid session, clear it
958 err := p.ClearSessionCookie(rw, req)
959 if err != nil {
960 logger.Errorf("Error clearing session cookie: %v", err)
961 }
962 return nil, ErrAccessDenied
963 }
965 return session, nil
968// authOnlyAuthorize handles special authorization logic that is only done
969// on the AuthOnly endpoint for use with Nginx subrequest architectures.
970//
971// TODO (@NickMeves): This method is a placeholder to be extended but currently
972// fails the linter. Remove the nolint when functionality expands.
973//
974//nolint:gosimple
975func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
976 // Allow secondary group restrictions based on the `allowed_groups`
977 // querystring parameter
978 if !checkAllowedGroups(req, s) {
979 return false
980 }
982 return true
985func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
986 allowedGroups := extractAllowedGroups(req)
987 if len(allowedGroups) == 0 {
988 return true
989 }
991 for _, group := range s.Groups {
992 if _, ok := allowedGroups[group]; ok {
993 return true
994 }
995 }
997 return false
1000func extractAllowedGroups(req *http.Request) map[string]struct{} {
1001 groups := map[string]struct{}{}
1003 query := req.URL.Query()
1004 for _, allowedGroups := range query["allowed_groups"] {
1005 for _, group := range strings.Split(allowedGroups, ",") {
1006 if group != "" {
1007 groups[group] = struct{}{}
1012 return groups
1015// encodedState builds the OAuth state param out of our nonce and
1016// original application redirect
1017func encodeState(nonce string, redirect string) string {
1018 return fmt.Sprintf("%v:%v", nonce, redirect)
1021// decodeState splits the reflected OAuth state response back into
1022// the nonce and original application redirect
1023func decodeState(req *http.Request) (string, string, error) {
1024 state := strings.SplitN(req.Form.Get("state"), ":", 2)
1025 if len(state) != 2 {
1026 return "", "", errors.New("invalid length")
1028 return state[0], state[1], nil
1031// addHeadersForProxying adds the appropriate headers the request / response for proxying
1032func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
1033 if session == nil {
1034 return
1036 if session.Email == "" {
1037 rw.Header().Set("GAP-Auth", session.User)
1038 } else {
1039 rw.Header().Set("GAP-Auth", session.Email)
1043// isAjax checks if a request is an ajax request
1044func isAjax(req *http.Request) bool {
1045 acceptValues := req.Header.Values("Accept")
1046 const ajaxReq = applicationJSON
1047 // Iterate over multiple Accept headers, i.e.
1048 // Accept: application/json
1049 // Accept: text/plain
1050 for _, mimeTypes := range acceptValues {
1051 // Iterate over multiple mimetypes in a single header, i.e.
1052 // Accept: application/json, text/plain, */*
1053 for _, mimeType := range strings.Split(mimeTypes, ",") {
1054 mimeType = strings.TrimSpace(mimeType)
1055 if mimeType == ajaxReq {
1056 return true
1060 return false
1063// errorJSON returns the error code with an application/json mime type
1064func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
1065 rw.Header().Set("Content-Type", applicationJSON)
1066 rw.WriteHeader(code)
1067 // we need to send some JSON response because we set the Content-Type to
1068 // application/json
1069 rw.Write([]byte("{}"))