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 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
27 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"
39 applicationJSON = "application/json"
43 // ErrNeedsLogin means the user should be redirected to the login page
44 ErrNeedsLogin = errors.New("redirect to login page")
46 // ErrAccessDenied means the user should receive a 401 Unauthorized response
47 ErrAccessDenied = errors.New("access denied")
49 // Used to check final redirects are not susceptible to open redirects.
50 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
51 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
54// allowedRoute manages method + path based allowlists
55type allowedRoute struct {
57 pathRegex *regexp.Regexp
60// OAuthProxy is the main authentication proxy
61type OAuthProxy struct {
65 CookieDomains []string
69 CookieExpire time.Duration
70 CookieRefresh time.Duration
72 Validator func(string) bool
78 OAuthCallbackPath string
82 allowedRoutes []allowedRoute
83 redirectURL *url.URL // the url to receive requests at
84 whitelistDomains []string
85 provider providers.Provider
86 sessionStore sessionsapi.SessionStore
88 basicAuthValidator basic.Validator
93 SkipProviderButton bool
95 BasicAuthPassword string
98 PassAuthorization bool
99 PreferEmailToUser bool
100 skipAuthPreflight bool
101 skipJwtBearerTokens bool
102 realClientIPParser ipapi.RealClientIPParser
103 trustedIPs *ip.NetSet
105 sessionChain alice.Chain
106 headersChain alice.Chain
107 preAuthChain alice.Chain
108 pageWriter pagewriter.Writer
109 server proxyhttp.Server
112// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
113func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
114 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
116 return nil, fmt.Errorf("error initialising session store: %v", err)
119 var basicAuthValidator basic.Validator
120 if opts.HtpasswdFile != "" {
121 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
123 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
125 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
129 pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
130 TemplatesPath: opts.Templates.Path,
131 CustomLogo: opts.Templates.CustomLogo,
132 ProxyPrefix: opts.ProxyPrefix,
133 Footer: opts.Templates.Footer,
135 Debug: opts.Templates.Debug,
136 ProviderName: buildProviderName(opts.GetProvider(), opts.ProviderName),
137 SignInMessage: buildSignInMessage(opts),
138 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
141 return nil, fmt.Errorf("error initialising page writer: %v", err)
144 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter.ProxyErrorHandler)
146 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
149 if opts.SkipJwtBearerTokens {
150 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
151 for _, issuer := range opts.ExtraJwtIssuers {
152 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
155 redirectURL := opts.GetRedirectURL()
156 if redirectURL.Path == "" {
157 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
160 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
161 refresh := "disabled"
162 if opts.Cookie.Refresh != time.Duration(0) {
163 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
166 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)
168 trustedIPs := ip.NewNetSet()
169 for _, ipStr := range opts.TrustedIPs {
170 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
171 trustedIPs.AddIPNet(*ipNet)
173 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
177 allowedRoutes, err := buildRoutesAllowlist(opts)
182 preAuthChain, err := buildPreAuthChain(opts)
184 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
186 sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
187 headersChain, err := buildHeadersChain(opts)
189 return nil, fmt.Errorf("could not build headers chain: %v", err)
193 CookieName: opts.Cookie.Name,
194 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
195 CookieSeed: opts.Cookie.Secret,
196 CookieDomains: opts.Cookie.Domains,
197 CookiePath: opts.Cookie.Path,
198 CookieSecure: opts.Cookie.Secure,
199 CookieHTTPOnly: opts.Cookie.HTTPOnly,
200 CookieExpire: opts.Cookie.Expire,
201 CookieRefresh: opts.Cookie.Refresh,
202 CookieSameSite: opts.Cookie.SameSite,
203 Validator: validator,
205 RobotsPath: "/robots.txt",
206 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
207 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
208 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
209 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
210 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
211 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
213 ProxyPrefix: opts.ProxyPrefix,
214 provider: opts.GetProvider(),
215 sessionStore: sessionStore,
216 serveMux: upstreamProxy,
217 redirectURL: redirectURL,
218 allowedRoutes: allowedRoutes,
219 whitelistDomains: opts.WhitelistDomains,
220 skipAuthPreflight: opts.SkipAuthPreflight,
221 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
222 realClientIPParser: opts.GetRealClientIPParser(),
223 SkipProviderButton: opts.SkipProviderButton,
224 trustedIPs: trustedIPs,
226 basicAuthValidator: basicAuthValidator,
227 sessionChain: sessionChain,
228 headersChain: headersChain,
229 preAuthChain: preAuthChain,
230 pageWriter: pageWriter,
233 if err := p.setupServer(opts); err != nil {
234 return nil, fmt.Errorf("error setting up server: %v", err)
240func (p *OAuthProxy) Start() error {
242 // We have to call setupServer before Start is called.
243 // If this doesn't happen it's a programming error.
244 panic("server has not been initialised")
247 ctx, cancel := context.WithCancel(context.Background())
249 // Observe signals in background goroutine.
251 sigint := make(chan os.Signal, 1)
252 signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
254 cancel() // cancel the context
257 return p.server.Start(ctx)
260func (p *OAuthProxy) setupServer(opts *options.Options) error {
261 serverOpts := proxyhttp.Opts{
263 BindAddress: opts.Server.BindAddress,
264 SecureBindAddress: opts.Server.SecureBindAddress,
265 TLS: opts.Server.TLS,
268 appServer, err := proxyhttp.NewServer(serverOpts)
270 return fmt.Errorf("could not build app server: %v", err)
273 metricsServer, err := proxyhttp.NewServer(proxyhttp.Opts{
274 Handler: middleware.DefaultMetricsHandler,
275 BindAddress: opts.MetricsServer.BindAddress,
276 SecureBindAddress: opts.MetricsServer.BindAddress,
277 TLS: opts.MetricsServer.TLS,
280 return fmt.Errorf("could not build metrics server: %v", err)
283 p.server = proxyhttp.NewServerGroup(appServer, metricsServer)
287// buildPreAuthChain constructs a chain that should process every request before
288// the OAuth2 Proxy authentication logic kicks in.
289// For example forcing HTTPS or health checks.
290func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
291 chain := alice.New(middleware.NewScope(opts.ReverseProxy))
294 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
296 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err)
298 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
301 healthCheckPaths := []string{opts.PingPath}
302 healthCheckUserAgents := []string{opts.PingUserAgent}
303 if opts.GCPHealthChecks {
304 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
305 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
308 // To silence logging of health checks, register the health check handler before
309 // the logging handler
310 if opts.Logging.SilencePing {
311 chain = chain.Append(
312 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
313 middleware.NewRequestLogger(),
316 chain = chain.Append(
317 middleware.NewRequestLogger(),
318 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
322 chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry())
327func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
330 if opts.SkipJwtBearerTokens {
331 sessionLoaders := []middlewareapi.TokenToSessionFunc{
332 opts.GetProvider().CreateSessionFromToken,
335 for _, verifier := range opts.GetJWTBearerVerifiers() {
336 sessionLoaders = append(sessionLoaders,
337 middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
340 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
343 if validator != nil {
344 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups))
347 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
348 SessionStore: sessionStore,
349 RefreshPeriod: opts.Cookie.Refresh,
350 RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
351 ValidateSessionState: opts.GetProvider().ValidateSession,
357func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
358 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
360 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
363 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
365 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
368 return alice.New(requestInjector, responseInjector), nil
371func buildSignInMessage(opts *options.Options) string {
373 if len(opts.Templates.Banner) >= 1 {
374 if opts.Templates.Banner == "-" {
377 msg = opts.Templates.Banner
379 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
380 if len(opts.EmailDomains) > 1 {
381 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
382 } else if opts.EmailDomains[0] != "*" {
383 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
389func buildProviderName(p providers.Provider, override string) string {
393 return p.Data().ProviderName
396// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
397// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
398// (method=path support)
399func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
400 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
402 for _, path := range opts.SkipAuthRegex {
403 compiledRegex, err := regexp.Compile(path)
407 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
408 routes = append(routes, allowedRoute{
410 pathRegex: compiledRegex,
414 for _, methodPath := range opts.SkipAuthRoutes {
420 parts := strings.SplitN(methodPath, "=", 2)
425 method = strings.ToUpper(parts[0])
429 compiledRegex, err := regexp.Compile(path)
433 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
434 routes = append(routes, allowedRoute{
436 pathRegex: compiledRegex,
443// MakeCSRFCookie creates a cookie for CSRF
444func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
445 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
448func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
449 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
451 if cookieDomain != "" {
452 domain := requestutil.GetRequestHost(req)
453 if h, _, err := net.SplitHostPort(domain); err == nil {
456 if !strings.HasSuffix(domain, cookieDomain) {
457 logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
465 Domain: cookieDomain,
466 HttpOnly: p.CookieHTTPOnly,
467 Secure: p.CookieSecure,
468 Expires: now.Add(expiration),
469 SameSite: cookies.ParseSameSite(p.CookieSameSite),
473// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
475func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
476 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
479// SetCSRFCookie adds a CSRF cookie to the response
480func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
481 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
484// ClearSessionCookie creates a cookie to unset the user's authentication cookie
485// stored in the user's session
486func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
487 return p.sessionStore.Clear(rw, req)
490// LoadCookiedSession reads the user's authentication details from the request
491func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
492 return p.sessionStore.Load(req)
495// SaveSession creates a new session cookie value and sets this on the response
496func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
497 return p.sessionStore.Save(rw, req, s)
500// IsValidRedirect checks whether the redirect URL is whitelisted
501func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
504 // The user didn't specify a redirect, should fallback to `/`
506 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
508 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
509 redirectURL, err := url.Parse(redirect)
511 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
514 redirectHostname := redirectURL.Hostname()
516 for _, allowedDomain := range p.whitelistDomains {
517 allowedHost, allowedPort := splitHostPort(allowedDomain)
518 if allowedHost == "" {
522 if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
523 (strings.HasPrefix(allowedHost, ".") &&
524 strings.HasSuffix(redirectHostname, allowedHost)) {
525 // the domain names match, now validate the ports
526 // if the whitelisted domain's port is '*', allow all ports
527 // if the whitelisted domain contains a specific port, only allow that port
528 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
529 redirectPort := redirectURL.Port()
530 if allowedPort == "*" ||
531 allowedPort == redirectPort ||
532 (allowedPort == "" && redirectPort == "") {
538 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
541 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
546func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
547 p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
550func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
551 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
555 switch path := req.URL.Path; {
556 case path == p.RobotsPath:
558 case p.IsAllowedRequest(req):
559 p.SkipAuthProxy(rw, req)
560 case path == p.SignInPath:
562 case path == p.SignOutPath:
564 case path == p.OAuthStartPath:
565 p.OAuthStart(rw, req)
566 case path == p.OAuthCallbackPath:
567 p.OAuthCallback(rw, req)
568 case path == p.AuthOnlyPath:
570 case path == p.UserInfoPath:
577// RobotsTxt disallows scraping pages from the OAuthProxy
578func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter, req *http.Request) {
579 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
581 logger.Printf("Error writing robots.txt: %v", err)
582 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
585 rw.WriteHeader(http.StatusOK)
588// ErrorPage writes an error response
589func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
590 redirectURL, err := p.getAppRedirect(req)
592 logger.Errorf("Error obtaining redirect: %v", err)
594 if redirectURL == p.SignInPath || redirectURL == "" {
598 p.pageWriter.WriteErrorPage(rw, code, redirectURL, appError, messages...)
601// IsAllowedRequest is used to check if auth should be skipped for this request
602func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
603 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
604 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
607// IsAllowedRoute is used to check if the request method & path is allowed without auth
608func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
609 for _, route := range p.allowedRoutes {
610 if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
617// isTrustedIP is used to check if a request comes from a trusted client IP address.
618func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
619 if p.trustedIPs == nil {
623 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
625 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
626 // Possibly spoofed X-Real-IP header
630 if remoteAddr == nil {
634 return p.trustedIPs.Has(remoteAddr)
637// SignInPage writes the sing in template to the response
638func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
640 err := p.ClearSessionCookie(rw, req)
642 logger.Printf("Error clearing session cookie: %v", err)
643 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
648 redirectURL, err := p.getAppRedirect(req)
650 logger.Errorf("Error obtaining redirect: %v", err)
651 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
655 if redirectURL == p.SignInPath {
659 p.pageWriter.WriteSignInPage(rw, redirectURL)
662// ManualSignIn handles basic auth logins to the proxy
663func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
664 if req.Method != "POST" || p.basicAuthValidator == nil {
667 user := req.FormValue("username")
668 passwd := req.FormValue("password")
673 if p.basicAuthValidator.Validate(user, passwd) {
674 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
677 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
681// SignIn serves a page prompting users to sign in
682func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
683 redirect, err := p.getAppRedirect(req)
685 logger.Errorf("Error obtaining redirect: %v", err)
686 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
690 user, ok := p.ManualSignIn(req)
692 session := &sessionsapi.SessionState{User: user}
693 err = p.SaveSession(rw, req, session)
695 logger.Printf("Error saving session: %v", err)
696 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
699 http.Redirect(rw, req, redirect, http.StatusFound)
701 if p.SkipProviderButton {
702 p.OAuthStart(rw, req)
704 p.SignInPage(rw, req, http.StatusOK)
709// UserInfo endpoint outputs session email and preferred username in JSON format
710func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
712 session, err := p.getAuthenticatedSession(rw, req)
714 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
719 User string `json:"user"`
720 Email string `json:"email"`
721 Groups []string `json:"groups,omitempty"`
722 PreferredUsername string `json:"preferredUsername,omitempty"`
725 Email: session.Email,
726 Groups: session.Groups,
727 PreferredUsername: session.PreferredUsername,
730 rw.Header().Set("Content-Type", "application/json")
731 rw.WriteHeader(http.StatusOK)
732 err = json.NewEncoder(rw).Encode(userInfo)
734 logger.Printf("Error encoding user info: %v", err)
735 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
739// SignOut sends a response to clear the authentication cookie
740func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
741 redirect, err := p.getAppRedirect(req)
743 logger.Errorf("Error obtaining redirect: %v", err)
744 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
747 err = p.ClearSessionCookie(rw, req)
749 logger.Errorf("Error clearing session cookie: %v", err)
750 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
753 http.Redirect(rw, req, redirect, http.StatusFound)
756// OAuthStart starts the OAuth2 authentication flow
757func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
759 nonce, err := encryption.Nonce()
761 logger.Errorf("Error obtaining nonce: %v", err)
762 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
765 p.SetCSRFCookie(rw, req, nonce)
766 redirect, err := p.getAppRedirect(req)
768 logger.Errorf("Error obtaining redirect: %v", err)
769 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
772 redirectURI := p.getOAuthRedirectURI(req)
773 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
776// OAuthCallback is the OAuth2 authentication flow callback that finishes the
777// OAuth2 authentication flow
778func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
779 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
781 // finish the oauth cycle
782 err := req.ParseForm()
784 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
785 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
788 errorString := req.Form.Get("error")
789 if errorString != "" {
790 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
791 message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString)
792 // Set the debug message and override the non debug message to be the same for this case
793 p.ErrorPage(rw, req, http.StatusForbidden, message, message)
797 session, err := p.redeemCode(req)
799 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
800 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
804 err = p.enrichSessionState(req.Context(), session)
806 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
807 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
811 state := strings.SplitN(req.Form.Get("state"), ":", 2)
813 logger.Error("Error while parsing OAuth2 state: invalid length")
814 p.ErrorPage(rw, req, http.StatusInternalServerError, "State paremeter did not have expected length", "Login Failed: Invalid State after login.")
819 c, err := req.Cookie(p.CSRFCookieName)
821 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
822 p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
825 p.ClearCSRFCookie(rw, req)
826 if c.Value != nonce {
827 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
828 p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
832 if !p.IsValidRedirect(redirect) {
836 // set cookie, or deny
837 authorized, err := p.provider.Authorize(req.Context(), session)
839 logger.Errorf("Error with authorization: %v", err)
841 if p.Validator(session.Email) && authorized {
842 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
843 err := p.SaveSession(rw, req, session)
845 logger.Errorf("Error saving session state for %s: %v", remoteAddr, err)
846 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
849 http.Redirect(rw, req, redirect, http.StatusFound)
851 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
852 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
856func (p *OAuthProxy) redeemCode(req *http.Request) (*sessionsapi.SessionState, error) {
857 code := req.Form.Get("code")
859 return nil, providers.ErrMissingCode
862 redirectURI := p.getOAuthRedirectURI(req)
863 s, err := p.provider.Redeem(req.Context(), redirectURI, code)
870func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
873 // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession
874 // nolint:staticcheck
875 s.Email, err = p.provider.GetEmailAddress(ctx, s)
876 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
881 return p.provider.EnrichSession(ctx, s)
884// AuthOnly checks whether the user is currently logged in (both authentication
885// and optional authorization).
886func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
887 session, err := p.getAuthenticatedSession(rw, req)
889 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
893 // Unauthorized cases need to return 403 to prevent infinite redirects with
894 // subrequest architectures
895 if !authOnlyAuthorize(req, session) {
896 http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
900 // we are authenticated
901 p.addHeadersForProxying(rw, session)
902 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
903 rw.WriteHeader(http.StatusAccepted)
904 })).ServeHTTP(rw, req)
907// SkipAuthProxy proxies allowlisted requests and skips authentication
908func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
909 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
912// Proxy proxies the user request if the user is authenticated else it prompts
913// them to authenticate
914func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
915 session, err := p.getAuthenticatedSession(rw, req)
918 // we are authenticated
919 p.addHeadersForProxying(rw, session)
920 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
922 // we need to send the user to a login screen
924 // no point redirecting an AJAX request
925 p.errorJSON(rw, http.StatusUnauthorized)
929 if p.SkipProviderButton {
930 p.OAuthStart(rw, req)
932 p.SignInPage(rw, req, http.StatusForbidden)
935 case ErrAccessDenied:
936 p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks")
940 logger.Errorf("Unexpected internal error: %v", err)
941 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
945// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
946var noCacheHeaders = map[string]string{
947 "Expires": time.Unix(0, 0).Format(time.RFC1123),
948 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
949 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
952// prepareNoCache prepares headers for preventing browser caching.
953func prepareNoCache(w http.ResponseWriter) {
954 // Set NoCache headers
955 for k, v := range noCacheHeaders {
960// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
961// redirect clients to once authenticated.
962// This is usually the OAuthProxy callback URL.
963func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
964 // if `p.redirectURL` already has a host, return it
965 if p.redirectURL.Host != "" {
966 return p.redirectURL.String()
969 // Otherwise figure out the scheme + host from the request
971 rd.Host = requestutil.GetRequestHost(req)
972 rd.Scheme = requestutil.GetRequestProto(req)
974 // If CookieSecure is true, return `https` no matter what
975 // Not all reverse proxies set X-Forwarded-Proto
977 rd.Scheme = schemeHTTPS
982// getAppRedirect determines the full URL or URI path to redirect clients to
983// once authenticated with the OAuthProxy
984// Strategy priority (first legal result is used):
985// - `rd` querysting parameter
986// - `X-Auth-Request-Redirect` header
987// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
988// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
989// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
990// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
992func (p *OAuthProxy) getAppRedirect(req *http.Request) (string, error) {
993 err := req.ParseForm()
998 // These redirect getter functions are strategies ordered by priority
999 // for figuring out the redirect URL.
1000 type redirectGetter func(req *http.Request) string
1001 for _, rdGetter := range []redirectGetter{
1002 p.getRdQuerystringRedirect,
1003 p.getXAuthRequestRedirect,
1004 p.getXForwardedHeadersRedirect,
1007 redirect := rdGetter(req)
1008 // Call `p.IsValidRedirect` again here a final time to be safe
1009 if redirect != "" && p.IsValidRedirect(redirect) {
1010 return redirect, nil
1017func isForwardedRequest(req *http.Request) bool {
1018 return requestutil.IsProxied(req) &&
1019 req.Host != requestutil.GetRequestHost(req)
1022func (p *OAuthProxy) hasProxyPrefix(path string) bool {
1023 return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
1026func (p *OAuthProxy) validateRedirect(redirect string, errorFormat string) string {
1027 if p.IsValidRedirect(redirect) {
1031 logger.Errorf(errorFormat, redirect)
1036// getRdQuerystringRedirect handles this getAppRedirect strategy:
1037// - `rd` querysting parameter
1038func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
1039 return p.validateRedirect(
1041 "Invalid redirect provided in rd querystring parameter: %s",
1045// getXAuthRequestRedirect handles this getAppRedirect strategy:
1046// - `X-Auth-Request-Redirect` Header
1047func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
1048 return p.validateRedirect(
1049 req.Header.Get("X-Auth-Request-Redirect"),
1050 "Invalid redirect provided in X-Auth-Request-Redirect header: %s",
1054// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
1055// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
1056// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
1057func (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
1058 if !isForwardedRequest(req) {
1062 uri := requestutil.GetRequestURI(req)
1063 if p.hasProxyPrefix(uri) {
1067 redirect := fmt.Sprintf(
1069 requestutil.GetRequestProto(req),
1070 requestutil.GetRequestHost(req),
1074 return p.validateRedirect(redirect,
1075 "Invalid redirect generated from X-Forwarded-* headers: %s")
1078// getURIRedirect handles these getAppRedirect strategies:
1079// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
1080// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
1082func (p *OAuthProxy) getURIRedirect(req *http.Request) string {
1083 redirect := p.validateRedirect(
1084 requestutil.GetRequestURI(req),
1085 "Invalid redirect generated from X-Forwarded-Uri header: %s",
1088 redirect = req.URL.RequestURI()
1091 if p.hasProxyPrefix(redirect) {
1097// splitHostPort separates host and port. If the port is not valid, it returns
1098// the entire input as host, and it doesn't check the validity of the host.
1099// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
1100// *** taken from net/url, modified validOptionalPort() to accept ":*"
1101func splitHostPort(hostport string) (host, port string) {
1104 colon := strings.LastIndexByte(host, ':')
1105 if colon != -1 && validOptionalPort(host[colon:]) {
1106 host, port = host[:colon], host[colon+1:]
1109 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
1110 host = host[1 : len(host)-1]
1116// validOptionalPort reports whether port is either an empty string
1117// or matches /^:\d*$/
1118// *** taken from net/url, modified to accept ":*"
1119func validOptionalPort(port string) bool {
1120 if port == "" || port == ":*" {
1126 for _, b := range port[1:] {
1127 if b < '0' || b > '9' {
1134// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
1136// - `nil, ErrNeedsLogin` if user needs to login.
1137// - `nil, ErrAccessDenied` if the authenticated user is not authorized
1138// Set-Cookie headers may be set on the response as a side-effect of calling this method.
1139func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
1140 var session *sessionsapi.SessionState
1142 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1143 session = middlewareapi.GetRequestScope(req).Session
1145 getSession.ServeHTTP(rw, req)
1148 return nil, ErrNeedsLogin
1151 invalidEmail := session.Email != "" && !p.Validator(session.Email)
1152 authorized, err := p.provider.Authorize(req.Context(), session)
1154 logger.Errorf("Error with authorization: %v", err)
1157 if invalidEmail || !authorized {
1158 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session: removing session %s", session)
1159 // Invalid session, clear it
1160 err := p.ClearSessionCookie(rw, req)
1162 logger.Errorf("Error clearing session cookie: %v", err)
1164 return nil, ErrAccessDenied
1170// authOnlyAuthorize handles special authorization logic that is only done
1171// on the AuthOnly endpoint for use with Nginx subrequest architectures.
1173// TODO (@NickMeves): This method is a placeholder to be extended but currently
1174// fails the linter. Remove the nolint when functionality expands.
1177func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
1178 // Allow secondary group restrictions based on the `allowed_groups`
1179 // querystring parameter
1180 if !checkAllowedGroups(req, s) {
1187func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1188 allowedGroups := extractAllowedGroups(req)
1189 if len(allowedGroups) == 0 {
1193 for _, group := range s.Groups {
1194 if _, ok := allowedGroups[group]; ok {
1202func extractAllowedGroups(req *http.Request) map[string]struct{} {
1203 groups := map[string]struct{}{}
1205 query := req.URL.Query()
1206 for _, allowedGroups := range query["allowed_groups"] {
1207 for _, group := range strings.Split(allowedGroups, ",") {
1209 groups[group] = struct{}{}
1217// addHeadersForProxying adds the appropriate headers the request / response for proxying
1218func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
1219 if session.Email == "" {
1220 rw.Header().Set("GAP-Auth", session.User)
1222 rw.Header().Set("GAP-Auth", session.Email)
1226// isAjax checks if a request is an ajax request
1227func isAjax(req *http.Request) bool {
1228 acceptValues := req.Header.Values("Accept")
1229 const ajaxReq = applicationJSON
1230 // Iterate over multiple Accept headers, i.e.
1231 // Accept: application/json
1232 // Accept: text/plain
1233 for _, mimeTypes := range acceptValues {
1234 // Iterate over multiple mimetypes in a single header, i.e.
1235 // Accept: application/json, text/plain, */*
1236 for _, mimeType := range strings.Split(mimeTypes, ",") {
1237 mimeType = strings.TrimSpace(mimeType)
1238 if mimeType == ajaxReq {
1246// errorJSON returns the error code with an application/json mime type
1247func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) {
1248 rw.Header().Set("Content-Type", applicationJSON)
1249 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