16 "github.com/coreos/go-oidc"
17 "github.com/justinas/alice"
18 ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
19 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
21 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
22 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
30 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
31 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
38 applicationJSON = "application/json"
42 // ErrNeedsLogin means the user should be redirected to the login page
43 ErrNeedsLogin = errors.New("redirect to login page")
45 // Used to check final redirects are not susceptible to open redirects.
46 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
47 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
50// allowedRoute manages method + path based allowlists
51type allowedRoute struct {
53 pathRegex *regexp.Regexp
56// OAuthProxy is the main authentication proxy
57type OAuthProxy struct {
61 CookieDomains []string
65 CookieExpire time.Duration
66 CookieRefresh time.Duration
68 Validator func(string) bool
74 OAuthCallbackPath string
78 allowedRoutes []allowedRoute
79 redirectURL *url.URL // the url to receive requests at
80 whitelistDomains []string
81 provider providers.Provider
82 providerNameOverride string
83 sessionStore sessionsapi.SessionStore
86 basicAuthValidator basic.Validator
87 displayHtpasswdForm bool
92 SkipProviderButton bool
94 BasicAuthPassword string
97 PassAuthorization bool
98 PreferEmailToUser bool
99 skipAuthPreflight bool
100 skipJwtBearerTokens bool
101 mainJwtBearerVerifier *oidc.IDTokenVerifier
102 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
103 templates *template.Template
104 realClientIPParser ipapi.RealClientIPParser
105 trustedIPs *ip.NetSet
108 AllowedGroups []string
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 provider: opts.GetProvider(),
205 providerNameOverride: opts.ProviderName,
206 sessionStore: sessionStore,
207 serveMux: upstreamProxy,
208 redirectURL: redirectURL,
209 allowedRoutes: allowedRoutes,
210 whitelistDomains: opts.WhitelistDomains,
211 skipAuthPreflight: opts.SkipAuthPreflight,
212 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
213 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
214 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
215 realClientIPParser: opts.GetRealClientIPParser(),
216 SkipProviderButton: opts.SkipProviderButton,
217 templates: templates,
218 trustedIPs: trustedIPs,
221 SignInMessage: buildSignInMessage(opts),
222 AllowedGroups: opts.AllowedGroups,
224 basicAuthValidator: basicAuthValidator,
225 displayHtpasswdForm: basicAuthValidator != nil && opts.DisplayHtpasswdForm,
226 sessionChain: sessionChain,
227 headersChain: headersChain,
228 preAuthChain: preAuthChain,
232// buildPreAuthChain constructs a chain that should process every request before
233// the OAuth2 Proxy authentication logic kicks in.
234// For example forcing HTTPS or health checks.
235func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
236 chain := alice.New(middleware.NewScope())
239 _, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress)
241 return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.HTTPAddress, err)
243 chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
246 healthCheckPaths := []string{opts.PingPath}
247 healthCheckUserAgents := []string{opts.PingUserAgent}
248 if opts.GCPHealthChecks {
249 healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
250 healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
253 // To silence logging of health checks, register the health check handler before
254 // the logging handler
255 if opts.Logging.SilencePing {
256 chain = chain.Append(middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), LoggingHandler)
258 chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents))
264func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
267 if opts.SkipJwtBearerTokens {
268 sessionLoaders := []middlewareapi.TokenToSessionLoader{}
269 if opts.GetOIDCVerifier() != nil {
270 sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
271 Verifier: opts.GetOIDCVerifier(),
272 TokenToSession: opts.GetProvider().CreateSessionStateFromBearerToken,
276 for _, verifier := range opts.GetJWTBearerVerifiers() {
277 sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
282 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
285 if validator != nil {
286 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
289 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
290 SessionStore: sessionStore,
291 RefreshPeriod: opts.Cookie.Refresh,
292 RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
293 ValidateSessionState: opts.GetProvider().ValidateSessionState,
299func buildHeadersChain(opts *options.Options) (alice.Chain, error) {
300 requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders)
302 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
305 responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders)
307 return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err)
310 return alice.New(requestInjector, responseInjector), nil
313func buildSignInMessage(opts *options.Options) string {
315 if len(opts.Banner) >= 1 {
316 if opts.Banner == "-" {
321 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
322 if len(opts.EmailDomains) > 1 {
323 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
324 } else if opts.EmailDomains[0] != "*" {
325 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
331// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
332// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
333// (method=path support)
334func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
335 routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
337 for _, path := range opts.SkipAuthRegex {
338 compiledRegex, err := regexp.Compile(path)
342 logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
343 routes = append(routes, allowedRoute{
345 pathRegex: compiledRegex,
349 for _, methodPath := range opts.SkipAuthRoutes {
355 parts := strings.SplitN(methodPath, "=", 2)
360 method = strings.ToUpper(parts[0])
364 compiledRegex, err := regexp.Compile(path)
368 logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
369 routes = append(routes, allowedRoute{
371 pathRegex: compiledRegex,
378// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
379// redirect clients to once authenticated
380func (p *OAuthProxy) GetRedirectURI(host string) string {
381 // default to the request Host if not set
382 if p.redirectURL.Host != "" {
383 return p.redirectURL.String()
388 u.Scheme = httpsScheme
390 u.Scheme = httpScheme
397func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
399 return nil, errors.New("missing code")
401 redirectURI := p.GetRedirectURI(host)
402 s, err := p.provider.Redeem(ctx, redirectURI, code)
409func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
412 s.Email, err = p.provider.GetEmailAddress(ctx, s)
413 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
418 return p.provider.EnrichSessionState(ctx, s)
421// MakeCSRFCookie creates a cookie for CSRF
422func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
423 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
426func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
427 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
429 if cookieDomain != "" {
430 domain := util.GetRequestHost(req)
431 if h, _, err := net.SplitHostPort(domain); err == nil {
434 if !strings.HasSuffix(domain, cookieDomain) {
435 logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
443 Domain: cookieDomain,
444 HttpOnly: p.CookieHTTPOnly,
445 Secure: p.CookieSecure,
446 Expires: now.Add(expiration),
447 SameSite: cookies.ParseSameSite(p.CookieSameSite),
451// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
453func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
454 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
457// SetCSRFCookie adds a CSRF cookie to the response
458func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
459 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
462// ClearSessionCookie creates a cookie to unset the user's authentication cookie
463// stored in the user's session
464func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
465 return p.sessionStore.Clear(rw, req)
468// LoadCookiedSession reads the user's authentication details from the request
469func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
470 return p.sessionStore.Load(req)
473// SaveSession creates a new session cookie value and sets this on the response
474func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
475 return p.sessionStore.Save(rw, req, s)
478// RobotsTxt disallows scraping pages from the OAuthProxy
479func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
480 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
482 logger.Printf("Error writing robots.txt: %v", err)
483 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
486 rw.WriteHeader(http.StatusOK)
489// ErrorPage writes an error response
490func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
497 Title: fmt.Sprintf("%d %s", code, title),
499 ProxyPrefix: p.ProxyPrefix,
501 err := p.templates.ExecuteTemplate(rw, "error.html", t)
503 logger.Printf("Error rendering error.html template: %v", err)
504 http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
508// SignInPage writes the sing in template to the response
509func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
511 err := p.ClearSessionCookie(rw, req)
513 logger.Printf("Error clearing session cookie: %v", err)
514 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
519 redirectURL, err := p.GetRedirect(req)
521 logger.Errorf("Error obtaining redirect: %v", err)
522 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
526 if redirectURL == p.SignInPath {
530 // We allow unescaped template.HTML since it is user configured options
534 SignInMessage template.HTML
541 ProviderName: p.provider.Data().ProviderName,
542 SignInMessage: template.HTML(p.SignInMessage),
543 CustomLogin: p.displayHtpasswdForm,
544 Redirect: redirectURL,
546 ProxyPrefix: p.ProxyPrefix,
547 Footer: template.HTML(p.Footer),
549 if p.providerNameOverride != "" {
550 t.ProviderName = p.providerNameOverride
552 err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
554 logger.Printf("Error rendering sign_in.html template: %v", err)
555 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
559// ManualSignIn handles basic auth logins to the proxy
560func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
561 if req.Method != "POST" || p.basicAuthValidator == nil {
564 user := req.FormValue("username")
565 passwd := req.FormValue("password")
570 if p.basicAuthValidator.Validate(user, passwd) {
571 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
574 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
578// GetRedirect reads the query parameter to get the URL to redirect clients to
579// once authenticated with the OAuthProxy
580func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
581 err = req.ParseForm()
586 redirect = req.Header.Get("X-Auth-Request-Redirect")
587 if req.Form.Get("rd") != "" {
588 redirect = req.Form.Get("rd")
590 if !p.IsValidRedirect(redirect) {
591 // Use RequestURI to preserve ?query
592 redirect = req.URL.RequestURI()
593 if strings.HasPrefix(redirect, p.ProxyPrefix) {
601// splitHostPort separates host and port. If the port is not valid, it returns
602// the entire input as host, and it doesn't check the validity of the host.
603// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
604// *** taken from net/url, modified validOptionalPort() to accept ":*"
605func splitHostPort(hostport string) (host, port string) {
608 colon := strings.LastIndexByte(host, ':')
609 if colon != -1 && validOptionalPort(host[colon:]) {
610 host, port = host[:colon], host[colon+1:]
613 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
614 host = host[1 : len(host)-1]
620// validOptionalPort reports whether port is either an empty string
621// or matches /^:\d*$/
622// *** taken from net/url, modified to accept ":*"
623func validOptionalPort(port string) bool {
624 if port == "" || port == ":*" {
630 for _, b := range port[1:] {
631 if b < '0' || b > '9' {
638// IsValidRedirect checks whether the redirect URL is whitelisted
639func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
642 // The user didn't specify a redirect, should fallback to `/`
644 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
646 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
647 redirectURL, err := url.Parse(redirect)
649 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
652 redirectHostname := redirectURL.Hostname()
654 for _, domain := range p.whitelistDomains {
655 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
656 if domainHostname == "" {
660 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
661 // the domain names match, now validate the ports
662 // if the whitelisted domain's port is '*', allow all ports
663 // if the whitelisted domain contains a specific port, only allow that port
664 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
665 redirectPort := redirectURL.Port()
666 if (domainPort == "*") ||
667 (domainPort == redirectPort) ||
668 (domainPort == "" && redirectPort == "") {
674 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
677 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
682// IsAllowedRequest is used to check if auth should be skipped for this request
683func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
684 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
685 return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
688// IsAllowedRoute is used to check if the request method & path is allowed without auth
689func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
690 for _, route := range p.allowedRoutes {
691 if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
698// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
699var noCacheHeaders = map[string]string{
700 "Expires": time.Unix(0, 0).Format(time.RFC1123),
701 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
702 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
705// prepareNoCache prepares headers for preventing browser caching.
706func prepareNoCache(w http.ResponseWriter) {
707 // Set NoCache headers
708 for k, v := range noCacheHeaders {
713// IsTrustedIP is used to check if a request comes from a trusted client IP address.
714func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
715 if p.trustedIPs == nil {
719 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
721 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
722 // Possibly spoofed X-Real-IP header
726 if remoteAddr == nil {
730 return p.trustedIPs.Has(remoteAddr)
733func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
734 p.preAuthChain.Then(http.HandlerFunc(p.serveHTTP)).ServeHTTP(rw, req)
737func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
738 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
742 switch path := req.URL.Path; {
743 case path == p.RobotsPath:
745 case p.IsAllowedRequest(req):
746 p.SkipAuthProxy(rw, req)
747 case path == p.SignInPath:
749 case path == p.SignOutPath:
751 case path == p.OAuthStartPath:
752 p.OAuthStart(rw, req)
753 case path == p.OAuthCallbackPath:
754 p.OAuthCallback(rw, req)
755 case path == p.AuthOnlyPath:
756 p.AuthenticateOnly(rw, req)
757 case path == p.UserInfoPath:
764// SignIn serves a page prompting users to sign in
765func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
766 redirect, err := p.GetRedirect(req)
768 logger.Errorf("Error obtaining redirect: %v", err)
769 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
773 user, ok := p.ManualSignIn(req)
775 session := &sessionsapi.SessionState{User: user}
776 err = p.SaveSession(rw, req, session)
778 logger.Printf("Error saving session: %v", err)
779 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
782 http.Redirect(rw, req, redirect, http.StatusFound)
784 if p.SkipProviderButton {
785 p.OAuthStart(rw, req)
787 p.SignInPage(rw, req, http.StatusOK)
792//UserInfo endpoint outputs session email and preferred username in JSON format
793func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
795 session, err := p.getAuthenticatedSession(rw, req)
797 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
801 Email string `json:"email"`
802 PreferredUsername string `json:"preferredUsername,omitempty"`
804 Email: session.Email,
805 PreferredUsername: session.PreferredUsername,
807 rw.Header().Set("Content-Type", "application/json")
808 rw.WriteHeader(http.StatusOK)
809 err = json.NewEncoder(rw).Encode(userInfo)
811 logger.Printf("Error encoding user info: %v", err)
812 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
816// SignOut sends a response to clear the authentication cookie
817func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
818 redirect, err := p.GetRedirect(req)
820 logger.Errorf("Error obtaining redirect: %v", err)
821 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
824 err = p.ClearSessionCookie(rw, req)
826 logger.Errorf("Error clearing session cookie: %v", err)
827 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
830 http.Redirect(rw, req, redirect, http.StatusFound)
833// OAuthStart starts the OAuth2 authentication flow
834func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
836 nonce, err := encryption.Nonce()
838 logger.Errorf("Error obtaining nonce: %v", err)
839 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
842 p.SetCSRFCookie(rw, req, nonce)
843 redirect, err := p.GetRedirect(req)
845 logger.Errorf("Error obtaining redirect: %v", err)
846 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
849 redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
850 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
853// OAuthCallback is the OAuth2 authentication flow callback that finishes the
854// OAuth2 authentication flow
855func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
856 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
858 // finish the oauth cycle
859 err := req.ParseForm()
861 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
862 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
865 errorString := req.Form.Get("error")
866 if errorString != "" {
867 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
868 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
872 session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
874 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
875 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
879 err = p.enrichSessionState(req.Context(), session)
881 logger.Errorf("Error creating session during OAuth2 callback: %v", err)
882 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
886 state := strings.SplitN(req.Form.Get("state"), ":", 2)
888 logger.Error("Error while parsing OAuth2 state: invalid length")
889 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
894 c, err := req.Cookie(p.CSRFCookieName)
896 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
897 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
900 p.ClearCSRFCookie(rw, req)
901 if c.Value != nonce {
902 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
903 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
907 if !p.IsValidRedirect(redirect) {
911 // set cookie, or deny
912 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
913 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
914 err := p.SaveSession(rw, req, session)
916 logger.Printf("Error saving session state for %s: %v", remoteAddr, err)
917 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
920 http.Redirect(rw, req, redirect, http.StatusFound)
922 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
923 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
927// AuthenticateOnly checks whether the user is currently logged in
928func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
929 session, err := p.getAuthenticatedSession(rw, req)
931 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
935 // we are authenticated
936 p.addHeadersForProxying(rw, req, session)
937 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
938 rw.WriteHeader(http.StatusAccepted)
939 })).ServeHTTP(rw, req)
942// SkipAuthProxy proxies allowlisted requests and skips authentication
943func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
944 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
947// Proxy proxies the user request if the user is authenticated else it prompts
948// them to authenticate
949func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
950 session, err := p.getAuthenticatedSession(rw, req)
953 // we are authenticated
954 p.addHeadersForProxying(rw, req, session)
955 p.headersChain.Then(p.serveMux).ServeHTTP(rw, req)
957 // we need to send the user to a login screen
959 // no point redirecting an AJAX request
960 p.ErrorJSON(rw, http.StatusUnauthorized)
964 if p.SkipProviderButton {
965 p.OAuthStart(rw, req)
967 p.SignInPage(rw, req, http.StatusForbidden)
972 logger.Errorf("Unexpected internal error: %v", err)
973 p.ErrorPage(rw, http.StatusInternalServerError,
974 "Internal Error", "Internal Error")
979// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
980// Returns nil, ErrNeedsLogin if user needs to login.
981// Set-Cookie headers may be set on the response as a side-effect of calling this method.
982func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
983 var session *sessionsapi.SessionState
985 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
986 session = middleware.GetRequestScope(req).Session
988 getSession.ServeHTTP(rw, req)
991 return nil, ErrNeedsLogin
994 invalidEmail := session != nil && session.Email != "" && !p.Validator(session.Email)
995 invalidGroups := session != nil && !p.validateGroups(session.Groups)
997 if invalidEmail || invalidGroups {
998 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
999 // Invalid session, clear it
1000 err := p.ClearSessionCookie(rw, req)
1002 logger.Printf("Error clearing session cookie: %v", err)
1004 return nil, ErrNeedsLogin
1010// addHeadersForProxying adds the appropriate headers the request / response for proxying
1011func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1012 if session.Email == "" {
1013 rw.Header().Set("GAP-Auth", session.User)
1015 rw.Header().Set("GAP-Auth", session.Email)
1019// isAjax checks if a request is an ajax request
1020func isAjax(req *http.Request) bool {
1021 acceptValues := req.Header.Values("Accept")
1022 const ajaxReq = applicationJSON
1023 for _, v := range acceptValues {
1031// ErrorJSON returns the error code with an application/json mime type
1032func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1033 rw.Header().Set("Content-Type", applicationJSON)
1034 rw.WriteHeader(code)
1037func (p *OAuthProxy) validateGroups(groups []string) bool {
1038 if len(p.AllowedGroups) == 0 {
1042 allowedGroups := map[string]struct{}{}
1044 for _, group := range p.AllowedGroups {
1045 allowedGroups[group] = struct{}{}
1048 for _, group := range groups {
1049 if _, ok := allowedGroups[group]; ok {
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