git.iko.soy/typeable/oauth2-proxy
on 61caa9e help sign in
typeable/oauth2-proxy/oauthproxy.go
1369 lines 45 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 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/proxyhttp"
32 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
33 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/version"
35 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
36 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
37 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
38 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
39 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
40 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
41 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
44const (
45 schemeHTTP = "http"
46 schemeHTTPS = "https"
47 applicationJSON = "application/json"
49 robotsPath = "/robots.txt"
50 signInPath = "/sign_in"
51 signOutPath = "/sign_out"
52 oauthStartPath = "/start"
53 oauthCallbackPath = "/callback"
54 authOnlyPath = "/auth"
55 userInfoPath = "/userinfo"
56 staticPathPrefix = "/static/"
58 idTokenPlaceholder = "{id_token}"
61var (
62 defaultTrustedProxyIPs = []string{"0.0.0.0/0", "::/0"}
64 // ErrNeedsLogin means the user should be redirected to the login page
65 ErrNeedsLogin = errors.New("redirect to login page")
67 // ErrAccessDenied means the user should receive a 401 Unauthorized response
68 ErrAccessDenied = errors.New("access denied")
70 //go:embed static/*
71 staticFiles embed.FS
74// allowedRoute manages method + path based allowlists
75type allowedRoute struct {
76 method string
77 negate bool
78 pathRegex *regexp.Regexp
81type apiRoute struct {
82 pathRegex *regexp.Regexp
85// OAuthProxy is the main authentication proxy
86type OAuthProxy struct {
87 CookieOptions *options.Cookie
88 Validator func(string) bool
90 SignInPath string
92 allowedRoutes []allowedRoute
93 apiRoutes []apiRoute
94 redirectURL *url.URL // the url to receive requests at
95 relativeRedirectURL bool
96 whitelistDomains []string
97 provider providers.Provider
98 sessionStore sessionsapi.SessionStore
99 ProxyPrefix string
100 basicAuthValidator basic.Validator
101 basicAuthGroups []string
102 SkipProviderButton bool
103 skipAuthPreflight bool
104 skipJwtBearerTokens bool
105 forceJSONErrors bool
106 allowQuerySemicolons bool
107 realClientIPParser ipapi.RealClientIPParser
108 trustedIPs *ip.NetSet
110 sessionChain alice.Chain
111 headersChain alice.Chain
112 preAuthChain alice.Chain
113 pageWriter pagewriter.Writer
114 server proxyhttp.Server
115 upstreamProxy http.Handler
116 serveMux *mux.Router
117 redirectValidator redirect.Validator
118 appDirector redirect.AppDirector
120 encodeState bool
123// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
124func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
125 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
126 if err != nil {
127 return nil, fmt.Errorf("error initialising session store: %v", err)
128 }
130 var basicAuthValidator basic.Validator
131 if opts.HtpasswdFile != "" {
132 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
133 var err error
134 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
135 if err != nil {
136 return nil, fmt.Errorf("could not validate htpasswd: %v", err)
137 }
138 }
140 provider, err := providers.NewProvider(opts.Providers[0])
141 if err != nil {
142 return nil, fmt.Errorf("error initialising provider: %v", err)
143 }
145 pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
146 TemplatesPath: opts.Templates.Path,
147 CustomLogo: opts.Templates.CustomLogo,
148 ProxyPrefix: opts.ProxyPrefix,
149 Footer: opts.Templates.Footer,
150 Version: version.VERSION,
151 Debug: opts.Templates.Debug,
152 ProviderName: buildProviderName(provider, opts.Providers[0].Name),
153 SignInMessage: buildSignInMessage(opts),
154 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
155 })
156 if err != nil {
157 return nil, fmt.Errorf("error initialising page writer: %v", err)
158 }
160 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter)
161 if err != nil {
162 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
163 }
165 if opts.SkipJwtBearerTokens {
166 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL)
167 for _, issuer := range opts.ExtraJwtIssuers {
168 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
169 }
170 if !opts.BearerTokenLoginFallback {
171 logger.Println("Denying requests with invalid JWT tokens")
172 }
174 }
175 redirectURL := opts.GetRedirectURL()
176 if redirectURL.Path == "" {
177 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
178 }
180 logger.Printf("OAuthProxy configured for %s Client ID: %s", provider.Data().ProviderName, opts.Providers[0].ClientID)
181 refresh := "disabled"
182 if opts.Cookie.Refresh != time.Duration(0) {
183 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
184 }
186 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)
188 trustedIPs, err := ip.ParseNetSet(opts.TrustedIPs)
189 if err != nil {
190 return nil, err
191 }
193 trustedProxies, err := buildTrustedProxyNetSet(opts)
194 if err != nil {
195 return nil, err
196 }
198 allowedRoutes, err := buildRoutesAllowlist(opts)
199 if err != nil {
200 return nil, err
201 }
203 apiRoutes, err := buildAPIRoutes(opts)
204 if err != nil {
205 return nil, err
206 }
208 preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies)
209 if err != nil {
210 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
211 }
212 sessionChain := buildSessionChain(opts, provider, sessionStore, basicAuthValidator)
213 headersChain, err := buildHeadersChain(opts)
214 if err != nil {
215 return nil, fmt.Errorf("could not build headers chain: %v", err)
216 }
218 redirectValidator := redirect.NewValidator(opts.WhitelistDomains)
219 appDirector := redirect.NewAppDirector(redirect.AppDirectorOpts{
220 ProxyPrefix: opts.ProxyPrefix,
221 Validator: redirectValidator,
222 })
224 p := &OAuthProxy{
225 CookieOptions: &opts.Cookie,
226 Validator: validator,
228 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
230 ProxyPrefix: opts.ProxyPrefix,
231 provider: provider,
232 sessionStore: sessionStore,
233 redirectURL: redirectURL,
234 relativeRedirectURL: opts.RelativeRedirectURL,
235 apiRoutes: apiRoutes,
236 allowedRoutes: allowedRoutes,
237 whitelistDomains: opts.WhitelistDomains,
238 skipAuthPreflight: opts.SkipAuthPreflight,
239 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
240 realClientIPParser: opts.GetRealClientIPParser(),
241 SkipProviderButton: opts.SkipProviderButton,
242 forceJSONErrors: opts.ForceJSONErrors,
243 allowQuerySemicolons: opts.AllowQuerySemicolons,
244 trustedIPs: trustedIPs,
246 basicAuthValidator: basicAuthValidator,
247 basicAuthGroups: opts.HtpasswdUserGroups,
248 sessionChain: sessionChain,
249 headersChain: headersChain,
250 preAuthChain: preAuthChain,
251 pageWriter: pageWriter,
252 upstreamProxy: upstreamProxy,
253 redirectValidator: redirectValidator,
254 appDirector: appDirector,
255 encodeState: opts.EncodeState,
256 }
257 p.buildServeMux(opts.ProxyPrefix)
259 if err := p.setupServer(opts); err != nil {
260 return nil, fmt.Errorf("error setting up server: %v", err)
261 }
263 return p, nil
266func (p *OAuthProxy) Start() error {
267 if p.server == nil {
268 // We have to call setupServer before Start is called.
269 // If this doesn't happen it's a programming error.
270 panic("server has not been initialised")
271 }
273 ctx, cancel := context.WithCancel(context.Background())
275 // Observe signals in background goroutine.
276 go func() {
277 sigint := make(chan os.Signal, 1)
278 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
279 <-sigint
280 cancel() // cancel the context
281 }()
283 return p.server.Start(ctx)
286func (p *OAuthProxy) setupServer(opts *options.Options) error {
287 serverOpts := proxyhttp.Opts{
288 Handler: p,
289 BindAddress: opts.Server.BindAddress,
290 SecureBindAddress: opts.Server.SecureBindAddress,
291 TLS: opts.Server.TLS,
292 }
294 // Option: AllowQuerySemicolons
295 if opts.AllowQuerySemicolons {
296 serverOpts.Handler = http.AllowQuerySemicolons(serverOpts.Handler)
297 }
299 appServer, err := proxyhttp.NewServer(serverOpts)
300 if err != nil {
301 return fmt.Errorf("could not build app server: %v", err)
302 }
304 metricsServer, err := proxyhttp.NewServer(proxyhttp.Opts{
305 Handler: middleware.DefaultMetricsHandler,
306 BindAddress: opts.MetricsServer.BindAddress,
307 SecureBindAddress: opts.MetricsServer.SecureBindAddress,
308 TLS: opts.MetricsServer.TLS,
309 })
310 if err != nil {
311 return fmt.Errorf("could not build metrics server: %v", err)
312 }
314 p.server = proxyhttp.NewServerGroup(appServer, metricsServer)
315 return nil
318func (p *OAuthProxy) buildServeMux(proxyPrefix string) {
319 // Use the encoded path here so we can have the option to pass it on in the upstream mux.
320 // Otherwise something like /%2F/ would be redirected to / here already.
321 r := mux.NewRouter().UseEncodedPath()
322 // Everything served by the router must go through the preAuthChain first.
323 r.Use(p.preAuthChain.Then)
325 // Register the robots path writer
326 r.Path(robotsPath).HandlerFunc(p.pageWriter.WriteRobotsTxt)
328 // The authonly path should be registered separately to prevent it from getting no-cache headers.
329 // We do this to allow users to have a short cache (via nginx) of the response to reduce the
330 // likelihood of multiple requests trying to refresh sessions simultaneously.
331 r.Path(proxyPrefix + authOnlyPath).Handler(p.sessionChain.ThenFunc(p.AuthOnly))
333 // This will register all of the paths under the proxy prefix, except the auth only path so that no cache headers
334 // are not applied.
335 p.buildProxySubrouter(r.PathPrefix(proxyPrefix).Subrouter())
337 // Register serveHTTP last so it catches anything that isn't already caught earlier.
338 // Anything that got to this point needs to have a session loaded.
339 r.PathPrefix("/").Handler(p.sessionChain.ThenFunc(p.Proxy))
340 p.serveMux = r
343func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
344 s.Use(prepareNoCacheMiddleware)
346 s.Path(signInPath).HandlerFunc(p.SignIn)
347 s.Path(oauthStartPath).HandlerFunc(p.OAuthStart)
348 s.Path(oauthCallbackPath).HandlerFunc(p.OAuthCallback)
350 // Static file paths
351 s.PathPrefix(staticPathPrefix).Handler(http.StripPrefix(p.ProxyPrefix, http.FileServer(http.FS(staticFiles))))
353 // The userinfo and logout endpoints needs to load sessions before handling the request
354 s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo))
355 s.Path(signOutPath).Handler(p.sessionChain.ThenFunc(p.SignOut))
358// buildPreAuthChain constructs a chain that should process every request before
359// the OAuth2 Proxy authentication logic kicks in.
360// For example forcing HTTPS or health checks.
361func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore, trustedProxies *ip.NetSet) (alice.Chain, error) {
362 chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader, trustedProxies))
364 if opts.ForceHTTPS {
365 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
366 if err != nil {
367 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err)
368 }
369 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
370 }
372 healthCheckPaths := []string{opts.PingPath}
373 healthCheckUserAgents := []string{opts.PingUserAgent}
374 if opts.GCPHealthChecks {
375 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")
376 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
377 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
378 }
380 // To silence logging of health checks, register the health check handler before
381 // the logging handler
382 if opts.Logging.SilencePing {
383 chain = chain.Append(
384 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
385 middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
386 middleware.NewRequestLogger(),
387 )
388 } else {
389 chain = chain.Append(
390 middleware.NewRequestLogger(),
391 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
392 middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
393 )
394 }
396 chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
398 return chain, nil
401func buildTrustedProxyNetSet(opts *options.Options) (*ip.NetSet, error) {
402 trustedProxyIPs := opts.TrustedProxyIPs
403 if opts.ReverseProxy && len(trustedProxyIPs) == 0 {
404 logger.Print("WARNING: --reverse-proxy is enabled but no --trusted-proxy-ip CIDRs were configured. All connecting IPs are trusted to supply X-Forwarded-* headers by default (0.0.0.0/0, ::/0). This preserves backwards compatibility but is a potential security risk; configure --trusted-proxy-ip to match your reverse proxy addresses.")
405 trustedProxyIPs = defaultTrustedProxyIPs
406 }
408 return ip.ParseNetSet(trustedProxyIPs)
411func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
412 chain := alice.New()
414 if opts.SkipJwtBearerTokens {
415 verifiers := opts.GetJWTBearerVerifiers()
417 sessionLoaders := make([]middlewareapi.TokenToSessionFunc, 0, len(verifiers)+1)
418 sessionLoaders = append(sessionLoaders, provider.CreateSessionFromToken)
420 for _, verifier := range opts.GetJWTBearerVerifiers() {
421 sessionLoaders = append(sessionLoaders,
422 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
423 }
425 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders, opts.BearerTokenLoginFallback))
426 }
428 if validator != nil {
429 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups, opts.LegacyPreferEmailToUser))
430 }
432 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
433 SessionStore: sessionStore,
434 RefreshPeriod: opts.Cookie.Refresh,
435 RefreshSession: provider.RefreshSession,
436 ValidateSession: provider.ValidateSession,
437 }))
439 return chain
442func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
443 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
444 if err != nil {
445 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
446 }
448 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
449 if err != nil {
450 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
451 }
453 return alice.New(requestInjector, responseInjector), nil
456func buildSignInMessage(opts *options.Options) string {
457 var msg string
458 if len(opts.Templates.Banner) >= 1 {
459 if opts.Templates.Banner == "-" {
460 msg = ""
461 } else {
462 msg = opts.Templates.Banner
463 }
464 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
465 if len(opts.EmailDomains) > 1 {
466 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
467 } else if opts.EmailDomains[0] != "*" {
468 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
469 }
470 }
471 return msg
474func buildProviderName(p providers.Provider, override string) string {
475 if override != "" {
476 return override
477 }
478 return p.Data().ProviderName
481// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
482// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
483// (method=path support)
484func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
485 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
487 for _, path := range opts.SkipAuthRegex {
488 compiledRegex, err := regexp.Compile(path)
489 if err != nil {
490 return nil, err
491 }
492 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
493 routes = append(routes, allowedRoute{
494 method: "",
495 pathRegex: compiledRegex,
496 })
497 }
499 for _, methodPath := range opts.SkipAuthRoutes {
500 var (
501 method string
502 path string
503 negate = strings.Contains(methodPath, "!=")
504 )
506 parts := regexp.MustCompile("!?=").Split(methodPath, 2)
507 if len(parts) == 1 {
508 method = ""
509 path = parts[0]
510 } else {
511 method = strings.ToUpper(parts[0])
512 path = parts[1]
513 }
515 compiledRegex, err := regexp.Compile(path)
516 if err != nil {
517 return nil, err
518 }
519 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
520 routes = append(routes, allowedRoute{
521 method: method,
522 negate: negate,
523 pathRegex: compiledRegex,
524 })
525 }
527 return routes, nil
530// buildAPIRoutes builds an []apiRoute from ApiRoutes option
531func buildAPIRoutes(opts *options.Options) ([]apiRoute, error) {
532 routes := make([]apiRoute, 0, len(opts.APIRoutes))
534 for _, path := range opts.APIRoutes {
535 compiledRegex, err := regexp.Compile(path)
536 if err != nil {
537 return nil, err
538 }
539 logger.Printf("API route - Path: %s", path)
540 routes = append(routes, apiRoute{
541 pathRegex: compiledRegex,
542 })
543 }
545 return routes, nil
548// ClearSessionCookie creates a cookie to unset the user's authentication cookie
549// stored in the user's session
550func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
551 return p.sessionStore.Clear(rw, req)
554// LoadCookiedSession reads the user's authentication details from the request
555func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
556 return p.sessionStore.Load(req)
559// SaveSession creates a new session cookie value and sets this on the response
560func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
561 return p.sessionStore.Save(rw, req, s)
564func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
565 p.serveMux.ServeHTTP(rw, req)
568// ErrorPage writes an error response
569func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
570 redirectURL, err := p.appDirector.GetRedirect(req)
571 if err != nil {
572 logger.Errorf("Error obtaining redirect: %v", err)
573 }
574 if redirectURL == p.SignInPath || redirectURL == "" {
575 redirectURL = "/"
576 }
578 scope := middlewareapi.GetRequestScope(req)
579 p.pageWriter.WriteErrorPage(rw, pagewriter.ErrorPageOpts{
580 Status: code,
581 RedirectURL: redirectURL,
582 RequestID: scope.RequestID,
583 AppError: appError,
584 Messages: messages,
585 })
588// IsAllowedRequest is used to check if auth should be skipped for this request
589func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
590 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == http.MethodOptions
591 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
594func isAllowedMethod(req *http.Request, route allowedRoute) bool {
595 return route.method == "" || req.Method == route.method
598func isAllowedPath(req *http.Request, route allowedRoute) bool {
599 matches := route.pathRegex.MatchString(requestutil.GetRequestPath(req))
601 if route.negate {
602 return !matches
603 }
605 return matches
608// IsAllowedRoute is used to check if the request method & path is allowed without auth
609func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
610 for _, route := range p.allowedRoutes {
611 if isAllowedMethod(req, route) && isAllowedPath(req, route) {
612 return true
613 }
614 }
615 return false
618func (p *OAuthProxy) isAPIPath(req *http.Request) bool {
619 for _, route := range p.apiRoutes {
620 if route.pathRegex.MatchString(requestutil.GetRequestURI(req)) {
621 return true
622 }
623 }
624 return false
627// isTrustedIP is used to check if a request comes from a trusted client IP address.
628func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
629 if p.trustedIPs == nil {
630 return false
631 }
633 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
634 if err != nil {
635 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
636 // Possibly spoofed X-Real-IP header
637 return false
638 }
640 if remoteAddr == nil {
641 return false
642 }
644 return p.trustedIPs.Has(remoteAddr)
647// SignInPage writes the sign in template to the response
648func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
649 prepareNoCache(rw)
651 if err := p.ClearSessionCookie(rw, req); err != nil {
652 logger.Printf("Error clearing session cookie: %v", err)
653 }
654 rw.WriteHeader(code)
656 redirectURL, err := p.appDirector.GetRedirect(req)
657 if err != nil {
658 logger.Errorf("Error obtaining redirect: %v", err)
659 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
660 return
661 }
663 if redirectURL == p.SignInPath {
664 redirectURL = "/"
665 }
667 p.pageWriter.WriteSignInPage(rw, req, redirectURL, code)
670// ManualSignIn handles basic auth logins to the proxy
671func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool, int) {
672 if req.Method != http.MethodPost || p.basicAuthValidator == nil {
673 return "", false, http.StatusOK
674 }
675 user := req.FormValue("username")
676 passwd := req.FormValue("password")
677 if user == "" {
678 return "", false, http.StatusBadRequest
679 }
680 // check auth
681 if p.basicAuthValidator.Validate(user, passwd) {
682 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
683 return user, true, http.StatusOK
684 }
685 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
686 return "", false, http.StatusUnauthorized
689// SignIn serves a page prompting users to sign in
690func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
691 redirect, err := p.appDirector.GetRedirect(req)
692 if err != nil {
693 logger.Errorf("Error obtaining redirect: %v", err)
694 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
695 return
696 }
698 user, ok, statusCode := p.ManualSignIn(req)
699 if ok {
700 session := &sessionsapi.SessionState{User: user, Groups: p.basicAuthGroups}
701 err = p.SaveSession(rw, req, session)
702 if err != nil {
703 logger.Printf("Error saving session: %v", err)
704 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
705 return
706 }
707 http.Redirect(rw, req, redirect, http.StatusFound)
708 } else {
709 if p.SkipProviderButton {
710 p.OAuthStart(rw, req)
711 } else {
712 // TODO - should we pass on /oauth2/sign_in query params to /oauth2/start?
713 p.SignInPage(rw, req, statusCode)
714 }
715 }
718// UserInfo endpoint outputs session email and preferred username in JSON format
719func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
720 session, err := p.getAuthenticatedSession(rw, req)
721 if err != nil {
722 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
723 return
724 }
726 rw.Header().Set("Content-Type", "application/json")
727 rw.WriteHeader(http.StatusOK)
728 if session == nil {
729 if _, err := rw.Write([]byte("{}")); err != nil {
730 logger.Printf("Error encoding empty user info: %v", err)
731 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
732 }
733 return
734 }
736 userInfo := struct {
737 User string `json:"user"`
738 Email string `json:"email"`
739 Groups []string `json:"groups,omitempty"`
740 PreferredUsername string `json:"preferredUsername,omitempty"`
741 AdditionalClaims map[string]interface{} `json:"additionalClaims,omitempty"`
742 }{
743 User: session.User,
744 Email: session.Email,
745 Groups: session.Groups,
746 PreferredUsername: session.PreferredUsername,
747 AdditionalClaims: session.AdditionalClaims,
748 }
750 if err := json.NewEncoder(rw).Encode(userInfo); err != nil {
751 logger.Printf("Error encoding user info: %v", err)
752 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
753 }
756// SignOut sends a response to clear the authentication cookie
757func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
758 redirect, err := p.appDirector.GetRedirect(req)
759 if err != nil {
760 logger.Errorf("Error obtaining redirect: %v", err)
761 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
762 return
763 }
765 if strings.Contains(redirect, idTokenPlaceholder) {
766 session, err := p.getAuthenticatedSession(rw, req)
767 if err != nil {
768 logger.Errorf("error getting authenticated session during SignOut, won't replace id_token placeholder in redirect URL: %v", err)
769 } else {
770 redirect = strings.ReplaceAll(redirect, idTokenPlaceholder, session.IDToken)
771 }
772 }
774 // Call backend logout before clearing the session so we still have the session
775 // (and id_token) available to invoke the provider's logout endpoint
776 p.backendLogout(rw, req)
778 err = p.ClearSessionCookie(rw, req)
779 if err != nil {
780 logger.Errorf("Error clearing session cookie: %v", err)
781 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
782 return
783 }
785 http.Redirect(rw, req, redirect, http.StatusFound)
788func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request) {
789 session, err := p.getAuthenticatedSession(rw, req)
790 if err != nil {
791 logger.Errorf("error getting authenticated session during backend logout: %v", err)
792 return
793 }
795 if session == nil {
796 return
797 }
799 providerData := p.provider.Data()
800 if providerData.BackendLogoutURL == "" {
801 return
802 }
804 backendLogoutURL := strings.ReplaceAll(providerData.BackendLogoutURL, idTokenPlaceholder, session.IDToken)
805 // security exception because URL is dynamic ({id_token} replacement) but
806 // base is not end-user provided but comes from configuration somewhat secure
807 resp, err := http.Get(backendLogoutURL) // #nosec G107
808 if err != nil {
809 logger.Errorf("error while calling backend logout: %v", err)
810 return
811 }
813 defer resp.Body.Close()
814 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
815 logger.Errorf("error while calling backend logout url, returned error code %v", resp.StatusCode)
816 }
819// OAuthStart starts the OAuth2 authentication flow
820func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
821 // start the flow permitting login URL query parameters to be overridden from the request URL
822 p.doOAuthStart(rw, req, req.URL.Query())
825func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, overrides url.Values) {
826 extraParams := p.provider.Data().LoginURLParams(overrides)
827 prepareNoCache(rw)
829 var (
830 err error
831 codeChallenge, codeVerifier, codeChallengeMethod string
832 )
833 if p.provider.Data().CodeChallengeMethod != "" {
834 codeChallengeMethod = p.provider.Data().CodeChallengeMethod
835 codeVerifier, err = encryption.GenerateCodeVerifierString(96)
836 if err != nil {
837 logger.Errorf("Unable to build random ASCII string for code verifier: %v", err)
838 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
839 return
840 }
842 codeChallenge, err = encryption.GenerateCodeChallenge(p.provider.Data().CodeChallengeMethod, codeVerifier)
843 if err != nil {
844 logger.Errorf("Error creating code challenge: %v", err)
845 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
846 return
847 }
849 extraParams.Add("code_challenge", codeChallenge)
850 extraParams.Add("code_challenge_method", codeChallengeMethod)
851 }
853 csrf, err := cookies.NewCSRF(p.CookieOptions, codeVerifier)
854 if err != nil {
855 logger.Errorf("Error creating CSRF nonce: %v", err)
856 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
857 return
858 }
860 appRedirect, err := p.appDirector.GetRedirect(req)
861 if err != nil {
862 logger.Errorf("Error obtaining application redirect: %v", err)
863 p.ErrorPage(rw, req, http.StatusBadRequest, err.Error())
864 return
865 }
867 callbackRedirect := p.getOAuthRedirectURI(req)
868 loginURL := p.provider.GetLoginURL(
869 callbackRedirect,
870 encodeState(csrf.HashOAuthState(), appRedirect, p.encodeState),
871 csrf.HashOIDCNonce(),
872 extraParams,
873 )
874 cookies.ClearExtraCsrfCookies(p.CookieOptions, rw, req)
875 if _, err := csrf.SetCookie(rw, req); err != nil {
876 logger.Errorf("Error setting CSRF cookie: %v", err)
877 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
878 return
879 }
880 http.Redirect(rw, req, loginURL, http.StatusFound)
883// OAuthCallback is the OAuth2 authentication flow callback that finishes the
884// OAuth2 authentication flow
885func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
886 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
888 // finish the oauth cycle
889 // #nosec G120 -- The default max size in Go is already capped at 10MB so this would be the absolute max and is
890 // unlikely to be hit in practice.
891 err := req.ParseForm()
892 if err != nil {
893 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
894 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
895 return
896 }
897 errorString := req.Form.Get("error")
898 if errorString != "" {
899 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
900 message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
901 // Set the debug message and override the non debug message to be the same for this case
902 p.ErrorPage(rw, req, http.StatusForbidden, message, message)
903 return
904 }
906 nonce, appRedirect, err := decodeState(req.Form.Get("state"), p.encodeState)
907 if err != nil {
908 logger.Errorf("Error while parsing OAuth2 state: %v", err)
909 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
910 return
911 }
913 // calculate the cookie name
914 cookieName := cookies.GenerateCookieName(p.CookieOptions, nonce)
915 // Try to find the CSRF cookie and decode it
916 csrf, err := cookies.LoadCSRFCookie(req, cookieName, p.CookieOptions)
917 if err != nil {
918 // There are a lot of issues opened complaining about missing CSRF cookies.
919 // Try to log the INs and OUTs of OAuthProxy, to be easier to analyse these issues.
920 LoggingCSRFCookiesInOAuthCallback(req, cookieName)
921 logger.Println(req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie: %s (state=%s)", err, nonce)
922 p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
923 return
924 }
926 session, err := p.redeemCode(req, csrf.GetCodeVerifier())
927 if err != nil {
928 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
929 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
930 return
931 }
933 err = p.enrichSessionState(req.Context(), session)
934 if err != nil {
935 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
936 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
937 return
938 }
940 csrf.ClearCookie(rw, req)
942 if !csrf.CheckOAuthState(nonce) {
943 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
944 p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
945 return
946 }
948 csrf.SetSessionNonce(session)
949 if !p.provider.ValidateSession(req.Context(), session) {
950 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Session validation failed: %s", session)
951 p.ErrorPage(rw, req, http.StatusForbidden, "Session validation failed")
952 return
953 }
955 if !p.redirectValidator.IsValidRedirect(appRedirect) {
956 appRedirect = "/"
957 }
959 // set cookie, or deny
960 authorized, err := p.provider.Authorize(req.Context(), session)
961 if err != nil {
962 logger.Errorf("Error with authorization: %v", err)
963 }
964 if p.Validator(session.Email) && authorized {
965 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
966 err := p.SaveSession(rw, req, session)
967 if err != nil {
968 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
969 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
970 return
971 }
972 http.Redirect(rw, req, appRedirect, http.StatusFound)
973 } else {
974 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
975 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
976 }
979func (p *OAuthProxy) redeemCode(req *http.Request, codeVerifier string) (*sessionsapi.SessionState, error) {
980 code := req.Form.Get("code")
981 if code == "" {
982 return nil, providers.ErrMissingCode
983 }
985 redirectURI := p.getOAuthRedirectURI(req)
986 s, err := p.provider.Redeem(req.Context(), redirectURI, code, codeVerifier)
987 if err != nil {
988 return nil, err
989 }
991 // Force setting these in case the Provider didn't
992 if s.CreatedAt == nil {
993 s.CreatedAtNow()
994 }
995 if s.ExpiresOn == nil {
996 s.ExpiresIn(p.CookieOptions.Expire)
997 }
999 return s, nil
1002func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
1003 var err error
1004 if s.Email == "" {
1005 // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
1006 // nolint:staticcheck
1007 s.Email, err = p.provider.GetEmailAddress(ctx, s)
1008 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
1009 return err
1013 return p.provider.EnrichSession(ctx, s)
1016// AuthOnly checks whether the user is currently logged in (both authentication
1017// and optional authorization).
1018func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
1019 session, err := p.getAuthenticatedSession(rw, req)
1020 if err != nil {
1021 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
1022 return
1025 // Unauthorized cases need to return 403 to prevent infinite redirects with
1026 // subrequest architectures
1027 if !authOnlyAuthorize(req, session) {
1028 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
1029 return
1032 // we are authenticated
1033 p.addHeadersForProxying(rw, session)
1034 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
1035 rw.WriteHeader(http.StatusAccepted)
1036 })).ServeHTTP(rw, req)
1039// Proxy proxies the user request if the user is authenticated else it prompts
1040// them to authenticate
1041func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
1042 session, err := p.getAuthenticatedSession(rw, req)
1043 switch err {
1044 case nil:
1045 // Check against our authorization constraints and return forbidden
1046 // if this request fails to satisfy them.
1047 if !authOnlyAuthorize(req, session) {
1048 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
1049 return
1052 // we are authenticated
1053 p.addHeadersForProxying(rw, session)
1054 p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
1055 case ErrNeedsLogin:
1056 // we need to send the user to a login screen
1057 if p.forceJSONErrors || isAjax(req) || p.isAPIPath(req) {
1058 logger.Printf("No valid authentication in request. Access Denied.")
1059 // no point redirecting an AJAX request
1060 p.errorJSON(rw, http.StatusUnauthorized)
1061 return
1064 logger.Printf("No valid authentication in request. Initiating login.")
1065 if p.SkipProviderButton {
1066 // start OAuth flow, but only with the default login URL params - do not
1067 // consider this request's query params as potential overrides, since
1068 // the user did not explicitly start the login flow
1069 p.doOAuthStart(rw, req, nil)
1070 } else {
1071 p.SignInPage(rw, req, http.StatusForbidden)
1074 case ErrAccessDenied:
1075 if p.forceJSONErrors {
1076 p.errorJSON(rw, http.StatusForbidden)
1077 } else {
1078 p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
1081 default:
1082 // unknown error
1083 logger.Errorf("Unexpected internal error: %v", err)
1084 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
1088// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
1089var noCacheHeaders = map[string]string{
1090 "Expires": time.Unix(0, 0).Format(time.RFC1123),
1091 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
1092 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
1095// prepareNoCache prepares headers for preventing browser caching.
1096func prepareNoCache(w http.ResponseWriter) {
1097 // Set NoCache headers
1098 for k, v := range noCacheHeaders {
1099 w.Header().Set(k, v)
1103func prepareNoCacheMiddleware(next http.Handler) http.Handler {
1104 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1105 prepareNoCache(rw)
1106 next.ServeHTTP(rw, req)
1107 })
1110// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
1111// redirect clients to once authenticated.
1112// This is usually the OAuthProxy callback URL.
1113func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
1114 // if `p.redirectURL` already has a host, return it
1115 if p.relativeRedirectURL || p.redirectURL.Host != "" {
1116 return p.redirectURL.String()
1119 // Otherwise figure out the scheme + host from the request
1120 rd := *p.redirectURL
1121 rd.Host = requestutil.GetRequestHost(req)
1122 rd.Scheme = requestutil.GetRequestProto(req)
1124 // If there's no scheme in the request, we should still include one
1125 if rd.Scheme == "" {
1126 rd.Scheme = schemeHTTP
1129 // If CookieSecure is true, return `https` no matter what
1130 // Not all reverse proxies set X-Forwarded-Proto
1131 if p.CookieOptions.Secure {
1132 rd.Scheme = schemeHTTPS
1134 return rd.String()
1137// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
1138// Returns:
1139// - `nil, ErrNeedsLogin` if user needs to login.
1140// - `nil, ErrAccessDenied` if the authenticated user is not authorized
1141// Set-Cookie headers may be set on the response as a side-effect of calling this method.
1142func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
1143 session := middlewareapi.GetRequestScope(req).Session
1145 // Check this after loading the session so that if a valid session exists, we can add headers from it
1146 if p.IsAllowedRequest(req) {
1147 return session, nil
1150 if session == nil {
1151 return nil, ErrNeedsLogin
1154 invalidEmail := session.Email != "" && !p.Validator(session.Email)
1155 authorized, err := p.provider.Authorize(req.Context(), session)
1156 if err != nil {
1157 logger.Errorf("Error with authorization: %v", err)
1160 if invalidEmail || !authorized {
1161 cause := "unauthorized"
1162 if invalidEmail {
1163 cause = "invalid email"
1166 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session (%s): removing session %s", cause, session)
1167 // Invalid session, clear it
1168 err := p.ClearSessionCookie(rw, req)
1169 if err != nil {
1170 logger.Errorf("Error clearing session cookie: %v", err)
1172 return nil, ErrAccessDenied
1175 return session, nil
1178// authOnlyAuthorize handles special authorization logic that is only done
1179// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1180func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1181 // Allow requests previously allowed to be bypassed
1182 if s == nil {
1183 return true
1186 constraints := []func(*http.Request, *sessionsapi.SessionState) bool{
1187 checkAllowedGroups,
1188 checkAllowedEmailDomains,
1189 checkAllowedEmails,
1192 for _, constraint := range constraints {
1193 if !constraint(req, s) {
1194 return false
1198 return true
1201// extractAllowedEntities aims to extract and split allowed entities linked by a key,
1202// from an HTTP request query. Output is a map[string]struct{} where keys are valuable,
1203// the goal is to avoid time complexity O(N^2) while finding matches during membership checks.
1204func extractAllowedEntities(req *http.Request, key string) map[string]struct{} {
1205 entities := map[string]struct{}{}
1207 query := req.URL.Query()
1208 for _, allowedEntities := range query[key] {
1209 for _, entity := range strings.Split(allowedEntities, ",") {
1210 if entity != "" {
1211 entities[entity] = struct{}{}
1216 return entities
1219// checkAllowedEmailDomains allow email domain restrictions based on the `allowed_email_domains`
1220// querystring parameter
1221func checkAllowedEmailDomains(req *http.Request, s *sessionsapi.SessionState) bool {
1222 allowedEmailDomains := extractAllowedEntities(req, "allowed_email_domains")
1223 if len(allowedEmailDomains) == 0 {
1224 return true
1227 splitEmail := strings.Split(s.Email, "@")
1228 if len(splitEmail) != 2 {
1229 return false
1232 endpoint, _ := url.Parse("")
1233 endpoint.Host = splitEmail[1]
1235 allowedEmailDomainsList := []string{}
1236 for ed := range allowedEmailDomains {
1237 allowedEmailDomainsList = append(allowedEmailDomainsList, ed)
1240 return util.IsEndpointAllowed(endpoint, allowedEmailDomainsList)
1243// checkAllowedGroups allow secondary group restrictions based on the `allowed_groups`
1244// querystring parameter
1245func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1246 allowedGroups := extractAllowedEntities(req, "allowed_groups")
1247 if len(allowedGroups) == 0 {
1248 return true
1251 for _, group := range s.Groups {
1252 if _, ok := allowedGroups[group]; ok {
1253 return true
1257 return false
1260// checkAllowedEmails allow email restrictions based on the `allowed_emails`
1261// querystring parameter
1262func checkAllowedEmails(req *http.Request, s *sessionsapi.SessionState) bool {
1263 allowedEmails := extractAllowedEntities(req, "allowed_emails")
1264 if len(allowedEmails) == 0 {
1265 return true
1268 allowed := false
1270 for email := range allowedEmails {
1271 if email == s.Email {
1272 allowed = true
1273 break
1277 return allowed
1280// encodeState builds the OAuth state param out of our nonce and
1281// original application redirect
1282func encodeState(nonce string, redirect string, encode bool) string {
1283 rawString := fmt.Sprintf("%v:%v", nonce, redirect)
1284 if encode {
1285 return base64.RawURLEncoding.EncodeToString([]byte(rawString))
1287 return rawString
1290// decodeState splits the reflected OAuth state response back into
1291// the nonce and original application redirect
1292func decodeState(state string, encode bool) (string, string, error) {
1293 toParse := state
1294 if encode {
1295 decoded, _ := base64.RawURLEncoding.DecodeString(state)
1296 toParse = string(decoded)
1299 parsedState := strings.SplitN(toParse, ":", 2)
1300 if len(parsedState) != 2 {
1301 return "", "", errors.New("invalid length")
1303 return parsedState[0], parsedState[1], nil
1306// addHeadersForProxying adds the appropriate headers the request / response for proxying
1307func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
1308 if session == nil {
1309 return
1311 if session.Email == "" {
1312 rw.Header().Set("GAP-Auth", session.User)
1313 } else {
1314 rw.Header().Set("GAP-Auth", session.Email)
1318// isAjax checks if a request is an ajax request
1319func isAjax(req *http.Request) bool {
1320 acceptValues := req.Header.Values("Accept")
1321 const ajaxReq = applicationJSON
1322 // Iterate over multiple Accept headers, i.e.
1323 // Accept: application/json
1324 // Accept: text/plain
1325 for _, mimeTypes := range acceptValues {
1326 // Iterate over multiple mimetypes in a single header, i.e.
1327 // Accept: application/json, text/plain, */*
1328 for _, mimeType := range strings.Split(mimeTypes, ",") {
1329 mimeType = strings.TrimSpace(mimeType)
1330 if mimeType == ajaxReq {
1331 return true
1335 return false
1338// errorJSON returns the error code with an application/json mime type
1339func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
1340 rw.Header().Set("Content-Type", applicationJSON)
1341 rw.WriteHeader(code)
1342 // we need to send some JSON response because we set the Content-Type to
1343 // application/json
1344 rw.Write([]byte("{}"))
1347// LoggingCSRFCookiesInOAuthCallback Log all CSRF cookies found in HTTP request OAuth callback,
1348// which were successfully parsed
1349func LoggingCSRFCookiesInOAuthCallback(req *http.Request, cookieName string) {
1350 cookies := req.Cookies()
1351 if len(cookies) == 0 {
1352 logger.Println(req, logger.AuthFailure, "No cookies were found in OAuth callback.")
1353 return
1356 for _, c := range cookies {
1357 if cookieName == c.Name {
1358 logger.Println(req, logger.AuthFailure, "CSRF cookie %s was found in OAuth callback.", c.Name)
1359 return
1362 if strings.HasSuffix(c.Name, "_csrf") {
1363 logger.Println(req, logger.AuthFailure, "CSRF cookie %s was found in OAuth callback, but it is not the expected one (%s).", c.Name, cookieName)
1364 return
1368 logger.Println(req, logger.AuthFailure, "Cookies were found in OAuth callback, but none was a CSRF cookie.")