19 "github.com/gorilla/mux"
20 "github.com/justinas/alice"
21 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
22 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
24 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/redirect"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
30 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
31 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
33 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
34 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
35 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
36 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
37 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
38 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
39 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
45 applicationJSON = "application/json"
47 robotsPath = "/robots.txt"
48 signInPath = "/sign_in"
49 signOutPath = "/sign_out"
50 oauthStartPath = "/start"
51 oauthCallbackPath = "/callback"
52 authOnlyPath = "/auth"
53 userInfoPath = "/userinfo"
57 // ErrNeedsLogin means the user should be redirected to the login page
58 ErrNeedsLogin = errors.New("redirect to login page")
60 // ErrAccessDenied means the user should receive a 401 Unauthorized response
61 ErrAccessDenied = errors.New("access denied")
64// allowedRoute manages method + path based allowlists
65type allowedRoute struct {
67 pathRegex *regexp.Regexp
70// OAuthProxy is the main authentication proxy
71type OAuthProxy struct {
72 CookieOptions *options.Cookie
73 Validator func(string) bool
77 allowedRoutes []allowedRoute
78 redirectURL *url.URL // the url to receive requests at
79 whitelistDomains []string
80 provider providers.Provider
81 sessionStore sessionsapi.SessionStore
83 basicAuthValidator basic.Validator
84 basicAuthGroups []string
85 SkipProviderButton bool
86 skipAuthPreflight bool
87 skipJwtBearerTokens bool
89 realClientIPParser ipapi.RealClientIPParser
92 sessionChain alice.Chain
93 headersChain alice.Chain
94 preAuthChain alice.Chain
95 pageWriter pagewriter.Writer
96 server proxyhttp.Server
97 upstreamProxy http.Handler
99 redirectValidator redirect.Validator
100 appDirector redirect.AppDirector
103// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
104func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
105 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
107 return nil, fmt.Errorf("error initialising session store: %v", err)
110 var basicAuthValidator basic.Validator
111 if opts.HtpasswdFile != "" {
112 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
114 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
116 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
120 provider, err := providers.NewProvider(opts.Providers[0])
122 return nil, fmt.Errorf("error intiailising provider: %v", err)
125 pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
126 TemplatesPath: opts.Templates.Path,
127 CustomLogo: opts.Templates.CustomLogo,
128 ProxyPrefix: opts.ProxyPrefix,
129 Footer: opts.Templates.Footer,
131 Debug: opts.Templates.Debug,
132 ProviderName: buildProviderName(provider, opts.Providers[0].Name),
133 SignInMessage: buildSignInMessage(opts),
134 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
137 return nil, fmt.Errorf("error initialising page writer: %v", err)
140 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter)
142 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
145 if opts.SkipJwtBearerTokens {
146 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL)
147 for _, issuer := range opts.ExtraJwtIssuers {
148 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
151 redirectURL := opts.GetRedirectURL()
152 if redirectURL.Path == "" {
153 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
156 logger.Printf("OAuthProxy configured for %s Client ID: %s", provider.Data().ProviderName, opts.Providers[0].ClientID)
157 refresh := "disabled"
158 if opts.Cookie.Refresh != time.Duration(0) {
159 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
162 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)
164 trustedIPs := ip.NewNetSet()
165 for _, ipStr := range opts.TrustedIPs {
166 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
167 trustedIPs.AddIPNet(*ipNet)
169 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
173 allowedRoutes, err := buildRoutesAllowlist(opts)
178 preAuthChain, err := buildPreAuthChain(opts)
180 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
182 sessionChain := buildSessionChain(opts, provider, sessionStore, basicAuthValidator)
183 headersChain, err := buildHeadersChain(opts)
185 return nil, fmt.Errorf("could not build headers chain: %v", err)
188 redirectValidator := redirect.NewValidator(opts.WhitelistDomains)
189 appDirector := redirect.NewAppDirector(redirect.AppDirectorOpts{
190 ProxyPrefix: opts.ProxyPrefix,
191 Validator: redirectValidator,
195 CookieOptions: &opts.Cookie,
196 Validator: validator,
198 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
200 ProxyPrefix: opts.ProxyPrefix,
202 sessionStore: sessionStore,
203 redirectURL: redirectURL,
204 allowedRoutes: allowedRoutes,
205 whitelistDomains: opts.WhitelistDomains,
206 skipAuthPreflight: opts.SkipAuthPreflight,
207 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
208 realClientIPParser: opts.GetRealClientIPParser(),
209 SkipProviderButton: opts.SkipProviderButton,
210 forceJSONErrors: opts.ForceJSONErrors,
211 trustedIPs: trustedIPs,
213 basicAuthValidator: basicAuthValidator,
214 basicAuthGroups: opts.HtpasswdUserGroups,
215 sessionChain: sessionChain,
216 headersChain: headersChain,
217 preAuthChain: preAuthChain,
218 pageWriter: pageWriter,
219 upstreamProxy: upstreamProxy,
220 redirectValidator: redirectValidator,
221 appDirector: appDirector,
223 p.buildServeMux(opts.ProxyPrefix)
225 if err := p.setupServer(opts); err != nil {
226 return nil, fmt.Errorf("error setting up server: %v", err)
232func (p *OAuthProxy) Start() error {
234 // We have to call setupServer before Start is called.
235 // If this doesn't happen it's a programming error.
236 panic("server has not been initialised")
239 ctx, cancel := context.WithCancel(context.Background())
241 // Observe signals in background goroutine.
243 sigint := make(chan os.Signal, 1)
244 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
246 cancel() // cancel the context
249 return p.server.Start(ctx)
252func (p *OAuthProxy) setupServer(opts *options.Options) error {
253 serverOpts := proxyhttp.Opts{
255 BindAddress: opts.Server.BindAddress,
256 SecureBindAddress: opts.Server.SecureBindAddress,
257 TLS: opts.Server.TLS,
260 appServer, err := proxyhttp.NewServer(serverOpts)
262 return fmt.Errorf("could not build app server: %v", err)
265 metricsServer, err := proxyhttp.NewServer(proxyhttp.Opts{
266 Handler: middleware.DefaultMetricsHandler,
267 BindAddress: opts.MetricsServer.BindAddress,
268 SecureBindAddress: opts.MetricsServer.SecureBindAddress,
269 TLS: opts.MetricsServer.TLS,
272 return fmt.Errorf("could not build metrics server: %v", err)
275 p.server = proxyhttp.NewServerGroup(appServer, metricsServer)
279func (p *OAuthProxy) buildServeMux(proxyPrefix string) {
280 // Use the encoded path here so we can have the option to pass it on in the upstream mux.
281 // Otherwise something like /%2F/ would be redirected to / here already.
282 r := mux.NewRouter().UseEncodedPath()
283 // Everything served by the router must go through the preAuthChain first.
284 r.Use(p.preAuthChain.Then)
286 // Register the robots path writer
287 r.Path(robotsPath).HandlerFunc(p.pageWriter.WriteRobotsTxt)
289 // The authonly path should be registered separately to prevent it from getting no-cache headers.
290 // We do this to allow users to have a short cache (via nginx) of the response to reduce the
291 // likelihood of multiple reuests trying to referesh sessions simultaneously.
292 r.Path(proxyPrefix + authOnlyPath).Handler(p.sessionChain.ThenFunc(p.AuthOnly))
294 // This will register all of the paths under the proxy prefix, except the auth only path so that no cache headers
296 p.buildProxySubrouter(r.PathPrefix(proxyPrefix).Subrouter())
298 // Register serveHTTP last so it catches anything that isn't already caught earlier.
299 // Anything that got to this point needs to have a session loaded.
300 r.PathPrefix("/").Handler(p.sessionChain.ThenFunc(p.Proxy))
304func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
305 s.Use(prepareNoCacheMiddleware)
307 s.Path(signInPath).HandlerFunc(p.SignIn)
308 s.Path(signOutPath).HandlerFunc(p.SignOut)
309 s.Path(oauthStartPath).HandlerFunc(p.OAuthStart)
310 s.Path(oauthCallbackPath).HandlerFunc(p.OAuthCallback)
312 // The userinfo endpoint needs to load sessions before handling the request
313 s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo))
316// buildPreAuthChain constructs a chain that should process every request before
317// the OAuth2 Proxy authentication logic kicks in.
318// For example forcing HTTPS or health checks.
319func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
320 chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
323 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
325 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err)
327 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
330 healthCheckPaths := []string{opts.PingPath}
331 healthCheckUserAgents := []string{opts.PingUserAgent}
332 if opts.GCPHealthChecks {
333 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")
334 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
335 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
338 // To silence logging of health checks, register the health check handler before
339 // the logging handler
340 if opts.Logging.SilencePing {
341 chain = chain.Append(
342 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
343 middleware.NewRequestLogger(),
346 chain = chain.Append(
347 middleware.NewRequestLogger(),
348 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
352 chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
357func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
360 if opts.SkipJwtBearerTokens {
361 sessionLoaders := []middlewareapi.TokenToSessionFunc{
362 provider.CreateSessionFromToken,
365 for _, verifier := range opts.GetJWTBearerVerifiers() {
366 sessionLoaders = append(sessionLoaders,
367 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
370 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
373 if validator != nil {
374 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups, opts.LegacyPreferEmailToUser))
377 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
378 SessionStore: sessionStore,
379 RefreshPeriod: opts.Cookie.Refresh,
380 RefreshSession: provider.RefreshSession,
381 ValidateSession: provider.ValidateSession,
387func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
388 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
390 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
393 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
395 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
398 return alice.New(requestInjector, responseInjector), nil
401func buildSignInMessage(opts *options.Options) string {
403 if len(opts.Templates.Banner) >= 1 {
404 if opts.Templates.Banner == "-" {
407 msg = opts.Templates.Banner
409 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
410 if len(opts.EmailDomains) > 1 {
411 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
412 } else if opts.EmailDomains[0] != "*" {
413 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
419func buildProviderName(p providers.Provider, override string) string {
423 return p.Data().ProviderName
426// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
427// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
428// (method=path support)
429func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
430 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
432 for _, path := range opts.SkipAuthRegex {
433 compiledRegex, err := regexp.Compile(path)
437 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
438 routes = append(routes, allowedRoute{
440 pathRegex: compiledRegex,
444 for _, methodPath := range opts.SkipAuthRoutes {
450 parts := strings.SplitN(methodPath, "=", 2)
455 method = strings.ToUpper(parts[0])
459 compiledRegex, err := regexp.Compile(path)
463 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
464 routes = append(routes, allowedRoute{
466 pathRegex: compiledRegex,
473// ClearSessionCookie creates a cookie to unset the user's authentication cookie
474// stored in the user's session
475func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
476 return p.sessionStore.Clear(rw, req)
479// LoadCookiedSession reads the user's authentication details from the request
480func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
481 return p.sessionStore.Load(req)
484// SaveSession creates a new session cookie value and sets this on the response
485func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
486 return p.sessionStore.Save(rw, req, s)
489func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
490 p.serveMux.ServeHTTP(rw, req)
493// ErrorPage writes an error response
494func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
495 redirectURL, err := p.appDirector.GetRedirect(req)
497 logger.Errorf("Error obtaining redirect: %v", err)
499 if redirectURL == p.SignInPath || redirectURL == "" {
503 scope := middlewareapi.GetRequestScope(req)
504 p.pageWriter.WriteErrorPage(rw, pagewriter.ErrorPageOpts{
506 RedirectURL: redirectURL,
507 RequestID: scope.RequestID,
513// IsAllowedRequest is used to check if auth should be skipped for this request
514func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
515 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
516 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
519// IsAllowedRoute is used to check if the request method & path is allowed without auth
520func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
521 for _, route := range p.allowedRoutes {
522 if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
529// isTrustedIP is used to check if a request comes from a trusted client IP address.
530func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
531 if p.trustedIPs == nil {
535 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
537 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
538 // Possibly spoofed X-Real-IP header
542 if remoteAddr == nil {
546 return p.trustedIPs.Has(remoteAddr)
549// SignInPage writes the sign in template to the response
550func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
552 err := p.ClearSessionCookie(rw, req)
554 logger.Printf("Error clearing session cookie: %v", err)
555 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
560 redirectURL, err := p.appDirector.GetRedirect(req)
562 logger.Errorf("Error obtaining redirect: %v", err)
563 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
567 if redirectURL == p.SignInPath {
571 p.pageWriter.WriteSignInPage(rw, req, redirectURL)
574// ManualSignIn handles basic auth logins to the proxy
575func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
576 if req.Method != "POST" || p.basicAuthValidator == nil {
579 user := req.FormValue("username")
580 passwd := req.FormValue("password")
585 if p.basicAuthValidator.Validate(user, passwd) {
586 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
589 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
593// SignIn serves a page prompting users to sign in
594func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
595 redirect, err := p.appDirector.GetRedirect(req)
597 logger.Errorf("Error obtaining redirect: %v", err)
598 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
602 user, ok := p.ManualSignIn(req)
604 session := &sessionsapi.SessionState{User: user, Groups: p.basicAuthGroups}
605 err = p.SaveSession(rw, req, session)
607 logger.Printf("Error saving session: %v", err)
608 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
611 http.Redirect(rw, req, redirect, http.StatusFound)
613 if p.SkipProviderButton {
614 p.OAuthStart(rw, req)
616 // TODO - should we pass on /oauth2/sign_in query params to /oauth2/start?
617 p.SignInPage(rw, req, http.StatusOK)
622// UserInfo endpoint outputs session email and preferred username in JSON format
623func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
624 session, err := p.getAuthenticatedSession(rw, req)
626 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
630 rw.Header().Set("Content-Type", "application/json")
631 rw.WriteHeader(http.StatusOK)
633 if _, err := rw.Write([]byte("{}")); err != nil {
634 logger.Printf("Error encoding empty user info: %v", err)
635 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
641 User string `json:"user"`
642 Email string `json:"email"`
643 Groups []string `json:"groups,omitempty"`
644 PreferredUsername string `json:"preferredUsername,omitempty"`
647 Email: session.Email,
648 Groups: session.Groups,
649 PreferredUsername: session.PreferredUsername,
652 if err := json.NewEncoder(rw).Encode(userInfo); err != nil {
653 logger.Printf("Error encoding user info: %v", err)
654 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
658// SignOut sends a response to clear the authentication cookie
659func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
660 redirect, err := p.appDirector.GetRedirect(req)
662 logger.Errorf("Error obtaining redirect: %v", err)
663 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
666 err = p.ClearSessionCookie(rw, req)
668 logger.Errorf("Error clearing session cookie: %v", err)
669 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
672 http.Redirect(rw, req, redirect, http.StatusFound)
675// OAuthStart starts the OAuth2 authentication flow
676func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
677 // start the flow permitting login URL query parameters to be overridden from the request URL
678 p.doOAuthStart(rw, req, req.URL.Query())
681func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, overrides url.Values) {
682 extraParams := p.provider.Data().LoginURLParams(overrides)
685 var codeChallenge, codeVerifier, codeChallengeMethod string
686 if p.provider.Data().CodeChallengeMethod != "" {
687 codeChallengeMethod = p.provider.Data().CodeChallengeMethod
688 preEncodedCodeVerifier, err := encryption.Nonce(96)
690 logger.Errorf("Unable to build random string: %v", err)
691 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
694 codeVerifier = base64.RawURLEncoding.EncodeToString(preEncodedCodeVerifier)
696 codeChallenge, err = encryption.GenerateCodeChallenge(p.provider.Data().CodeChallengeMethod, codeVerifier)
698 logger.Errorf("Error creating code challenge: %v", err)
699 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
703 extraParams.Add("code_challenge", codeChallenge)
704 extraParams.Add("code_challenge_method", codeChallengeMethod)
707 csrf, err := cookies.NewCSRF(p.CookieOptions, codeVerifier)
709 logger.Errorf("Error creating CSRF nonce: %v", err)
710 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
714 appRedirect, err := p.appDirector.GetRedirect(req)
716 logger.Errorf("Error obtaining application redirect: %v", err)
717 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
721 callbackRedirect := p.getOAuthRedirectURI(req)
722 loginURL := p.provider.GetLoginURL(
724 encodeState(csrf.HashOAuthState(), appRedirect),
725 csrf.HashOIDCNonce(),
729 if _, err := csrf.SetCookie(rw, req); err != nil {
730 logger.Errorf("Error setting CSRF cookie: %v", err)
731 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
735 http.Redirect(rw, req, loginURL, http.StatusFound)
738// OAuthCallback is the OAuth2 authentication flow callback that finishes the
739// OAuth2 authentication flow
740func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
741 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
743 // finish the oauth cycle
744 err := req.ParseForm()
746 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
747 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
750 errorString := req.Form.Get("error")
751 if errorString != "" {
752 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
753 message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
754 // Set the debug message and override the non debug message to be the same for this case
755 p.ErrorPage(rw, req, http.StatusForbidden, message, message)
759 csrf, err := cookies.LoadCSRFCookie(req, p.CookieOptions)
761 logger.Println(req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
762 p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
766 session, err := p.redeemCode(req, csrf.GetCodeVerifier())
768 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
769 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
773 err = p.enrichSessionState(req.Context(), session)
775 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
776 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
780 csrf.ClearCookie(rw, req)
782 nonce, appRedirect, err := decodeState(req)
784 logger.Errorf("Error while parsing OAuth2 state: %v", err)
785 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
789 if !csrf.CheckOAuthState(nonce) {
790 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
791 p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
795 csrf.SetSessionNonce(session)
796 if !p.provider.ValidateSession(req.Context(), session) {
797 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Session validation failed: %s", session)
798 p.ErrorPage(rw, req, http.StatusForbidden, "Session validation failed")
802 if !p.redirectValidator.IsValidRedirect(appRedirect) {
806 // set cookie, or deny
807 authorized, err := p.provider.Authorize(req.Context(), session)
809 logger.Errorf("Error with authorization: %v", err)
811 if p.Validator(session.Email) && authorized {
812 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
813 err := p.SaveSession(rw, req, session)
815 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
816 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
819 http.Redirect(rw, req, appRedirect, http.StatusFound)
821 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
822 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
826func (p *OAuthProxy) redeemCode(req *http.Request, codeVerifier string) (*sessionsapi.SessionState, error) {
827 code := req.Form.Get("code")
829 return nil, providers.ErrMissingCode
832 redirectURI := p.getOAuthRedirectURI(req)
833 s, err := p.provider.Redeem(req.Context(), redirectURI, code, codeVerifier)
838 // Force setting these in case the Provider didn't
839 if s.CreatedAt == nil {
842 if s.ExpiresOn == nil {
843 s.ExpiresIn(p.CookieOptions.Expire)
849func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
852 // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
853 // nolint:staticcheck
854 s.Email, err = p.provider.GetEmailAddress(ctx, s)
855 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
860 return p.provider.EnrichSession(ctx, s)
863// AuthOnly checks whether the user is currently logged in (both authentication
864// and optional authorization).
865func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
866 session, err := p.getAuthenticatedSession(rw, req)
868 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
872 // Unauthorized cases need to return 403 to prevent infinite redirects with
873 // subrequest architectures
874 if !authOnlyAuthorize(req, session) {
875 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
879 // we are authenticated
880 p.addHeadersForProxying(rw, session)
881 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
882 rw.WriteHeader(http.StatusAccepted)
883 })).ServeHTTP(rw, req)
886// Proxy proxies the user request if the user is authenticated else it prompts
887// them to authenticate
888func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
889 session, err := p.getAuthenticatedSession(rw, req)
892 // we are authenticated
893 p.addHeadersForProxying(rw, session)
894 p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
896 // we need to send the user to a login screen
897 if p.forceJSONErrors || isAjax(req) {
898 logger.Printf("No valid authentication in request. Access Denied.")
899 // no point redirecting an AJAX request
900 p.errorJSON(rw, http.StatusUnauthorized)
904 logger.Printf("No valid authentication in request. Initiating login.")
905 if p.SkipProviderButton {
906 // start OAuth flow, but only with the default login URL params - do not
907 // consider this request's query params as potential overrides, since
908 // the user did not explicitly start the login flow
909 p.doOAuthStart(rw, req, nil)
911 p.SignInPage(rw, req, http.StatusForbidden)
914 case ErrAccessDenied:
915 if p.forceJSONErrors {
916 p.errorJSON(rw, http.StatusForbidden)
918 p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
923 logger.Errorf("Unexpected internal error: %v", err)
924 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
928// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
929var noCacheHeaders = map[string]string{
930 "Expires": time.Unix(0, 0).Format(time.RFC1123),
931 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
932 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
935// prepareNoCache prepares headers for preventing browser caching.
936func prepareNoCache(w http.ResponseWriter) {
937 // Set NoCache headers
938 for k, v := range noCacheHeaders {
943func prepareNoCacheMiddleware(next http.Handler) http.Handler {
944 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
946 next.ServeHTTP(rw, req)
950// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
951// redirect clients to once authenticated.
952// This is usually the OAuthProxy callback URL.
953func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
954 // if `p.redirectURL` already has a host, return it
955 if p.redirectURL.Host != "" {
956 return p.redirectURL.String()
959 // Otherwise figure out the scheme + host from the request
961 rd.Host = requestutil.GetRequestHost(req)
962 rd.Scheme = requestutil.GetRequestProto(req)
964 // If there's no scheme in the request, we should still include one
966 rd.Scheme = schemeHTTP
969 // If CookieSecure is true, return `https` no matter what
970 // Not all reverse proxies set X-Forwarded-Proto
971 if p.CookieOptions.Secure {
972 rd.Scheme = schemeHTTPS
977// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
979// - `nil, ErrNeedsLogin` if user needs to login.
980// - `nil, ErrAccessDenied` if the authenticated user is not authorized
981// Set-Cookie headers may be set on the response as a side-effect of calling this method.
982func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
983 session := middlewareapi.GetRequestScope(req).Session
985 // Check this after loading the session so that if a valid session exists, we can add headers from it
986 if p.IsAllowedRequest(req) {
991 return nil, ErrNeedsLogin
994 invalidEmail := session.Email != "" && !p.Validator(session.Email)
995 authorized, err := p.provider.Authorize(req.Context(), session)
997 logger.Errorf("Error with authorization: %v", err)
1000 if invalidEmail || !authorized {
1001 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session: removing session %s", session)
1002 // Invalid session, clear it
1003 err := p.ClearSessionCookie(rw, req)
1005 logger.Errorf("Error clearing session cookie: %v", err)
1007 return nil, ErrAccessDenied
1013// authOnlyAuthorize handles special authorization logic that is only done
1014// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1015func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1016 // Allow requests previously allowed to be bypassed
1021 constraints := []func(*http.Request, *sessionsapi.SessionState) bool{
1023 checkAllowedEmailDomains,
1027 for _, constraint := range constraints {
1028 if !constraint(req, s) {
1036// extractAllowedEntities aims to extract and split allowed entities linked by a key,
1037// from an HTTP request query. Output is a map[string]struct{} where keys are valuable,
1038// the goal is to avoid time complexity O(N^2) while finding matches during membership checks.
1039func extractAllowedEntities(req *http.Request, key string) map[string]struct{} {
1040 entities := map[string]struct{}{}
1042 query := req.URL.Query()
1043 for _, allowedEntities := range query[key] {
1044 for _, entity := range strings.Split(allowedEntities, ",") {
1046 entities[entity] = struct{}{}
1054// checkAllowedEmailDomains allow email domain restrictions based on the `allowed_email_domains`
1055// querystring parameter
1056func checkAllowedEmailDomains(req *http.Request, s *sessionsapi.SessionState) bool {
1057 allowedEmailDomains := extractAllowedEntities(req, "allowed_email_domains")
1058 if len(allowedEmailDomains) == 0 {
1062 splitEmail := strings.Split(s.Email, "@")
1063 if len(splitEmail) != 2 {
1067 endpoint, _ := url.Parse("")
1068 endpoint.Host = splitEmail[1]
1070 allowedEmailDomainsList := []string{}
1071 for ed := range allowedEmailDomains {
1072 allowedEmailDomainsList = append(allowedEmailDomainsList, ed)
1075 return util.IsEndpointAllowed(endpoint, allowedEmailDomainsList)
1078// checkAllowedGroups allow secondary group restrictions based on the `allowed_groups`
1079// querystring parameter
1080func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1081 allowedGroups := extractAllowedEntities(req, "allowed_groups")
1082 if len(allowedGroups) == 0 {
1086 for _, group := range s.Groups {
1087 if _, ok := allowedGroups[group]; ok {
1095// checkAllowedEmails allow email restrictions based on the `allowed_emails`
1096// querystring parameter
1097func checkAllowedEmails(req *http.Request, s *sessionsapi.SessionState) bool {
1098 allowedEmails := extractAllowedEntities(req, "allowed_emails")
1099 if len(allowedEmails) == 0 {
1105 for email := range allowedEmails {
1106 if email == s.Email {
1115// encodedState builds the OAuth state param out of our nonce and
1116// original application redirect
1117func encodeState(nonce string, redirect string) string {
1118 return fmt.Sprintf("%v:%v", nonce, redirect)
1121// decodeState splits the reflected OAuth state response back into
1122// the nonce and original application redirect
1123func decodeState(req *http.Request) (string, string, error) {
1124 state := strings.SplitN(req.Form.Get("state"), ":", 2)
1125 if len(state) != 2 {
1126 return "", "", errors.New("invalid length")
1128 return state[0], state[1], nil
1131// addHeadersForProxying adds the appropriate headers the request / response for proxying
1132func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
1136 if session.Email == "" {
1137 rw.Header().Set("GAP-Auth", session.User)
1139 rw.Header().Set("GAP-Auth", session.Email)
1143// isAjax checks if a request is an ajax request
1144func isAjax(req *http.Request) bool {
1145 acceptValues := req.Header.Values("Accept")
1146 const ajaxReq = applicationJSON
1147 // Iterate over multiple Accept headers, i.e.
1148 // Accept: application/json
1149 // Accept: text/plain
1150 for _, mimeTypes := range acceptValues {
1151 // Iterate over multiple mimetypes in a single header, i.e.
1152 // Accept: application/json, text/plain, */*
1153 for _, mimeType := range strings.Split(mimeTypes, ",") {
1154 mimeType = strings.TrimSpace(mimeType)
1155 if mimeType == ajaxReq {
1163// errorJSON returns the error code with an application/json mime type
1164func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
1165 rw.Header().Set("Content-Type", applicationJSON)
1166 rw.WriteHeader(code)
1167 // we need to send some JSON response because we set the Content-Type to
1169 rw.Write([]byte("{}"))
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