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