18 "github.com/justinas/alice"
19 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
20 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
22 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
26 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
30 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
31 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
32 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
33 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
34 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
40 applicationJSON = "application/json"
44 // ErrNeedsLogin means the user should be redirected to the login page
45 ErrNeedsLogin = errors.New("redirect to login page")
47 // ErrAccessDenied means the user should receive a 401 Unauthorized response
48 ErrAccessDenied = errors.New("access denied")
50 // Used to check final redirects are not susceptible to open redirects.
51 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
52 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
55// allowedRoute manages method + path based allowlists
56type allowedRoute struct {
58 pathRegex *regexp.Regexp
61// OAuthProxy is the main authentication proxy
62type OAuthProxy struct {
63 CookieOptions *options.Cookie
64 Validator func(string) bool
70 OAuthCallbackPath string
74 allowedRoutes []allowedRoute
75 redirectURL *url.URL // the url to receive requests at
76 whitelistDomains []string
77 provider providers.Provider
78 sessionStore sessionsapi.SessionStore
80 basicAuthValidator basic.Validator
82 SkipProviderButton bool
83 skipAuthPreflight bool
84 skipJwtBearerTokens bool
85 realClientIPParser ipapi.RealClientIPParser
88 sessionChain alice.Chain
89 headersChain alice.Chain
90 preAuthChain alice.Chain
91 pageWriter pagewriter.Writer
92 server proxyhttp.Server
95// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
96func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
97 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
99 return nil, fmt.Errorf("error initialising session store: %v", err)
102 var basicAuthValidator basic.Validator
103 if opts.HtpasswdFile != "" {
104 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
106 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
108 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
112 pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
113 TemplatesPath: opts.Templates.Path,
114 CustomLogo: opts.Templates.CustomLogo,
115 ProxyPrefix: opts.ProxyPrefix,
116 Footer: opts.Templates.Footer,
118 Debug: opts.Templates.Debug,
119 ProviderName: buildProviderName(opts.GetProvider(), opts.Providers[0].Name),
120 SignInMessage: buildSignInMessage(opts),
121 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
124 return nil, fmt.Errorf("error initialising page writer: %v", err)
127 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter.ProxyErrorHandler)
129 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
132 if opts.SkipJwtBearerTokens {
133 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL)
134 for _, issuer := range opts.ExtraJwtIssuers {
135 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
138 redirectURL := opts.GetRedirectURL()
139 if redirectURL.Path == "" {
140 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
143 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.Providers[0].ClientID)
144 refresh := "disabled"
145 if opts.Cookie.Refresh != time.Duration(0) {
146 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
149 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)
151 trustedIPs := ip.NewNetSet()
152 for _, ipStr := range opts.TrustedIPs {
153 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
154 trustedIPs.AddIPNet(*ipNet)
156 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
160 allowedRoutes, err := buildRoutesAllowlist(opts)
165 preAuthChain, err := buildPreAuthChain(opts)
167 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
169 sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
170 headersChain, err := buildHeadersChain(opts)
172 return nil, fmt.Errorf("could not build headers chain: %v", err)
176 CookieOptions: &opts.Cookie,
177 Validator: validator,
179 RobotsPath: "/robots.txt",
180 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
181 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
182 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
183 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
184 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
185 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
187 ProxyPrefix: opts.ProxyPrefix,
188 provider: opts.GetProvider(),
189 sessionStore: sessionStore,
190 serveMux: upstreamProxy,
191 redirectURL: redirectURL,
192 allowedRoutes: allowedRoutes,
193 whitelistDomains: opts.WhitelistDomains,
194 skipAuthPreflight: opts.SkipAuthPreflight,
195 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
196 realClientIPParser: opts.GetRealClientIPParser(),
197 SkipProviderButton: opts.SkipProviderButton,
198 trustedIPs: trustedIPs,
200 basicAuthValidator: basicAuthValidator,
201 sessionChain: sessionChain,
202 headersChain: headersChain,
203 preAuthChain: preAuthChain,
204 pageWriter: pageWriter,
207 if err := p.setupServer(opts); err != nil {
208 return nil, fmt.Errorf("error setting up server: %v", err)
214func (p *OAuthProxy) Start() error {
216 // We have to call setupServer before Start is called.
217 // If this doesn't happen it's a programming error.
218 panic("server has not been initialised")
221 ctx, cancel := context.WithCancel(context.Background())
223 // Observe signals in background goroutine.
225 sigint := make(chan os.Signal, 1)
226 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
228 cancel() // cancel the context
231 return p.server.Start(ctx)
234func (p *OAuthProxy) setupServer(opts *options.Options) error {
235 serverOpts := proxyhttp.Opts{
237 BindAddress: opts.Server.BindAddress,
238 SecureBindAddress: opts.Server.SecureBindAddress,
239 TLS: opts.Server.TLS,
242 appServer, err := proxyhttp.NewServer(serverOpts)
244 return fmt.Errorf("could not build app server: %v", err)
247 metricsServer, err := proxyhttp.NewServer(proxyhttp.Opts{
248 Handler: middleware.DefaultMetricsHandler,
249 BindAddress: opts.MetricsServer.BindAddress,
250 SecureBindAddress: opts.MetricsServer.SecureBindAddress,
251 TLS: opts.MetricsServer.TLS,
254 return fmt.Errorf("could not build metrics server: %v", err)
257 p.server = proxyhttp.NewServerGroup(appServer, metricsServer)
261// buildPreAuthChain constructs a chain that should process every request before
262// the OAuth2 Proxy authentication logic kicks in.
263// For example forcing HTTPS or health checks.
264func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
265 chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
268 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
270 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err)
272 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
275 healthCheckPaths := []string{opts.PingPath}
276 healthCheckUserAgents := []string{opts.PingUserAgent}
277 if opts.GCPHealthChecks {
278 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")
279 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
280 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
283 // To silence logging of health checks, register the health check handler before
284 // the logging handler
285 if opts.Logging.SilencePing {
286 chain = chain.Append(
287 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
288 middleware.NewRequestLogger(),
291 chain = chain.Append(
292 middleware.NewRequestLogger(),
293 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
297 chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
302func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
305 if opts.SkipJwtBearerTokens {
306 sessionLoaders := []middlewareapi.TokenToSessionFunc{
307 opts.GetProvider().CreateSessionFromToken,
310 for _, verifier := range opts.GetJWTBearerVerifiers() {
311 sessionLoaders = append(sessionLoaders,
312 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
315 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
318 if validator != nil {
319 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups, opts.LegacyPreferEmailToUser))
322 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
323 SessionStore: sessionStore,
324 RefreshPeriod: opts.Cookie.Refresh,
325 RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
326 ValidateSessionState: opts.GetProvider().ValidateSession,
332func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
333 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
335 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
338 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
340 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
343 return alice.New(requestInjector, responseInjector), nil
346func buildSignInMessage(opts *options.Options) string {
348 if len(opts.Templates.Banner) >= 1 {
349 if opts.Templates.Banner == "-" {
352 msg = opts.Templates.Banner
354 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
355 if len(opts.EmailDomains) > 1 {
356 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
357 } else if opts.EmailDomains[0] != "*" {
358 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
364func buildProviderName(p providers.Provider, override string) string {
368 return p.Data().ProviderName
371// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
372// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
373// (method=path support)
374func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
375 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
377 for _, path := range opts.SkipAuthRegex {
378 compiledRegex, err := regexp.Compile(path)
382 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
383 routes = append(routes, allowedRoute{
385 pathRegex: compiledRegex,
389 for _, methodPath := range opts.SkipAuthRoutes {
395 parts := strings.SplitN(methodPath, "=", 2)
400 method = strings.ToUpper(parts[0])
404 compiledRegex, err := regexp.Compile(path)
408 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
409 routes = append(routes, allowedRoute{
411 pathRegex: compiledRegex,
418// ClearSessionCookie creates a cookie to unset the user's authentication cookie
419// stored in the user's session
420func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
421 return p.sessionStore.Clear(rw, req)
424// LoadCookiedSession reads the user's authentication details from the request
425func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
426 return p.sessionStore.Load(req)
429// SaveSession creates a new session cookie value and sets this on the response
430func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
431 return p.sessionStore.Save(rw, req, s)
434// IsValidRedirect checks whether the redirect URL is whitelisted
435func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
438 // The user didn't specify a redirect, should fallback to `/`
440 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
442 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
443 redirectURL, err := url.Parse(redirect)
445 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
448 redirectHostname := redirectURL.Hostname()
450 for _, allowedDomain := range p.whitelistDomains {
451 allowedHost, allowedPort := splitHostPort(allowedDomain)
452 if allowedHost == "" {
456 if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
457 (strings.HasPrefix(allowedHost, ".") &&
458 strings.HasSuffix(redirectHostname, allowedHost)) {
459 // the domain names match, now validate the ports
460 // if the whitelisted domain's port is '*', allow all ports
461 // if the whitelisted domain contains a specific port, only allow that port
462 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
463 redirectPort := redirectURL.Port()
464 if allowedPort == "*" ||
465 allowedPort == redirectPort ||
466 (allowedPort == "" && redirectPort == "") {
472 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
475 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
480func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
481 p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
484func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
485 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
489 switch path := req.URL.Path; {
490 case path == p.RobotsPath:
492 case p.IsAllowedRequest(req):
493 p.SkipAuthProxy(rw, req)
494 case path == p.SignInPath:
496 case path == p.SignOutPath:
498 case path == p.OAuthStartPath:
499 p.OAuthStart(rw, req)
500 case path == p.OAuthCallbackPath:
501 p.OAuthCallback(rw, req)
502 case path == p.AuthOnlyPath:
504 case path == p.UserInfoPath:
511// RobotsTxt disallows scraping pages from the OAuthProxy
512func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter, req *http.Request) {
513 p.pageWriter.WriteRobotsTxt(rw, req)
516// ErrorPage writes an error response
517func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
518 redirectURL, err := p.getAppRedirect(req)
520 logger.Errorf("Error obtaining redirect: %v", err)
522 if redirectURL == p.SignInPath || redirectURL == "" {
526 scope := middlewareapi.GetRequestScope(req)
527 p.pageWriter.WriteErrorPage(rw, pagewriter.ErrorPageOpts{
529 RedirectURL: redirectURL,
530 RequestID: scope.RequestID,
536// IsAllowedRequest is used to check if auth should be skipped for this request
537func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
538 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
539 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
542// IsAllowedRoute is used to check if the request method & path is allowed without auth
543func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
544 for _, route := range p.allowedRoutes {
545 if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
552// isTrustedIP is used to check if a request comes from a trusted client IP address.
553func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
554 if p.trustedIPs == nil {
558 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
560 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
561 // Possibly spoofed X-Real-IP header
565 if remoteAddr == nil {
569 return p.trustedIPs.Has(remoteAddr)
572// SignInPage writes the sing in template to the response
573func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
575 err := p.ClearSessionCookie(rw, req)
577 logger.Printf("Error clearing session cookie: %v", err)
578 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
583 redirectURL, err := p.getAppRedirect(req)
585 logger.Errorf("Error obtaining redirect: %v", err)
586 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
590 if redirectURL == p.SignInPath {
594 p.pageWriter.WriteSignInPage(rw, req, redirectURL)
597// ManualSignIn handles basic auth logins to the proxy
598func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
599 if req.Method != "POST" || p.basicAuthValidator == nil {
602 user := req.FormValue("username")
603 passwd := req.FormValue("password")
608 if p.basicAuthValidator.Validate(user, passwd) {
609 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
612 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
616// SignIn serves a page prompting users to sign in
617func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
618 redirect, err := p.getAppRedirect(req)
620 logger.Errorf("Error obtaining redirect: %v", err)
621 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
625 user, ok := p.ManualSignIn(req)
627 session := &sessionsapi.SessionState{User: user}
628 err = p.SaveSession(rw, req, session)
630 logger.Printf("Error saving session: %v", err)
631 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
634 http.Redirect(rw, req, redirect, http.StatusFound)
636 if p.SkipProviderButton {
637 p.OAuthStart(rw, req)
639 p.SignInPage(rw, req, http.StatusOK)
644// UserInfo endpoint outputs session email and preferred username in JSON format
645func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
647 session, err := p.getAuthenticatedSession(rw, req)
649 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
654 User string `json:"user"`
655 Email string `json:"email"`
656 Groups []string `json:"groups,omitempty"`
657 PreferredUsername string `json:"preferredUsername,omitempty"`
660 Email: session.Email,
661 Groups: session.Groups,
662 PreferredUsername: session.PreferredUsername,
665 rw.Header().Set("Content-Type", "application/json")
666 rw.WriteHeader(http.StatusOK)
667 err = json.NewEncoder(rw).Encode(userInfo)
669 logger.Printf("Error encoding user info: %v", err)
670 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
674// SignOut sends a response to clear the authentication cookie
675func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
676 redirect, err := p.getAppRedirect(req)
678 logger.Errorf("Error obtaining redirect: %v", err)
679 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
682 err = p.ClearSessionCookie(rw, req)
684 logger.Errorf("Error clearing session cookie: %v", err)
685 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
688 http.Redirect(rw, req, redirect, http.StatusFound)
691// OAuthStart starts the OAuth2 authentication flow
692func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
695 csrf, err := cookies.NewCSRF(p.CookieOptions)
697 logger.Errorf("Error creating CSRF nonce: %v", err)
698 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
702 appRedirect, err := p.getAppRedirect(req)
704 logger.Errorf("Error obtaining application redirect: %v", err)
705 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
709 callbackRedirect := p.getOAuthRedirectURI(req)
710 loginURL := p.provider.GetLoginURL(
712 encodeState(csrf.HashOAuthState(), appRedirect),
713 csrf.HashOIDCNonce(),
716 if _, err := csrf.SetCookie(rw, req); err != nil {
717 logger.Errorf("Error setting CSRF cookie: %v", err)
718 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
722 http.Redirect(rw, req, loginURL, http.StatusFound)
725// OAuthCallback is the OAuth2 authentication flow callback that finishes the
726// OAuth2 authentication flow
727func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
728 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
730 // finish the oauth cycle
731 err := req.ParseForm()
733 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
734 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
737 errorString := req.Form.Get("error")
738 if errorString != "" {
739 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
740 message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
741 // Set the debug message and override the non debug message to be the same for this case
742 p.ErrorPage(rw, req, http.StatusForbidden, message, message)
746 session, err := p.redeemCode(req)
748 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
749 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
753 err = p.enrichSessionState(req.Context(), session)
755 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
756 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
760 csrf, err := cookies.LoadCSRFCookie(req, p.CookieOptions)
762 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
763 p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
767 csrf.ClearCookie(rw, req)
769 nonce, appRedirect, err := decodeState(req)
771 logger.Errorf("Error while parsing OAuth2 state: %v", err)
772 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
776 if !csrf.CheckOAuthState(nonce) {
777 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
778 p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
782 csrf.SetSessionNonce(session)
783 p.provider.ValidateSession(req.Context(), session)
785 if !p.IsValidRedirect(appRedirect) {
789 // set cookie, or deny
790 authorized, err := p.provider.Authorize(req.Context(), session)
792 logger.Errorf("Error with authorization: %v", err)
794 if p.Validator(session.Email) && authorized {
795 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
796 err := p.SaveSession(rw, req, session)
798 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
799 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
802 http.Redirect(rw, req, appRedirect, http.StatusFound)
804 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
805 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
809func (p *OAuthProxy) redeemCode(req *http.Request) (*sessionsapi.SessionState, error) {
810 code := req.Form.Get("code")
812 return nil, providers.ErrMissingCode
815 redirectURI := p.getOAuthRedirectURI(req)
816 s, err := p.provider.Redeem(req.Context(), redirectURI, code)
823func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
826 // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
827 // nolint:staticcheck
828 s.Email, err = p.provider.GetEmailAddress(ctx, s)
829 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
834 return p.provider.EnrichSession(ctx, s)
837// AuthOnly checks whether the user is currently logged in (both authentication
838// and optional authorization).
839func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
840 session, err := p.getAuthenticatedSession(rw, req)
842 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
846 // Unauthorized cases need to return 403 to prevent infinite redirects with
847 // subrequest architectures
848 if !authOnlyAuthorize(req, session) {
849 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
853 // we are authenticated
854 p.addHeadersForProxying(rw, session)
855 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
856 rw.WriteHeader(http.StatusAccepted)
857 })).ServeHTTP(rw, req)
860// SkipAuthProxy proxies allowlisted requests and skips authentication
861func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
862 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
865// Proxy proxies the user request if the user is authenticated else it prompts
866// them to authenticate
867func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
868 session, err := p.getAuthenticatedSession(rw, req)
871 // we are authenticated
872 p.addHeadersForProxying(rw, session)
873 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
875 // we need to send the user to a login screen
877 // no point redirecting an AJAX request
878 p.errorJSON(rw, http.StatusUnauthorized)
882 if p.SkipProviderButton {
883 p.OAuthStart(rw, req)
885 p.SignInPage(rw, req, http.StatusForbidden)
888 case ErrAccessDenied:
889 p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
893 logger.Errorf("Unexpected internal error: %v", err)
894 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
898// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
899var noCacheHeaders = map[string]string{
900 "Expires": time.Unix(0, 0).Format(time.RFC1123),
901 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
902 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
905// prepareNoCache prepares headers for preventing browser caching.
906func prepareNoCache(w http.ResponseWriter) {
907 // Set NoCache headers
908 for k, v := range noCacheHeaders {
913// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
914// redirect clients to once authenticated.
915// This is usually the OAuthProxy callback URL.
916func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
917 // if `p.redirectURL` already has a host, return it
918 if p.redirectURL.Host != "" {
919 return p.redirectURL.String()
922 // Otherwise figure out the scheme + host from the request
924 rd.Host = requestutil.GetRequestHost(req)
925 rd.Scheme = requestutil.GetRequestProto(req)
927 // If there's no scheme in the request, we should still include one
929 rd.Scheme = schemeHTTP
932 // If CookieSecure is true, return `https` no matter what
933 // Not all reverse proxies set X-Forwarded-Proto
934 if p.CookieOptions.Secure {
935 rd.Scheme = schemeHTTPS
940// getAppRedirect determines the full URL or URI path to redirect clients to
941// once authenticated with the OAuthProxy
942// Strategy priority (first legal result is used):
943// - `rd` querysting parameter
944// - `X-Auth-Request-Redirect` header
945// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
946// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
947// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
948// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
950func (p *OAuthProxy) getAppRedirect(req *http.Request) (string, error) {
951 err := req.ParseForm()
956 // These redirect getter functions are strategies ordered by priority
957 // for figuring out the redirect URL.
958 type redirectGetter func(req *http.Request) string
959 for _, rdGetter := range []redirectGetter{
960 p.getRdQuerystringRedirect,
961 p.getXAuthRequestRedirect,
962 p.getXForwardedHeadersRedirect,
965 redirect := rdGetter(req)
966 // Call `p.IsValidRedirect` again here a final time to be safe
967 if redirect != "" && p.IsValidRedirect(redirect) {
975func isForwardedRequest(req *http.Request) bool {
976 return requestutil.IsProxied(req) &&
977 req.Host != requestutil.GetRequestHost(req)
980func (p *OAuthProxy) hasProxyPrefix(path string) bool {
981 return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
984func (p *OAuthProxy) validateRedirect(redirect string, errorFormat string) string {
985 if p.IsValidRedirect(redirect) {
989 logger.Errorf(errorFormat, redirect)
994// getRdQuerystringRedirect handles this getAppRedirect strategy:
995// - `rd` querysting parameter
996func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
997 return p.validateRedirect(
999 "Invalid redirect provided in rd querystring parameter: %s",
1003// getXAuthRequestRedirect handles this getAppRedirect strategy:
1004// - `X-Auth-Request-Redirect` Header
1005func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
1006 return p.validateRedirect(
1007 req.Header.Get("X-Auth-Request-Redirect"),
1008 "Invalid redirect provided in X-Auth-Request-Redirect header: %s",
1012// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
1013// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
1014// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
1015func (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
1016 if !isForwardedRequest(req) {
1020 uri := requestutil.GetRequestURI(req)
1021 if p.hasProxyPrefix(uri) {
1025 redirect := fmt.Sprintf(
1027 requestutil.GetRequestProto(req),
1028 requestutil.GetRequestHost(req),
1032 return p.validateRedirect(redirect,
1033 "Invalid redirect generated from X-Forwarded-* headers: %s")
1036// getURIRedirect handles these getAppRedirect strategies:
1037// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
1038// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
1040func (p *OAuthProxy) getURIRedirect(req *http.Request) string {
1041 redirect := p.validateRedirect(
1042 requestutil.GetRequestURI(req),
1043 "Invalid redirect generated from X-Forwarded-Uri header: %s",
1046 redirect = req.URL.RequestURI()
1049 if p.hasProxyPrefix(redirect) {
1055// splitHostPort separates host and port. If the port is not valid, it returns
1056// the entire input as host, and it doesn't check the validity of the host.
1057// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
1058// *** taken from net/url, modified validOptionalPort() to accept ":*"
1059func splitHostPort(hostport string) (host, port string) {
1062 colon := strings.LastIndexByte(host, ':')
1063 if colon != -1 && validOptionalPort(host[colon:]) {
1064 host, port = host[:colon], host[colon+1:]
1067 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
1068 host = host[1 : len(host)-1]
1074// validOptionalPort reports whether port is either an empty string
1075// or matches /^:\d*$/
1076// *** taken from net/url, modified to accept ":*"
1077func validOptionalPort(port string) bool {
1078 if port == "" || port == ":*" {
1084 for _, b := range port[1:] {
1085 if b < '0' || b > '9' {
1092// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
1094// - `nil, ErrNeedsLogin` if user needs to login.
1095// - `nil, ErrAccessDenied` if the authenticated user is not authorized
1096// Set-Cookie headers may be set on the response as a side-effect of calling this method.
1097func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
1098 var session *sessionsapi.SessionState
1100 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1101 session = middlewareapi.GetRequestScope(req).Session
1103 getSession.ServeHTTP(rw, req)
1106 return nil, ErrNeedsLogin
1109 invalidEmail := session.Email != "" && !p.Validator(session.Email)
1110 authorized, err := p.provider.Authorize(req.Context(), session)
1112 logger.Errorf("Error with authorization: %v", err)
1115 if invalidEmail || !authorized {
1116 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session: removing session %s", session)
1117 // Invalid session, clear it
1118 err := p.ClearSessionCookie(rw, req)
1120 logger.Errorf("Error clearing session cookie: %v", err)
1122 return nil, ErrAccessDenied
1128// authOnlyAuthorize handles special authorization logic that is only done
1129// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1131// TODO (@NickMeves): This method is a placeholder to be extended but currently
1132// fails the linter. Remove the nolint when functionality expands.
1135func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1136 // Allow secondary group restrictions based on the `allowed_groups`
1137 // querystring parameter
1138 if !checkAllowedGroups(req, s) {
1145func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1146 allowedGroups := extractAllowedGroups(req)
1147 if len(allowedGroups) == 0 {
1151 for _, group := range s.Groups {
1152 if _, ok := allowedGroups[group]; ok {
1160func extractAllowedGroups(req *http.Request) map[string]struct{} {
1161 groups := map[string]struct{}{}
1163 query := req.URL.Query()
1164 for _, allowedGroups := range query["allowed_groups"] {
1165 for _, group := range strings.Split(allowedGroups, ",") {
1167 groups[group] = struct{}{}
1175// encodedState builds the OAuth state param out of our nonce and
1176// original application redirect
1177func encodeState(nonce string, redirect string) string {
1178 return fmt.Sprintf("%v:%v", nonce, redirect)
1181// decodeState splits the reflected OAuth state response back into
1182// the nonce and original application redirect
1183func decodeState(req *http.Request) (string, string, error) {
1184 state := strings.SplitN(req.Form.Get("state"), ":", 2)
1185 if len(state) != 2 {
1186 return "", "", errors.New("invalid length")
1188 return state[0], state[1], nil
1191// addHeadersForProxying adds the appropriate headers the request / response for proxying
1192func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
1193 if session.Email == "" {
1194 rw.Header().Set("GAP-Auth", session.User)
1196 rw.Header().Set("GAP-Auth", session.Email)
1200// isAjax checks if a request is an ajax request
1201func isAjax(req *http.Request) bool {
1202 acceptValues := req.Header.Values("Accept")
1203 const ajaxReq = applicationJSON
1204 // Iterate over multiple Accept headers, i.e.
1205 // Accept: application/json
1206 // Accept: text/plain
1207 for _, mimeTypes := range acceptValues {
1208 // Iterate over multiple mimetypes in a single header, i.e.
1209 // Accept: application/json, text/plain, */*
1210 for _, mimeType := range strings.Split(mimeTypes, ",") {
1211 mimeType = strings.TrimSpace(mimeType)
1212 if mimeType == ajaxReq {
1220// errorJSON returns the error code with an application/json mime type
1221func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
1222 rw.Header().Set("Content-Type", applicationJSON)
1223 rw.WriteHeader(code)
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