20 "github.com/coreos/go-oidc"
21 "github.com/mbland/hmacauth"
22 sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
23 "github.com/pusher/oauth2_proxy/pkg/cookies"
24 "github.com/pusher/oauth2_proxy/pkg/encryption"
25 "github.com/pusher/oauth2_proxy/pkg/logger"
26 "github.com/pusher/oauth2_proxy/providers"
27 "github.com/yhat/wsutil"
31 // SignatureHeader is the name of the request header containing the GAP Signature
33 SignatureHeader = "GAP-Signature"
38 applicationJSON = "application/json"
41// SignatureHeaders contains the headers to be signed by the hmac algorithm
43var SignatureHeaders = []string{
51 "X-Forwarded-Access-Token",
57 // ErrNeedsLogin means the user should be redirected to the login page
58 ErrNeedsLogin = errors.New("redirect to login page")
61// OAuthProxy is the main authentication proxy
62type OAuthProxy struct {
70 CookieExpire time.Duration
71 CookieRefresh time.Duration
73 Validator func(string) bool
80 OAuthCallbackPath string
84 redirectURL *url.URL // the url to receive requests at
85 whitelistDomains []string
86 provider providers.Provider
87 providerNameOverride string
88 sessionStore sessionsapi.SessionStore
91 HtpasswdFile *HtpasswdFile
92 DisplayHtpasswdForm bool
96 SkipProviderButton bool
98 BasicAuthPassword string
100 SetAuthorization bool
101 PassAuthorization bool
102 skipAuthRegex []string
103 skipAuthPreflight bool
104 skipJwtBearerTokens bool
105 jwtBearerVerifiers []*oidc.IDTokenVerifier
106 compiledRegex []*regexp.Regexp
107 templates *template.Template
112// UpstreamProxy represents an upstream server to proxy to
113type UpstreamProxy struct {
116 wsHandler http.Handler
117 auth hmacauth.HmacAuth
120// ServeHTTP proxies requests to the upstream provider while signing the
122func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
123 w.Header().Set("GAP-Upstream-Address", u.upstream)
125 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
126 u.auth.SignRequest(r)
128 if u.wsHandler != nil && strings.EqualFold(r.Header.Get("Connection"), "upgrade") && r.Header.Get("Upgrade") == "websocket" {
129 u.wsHandler.ServeHTTP(w, r)
131 u.handler.ServeHTTP(w, r)
136// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
138func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) {
139 proxy = httputil.NewSingleHostReverseProxy(target)
140 proxy.FlushInterval = opts.FlushInterval
141 if opts.SSLUpstreamInsecureSkipVerify {
142 proxy.Transport = &http.Transport{
143 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
149func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
150 director := proxy.Director
151 proxy.Director = func(req *http.Request) {
153 // use RequestURI so that we aren't unescaping encoded slashes in the request path
154 req.Host = target.Host
155 req.URL.Opaque = req.RequestURI
156 req.URL.RawQuery = ""
160func setProxyDirector(proxy *httputil.ReverseProxy) {
161 director := proxy.Director
162 proxy.Director = func(req *http.Request) {
164 // use RequestURI so that we aren't unescaping encoded slashes in the request path
165 req.URL.Opaque = req.RequestURI
166 req.URL.RawQuery = ""
170// NewFileServer creates a http.Handler to serve files from the filesystem
171func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
172 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
175// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
176func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
178 proxy := NewReverseProxy(u, opts)
179 if !opts.PassHostHeader {
180 setProxyUpstreamHostHeader(proxy, u)
182 setProxyDirector(proxy)
185 // this should give us a wss:// scheme if the url is https:// based.
186 var wsProxy *wsutil.ReverseProxy
187 if opts.ProxyWebSockets {
188 wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
189 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
190 wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
192 return &UpstreamProxy{
200// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
201func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
202 serveMux := http.NewServeMux()
203 var auth hmacauth.HmacAuth
204 if sigData := opts.signatureData; sigData != nil {
205 auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
206 SignatureHeader, SignatureHeaders)
208 for _, u := range opts.proxyURLs {
212 case httpScheme, httpsScheme:
213 logger.Printf("mapping path %q => upstream %q", path, u)
214 proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
215 serveMux.Handle(path, proxy)
217 responseCode, err := strconv.Atoi(host)
219 logger.Printf("unable to convert %q to int, use default \"200\"", host)
223 serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
224 rw.WriteHeader(responseCode)
225 fmt.Fprintf(rw, "Authenticated")
228 if u.Fragment != "" {
231 logger.Printf("mapping path %q => file system %q", path, u.Path)
232 proxy := NewFileServer(path, u.Path)
233 uProxy := UpstreamProxy{
239 serveMux.Handle(path, &uProxy)
241 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
244 for _, u := range opts.CompiledRegex {
245 logger.Printf("compiled skip-auth-regex => %q", u)
248 if opts.SkipJwtBearerTokens {
249 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
250 for _, issuer := range opts.ExtraJwtIssuers {
251 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
254 redirectURL := opts.redirectURL
255 if redirectURL.Path == "" {
256 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
259 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID)
260 refresh := "disabled"
261 if opts.CookieRefresh != time.Duration(0) {
262 refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
265 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s samesite:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, opts.CookieSameSite, refresh)
268 CookieName: opts.CookieName,
269 CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
270 CookieSeed: opts.CookieSecret,
271 CookieDomain: opts.CookieDomain,
272 CookiePath: opts.CookiePath,
273 CookieSecure: opts.CookieSecure,
274 CookieHTTPOnly: opts.CookieHTTPOnly,
275 CookieExpire: opts.CookieExpire,
276 CookieRefresh: opts.CookieRefresh,
277 CookieSameSite: opts.CookieSameSite,
278 Validator: validator,
280 RobotsPath: "/robots.txt",
281 PingPath: opts.PingPath,
282 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
283 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
284 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
285 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
286 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
287 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
289 ProxyPrefix: opts.ProxyPrefix,
290 provider: opts.provider,
291 providerNameOverride: opts.ProviderName,
292 sessionStore: opts.sessionStore,
294 redirectURL: redirectURL,
295 whitelistDomains: opts.WhitelistDomains,
296 skipAuthRegex: opts.SkipAuthRegex,
297 skipAuthPreflight: opts.SkipAuthPreflight,
298 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
299 jwtBearerVerifiers: opts.jwtBearerVerifiers,
300 compiledRegex: opts.CompiledRegex,
301 SetXAuthRequest: opts.SetXAuthRequest,
302 PassBasicAuth: opts.PassBasicAuth,
303 PassUserHeaders: opts.PassUserHeaders,
304 BasicAuthPassword: opts.BasicAuthPassword,
305 PassAccessToken: opts.PassAccessToken,
306 SetAuthorization: opts.SetAuthorization,
307 PassAuthorization: opts.PassAuthorization,
308 SkipProviderButton: opts.SkipProviderButton,
309 templates: loadTemplates(opts.CustomTemplatesDir),
315// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
316// redirect clients to once authenticated
317func (p *OAuthProxy) GetRedirectURI(host string) string {
318 // default to the request Host if not set
319 if p.redirectURL.Host != "" {
320 return p.redirectURL.String()
326 u.Scheme = httpsScheme
328 u.Scheme = httpScheme
335func (p *OAuthProxy) displayCustomLoginForm() bool {
336 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
339func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState, err error) {
341 return nil, errors.New("missing code")
343 redirectURI := p.GetRedirectURI(host)
344 s, err = p.provider.Redeem(redirectURI, code)
350 s.Email, err = p.provider.GetEmailAddress(s)
354 s.User, err = p.provider.GetUserName(s)
355 if err != nil && err.Error() == "not implemented" {
362// MakeCSRFCookie creates a cookie for CSRF
363func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
364 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
367func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
368 if p.CookieDomain != "" {
370 if h, _, err := net.SplitHostPort(domain); err == nil {
373 if !strings.HasSuffix(domain, p.CookieDomain) {
374 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain)
382 Domain: p.CookieDomain,
383 HttpOnly: p.CookieHTTPOnly,
384 Secure: p.CookieSecure,
385 Expires: now.Add(expiration),
386 SameSite: cookies.ParseSameSite(p.CookieSameSite),
390// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
392func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
393 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
396// SetCSRFCookie adds a CSRF cookie to the response
397func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
398 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
401// ClearSessionCookie creates a cookie to unset the user's authentication cookie
402// stored in the user's session
403func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
404 return p.sessionStore.Clear(rw, req)
407// LoadCookiedSession reads the user's authentication details from the request
408func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
409 return p.sessionStore.Load(req)
412// SaveSession creates a new session cookie value and sets this on the response
413func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
414 return p.sessionStore.Save(rw, req, s)
417// RobotsTxt disallows scraping pages from the OAuthProxy
418func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
419 rw.WriteHeader(http.StatusOK)
420 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
423// PingPage responds 200 OK to requests
424func (p *OAuthProxy) PingPage(rw http.ResponseWriter) {
425 rw.WriteHeader(http.StatusOK)
426 fmt.Fprintf(rw, "OK")
429// ErrorPage writes an error response
430func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
437 Title: fmt.Sprintf("%d %s", code, title),
439 ProxyPrefix: p.ProxyPrefix,
441 p.templates.ExecuteTemplate(rw, "error.html", t)
444// SignInPage writes the sing in template to the response
445func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
446 p.ClearSessionCookie(rw, req)
449 redirecURL := req.URL.RequestURI()
450 if req.Header.Get("X-Auth-Request-Redirect") != "" {
451 redirecURL = req.Header.Get("X-Auth-Request-Redirect")
453 if redirecURL == p.SignInPath {
466 ProviderName: p.provider.Data().ProviderName,
467 SignInMessage: p.SignInMessage,
468 CustomLogin: p.displayCustomLoginForm(),
469 Redirect: redirecURL,
471 ProxyPrefix: p.ProxyPrefix,
472 Footer: template.HTML(p.Footer),
474 if p.providerNameOverride != "" {
475 t.ProviderName = p.providerNameOverride
477 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
480// ManualSignIn handles basic auth logins to the proxy
481func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
482 if req.Method != "POST" || p.HtpasswdFile == nil {
485 user := req.FormValue("username")
486 passwd := req.FormValue("password")
491 if p.HtpasswdFile.Validate(user, passwd) {
492 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
495 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
499// GetRedirect reads the query parameter to get the URL to redirect clients to
500// once authenticated with the OAuthProxy
501func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
502 err = req.ParseForm()
507 redirect = req.Header.Get("X-Auth-Request-Redirect")
508 if req.Form.Get("rd") != "" {
509 redirect = req.Form.Get("rd")
511 if !p.IsValidRedirect(redirect) {
512 redirect = req.URL.Path
513 if strings.HasPrefix(redirect, p.ProxyPrefix) {
521// splitHostPort separates host and port. If the port is not valid, it returns
522// the entire input as host, and it doesn't check the validity of the host.
523// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
524// *** taken from net/url, modified validOptionalPort() to accept ":*"
525func splitHostPort(hostport string) (host, port string) {
528 colon := strings.LastIndexByte(host, ':')
529 if colon != -1 && validOptionalPort(host[colon:]) {
530 host, port = host[:colon], host[colon+1:]
533 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
534 host = host[1 : len(host)-1]
540// validOptionalPort reports whether port is either an empty string
541// or matches /^:\d*$/
542// *** taken from net/url, modified to accept ":*"
543func validOptionalPort(port string) bool {
544 if port == "" || port == ":*" {
550 for _, b := range port[1:] {
551 if b < '0' || b > '9' {
558// IsValidRedirect checks whether the redirect URL is whitelisted
559func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
561 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !strings.HasPrefix(redirect, "/\\"):
563 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
564 redirectURL, err := url.Parse(redirect)
568 redirectHostname := redirectURL.Hostname()
570 for _, domain := range p.whitelistDomains {
571 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
572 if domainHostname == "" {
576 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
577 // the domain names match, now validate the ports
578 // if the whitelisted domain's port is '*', allow all ports
579 // if the whitelisted domain contains a specific port, only allow that port
580 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
581 redirectPort := redirectURL.Port()
582 if (domainPort == "*") ||
583 (domainPort == redirectPort) ||
584 (domainPort == "" && redirectPort == "") {
596// IsWhitelistedRequest is used to check if auth should be skipped for this request
597func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
598 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
599 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
602// IsWhitelistedPath is used to check if the request path is allowed without auth
603func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
604 for _, u := range p.compiledRegex {
605 if u.MatchString(path) {
612func getRemoteAddr(req *http.Request) (s string) {
614 if req.Header.Get("X-Real-IP") != "" {
615 s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
620func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
621 switch path := req.URL.Path; {
622 case path == p.RobotsPath:
624 case path == p.PingPath:
626 case p.IsWhitelistedRequest(req):
627 p.serveMux.ServeHTTP(rw, req)
628 case path == p.SignInPath:
630 case path == p.SignOutPath:
632 case path == p.OAuthStartPath:
633 p.OAuthStart(rw, req)
634 case path == p.OAuthCallbackPath:
635 p.OAuthCallback(rw, req)
636 case path == p.AuthOnlyPath:
637 p.AuthenticateOnly(rw, req)
638 case path == p.UserInfoPath:
645// SignIn serves a page prompting users to sign in
646func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
647 redirect, err := p.GetRedirect(req)
649 logger.Printf("Error obtaining redirect: %s", err.Error())
650 p.ErrorPage(rw, 500, "Internal Error", err.Error())
654 user, ok := p.ManualSignIn(rw, req)
656 session := &sessionsapi.SessionState{User: user}
657 p.SaveSession(rw, req, session)
658 http.Redirect(rw, req, redirect, 302)
660 if p.SkipProviderButton {
661 p.OAuthStart(rw, req)
663 p.SignInPage(rw, req, http.StatusOK)
668//UserInfo endpoint outputs session email in JSON format
669func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
671 session, err := p.getAuthenticatedSession(rw, req)
673 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
677 Email string `json:"email"`
679 rw.Header().Set("Content-Type", "application/json")
680 rw.WriteHeader(http.StatusOK)
681 json.NewEncoder(rw).Encode(userInfo)
684// SignOut sends a response to clear the authentication cookie
685func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
686 redirect, err := p.GetRedirect(req)
688 logger.Printf("Error obtaining redirect: %s", err.Error())
689 p.ErrorPage(rw, 500, "Internal Error", err.Error())
692 p.ClearSessionCookie(rw, req)
693 http.Redirect(rw, req, redirect, 302)
696// OAuthStart starts the OAuth2 authentication flow
697func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
698 nonce, err := encryption.Nonce()
700 logger.Printf("Error obtaining nonce: %s", err.Error())
701 p.ErrorPage(rw, 500, "Internal Error", err.Error())
704 p.SetCSRFCookie(rw, req, nonce)
705 redirect, err := p.GetRedirect(req)
707 logger.Printf("Error obtaining redirect: %s", err.Error())
708 p.ErrorPage(rw, 500, "Internal Error", err.Error())
711 redirectURI := p.GetRedirectURI(req.Host)
712 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302)
715// OAuthCallback is the OAuth2 authentication flow callback that finishes the
716// OAuth2 authentication flow
717func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
718 remoteAddr := getRemoteAddr(req)
720 // finish the oauth cycle
721 err := req.ParseForm()
723 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
724 p.ErrorPage(rw, 500, "Internal Error", err.Error())
727 errorString := req.Form.Get("error")
728 if errorString != "" {
729 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
730 p.ErrorPage(rw, 403, "Permission Denied", errorString)
734 session, err := p.redeemCode(req.Host, req.Form.Get("code"))
736 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
737 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
741 s := strings.SplitN(req.Form.Get("state"), ":", 2)
743 logger.Printf("Error while parsing OAuth2 state: invalid length")
744 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
749 c, err := req.Cookie(p.CSRFCookieName)
751 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
752 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
755 p.ClearCSRFCookie(rw, req)
756 if c.Value != nonce {
757 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
758 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
762 if !p.IsValidRedirect(redirect) {
766 // set cookie, or deny
767 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
768 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
769 err := p.SaveSession(rw, req, session)
771 logger.Printf("%s %s", remoteAddr, err)
772 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
775 http.Redirect(rw, req, redirect, 302)
777 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
778 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
782// AuthenticateOnly checks whether the user is currently logged in
783func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
784 session, err := p.getAuthenticatedSession(rw, req)
786 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
790 // we are authenticated
791 p.addHeadersForProxying(rw, req, session)
792 rw.WriteHeader(http.StatusAccepted)
795// Proxy proxies the user request if the user is authenticated else it prompts
796// them to authenticate
797func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
798 session, err := p.getAuthenticatedSession(rw, req)
801 // we are authenticated
802 p.addHeadersForProxying(rw, req, session)
803 p.serveMux.ServeHTTP(rw, req)
806 // we need to send the user to a login screen
808 // no point redirecting an AJAX request
809 p.ErrorJSON(rw, http.StatusUnauthorized)
813 if p.SkipProviderButton {
814 p.OAuthStart(rw, req)
816 p.SignInPage(rw, req, http.StatusForbidden)
821 logger.Printf("Unexpected internal error: %s", err)
822 p.ErrorPage(rw, http.StatusInternalServerError,
823 "Internal Error", "Internal Error")
828// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
829// Returns nil, ErrNeedsLogin if user needs to login.
830// Set-Cookie headers may be set on the response as a side-effect of calling this method.
831func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
832 var session *sessionsapi.SessionState
834 var saveSession, clearSession, revalidated bool
836 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
837 session, err = p.GetJwtSession(req)
839 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
846 remoteAddr := getRemoteAddr(req)
848 session, err = p.LoadCookiedSession(req)
850 logger.Printf("Error loading cookied session: %s", err)
854 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
855 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
859 if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
860 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
870 if session != nil && session.IsExpired() {
871 logger.Printf("Removing session: token expired %s", session)
877 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
878 if !p.provider.ValidateSessionState(session) {
879 logger.Printf("Removing session: error validating %s", session)
886 if session != nil && session.Email != "" {
887 if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
888 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
895 if saveSession && session != nil {
896 err = p.SaveSession(rw, req, session)
898 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
904 p.ClearSessionCookie(rw, req)
908 session, err = p.CheckBasicAuth(req)
910 logger.Printf("Error during basic auth validation: %s", err)
915 return nil, ErrNeedsLogin
921// addHeadersForProxying adds the appropriate headers the request / response for proxying
922func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
924 req.SetBasicAuth(session.User, p.BasicAuthPassword)
925 req.Header["X-Forwarded-User"] = []string{session.User}
926 if session.Email != "" {
927 req.Header["X-Forwarded-Email"] = []string{session.Email}
929 req.Header.Del("X-Forwarded-Email")
933 if p.PassUserHeaders {
934 req.Header["X-Forwarded-User"] = []string{session.User}
935 if session.Email != "" {
936 req.Header["X-Forwarded-Email"] = []string{session.Email}
938 req.Header.Del("X-Forwarded-Email")
942 if p.SetXAuthRequest {
943 rw.Header().Set("X-Auth-Request-User", session.User)
944 if session.Email != "" {
945 rw.Header().Set("X-Auth-Request-Email", session.Email)
947 rw.Header().Del("X-Auth-Request-Email")
950 if p.PassAccessToken {
951 if session.AccessToken != "" {
952 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
954 rw.Header().Del("X-Auth-Request-Access-Token")
959 if p.PassAccessToken {
960 if session.AccessToken != "" {
961 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
963 req.Header.Del("X-Forwarded-Access-Token")
967 if p.PassAuthorization {
968 if session.IDToken != "" {
969 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
971 req.Header.Del("Authorization")
974 if p.SetAuthorization {
975 if session.IDToken != "" {
976 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
978 rw.Header().Del("Authorization")
982 if session.Email == "" {
983 rw.Header().Set("GAP-Auth", session.User)
985 rw.Header().Set("GAP-Auth", session.Email)
989// CheckBasicAuth checks the requests Authorization header for basic auth
990// credentials and authenticates these against the proxies HtpasswdFile
991func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
992 if p.HtpasswdFile == nil {
995 auth := req.Header.Get("Authorization")
999 s := strings.SplitN(auth, " ", 2)
1000 if len(s) != 2 || s[0] != "Basic" {
1001 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1003 b, err := b64.StdEncoding.DecodeString(s[1])
1007 pair := strings.SplitN(string(b), ":", 2)
1009 return nil, fmt.Errorf("invalid format %s", b)
1011 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
1012 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1013 return &sessionsapi.SessionState{User: pair[0]}, nil
1015 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1019// isAjax checks if a request is an ajax request
1020func isAjax(req *http.Request) bool {
1021 acceptValues, ok := req.Header["accept"]
1023 acceptValues = req.Header["Accept"]
1025 const ajaxReq = applicationJSON
1026 for _, v := range acceptValues {
1034// ErrorJSON returns the error code with an application/json mime type
1035func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1036 rw.Header().Set("Content-Type", applicationJSON)
1037 rw.WriteHeader(code)
1040// GetJwtSession loads a session based on a JWT token in the authorization header.
1041func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1042 rawBearerToken, err := p.findBearerToken(req)
1047 ctx := context.Background()
1048 var session *sessionsapi.SessionState
1049 for _, verifier := range p.jwtBearerVerifiers {
1050 bearerToken, err := verifier.Verify(ctx, rawBearerToken)
1053 logger.Printf("failed to verify bearer token: %v", err)
1058 Subject string `json:"sub"`
1059 Email string `json:"email"`
1060 Verified *bool `json:"email_verified"`
1063 if err := bearerToken.Claims(&claims); err != nil {
1064 return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
1067 if claims.Email == "" {
1068 claims.Email = claims.Subject
1071 if claims.Verified != nil && !*claims.Verified {
1072 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
1075 session = &sessionsapi.SessionState{
1076 AccessToken: rawBearerToken,
1077 IDToken: rawBearerToken,
1079 ExpiresOn: bearerToken.Expiry,
1080 Email: claims.Email,
1085 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1088// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1089func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1090 auth := req.Header.Get("Authorization")
1091 s := strings.SplitN(auth, " ", 2)
1093 return "", fmt.Errorf("invalid authorization header %s", auth)
1095 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1096 var rawBearerToken string
1097 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1098 rawBearerToken = s[1]
1099 } else if s[0] == "Basic" {
1100 // Check if we have a Bearer token masquerading in Basic
1101 b, err := b64.StdEncoding.DecodeString(s[1])
1105 pair := strings.SplitN(string(b), ":", 2)
1107 return "", fmt.Errorf("invalid format %s", b)
1109 user, password := pair[0], pair[1]
1111 // check user, user+password, or just password for a token
1112 if jwtRegex.MatchString(user) {
1113 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1114 if password == "" || password == "x-oauth-basic" {
1115 rawBearerToken = user
1117 } else if jwtRegex.MatchString(password) {
1118 // support passwords and ignore user
1119 rawBearerToken = password
1122 if rawBearerToken == "" {
1123 return "", fmt.Errorf("no valid bearer token found in authorization header")
1126 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