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"
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}"
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")
74// allowedRoute manages method + path based allowlists
75type allowedRoute struct {
78 pathRegex *regexp.Regexp
82 pathRegex *regexp.Regexp
85// OAuthProxy is the main authentication proxy
86type OAuthProxy struct {
87 CookieOptions *options.Cookie
88 Validator func(string) bool
92 allowedRoutes []allowedRoute
94 redirectURL *url.URL // the url to receive requests at
95 relativeRedirectURL bool
96 whitelistDomains []string
97 provider providers.Provider
98 sessionStore sessionsapi.SessionStore
100 basicAuthValidator basic.Validator
101 basicAuthGroups []string
102 SkipProviderButton bool
103 skipAuthPreflight bool
104 skipJwtBearerTokens 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
117 redirectValidator redirect.Validator
118 appDirector redirect.AppDirector
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)
127 return nil, fmt.Errorf("error initialising session store: %v", err)
130 var basicAuthValidator basic.Validator
131 if opts.HtpasswdFile != "" {
132 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
134 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
136 return nil, fmt.Errorf("could not validate htpasswd: %v", err)
140 provider, err := providers.NewProvider(opts.Providers[0])
142 return nil, fmt.Errorf("error initialising provider: %v", err)
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,
157 return nil, fmt.Errorf("error initialising page writer: %v", err)
160 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter)
162 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
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)
170 if !opts.BearerTokenLoginFallback {
171 logger.Println("Denying requests with invalid JWT tokens")
175 redirectURL := opts.GetRedirectURL()
176 if redirectURL.Path == "" {
177 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
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)
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)
193 trustedProxies, err := buildTrustedProxyNetSet(opts)
198 allowedRoutes, err := buildRoutesAllowlist(opts)
203 apiRoutes, err := buildAPIRoutes(opts)
208 preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies)
210 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
212 sessionChain := buildSessionChain(opts, provider, sessionStore, basicAuthValidator)
213 headersChain, err := buildHeadersChain(opts)
215 return nil, fmt.Errorf("could not build headers chain: %v", err)
218 redirectValidator := redirect.NewValidator(opts.WhitelistDomains)
219 appDirector := redirect.NewAppDirector(redirect.AppDirectorOpts{
220 ProxyPrefix: opts.ProxyPrefix,
221 Validator: redirectValidator,
225 CookieOptions: &opts.Cookie,
226 Validator: validator,
228 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
230 ProxyPrefix: opts.ProxyPrefix,
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,
257 p.buildServeMux(opts.ProxyPrefix)
259 if err := p.setupServer(opts); err != nil {
260 return nil, fmt.Errorf("error setting up server: %v", err)
266func (p *OAuthProxy) Start() error {
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")
273 ctx, cancel := context.WithCancel(context.Background())
275 // Observe signals in background goroutine.
277 sigint := make(chan os.Signal, 1)
278 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
280 cancel() // cancel the context
283 return p.server.Start(ctx)
286func (p *OAuthProxy) setupServer(opts *options.Options) error {
287 serverOpts := proxyhttp.Opts{
289 BindAddress: opts.Server.BindAddress,
290 SecureBindAddress: opts.Server.SecureBindAddress,
291 TLS: opts.Server.TLS,
294 // Option: AllowQuerySemicolons
295 if opts.AllowQuerySemicolons {
296 serverOpts.Handler = http.AllowQuerySemicolons(serverOpts.Handler)
299 appServer, err := proxyhttp.NewServer(serverOpts)
301 return fmt.Errorf("could not build app server: %v", err)
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,
311 return fmt.Errorf("could not build metrics server: %v", err)
314 p.server = proxyhttp.NewServerGroup(appServer, metricsServer)
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
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))
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)
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))
365 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
367 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err)
369 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
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")
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(),
389 chain = chain.Append(
390 middleware.NewRequestLogger(),
391 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
392 middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
396 chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
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
408 return ip.ParseNetSet(trustedProxyIPs)
411func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
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))
425 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders, opts.BearerTokenLoginFallback))
428 if validator != nil {
429 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups, opts.LegacyPreferEmailToUser))
432 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
433 SessionStore: sessionStore,
434 RefreshPeriod: opts.Cookie.Refresh,
435 RefreshSession: provider.RefreshSession,
436 ValidateSession: provider.ValidateSession,
442func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
443 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
445 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
448 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
450 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
453 return alice.New(requestInjector, responseInjector), nil
456func buildSignInMessage(opts *options.Options) string {
458 if len(opts.Templates.Banner) >= 1 {
459 if opts.Templates.Banner == "-" {
462 msg = opts.Templates.Banner
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])
474func buildProviderName(p providers.Provider, override string) string {
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)
492 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
493 routes = append(routes, allowedRoute{
495 pathRegex: compiledRegex,
499 for _, methodPath := range opts.SkipAuthRoutes {
503 negate = strings.Contains(methodPath, "!=")
506 parts := regexp.MustCompile("!?=").Split(methodPath, 2)
511 method = strings.ToUpper(parts[0])
515 compiledRegex, err := regexp.Compile(path)
519 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
520 routes = append(routes, allowedRoute{
523 pathRegex: compiledRegex,
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)
539 logger.Printf("API route - Path: %s", path)
540 routes = append(routes, apiRoute{
541 pathRegex: compiledRegex,
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)
572 logger.Errorf("Error obtaining redirect: %v", err)
574 if redirectURL == p.SignInPath || redirectURL == "" {
578 scope := middlewareapi.GetRequestScope(req)
579 p.pageWriter.WriteErrorPage(rw, pagewriter.ErrorPageOpts{
581 RedirectURL: redirectURL,
582 RequestID: scope.RequestID,
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))
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) {
618func (p *OAuthProxy) isAPIPath(req *http.Request) bool {
619 for _, route := range p.apiRoutes {
620 if route.pathRegex.MatchString(requestutil.GetRequestURI(req)) {
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 {
633 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
635 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
636 // Possibly spoofed X-Real-IP header
640 if remoteAddr == nil {
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) {
651 if err := p.ClearSessionCookie(rw, req); err != nil {
652 logger.Printf("Error clearing session cookie: %v", err)
656 redirectURL, err := p.appDirector.GetRedirect(req)
658 logger.Errorf("Error obtaining redirect: %v", err)
659 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
663 if redirectURL == p.SignInPath {
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
675 user := req.FormValue("username")
676 passwd := req.FormValue("password")
678 return "", false, http.StatusBadRequest
681 if p.basicAuthValidator.Validate(user, passwd) {
682 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
683 return user, true, http.StatusOK
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)
693 logger.Errorf("Error obtaining redirect: %v", err)
694 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
698 user, ok, statusCode := p.ManualSignIn(req)
700 session := &sessionsapi.SessionState{User: user, Groups: p.basicAuthGroups}
701 err = p.SaveSession(rw, req, session)
703 logger.Printf("Error saving session: %v", err)
704 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
707 http.Redirect(rw, req, redirect, http.StatusFound)
709 if p.SkipProviderButton {
710 p.OAuthStart(rw, req)
712 // TODO - should we pass on /oauth2/sign_in query params to /oauth2/start?
713 p.SignInPage(rw, req, statusCode)
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)
722 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
726 rw.Header().Set("Content-Type", "application/json")
727 rw.WriteHeader(http.StatusOK)
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())
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"`
744 Email: session.Email,
745 Groups: session.Groups,
746 PreferredUsername: session.PreferredUsername,
747 AdditionalClaims: session.AdditionalClaims,
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())
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)
760 logger.Errorf("Error obtaining redirect: %v", err)
761 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
765 if strings.Contains(redirect, idTokenPlaceholder) {
766 session, err := p.getAuthenticatedSession(rw, req)
768 logger.Errorf("error getting authenticated session during SignOut, won't replace id_token placeholder in redirect URL: %v", err)
770 redirect = strings.ReplaceAll(redirect, idTokenPlaceholder, session.IDToken)
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)
780 logger.Errorf("Error clearing session cookie: %v", err)
781 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
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)
791 logger.Errorf("error getting authenticated session during backend logout: %v", err)
799 providerData := p.provider.Data()
800 if providerData.BackendLogoutURL == "" {
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
809 logger.Errorf("error while calling backend logout: %v", err)
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)
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)
831 codeChallenge, codeVerifier, codeChallengeMethod string
833 if p.provider.Data().CodeChallengeMethod != "" {
834 codeChallengeMethod = p.provider.Data().CodeChallengeMethod
835 codeVerifier, err = encryption.GenerateCodeVerifierString(96)
837 logger.Errorf("Unable to build random ASCII string for code verifier: %v", err)
838 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
842 codeChallenge, err = encryption.GenerateCodeChallenge(p.provider.Data().CodeChallengeMethod, codeVerifier)
844 logger.Errorf("Error creating code challenge: %v", err)
845 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
849 extraParams.Add("code_challenge", codeChallenge)
850 extraParams.Add("code_challenge_method", codeChallengeMethod)
853 csrf, err := cookies.NewCSRF(p.CookieOptions, codeVerifier)
855 logger.Errorf("Error creating CSRF nonce: %v", err)
856 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
860 appRedirect, err := p.appDirector.GetRedirect(req)
862 logger.Errorf("Error obtaining application redirect: %v", err)
863 p.ErrorPage(rw, req, http.StatusBadRequest, err.Error())
867 callbackRedirect := p.getOAuthRedirectURI(req)
868 loginURL := p.provider.GetLoginURL(
870 encodeState(csrf.HashOAuthState(), appRedirect, p.encodeState),
871 csrf.HashOIDCNonce(),
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())
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()
893 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
894 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
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)
906 nonce, appRedirect, err := decodeState(req.Form.Get("state"), p.encodeState)
908 logger.Errorf("Error while parsing OAuth2 state: %v", err)
909 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
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)
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.")
926 session, err := p.redeemCode(req, csrf.GetCodeVerifier())
928 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
929 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
933 err = p.enrichSessionState(req.Context(), session)
935 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
936 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
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.")
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")
955 if !p.redirectValidator.IsValidRedirect(appRedirect) {
959 // set cookie, or deny
960 authorized, err := p.provider.Authorize(req.Context(), session)
962 logger.Errorf("Error with authorization: %v", err)
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)
968 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
969 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
972 http.Redirect(rw, req, appRedirect, http.StatusFound)
974 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
975 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
979func (p *OAuthProxy) redeemCode(req *http.Request, codeVerifier string) (*sessionsapi.SessionState, error) {
980 code := req.Form.Get("code")
982 return nil, providers.ErrMissingCode
985 redirectURI := p.getOAuthRedirectURI(req)
986 s, err := p.provider.Redeem(req.Context(), redirectURI, code, codeVerifier)
991 // Force setting these in case the Provider didn't
992 if s.CreatedAt == nil {
995 if s.ExpiresOn == nil {
996 s.ExpiresIn(p.CookieOptions.Expire)
1002func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
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) {
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)
1021 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
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)
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)
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)
1052 // we are authenticated
1053 p.addHeadersForProxying(rw, session)
1054 p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
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)
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)
1071 p.SignInPage(rw, req, http.StatusForbidden)
1074 case ErrAccessDenied:
1075 if p.forceJSONErrors {
1076 p.errorJSON(rw, http.StatusForbidden)
1078 p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
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) {
1106 next.ServeHTTP(rw, req)
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
1137// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
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) {
1151 return nil, ErrNeedsLogin
1154 invalidEmail := session.Email != "" && !p.Validator(session.Email)
1155 authorized, err := p.provider.Authorize(req.Context(), session)
1157 logger.Errorf("Error with authorization: %v", err)
1160 if invalidEmail || !authorized {
1161 cause := "unauthorized"
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)
1170 logger.Errorf("Error clearing session cookie: %v", err)
1172 return nil, ErrAccessDenied
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
1186 constraints := []func(*http.Request, *sessionsapi.SessionState) bool{
1188 checkAllowedEmailDomains,
1192 for _, constraint := range constraints {
1193 if !constraint(req, s) {
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, ",") {
1211 entities[entity] = struct{}{}
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 {
1227 splitEmail := strings.Split(s.Email, "@")
1228 if len(splitEmail) != 2 {
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 {
1251 for _, group := range s.Groups {
1252 if _, ok := allowedGroups[group]; ok {
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 {
1270 for email := range allowedEmails {
1271 if email == s.Email {
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)
1285 return base64.RawURLEncoding.EncodeToString([]byte(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) {
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) {
1311 if session.Email == "" {
1312 rw.Header().Set("GAP-Auth", session.User)
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 {
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
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.")
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)
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)
1368 logger.Println(req, logger.AuthFailure, "Cookies were found in OAuth callback, but none was a CSRF cookie.")
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list