16 "github.com/justinas/alice"
17 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
18 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
20 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
22 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
30 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
37 applicationJSON = "application/json"
41 // ErrNeedsLogin means the user should be redirected to the login page
42 ErrNeedsLogin = errors.New("redirect to login page")
44 // ErrAccessDenied means the user should receive a 401 Unauthorized response
45 ErrAccessDenied = errors.New("access denied")
47 // Used to check final redirects are not susceptible to open redirects.
48 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
49 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
52// allowedRoute manages method + path based allowlists
53type allowedRoute struct {
55 pathRegex *regexp.Regexp
58// OAuthProxy is the main authentication proxy
59type OAuthProxy struct {
63 CookieDomains []string
67 CookieExpire time.Duration
68 CookieRefresh time.Duration
70 Validator func(string) bool
76 OAuthCallbackPath string
80 allowedRoutes []allowedRoute
81 redirectURL *url.URL // the url to receive requests at
82 whitelistDomains []string
83 provider providers.Provider
84 providerNameOverride string
85 sessionStore sessionsapi.SessionStore
88 basicAuthValidator basic.Validator
89 displayHtpasswdForm bool
94 SkipProviderButton bool
96 BasicAuthPassword string
99 PassAuthorization bool
100 PreferEmailToUser bool
102 skipAuthPreflight bool
103 skipJwtBearerTokens bool
104 templates *template.Template
105 realClientIPParser ipapi.RealClientIPParser
106 trustedIPs *ip.NetSet
110 sessionChain alice.Chain
111 headersChain alice.Chain
112 preAuthChain alice.Chain
115// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
116func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
117 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
119 return nil, fmt.Errorf("error initialising session store: %v", err)
122 templates := loadTemplates(opts.CustomTemplatesDir)
123 proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
124 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
126 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
129 if opts.SkipJwtBearerTokens {
130 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
131 for _, issuer := range opts.ExtraJwtIssuers {
132 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
135 redirectURL := opts.GetRedirectURL()
136 if redirectURL.Path == "" {
137 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
140 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
141 refresh := "disabled"
142 if opts.Cookie.Refresh != time.Duration(0) {
143 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
146 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)
148 trustedIPs := ip.NewNetSet()
149 for _, ipStr := range opts.TrustedIPs {
150 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
151 trustedIPs.AddIPNet(*ipNet)
153 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
157 var basicAuthValidator basic.Validator
158 if opts.HtpasswdFile != "" {
159 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
161 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
163 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
167 allowedRoutes, err := buildRoutesAllowlist(opts)
172 preAuthChain, err := buildPreAuthChain(opts)
174 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
176 sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
177 headersChain, err := buildHeadersChain(opts)
179 return nil, fmt.Errorf("could not build headers chain: %v", err)
183 CookieName: opts.Cookie.Name,
184 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
185 CookieSeed: opts.Cookie.Secret,
186 CookieDomains: opts.Cookie.Domains,
187 CookiePath: opts.Cookie.Path,
188 CookieSecure: opts.Cookie.Secure,
189 CookieHTTPOnly: opts.Cookie.HTTPOnly,
190 CookieExpire: opts.Cookie.Expire,
191 CookieRefresh: opts.Cookie.Refresh,
192 CookieSameSite: opts.Cookie.SameSite,
193 Validator: validator,
195 RobotsPath: "/robots.txt",
196 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
197 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
198 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
199 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
200 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
201 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
203 ProxyPrefix: opts.ProxyPrefix,
204 ReverseProxy: opts.ReverseProxy,
205 provider: opts.GetProvider(),
206 providerNameOverride: opts.ProviderName,
207 sessionStore: sessionStore,
208 serveMux: upstreamProxy,
209 redirectURL: redirectURL,
210 allowedRoutes: allowedRoutes,
211 whitelistDomains: opts.WhitelistDomains,
212 skipAuthPreflight: opts.SkipAuthPreflight,
213 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
214 realClientIPParser: opts.GetRealClientIPParser(),
215 SkipProviderButton: opts.SkipProviderButton,
216 templates: templates,
217 trustedIPs: trustedIPs,
220 SignInMessage: buildSignInMessage(opts),
222 basicAuthValidator: basicAuthValidator,
223 displayHtpasswdForm: basicAuthValidator != nil && opts.DisplayHtpasswdForm,
224 sessionChain: sessionChain,
225 headersChain: headersChain,
226 preAuthChain: preAuthChain,
230// buildPreAuthChain constructs a chain that should process every request before
231// the OAuth2 Proxy authentication logic kicks in.
232// For example forcing HTTPS or health checks.
233func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
234 chain := alice.New(middleware.NewScope())
237 _, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress)
239 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.HTTPAddress, err)
241 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
244 healthCheckPaths := []string{opts.PingPath}
245 healthCheckUserAgents := []string{opts.PingUserAgent}
246 if opts.GCPHealthChecks {
247 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
248 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
251 // To silence logging of health checks, register the health check handler before
252 // the logging handler
253 if opts.Logging.SilencePing {
254 chain = chain.Append(middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), LoggingHandler)
256 chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents))
262func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
265 if opts.SkipJwtBearerTokens {
266 sessionLoaders := []middlewareapi.TokenToSessionFunc{
267 opts.GetProvider().CreateSessionFromToken,
270 for _, verifier := range opts.GetJWTBearerVerifiers() {
271 sessionLoaders = append(sessionLoaders,
272 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
275 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
278 if validator != nil {
279 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
282 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
283 SessionStore: sessionStore,
284 RefreshPeriod: opts.Cookie.Refresh,
285 RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
286 ValidateSessionState: opts.GetProvider().ValidateSession,
292func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
293 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
295 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
298 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
300 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
303 return alice.New(requestInjector, responseInjector), nil
306func buildSignInMessage(opts *options.Options) string {
308 if len(opts.Banner) >= 1 {
309 if opts.Banner == "-" {
314 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
315 if len(opts.EmailDomains) > 1 {
316 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
317 } else if opts.EmailDomains[0] != "*" {
318 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
324// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
325// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
326// (method=path support)
327func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
328 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
330 for _, path := range opts.SkipAuthRegex {
331 compiledRegex, err := regexp.Compile(path)
335 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
336 routes = append(routes, allowedRoute{
338 pathRegex: compiledRegex,
342 for _, methodPath := range opts.SkipAuthRoutes {
348 parts := strings.SplitN(methodPath, "=", 2)
353 method = strings.ToUpper(parts[0])
357 compiledRegex, err := regexp.Compile(path)
361 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
362 routes = append(routes, allowedRoute{
364 pathRegex: compiledRegex,
371// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
372// redirect clients to once authenticated
373func (p *OAuthProxy) GetRedirectURI(host string) string {
374 // default to the request Host if not set
375 if p.redirectURL.Host != "" {
376 return p.redirectURL.String()
381 u.Scheme = httpsScheme
383 u.Scheme = httpScheme
390func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
392 return nil, providers.ErrMissingCode
394 redirectURI := p.GetRedirectURI(host)
395 s, err := p.provider.Redeem(ctx, redirectURI, code)
402func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
405 s.Email, err = p.provider.GetEmailAddress(ctx, s)
406 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
411 return p.provider.EnrichSession(ctx, s)
414// MakeCSRFCookie creates a cookie for CSRF
415func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
416 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
419func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
420 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
422 if cookieDomain != "" {
423 domain := util.GetRequestHost(req)
424 if h, _, err := net.SplitHostPort(domain); err == nil {
427 if !strings.HasSuffix(domain, cookieDomain) {
428 logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
436 Domain: cookieDomain,
437 HttpOnly: p.CookieHTTPOnly,
438 Secure: p.CookieSecure,
439 Expires: now.Add(expiration),
440 SameSite: cookies.ParseSameSite(p.CookieSameSite),
444// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
446func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
447 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
450// SetCSRFCookie adds a CSRF cookie to the response
451func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
452 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
455// ClearSessionCookie creates a cookie to unset the user's authentication cookie
456// stored in the user's session
457func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
458 return p.sessionStore.Clear(rw, req)
461// LoadCookiedSession reads the user's authentication details from the request
462func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
463 return p.sessionStore.Load(req)
466// SaveSession creates a new session cookie value and sets this on the response
467func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
468 return p.sessionStore.Save(rw, req, s)
471// RobotsTxt disallows scraping pages from the OAuthProxy
472func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
473 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
475 logger.Printf("Error writing robots.txt: %v", err)
476 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
479 rw.WriteHeader(http.StatusOK)
482// ErrorPage writes an error response
483func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
490 Title: fmt.Sprintf("%d %s", code, title),
492 ProxyPrefix: p.ProxyPrefix,
494 err := p.templates.ExecuteTemplate(rw, "error.html", t)
496 logger.Printf("Error rendering error.html template: %v", err)
497 http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
501// SignInPage writes the sing in template to the response
502func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
504 err := p.ClearSessionCookie(rw, req)
506 logger.Printf("Error clearing session cookie: %v", err)
507 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
512 redirectURL, err := p.GetRedirect(req)
514 logger.Errorf("Error obtaining redirect: %v", err)
515 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
519 if redirectURL == p.SignInPath {
523 // We allow unescaped template.HTML since it is user configured options
527 SignInMessage template.HTML
534 ProviderName: p.provider.Data().ProviderName,
535 SignInMessage: template.HTML(p.SignInMessage),
536 CustomLogin: p.displayHtpasswdForm,
537 Redirect: redirectURL,
539 ProxyPrefix: p.ProxyPrefix,
540 Footer: template.HTML(p.Footer),
542 if p.providerNameOverride != "" {
543 t.ProviderName = p.providerNameOverride
545 err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
547 logger.Printf("Error rendering sign_in.html template: %v", err)
548 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
552// ManualSignIn handles basic auth logins to the proxy
553func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
554 if req.Method != "POST" || p.basicAuthValidator == nil {
557 user := req.FormValue("username")
558 passwd := req.FormValue("password")
563 if p.basicAuthValidator.Validate(user, passwd) {
564 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
567 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
571// GetRedirect reads the query parameter to get the URL to redirect clients to
572// once authenticated with the OAuthProxy
573func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
574 err = req.ParseForm()
579 redirect = req.Header.Get("X-Auth-Request-Redirect")
580 if req.Form.Get("rd") != "" {
581 redirect = req.Form.Get("rd")
583 // Quirk: On reverse proxies that doesn't have support for
584 // "X-Auth-Request-Redirect" header or dynamic header/query string
585 // manipulation (like Traefik v1 and v2), we can try if the header
586 // X-Forwarded-Host exists or not.
587 if redirect == "" && isForwardedRequest(req, p.ReverseProxy) {
588 redirect = p.getRedirectFromForwardHeaders(req)
590 if !p.IsValidRedirect(redirect) {
591 // Use RequestURI to preserve ?query
592 redirect = req.URL.RequestURI()
594 if strings.HasPrefix(redirect, fmt.Sprintf("%s/", p.ProxyPrefix)) {
602// getRedirectFromForwardHeaders returns the redirect URL based on X-Forwarded-{Proto,Host,Uri} headers
603func (p *OAuthProxy) getRedirectFromForwardHeaders(req *http.Request) string {
604 uri := util.GetRequestURI(req)
606 if strings.HasPrefix(uri, fmt.Sprintf("%s/", p.ProxyPrefix)) {
610 return fmt.Sprintf("%s://%s%s", util.GetRequestProto(req), util.GetRequestHost(req), uri)
613// splitHostPort separates host and port. If the port is not valid, it returns
614// the entire input as host, and it doesn't check the validity of the host.
615// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
616// *** taken from net/url, modified validOptionalPort() to accept ":*"
617func splitHostPort(hostport string) (host, port string) {
620 colon := strings.LastIndexByte(host, ':')
621 if colon != -1 && validOptionalPort(host[colon:]) {
622 host, port = host[:colon], host[colon+1:]
625 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
626 host = host[1 : len(host)-1]
632// validOptionalPort reports whether port is either an empty string
633// or matches /^:\d*$/
634// *** taken from net/url, modified to accept ":*"
635func validOptionalPort(port string) bool {
636 if port == "" || port == ":*" {
642 for _, b := range port[1:] {
643 if b < '0' || b > '9' {
650// IsValidRedirect checks whether the redirect URL is whitelisted
651func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
654 // The user didn't specify a redirect, should fallback to `/`
656 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
658 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
659 redirectURL, err := url.Parse(redirect)
661 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
664 redirectHostname := redirectURL.Hostname()
666 for _, domain := range p.whitelistDomains {
667 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
668 if domainHostname == "" {
672 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
673 // the domain names match, now validate the ports
674 // if the whitelisted domain's port is '*', allow all ports
675 // if the whitelisted domain contains a specific port, only allow that port
676 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
677 redirectPort := redirectURL.Port()
678 if (domainPort == "*") ||
679 (domainPort == redirectPort) ||
680 (domainPort == "" && redirectPort == "") {
686 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
689 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
694// IsAllowedRequest is used to check if auth should be skipped for this request
695func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
696 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
697 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
700// IsAllowedRoute is used to check if the request method & path is allowed without auth
701func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
702 for _, route := range p.allowedRoutes {
703 if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
710// isForwardedRequest is used to check if X-Forwarded-Host header exists or not
711func isForwardedRequest(req *http.Request, reverseProxy bool) bool {
712 isForwarded := req.Host != util.GetRequestHost(req)
713 return isForwarded && reverseProxy
716// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
717var noCacheHeaders = map[string]string{
718 "Expires": time.Unix(0, 0).Format(time.RFC1123),
719 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
720 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
723// prepareNoCache prepares headers for preventing browser caching.
724func prepareNoCache(w http.ResponseWriter) {
725 // Set NoCache headers
726 for k, v := range noCacheHeaders {
731// IsTrustedIP is used to check if a request comes from a trusted client IP address.
732func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
733 if p.trustedIPs == nil {
737 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
739 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
740 // Possibly spoofed X-Real-IP header
744 if remoteAddr == nil {
748 return p.trustedIPs.Has(remoteAddr)
751func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
752 p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
755func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
756 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
760 switch path := req.URL.Path; {
761 case path == p.RobotsPath:
763 case p.IsAllowedRequest(req):
764 p.SkipAuthProxy(rw, req)
765 case path == p.SignInPath:
767 case path == p.SignOutPath:
769 case path == p.OAuthStartPath:
770 p.OAuthStart(rw, req)
771 case path == p.OAuthCallbackPath:
772 p.OAuthCallback(rw, req)
773 case path == p.AuthOnlyPath:
775 case path == p.UserInfoPath:
782// SignIn serves a page prompting users to sign in
783func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
784 redirect, err := p.GetRedirect(req)
786 logger.Errorf("Error obtaining redirect: %v", err)
787 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
791 user, ok := p.ManualSignIn(req)
793 session := &sessionsapi.SessionState{User: user}
794 err = p.SaveSession(rw, req, session)
796 logger.Printf("Error saving session: %v", err)
797 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
800 http.Redirect(rw, req, redirect, http.StatusFound)
802 if p.SkipProviderButton {
803 p.OAuthStart(rw, req)
805 p.SignInPage(rw, req, http.StatusOK)
810//UserInfo endpoint outputs session email and preferred username in JSON format
811func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
813 session, err := p.getAuthenticatedSession(rw, req)
815 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
820 User string `json:"user"`
821 Email string `json:"email"`
822 Groups []string `json:"groups,omitempty"`
823 PreferredUsername string `json:"preferredUsername,omitempty"`
826 Email: session.Email,
827 Groups: session.Groups,
828 PreferredUsername: session.PreferredUsername,
831 rw.Header().Set("Content-Type", "application/json")
832 rw.WriteHeader(http.StatusOK)
833 err = json.NewEncoder(rw).Encode(userInfo)
835 logger.Printf("Error encoding user info: %v", err)
836 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
840// SignOut sends a response to clear the authentication cookie
841func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
842 redirect, err := p.GetRedirect(req)
844 logger.Errorf("Error obtaining redirect: %v", err)
845 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
848 err = p.ClearSessionCookie(rw, req)
850 logger.Errorf("Error clearing session cookie: %v", err)
851 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
854 http.Redirect(rw, req, redirect, http.StatusFound)
857// OAuthStart starts the OAuth2 authentication flow
858func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
860 nonce, err := encryption.Nonce()
862 logger.Errorf("Error obtaining nonce: %v", err)
863 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
866 p.SetCSRFCookie(rw, req, nonce)
867 redirect, err := p.GetRedirect(req)
869 logger.Errorf("Error obtaining redirect: %v", err)
870 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
873 redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
874 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
877// OAuthCallback is the OAuth2 authentication flow callback that finishes the
878// OAuth2 authentication flow
879func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
880 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
882 // finish the oauth cycle
883 err := req.ParseForm()
885 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
886 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
889 errorString := req.Form.Get("error")
890 if errorString != "" {
891 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
892 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
896 session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
898 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
899 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
903 err = p.enrichSessionState(req.Context(), session)
905 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
906 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
910 state := strings.SplitN(req.Form.Get("state"), ":", 2)
912 logger.Error("Error while parsing OAuth2 state: invalid length")
913 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
918 c, err := req.Cookie(p.CSRFCookieName)
920 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
921 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
924 p.ClearCSRFCookie(rw, req)
925 if c.Value != nonce {
926 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
927 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
931 if !p.IsValidRedirect(redirect) {
935 // set cookie, or deny
936 authorized, err := p.provider.Authorize(req.Context(), session)
938 logger.Errorf("Error with authorization: %v", err)
940 if p.Validator(session.Email) && authorized {
941 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
942 err := p.SaveSession(rw, req, session)
944 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
945 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
948 http.Redirect(rw, req, redirect, http.StatusFound)
950 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
951 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
955// AuthOnly checks whether the user is currently logged in (both authentication
956// and optional authorization).
957func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
958 session, err := p.getAuthenticatedSession(rw, req)
960 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
964 // Unauthorized cases need to return 403 to prevent infinite redirects with
965 // subrequest architectures
966 if !authOnlyAuthorize(req, session) {
967 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
971 // we are authenticated
972 p.addHeadersForProxying(rw, req, session)
973 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
974 rw.WriteHeader(http.StatusAccepted)
975 })).ServeHTTP(rw, req)
978// SkipAuthProxy proxies allowlisted requests and skips authentication
979func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
980 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
983// Proxy proxies the user request if the user is authenticated else it prompts
984// them to authenticate
985func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
986 session, err := p.getAuthenticatedSession(rw, req)
989 // we are authenticated
990 p.addHeadersForProxying(rw, req, session)
991 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
993 // we need to send the user to a login screen
995 // no point redirecting an AJAX request
996 p.ErrorJSON(rw, http.StatusUnauthorized)
1000 if p.SkipProviderButton {
1001 p.OAuthStart(rw, req)
1003 p.SignInPage(rw, req, http.StatusForbidden)
1006 case ErrAccessDenied:
1007 p.ErrorPage(rw, http.StatusUnauthorized, "Permission Denied", "Unauthorized")
1011 logger.Errorf("Unexpected internal error: %v", err)
1012 p.ErrorPage(rw, http.StatusInternalServerError,
1013 "Internal Error", "Internal Error")
1018// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
1020// - `nil, ErrNeedsLogin` if user needs to login.
1021// - `nil, ErrAccessDenied` if the authenticated user is not authorized
1022// Set-Cookie headers may be set on the response as a side-effect of calling this method.
1023func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
1024 var session *sessionsapi.SessionState
1026 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1027 session = middleware.GetRequestScope(req).Session
1029 getSession.ServeHTTP(rw, req)
1032 return nil, ErrNeedsLogin
1035 invalidEmail := session.Email != "" && !p.Validator(session.Email)
1036 authorized, err := p.provider.Authorize(req.Context(), session)
1038 logger.Errorf("Error with authorization: %v", err)
1041 if invalidEmail || !authorized {
1042 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session: removing session %s", session)
1043 // Invalid session, clear it
1044 err := p.ClearSessionCookie(rw, req)
1046 logger.Errorf("Error clearing session cookie: %v", err)
1048 return nil, ErrAccessDenied
1054// authOnlyAuthorize handles special authorization logic that is only done
1055// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1057// TODO (@NickMeves): This method is a placeholder to be extended but currently
1058// fails the linter. Remove the nolint when functionality expands.
1061func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1062 // Allow secondary group restrictions based on the `allowed_groups`
1063 // querystring parameter
1064 if !checkAllowedGroups(req, s) {
1071func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1072 allowedGroups := extractAllowedGroups(req)
1073 if len(allowedGroups) == 0 {
1077 for _, group := range s.Groups {
1078 if _, ok := allowedGroups[group]; ok {
1086func extractAllowedGroups(req *http.Request) map[string]struct{} {
1087 groups := map[string]struct{}{}
1089 query := req.URL.Query()
1090 for _, allowedGroups := range query["allowed_groups"] {
1091 for _, group := range strings.Split(allowedGroups, ",") {
1093 groups[group] = struct{}{}
1101// addHeadersForProxying adds the appropriate headers the request / response for proxying
1102func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1103 if session.Email == "" {
1104 rw.Header().Set("GAP-Auth", session.User)
1106 rw.Header().Set("GAP-Auth", session.Email)
1110// isAjax checks if a request is an ajax request
1111func isAjax(req *http.Request) bool {
1112 acceptValues := req.Header.Values("Accept")
1113 const ajaxReq = applicationJSON
1114 for _, v := range acceptValues {
1122// ErrorJSON returns the error code with an application/json mime type
1123func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1124 rw.Header().Set("Content-Type", applicationJSON)
1125 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