package main import ( "context" "crypto/tls" b64 "encoding/base64" "errors" "fmt" "html/template" "net" "net/http" "net/http/httputil" "net/url" "regexp" "strings" "time" "github.com/coreos/go-oidc" "github.com/mbland/hmacauth" sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions" "github.com/pusher/oauth2_proxy/pkg/encryption" "github.com/pusher/oauth2_proxy/pkg/logger" "github.com/pusher/oauth2_proxy/providers" "github.com/yhat/wsutil" ) const ( // SignatureHeader is the name of the request header containing the GAP Signature // Part of hmacauth SignatureHeader = "GAP-Signature" httpScheme = "http" httpsScheme = "https" applicationJSON = "application/json" ) // SignatureHeaders contains the headers to be signed by the hmac algorithm // Part of hmacauth var SignatureHeaders = []string{ "Content-Length", "Content-Md5", "Content-Type", "Date", "Authorization", "X-Forwarded-User", "X-Forwarded-Email", "X-Forwarded-Access-Token", "Cookie", "Gap-Auth", } var ( // ErrNeedsLogin means the user should be redirected to the login page ErrNeedsLogin = errors.New("redirect to login page") ) // OAuthProxy is the main authentication proxy type OAuthProxy struct { CookieSeed string CookieName string CSRFCookieName string CookieDomain string CookiePath string CookieSecure bool CookieHTTPOnly bool CookieExpire time.Duration CookieRefresh time.Duration Validator func(string) bool RobotsPath string PingPath string SignInPath string SignOutPath string OAuthStartPath string OAuthCallbackPath string AuthOnlyPath string redirectURL *url.URL // the url to receive requests at whitelistDomains []string provider providers.Provider sessionStore sessionsapi.SessionStore ProxyPrefix string SignInMessage string HtpasswdFile *HtpasswdFile DisplayHtpasswdForm bool serveMux http.Handler SetXAuthRequest bool PassBasicAuth bool SkipProviderButton bool PassUserHeaders bool BasicAuthPassword string PassAccessToken bool SetAuthorization bool PassAuthorization bool skipAuthRegex []string skipAuthPreflight bool skipJwtBearerTokens bool jwtBearerVerifiers []*oidc.IDTokenVerifier compiledRegex []*regexp.Regexp templates *template.Template Banner string Footer string } // UpstreamProxy represents an upstream server to proxy to type UpstreamProxy struct { upstream string handler http.Handler wsHandler http.Handler auth hmacauth.HmacAuth } // ServeHTTP proxies requests to the upstream provider while signing the // request headers func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("GAP-Upstream-Address", u.upstream) if u.auth != nil { r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth")) u.auth.SignRequest(r) } if u.wsHandler != nil && strings.ToLower(r.Header.Get("Connection")) == "upgrade" && r.Header.Get("Upgrade") == "websocket" { u.wsHandler.ServeHTTP(w, r) } else { u.handler.ServeHTTP(w, r) } } // NewReverseProxy creates a new reverse proxy for proxying requests to upstream // servers func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) { proxy = httputil.NewSingleHostReverseProxy(target) proxy.FlushInterval = opts.FlushInterval if opts.SSLUpstreamInsecureSkipVerify { proxy.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } return proxy } func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) { director := proxy.Director proxy.Director = func(req *http.Request) { director(req) // use RequestURI so that we aren't unescaping encoded slashes in the request path req.Host = target.Host req.URL.Opaque = req.RequestURI req.URL.RawQuery = "" } } func setProxyDirector(proxy *httputil.ReverseProxy) { director := proxy.Director proxy.Director = func(req *http.Request) { director(req) // use RequestURI so that we aren't unescaping encoded slashes in the request path req.URL.Opaque = req.RequestURI req.URL.RawQuery = "" } } // NewFileServer creates a http.Handler to serve files from the filesystem func NewFileServer(path string, filesystemPath string) (proxy http.Handler) { return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath))) } // NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler { u.Path = "" proxy := NewReverseProxy(u, opts) if !opts.PassHostHeader { setProxyUpstreamHostHeader(proxy, u) } else { setProxyDirector(proxy) } // this should give us a wss:// scheme if the url is https:// based. var wsProxy *wsutil.ReverseProxy if opts.ProxyWebSockets { wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http") wsURL := &url.URL{Scheme: wsScheme, Host: u.Host} wsProxy = wsutil.NewSingleHostReverseProxy(wsURL) } return &UpstreamProxy{ upstream: u.Host, handler: proxy, wsHandler: wsProxy, auth: auth, } } // NewOAuthProxy creates a new instance of OOuthProxy from the options provided func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { serveMux := http.NewServeMux() var auth hmacauth.HmacAuth if sigData := opts.signatureData; sigData != nil { auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key), SignatureHeader, SignatureHeaders) } for _, u := range opts.proxyURLs { path := u.Path switch u.Scheme { case httpScheme, httpsScheme: logger.Printf("mapping path %q => upstream %q", path, u) proxy := NewWebSocketOrRestReverseProxy(u, opts, auth) serveMux.Handle(path, proxy) case "file": if u.Fragment != "" { path = u.Fragment } logger.Printf("mapping path %q => file system %q", path, u.Path) proxy := NewFileServer(path, u.Path) uProxy := UpstreamProxy{ upstream: path, handler: proxy, wsHandler: nil, auth: nil, } serveMux.Handle(path, &uProxy) default: panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme)) } } for _, u := range opts.CompiledRegex { logger.Printf("compiled skip-auth-regex => %q", u) } if opts.SkipJwtBearerTokens { logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL) for _, issuer := range opts.ExtraJwtIssuers { logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer) } } redirectURL := opts.redirectURL if redirectURL.Path == "" { redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) } logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID) refresh := "disabled" if opts.CookieRefresh != time.Duration(0) { refresh = fmt.Sprintf("after %s", opts.CookieRefresh) } logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, refresh) return &OAuthProxy{ CookieName: opts.CookieName, CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"), CookieSeed: opts.CookieSecret, CookieDomain: opts.CookieDomain, CookiePath: opts.CookiePath, CookieSecure: opts.CookieSecure, CookieHTTPOnly: opts.CookieHTTPOnly, CookieExpire: opts.CookieExpire, CookieRefresh: opts.CookieRefresh, Validator: validator, RobotsPath: "/robots.txt", PingPath: opts.PingPath, SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix), OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix), OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix), AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix), ProxyPrefix: opts.ProxyPrefix, provider: opts.provider, sessionStore: opts.sessionStore, serveMux: serveMux, redirectURL: redirectURL, whitelistDomains: opts.WhitelistDomains, skipAuthRegex: opts.SkipAuthRegex, skipAuthPreflight: opts.SkipAuthPreflight, skipJwtBearerTokens: opts.SkipJwtBearerTokens, jwtBearerVerifiers: opts.jwtBearerVerifiers, compiledRegex: opts.CompiledRegex, SetXAuthRequest: opts.SetXAuthRequest, PassBasicAuth: opts.PassBasicAuth, PassUserHeaders: opts.PassUserHeaders, BasicAuthPassword: opts.BasicAuthPassword, PassAccessToken: opts.PassAccessToken, SetAuthorization: opts.SetAuthorization, PassAuthorization: opts.PassAuthorization, SkipProviderButton: opts.SkipProviderButton, templates: loadTemplates(opts.CustomTemplatesDir), Banner: opts.Banner, Footer: opts.Footer, } } // GetRedirectURI returns the redirectURL that the upstream OAuth Provider will // redirect clients to once authenticated func (p *OAuthProxy) GetRedirectURI(host string) string { // default to the request Host if not set if p.redirectURL.Host != "" { return p.redirectURL.String() } var u url.URL u = *p.redirectURL if u.Scheme == "" { if p.CookieSecure { u.Scheme = httpsScheme } else { u.Scheme = httpScheme } } u.Host = host return u.String() } func (p *OAuthProxy) displayCustomLoginForm() bool { return p.HtpasswdFile != nil && p.DisplayHtpasswdForm } func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState, err error) { if code == "" { return nil, errors.New("missing code") } redirectURI := p.GetRedirectURI(host) s, err = p.provider.Redeem(redirectURI, code) if err != nil { return } if s.Email == "" { s.Email, err = p.provider.GetEmailAddress(s) } if s.User == "" { s.User, err = p.provider.GetUserName(s) if err != nil && err.Error() == "not implemented" { err = nil } } return } // MakeCSRFCookie creates a cookie for CSRF func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie { return p.makeCookie(req, p.CSRFCookieName, value, expiration, now) } func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie { if p.CookieDomain != "" { domain := req.Host if h, _, err := net.SplitHostPort(domain); err == nil { domain = h } if !strings.HasSuffix(domain, p.CookieDomain) { logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain) } } return &http.Cookie{ Name: name, Value: value, Path: p.CookiePath, Domain: p.CookieDomain, HttpOnly: p.CookieHTTPOnly, Secure: p.CookieSecure, Expires: now.Add(expiration), } } // ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's // session func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) { http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now())) } // SetCSRFCookie adds a CSRF cookie to the response func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) { http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now())) } // ClearSessionCookie creates a cookie to unset the user's authentication cookie // stored in the user's session func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error { return p.sessionStore.Clear(rw, req) } // LoadCookiedSession reads the user's authentication details from the request func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) { return p.sessionStore.Load(req) } // SaveSession creates a new session cookie value and sets this on the response func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error { return p.sessionStore.Save(rw, req, s) } // RobotsTxt disallows scraping pages from the OAuthProxy func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) { rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, "User-agent: *\nDisallow: /") } // PingPage responds 200 OK to requests func (p *OAuthProxy) PingPage(rw http.ResponseWriter) { rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, "OK") } // ErrorPage writes an error response func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) { rw.WriteHeader(code) t := struct { Title string Message string ProxyPrefix string }{ Title: fmt.Sprintf("%d %s", code, title), Message: message, ProxyPrefix: p.ProxyPrefix, } p.templates.ExecuteTemplate(rw, "error.html", t) } // SignInPage writes the sing in template to the response func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) { p.ClearSessionCookie(rw, req) rw.WriteHeader(code) redirecURL := req.URL.RequestURI() if req.Header.Get("X-Auth-Request-Redirect") != "" { redirecURL = req.Header.Get("X-Auth-Request-Redirect") } if redirecURL == p.SignInPath { redirecURL = "/" } t := struct { ProviderName string SignInMessage string CustomLogin bool Redirect string Version string ProxyPrefix string Footer template.HTML }{ ProviderName: p.provider.Data().ProviderName, SignInMessage: p.SignInMessage, CustomLogin: p.displayCustomLoginForm(), Redirect: redirecURL, Version: VERSION, ProxyPrefix: p.ProxyPrefix, Footer: template.HTML(p.Footer), } p.templates.ExecuteTemplate(rw, "sign_in.html", t) } // ManualSignIn handles basic auth logins to the proxy func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) { if req.Method != "POST" || p.HtpasswdFile == nil { return "", false } user := req.FormValue("username") passwd := req.FormValue("password") if user == "" { return "", false } // check auth if p.HtpasswdFile.Validate(user, passwd) { logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile") return user, true } logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile") return "", false } // GetRedirect reads the query parameter to get the URL to redirect clients to // once authenticated with the OAuthProxy func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) { err = req.ParseForm() if err != nil { return } redirect = req.Header.Get("X-Auth-Request-Redirect") if req.Form.Get("rd") != "" { redirect = req.Form.Get("rd") } if !p.IsValidRedirect(redirect) { redirect = req.URL.Path if strings.HasPrefix(redirect, p.ProxyPrefix) { redirect = "/" } } return } // IsValidRedirect checks whether the redirect URL is whitelisted func (p *OAuthProxy) IsValidRedirect(redirect string) bool { switch { case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//"): return true case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"): redirectURL, err := url.Parse(redirect) if err != nil { return false } for _, domain := range p.whitelistDomains { if (redirectURL.Host == domain) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectURL.Host, domain)) { return true } } return false default: return false } } // IsWhitelistedRequest is used to check if auth should be skipped for this request func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool { isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) } // IsWhitelistedPath is used to check if the request path is allowed without auth func (p *OAuthProxy) IsWhitelistedPath(path string) bool { for _, u := range p.compiledRegex { if u.MatchString(path) { return true } } return false } func getRemoteAddr(req *http.Request) (s string) { s = req.RemoteAddr if req.Header.Get("X-Real-IP") != "" { s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP")) } return } func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { switch path := req.URL.Path; { case path == p.RobotsPath: p.RobotsTxt(rw) case path == p.PingPath: p.PingPage(rw) case p.IsWhitelistedRequest(req): p.serveMux.ServeHTTP(rw, req) case path == p.SignInPath: p.SignIn(rw, req) case path == p.SignOutPath: p.SignOut(rw, req) case path == p.OAuthStartPath: p.OAuthStart(rw, req) case path == p.OAuthCallbackPath: p.OAuthCallback(rw, req) case path == p.AuthOnlyPath: p.AuthenticateOnly(rw, req) default: p.Proxy(rw, req) } } // SignIn serves a page prompting users to sign in func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) { redirect, err := p.GetRedirect(req) if err != nil { logger.Printf("Error obtaining redirect: %s", err.Error()) p.ErrorPage(rw, 500, "Internal Error", err.Error()) return } user, ok := p.ManualSignIn(rw, req) if ok { session := &sessionsapi.SessionState{User: user} p.SaveSession(rw, req, session) http.Redirect(rw, req, redirect, 302) } else { if p.SkipProviderButton { p.OAuthStart(rw, req) } else { p.SignInPage(rw, req, http.StatusOK) } } } // SignOut sends a response to clear the authentication cookie func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { p.ClearSessionCookie(rw, req) http.Redirect(rw, req, "/", 302) } // OAuthStart starts the OAuth2 authentication flow func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { nonce, err := encryption.Nonce() if err != nil { logger.Printf("Error obtaining nonce: %s", err.Error()) p.ErrorPage(rw, 500, "Internal Error", err.Error()) return } p.SetCSRFCookie(rw, req, nonce) redirect, err := p.GetRedirect(req) if err != nil { logger.Printf("Error obtaining redirect: %s", err.Error()) p.ErrorPage(rw, 500, "Internal Error", err.Error()) return } redirectURI := p.GetRedirectURI(req.Host) http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302) } // OAuthCallback is the OAuth2 authentication flow callback that finishes the // OAuth2 authentication flow func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { remoteAddr := getRemoteAddr(req) // finish the oauth cycle err := req.ParseForm() if err != nil { logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error()) p.ErrorPage(rw, 500, "Internal Error", err.Error()) return } errorString := req.Form.Get("error") if errorString != "" { logger.Printf("Error while parsing OAuth2 callback: %s ", errorString) p.ErrorPage(rw, 403, "Permission Denied", errorString) return } session, err := p.redeemCode(req.Host, req.Form.Get("code")) if err != nil { logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error()) p.ErrorPage(rw, 500, "Internal Error", "Internal Error") return } s := strings.SplitN(req.Form.Get("state"), ":", 2) if len(s) != 2 { logger.Printf("Error while parsing OAuth2 state: invalid length") p.ErrorPage(rw, 500, "Internal Error", "Invalid State") return } nonce := s[0] redirect := s[1] c, err := req.Cookie(p.CSRFCookieName) if err != nil { logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie") p.ErrorPage(rw, 403, "Permission Denied", err.Error()) return } p.ClearCSRFCookie(rw, req) if c.Value != nonce { logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack") p.ErrorPage(rw, 403, "Permission Denied", "csrf failed") return } if !p.IsValidRedirect(redirect) { redirect = "/" } // set cookie, or deny if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) { logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session) err := p.SaveSession(rw, req, session) if err != nil { logger.Printf("%s %s", remoteAddr, err) p.ErrorPage(rw, 500, "Internal Error", "Internal Error") return } http.Redirect(rw, req, redirect, 302) } else { logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized") p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account") } } // AuthenticateOnly checks whether the user is currently logged in func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) { session, err := p.getAuthenticatedSession(rw, req) if err != nil { http.Error(rw, "unauthorized request", http.StatusUnauthorized) return } // we are authenticated p.addHeadersForProxying(rw, req, session) rw.WriteHeader(http.StatusAccepted) } // Proxy proxies the user request if the user is authenticated else it prompts // them to authenticate func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { session, err := p.getAuthenticatedSession(rw, req) switch err { case nil: // we are authenticated p.addHeadersForProxying(rw, req, session) p.serveMux.ServeHTTP(rw, req) case ErrNeedsLogin: // we need to send the user to a login screen if isAjax(req) { // no point redirecting an AJAX request p.ErrorJSON(rw, http.StatusUnauthorized) return } if p.SkipProviderButton { p.OAuthStart(rw, req) } else { p.SignInPage(rw, req, http.StatusForbidden) } default: // unknown error logger.Printf("Unexpected internal error: %s", err) p.ErrorPage(rw, http.StatusInternalServerError, "Internal Error", "Internal Error") } } // getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so // Returns nil, ErrNeedsLogin if user needs to login. // Set-Cookie headers may be set on the response as a side-effect of calling this method. func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) { var session *sessionsapi.SessionState var err error var saveSession, clearSession, revalidated bool if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" { session, err = p.GetJwtSession(req) if err != nil { logger.Printf("Error retrieving session from token in Authorization header: %s", err) } if session != nil { saveSession = false } } remoteAddr := getRemoteAddr(req) if session == nil { session, err = p.LoadCookiedSession(req) if err != nil { logger.Printf("Error loading cookied session: %s", err) } if session != nil { if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) { logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh) saveSession = true } if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil { logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session) clearSession = true session = nil } else if ok { saveSession = true revalidated = true } } } if session != nil && session.IsExpired() { logger.Printf("Removing session: token expired %s", session) session = nil saveSession = false clearSession = true } if saveSession && !revalidated && session != nil && session.AccessToken != "" { if !p.provider.ValidateSessionState(session) { logger.Printf("Removing session: error validating %s", session) saveSession = false session = nil clearSession = true } } if session != nil && session.Email != "" { if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) { logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session) session = nil saveSession = false clearSession = true } } if saveSession && session != nil { err = p.SaveSession(rw, req, session) if err != nil { logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err) return nil, err } } if clearSession { p.ClearSessionCookie(rw, req) } if session == nil { session, err = p.CheckBasicAuth(req) if err != nil { logger.Printf("Error during basic auth validation: %s", err) } } if session == nil { return nil, ErrNeedsLogin } return session, nil } // addHeadersForProxying adds the appropriate headers the request / response for proxying func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) { if p.PassBasicAuth { req.SetBasicAuth(session.User, p.BasicAuthPassword) req.Header["X-Forwarded-User"] = []string{session.User} if session.Email != "" { req.Header["X-Forwarded-Email"] = []string{session.Email} } else { req.Header.Del("X-Forwarded-Email") } } if p.PassUserHeaders { req.Header["X-Forwarded-User"] = []string{session.User} if session.Email != "" { req.Header["X-Forwarded-Email"] = []string{session.Email} } else { req.Header.Del("X-Forwarded-Email") } } if p.SetXAuthRequest { rw.Header().Set("X-Auth-Request-User", session.User) if session.Email != "" { rw.Header().Set("X-Auth-Request-Email", session.Email) } else { rw.Header().Del("X-Auth-Request-Email") } if p.PassAccessToken { if session.AccessToken != "" { rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken) } else { rw.Header().Del("X-Auth-Request-Access-Token") } } } if p.PassAccessToken { if session.AccessToken != "" { req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken} } else { req.Header.Del("X-Forwarded-Access-Token") } } if p.PassAuthorization { if session.IDToken != "" { req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)} } else { req.Header.Del("Authorization") } } if p.SetAuthorization { if session.IDToken != "" { rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken)) } else { rw.Header().Del("Authorization") } } if session.Email == "" { rw.Header().Set("GAP-Auth", session.User) } else { rw.Header().Set("GAP-Auth", session.Email) } } // CheckBasicAuth checks the requests Authorization header for basic auth // credentials and authenticates these against the proxies HtpasswdFile func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) { if p.HtpasswdFile == nil { return nil, nil } auth := req.Header.Get("Authorization") if auth == "" { return nil, nil } s := strings.SplitN(auth, " ", 2) if len(s) != 2 || s[0] != "Basic" { return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization")) } b, err := b64.StdEncoding.DecodeString(s[1]) if err != nil { return nil, err } pair := strings.SplitN(string(b), ":", 2) if len(pair) != 2 { return nil, fmt.Errorf("invalid format %s", b) } if p.HtpasswdFile.Validate(pair[0], pair[1]) { logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File") return &sessionsapi.SessionState{User: pair[0]}, nil } logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File") return nil, nil } // isAjax checks if a request is an ajax request func isAjax(req *http.Request) bool { acceptValues, ok := req.Header["accept"] if !ok { acceptValues = req.Header["Accept"] } const ajaxReq = applicationJSON for _, v := range acceptValues { if v == ajaxReq { return true } } return false } // ErrorJSON returns the error code with an application/json mime type func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) { rw.Header().Set("Content-Type", applicationJSON) rw.WriteHeader(code) } // GetJwtSession loads a session based on a JWT token in the authorization header. func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) { rawBearerToken, err := p.findBearerToken(req) if err != nil { return nil, err } ctx := context.Background() var session *sessionsapi.SessionState for _, verifier := range p.jwtBearerVerifiers { bearerToken, err := verifier.Verify(ctx, rawBearerToken) if err != nil { logger.Printf("failed to verify bearer token: %v", err) continue } var claims struct { Subject string `json:"sub"` Email string `json:"email"` Verified *bool `json:"email_verified"` } if err := bearerToken.Claims(&claims); err != nil { return nil, fmt.Errorf("failed to parse bearer token claims: %v", err) } if claims.Email == "" { claims.Email = claims.Subject } if claims.Verified != nil && !*claims.Verified { return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email) } session = &sessionsapi.SessionState{ AccessToken: rawBearerToken, IDToken: rawBearerToken, RefreshToken: "", ExpiresOn: bearerToken.Expiry, Email: claims.Email, User: claims.Email, } return session, nil } return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization")) } // findBearerToken finds a valid JWT token from the Authorization header of a given request. func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) { auth := req.Header.Get("Authorization") s := strings.SplitN(auth, " ", 2) if len(s) != 2 { return "", fmt.Errorf("invalid authorization header %s", auth) } jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`) var rawBearerToken string if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) { rawBearerToken = s[1] } else if s[0] == "Basic" { // Check if we have a Bearer token masquerading in Basic b, err := b64.StdEncoding.DecodeString(s[1]) if err != nil { return "", err } pair := strings.SplitN(string(b), ":", 2) if len(pair) != 2 { return "", fmt.Errorf("invalid format %s", b) } user, password := pair[0], pair[1] // check user, user+password, or just password for a token if jwtRegex.MatchString(user) { // Support blank passwords or magic `x-oauth-basic` passwords - nothing else if password == "" || password == "x-oauth-basic" { rawBearerToken = user } } else if jwtRegex.MatchString(password) { // support passwords and ignore user rawBearerToken = password } } if rawBearerToken == "" { return "", fmt.Errorf("no valid bearer token found in authorization header") } return rawBearerToken, nil }