git.iko.soy/typeable/oauth2-proxy
on request-rules help sign in
typeable/oauth2-proxy/oauthproxy.go
1286 lines 41 kB history raw
1package main
3import (
4 "context"
5 "crypto/tls"
6 b64 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "html/template"
11 "net"
12 "net/http"
13 "net/http/httputil"
14 "net/url"
15 "regexp"
16 "strconv"
17 "strings"
18 "time"
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"
35const (
36 // SignatureHeader is the name of the request header containing the GAP Signature
37 // Part of hmacauth
38 SignatureHeader = "GAP-Signature"
40 httpScheme = "http"
41 httpsScheme = "https"
43 applicationJSON = "application/json"
46// SignatureHeaders contains the headers to be signed by the hmac algorithm
47// Part of hmacauth
48var SignatureHeaders = []string{
49 "Content-Length",
50 "Content-Md5",
51 "Content-Type",
52 "Date",
53 "Authorization",
54 "X-Forwarded-User",
55 "X-Forwarded-Email",
56 "X-Forwarded-Preferred-User",
57 "X-Forwarded-Access-Token",
58 "Cookie",
59 "Gap-Auth",
62var (
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 {
73 CookieSeed string
74 CookieName string
75 CSRFCookieName string
76 CookieDomains []string
77 CookiePath string
78 CookieSecure bool
79 CookieHTTPOnly bool
80 CookieExpire time.Duration
81 CookieRefresh time.Duration
82 CookieSameSite string
83 Validator func(string) bool
85 RobotsPath string
86 SignInPath string
87 SignOutPath string
88 OAuthStartPath string
89 OAuthCallbackPath string
90 AuthOnlyPath string
91 UserInfoPath 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
98 ProxyPrefix string
99 SignInMessage string
100 basicAuthValidator basic.Validator
101 displayHtpasswdForm bool
102 serveMux http.Handler
103 SetXAuthRequest bool
104 PassBasicAuth bool
105 SetBasicAuth bool
106 SkipProviderButton bool
107 PassUserHeaders bool
108 BasicAuthPassword string
109 PassAccessToken bool
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
123 Banner string
124 Footer string
127// UpstreamProxy represents an upstream server to proxy to
128type UpstreamProxy struct {
129 upstream string
130 handler http.Handler
131 wsHandler http.Handler
132 auth hmacauth.HmacAuth
135// ServeHTTP proxies requests to the upstream provider while signing the
136// request headers
137func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
138 w.Header().Set("GAP-Upstream-Address", u.upstream)
139 if u.auth != nil {
140 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
141 u.auth.SignRequest(r)
142 }
143 if u.wsHandler != nil && strings.EqualFold(r.Header.Get("Connection"), "upgrade") && r.Header.Get("Upgrade") == "websocket" {
144 u.wsHandler.ServeHTTP(w, r)
145 } else {
146 u.handler.ServeHTTP(w, r)
147 }
151// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
152// servers
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},
159 }
160 }
161 setProxyErrorHandler(proxy, opts)
162 return proxy
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)
170 data := struct {
171 Title string
172 Message string
173 ProxyPrefix string
174 }{
175 Title: "Bad Gateway",
176 Message: "Error proxying to upstream server",
177 ProxyPrefix: opts.ProxyPrefix,
178 }
179 templates.ExecuteTemplate(w, "error.html", data)
180 }
183func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
184 director := proxy.Director
185 proxy.Director = func(req *http.Request) {
186 director(req)
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 = ""
191 }
194func setProxyDirector(proxy *httputil.ReverseProxy) {
195 director := proxy.Director
196 proxy.Director = func(req *http.Request) {
197 director(req)
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 = ""
201 }
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 {
211 u.Path = ""
212 proxy := NewReverseProxy(u, opts)
213 if !opts.PassHostHeader {
214 setProxyUpstreamHostHeader(proxy, u)
215 } else {
216 setProxyDirector(proxy)
217 }
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}
227 }
228 }
229 return &UpstreamProxy{
230 upstream: u.Host,
231 handler: proxy,
232 wsHandler: wsProxy,
233 auth: auth,
234 }
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)
240 if err != nil {
241 return nil, fmt.Errorf("error initialising session store: %v", err)
242 }
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)
249 }
250 for _, u := range opts.GetProxyURLs() {
251 path := u.Path
252 host := u.Host
253 switch u.Scheme {
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)
258 case "static":
259 responseCode, err := strconv.Atoi(host)
260 if err != nil {
261 logger.Printf("unable to convert %q to int, use default \"200\"", host)
262 responseCode = 200
263 }
265 serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
266 rw.WriteHeader(responseCode)
267 fmt.Fprintf(rw, "Authenticated")
268 })
269 case "file":
270 if u.Fragment != "" {
271 path = u.Fragment
272 }
273 logger.Printf("mapping path %q => file system %q", path, u.Path)
274 proxy := NewFileServer(path, u.Path)
275 uProxy := UpstreamProxy{
276 upstream: path,
277 handler: proxy,
278 wsHandler: nil,
279 auth: nil,
280 }
281 serveMux.Handle(path, &uProxy)
282 default:
283 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
284 }
285 }
286 for _, u := range opts.GetCompiledRegex() {
287 logger.Printf("compiled skip-auth-regex => %q", u)
288 }
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)
294 }
295 }
296 redirectURL := opts.GetRedirectURL()
297 if redirectURL.Path == "" {
298 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
299 }
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)
305 }
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)
313 } else {
314 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
315 }
316 }
318 var basicAuthValidator basic.Validator
319 if opts.HtpasswdFile != "" {
320 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
321 var err error
322 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
323 if err != nil {
324 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
325 }
326 }
328 return &OAuthProxy{
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,
353 serveMux: serveMux,
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,
376 Banner: opts.Banner,
377 Footer: opts.Footer,
379 basicAuthValidator: basicAuthValidator,
380 displayHtpasswdForm: basicAuthValidator != nil,
381 }, 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()
390 }
391 u := *p.redirectURL
392 if u.Scheme == "" {
393 if p.CookieSecure {
394 u.Scheme = httpsScheme
395 } else {
396 u.Scheme = httpScheme
397 }
398 }
399 u.Host = host
400 return u.String()
403func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) {
404 if code == "" {
405 return nil, errors.New("missing code")
406 }
407 redirectURI := p.GetRedirectURI(host)
408 s, err = p.provider.Redeem(ctx, redirectURI, code)
409 if err != nil {
410 return
411 }
413 if s.Email == "" {
414 s.Email, err = p.provider.GetEmailAddress(ctx, s)
415 }
417 if s.PreferredUsername == "" {
418 s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s)
419 if err != nil && err.Error() == "not implemented" {
420 err = nil
421 }
422 }
424 if s.User == "" {
425 s.User, err = p.provider.GetUserName(ctx, s)
426 if err != nil && err.Error() == "not implemented" {
427 err = nil
428 }
429 }
430 return
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 {
444 domain = h
445 }
446 if !strings.HasSuffix(domain, cookieDomain) {
447 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
448 }
449 }
451 return &http.Cookie{
452 Name: name,
453 Value: value,
454 Path: p.CookiePath,
455 Domain: cookieDomain,
456 HttpOnly: p.CookieHTTPOnly,
457 Secure: p.CookieSecure,
458 Expires: now.Add(expiration),
459 SameSite: cookies.ParseSameSite(p.CookieSameSite),
460 }
463// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
464// session
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) {
498 rw.WriteHeader(code)
499 t := struct {
500 Title string
501 Message string
502 ProxyPrefix string
503 }{
504 Title: fmt.Sprintf("%d %s", code, title),
505 Message: message,
506 ProxyPrefix: p.ProxyPrefix,
507 }
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) {
513 prepareNoCache(rw)
514 p.ClearSessionCookie(rw, req)
515 rw.WriteHeader(code)
517 redirectURL, err := p.GetRedirect(req)
518 if err != nil {
519 logger.Printf("Error obtaining redirect: %s", err.Error())
520 p.ErrorPage(rw, 500, "Internal Error", err.Error())
521 return
522 }
524 if redirectURL == p.SignInPath {
525 redirectURL = "/"
526 }
528 t := struct {
529 ProviderName string
530 SignInMessage template.HTML
531 CustomLogin bool
532 Redirect string
533 Version string
534 ProxyPrefix string
535 Footer template.HTML
536 }{
537 ProviderName: p.provider.Data().ProviderName,
538 SignInMessage: template.HTML(p.SignInMessage),
539 CustomLogin: p.displayHtpasswdForm,
540 Redirect: redirectURL,
541 Version: VERSION,
542 ProxyPrefix: p.ProxyPrefix,
543 Footer: template.HTML(p.Footer),
544 }
545 if p.providerNameOverride != "" {
546 t.ProviderName = p.providerNameOverride
547 }
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 {
554 return "", false
555 }
556 user := req.FormValue("username")
557 passwd := req.FormValue("password")
558 if user == "" {
559 return "", false
560 }
561 // check auth
562 if p.basicAuthValidator.Validate(user, passwd) {
563 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
564 return user, true
565 }
566 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
567 return "", false
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()
574 if err != nil {
575 return
576 }
578 redirect = req.Header.Get("X-Auth-Request-Redirect")
579 if req.Form.Get("rd") != "" {
580 redirect = req.Form.Get("rd")
581 }
582 if !p.IsValidRedirect(redirect) {
583 redirect = req.URL.Path
584 if strings.HasPrefix(redirect, p.ProxyPrefix) {
585 redirect = "/"
586 }
587 }
589 return
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) {
597 host = hostport
599 colon := strings.LastIndexByte(host, ':')
600 if colon != -1 && validOptionalPort(host[colon:]) {
601 host, port = host[:colon], host[colon+1:]
602 }
604 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
605 host = host[1 : len(host)-1]
606 }
608 return
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 == ":*" {
616 return true
617 }
618 if port[0] != ':' {
619 return false
620 }
621 for _, b := range port[1:] {
622 if b < '0' || b > '9' {
623 return false
624 }
625 }
626 return true
629// IsValidRedirect checks whether the redirect URL is whitelisted
630func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
631 switch {
632 case redirect == "":
633 // The user didn't specify a redirect, should fallback to `/`
634 return false
635 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
636 return true
637 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
638 redirectURL, err := url.Parse(redirect)
639 if err != nil {
640 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
641 return false
642 }
643 redirectHostname := redirectURL.Hostname()
645 for _, domain := range p.whitelistDomains {
646 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
647 if domainHostname == "" {
648 continue
649 }
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 == "") {
660 return true
661 }
662 }
663 }
665 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
666 return false
667 default:
668 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
669 return false
670 }
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) {
683 return true
684 }
685 }
686 return false
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 {
700 w.Header().Set(k, v)
701 }
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 {
707 return false
708 }
710 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
711 if err != nil {
712 logger.Printf("Error obtaining real IP for trusted IP list: %v", err)
713 // Possibly spoofed X-Real-IP header
714 return false
715 }
717 if remoteAddr == nil {
718 return false
719 }
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) {
726 prepareNoCache(rw)
727 }
729 switch path := req.URL.Path; {
730 case path == p.RobotsPath:
731 p.RobotsTxt(rw)
732 case p.IsWhitelistedRequest(req):
733 p.SkipAuthProxy(rw, req)
734 case path == p.SignInPath:
735 p.SignIn(rw, req)
736 case path == p.SignOutPath:
737 p.SignOut(rw, req)
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:
745 p.UserInfo(rw, req)
746 default:
747 p.Proxy(rw, req)
748 }
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)
754 if err != nil {
755 logger.Printf("Error obtaining redirect: %s", err.Error())
756 p.ErrorPage(rw, 500, "Internal Error", err.Error())
757 return
758 }
760 user, ok := p.ManualSignIn(rw, req)
761 if ok {
762 session := &sessionsapi.SessionState{User: user}
763 p.SaveSession(rw, req, session)
764 http.Redirect(rw, req, redirect, http.StatusFound)
765 } else {
766 if p.SkipProviderButton {
767 p.OAuthStart(rw, req)
768 } else {
769 p.SignInPage(rw, req, http.StatusOK)
770 }
771 }
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)
778 if err != nil {
779 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
780 return
781 }
782 userInfo := struct {
783 Email string `json:"email"`
784 PreferredUsername string `json:"preferredUsername,omitempty"`
785 }{
786 Email: session.Email,
787 PreferredUsername: session.PreferredUsername,
788 }
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)
797 if err != nil {
798 logger.Printf("Error obtaining redirect: %s", err.Error())
799 p.ErrorPage(rw, 500, "Internal Error", err.Error())
800 return
801 }
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) {
808 prepareNoCache(rw)
809 nonce, err := encryption.Nonce()
810 if err != nil {
811 logger.Printf("Error obtaining nonce: %s", err.Error())
812 p.ErrorPage(rw, 500, "Internal Error", err.Error())
813 return
814 }
815 p.SetCSRFCookie(rw, req, nonce)
816 redirect, err := p.GetRedirect(req)
817 if err != nil {
818 logger.Printf("Error obtaining redirect: %s", err.Error())
819 p.ErrorPage(rw, 500, "Internal Error", err.Error())
820 return
821 }
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()
833 if err != nil {
834 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
835 p.ErrorPage(rw, 500, "Internal Error", err.Error())
836 return
837 }
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)
842 return
843 }
845 session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code"))
846 if err != nil {
847 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
848 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
849 return
850 }
852 s := strings.SplitN(req.Form.Get("state"), ":", 2)
853 if len(s) != 2 {
854 logger.Printf("Error while parsing OAuth2 state: invalid length")
855 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
856 return
857 }
858 nonce := s[0]
859 redirect := s[1]
860 c, err := req.Cookie(p.CSRFCookieName)
861 if err != nil {
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())
864 return
865 }
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")
870 return
871 }
873 if !p.IsValidRedirect(redirect) {
874 redirect = "/"
875 }
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)
881 if err != nil {
882 logger.Printf("%s %s", remoteAddr, err)
883 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
884 return
885 }
886 http.Redirect(rw, req, redirect, http.StatusFound)
887 } else {
888 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
889 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
890 }
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)
896 if err != nil {
897 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
898 return
899 }
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)
910 }
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)
918 switch err {
919 case nil:
920 // we are authenticated
921 p.addHeadersForProxying(rw, req, session)
922 p.serveMux.ServeHTTP(rw, req)
924 case ErrNeedsLogin:
925 // we need to send the user to a login screen
926 if isAjax(req) {
927 // no point redirecting an AJAX request
928 p.ErrorJSON(rw, http.StatusUnauthorized)
929 return
930 }
932 if p.SkipProviderButton {
933 p.OAuthStart(rw, req)
934 } else {
935 p.SignInPage(rw, req, http.StatusForbidden)
936 }
938 default:
939 // unknown error
940 logger.Printf("Unexpected internal error: %s", err)
941 p.ErrorPage(rw, http.StatusInternalServerError,
942 "Internal Error", "Internal Error")
943 }
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
952 var err error
953 var saveSession, clearSession, revalidated bool
955 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
956 session, err = p.GetJwtSession(req)
957 if err != nil {
958 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
959 }
960 if session != nil {
961 saveSession = false
962 }
963 }
965 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
966 if session == nil {
967 session, err = p.LoadCookiedSession(req)
968 if err != nil {
969 logger.Printf("Error loading cookied session: %s", err)
970 }
972 if session != nil {
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)
975 saveSession = true
976 }
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)
980 clearSession = true
981 session = nil
982 } else if ok {
983 saveSession = true
984 revalidated = true
985 }
986 }
987 }
989 if session != nil && session.IsExpired() {
990 logger.Printf("Removing session: token expired %s", session)
991 session = nil
992 saveSession = false
993 clearSession = true
994 }
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)
999 saveSession = false
1000 session = nil
1001 clearSession = true
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)
1007 session = nil
1008 saveSession = false
1009 clearSession = true
1012 if saveSession && session != nil {
1013 err = p.SaveSession(rw, req, session)
1014 if err != nil {
1015 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
1016 return nil, err
1020 if clearSession {
1021 p.ClearSessionCookie(rw, req)
1024 if session == nil {
1025 session, err = p.CheckBasicAuth(req)
1026 if err != nil {
1027 logger.Printf("Error during basic auth validation: %s", err)
1031 if session == nil {
1032 return nil, ErrNeedsLogin
1035 return session, nil
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")
1045 } else {
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}
1050 } else {
1051 req.Header.Del("X-Forwarded-Email")
1054 if session.PreferredUsername != "" {
1055 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
1056 } else {
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")
1065 } else {
1066 req.Header["X-Forwarded-User"] = []string{session.User}
1067 if session.Email != "" {
1068 req.Header["X-Forwarded-Email"] = []string{session.Email}
1069 } else {
1070 req.Header.Del("X-Forwarded-Email")
1074 if session.PreferredUsername != "" {
1075 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
1076 } else {
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)
1085 } else {
1086 rw.Header().Del("X-Auth-Request-Email")
1088 if session.PreferredUsername != "" {
1089 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
1090 } else {
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)
1097 } else {
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}
1106 } else {
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)}
1114 } else {
1115 req.Header.Del("Authorization")
1118 if p.SetBasicAuth {
1119 switch {
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)
1126 default:
1127 rw.Header().Del("Authorization")
1130 if p.SetAuthorization {
1131 if session.IDToken != "" {
1132 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
1133 } else {
1134 rw.Header().Del("Authorization")
1138 if session.Email == "" {
1139 rw.Header().Set("GAP-Auth", session.User)
1140 } else {
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 {
1173 return nil, nil
1175 auth := req.Header.Get("Authorization")
1176 if auth == "" {
1177 return nil, nil
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])
1184 if err != nil {
1185 return nil, err
1187 pair := strings.SplitN(string(b), ":", 2)
1188 if len(pair) != 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")
1196 return nil, nil
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 {
1204 if v == ajaxReq {
1205 return true
1208 return false
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)
1221 if err != nil {
1222 return nil, err
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)
1229 if err == nil {
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)
1238 if err != nil {
1239 continue
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)
1251 if len(s) != 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])
1261 if err != nil {
1262 return "", err
1264 pair := strings.SplitN(string(b), ":", 2)
1265 if len(pair) != 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