17 "github.com/coreos/go-oidc"
18 ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
19 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
20 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
21 "github.com/oauth2-proxy/oauth2-proxy/pkg/authentication/basic"
22 "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
23 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
24 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
25 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
26 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
27 "github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
28 "github.com/oauth2-proxy/oauth2-proxy/providers"
35 applicationJSON = "application/json"
39 // ErrNeedsLogin means the user should be redirected to the login page
40 ErrNeedsLogin = errors.New("redirect to login page")
42 // Used to check final redirects are not susceptible to open redirects.
43 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
44 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
47// OAuthProxy is the main authentication proxy
48type OAuthProxy struct {
52 CookieDomains []string
56 CookieExpire time.Duration
57 CookieRefresh time.Duration
59 Validator func(string) bool
65 OAuthCallbackPath string
69 redirectURL *url.URL // the url to receive requests at
70 whitelistDomains []string
71 provider providers.Provider
72 providerNameOverride string
73 sessionStore sessionsapi.SessionStore
76 basicAuthValidator basic.Validator
77 displayHtpasswdForm bool
82 SkipProviderButton bool
84 BasicAuthPassword string
87 PassAuthorization bool
88 PreferEmailToUser bool
89 skipAuthRegex []string
90 skipAuthPreflight bool
91 skipAuthStripHeaders bool
92 skipJwtBearerTokens bool
93 mainJwtBearerVerifier *oidc.IDTokenVerifier
94 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
95 compiledRegex []*regexp.Regexp
96 templates *template.Template
97 realClientIPParser ipapi.RealClientIPParser
103// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
104func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
105 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
107 return nil, fmt.Errorf("error initialising session store: %v", err)
110 templates := loadTemplates(opts.CustomTemplatesDir)
111 proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
112 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
114 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
117 for _, u := range opts.GetCompiledRegex() {
118 logger.Printf("compiled skip-auth-regex => %q", u)
121 if opts.SkipJwtBearerTokens {
122 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
123 for _, issuer := range opts.ExtraJwtIssuers {
124 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
127 redirectURL := opts.GetRedirectURL()
128 if redirectURL.Path == "" {
129 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
132 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
133 refresh := "disabled"
134 if opts.Cookie.Refresh != time.Duration(0) {
135 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
138 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)
140 trustedIPs := ip.NewNetSet()
141 for _, ipStr := range opts.TrustedIPs {
142 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
143 trustedIPs.AddIPNet(*ipNet)
145 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
149 var basicAuthValidator basic.Validator
150 if opts.HtpasswdFile != "" {
151 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
153 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
155 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
160 CookieName: opts.Cookie.Name,
161 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
162 CookieSeed: opts.Cookie.Secret,
163 CookieDomains: opts.Cookie.Domains,
164 CookiePath: opts.Cookie.Path,
165 CookieSecure: opts.Cookie.Secure,
166 CookieHTTPOnly: opts.Cookie.HTTPOnly,
167 CookieExpire: opts.Cookie.Expire,
168 CookieRefresh: opts.Cookie.Refresh,
169 CookieSameSite: opts.Cookie.SameSite,
170 Validator: validator,
172 RobotsPath: "/robots.txt",
173 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
174 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
175 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
176 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
177 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
178 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
180 ProxyPrefix: opts.ProxyPrefix,
181 provider: opts.GetProvider(),
182 providerNameOverride: opts.ProviderName,
183 sessionStore: sessionStore,
184 serveMux: upstreamProxy,
185 redirectURL: redirectURL,
186 whitelistDomains: opts.WhitelistDomains,
187 skipAuthRegex: opts.SkipAuthRegex,
188 skipAuthPreflight: opts.SkipAuthPreflight,
189 skipAuthStripHeaders: opts.SkipAuthStripHeaders,
190 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
191 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
192 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
193 compiledRegex: opts.GetCompiledRegex(),
194 realClientIPParser: opts.GetRealClientIPParser(),
195 SetXAuthRequest: opts.SetXAuthRequest,
196 PassBasicAuth: opts.PassBasicAuth,
197 SetBasicAuth: opts.SetBasicAuth,
198 PassUserHeaders: opts.PassUserHeaders,
199 BasicAuthPassword: opts.BasicAuthPassword,
200 PassAccessToken: opts.PassAccessToken,
201 SetAuthorization: opts.SetAuthorization,
202 PassAuthorization: opts.PassAuthorization,
203 PreferEmailToUser: opts.PreferEmailToUser,
204 SkipProviderButton: opts.SkipProviderButton,
205 templates: templates,
206 trustedIPs: trustedIPs,
210 basicAuthValidator: basicAuthValidator,
211 displayHtpasswdForm: basicAuthValidator != nil,
215// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
216// redirect clients to once authenticated
217func (p *OAuthProxy) GetRedirectURI(host string) string {
218 // default to the request Host if not set
219 if p.redirectURL.Host != "" {
220 return p.redirectURL.String()
225 u.Scheme = httpsScheme
227 u.Scheme = httpScheme
234func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) {
236 return nil, errors.New("missing code")
238 redirectURI := p.GetRedirectURI(host)
239 s, err = p.provider.Redeem(ctx, redirectURI, code)
245 s.Email, err = p.provider.GetEmailAddress(ctx, s)
248 if s.PreferredUsername == "" {
249 s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s)
250 if err != nil && err.Error() == "not implemented" {
256 s.User, err = p.provider.GetUserName(ctx, s)
257 if err != nil && err.Error() == "not implemented" {
264// MakeCSRFCookie creates a cookie for CSRF
265func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
266 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
269func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
270 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
272 if cookieDomain != "" {
273 domain := cookies.GetRequestHost(req)
274 if h, _, err := net.SplitHostPort(domain); err == nil {
277 if !strings.HasSuffix(domain, cookieDomain) {
278 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
286 Domain: cookieDomain,
287 HttpOnly: p.CookieHTTPOnly,
288 Secure: p.CookieSecure,
289 Expires: now.Add(expiration),
290 SameSite: cookies.ParseSameSite(p.CookieSameSite),
294// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
296func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
297 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
300// SetCSRFCookie adds a CSRF cookie to the response
301func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
302 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
305// ClearSessionCookie creates a cookie to unset the user's authentication cookie
306// stored in the user's session
307func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
308 return p.sessionStore.Clear(rw, req)
311// LoadCookiedSession reads the user's authentication details from the request
312func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
313 return p.sessionStore.Load(req)
316// SaveSession creates a new session cookie value and sets this on the response
317func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
318 return p.sessionStore.Save(rw, req, s)
321// RobotsTxt disallows scraping pages from the OAuthProxy
322func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
323 rw.WriteHeader(http.StatusOK)
324 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
327// ErrorPage writes an error response
328func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
335 Title: fmt.Sprintf("%d %s", code, title),
337 ProxyPrefix: p.ProxyPrefix,
339 p.templates.ExecuteTemplate(rw, "error.html", t)
342// SignInPage writes the sing in template to the response
343func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
345 p.ClearSessionCookie(rw, req)
348 redirectURL, err := p.GetRedirect(req)
350 logger.Printf("Error obtaining redirect: %s", err.Error())
351 p.ErrorPage(rw, 500, "Internal Error", err.Error())
355 if redirectURL == p.SignInPath {
361 SignInMessage template.HTML
368 ProviderName: p.provider.Data().ProviderName,
369 SignInMessage: template.HTML(p.SignInMessage),
370 CustomLogin: p.displayHtpasswdForm,
371 Redirect: redirectURL,
373 ProxyPrefix: p.ProxyPrefix,
374 Footer: template.HTML(p.Footer),
376 if p.providerNameOverride != "" {
377 t.ProviderName = p.providerNameOverride
379 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
382// ManualSignIn handles basic auth logins to the proxy
383func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
384 if req.Method != "POST" || p.basicAuthValidator == nil {
387 user := req.FormValue("username")
388 passwd := req.FormValue("password")
393 if p.basicAuthValidator.Validate(user, passwd) {
394 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
397 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
401// GetRedirect reads the query parameter to get the URL to redirect clients to
402// once authenticated with the OAuthProxy
403func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
404 err = req.ParseForm()
409 redirect = req.Header.Get("X-Auth-Request-Redirect")
410 if req.Form.Get("rd") != "" {
411 redirect = req.Form.Get("rd")
413 if !p.IsValidRedirect(redirect) {
414 redirect = req.URL.Path
415 if strings.HasPrefix(redirect, p.ProxyPrefix) {
423// splitHostPort separates host and port. If the port is not valid, it returns
424// the entire input as host, and it doesn't check the validity of the host.
425// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
426// *** taken from net/url, modified validOptionalPort() to accept ":*"
427func splitHostPort(hostport string) (host, port string) {
430 colon := strings.LastIndexByte(host, ':')
431 if colon != -1 && validOptionalPort(host[colon:]) {
432 host, port = host[:colon], host[colon+1:]
435 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
436 host = host[1 : len(host)-1]
442// validOptionalPort reports whether port is either an empty string
443// or matches /^:\d*$/
444// *** taken from net/url, modified to accept ":*"
445func validOptionalPort(port string) bool {
446 if port == "" || port == ":*" {
452 for _, b := range port[1:] {
453 if b < '0' || b > '9' {
460// IsValidRedirect checks whether the redirect URL is whitelisted
461func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
464 // The user didn't specify a redirect, should fallback to `/`
466 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
468 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
469 redirectURL, err := url.Parse(redirect)
471 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
474 redirectHostname := redirectURL.Hostname()
476 for _, domain := range p.whitelistDomains {
477 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
478 if domainHostname == "" {
482 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
483 // the domain names match, now validate the ports
484 // if the whitelisted domain's port is '*', allow all ports
485 // if the whitelisted domain contains a specific port, only allow that port
486 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
487 redirectPort := redirectURL.Port()
488 if (domainPort == "*") ||
489 (domainPort == redirectPort) ||
490 (domainPort == "" && redirectPort == "") {
496 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
499 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
504// IsWhitelistedRequest is used to check if auth should be skipped for this request
505func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
506 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
507 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) || p.IsTrustedIP(req)
510// IsWhitelistedPath is used to check if the request path is allowed without auth
511func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
512 for _, u := range p.compiledRegex {
513 if u.MatchString(path) {
520// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
521var noCacheHeaders = map[string]string{
522 "Expires": time.Unix(0, 0).Format(time.RFC1123),
523 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
524 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
527// prepareNoCache prepares headers for preventing browser caching.
528func prepareNoCache(w http.ResponseWriter) {
529 // Set NoCache headers
530 for k, v := range noCacheHeaders {
535// IsTrustedIP is used to check if a request comes from a trusted client IP address.
536func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
537 if p.trustedIPs == nil {
541 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
543 logger.Printf("Error obtaining real IP for trusted IP list: %v", err)
544 // Possibly spoofed X-Real-IP header
548 if remoteAddr == nil {
552 return p.trustedIPs.Has(remoteAddr)
555func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
556 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
560 switch path := req.URL.Path; {
561 case path == p.RobotsPath:
563 case p.IsWhitelistedRequest(req):
564 p.SkipAuthProxy(rw, req)
565 case path == p.SignInPath:
567 case path == p.SignOutPath:
569 case path == p.OAuthStartPath:
570 p.OAuthStart(rw, req)
571 case path == p.OAuthCallbackPath:
572 p.OAuthCallback(rw, req)
573 case path == p.AuthOnlyPath:
574 p.AuthenticateOnly(rw, req)
575 case path == p.UserInfoPath:
582// SignIn serves a page prompting users to sign in
583func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
584 redirect, err := p.GetRedirect(req)
586 logger.Printf("Error obtaining redirect: %s", err.Error())
587 p.ErrorPage(rw, 500, "Internal Error", err.Error())
591 user, ok := p.ManualSignIn(rw, req)
593 session := &sessionsapi.SessionState{User: user}
594 p.SaveSession(rw, req, session)
595 http.Redirect(rw, req, redirect, http.StatusFound)
597 if p.SkipProviderButton {
598 p.OAuthStart(rw, req)
600 p.SignInPage(rw, req, http.StatusOK)
605//UserInfo endpoint outputs session email and preferred username in JSON format
606func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
608 session, err := p.getAuthenticatedSession(rw, req)
610 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
614 Email string `json:"email"`
615 PreferredUsername string `json:"preferredUsername,omitempty"`
617 Email: session.Email,
618 PreferredUsername: session.PreferredUsername,
620 rw.Header().Set("Content-Type", "application/json")
621 rw.WriteHeader(http.StatusOK)
622 json.NewEncoder(rw).Encode(userInfo)
625// SignOut sends a response to clear the authentication cookie
626func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
627 redirect, err := p.GetRedirect(req)
629 logger.Printf("Error obtaining redirect: %s", err.Error())
630 p.ErrorPage(rw, 500, "Internal Error", err.Error())
633 p.ClearSessionCookie(rw, req)
634 http.Redirect(rw, req, redirect, http.StatusFound)
637// OAuthStart starts the OAuth2 authentication flow
638func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
640 nonce, err := encryption.Nonce()
642 logger.Printf("Error obtaining nonce: %s", err.Error())
643 p.ErrorPage(rw, 500, "Internal Error", err.Error())
646 p.SetCSRFCookie(rw, req, nonce)
647 redirect, err := p.GetRedirect(req)
649 logger.Printf("Error obtaining redirect: %s", err.Error())
650 p.ErrorPage(rw, 500, "Internal Error", err.Error())
653 redirectURI := p.GetRedirectURI(req.Host)
654 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
657// OAuthCallback is the OAuth2 authentication flow callback that finishes the
658// OAuth2 authentication flow
659func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
660 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
662 // finish the oauth cycle
663 err := req.ParseForm()
665 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
666 p.ErrorPage(rw, 500, "Internal Error", err.Error())
669 errorString := req.Form.Get("error")
670 if errorString != "" {
671 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
672 p.ErrorPage(rw, 403, "Permission Denied", errorString)
676 session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code"))
678 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
679 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
683 s := strings.SplitN(req.Form.Get("state"), ":", 2)
685 logger.Printf("Error while parsing OAuth2 state: invalid length")
686 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
691 c, err := req.Cookie(p.CSRFCookieName)
693 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
694 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
697 p.ClearCSRFCookie(rw, req)
698 if c.Value != nonce {
699 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
700 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
704 if !p.IsValidRedirect(redirect) {
708 // set cookie, or deny
709 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
710 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
711 err := p.SaveSession(rw, req, session)
713 logger.Printf("%s %s", remoteAddr, err)
714 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
717 http.Redirect(rw, req, redirect, http.StatusFound)
719 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
720 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
724// AuthenticateOnly checks whether the user is currently logged in
725func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
726 session, err := p.getAuthenticatedSession(rw, req)
728 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
732 // we are authenticated
733 p.addHeadersForProxying(rw, req, session)
734 rw.WriteHeader(http.StatusAccepted)
737// SkipAuthProxy proxies whitelisted requests and skips authentication
738func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
739 if p.skipAuthStripHeaders {
740 p.stripAuthHeaders(req)
742 p.serveMux.ServeHTTP(rw, req)
745// Proxy proxies the user request if the user is authenticated else it prompts
746// them to authenticate
747func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
748 session, err := p.getAuthenticatedSession(rw, req)
751 // we are authenticated
752 p.addHeadersForProxying(rw, req, session)
753 p.serveMux.ServeHTTP(rw, req)
756 // we need to send the user to a login screen
758 // no point redirecting an AJAX request
759 p.ErrorJSON(rw, http.StatusUnauthorized)
763 if p.SkipProviderButton {
764 p.OAuthStart(rw, req)
766 p.SignInPage(rw, req, http.StatusForbidden)
771 logger.Printf("Unexpected internal error: %s", err)
772 p.ErrorPage(rw, http.StatusInternalServerError,
773 "Internal Error", "Internal Error")
778// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
779// Returns nil, ErrNeedsLogin if user needs to login.
780// Set-Cookie headers may be set on the response as a side-effect of calling this method.
781func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
782 var session *sessionsapi.SessionState
784 var saveSession, clearSession, revalidated bool
786 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
787 session, err = p.GetJwtSession(req)
789 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
796 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
798 session, err = p.LoadCookiedSession(req)
800 logger.Printf("Error loading cookied session: %s", err)
804 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
805 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
809 if ok, err := p.provider.RefreshSessionIfNeeded(req.Context(), session); err != nil {
810 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
820 if session != nil && session.IsExpired() {
821 logger.Printf("Removing session: token expired %s", session)
827 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
828 if !p.provider.ValidateSessionState(req.Context(), session) {
829 logger.Printf("Removing session: error validating %s", session)
836 if session != nil && session.Email != "" && !p.Validator(session.Email) {
837 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
843 if saveSession && session != nil {
844 err = p.SaveSession(rw, req, session)
846 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
852 p.ClearSessionCookie(rw, req)
856 session, err = p.CheckBasicAuth(req)
858 logger.Printf("Error during basic auth validation: %s", err)
863 return nil, ErrNeedsLogin
869// addHeadersForProxying adds the appropriate headers the request / response for proxying
870func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
872 if p.PreferEmailToUser && session.Email != "" {
873 req.SetBasicAuth(session.Email, p.BasicAuthPassword)
874 req.Header["X-Forwarded-User"] = []string{session.Email}
875 req.Header.Del("X-Forwarded-Email")
877 req.SetBasicAuth(session.User, p.BasicAuthPassword)
878 req.Header["X-Forwarded-User"] = []string{session.User}
879 if session.Email != "" {
880 req.Header["X-Forwarded-Email"] = []string{session.Email}
882 req.Header.Del("X-Forwarded-Email")
885 if session.PreferredUsername != "" {
886 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
888 req.Header.Del("X-Forwarded-Preferred-Username")
892 if p.PassUserHeaders {
893 if p.PreferEmailToUser && session.Email != "" {
894 req.Header["X-Forwarded-User"] = []string{session.Email}
895 req.Header.Del("X-Forwarded-Email")
897 req.Header["X-Forwarded-User"] = []string{session.User}
898 if session.Email != "" {
899 req.Header["X-Forwarded-Email"] = []string{session.Email}
901 req.Header.Del("X-Forwarded-Email")
905 if session.PreferredUsername != "" {
906 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
908 req.Header.Del("X-Forwarded-Preferred-Username")
912 if p.SetXAuthRequest {
913 rw.Header().Set("X-Auth-Request-User", session.User)
914 if session.Email != "" {
915 rw.Header().Set("X-Auth-Request-Email", session.Email)
917 rw.Header().Del("X-Auth-Request-Email")
919 if session.PreferredUsername != "" {
920 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
922 rw.Header().Del("X-Auth-Request-Preferred-Username")
925 if p.PassAccessToken {
926 if session.AccessToken != "" {
927 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
929 rw.Header().Del("X-Auth-Request-Access-Token")
934 if p.PassAccessToken {
935 if session.AccessToken != "" {
936 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
938 req.Header.Del("X-Forwarded-Access-Token")
942 if p.PassAuthorization {
943 if session.IDToken != "" {
944 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
946 req.Header.Del("Authorization")
951 case p.PreferEmailToUser && session.Email != "":
952 authVal := b64.StdEncoding.EncodeToString([]byte(session.Email + ":" + p.BasicAuthPassword))
953 rw.Header().Set("Authorization", "Basic "+authVal)
954 case session.User != "":
955 authVal := b64.StdEncoding.EncodeToString([]byte(session.User + ":" + p.BasicAuthPassword))
956 rw.Header().Set("Authorization", "Basic "+authVal)
958 rw.Header().Del("Authorization")
961 if p.SetAuthorization {
962 if session.IDToken != "" {
963 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
965 rw.Header().Del("Authorization")
969 if session.Email == "" {
970 rw.Header().Set("GAP-Auth", session.User)
972 rw.Header().Set("GAP-Auth", session.Email)
976// stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex
977func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
979 req.Header.Del("X-Forwarded-User")
980 req.Header.Del("X-Forwarded-Email")
981 req.Header.Del("X-Forwarded-Preferred-Username")
982 req.Header.Del("Authorization")
985 if p.PassUserHeaders {
986 req.Header.Del("X-Forwarded-User")
987 req.Header.Del("X-Forwarded-Email")
988 req.Header.Del("X-Forwarded-Preferred-Username")
991 if p.PassAccessToken {
992 req.Header.Del("X-Forwarded-Access-Token")
995 if p.PassAuthorization {
996 req.Header.Del("Authorization")
1000// CheckBasicAuth checks the requests Authorization header for basic auth
1001// credentials and authenticates these against the proxies HtpasswdFile
1002func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
1003 if p.basicAuthValidator == nil {
1006 auth := req.Header.Get("Authorization")
1010 s := strings.SplitN(auth, " ", 2)
1011 if len(s) != 2 || s[0] != "Basic" {
1012 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1014 b, err := b64.StdEncoding.DecodeString(s[1])
1018 pair := strings.SplitN(string(b), ":", 2)
1020 return nil, fmt.Errorf("invalid format %s", b)
1022 if p.basicAuthValidator.Validate(pair[0], pair[1]) {
1023 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1024 return &sessionsapi.SessionState{User: pair[0]}, nil
1026 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1030// isAjax checks if a request is an ajax request
1031func isAjax(req *http.Request) bool {
1032 acceptValues := req.Header.Values("Accept")
1033 const ajaxReq = applicationJSON
1034 for _, v := range acceptValues {
1042// ErrorJSON returns the error code with an application/json mime type
1043func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1044 rw.Header().Set("Content-Type", applicationJSON)
1045 rw.WriteHeader(code)
1048// GetJwtSession loads a session based on a JWT token in the authorization header.
1049// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
1050func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1051 rawBearerToken, err := p.findBearerToken(req)
1056 // If we are using an oidc provider, go ahead and try that provider first with its Verifier
1057 // and Bearer Token -> Session converter
1058 if p.mainJwtBearerVerifier != nil {
1059 bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
1061 return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1065 // Otherwise, attempt to verify against the extra JWT issuers and use a more generic
1066 // Bearer Token -> Session converter
1067 for _, verifier := range p.extraJwtBearerVerifiers {
1068 bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1073 return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1075 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1078// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1079func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1080 auth := req.Header.Get("Authorization")
1081 s := strings.SplitN(auth, " ", 2)
1083 return "", fmt.Errorf("invalid authorization header %s", auth)
1085 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1086 var rawBearerToken string
1087 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1088 rawBearerToken = s[1]
1089 } else if s[0] == "Basic" {
1090 // Check if we have a Bearer token masquerading in Basic
1091 b, err := b64.StdEncoding.DecodeString(s[1])
1095 pair := strings.SplitN(string(b), ":", 2)
1097 return "", fmt.Errorf("invalid format %s", b)
1099 user, password := pair[0], pair[1]
1101 // check user, user+password, or just password for a token
1102 if jwtRegex.MatchString(user) {
1103 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1104 if password == "" || password == "x-oauth-basic" {
1105 rawBearerToken = user
1107 } else if jwtRegex.MatchString(password) {
1108 // support passwords and ignore user
1109 rawBearerToken = password
1112 if rawBearerToken == "" {
1113 return "", fmt.Errorf("no valid bearer token found in authorization header")
1116 return rawBearerToken, nil
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