git.iko.soy/typeable/oauth2-proxy
on v6.0.0 help sign in
typeable/oauth2-proxy/oauthproxy.go
1205 lines 38 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/cookies"
26 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
27 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
28 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
29 "github.com/oauth2-proxy/oauth2-proxy/providers"
30 "github.com/yhat/wsutil"
33const (
34 // SignatureHeader is the name of the request header containing the GAP Signature
35 // Part of hmacauth
36 SignatureHeader = "GAP-Signature"
38 httpScheme = "http"
39 httpsScheme = "https"
41 applicationJSON = "application/json"
44// SignatureHeaders contains the headers to be signed by the hmac algorithm
45// Part of hmacauth
46var SignatureHeaders = []string{
47 "Content-Length",
48 "Content-Md5",
49 "Content-Type",
50 "Date",
51 "Authorization",
52 "X-Forwarded-User",
53 "X-Forwarded-Email",
54 "X-Forwarded-Preferred-User",
55 "X-Forwarded-Access-Token",
56 "Cookie",
57 "Gap-Auth",
60var (
61 // ErrNeedsLogin means the user should be redirected to the login page
62 ErrNeedsLogin = errors.New("redirect to login page")
64 // Used to check final redirects are not susceptible to open redirects.
65 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
66 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
69// OAuthProxy is the main authentication proxy
70type OAuthProxy struct {
71 CookieSeed string
72 CookieName string
73 CSRFCookieName string
74 CookieDomains []string
75 CookiePath string
76 CookieSecure bool
77 CookieHTTPOnly bool
78 CookieExpire time.Duration
79 CookieRefresh time.Duration
80 CookieSameSite string
81 Validator func(string) bool
83 RobotsPath string
84 SignInPath string
85 SignOutPath string
86 OAuthStartPath string
87 OAuthCallbackPath string
88 AuthOnlyPath string
89 UserInfoPath string
91 redirectURL *url.URL // the url to receive requests at
92 whitelistDomains []string
93 provider providers.Provider
94 providerNameOverride string
95 sessionStore sessionsapi.SessionStore
96 ProxyPrefix string
97 SignInMessage string
98 HtpasswdFile *HtpasswdFile
99 DisplayHtpasswdForm bool
100 serveMux http.Handler
101 SetXAuthRequest bool
102 PassBasicAuth bool
103 SetBasicAuth bool
104 SkipProviderButton bool
105 PassUserHeaders bool
106 BasicAuthPassword string
107 PassAccessToken bool
108 SetAuthorization bool
109 PassAuthorization bool
110 PreferEmailToUser bool
111 skipAuthRegex []string
112 skipAuthPreflight bool
113 skipJwtBearerTokens bool
114 mainJwtBearerVerifier *oidc.IDTokenVerifier
115 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
116 compiledRegex []*regexp.Regexp
117 templates *template.Template
118 realClientIPParser ipapi.RealClientIPParser
119 Banner string
120 Footer string
123// UpstreamProxy represents an upstream server to proxy to
124type UpstreamProxy struct {
125 upstream string
126 handler http.Handler
127 wsHandler http.Handler
128 auth hmacauth.HmacAuth
131// ServeHTTP proxies requests to the upstream provider while signing the
132// request headers
133func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
134 w.Header().Set("GAP-Upstream-Address", u.upstream)
135 if u.auth != nil {
136 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
137 u.auth.SignRequest(r)
138 }
139 if u.wsHandler != nil && strings.EqualFold(r.Header.Get("Connection"), "upgrade") && r.Header.Get("Upgrade") == "websocket" {
140 u.wsHandler.ServeHTTP(w, r)
141 } else {
142 u.handler.ServeHTTP(w, r)
143 }
147// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
148// servers
149func NewReverseProxy(target *url.URL, opts *options.Options) (proxy *httputil.ReverseProxy) {
150 proxy = httputil.NewSingleHostReverseProxy(target)
151 proxy.FlushInterval = opts.FlushInterval
152 if opts.SSLUpstreamInsecureSkipVerify {
153 proxy.Transport = &http.Transport{
154 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
155 }
156 }
157 setProxyErrorHandler(proxy, opts)
158 return proxy
161func setProxyErrorHandler(proxy *httputil.ReverseProxy, opts *options.Options) {
162 templates := loadTemplates(opts.CustomTemplatesDir)
163 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, proxyErr error) {
164 logger.Printf("Error proxying to upstream server: %v", proxyErr)
165 w.WriteHeader(http.StatusBadGateway)
166 data := struct {
167 Title string
168 Message string
169 ProxyPrefix string
170 }{
171 Title: "Bad Gateway",
172 Message: "Error proxying to upstream server",
173 ProxyPrefix: opts.ProxyPrefix,
174 }
175 templates.ExecuteTemplate(w, "error.html", data)
176 }
179func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
180 director := proxy.Director
181 proxy.Director = func(req *http.Request) {
182 director(req)
183 // use RequestURI so that we aren't unescaping encoded slashes in the request path
184 req.Host = target.Host
185 req.URL.Opaque = req.RequestURI
186 req.URL.RawQuery = ""
187 }
190func setProxyDirector(proxy *httputil.ReverseProxy) {
191 director := proxy.Director
192 proxy.Director = func(req *http.Request) {
193 director(req)
194 // use RequestURI so that we aren't unescaping encoded slashes in the request path
195 req.URL.Opaque = req.RequestURI
196 req.URL.RawQuery = ""
197 }
200// NewFileServer creates a http.Handler to serve files from the filesystem
201func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
202 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
205// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
206func NewWebSocketOrRestReverseProxy(u *url.URL, opts *options.Options, auth hmacauth.HmacAuth) http.Handler {
207 u.Path = ""
208 proxy := NewReverseProxy(u, opts)
209 if !opts.PassHostHeader {
210 setProxyUpstreamHostHeader(proxy, u)
211 } else {
212 setProxyDirector(proxy)
213 }
215 // this should give us a wss:// scheme if the url is https:// based.
216 var wsProxy *wsutil.ReverseProxy
217 if opts.ProxyWebSockets {
218 wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
219 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
220 wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
221 if opts.SSLUpstreamInsecureSkipVerify {
222 wsProxy.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
223 }
224 }
225 return &UpstreamProxy{
226 upstream: u.Host,
227 handler: proxy,
228 wsHandler: wsProxy,
229 auth: auth,
230 }
233// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
234func NewOAuthProxy(opts *options.Options, validator func(string) bool) *OAuthProxy {
235 serveMux := http.NewServeMux()
236 var auth hmacauth.HmacAuth
237 if sigData := opts.GetSignatureData(); sigData != nil {
238 auth = hmacauth.NewHmacAuth(sigData.Hash, []byte(sigData.Key),
239 SignatureHeader, SignatureHeaders)
240 }
241 for _, u := range opts.GetProxyURLs() {
242 path := u.Path
243 host := u.Host
244 switch u.Scheme {
245 case httpScheme, httpsScheme:
246 logger.Printf("mapping path %q => upstream %q", path, u)
247 proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
248 serveMux.Handle(path, proxy)
249 case "static":
250 responseCode, err := strconv.Atoi(host)
251 if err != nil {
252 logger.Printf("unable to convert %q to int, use default \"200\"", host)
253 responseCode = 200
254 }
256 serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
257 rw.WriteHeader(responseCode)
258 fmt.Fprintf(rw, "Authenticated")
259 })
260 case "file":
261 if u.Fragment != "" {
262 path = u.Fragment
263 }
264 logger.Printf("mapping path %q => file system %q", path, u.Path)
265 proxy := NewFileServer(path, u.Path)
266 uProxy := UpstreamProxy{
267 upstream: path,
268 handler: proxy,
269 wsHandler: nil,
270 auth: nil,
271 }
272 serveMux.Handle(path, &uProxy)
273 default:
274 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
275 }
276 }
277 for _, u := range opts.GetCompiledRegex() {
278 logger.Printf("compiled skip-auth-regex => %q", u)
279 }
281 if opts.SkipJwtBearerTokens {
282 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
283 for _, issuer := range opts.ExtraJwtIssuers {
284 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
285 }
286 }
287 redirectURL := opts.GetRedirectURL()
288 if redirectURL.Path == "" {
289 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
290 }
292 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
293 refresh := "disabled"
294 if opts.Cookie.Refresh != time.Duration(0) {
295 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
296 }
298 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh)
300 return &OAuthProxy{
301 CookieName: opts.Cookie.Name,
302 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
303 CookieSeed: opts.Cookie.Secret,
304 CookieDomains: opts.Cookie.Domains,
305 CookiePath: opts.Cookie.Path,
306 CookieSecure: opts.Cookie.Secure,
307 CookieHTTPOnly: opts.Cookie.HTTPOnly,
308 CookieExpire: opts.Cookie.Expire,
309 CookieRefresh: opts.Cookie.Refresh,
310 CookieSameSite: opts.Cookie.SameSite,
311 Validator: validator,
313 RobotsPath: "/robots.txt",
314 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
315 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
316 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
317 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
318 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
319 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
321 ProxyPrefix: opts.ProxyPrefix,
322 provider: opts.GetProvider(),
323 providerNameOverride: opts.ProviderName,
324 sessionStore: opts.GetSessionStore(),
325 serveMux: serveMux,
326 redirectURL: redirectURL,
327 whitelistDomains: opts.WhitelistDomains,
328 skipAuthRegex: opts.SkipAuthRegex,
329 skipAuthPreflight: opts.SkipAuthPreflight,
330 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
331 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
332 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
333 compiledRegex: opts.GetCompiledRegex(),
334 realClientIPParser: opts.GetRealClientIPParser(),
335 SetXAuthRequest: opts.SetXAuthRequest,
336 PassBasicAuth: opts.PassBasicAuth,
337 SetBasicAuth: opts.SetBasicAuth,
338 PassUserHeaders: opts.PassUserHeaders,
339 BasicAuthPassword: opts.BasicAuthPassword,
340 PassAccessToken: opts.PassAccessToken,
341 SetAuthorization: opts.SetAuthorization,
342 PassAuthorization: opts.PassAuthorization,
343 PreferEmailToUser: opts.PreferEmailToUser,
344 SkipProviderButton: opts.SkipProviderButton,
345 templates: loadTemplates(opts.CustomTemplatesDir),
346 Banner: opts.Banner,
347 Footer: opts.Footer,
348 }
351// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
352// redirect clients to once authenticated
353func (p *OAuthProxy) GetRedirectURI(host string) string {
354 // default to the request Host if not set
355 if p.redirectURL.Host != "" {
356 return p.redirectURL.String()
357 }
358 u := *p.redirectURL
359 if u.Scheme == "" {
360 if p.CookieSecure {
361 u.Scheme = httpsScheme
362 } else {
363 u.Scheme = httpScheme
364 }
365 }
366 u.Host = host
367 return u.String()
370func (p *OAuthProxy) displayCustomLoginForm() bool {
371 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
374func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) {
375 if code == "" {
376 return nil, errors.New("missing code")
377 }
378 redirectURI := p.GetRedirectURI(host)
379 s, err = p.provider.Redeem(ctx, redirectURI, code)
380 if err != nil {
381 return
382 }
384 if s.Email == "" {
385 s.Email, err = p.provider.GetEmailAddress(ctx, s)
386 }
388 if s.PreferredUsername == "" {
389 s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s)
390 if err != nil && err.Error() == "not implemented" {
391 err = nil
392 }
393 }
395 if s.User == "" {
396 s.User, err = p.provider.GetUserName(ctx, s)
397 if err != nil && err.Error() == "not implemented" {
398 err = nil
399 }
400 }
401 return
404// MakeCSRFCookie creates a cookie for CSRF
405func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
406 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
409func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
410 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
412 if cookieDomain != "" {
413 domain := cookies.GetRequestHost(req)
414 if h, _, err := net.SplitHostPort(domain); err == nil {
415 domain = h
416 }
417 if !strings.HasSuffix(domain, cookieDomain) {
418 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
419 }
420 }
422 return &http.Cookie{
423 Name: name,
424 Value: value,
425 Path: p.CookiePath,
426 Domain: cookieDomain,
427 HttpOnly: p.CookieHTTPOnly,
428 Secure: p.CookieSecure,
429 Expires: now.Add(expiration),
430 SameSite: cookies.ParseSameSite(p.CookieSameSite),
431 }
434// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
435// session
436func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
437 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
440// SetCSRFCookie adds a CSRF cookie to the response
441func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
442 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
445// ClearSessionCookie creates a cookie to unset the user's authentication cookie
446// stored in the user's session
447func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
448 return p.sessionStore.Clear(rw, req)
451// LoadCookiedSession reads the user's authentication details from the request
452func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
453 return p.sessionStore.Load(req)
456// SaveSession creates a new session cookie value and sets this on the response
457func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
458 return p.sessionStore.Save(rw, req, s)
461// RobotsTxt disallows scraping pages from the OAuthProxy
462func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
463 rw.WriteHeader(http.StatusOK)
464 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
467// ErrorPage writes an error response
468func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
469 rw.WriteHeader(code)
470 t := struct {
471 Title string
472 Message string
473 ProxyPrefix string
474 }{
475 Title: fmt.Sprintf("%d %s", code, title),
476 Message: message,
477 ProxyPrefix: p.ProxyPrefix,
478 }
479 p.templates.ExecuteTemplate(rw, "error.html", t)
482// SignInPage writes the sing in template to the response
483func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
484 prepareNoCache(rw)
485 p.ClearSessionCookie(rw, req)
486 rw.WriteHeader(code)
488 redirectURL, err := p.GetRedirect(req)
489 if err != nil {
490 logger.Printf("Error obtaining redirect: %s", err.Error())
491 p.ErrorPage(rw, 500, "Internal Error", err.Error())
492 return
493 }
495 if redirectURL == p.SignInPath {
496 redirectURL = "/"
497 }
499 t := struct {
500 ProviderName string
501 SignInMessage template.HTML
502 CustomLogin bool
503 Redirect string
504 Version string
505 ProxyPrefix string
506 Footer template.HTML
507 }{
508 ProviderName: p.provider.Data().ProviderName,
509 SignInMessage: template.HTML(p.SignInMessage),
510 CustomLogin: p.displayCustomLoginForm(),
511 Redirect: redirectURL,
512 Version: VERSION,
513 ProxyPrefix: p.ProxyPrefix,
514 Footer: template.HTML(p.Footer),
515 }
516 if p.providerNameOverride != "" {
517 t.ProviderName = p.providerNameOverride
518 }
519 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
522// ManualSignIn handles basic auth logins to the proxy
523func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
524 if req.Method != "POST" || p.HtpasswdFile == nil {
525 return "", false
526 }
527 user := req.FormValue("username")
528 passwd := req.FormValue("password")
529 if user == "" {
530 return "", false
531 }
532 // check auth
533 if p.HtpasswdFile.Validate(user, passwd) {
534 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
535 return user, true
536 }
537 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
538 return "", false
541// GetRedirect reads the query parameter to get the URL to redirect clients to
542// once authenticated with the OAuthProxy
543func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
544 err = req.ParseForm()
545 if err != nil {
546 return
547 }
549 redirect = req.Header.Get("X-Auth-Request-Redirect")
550 if req.Form.Get("rd") != "" {
551 redirect = req.Form.Get("rd")
552 }
553 if !p.IsValidRedirect(redirect) {
554 redirect = req.URL.Path
555 if strings.HasPrefix(redirect, p.ProxyPrefix) {
556 redirect = "/"
557 }
558 }
560 return
563// splitHostPort separates host and port. If the port is not valid, it returns
564// the entire input as host, and it doesn't check the validity of the host.
565// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
566// *** taken from net/url, modified validOptionalPort() to accept ":*"
567func splitHostPort(hostport string) (host, port string) {
568 host = hostport
570 colon := strings.LastIndexByte(host, ':')
571 if colon != -1 && validOptionalPort(host[colon:]) {
572 host, port = host[:colon], host[colon+1:]
573 }
575 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
576 host = host[1 : len(host)-1]
577 }
579 return
582// validOptionalPort reports whether port is either an empty string
583// or matches /^:\d*$/
584// *** taken from net/url, modified to accept ":*"
585func validOptionalPort(port string) bool {
586 if port == "" || port == ":*" {
587 return true
588 }
589 if port[0] != ':' {
590 return false
591 }
592 for _, b := range port[1:] {
593 if b < '0' || b > '9' {
594 return false
595 }
596 }
597 return true
600// IsValidRedirect checks whether the redirect URL is whitelisted
601func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
602 switch {
603 case redirect == "":
604 // The user didn't specify a redirect, should fallback to `/`
605 return false
606 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
607 return true
608 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
609 redirectURL, err := url.Parse(redirect)
610 if err != nil {
611 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
612 return false
613 }
614 redirectHostname := redirectURL.Hostname()
616 for _, domain := range p.whitelistDomains {
617 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
618 if domainHostname == "" {
619 continue
620 }
622 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
623 // the domain names match, now validate the ports
624 // if the whitelisted domain's port is '*', allow all ports
625 // if the whitelisted domain contains a specific port, only allow that port
626 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
627 redirectPort := redirectURL.Port()
628 if (domainPort == "*") ||
629 (domainPort == redirectPort) ||
630 (domainPort == "" && redirectPort == "") {
631 return true
632 }
633 }
634 }
636 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
637 return false
638 default:
639 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
640 return false
641 }
644// IsWhitelistedRequest is used to check if auth should be skipped for this request
645func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
646 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
647 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
650// IsWhitelistedPath is used to check if the request path is allowed without auth
651func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
652 for _, u := range p.compiledRegex {
653 if u.MatchString(path) {
654 return true
655 }
656 }
657 return false
660// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
661var noCacheHeaders = map[string]string{
662 "Expires": time.Unix(0, 0).Format(time.RFC1123),
663 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
664 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
667// prepareNoCache prepares headers for preventing browser caching.
668func prepareNoCache(w http.ResponseWriter) {
669 // Set NoCache headers
670 for k, v := range noCacheHeaders {
671 w.Header().Set(k, v)
672 }
675func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
676 if strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
677 prepareNoCache(rw)
678 }
680 switch path := req.URL.Path; {
681 case path == p.RobotsPath:
682 p.RobotsTxt(rw)
683 case p.IsWhitelistedRequest(req):
684 p.serveMux.ServeHTTP(rw, req)
685 case path == p.SignInPath:
686 p.SignIn(rw, req)
687 case path == p.SignOutPath:
688 p.SignOut(rw, req)
689 case path == p.OAuthStartPath:
690 p.OAuthStart(rw, req)
691 case path == p.OAuthCallbackPath:
692 p.OAuthCallback(rw, req)
693 case path == p.AuthOnlyPath:
694 p.AuthenticateOnly(rw, req)
695 case path == p.UserInfoPath:
696 p.UserInfo(rw, req)
697 default:
698 p.Proxy(rw, req)
699 }
702// SignIn serves a page prompting users to sign in
703func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
704 redirect, err := p.GetRedirect(req)
705 if err != nil {
706 logger.Printf("Error obtaining redirect: %s", err.Error())
707 p.ErrorPage(rw, 500, "Internal Error", err.Error())
708 return
709 }
711 user, ok := p.ManualSignIn(rw, req)
712 if ok {
713 session := &sessionsapi.SessionState{User: user}
714 p.SaveSession(rw, req, session)
715 http.Redirect(rw, req, redirect, http.StatusFound)
716 } else {
717 if p.SkipProviderButton {
718 p.OAuthStart(rw, req)
719 } else {
720 p.SignInPage(rw, req, http.StatusOK)
721 }
722 }
725//UserInfo endpoint outputs session email and preferred username in JSON format
726func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
728 session, err := p.getAuthenticatedSession(rw, req)
729 if err != nil {
730 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
731 return
732 }
733 userInfo := struct {
734 Email string `json:"email"`
735 PreferredUsername string `json:"preferredUsername,omitempty"`
736 }{
737 Email: session.Email,
738 PreferredUsername: session.PreferredUsername,
739 }
740 rw.Header().Set("Content-Type", "application/json")
741 rw.WriteHeader(http.StatusOK)
742 json.NewEncoder(rw).Encode(userInfo)
745// SignOut sends a response to clear the authentication cookie
746func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
747 redirect, err := p.GetRedirect(req)
748 if err != nil {
749 logger.Printf("Error obtaining redirect: %s", err.Error())
750 p.ErrorPage(rw, 500, "Internal Error", err.Error())
751 return
752 }
753 p.ClearSessionCookie(rw, req)
754 http.Redirect(rw, req, redirect, http.StatusFound)
757// OAuthStart starts the OAuth2 authentication flow
758func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
759 prepareNoCache(rw)
760 nonce, err := encryption.Nonce()
761 if err != nil {
762 logger.Printf("Error obtaining nonce: %s", err.Error())
763 p.ErrorPage(rw, 500, "Internal Error", err.Error())
764 return
765 }
766 p.SetCSRFCookie(rw, req, nonce)
767 redirect, err := p.GetRedirect(req)
768 if err != nil {
769 logger.Printf("Error obtaining redirect: %s", err.Error())
770 p.ErrorPage(rw, 500, "Internal Error", err.Error())
771 return
772 }
773 redirectURI := p.GetRedirectURI(req.Host)
774 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
777// OAuthCallback is the OAuth2 authentication flow callback that finishes the
778// OAuth2 authentication flow
779func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
780 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
782 // finish the oauth cycle
783 err := req.ParseForm()
784 if err != nil {
785 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
786 p.ErrorPage(rw, 500, "Internal Error", err.Error())
787 return
788 }
789 errorString := req.Form.Get("error")
790 if errorString != "" {
791 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
792 p.ErrorPage(rw, 403, "Permission Denied", errorString)
793 return
794 }
796 session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code"))
797 if err != nil {
798 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
799 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
800 return
801 }
803 s := strings.SplitN(req.Form.Get("state"), ":", 2)
804 if len(s) != 2 {
805 logger.Printf("Error while parsing OAuth2 state: invalid length")
806 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
807 return
808 }
809 nonce := s[0]
810 redirect := s[1]
811 c, err := req.Cookie(p.CSRFCookieName)
812 if err != nil {
813 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
814 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
815 return
816 }
817 p.ClearCSRFCookie(rw, req)
818 if c.Value != nonce {
819 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
820 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
821 return
822 }
824 if !p.IsValidRedirect(redirect) {
825 redirect = "/"
826 }
828 // set cookie, or deny
829 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
830 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
831 err := p.SaveSession(rw, req, session)
832 if err != nil {
833 logger.Printf("%s %s", remoteAddr, err)
834 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
835 return
836 }
837 http.Redirect(rw, req, redirect, http.StatusFound)
838 } else {
839 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
840 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
841 }
844// AuthenticateOnly checks whether the user is currently logged in
845func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
846 session, err := p.getAuthenticatedSession(rw, req)
847 if err != nil {
848 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
849 return
850 }
852 // we are authenticated
853 p.addHeadersForProxying(rw, req, session)
854 rw.WriteHeader(http.StatusAccepted)
857// Proxy proxies the user request if the user is authenticated else it prompts
858// them to authenticate
859func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
860 session, err := p.getAuthenticatedSession(rw, req)
861 switch err {
862 case nil:
863 // we are authenticated
864 p.addHeadersForProxying(rw, req, session)
865 p.serveMux.ServeHTTP(rw, req)
867 case ErrNeedsLogin:
868 // we need to send the user to a login screen
869 if isAjax(req) {
870 // no point redirecting an AJAX request
871 p.ErrorJSON(rw, http.StatusUnauthorized)
872 return
873 }
875 if p.SkipProviderButton {
876 p.OAuthStart(rw, req)
877 } else {
878 p.SignInPage(rw, req, http.StatusForbidden)
879 }
881 default:
882 // unknown error
883 logger.Printf("Unexpected internal error: %s", err)
884 p.ErrorPage(rw, http.StatusInternalServerError,
885 "Internal Error", "Internal Error")
886 }
890// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
891// Returns nil, ErrNeedsLogin if user needs to login.
892// Set-Cookie headers may be set on the response as a side-effect of calling this method.
893func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
894 var session *sessionsapi.SessionState
895 var err error
896 var saveSession, clearSession, revalidated bool
898 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
899 session, err = p.GetJwtSession(req)
900 if err != nil {
901 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
902 }
903 if session != nil {
904 saveSession = false
905 }
906 }
908 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
909 if session == nil {
910 session, err = p.LoadCookiedSession(req)
911 if err != nil {
912 logger.Printf("Error loading cookied session: %s", err)
913 }
915 if session != nil {
916 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
917 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
918 saveSession = true
919 }
921 if ok, err := p.provider.RefreshSessionIfNeeded(req.Context(), session); err != nil {
922 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
923 clearSession = true
924 session = nil
925 } else if ok {
926 saveSession = true
927 revalidated = true
928 }
929 }
930 }
932 if session != nil && session.IsExpired() {
933 logger.Printf("Removing session: token expired %s", session)
934 session = nil
935 saveSession = false
936 clearSession = true
937 }
939 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
940 if !p.provider.ValidateSessionState(req.Context(), session) {
941 logger.Printf("Removing session: error validating %s", session)
942 saveSession = false
943 session = nil
944 clearSession = true
945 }
946 }
948 if session != nil && session.Email != "" && !p.Validator(session.Email) {
949 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
950 session = nil
951 saveSession = false
952 clearSession = true
953 }
955 if saveSession && session != nil {
956 err = p.SaveSession(rw, req, session)
957 if err != nil {
958 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
959 return nil, err
960 }
961 }
963 if clearSession {
964 p.ClearSessionCookie(rw, req)
965 }
967 if session == nil {
968 session, err = p.CheckBasicAuth(req)
969 if err != nil {
970 logger.Printf("Error during basic auth validation: %s", err)
971 }
972 }
974 if session == nil {
975 return nil, ErrNeedsLogin
976 }
978 return session, nil
981// addHeadersForProxying adds the appropriate headers the request / response for proxying
982func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
983 if p.PassBasicAuth {
984 if p.PreferEmailToUser && session.Email != "" {
985 req.SetBasicAuth(session.Email, p.BasicAuthPassword)
986 req.Header["X-Forwarded-User"] = []string{session.Email}
987 req.Header.Del("X-Forwarded-Email")
988 } else {
989 req.SetBasicAuth(session.User, p.BasicAuthPassword)
990 req.Header["X-Forwarded-User"] = []string{session.User}
991 if session.Email != "" {
992 req.Header["X-Forwarded-Email"] = []string{session.Email}
993 } else {
994 req.Header.Del("X-Forwarded-Email")
995 }
996 }
997 if session.PreferredUsername != "" {
998 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
999 } else {
1000 req.Header.Del("X-Forwarded-Preferred-Username")
1004 if p.PassUserHeaders {
1005 if p.PreferEmailToUser && session.Email != "" {
1006 req.Header["X-Forwarded-User"] = []string{session.Email}
1007 req.Header.Del("X-Forwarded-Email")
1008 } else {
1009 req.Header["X-Forwarded-User"] = []string{session.User}
1010 if session.Email != "" {
1011 req.Header["X-Forwarded-Email"] = []string{session.Email}
1012 } else {
1013 req.Header.Del("X-Forwarded-Email")
1017 if session.PreferredUsername != "" {
1018 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
1019 } else {
1020 req.Header.Del("X-Forwarded-Preferred-Username")
1024 if p.SetXAuthRequest {
1025 rw.Header().Set("X-Auth-Request-User", session.User)
1026 if session.Email != "" {
1027 rw.Header().Set("X-Auth-Request-Email", session.Email)
1028 } else {
1029 rw.Header().Del("X-Auth-Request-Email")
1031 if session.PreferredUsername != "" {
1032 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
1033 } else {
1034 rw.Header().Del("X-Auth-Request-Preferred-Username")
1037 if p.PassAccessToken {
1038 if session.AccessToken != "" {
1039 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
1040 } else {
1041 rw.Header().Del("X-Auth-Request-Access-Token")
1046 if p.PassAccessToken {
1047 if session.AccessToken != "" {
1048 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
1049 } else {
1050 req.Header.Del("X-Forwarded-Access-Token")
1054 if p.PassAuthorization {
1055 if session.IDToken != "" {
1056 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
1057 } else {
1058 req.Header.Del("Authorization")
1061 if p.SetBasicAuth {
1062 switch {
1063 case p.PreferEmailToUser && session.Email != "":
1064 authVal := b64.StdEncoding.EncodeToString([]byte(session.Email + ":" + p.BasicAuthPassword))
1065 rw.Header().Set("Authorization", "Basic "+authVal)
1066 case session.User != "":
1067 authVal := b64.StdEncoding.EncodeToString([]byte(session.User + ":" + p.BasicAuthPassword))
1068 rw.Header().Set("Authorization", "Basic "+authVal)
1069 default:
1070 rw.Header().Del("Authorization")
1073 if p.SetAuthorization {
1074 if session.IDToken != "" {
1075 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
1076 } else {
1077 rw.Header().Del("Authorization")
1081 if session.Email == "" {
1082 rw.Header().Set("GAP-Auth", session.User)
1083 } else {
1084 rw.Header().Set("GAP-Auth", session.Email)
1088// CheckBasicAuth checks the requests Authorization header for basic auth
1089// credentials and authenticates these against the proxies HtpasswdFile
1090func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
1091 if p.HtpasswdFile == nil {
1092 return nil, nil
1094 auth := req.Header.Get("Authorization")
1095 if auth == "" {
1096 return nil, nil
1098 s := strings.SplitN(auth, " ", 2)
1099 if len(s) != 2 || s[0] != "Basic" {
1100 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1102 b, err := b64.StdEncoding.DecodeString(s[1])
1103 if err != nil {
1104 return nil, err
1106 pair := strings.SplitN(string(b), ":", 2)
1107 if len(pair) != 2 {
1108 return nil, fmt.Errorf("invalid format %s", b)
1110 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
1111 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1112 return &sessionsapi.SessionState{User: pair[0]}, nil
1114 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1115 return nil, nil
1118// isAjax checks if a request is an ajax request
1119func isAjax(req *http.Request) bool {
1120 acceptValues := req.Header.Values("Accept")
1121 const ajaxReq = applicationJSON
1122 for _, v := range acceptValues {
1123 if v == ajaxReq {
1124 return true
1127 return false
1130// ErrorJSON returns the error code with an application/json mime type
1131func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1132 rw.Header().Set("Content-Type", applicationJSON)
1133 rw.WriteHeader(code)
1136// GetJwtSession loads a session based on a JWT token in the authorization header.
1137// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
1138func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1139 rawBearerToken, err := p.findBearerToken(req)
1140 if err != nil {
1141 return nil, err
1144 // If we are using an oidc provider, go ahead and try that provider first with its Verifier
1145 // and Bearer Token -> Session converter
1146 if p.mainJwtBearerVerifier != nil {
1147 bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
1148 if err == nil {
1149 return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1153 // Otherwise, attempt to verify against the extra JWT issuers and use a more generic
1154 // Bearer Token -> Session converter
1155 for _, verifier := range p.extraJwtBearerVerifiers {
1156 bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1157 if err != nil {
1158 continue
1161 return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1163 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1166// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1167func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1168 auth := req.Header.Get("Authorization")
1169 s := strings.SplitN(auth, " ", 2)
1170 if len(s) != 2 {
1171 return "", fmt.Errorf("invalid authorization header %s", auth)
1173 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1174 var rawBearerToken string
1175 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1176 rawBearerToken = s[1]
1177 } else if s[0] == "Basic" {
1178 // Check if we have a Bearer token masquerading in Basic
1179 b, err := b64.StdEncoding.DecodeString(s[1])
1180 if err != nil {
1181 return "", err
1183 pair := strings.SplitN(string(b), ":", 2)
1184 if len(pair) != 2 {
1185 return "", fmt.Errorf("invalid format %s", b)
1187 user, password := pair[0], pair[1]
1189 // check user, user+password, or just password for a token
1190 if jwtRegex.MatchString(user) {
1191 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1192 if password == "" || password == "x-oauth-basic" {
1193 rawBearerToken = user
1195 } else if jwtRegex.MatchString(password) {
1196 // support passwords and ignore user
1197 rawBearerToken = password
1200 if rawBearerToken == "" {
1201 return "", fmt.Errorf("no valid bearer token found in authorization header")
1204 return rawBearerToken, nil