20 "github.com/coreos/go-oidc"
21 "github.com/mbland/hmacauth"
22 ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
23 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
24 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
25 "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
26 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
27 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
28 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
29 "github.com/oauth2-proxy/oauth2-proxy/providers"
30 "github.com/yhat/wsutil"
34 // SignatureHeader is the name of the request header containing the GAP Signature
36 SignatureHeader = "GAP-Signature"
41 applicationJSON = "application/json"
44// SignatureHeaders contains the headers to be signed by the hmac algorithm
46var SignatureHeaders = []string{
54 "X-Forwarded-Preferred-User",
55 "X-Forwarded-Access-Token",
61 // ErrNeedsLogin means the user should be redirected to the login page
62 ErrNeedsLogin = errors.New("redirect to login page")
64 // Used to check final redirects are not susceptible to open redirects.
65 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
66 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
69// OAuthProxy is the main authentication proxy
70type OAuthProxy struct {
74 CookieDomains []string
78 CookieExpire time.Duration
79 CookieRefresh time.Duration
81 Validator func(string) bool
87 OAuthCallbackPath string
91 redirectURL *url.URL // the url to receive requests at
92 whitelistDomains []string
93 provider providers.Provider
94 providerNameOverride string
95 sessionStore sessionsapi.SessionStore
98 HtpasswdFile *HtpasswdFile
99 DisplayHtpasswdForm bool
100 serveMux http.Handler
104 SkipProviderButton bool
106 BasicAuthPassword string
108 SetAuthorization bool
109 PassAuthorization bool
110 PreferEmailToUser bool
111 skipAuthRegex []string
112 skipAuthPreflight bool
113 skipJwtBearerTokens bool
114 mainJwtBearerVerifier *oidc.IDTokenVerifier
115 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
116 compiledRegex []*regexp.Regexp
117 templates *template.Template
118 realClientIPParser ipapi.RealClientIPParser
123// UpstreamProxy represents an upstream server to proxy to
124type UpstreamProxy struct {
127 wsHandler http.Handler
128 auth hmacauth.HmacAuth
131// ServeHTTP proxies requests to the upstream provider while signing the
133func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
134 w.Header().Set("GAP-Upstream-Address", u.upstream)
136 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
137 u.auth.SignRequest(r)
139 if u.wsHandler != nil && strings.EqualFold(r.Header.Get("Connection"), "upgrade") && r.Header.Get("Upgrade") == "websocket" {
140 u.wsHandler.ServeHTTP(w, r)
142 u.handler.ServeHTTP(w, r)
147// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
149func NewReverseProxy(target *url.URL, opts *options.Options) (proxy *httputil.ReverseProxy) {
150 proxy = httputil.NewSingleHostReverseProxy(target)
151 proxy.FlushInterval = opts.FlushInterval
152 if opts.SSLUpstreamInsecureSkipVerify {
153 proxy.Transport = &http.Transport{
154 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
157 setProxyErrorHandler(proxy, opts)
161func setProxyErrorHandler(proxy *httputil.ReverseProxy, opts *options.Options) {
162 templates := loadTemplates(opts.CustomTemplatesDir)
163 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, proxyErr error) {
164 logger.Printf("Error proxying to upstream server: %v", proxyErr)
165 w.WriteHeader(http.StatusBadGateway)
171 Title: "Bad Gateway",
172 Message: "Error proxying to upstream server",
173 ProxyPrefix: opts.ProxyPrefix,
175 templates.ExecuteTemplate(w, "error.html", data)
179func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
180 director := proxy.Director
181 proxy.Director = func(req *http.Request) {
183 // use RequestURI so that we aren't unescaping encoded slashes in the request path
184 req.Host = target.Host
185 req.URL.Opaque = req.RequestURI
186 req.URL.RawQuery = ""
190func setProxyDirector(proxy *httputil.ReverseProxy) {
191 director := proxy.Director
192 proxy.Director = func(req *http.Request) {
194 // use RequestURI so that we aren't unescaping encoded slashes in the request path
195 req.URL.Opaque = req.RequestURI
196 req.URL.RawQuery = ""
200// NewFileServer creates a http.Handler to serve files from the filesystem
201func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
202 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
205// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
206func NewWebSocketOrRestReverseProxy(u *url.URL, opts *options.Options, auth hmacauth.HmacAuth) http.Handler {
208 proxy := NewReverseProxy(u, opts)
209 if !opts.PassHostHeader {
210 setProxyUpstreamHostHeader(proxy, u)
212 setProxyDirector(proxy)
215 // this should give us a wss:// scheme if the url is https:// based.
216 var wsProxy *wsutil.ReverseProxy
217 if opts.ProxyWebSockets {
218 wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
219 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
220 wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
221 if opts.SSLUpstreamInsecureSkipVerify {
222 wsProxy.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
225 return &UpstreamProxy{
233// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
234func NewOAuthProxy(opts *options.Options, validator func(string) bool) *OAuthProxy {
235 serveMux := http.NewServeMux()
236 var auth hmacauth.HmacAuth
237 if sigData := opts.GetSignatureData(); sigData != nil {
238 auth = hmacauth.NewHmacAuth(sigData.Hash, []byte(sigData.Key),
239 SignatureHeader, SignatureHeaders)
241 for _, u := range opts.GetProxyURLs() {
245 case httpScheme, httpsScheme:
246 logger.Printf("mapping path %q => upstream %q", path, u)
247 proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
248 serveMux.Handle(path, proxy)
250 responseCode, err := strconv.Atoi(host)
252 logger.Printf("unable to convert %q to int, use default \"200\"", host)
256 serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
257 rw.WriteHeader(responseCode)
258 fmt.Fprintf(rw, "Authenticated")
261 if u.Fragment != "" {
264 logger.Printf("mapping path %q => file system %q", path, u.Path)
265 proxy := NewFileServer(path, u.Path)
266 uProxy := UpstreamProxy{
272 serveMux.Handle(path, &uProxy)
274 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
277 for _, u := range opts.GetCompiledRegex() {
278 logger.Printf("compiled skip-auth-regex => %q", u)
281 if opts.SkipJwtBearerTokens {
282 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
283 for _, issuer := range opts.ExtraJwtIssuers {
284 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
287 redirectURL := opts.GetRedirectURL()
288 if redirectURL.Path == "" {
289 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
292 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
293 refresh := "disabled"
294 if opts.Cookie.Refresh != time.Duration(0) {
295 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
298 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)
301 CookieName: opts.Cookie.Name,
302 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
303 CookieSeed: opts.Cookie.Secret,
304 CookieDomains: opts.Cookie.Domains,
305 CookiePath: opts.Cookie.Path,
306 CookieSecure: opts.Cookie.Secure,
307 CookieHTTPOnly: opts.Cookie.HTTPOnly,
308 CookieExpire: opts.Cookie.Expire,
309 CookieRefresh: opts.Cookie.Refresh,
310 CookieSameSite: opts.Cookie.SameSite,
311 Validator: validator,
313 RobotsPath: "/robots.txt",
314 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
315 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
316 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
317 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
318 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
319 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
321 ProxyPrefix: opts.ProxyPrefix,
322 provider: opts.GetProvider(),
323 providerNameOverride: opts.ProviderName,
324 sessionStore: opts.GetSessionStore(),
326 redirectURL: redirectURL,
327 whitelistDomains: opts.WhitelistDomains,
328 skipAuthRegex: opts.SkipAuthRegex,
329 skipAuthPreflight: opts.SkipAuthPreflight,
330 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
331 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
332 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
333 compiledRegex: opts.GetCompiledRegex(),
334 realClientIPParser: opts.GetRealClientIPParser(),
335 SetXAuthRequest: opts.SetXAuthRequest,
336 PassBasicAuth: opts.PassBasicAuth,
337 SetBasicAuth: opts.SetBasicAuth,
338 PassUserHeaders: opts.PassUserHeaders,
339 BasicAuthPassword: opts.BasicAuthPassword,
340 PassAccessToken: opts.PassAccessToken,
341 SetAuthorization: opts.SetAuthorization,
342 PassAuthorization: opts.PassAuthorization,
343 PreferEmailToUser: opts.PreferEmailToUser,
344 SkipProviderButton: opts.SkipProviderButton,
345 templates: loadTemplates(opts.CustomTemplatesDir),
351// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
352// redirect clients to once authenticated
353func (p *OAuthProxy) GetRedirectURI(host string) string {
354 // default to the request Host if not set
355 if p.redirectURL.Host != "" {
356 return p.redirectURL.String()
361 u.Scheme = httpsScheme
363 u.Scheme = httpScheme
370func (p *OAuthProxy) displayCustomLoginForm() bool {
371 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
374func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) {
376 return nil, errors.New("missing code")
378 redirectURI := p.GetRedirectURI(host)
379 s, err = p.provider.Redeem(ctx, redirectURI, code)
385 s.Email, err = p.provider.GetEmailAddress(ctx, s)
388 if s.PreferredUsername == "" {
389 s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s)
390 if err != nil && err.Error() == "not implemented" {
396 s.User, err = p.provider.GetUserName(ctx, s)
397 if err != nil && err.Error() == "not implemented" {
404// MakeCSRFCookie creates a cookie for CSRF
405func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
406 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
409func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
410 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
412 if cookieDomain != "" {
413 domain := cookies.GetRequestHost(req)
414 if h, _, err := net.SplitHostPort(domain); err == nil {
417 if !strings.HasSuffix(domain, cookieDomain) {
418 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
426 Domain: cookieDomain,
427 HttpOnly: p.CookieHTTPOnly,
428 Secure: p.CookieSecure,
429 Expires: now.Add(expiration),
430 SameSite: cookies.ParseSameSite(p.CookieSameSite),
434// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
436func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
437 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
440// SetCSRFCookie adds a CSRF cookie to the response
441func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
442 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
445// ClearSessionCookie creates a cookie to unset the user's authentication cookie
446// stored in the user's session
447func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
448 return p.sessionStore.Clear(rw, req)
451// LoadCookiedSession reads the user's authentication details from the request
452func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
453 return p.sessionStore.Load(req)
456// SaveSession creates a new session cookie value and sets this on the response
457func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
458 return p.sessionStore.Save(rw, req, s)
461// RobotsTxt disallows scraping pages from the OAuthProxy
462func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
463 rw.WriteHeader(http.StatusOK)
464 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
467// ErrorPage writes an error response
468func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
475 Title: fmt.Sprintf("%d %s", code, title),
477 ProxyPrefix: p.ProxyPrefix,
479 p.templates.ExecuteTemplate(rw, "error.html", t)
482// SignInPage writes the sing in template to the response
483func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
485 p.ClearSessionCookie(rw, req)
488 redirectURL, err := p.GetRedirect(req)
490 logger.Printf("Error obtaining redirect: %s", err.Error())
491 p.ErrorPage(rw, 500, "Internal Error", err.Error())
495 if redirectURL == p.SignInPath {
501 SignInMessage template.HTML
508 ProviderName: p.provider.Data().ProviderName,
509 SignInMessage: template.HTML(p.SignInMessage),
510 CustomLogin: p.displayCustomLoginForm(),
511 Redirect: redirectURL,
513 ProxyPrefix: p.ProxyPrefix,
514 Footer: template.HTML(p.Footer),
516 if p.providerNameOverride != "" {
517 t.ProviderName = p.providerNameOverride
519 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
522// ManualSignIn handles basic auth logins to the proxy
523func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
524 if req.Method != "POST" || p.HtpasswdFile == nil {
527 user := req.FormValue("username")
528 passwd := req.FormValue("password")
533 if p.HtpasswdFile.Validate(user, passwd) {
534 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
537 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
541// GetRedirect reads the query parameter to get the URL to redirect clients to
542// once authenticated with the OAuthProxy
543func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
544 err = req.ParseForm()
549 redirect = req.Header.Get("X-Auth-Request-Redirect")
550 if req.Form.Get("rd") != "" {
551 redirect = req.Form.Get("rd")
553 if !p.IsValidRedirect(redirect) {
554 redirect = req.URL.Path
555 if strings.HasPrefix(redirect, p.ProxyPrefix) {
563// splitHostPort separates host and port. If the port is not valid, it returns
564// the entire input as host, and it doesn't check the validity of the host.
565// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
566// *** taken from net/url, modified validOptionalPort() to accept ":*"
567func splitHostPort(hostport string) (host, port string) {
570 colon := strings.LastIndexByte(host, ':')
571 if colon != -1 && validOptionalPort(host[colon:]) {
572 host, port = host[:colon], host[colon+1:]
575 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
576 host = host[1 : len(host)-1]
582// validOptionalPort reports whether port is either an empty string
583// or matches /^:\d*$/
584// *** taken from net/url, modified to accept ":*"
585func validOptionalPort(port string) bool {
586 if port == "" || port == ":*" {
592 for _, b := range port[1:] {
593 if b < '0' || b > '9' {
600// IsValidRedirect checks whether the redirect URL is whitelisted
601func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
604 // The user didn't specify a redirect, should fallback to `/`
606 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
608 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
609 redirectURL, err := url.Parse(redirect)
611 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
614 redirectHostname := redirectURL.Hostname()
616 for _, domain := range p.whitelistDomains {
617 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
618 if domainHostname == "" {
622 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
623 // the domain names match, now validate the ports
624 // if the whitelisted domain's port is '*', allow all ports
625 // if the whitelisted domain contains a specific port, only allow that port
626 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
627 redirectPort := redirectURL.Port()
628 if (domainPort == "*") ||
629 (domainPort == redirectPort) ||
630 (domainPort == "" && redirectPort == "") {
636 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
639 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
644// IsWhitelistedRequest is used to check if auth should be skipped for this request
645func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
646 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
647 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
650// IsWhitelistedPath is used to check if the request path is allowed without auth
651func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
652 for _, u := range p.compiledRegex {
653 if u.MatchString(path) {
660// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
661var noCacheHeaders = map[string]string{
662 "Expires": time.Unix(0, 0).Format(time.RFC1123),
663 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
664 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
667// prepareNoCache prepares headers for preventing browser caching.
668func prepareNoCache(w http.ResponseWriter) {
669 // Set NoCache headers
670 for k, v := range noCacheHeaders {
675func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
676 if strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
680 switch path := req.URL.Path; {
681 case path == p.RobotsPath:
683 case p.IsWhitelistedRequest(req):
684 p.serveMux.ServeHTTP(rw, req)
685 case path == p.SignInPath:
687 case path == p.SignOutPath:
689 case path == p.OAuthStartPath:
690 p.OAuthStart(rw, req)
691 case path == p.OAuthCallbackPath:
692 p.OAuthCallback(rw, req)
693 case path == p.AuthOnlyPath:
694 p.AuthenticateOnly(rw, req)
695 case path == p.UserInfoPath:
702// SignIn serves a page prompting users to sign in
703func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
704 redirect, err := p.GetRedirect(req)
706 logger.Printf("Error obtaining redirect: %s", err.Error())
707 p.ErrorPage(rw, 500, "Internal Error", err.Error())
711 user, ok := p.ManualSignIn(rw, req)
713 session := &sessionsapi.SessionState{User: user}
714 p.SaveSession(rw, req, session)
715 http.Redirect(rw, req, redirect, http.StatusFound)
717 if p.SkipProviderButton {
718 p.OAuthStart(rw, req)
720 p.SignInPage(rw, req, http.StatusOK)
725//UserInfo endpoint outputs session email and preferred username in JSON format
726func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
728 session, err := p.getAuthenticatedSession(rw, req)
730 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
734 Email string `json:"email"`
735 PreferredUsername string `json:"preferredUsername,omitempty"`
737 Email: session.Email,
738 PreferredUsername: session.PreferredUsername,
740 rw.Header().Set("Content-Type", "application/json")
741 rw.WriteHeader(http.StatusOK)
742 json.NewEncoder(rw).Encode(userInfo)
745// SignOut sends a response to clear the authentication cookie
746func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
747 redirect, err := p.GetRedirect(req)
749 logger.Printf("Error obtaining redirect: %s", err.Error())
750 p.ErrorPage(rw, 500, "Internal Error", err.Error())
753 p.ClearSessionCookie(rw, req)
754 http.Redirect(rw, req, redirect, http.StatusFound)
757// OAuthStart starts the OAuth2 authentication flow
758func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
760 nonce, err := encryption.Nonce()
762 logger.Printf("Error obtaining nonce: %s", err.Error())
763 p.ErrorPage(rw, 500, "Internal Error", err.Error())
766 p.SetCSRFCookie(rw, req, nonce)
767 redirect, err := p.GetRedirect(req)
769 logger.Printf("Error obtaining redirect: %s", err.Error())
770 p.ErrorPage(rw, 500, "Internal Error", err.Error())
773 redirectURI := p.GetRedirectURI(req.Host)
774 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
777// OAuthCallback is the OAuth2 authentication flow callback that finishes the
778// OAuth2 authentication flow
779func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
780 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
782 // finish the oauth cycle
783 err := req.ParseForm()
785 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
786 p.ErrorPage(rw, 500, "Internal Error", err.Error())
789 errorString := req.Form.Get("error")
790 if errorString != "" {
791 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
792 p.ErrorPage(rw, 403, "Permission Denied", errorString)
796 session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code"))
798 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
799 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
803 s := strings.SplitN(req.Form.Get("state"), ":", 2)
805 logger.Printf("Error while parsing OAuth2 state: invalid length")
806 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
811 c, err := req.Cookie(p.CSRFCookieName)
813 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
814 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
817 p.ClearCSRFCookie(rw, req)
818 if c.Value != nonce {
819 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
820 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
824 if !p.IsValidRedirect(redirect) {
828 // set cookie, or deny
829 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
830 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
831 err := p.SaveSession(rw, req, session)
833 logger.Printf("%s %s", remoteAddr, err)
834 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
837 http.Redirect(rw, req, redirect, http.StatusFound)
839 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
840 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
844// AuthenticateOnly checks whether the user is currently logged in
845func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
846 session, err := p.getAuthenticatedSession(rw, req)
848 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
852 // we are authenticated
853 p.addHeadersForProxying(rw, req, session)
854 rw.WriteHeader(http.StatusAccepted)
857// Proxy proxies the user request if the user is authenticated else it prompts
858// them to authenticate
859func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
860 session, err := p.getAuthenticatedSession(rw, req)
863 // we are authenticated
864 p.addHeadersForProxying(rw, req, session)
865 p.serveMux.ServeHTTP(rw, req)
868 // we need to send the user to a login screen
870 // no point redirecting an AJAX request
871 p.ErrorJSON(rw, http.StatusUnauthorized)
875 if p.SkipProviderButton {
876 p.OAuthStart(rw, req)
878 p.SignInPage(rw, req, http.StatusForbidden)
883 logger.Printf("Unexpected internal error: %s", err)
884 p.ErrorPage(rw, http.StatusInternalServerError,
885 "Internal Error", "Internal Error")
890// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
891// Returns nil, ErrNeedsLogin if user needs to login.
892// Set-Cookie headers may be set on the response as a side-effect of calling this method.
893func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
894 var session *sessionsapi.SessionState
896 var saveSession, clearSession, revalidated bool
898 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
899 session, err = p.GetJwtSession(req)
901 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
908 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
910 session, err = p.LoadCookiedSession(req)
912 logger.Printf("Error loading cookied session: %s", err)
916 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
917 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
921 if ok, err := p.provider.RefreshSessionIfNeeded(req.Context(), session); err != nil {
922 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
932 if session != nil && session.IsExpired() {
933 logger.Printf("Removing session: token expired %s", session)
939 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
940 if !p.provider.ValidateSessionState(req.Context(), session) {
941 logger.Printf("Removing session: error validating %s", session)
948 if session != nil && session.Email != "" && !p.Validator(session.Email) {
949 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
955 if saveSession && session != nil {
956 err = p.SaveSession(rw, req, session)
958 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
964 p.ClearSessionCookie(rw, req)
968 session, err = p.CheckBasicAuth(req)
970 logger.Printf("Error during basic auth validation: %s", err)
975 return nil, ErrNeedsLogin
981// addHeadersForProxying adds the appropriate headers the request / response for proxying
982func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
984 if p.PreferEmailToUser && session.Email != "" {
985 req.SetBasicAuth(session.Email, p.BasicAuthPassword)
986 req.Header["X-Forwarded-User"] = []string{session.Email}
987 req.Header.Del("X-Forwarded-Email")
989 req.SetBasicAuth(session.User, p.BasicAuthPassword)
990 req.Header["X-Forwarded-User"] = []string{session.User}
991 if session.Email != "" {
992 req.Header["X-Forwarded-Email"] = []string{session.Email}
994 req.Header.Del("X-Forwarded-Email")
997 if session.PreferredUsername != "" {
998 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
1000 req.Header.Del("X-Forwarded-Preferred-Username")
1004 if p.PassUserHeaders {
1005 if p.PreferEmailToUser && session.Email != "" {
1006 req.Header["X-Forwarded-User"] = []string{session.Email}
1007 req.Header.Del("X-Forwarded-Email")
1009 req.Header["X-Forwarded-User"] = []string{session.User}
1010 if session.Email != "" {
1011 req.Header["X-Forwarded-Email"] = []string{session.Email}
1013 req.Header.Del("X-Forwarded-Email")
1017 if session.PreferredUsername != "" {
1018 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
1020 req.Header.Del("X-Forwarded-Preferred-Username")
1024 if p.SetXAuthRequest {
1025 rw.Header().Set("X-Auth-Request-User", session.User)
1026 if session.Email != "" {
1027 rw.Header().Set("X-Auth-Request-Email", session.Email)
1029 rw.Header().Del("X-Auth-Request-Email")
1031 if session.PreferredUsername != "" {
1032 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
1034 rw.Header().Del("X-Auth-Request-Preferred-Username")
1037 if p.PassAccessToken {
1038 if session.AccessToken != "" {
1039 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
1041 rw.Header().Del("X-Auth-Request-Access-Token")
1046 if p.PassAccessToken {
1047 if session.AccessToken != "" {
1048 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
1050 req.Header.Del("X-Forwarded-Access-Token")
1054 if p.PassAuthorization {
1055 if session.IDToken != "" {
1056 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
1058 req.Header.Del("Authorization")
1063 case p.PreferEmailToUser && session.Email != "":
1064 authVal := b64.StdEncoding.EncodeToString([]byte(session.Email + ":" + p.BasicAuthPassword))
1065 rw.Header().Set("Authorization", "Basic "+authVal)
1066 case session.User != "":
1067 authVal := b64.StdEncoding.EncodeToString([]byte(session.User + ":" + p.BasicAuthPassword))
1068 rw.Header().Set("Authorization", "Basic "+authVal)
1070 rw.Header().Del("Authorization")
1073 if p.SetAuthorization {
1074 if session.IDToken != "" {
1075 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
1077 rw.Header().Del("Authorization")
1081 if session.Email == "" {
1082 rw.Header().Set("GAP-Auth", session.User)
1084 rw.Header().Set("GAP-Auth", session.Email)
1088// CheckBasicAuth checks the requests Authorization header for basic auth
1089// credentials and authenticates these against the proxies HtpasswdFile
1090func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
1091 if p.HtpasswdFile == nil {
1094 auth := req.Header.Get("Authorization")
1098 s := strings.SplitN(auth, " ", 2)
1099 if len(s) != 2 || s[0] != "Basic" {
1100 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1102 b, err := b64.StdEncoding.DecodeString(s[1])
1106 pair := strings.SplitN(string(b), ":", 2)
1108 return nil, fmt.Errorf("invalid format %s", b)
1110 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
1111 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1112 return &sessionsapi.SessionState{User: pair[0]}, nil
1114 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1118// isAjax checks if a request is an ajax request
1119func isAjax(req *http.Request) bool {
1120 acceptValues := req.Header.Values("Accept")
1121 const ajaxReq = applicationJSON
1122 for _, v := range acceptValues {
1130// ErrorJSON returns the error code with an application/json mime type
1131func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1132 rw.Header().Set("Content-Type", applicationJSON)
1133 rw.WriteHeader(code)
1136// GetJwtSession loads a session based on a JWT token in the authorization header.
1137// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
1138func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1139 rawBearerToken, err := p.findBearerToken(req)
1144 // If we are using an oidc provider, go ahead and try that provider first with its Verifier
1145 // and Bearer Token -> Session converter
1146 if p.mainJwtBearerVerifier != nil {
1147 bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
1149 return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1153 // Otherwise, attempt to verify against the extra JWT issuers and use a more generic
1154 // Bearer Token -> Session converter
1155 for _, verifier := range p.extraJwtBearerVerifiers {
1156 bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1161 return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1163 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1166// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1167func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1168 auth := req.Header.Get("Authorization")
1169 s := strings.SplitN(auth, " ", 2)
1171 return "", fmt.Errorf("invalid authorization header %s", auth)
1173 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1174 var rawBearerToken string
1175 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1176 rawBearerToken = s[1]
1177 } else if s[0] == "Basic" {
1178 // Check if we have a Bearer token masquerading in Basic
1179 b, err := b64.StdEncoding.DecodeString(s[1])
1183 pair := strings.SplitN(string(b), ":", 2)
1185 return "", fmt.Errorf("invalid format %s", b)
1187 user, password := pair[0], pair[1]
1189 // check user, user+password, or just password for a token
1190 if jwtRegex.MatchString(user) {
1191 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1192 if password == "" || password == "x-oauth-basic" {
1193 rawBearerToken = user
1195 } else if jwtRegex.MatchString(password) {
1196 // support passwords and ignore user
1197 rawBearerToken = password
1200 if rawBearerToken == "" {
1201 return "", fmt.Errorf("no valid bearer token found in authorization header")
1204 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