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/authentication/basic"
26 "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
27 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
28 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
29 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
30 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
31 "github.com/oauth2-proxy/oauth2-proxy/providers"
32 "github.com/yhat/wsutil"
36 // SignatureHeader is the name of the request header containing the GAP Signature
38 SignatureHeader = "GAP-Signature"
43 applicationJSON = "application/json"
46// SignatureHeaders contains the headers to be signed by the hmac algorithm
48var SignatureHeaders = []string{
56 "X-Forwarded-Preferred-User",
57 "X-Forwarded-Access-Token",
63 // ErrNeedsLogin means the user should be redirected to the login page
64 ErrNeedsLogin = errors.New("redirect to login page")
66 // Used to check final redirects are not susceptible to open redirects.
67 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
68 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
71// OAuthProxy is the main authentication proxy
72type OAuthProxy struct {
76 CookieDomains []string
80 CookieExpire time.Duration
81 CookieRefresh time.Duration
83 Validator func(string) bool
89 OAuthCallbackPath string
93 redirectURL *url.URL // the url to receive requests at
94 whitelistDomains []string
95 provider providers.Provider
96 providerNameOverride string
97 sessionStore sessionsapi.SessionStore
100 basicAuthValidator basic.Validator
101 displayHtpasswdForm bool
102 serveMux http.Handler
106 SkipProviderButton bool
108 BasicAuthPassword string
110 SetAuthorization bool
111 PassAuthorization bool
112 PreferEmailToUser bool
113 skipAuthRegex []string
114 skipAuthPreflight bool
115 skipAuthStripHeaders bool
116 skipJwtBearerTokens bool
117 mainJwtBearerVerifier *oidc.IDTokenVerifier
118 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
119 compiledRegex []*regexp.Regexp
120 templates *template.Template
121 realClientIPParser ipapi.RealClientIPParser
122 trustedIPs *ip.NetSet
127// UpstreamProxy represents an upstream server to proxy to
128type UpstreamProxy struct {
131 wsHandler http.Handler
132 auth hmacauth.HmacAuth
135// ServeHTTP proxies requests to the upstream provider while signing the
137func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
138 w.Header().Set("GAP-Upstream-Address", u.upstream)
140 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
141 u.auth.SignRequest(r)
143 if u.wsHandler != nil && strings.EqualFold(r.Header.Get("Connection"), "upgrade") && r.Header.Get("Upgrade") == "websocket" {
144 u.wsHandler.ServeHTTP(w, r)
146 u.handler.ServeHTTP(w, r)
151// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
153func NewReverseProxy(target *url.URL, opts *options.Options) (proxy *httputil.ReverseProxy) {
154 proxy = httputil.NewSingleHostReverseProxy(target)
155 proxy.FlushInterval = opts.FlushInterval
156 if opts.SSLUpstreamInsecureSkipVerify {
157 proxy.Transport = &http.Transport{
158 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
161 setProxyErrorHandler(proxy, opts)
165func setProxyErrorHandler(proxy *httputil.ReverseProxy, opts *options.Options) {
166 templates := loadTemplates(opts.CustomTemplatesDir)
167 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, proxyErr error) {
168 logger.Printf("Error proxying to upstream server: %v", proxyErr)
169 w.WriteHeader(http.StatusBadGateway)
175 Title: "Bad Gateway",
176 Message: "Error proxying to upstream server",
177 ProxyPrefix: opts.ProxyPrefix,
179 templates.ExecuteTemplate(w, "error.html", data)
183func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
184 director := proxy.Director
185 proxy.Director = func(req *http.Request) {
187 // use RequestURI so that we aren't unescaping encoded slashes in the request path
188 req.Host = target.Host
189 req.URL.Opaque = req.RequestURI
190 req.URL.RawQuery = ""
194func setProxyDirector(proxy *httputil.ReverseProxy) {
195 director := proxy.Director
196 proxy.Director = func(req *http.Request) {
198 // use RequestURI so that we aren't unescaping encoded slashes in the request path
199 req.URL.Opaque = req.RequestURI
200 req.URL.RawQuery = ""
204// NewFileServer creates a http.Handler to serve files from the filesystem
205func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
206 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
209// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
210func NewWebSocketOrRestReverseProxy(u *url.URL, opts *options.Options, auth hmacauth.HmacAuth) http.Handler {
212 proxy := NewReverseProxy(u, opts)
213 if !opts.PassHostHeader {
214 setProxyUpstreamHostHeader(proxy, u)
216 setProxyDirector(proxy)
219 // this should give us a wss:// scheme if the url is https:// based.
220 var wsProxy *wsutil.ReverseProxy
221 if opts.ProxyWebSockets {
222 wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
223 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
224 wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
225 if opts.SSLUpstreamInsecureSkipVerify {
226 wsProxy.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
229 return &UpstreamProxy{
237// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
238func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
239 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
241 return nil, fmt.Errorf("error initialising session store: %v", err)
244 serveMux := http.NewServeMux()
245 var auth hmacauth.HmacAuth
246 if sigData := opts.GetSignatureData(); sigData != nil {
247 auth = hmacauth.NewHmacAuth(sigData.Hash, []byte(sigData.Key),
248 SignatureHeader, SignatureHeaders)
250 for _, u := range opts.GetProxyURLs() {
254 case httpScheme, httpsScheme:
255 logger.Printf("mapping path %q => upstream %q", path, u)
256 proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
257 serveMux.Handle(path, proxy)
259 responseCode, err := strconv.Atoi(host)
261 logger.Printf("unable to convert %q to int, use default \"200\"", host)
265 serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
266 rw.WriteHeader(responseCode)
267 fmt.Fprintf(rw, "Authenticated")
270 if u.Fragment != "" {
273 logger.Printf("mapping path %q => file system %q", path, u.Path)
274 proxy := NewFileServer(path, u.Path)
275 uProxy := UpstreamProxy{
281 serveMux.Handle(path, &uProxy)
283 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
286 for _, u := range opts.GetCompiledRegex() {
287 logger.Printf("compiled skip-auth-regex => %q", u)
290 if opts.SkipJwtBearerTokens {
291 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
292 for _, issuer := range opts.ExtraJwtIssuers {
293 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
296 redirectURL := opts.GetRedirectURL()
297 if redirectURL.Path == "" {
298 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
301 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
302 refresh := "disabled"
303 if opts.Cookie.Refresh != time.Duration(0) {
304 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
307 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)
309 trustedIPs := ip.NewNetSet()
310 for _, ipStr := range opts.TrustedIPs {
311 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
312 trustedIPs.AddIPNet(*ipNet)
314 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
318 var basicAuthValidator basic.Validator
319 if opts.HtpasswdFile != "" {
320 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
322 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
324 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
329 CookieName: opts.Cookie.Name,
330 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
331 CookieSeed: opts.Cookie.Secret,
332 CookieDomains: opts.Cookie.Domains,
333 CookiePath: opts.Cookie.Path,
334 CookieSecure: opts.Cookie.Secure,
335 CookieHTTPOnly: opts.Cookie.HTTPOnly,
336 CookieExpire: opts.Cookie.Expire,
337 CookieRefresh: opts.Cookie.Refresh,
338 CookieSameSite: opts.Cookie.SameSite,
339 Validator: validator,
341 RobotsPath: "/robots.txt",
342 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
343 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
344 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
345 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
346 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
347 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
349 ProxyPrefix: opts.ProxyPrefix,
350 provider: opts.GetProvider(),
351 providerNameOverride: opts.ProviderName,
352 sessionStore: sessionStore,
354 redirectURL: redirectURL,
355 whitelistDomains: opts.WhitelistDomains,
356 skipAuthRegex: opts.SkipAuthRegex,
357 skipAuthPreflight: opts.SkipAuthPreflight,
358 skipAuthStripHeaders: opts.SkipAuthStripHeaders,
359 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
360 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
361 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
362 compiledRegex: opts.GetCompiledRegex(),
363 realClientIPParser: opts.GetRealClientIPParser(),
364 SetXAuthRequest: opts.SetXAuthRequest,
365 PassBasicAuth: opts.PassBasicAuth,
366 SetBasicAuth: opts.SetBasicAuth,
367 PassUserHeaders: opts.PassUserHeaders,
368 BasicAuthPassword: opts.BasicAuthPassword,
369 PassAccessToken: opts.PassAccessToken,
370 SetAuthorization: opts.SetAuthorization,
371 PassAuthorization: opts.PassAuthorization,
372 PreferEmailToUser: opts.PreferEmailToUser,
373 SkipProviderButton: opts.SkipProviderButton,
374 templates: loadTemplates(opts.CustomTemplatesDir),
375 trustedIPs: trustedIPs,
379 basicAuthValidator: basicAuthValidator,
380 displayHtpasswdForm: basicAuthValidator != nil,
384// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
385// redirect clients to once authenticated
386func (p *OAuthProxy) GetRedirectURI(host string) string {
387 // default to the request Host if not set
388 if p.redirectURL.Host != "" {
389 return p.redirectURL.String()
394 u.Scheme = httpsScheme
396 u.Scheme = httpScheme
403func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) {
405 return nil, errors.New("missing code")
407 redirectURI := p.GetRedirectURI(host)
408 s, err = p.provider.Redeem(ctx, redirectURI, code)
414 s.Email, err = p.provider.GetEmailAddress(ctx, s)
417 if s.PreferredUsername == "" {
418 s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s)
419 if err != nil && err.Error() == "not implemented" {
425 s.User, err = p.provider.GetUserName(ctx, s)
426 if err != nil && err.Error() == "not implemented" {
433// MakeCSRFCookie creates a cookie for CSRF
434func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
435 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
438func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
439 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
441 if cookieDomain != "" {
442 domain := cookies.GetRequestHost(req)
443 if h, _, err := net.SplitHostPort(domain); err == nil {
446 if !strings.HasSuffix(domain, cookieDomain) {
447 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
455 Domain: cookieDomain,
456 HttpOnly: p.CookieHTTPOnly,
457 Secure: p.CookieSecure,
458 Expires: now.Add(expiration),
459 SameSite: cookies.ParseSameSite(p.CookieSameSite),
463// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
465func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
466 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
469// SetCSRFCookie adds a CSRF cookie to the response
470func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
471 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
474// ClearSessionCookie creates a cookie to unset the user's authentication cookie
475// stored in the user's session
476func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
477 return p.sessionStore.Clear(rw, req)
480// LoadCookiedSession reads the user's authentication details from the request
481func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
482 return p.sessionStore.Load(req)
485// SaveSession creates a new session cookie value and sets this on the response
486func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
487 return p.sessionStore.Save(rw, req, s)
490// RobotsTxt disallows scraping pages from the OAuthProxy
491func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
492 rw.WriteHeader(http.StatusOK)
493 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
496// ErrorPage writes an error response
497func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
504 Title: fmt.Sprintf("%d %s", code, title),
506 ProxyPrefix: p.ProxyPrefix,
508 p.templates.ExecuteTemplate(rw, "error.html", t)
511// SignInPage writes the sing in template to the response
512func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
514 p.ClearSessionCookie(rw, req)
517 redirectURL, err := p.GetRedirect(req)
519 logger.Printf("Error obtaining redirect: %s", err.Error())
520 p.ErrorPage(rw, 500, "Internal Error", err.Error())
524 if redirectURL == p.SignInPath {
530 SignInMessage template.HTML
537 ProviderName: p.provider.Data().ProviderName,
538 SignInMessage: template.HTML(p.SignInMessage),
539 CustomLogin: p.displayHtpasswdForm,
540 Redirect: redirectURL,
542 ProxyPrefix: p.ProxyPrefix,
543 Footer: template.HTML(p.Footer),
545 if p.providerNameOverride != "" {
546 t.ProviderName = p.providerNameOverride
548 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
551// ManualSignIn handles basic auth logins to the proxy
552func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
553 if req.Method != "POST" || p.basicAuthValidator == nil {
556 user := req.FormValue("username")
557 passwd := req.FormValue("password")
562 if p.basicAuthValidator.Validate(user, passwd) {
563 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
566 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
570// GetRedirect reads the query parameter to get the URL to redirect clients to
571// once authenticated with the OAuthProxy
572func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
573 err = req.ParseForm()
578 redirect = req.Header.Get("X-Auth-Request-Redirect")
579 if req.Form.Get("rd") != "" {
580 redirect = req.Form.Get("rd")
582 if !p.IsValidRedirect(redirect) {
583 redirect = req.URL.Path
584 if strings.HasPrefix(redirect, p.ProxyPrefix) {
592// splitHostPort separates host and port. If the port is not valid, it returns
593// the entire input as host, and it doesn't check the validity of the host.
594// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
595// *** taken from net/url, modified validOptionalPort() to accept ":*"
596func splitHostPort(hostport string) (host, port string) {
599 colon := strings.LastIndexByte(host, ':')
600 if colon != -1 && validOptionalPort(host[colon:]) {
601 host, port = host[:colon], host[colon+1:]
604 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
605 host = host[1 : len(host)-1]
611// validOptionalPort reports whether port is either an empty string
612// or matches /^:\d*$/
613// *** taken from net/url, modified to accept ":*"
614func validOptionalPort(port string) bool {
615 if port == "" || port == ":*" {
621 for _, b := range port[1:] {
622 if b < '0' || b > '9' {
629// IsValidRedirect checks whether the redirect URL is whitelisted
630func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
633 // The user didn't specify a redirect, should fallback to `/`
635 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
637 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
638 redirectURL, err := url.Parse(redirect)
640 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
643 redirectHostname := redirectURL.Hostname()
645 for _, domain := range p.whitelistDomains {
646 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
647 if domainHostname == "" {
651 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
652 // the domain names match, now validate the ports
653 // if the whitelisted domain's port is '*', allow all ports
654 // if the whitelisted domain contains a specific port, only allow that port
655 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
656 redirectPort := redirectURL.Port()
657 if (domainPort == "*") ||
658 (domainPort == redirectPort) ||
659 (domainPort == "" && redirectPort == "") {
665 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
668 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
673// IsWhitelistedRequest is used to check if auth should be skipped for this request
674func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
675 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
676 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) || p.IsTrustedIP(req)
679// IsWhitelistedPath is used to check if the request path is allowed without auth
680func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
681 for _, u := range p.compiledRegex {
682 if u.MatchString(path) {
689// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
690var noCacheHeaders = map[string]string{
691 "Expires": time.Unix(0, 0).Format(time.RFC1123),
692 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
693 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
696// prepareNoCache prepares headers for preventing browser caching.
697func prepareNoCache(w http.ResponseWriter) {
698 // Set NoCache headers
699 for k, v := range noCacheHeaders {
704// IsTrustedIP is used to check if a request comes from a trusted client IP address.
705func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
706 if p.trustedIPs == nil {
710 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
712 logger.Printf("Error obtaining real IP for trusted IP list: %v", err)
713 // Possibly spoofed X-Real-IP header
717 if remoteAddr == nil {
721 return p.trustedIPs.Has(remoteAddr)
724func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
725 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
729 switch path := req.URL.Path; {
730 case path == p.RobotsPath:
732 case p.IsWhitelistedRequest(req):
733 p.SkipAuthProxy(rw, req)
734 case path == p.SignInPath:
736 case path == p.SignOutPath:
738 case path == p.OAuthStartPath:
739 p.OAuthStart(rw, req)
740 case path == p.OAuthCallbackPath:
741 p.OAuthCallback(rw, req)
742 case path == p.AuthOnlyPath:
743 p.AuthenticateOnly(rw, req)
744 case path == p.UserInfoPath:
751// SignIn serves a page prompting users to sign in
752func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
753 redirect, err := p.GetRedirect(req)
755 logger.Printf("Error obtaining redirect: %s", err.Error())
756 p.ErrorPage(rw, 500, "Internal Error", err.Error())
760 user, ok := p.ManualSignIn(rw, req)
762 session := &sessionsapi.SessionState{User: user}
763 p.SaveSession(rw, req, session)
764 http.Redirect(rw, req, redirect, http.StatusFound)
766 if p.SkipProviderButton {
767 p.OAuthStart(rw, req)
769 p.SignInPage(rw, req, http.StatusOK)
774//UserInfo endpoint outputs session email and preferred username in JSON format
775func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
777 session, err := p.getAuthenticatedSession(rw, req)
779 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
783 Email string `json:"email"`
784 PreferredUsername string `json:"preferredUsername,omitempty"`
786 Email: session.Email,
787 PreferredUsername: session.PreferredUsername,
789 rw.Header().Set("Content-Type", "application/json")
790 rw.WriteHeader(http.StatusOK)
791 json.NewEncoder(rw).Encode(userInfo)
794// SignOut sends a response to clear the authentication cookie
795func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
796 redirect, err := p.GetRedirect(req)
798 logger.Printf("Error obtaining redirect: %s", err.Error())
799 p.ErrorPage(rw, 500, "Internal Error", err.Error())
802 p.ClearSessionCookie(rw, req)
803 http.Redirect(rw, req, redirect, http.StatusFound)
806// OAuthStart starts the OAuth2 authentication flow
807func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
809 nonce, err := encryption.Nonce()
811 logger.Printf("Error obtaining nonce: %s", err.Error())
812 p.ErrorPage(rw, 500, "Internal Error", err.Error())
815 p.SetCSRFCookie(rw, req, nonce)
816 redirect, err := p.GetRedirect(req)
818 logger.Printf("Error obtaining redirect: %s", err.Error())
819 p.ErrorPage(rw, 500, "Internal Error", err.Error())
822 redirectURI := p.GetRedirectURI(req.Host)
823 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
826// OAuthCallback is the OAuth2 authentication flow callback that finishes the
827// OAuth2 authentication flow
828func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
829 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
831 // finish the oauth cycle
832 err := req.ParseForm()
834 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
835 p.ErrorPage(rw, 500, "Internal Error", err.Error())
838 errorString := req.Form.Get("error")
839 if errorString != "" {
840 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
841 p.ErrorPage(rw, 403, "Permission Denied", errorString)
845 session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code"))
847 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
848 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
852 s := strings.SplitN(req.Form.Get("state"), ":", 2)
854 logger.Printf("Error while parsing OAuth2 state: invalid length")
855 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
860 c, err := req.Cookie(p.CSRFCookieName)
862 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
863 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
866 p.ClearCSRFCookie(rw, req)
867 if c.Value != nonce {
868 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
869 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
873 if !p.IsValidRedirect(redirect) {
877 // set cookie, or deny
878 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
879 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
880 err := p.SaveSession(rw, req, session)
882 logger.Printf("%s %s", remoteAddr, err)
883 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
886 http.Redirect(rw, req, redirect, http.StatusFound)
888 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
889 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
893// AuthenticateOnly checks whether the user is currently logged in
894func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
895 session, err := p.getAuthenticatedSession(rw, req)
897 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
901 // we are authenticated
902 p.addHeadersForProxying(rw, req, session)
903 rw.WriteHeader(http.StatusAccepted)
906// SkipAuthProxy proxies whitelisted requests and skips authentication
907func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
908 if p.skipAuthStripHeaders {
909 p.stripAuthHeaders(req)
911 p.serveMux.ServeHTTP(rw, req)
914// Proxy proxies the user request if the user is authenticated else it prompts
915// them to authenticate
916func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
917 session, err := p.getAuthenticatedSession(rw, req)
920 // we are authenticated
921 p.addHeadersForProxying(rw, req, session)
922 p.serveMux.ServeHTTP(rw, req)
925 // we need to send the user to a login screen
927 // no point redirecting an AJAX request
928 p.ErrorJSON(rw, http.StatusUnauthorized)
932 if p.SkipProviderButton {
933 p.OAuthStart(rw, req)
935 p.SignInPage(rw, req, http.StatusForbidden)
940 logger.Printf("Unexpected internal error: %s", err)
941 p.ErrorPage(rw, http.StatusInternalServerError,
942 "Internal Error", "Internal Error")
947// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
948// Returns nil, ErrNeedsLogin if user needs to login.
949// Set-Cookie headers may be set on the response as a side-effect of calling this method.
950func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
951 var session *sessionsapi.SessionState
953 var saveSession, clearSession, revalidated bool
955 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
956 session, err = p.GetJwtSession(req)
958 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
965 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
967 session, err = p.LoadCookiedSession(req)
969 logger.Printf("Error loading cookied session: %s", err)
973 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
974 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
978 if ok, err := p.provider.RefreshSessionIfNeeded(req.Context(), session); err != nil {
979 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
989 if session != nil && session.IsExpired() {
990 logger.Printf("Removing session: token expired %s", session)
996 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
997 if !p.provider.ValidateSessionState(req.Context(), session) {
998 logger.Printf("Removing session: error validating %s", session)
1005 if session != nil && session.Email != "" && !p.Validator(session.Email) {
1006 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
1012 if saveSession && session != nil {
1013 err = p.SaveSession(rw, req, session)
1015 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
1021 p.ClearSessionCookie(rw, req)
1025 session, err = p.CheckBasicAuth(req)
1027 logger.Printf("Error during basic auth validation: %s", err)
1032 return nil, ErrNeedsLogin
1038// addHeadersForProxying adds the appropriate headers the request / response for proxying
1039func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1040 if p.PassBasicAuth {
1041 if p.PreferEmailToUser && session.Email != "" {
1042 req.SetBasicAuth(session.Email, p.BasicAuthPassword)
1043 req.Header["X-Forwarded-User"] = []string{session.Email}
1044 req.Header.Del("X-Forwarded-Email")
1046 req.SetBasicAuth(session.User, p.BasicAuthPassword)
1047 req.Header["X-Forwarded-User"] = []string{session.User}
1048 if session.Email != "" {
1049 req.Header["X-Forwarded-Email"] = []string{session.Email}
1051 req.Header.Del("X-Forwarded-Email")
1054 if session.PreferredUsername != "" {
1055 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
1057 req.Header.Del("X-Forwarded-Preferred-Username")
1061 if p.PassUserHeaders {
1062 if p.PreferEmailToUser && session.Email != "" {
1063 req.Header["X-Forwarded-User"] = []string{session.Email}
1064 req.Header.Del("X-Forwarded-Email")
1066 req.Header["X-Forwarded-User"] = []string{session.User}
1067 if session.Email != "" {
1068 req.Header["X-Forwarded-Email"] = []string{session.Email}
1070 req.Header.Del("X-Forwarded-Email")
1074 if session.PreferredUsername != "" {
1075 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
1077 req.Header.Del("X-Forwarded-Preferred-Username")
1081 if p.SetXAuthRequest {
1082 rw.Header().Set("X-Auth-Request-User", session.User)
1083 if session.Email != "" {
1084 rw.Header().Set("X-Auth-Request-Email", session.Email)
1086 rw.Header().Del("X-Auth-Request-Email")
1088 if session.PreferredUsername != "" {
1089 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
1091 rw.Header().Del("X-Auth-Request-Preferred-Username")
1094 if p.PassAccessToken {
1095 if session.AccessToken != "" {
1096 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
1098 rw.Header().Del("X-Auth-Request-Access-Token")
1103 if p.PassAccessToken {
1104 if session.AccessToken != "" {
1105 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
1107 req.Header.Del("X-Forwarded-Access-Token")
1111 if p.PassAuthorization {
1112 if session.IDToken != "" {
1113 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
1115 req.Header.Del("Authorization")
1120 case p.PreferEmailToUser && session.Email != "":
1121 authVal := b64.StdEncoding.EncodeToString([]byte(session.Email + ":" + p.BasicAuthPassword))
1122 rw.Header().Set("Authorization", "Basic "+authVal)
1123 case session.User != "":
1124 authVal := b64.StdEncoding.EncodeToString([]byte(session.User + ":" + p.BasicAuthPassword))
1125 rw.Header().Set("Authorization", "Basic "+authVal)
1127 rw.Header().Del("Authorization")
1130 if p.SetAuthorization {
1131 if session.IDToken != "" {
1132 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
1134 rw.Header().Del("Authorization")
1138 if session.Email == "" {
1139 rw.Header().Set("GAP-Auth", session.User)
1141 rw.Header().Set("GAP-Auth", session.Email)
1145// stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex
1146func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
1147 if p.PassBasicAuth {
1148 req.Header.Del("X-Forwarded-User")
1149 req.Header.Del("X-Forwarded-Email")
1150 req.Header.Del("X-Forwarded-Preferred-Username")
1151 req.Header.Del("Authorization")
1154 if p.PassUserHeaders {
1155 req.Header.Del("X-Forwarded-User")
1156 req.Header.Del("X-Forwarded-Email")
1157 req.Header.Del("X-Forwarded-Preferred-Username")
1160 if p.PassAccessToken {
1161 req.Header.Del("X-Forwarded-Access-Token")
1164 if p.PassAuthorization {
1165 req.Header.Del("Authorization")
1169// CheckBasicAuth checks the requests Authorization header for basic auth
1170// credentials and authenticates these against the proxies HtpasswdFile
1171func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
1172 if p.basicAuthValidator == nil {
1175 auth := req.Header.Get("Authorization")
1179 s := strings.SplitN(auth, " ", 2)
1180 if len(s) != 2 || s[0] != "Basic" {
1181 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1183 b, err := b64.StdEncoding.DecodeString(s[1])
1187 pair := strings.SplitN(string(b), ":", 2)
1189 return nil, fmt.Errorf("invalid format %s", b)
1191 if p.basicAuthValidator.Validate(pair[0], pair[1]) {
1192 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1193 return &sessionsapi.SessionState{User: pair[0]}, nil
1195 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1199// isAjax checks if a request is an ajax request
1200func isAjax(req *http.Request) bool {
1201 acceptValues := req.Header.Values("Accept")
1202 const ajaxReq = applicationJSON
1203 for _, v := range acceptValues {
1211// ErrorJSON returns the error code with an application/json mime type
1212func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1213 rw.Header().Set("Content-Type", applicationJSON)
1214 rw.WriteHeader(code)
1217// GetJwtSession loads a session based on a JWT token in the authorization header.
1218// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
1219func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1220 rawBearerToken, err := p.findBearerToken(req)
1225 // If we are using an oidc provider, go ahead and try that provider first with its Verifier
1226 // and Bearer Token -> Session converter
1227 if p.mainJwtBearerVerifier != nil {
1228 bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
1230 return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1234 // Otherwise, attempt to verify against the extra JWT issuers and use a more generic
1235 // Bearer Token -> Session converter
1236 for _, verifier := range p.extraJwtBearerVerifiers {
1237 bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1242 return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1244 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1247// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1248func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1249 auth := req.Header.Get("Authorization")
1250 s := strings.SplitN(auth, " ", 2)
1252 return "", fmt.Errorf("invalid authorization header %s", auth)
1254 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1255 var rawBearerToken string
1256 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1257 rawBearerToken = s[1]
1258 } else if s[0] == "Basic" {
1259 // Check if we have a Bearer token masquerading in Basic
1260 b, err := b64.StdEncoding.DecodeString(s[1])
1264 pair := strings.SplitN(string(b), ":", 2)
1266 return "", fmt.Errorf("invalid format %s", b)
1268 user, password := pair[0], pair[1]
1270 // check user, user+password, or just password for a token
1271 if jwtRegex.MatchString(user) {
1272 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1273 if password == "" || password == "x-oauth-basic" {
1274 rawBearerToken = user
1276 } else if jwtRegex.MatchString(password) {
1277 // support passwords and ignore user
1278 rawBearerToken = password
1281 if rawBearerToken == "" {
1282 return "", fmt.Errorf("no valid bearer token found in authorization header")
1285 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