git.iko.soy/typeable/oauth2-proxy
on v5.1.1 help sign in
typeable/oauth2-proxy/oauthproxy.go
1175 lines 36 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 sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
23 "github.com/pusher/oauth2_proxy/pkg/cookies"
24 "github.com/pusher/oauth2_proxy/pkg/encryption"
25 "github.com/pusher/oauth2_proxy/pkg/logger"
26 "github.com/pusher/oauth2_proxy/providers"
27 "github.com/yhat/wsutil"
30const (
31 // SignatureHeader is the name of the request header containing the GAP Signature
32 // Part of hmacauth
33 SignatureHeader = "GAP-Signature"
35 httpScheme = "http"
36 httpsScheme = "https"
38 applicationJSON = "application/json"
41// SignatureHeaders contains the headers to be signed by the hmac algorithm
42// Part of hmacauth
43var SignatureHeaders = []string{
44 "Content-Length",
45 "Content-Md5",
46 "Content-Type",
47 "Date",
48 "Authorization",
49 "X-Forwarded-User",
50 "X-Forwarded-Email",
51 "X-Forwarded-Preferred-User",
52 "X-Forwarded-Access-Token",
53 "Cookie",
54 "Gap-Auth",
57var (
58 // ErrNeedsLogin means the user should be redirected to the login page
59 ErrNeedsLogin = errors.New("redirect to login page")
61 // Used to check final redirects are not susceptible to open redirects.
62 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
63 invalidRedirectRegex = regexp.MustCompile(`^/(\s|\v)?(/|\\)`)
66// OAuthProxy is the main authentication proxy
67type OAuthProxy struct {
68 CookieSeed string
69 CookieName string
70 CSRFCookieName string
71 CookieDomain string
72 CookiePath string
73 CookieSecure bool
74 CookieHTTPOnly bool
75 CookieExpire time.Duration
76 CookieRefresh time.Duration
77 CookieSameSite string
78 Validator func(string) bool
80 RobotsPath string
81 PingPath string
82 SignInPath string
83 SignOutPath string
84 OAuthStartPath string
85 OAuthCallbackPath string
86 AuthOnlyPath string
87 UserInfoPath string
89 redirectURL *url.URL // the url to receive requests at
90 whitelistDomains []string
91 provider providers.Provider
92 providerNameOverride string
93 sessionStore sessionsapi.SessionStore
94 ProxyPrefix string
95 SignInMessage string
96 HtpasswdFile *HtpasswdFile
97 DisplayHtpasswdForm bool
98 serveMux http.Handler
99 SetXAuthRequest bool
100 PassBasicAuth bool
101 SkipProviderButton bool
102 PassUserHeaders bool
103 BasicAuthPassword string
104 PassAccessToken bool
105 SetAuthorization bool
106 PassAuthorization bool
107 PreferEmailToUser bool
108 skipAuthRegex []string
109 skipAuthPreflight bool
110 skipJwtBearerTokens bool
111 jwtBearerVerifiers []*oidc.IDTokenVerifier
112 compiledRegex []*regexp.Regexp
113 templates *template.Template
114 Banner string
115 Footer string
118// UpstreamProxy represents an upstream server to proxy to
119type UpstreamProxy struct {
120 upstream string
121 handler http.Handler
122 wsHandler http.Handler
123 auth hmacauth.HmacAuth
126// ServeHTTP proxies requests to the upstream provider while signing the
127// request headers
128func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
129 w.Header().Set("GAP-Upstream-Address", u.upstream)
130 if u.auth != nil {
131 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
132 u.auth.SignRequest(r)
133 }
134 if u.wsHandler != nil && strings.EqualFold(r.Header.Get("Connection"), "upgrade") && r.Header.Get("Upgrade") == "websocket" {
135 u.wsHandler.ServeHTTP(w, r)
136 } else {
137 u.handler.ServeHTTP(w, r)
138 }
142// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
143// servers
144func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) {
145 proxy = httputil.NewSingleHostReverseProxy(target)
146 proxy.FlushInterval = opts.FlushInterval
147 if opts.SSLUpstreamInsecureSkipVerify {
148 proxy.Transport = &http.Transport{
149 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
150 }
151 }
152 return proxy
155func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
156 director := proxy.Director
157 proxy.Director = func(req *http.Request) {
158 director(req)
159 // use RequestURI so that we aren't unescaping encoded slashes in the request path
160 req.Host = target.Host
161 req.URL.Opaque = req.RequestURI
162 req.URL.RawQuery = ""
163 }
166func setProxyDirector(proxy *httputil.ReverseProxy) {
167 director := proxy.Director
168 proxy.Director = func(req *http.Request) {
169 director(req)
170 // use RequestURI so that we aren't unescaping encoded slashes in the request path
171 req.URL.Opaque = req.RequestURI
172 req.URL.RawQuery = ""
173 }
176// NewFileServer creates a http.Handler to serve files from the filesystem
177func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
178 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
181// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
182func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
183 u.Path = ""
184 proxy := NewReverseProxy(u, opts)
185 if !opts.PassHostHeader {
186 setProxyUpstreamHostHeader(proxy, u)
187 } else {
188 setProxyDirector(proxy)
189 }
191 // this should give us a wss:// scheme if the url is https:// based.
192 var wsProxy *wsutil.ReverseProxy
193 if opts.ProxyWebSockets {
194 wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
195 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
196 wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
197 }
198 return &UpstreamProxy{
199 upstream: u.Host,
200 handler: proxy,
201 wsHandler: wsProxy,
202 auth: auth,
203 }
206// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
207func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
208 serveMux := http.NewServeMux()
209 var auth hmacauth.HmacAuth
210 if sigData := opts.signatureData; sigData != nil {
211 auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
212 SignatureHeader, SignatureHeaders)
213 }
214 for _, u := range opts.proxyURLs {
215 path := u.Path
216 host := u.Host
217 switch u.Scheme {
218 case httpScheme, httpsScheme:
219 logger.Printf("mapping path %q => upstream %q", path, u)
220 proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
221 serveMux.Handle(path, proxy)
222 case "static":
223 responseCode, err := strconv.Atoi(host)
224 if err != nil {
225 logger.Printf("unable to convert %q to int, use default \"200\"", host)
226 responseCode = 200
227 }
229 serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
230 rw.WriteHeader(responseCode)
231 fmt.Fprintf(rw, "Authenticated")
232 })
233 case "file":
234 if u.Fragment != "" {
235 path = u.Fragment
236 }
237 logger.Printf("mapping path %q => file system %q", path, u.Path)
238 proxy := NewFileServer(path, u.Path)
239 uProxy := UpstreamProxy{
240 upstream: path,
241 handler: proxy,
242 wsHandler: nil,
243 auth: nil,
244 }
245 serveMux.Handle(path, &uProxy)
246 default:
247 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
248 }
249 }
250 for _, u := range opts.CompiledRegex {
251 logger.Printf("compiled skip-auth-regex => %q", u)
252 }
254 if opts.SkipJwtBearerTokens {
255 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
256 for _, issuer := range opts.ExtraJwtIssuers {
257 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
258 }
259 }
260 redirectURL := opts.redirectURL
261 if redirectURL.Path == "" {
262 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
263 }
265 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID)
266 refresh := "disabled"
267 if opts.CookieRefresh != time.Duration(0) {
268 refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
269 }
271 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s samesite:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, opts.CookieSameSite, refresh)
273 return &OAuthProxy{
274 CookieName: opts.CookieName,
275 CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
276 CookieSeed: opts.CookieSecret,
277 CookieDomain: opts.CookieDomain,
278 CookiePath: opts.CookiePath,
279 CookieSecure: opts.CookieSecure,
280 CookieHTTPOnly: opts.CookieHTTPOnly,
281 CookieExpire: opts.CookieExpire,
282 CookieRefresh: opts.CookieRefresh,
283 CookieSameSite: opts.CookieSameSite,
284 Validator: validator,
286 RobotsPath: "/robots.txt",
287 PingPath: opts.PingPath,
288 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
289 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
290 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
291 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
292 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
293 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
295 ProxyPrefix: opts.ProxyPrefix,
296 provider: opts.provider,
297 providerNameOverride: opts.ProviderName,
298 sessionStore: opts.sessionStore,
299 serveMux: serveMux,
300 redirectURL: redirectURL,
301 whitelistDomains: opts.WhitelistDomains,
302 skipAuthRegex: opts.SkipAuthRegex,
303 skipAuthPreflight: opts.SkipAuthPreflight,
304 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
305 jwtBearerVerifiers: opts.jwtBearerVerifiers,
306 compiledRegex: opts.CompiledRegex,
307 SetXAuthRequest: opts.SetXAuthRequest,
308 PassBasicAuth: opts.PassBasicAuth,
309 PassUserHeaders: opts.PassUserHeaders,
310 BasicAuthPassword: opts.BasicAuthPassword,
311 PassAccessToken: opts.PassAccessToken,
312 SetAuthorization: opts.SetAuthorization,
313 PassAuthorization: opts.PassAuthorization,
314 PreferEmailToUser: opts.PreferEmailToUser,
315 SkipProviderButton: opts.SkipProviderButton,
316 templates: loadTemplates(opts.CustomTemplatesDir),
317 Banner: opts.Banner,
318 Footer: opts.Footer,
319 }
322// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
323// redirect clients to once authenticated
324func (p *OAuthProxy) GetRedirectURI(host string) string {
325 // default to the request Host if not set
326 if p.redirectURL.Host != "" {
327 return p.redirectURL.String()
328 }
329 var u url.URL
330 u = *p.redirectURL
331 if u.Scheme == "" {
332 if p.CookieSecure {
333 u.Scheme = httpsScheme
334 } else {
335 u.Scheme = httpScheme
336 }
337 }
338 u.Host = host
339 return u.String()
342func (p *OAuthProxy) displayCustomLoginForm() bool {
343 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
346func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState, err error) {
347 if code == "" {
348 return nil, errors.New("missing code")
349 }
350 redirectURI := p.GetRedirectURI(host)
351 s, err = p.provider.Redeem(redirectURI, code)
352 if err != nil {
353 return
354 }
356 if s.Email == "" {
357 s.Email, err = p.provider.GetEmailAddress(s)
358 }
360 if s.PreferredUsername == "" {
361 s.PreferredUsername, err = p.provider.GetPreferredUsername(s)
362 if err != nil && err.Error() == "not implemented" {
363 err = nil
364 }
365 }
367 if s.User == "" {
368 s.User, err = p.provider.GetUserName(s)
369 if err != nil && err.Error() == "not implemented" {
370 err = nil
371 }
372 }
373 return
376// MakeCSRFCookie creates a cookie for CSRF
377func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
378 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
381func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
382 if p.CookieDomain != "" {
383 domain := req.Host
384 if h, _, err := net.SplitHostPort(domain); err == nil {
385 domain = h
386 }
387 if !strings.HasSuffix(domain, p.CookieDomain) {
388 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain)
389 }
390 }
392 return &http.Cookie{
393 Name: name,
394 Value: value,
395 Path: p.CookiePath,
396 Domain: p.CookieDomain,
397 HttpOnly: p.CookieHTTPOnly,
398 Secure: p.CookieSecure,
399 Expires: now.Add(expiration),
400 SameSite: cookies.ParseSameSite(p.CookieSameSite),
401 }
404// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
405// session
406func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
407 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
410// SetCSRFCookie adds a CSRF cookie to the response
411func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
412 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
415// ClearSessionCookie creates a cookie to unset the user's authentication cookie
416// stored in the user's session
417func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
418 return p.sessionStore.Clear(rw, req)
421// LoadCookiedSession reads the user's authentication details from the request
422func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
423 return p.sessionStore.Load(req)
426// SaveSession creates a new session cookie value and sets this on the response
427func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
428 return p.sessionStore.Save(rw, req, s)
431// RobotsTxt disallows scraping pages from the OAuthProxy
432func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
433 rw.WriteHeader(http.StatusOK)
434 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
437// PingPage responds 200 OK to requests
438func (p *OAuthProxy) PingPage(rw http.ResponseWriter) {
439 rw.WriteHeader(http.StatusOK)
440 fmt.Fprintf(rw, "OK")
443// ErrorPage writes an error response
444func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
445 rw.WriteHeader(code)
446 t := struct {
447 Title string
448 Message string
449 ProxyPrefix string
450 }{
451 Title: fmt.Sprintf("%d %s", code, title),
452 Message: message,
453 ProxyPrefix: p.ProxyPrefix,
454 }
455 p.templates.ExecuteTemplate(rw, "error.html", t)
458// SignInPage writes the sing in template to the response
459func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
460 p.ClearSessionCookie(rw, req)
461 rw.WriteHeader(code)
463 redirectURL, err := p.GetRedirect(req)
464 if err != nil {
465 logger.Printf("Error obtaining redirect: %s", err.Error())
466 p.ErrorPage(rw, 500, "Internal Error", err.Error())
467 return
468 }
470 if redirectURL == p.SignInPath {
471 redirectURL = "/"
472 }
474 t := struct {
475 ProviderName string
476 SignInMessage string
477 CustomLogin bool
478 Redirect string
479 Version string
480 ProxyPrefix string
481 Footer template.HTML
482 }{
483 ProviderName: p.provider.Data().ProviderName,
484 SignInMessage: p.SignInMessage,
485 CustomLogin: p.displayCustomLoginForm(),
486 Redirect: redirectURL,
487 Version: VERSION,
488 ProxyPrefix: p.ProxyPrefix,
489 Footer: template.HTML(p.Footer),
490 }
491 if p.providerNameOverride != "" {
492 t.ProviderName = p.providerNameOverride
493 }
494 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
497// ManualSignIn handles basic auth logins to the proxy
498func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
499 if req.Method != "POST" || p.HtpasswdFile == nil {
500 return "", false
501 }
502 user := req.FormValue("username")
503 passwd := req.FormValue("password")
504 if user == "" {
505 return "", false
506 }
507 // check auth
508 if p.HtpasswdFile.Validate(user, passwd) {
509 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
510 return user, true
511 }
512 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
513 return "", false
516// GetRedirect reads the query parameter to get the URL to redirect clients to
517// once authenticated with the OAuthProxy
518func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
519 err = req.ParseForm()
520 if err != nil {
521 return
522 }
524 redirect = req.Header.Get("X-Auth-Request-Redirect")
525 if req.Form.Get("rd") != "" {
526 redirect = req.Form.Get("rd")
527 }
528 if !p.IsValidRedirect(redirect) {
529 redirect = req.URL.Path
530 if strings.HasPrefix(redirect, p.ProxyPrefix) {
531 redirect = "/"
532 }
533 }
535 return
538// splitHostPort separates host and port. If the port is not valid, it returns
539// the entire input as host, and it doesn't check the validity of the host.
540// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
541// *** taken from net/url, modified validOptionalPort() to accept ":*"
542func splitHostPort(hostport string) (host, port string) {
543 host = hostport
545 colon := strings.LastIndexByte(host, ':')
546 if colon != -1 && validOptionalPort(host[colon:]) {
547 host, port = host[:colon], host[colon+1:]
548 }
550 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
551 host = host[1 : len(host)-1]
552 }
554 return
557// validOptionalPort reports whether port is either an empty string
558// or matches /^:\d*$/
559// *** taken from net/url, modified to accept ":*"
560func validOptionalPort(port string) bool {
561 if port == "" || port == ":*" {
562 return true
563 }
564 if port[0] != ':' {
565 return false
566 }
567 for _, b := range port[1:] {
568 if b < '0' || b > '9' {
569 return false
570 }
571 }
572 return true
575// IsValidRedirect checks whether the redirect URL is whitelisted
576func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
577 switch {
578 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
579 return true
580 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
581 redirectURL, err := url.Parse(redirect)
582 if err != nil {
583 return false
584 }
585 redirectHostname := redirectURL.Hostname()
587 for _, domain := range p.whitelistDomains {
588 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
589 if domainHostname == "" {
590 continue
591 }
593 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
594 // the domain names match, now validate the ports
595 // if the whitelisted domain's port is '*', allow all ports
596 // if the whitelisted domain contains a specific port, only allow that port
597 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
598 redirectPort := redirectURL.Port()
599 if (domainPort == "*") ||
600 (domainPort == redirectPort) ||
601 (domainPort == "" && redirectPort == "") {
602 return true
603 }
604 }
605 }
607 return false
608 default:
609 return false
610 }
613// IsWhitelistedRequest is used to check if auth should be skipped for this request
614func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
615 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
616 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
619// IsWhitelistedPath is used to check if the request path is allowed without auth
620func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
621 for _, u := range p.compiledRegex {
622 if u.MatchString(path) {
623 return true
624 }
625 }
626 return false
629func getRemoteAddr(req *http.Request) (s string) {
630 s = req.RemoteAddr
631 if req.Header.Get("X-Real-IP") != "" {
632 s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
633 }
634 return
637func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
638 switch path := req.URL.Path; {
639 case path == p.RobotsPath:
640 p.RobotsTxt(rw)
641 case path == p.PingPath:
642 p.PingPage(rw)
643 case p.IsWhitelistedRequest(req):
644 p.serveMux.ServeHTTP(rw, req)
645 case path == p.SignInPath:
646 p.SignIn(rw, req)
647 case path == p.SignOutPath:
648 p.SignOut(rw, req)
649 case path == p.OAuthStartPath:
650 p.OAuthStart(rw, req)
651 case path == p.OAuthCallbackPath:
652 p.OAuthCallback(rw, req)
653 case path == p.AuthOnlyPath:
654 p.AuthenticateOnly(rw, req)
655 case path == p.UserInfoPath:
656 p.UserInfo(rw, req)
657 default:
658 p.Proxy(rw, req)
659 }
662// SignIn serves a page prompting users to sign in
663func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
664 redirect, err := p.GetRedirect(req)
665 if err != nil {
666 logger.Printf("Error obtaining redirect: %s", err.Error())
667 p.ErrorPage(rw, 500, "Internal Error", err.Error())
668 return
669 }
671 user, ok := p.ManualSignIn(rw, req)
672 if ok {
673 session := &sessionsapi.SessionState{User: user}
674 p.SaveSession(rw, req, session)
675 http.Redirect(rw, req, redirect, 302)
676 } else {
677 if p.SkipProviderButton {
678 p.OAuthStart(rw, req)
679 } else {
680 p.SignInPage(rw, req, http.StatusOK)
681 }
682 }
685//UserInfo endpoint outputs session email and preferred username in JSON format
686func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
688 session, err := p.getAuthenticatedSession(rw, req)
689 if err != nil {
690 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
691 return
692 }
693 userInfo := struct {
694 Email string `json:"email"`
695 PreferredUsername string `json:"preferredUsername,omitempty"`
696 }{
697 Email: session.Email,
698 PreferredUsername: session.PreferredUsername,
699 }
700 rw.Header().Set("Content-Type", "application/json")
701 rw.WriteHeader(http.StatusOK)
702 json.NewEncoder(rw).Encode(userInfo)
705// SignOut sends a response to clear the authentication cookie
706func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
707 redirect, err := p.GetRedirect(req)
708 if err != nil {
709 logger.Printf("Error obtaining redirect: %s", err.Error())
710 p.ErrorPage(rw, 500, "Internal Error", err.Error())
711 return
712 }
713 p.ClearSessionCookie(rw, req)
714 http.Redirect(rw, req, redirect, 302)
717// OAuthStart starts the OAuth2 authentication flow
718func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
719 nonce, err := encryption.Nonce()
720 if err != nil {
721 logger.Printf("Error obtaining nonce: %s", err.Error())
722 p.ErrorPage(rw, 500, "Internal Error", err.Error())
723 return
724 }
725 p.SetCSRFCookie(rw, req, nonce)
726 redirect, err := p.GetRedirect(req)
727 if err != nil {
728 logger.Printf("Error obtaining redirect: %s", err.Error())
729 p.ErrorPage(rw, 500, "Internal Error", err.Error())
730 return
731 }
732 redirectURI := p.GetRedirectURI(req.Host)
733 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302)
736// OAuthCallback is the OAuth2 authentication flow callback that finishes the
737// OAuth2 authentication flow
738func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
739 remoteAddr := getRemoteAddr(req)
741 // finish the oauth cycle
742 err := req.ParseForm()
743 if err != nil {
744 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
745 p.ErrorPage(rw, 500, "Internal Error", err.Error())
746 return
747 }
748 errorString := req.Form.Get("error")
749 if errorString != "" {
750 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
751 p.ErrorPage(rw, 403, "Permission Denied", errorString)
752 return
753 }
755 session, err := p.redeemCode(req.Host, req.Form.Get("code"))
756 if err != nil {
757 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
758 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
759 return
760 }
762 s := strings.SplitN(req.Form.Get("state"), ":", 2)
763 if len(s) != 2 {
764 logger.Printf("Error while parsing OAuth2 state: invalid length")
765 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
766 return
767 }
768 nonce := s[0]
769 redirect := s[1]
770 c, err := req.Cookie(p.CSRFCookieName)
771 if err != nil {
772 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
773 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
774 return
775 }
776 p.ClearCSRFCookie(rw, req)
777 if c.Value != nonce {
778 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
779 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
780 return
781 }
783 if !p.IsValidRedirect(redirect) {
784 redirect = "/"
785 }
787 // set cookie, or deny
788 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
789 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
790 err := p.SaveSession(rw, req, session)
791 if err != nil {
792 logger.Printf("%s %s", remoteAddr, err)
793 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
794 return
795 }
796 http.Redirect(rw, req, redirect, 302)
797 } else {
798 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
799 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
800 }
803// AuthenticateOnly checks whether the user is currently logged in
804func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
805 session, err := p.getAuthenticatedSession(rw, req)
806 if err != nil {
807 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
808 return
809 }
811 // we are authenticated
812 p.addHeadersForProxying(rw, req, session)
813 rw.WriteHeader(http.StatusAccepted)
816// Proxy proxies the user request if the user is authenticated else it prompts
817// them to authenticate
818func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
819 session, err := p.getAuthenticatedSession(rw, req)
820 switch err {
821 case nil:
822 // we are authenticated
823 p.addHeadersForProxying(rw, req, session)
824 p.serveMux.ServeHTTP(rw, req)
826 case ErrNeedsLogin:
827 // we need to send the user to a login screen
828 if isAjax(req) {
829 // no point redirecting an AJAX request
830 p.ErrorJSON(rw, http.StatusUnauthorized)
831 return
832 }
834 if p.SkipProviderButton {
835 p.OAuthStart(rw, req)
836 } else {
837 p.SignInPage(rw, req, http.StatusForbidden)
838 }
840 default:
841 // unknown error
842 logger.Printf("Unexpected internal error: %s", err)
843 p.ErrorPage(rw, http.StatusInternalServerError,
844 "Internal Error", "Internal Error")
845 }
849// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
850// Returns nil, ErrNeedsLogin if user needs to login.
851// Set-Cookie headers may be set on the response as a side-effect of calling this method.
852func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
853 var session *sessionsapi.SessionState
854 var err error
855 var saveSession, clearSession, revalidated bool
857 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
858 session, err = p.GetJwtSession(req)
859 if err != nil {
860 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
861 }
862 if session != nil {
863 saveSession = false
864 }
865 }
867 remoteAddr := getRemoteAddr(req)
868 if session == nil {
869 session, err = p.LoadCookiedSession(req)
870 if err != nil {
871 logger.Printf("Error loading cookied session: %s", err)
872 }
874 if session != nil {
875 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
876 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
877 saveSession = true
878 }
880 if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
881 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
882 clearSession = true
883 session = nil
884 } else if ok {
885 saveSession = true
886 revalidated = true
887 }
888 }
889 }
891 if session != nil && session.IsExpired() {
892 logger.Printf("Removing session: token expired %s", session)
893 session = nil
894 saveSession = false
895 clearSession = true
896 }
898 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
899 if !p.provider.ValidateSessionState(session) {
900 logger.Printf("Removing session: error validating %s", session)
901 saveSession = false
902 session = nil
903 clearSession = true
904 }
905 }
907 if session != nil && session.Email != "" && !p.Validator(session.Email) {
908 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
909 session = nil
910 saveSession = false
911 clearSession = true
912 }
914 if saveSession && session != nil {
915 err = p.SaveSession(rw, req, session)
916 if err != nil {
917 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
918 return nil, err
919 }
920 }
922 if clearSession {
923 p.ClearSessionCookie(rw, req)
924 }
926 if session == nil {
927 session, err = p.CheckBasicAuth(req)
928 if err != nil {
929 logger.Printf("Error during basic auth validation: %s", err)
930 }
931 }
933 if session == nil {
934 return nil, ErrNeedsLogin
935 }
937 return session, nil
940// addHeadersForProxying adds the appropriate headers the request / response for proxying
941func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
942 if p.PassBasicAuth {
943 if p.PreferEmailToUser && session.Email != "" {
944 req.SetBasicAuth(session.Email, p.BasicAuthPassword)
945 req.Header["X-Forwarded-User"] = []string{session.Email}
946 req.Header.Del("X-Forwarded-Email")
947 } else {
948 req.SetBasicAuth(session.User, p.BasicAuthPassword)
949 req.Header["X-Forwarded-User"] = []string{session.User}
950 if session.Email != "" {
951 req.Header["X-Forwarded-Email"] = []string{session.Email}
952 } else {
953 req.Header.Del("X-Forwarded-Email")
954 }
955 }
956 if session.PreferredUsername != "" {
957 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
958 } else {
959 req.Header.Del("X-Forwarded-Preferred-Username")
960 }
961 }
963 if p.PassUserHeaders {
964 if p.PreferEmailToUser && session.Email != "" {
965 req.Header["X-Forwarded-User"] = []string{session.Email}
966 req.Header.Del("X-Forwarded-Email")
967 } else {
968 req.Header["X-Forwarded-User"] = []string{session.User}
969 if session.Email != "" {
970 req.Header["X-Forwarded-Email"] = []string{session.Email}
971 } else {
972 req.Header.Del("X-Forwarded-Email")
973 }
974 }
976 if session.PreferredUsername != "" {
977 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
978 } else {
979 req.Header.Del("X-Forwarded-Preferred-Username")
980 }
981 }
983 if p.SetXAuthRequest {
984 rw.Header().Set("X-Auth-Request-User", session.User)
985 if session.Email != "" {
986 rw.Header().Set("X-Auth-Request-Email", session.Email)
987 } else {
988 rw.Header().Del("X-Auth-Request-Email")
989 }
990 if session.PreferredUsername != "" {
991 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
992 } else {
993 rw.Header().Del("X-Auth-Request-Preferred-Username")
994 }
996 if p.PassAccessToken {
997 if session.AccessToken != "" {
998 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
999 } else {
1000 rw.Header().Del("X-Auth-Request-Access-Token")
1005 if p.PassAccessToken {
1006 if session.AccessToken != "" {
1007 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
1008 } else {
1009 req.Header.Del("X-Forwarded-Access-Token")
1013 if p.PassAuthorization {
1014 if session.IDToken != "" {
1015 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
1016 } else {
1017 req.Header.Del("Authorization")
1020 if p.SetAuthorization {
1021 if session.IDToken != "" {
1022 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
1023 } else {
1024 rw.Header().Del("Authorization")
1028 if session.Email == "" {
1029 rw.Header().Set("GAP-Auth", session.User)
1030 } else {
1031 rw.Header().Set("GAP-Auth", session.Email)
1035// CheckBasicAuth checks the requests Authorization header for basic auth
1036// credentials and authenticates these against the proxies HtpasswdFile
1037func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
1038 if p.HtpasswdFile == nil {
1039 return nil, nil
1041 auth := req.Header.Get("Authorization")
1042 if auth == "" {
1043 return nil, nil
1045 s := strings.SplitN(auth, " ", 2)
1046 if len(s) != 2 || s[0] != "Basic" {
1047 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1049 b, err := b64.StdEncoding.DecodeString(s[1])
1050 if err != nil {
1051 return nil, err
1053 pair := strings.SplitN(string(b), ":", 2)
1054 if len(pair) != 2 {
1055 return nil, fmt.Errorf("invalid format %s", b)
1057 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
1058 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1059 return &sessionsapi.SessionState{User: pair[0]}, nil
1061 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1062 return nil, nil
1065// isAjax checks if a request is an ajax request
1066func isAjax(req *http.Request) bool {
1067 acceptValues, ok := req.Header["accept"]
1068 if !ok {
1069 acceptValues = req.Header["Accept"]
1071 const ajaxReq = applicationJSON
1072 for _, v := range acceptValues {
1073 if v == ajaxReq {
1074 return true
1077 return false
1080// ErrorJSON returns the error code with an application/json mime type
1081func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1082 rw.Header().Set("Content-Type", applicationJSON)
1083 rw.WriteHeader(code)
1086// GetJwtSession loads a session based on a JWT token in the authorization header.
1087func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1088 rawBearerToken, err := p.findBearerToken(req)
1089 if err != nil {
1090 return nil, err
1093 ctx := context.Background()
1094 var session *sessionsapi.SessionState
1095 for _, verifier := range p.jwtBearerVerifiers {
1096 bearerToken, err := verifier.Verify(ctx, rawBearerToken)
1098 if err != nil {
1099 logger.Printf("failed to verify bearer token: %v", err)
1100 continue
1103 var claims struct {
1104 Subject string `json:"sub"`
1105 Email string `json:"email"`
1106 Verified *bool `json:"email_verified"`
1107 PreferredUsername string `json:"preferred_username"`
1110 if err := bearerToken.Claims(&claims); err != nil {
1111 return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
1114 if claims.Email == "" {
1115 claims.Email = claims.Subject
1118 if claims.Verified != nil && !*claims.Verified {
1119 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
1122 session = &sessionsapi.SessionState{
1123 AccessToken: rawBearerToken,
1124 IDToken: rawBearerToken,
1125 RefreshToken: "",
1126 ExpiresOn: bearerToken.Expiry,
1127 Email: claims.Email,
1128 User: claims.Email,
1129 PreferredUsername: claims.PreferredUsername,
1131 return session, nil
1133 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1136// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1137func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1138 auth := req.Header.Get("Authorization")
1139 s := strings.SplitN(auth, " ", 2)
1140 if len(s) != 2 {
1141 return "", fmt.Errorf("invalid authorization header %s", auth)
1143 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1144 var rawBearerToken string
1145 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1146 rawBearerToken = s[1]
1147 } else if s[0] == "Basic" {
1148 // Check if we have a Bearer token masquerading in Basic
1149 b, err := b64.StdEncoding.DecodeString(s[1])
1150 if err != nil {
1151 return "", err
1153 pair := strings.SplitN(string(b), ":", 2)
1154 if len(pair) != 2 {
1155 return "", fmt.Errorf("invalid format %s", b)
1157 user, password := pair[0], pair[1]
1159 // check user, user+password, or just password for a token
1160 if jwtRegex.MatchString(user) {
1161 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1162 if password == "" || password == "x-oauth-basic" {
1163 rawBearerToken = user
1165 } else if jwtRegex.MatchString(password) {
1166 // support passwords and ignore user
1167 rawBearerToken = password
1170 if rawBearerToken == "" {
1171 return "", fmt.Errorf("no valid bearer token found in authorization header")
1174 return rawBearerToken, nil