git.iko.soy/typeable/oauth2-proxy
on v5.0.0 help sign in
typeable/oauth2-proxy/oauthproxy.go
1127 lines 34 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-Access-Token",
52 "Cookie",
53 "Gap-Auth",
56var (
57 // ErrNeedsLogin means the user should be redirected to the login page
58 ErrNeedsLogin = errors.New("redirect to login page")
61// OAuthProxy is the main authentication proxy
62type OAuthProxy struct {
63 CookieSeed string
64 CookieName string
65 CSRFCookieName string
66 CookieDomain string
67 CookiePath string
68 CookieSecure bool
69 CookieHTTPOnly bool
70 CookieExpire time.Duration
71 CookieRefresh time.Duration
72 CookieSameSite string
73 Validator func(string) bool
75 RobotsPath string
76 PingPath string
77 SignInPath string
78 SignOutPath string
79 OAuthStartPath string
80 OAuthCallbackPath string
81 AuthOnlyPath string
82 UserInfoPath string
84 redirectURL *url.URL // the url to receive requests at
85 whitelistDomains []string
86 provider providers.Provider
87 providerNameOverride string
88 sessionStore sessionsapi.SessionStore
89 ProxyPrefix string
90 SignInMessage string
91 HtpasswdFile *HtpasswdFile
92 DisplayHtpasswdForm bool
93 serveMux http.Handler
94 SetXAuthRequest bool
95 PassBasicAuth bool
96 SkipProviderButton bool
97 PassUserHeaders bool
98 BasicAuthPassword string
99 PassAccessToken bool
100 SetAuthorization bool
101 PassAuthorization bool
102 skipAuthRegex []string
103 skipAuthPreflight bool
104 skipJwtBearerTokens bool
105 jwtBearerVerifiers []*oidc.IDTokenVerifier
106 compiledRegex []*regexp.Regexp
107 templates *template.Template
108 Banner string
109 Footer string
112// UpstreamProxy represents an upstream server to proxy to
113type UpstreamProxy struct {
114 upstream string
115 handler http.Handler
116 wsHandler http.Handler
117 auth hmacauth.HmacAuth
120// ServeHTTP proxies requests to the upstream provider while signing the
121// request headers
122func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
123 w.Header().Set("GAP-Upstream-Address", u.upstream)
124 if u.auth != nil {
125 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
126 u.auth.SignRequest(r)
127 }
128 if u.wsHandler != nil && strings.EqualFold(r.Header.Get("Connection"), "upgrade") && r.Header.Get("Upgrade") == "websocket" {
129 u.wsHandler.ServeHTTP(w, r)
130 } else {
131 u.handler.ServeHTTP(w, r)
132 }
136// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
137// servers
138func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) {
139 proxy = httputil.NewSingleHostReverseProxy(target)
140 proxy.FlushInterval = opts.FlushInterval
141 if opts.SSLUpstreamInsecureSkipVerify {
142 proxy.Transport = &http.Transport{
143 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
144 }
145 }
146 return proxy
149func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
150 director := proxy.Director
151 proxy.Director = func(req *http.Request) {
152 director(req)
153 // use RequestURI so that we aren't unescaping encoded slashes in the request path
154 req.Host = target.Host
155 req.URL.Opaque = req.RequestURI
156 req.URL.RawQuery = ""
157 }
160func setProxyDirector(proxy *httputil.ReverseProxy) {
161 director := proxy.Director
162 proxy.Director = func(req *http.Request) {
163 director(req)
164 // use RequestURI so that we aren't unescaping encoded slashes in the request path
165 req.URL.Opaque = req.RequestURI
166 req.URL.RawQuery = ""
167 }
170// NewFileServer creates a http.Handler to serve files from the filesystem
171func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
172 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
175// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
176func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
177 u.Path = ""
178 proxy := NewReverseProxy(u, opts)
179 if !opts.PassHostHeader {
180 setProxyUpstreamHostHeader(proxy, u)
181 } else {
182 setProxyDirector(proxy)
183 }
185 // this should give us a wss:// scheme if the url is https:// based.
186 var wsProxy *wsutil.ReverseProxy
187 if opts.ProxyWebSockets {
188 wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
189 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
190 wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
191 }
192 return &UpstreamProxy{
193 upstream: u.Host,
194 handler: proxy,
195 wsHandler: wsProxy,
196 auth: auth,
197 }
200// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
201func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
202 serveMux := http.NewServeMux()
203 var auth hmacauth.HmacAuth
204 if sigData := opts.signatureData; sigData != nil {
205 auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
206 SignatureHeader, SignatureHeaders)
207 }
208 for _, u := range opts.proxyURLs {
209 path := u.Path
210 host := u.Host
211 switch u.Scheme {
212 case httpScheme, httpsScheme:
213 logger.Printf("mapping path %q => upstream %q", path, u)
214 proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
215 serveMux.Handle(path, proxy)
216 case "static":
217 responseCode, err := strconv.Atoi(host)
218 if err != nil {
219 logger.Printf("unable to convert %q to int, use default \"200\"", host)
220 responseCode = 200
221 }
223 serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
224 rw.WriteHeader(responseCode)
225 fmt.Fprintf(rw, "Authenticated")
226 })
227 case "file":
228 if u.Fragment != "" {
229 path = u.Fragment
230 }
231 logger.Printf("mapping path %q => file system %q", path, u.Path)
232 proxy := NewFileServer(path, u.Path)
233 uProxy := UpstreamProxy{
234 upstream: path,
235 handler: proxy,
236 wsHandler: nil,
237 auth: nil,
238 }
239 serveMux.Handle(path, &uProxy)
240 default:
241 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
242 }
243 }
244 for _, u := range opts.CompiledRegex {
245 logger.Printf("compiled skip-auth-regex => %q", u)
246 }
248 if opts.SkipJwtBearerTokens {
249 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
250 for _, issuer := range opts.ExtraJwtIssuers {
251 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
252 }
253 }
254 redirectURL := opts.redirectURL
255 if redirectURL.Path == "" {
256 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
257 }
259 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID)
260 refresh := "disabled"
261 if opts.CookieRefresh != time.Duration(0) {
262 refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
263 }
265 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)
267 return &OAuthProxy{
268 CookieName: opts.CookieName,
269 CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
270 CookieSeed: opts.CookieSecret,
271 CookieDomain: opts.CookieDomain,
272 CookiePath: opts.CookiePath,
273 CookieSecure: opts.CookieSecure,
274 CookieHTTPOnly: opts.CookieHTTPOnly,
275 CookieExpire: opts.CookieExpire,
276 CookieRefresh: opts.CookieRefresh,
277 CookieSameSite: opts.CookieSameSite,
278 Validator: validator,
280 RobotsPath: "/robots.txt",
281 PingPath: opts.PingPath,
282 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
283 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
284 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
285 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
286 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
287 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
289 ProxyPrefix: opts.ProxyPrefix,
290 provider: opts.provider,
291 providerNameOverride: opts.ProviderName,
292 sessionStore: opts.sessionStore,
293 serveMux: serveMux,
294 redirectURL: redirectURL,
295 whitelistDomains: opts.WhitelistDomains,
296 skipAuthRegex: opts.SkipAuthRegex,
297 skipAuthPreflight: opts.SkipAuthPreflight,
298 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
299 jwtBearerVerifiers: opts.jwtBearerVerifiers,
300 compiledRegex: opts.CompiledRegex,
301 SetXAuthRequest: opts.SetXAuthRequest,
302 PassBasicAuth: opts.PassBasicAuth,
303 PassUserHeaders: opts.PassUserHeaders,
304 BasicAuthPassword: opts.BasicAuthPassword,
305 PassAccessToken: opts.PassAccessToken,
306 SetAuthorization: opts.SetAuthorization,
307 PassAuthorization: opts.PassAuthorization,
308 SkipProviderButton: opts.SkipProviderButton,
309 templates: loadTemplates(opts.CustomTemplatesDir),
310 Banner: opts.Banner,
311 Footer: opts.Footer,
312 }
315// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
316// redirect clients to once authenticated
317func (p *OAuthProxy) GetRedirectURI(host string) string {
318 // default to the request Host if not set
319 if p.redirectURL.Host != "" {
320 return p.redirectURL.String()
321 }
322 var u url.URL
323 u = *p.redirectURL
324 if u.Scheme == "" {
325 if p.CookieSecure {
326 u.Scheme = httpsScheme
327 } else {
328 u.Scheme = httpScheme
329 }
330 }
331 u.Host = host
332 return u.String()
335func (p *OAuthProxy) displayCustomLoginForm() bool {
336 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
339func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState, err error) {
340 if code == "" {
341 return nil, errors.New("missing code")
342 }
343 redirectURI := p.GetRedirectURI(host)
344 s, err = p.provider.Redeem(redirectURI, code)
345 if err != nil {
346 return
347 }
349 if s.Email == "" {
350 s.Email, err = p.provider.GetEmailAddress(s)
351 }
353 if s.User == "" {
354 s.User, err = p.provider.GetUserName(s)
355 if err != nil && err.Error() == "not implemented" {
356 err = nil
357 }
358 }
359 return
362// MakeCSRFCookie creates a cookie for CSRF
363func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
364 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
367func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
368 if p.CookieDomain != "" {
369 domain := req.Host
370 if h, _, err := net.SplitHostPort(domain); err == nil {
371 domain = h
372 }
373 if !strings.HasSuffix(domain, p.CookieDomain) {
374 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain)
375 }
376 }
378 return &http.Cookie{
379 Name: name,
380 Value: value,
381 Path: p.CookiePath,
382 Domain: p.CookieDomain,
383 HttpOnly: p.CookieHTTPOnly,
384 Secure: p.CookieSecure,
385 Expires: now.Add(expiration),
386 SameSite: cookies.ParseSameSite(p.CookieSameSite),
387 }
390// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
391// session
392func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
393 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
396// SetCSRFCookie adds a CSRF cookie to the response
397func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
398 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
401// ClearSessionCookie creates a cookie to unset the user's authentication cookie
402// stored in the user's session
403func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
404 return p.sessionStore.Clear(rw, req)
407// LoadCookiedSession reads the user's authentication details from the request
408func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
409 return p.sessionStore.Load(req)
412// SaveSession creates a new session cookie value and sets this on the response
413func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
414 return p.sessionStore.Save(rw, req, s)
417// RobotsTxt disallows scraping pages from the OAuthProxy
418func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
419 rw.WriteHeader(http.StatusOK)
420 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
423// PingPage responds 200 OK to requests
424func (p *OAuthProxy) PingPage(rw http.ResponseWriter) {
425 rw.WriteHeader(http.StatusOK)
426 fmt.Fprintf(rw, "OK")
429// ErrorPage writes an error response
430func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
431 rw.WriteHeader(code)
432 t := struct {
433 Title string
434 Message string
435 ProxyPrefix string
436 }{
437 Title: fmt.Sprintf("%d %s", code, title),
438 Message: message,
439 ProxyPrefix: p.ProxyPrefix,
440 }
441 p.templates.ExecuteTemplate(rw, "error.html", t)
444// SignInPage writes the sing in template to the response
445func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
446 p.ClearSessionCookie(rw, req)
447 rw.WriteHeader(code)
449 redirecURL := req.URL.RequestURI()
450 if req.Header.Get("X-Auth-Request-Redirect") != "" {
451 redirecURL = req.Header.Get("X-Auth-Request-Redirect")
452 }
453 if redirecURL == p.SignInPath {
454 redirecURL = "/"
455 }
457 t := struct {
458 ProviderName string
459 SignInMessage string
460 CustomLogin bool
461 Redirect string
462 Version string
463 ProxyPrefix string
464 Footer template.HTML
465 }{
466 ProviderName: p.provider.Data().ProviderName,
467 SignInMessage: p.SignInMessage,
468 CustomLogin: p.displayCustomLoginForm(),
469 Redirect: redirecURL,
470 Version: VERSION,
471 ProxyPrefix: p.ProxyPrefix,
472 Footer: template.HTML(p.Footer),
473 }
474 if p.providerNameOverride != "" {
475 t.ProviderName = p.providerNameOverride
476 }
477 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
480// ManualSignIn handles basic auth logins to the proxy
481func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
482 if req.Method != "POST" || p.HtpasswdFile == nil {
483 return "", false
484 }
485 user := req.FormValue("username")
486 passwd := req.FormValue("password")
487 if user == "" {
488 return "", false
489 }
490 // check auth
491 if p.HtpasswdFile.Validate(user, passwd) {
492 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
493 return user, true
494 }
495 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
496 return "", false
499// GetRedirect reads the query parameter to get the URL to redirect clients to
500// once authenticated with the OAuthProxy
501func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
502 err = req.ParseForm()
503 if err != nil {
504 return
505 }
507 redirect = req.Header.Get("X-Auth-Request-Redirect")
508 if req.Form.Get("rd") != "" {
509 redirect = req.Form.Get("rd")
510 }
511 if !p.IsValidRedirect(redirect) {
512 redirect = req.URL.Path
513 if strings.HasPrefix(redirect, p.ProxyPrefix) {
514 redirect = "/"
515 }
516 }
518 return
521// splitHostPort separates host and port. If the port is not valid, it returns
522// the entire input as host, and it doesn't check the validity of the host.
523// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
524// *** taken from net/url, modified validOptionalPort() to accept ":*"
525func splitHostPort(hostport string) (host, port string) {
526 host = hostport
528 colon := strings.LastIndexByte(host, ':')
529 if colon != -1 && validOptionalPort(host[colon:]) {
530 host, port = host[:colon], host[colon+1:]
531 }
533 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
534 host = host[1 : len(host)-1]
535 }
537 return
540// validOptionalPort reports whether port is either an empty string
541// or matches /^:\d*$/
542// *** taken from net/url, modified to accept ":*"
543func validOptionalPort(port string) bool {
544 if port == "" || port == ":*" {
545 return true
546 }
547 if port[0] != ':' {
548 return false
549 }
550 for _, b := range port[1:] {
551 if b < '0' || b > '9' {
552 return false
553 }
554 }
555 return true
558// IsValidRedirect checks whether the redirect URL is whitelisted
559func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
560 switch {
561 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !strings.HasPrefix(redirect, "/\\"):
562 return true
563 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
564 redirectURL, err := url.Parse(redirect)
565 if err != nil {
566 return false
567 }
568 redirectHostname := redirectURL.Hostname()
570 for _, domain := range p.whitelistDomains {
571 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
572 if domainHostname == "" {
573 continue
574 }
576 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
577 // the domain names match, now validate the ports
578 // if the whitelisted domain's port is '*', allow all ports
579 // if the whitelisted domain contains a specific port, only allow that port
580 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
581 redirectPort := redirectURL.Port()
582 if (domainPort == "*") ||
583 (domainPort == redirectPort) ||
584 (domainPort == "" && redirectPort == "") {
585 return true
586 }
587 }
588 }
590 return false
591 default:
592 return false
593 }
596// IsWhitelistedRequest is used to check if auth should be skipped for this request
597func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
598 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
599 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
602// IsWhitelistedPath is used to check if the request path is allowed without auth
603func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
604 for _, u := range p.compiledRegex {
605 if u.MatchString(path) {
606 return true
607 }
608 }
609 return false
612func getRemoteAddr(req *http.Request) (s string) {
613 s = req.RemoteAddr
614 if req.Header.Get("X-Real-IP") != "" {
615 s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
616 }
617 return
620func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
621 switch path := req.URL.Path; {
622 case path == p.RobotsPath:
623 p.RobotsTxt(rw)
624 case path == p.PingPath:
625 p.PingPage(rw)
626 case p.IsWhitelistedRequest(req):
627 p.serveMux.ServeHTTP(rw, req)
628 case path == p.SignInPath:
629 p.SignIn(rw, req)
630 case path == p.SignOutPath:
631 p.SignOut(rw, req)
632 case path == p.OAuthStartPath:
633 p.OAuthStart(rw, req)
634 case path == p.OAuthCallbackPath:
635 p.OAuthCallback(rw, req)
636 case path == p.AuthOnlyPath:
637 p.AuthenticateOnly(rw, req)
638 case path == p.UserInfoPath:
639 p.UserInfo(rw, req)
640 default:
641 p.Proxy(rw, req)
642 }
645// SignIn serves a page prompting users to sign in
646func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
647 redirect, err := p.GetRedirect(req)
648 if err != nil {
649 logger.Printf("Error obtaining redirect: %s", err.Error())
650 p.ErrorPage(rw, 500, "Internal Error", err.Error())
651 return
652 }
654 user, ok := p.ManualSignIn(rw, req)
655 if ok {
656 session := &sessionsapi.SessionState{User: user}
657 p.SaveSession(rw, req, session)
658 http.Redirect(rw, req, redirect, 302)
659 } else {
660 if p.SkipProviderButton {
661 p.OAuthStart(rw, req)
662 } else {
663 p.SignInPage(rw, req, http.StatusOK)
664 }
665 }
668//UserInfo endpoint outputs session email in JSON format
669func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
671 session, err := p.getAuthenticatedSession(rw, req)
672 if err != nil {
673 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
674 return
675 }
676 userInfo := struct {
677 Email string `json:"email"`
678 }{session.Email}
679 rw.Header().Set("Content-Type", "application/json")
680 rw.WriteHeader(http.StatusOK)
681 json.NewEncoder(rw).Encode(userInfo)
684// SignOut sends a response to clear the authentication cookie
685func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
686 redirect, err := p.GetRedirect(req)
687 if err != nil {
688 logger.Printf("Error obtaining redirect: %s", err.Error())
689 p.ErrorPage(rw, 500, "Internal Error", err.Error())
690 return
691 }
692 p.ClearSessionCookie(rw, req)
693 http.Redirect(rw, req, redirect, 302)
696// OAuthStart starts the OAuth2 authentication flow
697func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
698 nonce, err := encryption.Nonce()
699 if err != nil {
700 logger.Printf("Error obtaining nonce: %s", err.Error())
701 p.ErrorPage(rw, 500, "Internal Error", err.Error())
702 return
703 }
704 p.SetCSRFCookie(rw, req, nonce)
705 redirect, err := p.GetRedirect(req)
706 if err != nil {
707 logger.Printf("Error obtaining redirect: %s", err.Error())
708 p.ErrorPage(rw, 500, "Internal Error", err.Error())
709 return
710 }
711 redirectURI := p.GetRedirectURI(req.Host)
712 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302)
715// OAuthCallback is the OAuth2 authentication flow callback that finishes the
716// OAuth2 authentication flow
717func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
718 remoteAddr := getRemoteAddr(req)
720 // finish the oauth cycle
721 err := req.ParseForm()
722 if err != nil {
723 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
724 p.ErrorPage(rw, 500, "Internal Error", err.Error())
725 return
726 }
727 errorString := req.Form.Get("error")
728 if errorString != "" {
729 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
730 p.ErrorPage(rw, 403, "Permission Denied", errorString)
731 return
732 }
734 session, err := p.redeemCode(req.Host, req.Form.Get("code"))
735 if err != nil {
736 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
737 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
738 return
739 }
741 s := strings.SplitN(req.Form.Get("state"), ":", 2)
742 if len(s) != 2 {
743 logger.Printf("Error while parsing OAuth2 state: invalid length")
744 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
745 return
746 }
747 nonce := s[0]
748 redirect := s[1]
749 c, err := req.Cookie(p.CSRFCookieName)
750 if err != nil {
751 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
752 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
753 return
754 }
755 p.ClearCSRFCookie(rw, req)
756 if c.Value != nonce {
757 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
758 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
759 return
760 }
762 if !p.IsValidRedirect(redirect) {
763 redirect = "/"
764 }
766 // set cookie, or deny
767 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
768 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
769 err := p.SaveSession(rw, req, session)
770 if err != nil {
771 logger.Printf("%s %s", remoteAddr, err)
772 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
773 return
774 }
775 http.Redirect(rw, req, redirect, 302)
776 } else {
777 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
778 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
779 }
782// AuthenticateOnly checks whether the user is currently logged in
783func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
784 session, err := p.getAuthenticatedSession(rw, req)
785 if err != nil {
786 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
787 return
788 }
790 // we are authenticated
791 p.addHeadersForProxying(rw, req, session)
792 rw.WriteHeader(http.StatusAccepted)
795// Proxy proxies the user request if the user is authenticated else it prompts
796// them to authenticate
797func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
798 session, err := p.getAuthenticatedSession(rw, req)
799 switch err {
800 case nil:
801 // we are authenticated
802 p.addHeadersForProxying(rw, req, session)
803 p.serveMux.ServeHTTP(rw, req)
805 case ErrNeedsLogin:
806 // we need to send the user to a login screen
807 if isAjax(req) {
808 // no point redirecting an AJAX request
809 p.ErrorJSON(rw, http.StatusUnauthorized)
810 return
811 }
813 if p.SkipProviderButton {
814 p.OAuthStart(rw, req)
815 } else {
816 p.SignInPage(rw, req, http.StatusForbidden)
817 }
819 default:
820 // unknown error
821 logger.Printf("Unexpected internal error: %s", err)
822 p.ErrorPage(rw, http.StatusInternalServerError,
823 "Internal Error", "Internal Error")
824 }
828// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
829// Returns nil, ErrNeedsLogin if user needs to login.
830// Set-Cookie headers may be set on the response as a side-effect of calling this method.
831func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
832 var session *sessionsapi.SessionState
833 var err error
834 var saveSession, clearSession, revalidated bool
836 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
837 session, err = p.GetJwtSession(req)
838 if err != nil {
839 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
840 }
841 if session != nil {
842 saveSession = false
843 }
844 }
846 remoteAddr := getRemoteAddr(req)
847 if session == nil {
848 session, err = p.LoadCookiedSession(req)
849 if err != nil {
850 logger.Printf("Error loading cookied session: %s", err)
851 }
853 if session != nil {
854 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
855 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
856 saveSession = true
857 }
859 if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
860 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
861 clearSession = true
862 session = nil
863 } else if ok {
864 saveSession = true
865 revalidated = true
866 }
867 }
868 }
870 if session != nil && session.IsExpired() {
871 logger.Printf("Removing session: token expired %s", session)
872 session = nil
873 saveSession = false
874 clearSession = true
875 }
877 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
878 if !p.provider.ValidateSessionState(session) {
879 logger.Printf("Removing session: error validating %s", session)
880 saveSession = false
881 session = nil
882 clearSession = true
883 }
884 }
886 if session != nil && session.Email != "" {
887 if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
888 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
889 session = nil
890 saveSession = false
891 clearSession = true
892 }
893 }
895 if saveSession && session != nil {
896 err = p.SaveSession(rw, req, session)
897 if err != nil {
898 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
899 return nil, err
900 }
901 }
903 if clearSession {
904 p.ClearSessionCookie(rw, req)
905 }
907 if session == nil {
908 session, err = p.CheckBasicAuth(req)
909 if err != nil {
910 logger.Printf("Error during basic auth validation: %s", err)
911 }
912 }
914 if session == nil {
915 return nil, ErrNeedsLogin
916 }
918 return session, nil
921// addHeadersForProxying adds the appropriate headers the request / response for proxying
922func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
923 if p.PassBasicAuth {
924 req.SetBasicAuth(session.User, p.BasicAuthPassword)
925 req.Header["X-Forwarded-User"] = []string{session.User}
926 if session.Email != "" {
927 req.Header["X-Forwarded-Email"] = []string{session.Email}
928 } else {
929 req.Header.Del("X-Forwarded-Email")
930 }
931 }
933 if p.PassUserHeaders {
934 req.Header["X-Forwarded-User"] = []string{session.User}
935 if session.Email != "" {
936 req.Header["X-Forwarded-Email"] = []string{session.Email}
937 } else {
938 req.Header.Del("X-Forwarded-Email")
939 }
940 }
942 if p.SetXAuthRequest {
943 rw.Header().Set("X-Auth-Request-User", session.User)
944 if session.Email != "" {
945 rw.Header().Set("X-Auth-Request-Email", session.Email)
946 } else {
947 rw.Header().Del("X-Auth-Request-Email")
948 }
950 if p.PassAccessToken {
951 if session.AccessToken != "" {
952 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
953 } else {
954 rw.Header().Del("X-Auth-Request-Access-Token")
955 }
956 }
957 }
959 if p.PassAccessToken {
960 if session.AccessToken != "" {
961 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
962 } else {
963 req.Header.Del("X-Forwarded-Access-Token")
964 }
965 }
967 if p.PassAuthorization {
968 if session.IDToken != "" {
969 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
970 } else {
971 req.Header.Del("Authorization")
972 }
973 }
974 if p.SetAuthorization {
975 if session.IDToken != "" {
976 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
977 } else {
978 rw.Header().Del("Authorization")
979 }
980 }
982 if session.Email == "" {
983 rw.Header().Set("GAP-Auth", session.User)
984 } else {
985 rw.Header().Set("GAP-Auth", session.Email)
986 }
989// CheckBasicAuth checks the requests Authorization header for basic auth
990// credentials and authenticates these against the proxies HtpasswdFile
991func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
992 if p.HtpasswdFile == nil {
993 return nil, nil
994 }
995 auth := req.Header.Get("Authorization")
996 if auth == "" {
997 return nil, nil
998 }
999 s := strings.SplitN(auth, " ", 2)
1000 if len(s) != 2 || s[0] != "Basic" {
1001 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1003 b, err := b64.StdEncoding.DecodeString(s[1])
1004 if err != nil {
1005 return nil, err
1007 pair := strings.SplitN(string(b), ":", 2)
1008 if len(pair) != 2 {
1009 return nil, fmt.Errorf("invalid format %s", b)
1011 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
1012 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1013 return &sessionsapi.SessionState{User: pair[0]}, nil
1015 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1016 return nil, nil
1019// isAjax checks if a request is an ajax request
1020func isAjax(req *http.Request) bool {
1021 acceptValues, ok := req.Header["accept"]
1022 if !ok {
1023 acceptValues = req.Header["Accept"]
1025 const ajaxReq = applicationJSON
1026 for _, v := range acceptValues {
1027 if v == ajaxReq {
1028 return true
1031 return false
1034// ErrorJSON returns the error code with an application/json mime type
1035func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1036 rw.Header().Set("Content-Type", applicationJSON)
1037 rw.WriteHeader(code)
1040// GetJwtSession loads a session based on a JWT token in the authorization header.
1041func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1042 rawBearerToken, err := p.findBearerToken(req)
1043 if err != nil {
1044 return nil, err
1047 ctx := context.Background()
1048 var session *sessionsapi.SessionState
1049 for _, verifier := range p.jwtBearerVerifiers {
1050 bearerToken, err := verifier.Verify(ctx, rawBearerToken)
1052 if err != nil {
1053 logger.Printf("failed to verify bearer token: %v", err)
1054 continue
1057 var claims struct {
1058 Subject string `json:"sub"`
1059 Email string `json:"email"`
1060 Verified *bool `json:"email_verified"`
1063 if err := bearerToken.Claims(&claims); err != nil {
1064 return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
1067 if claims.Email == "" {
1068 claims.Email = claims.Subject
1071 if claims.Verified != nil && !*claims.Verified {
1072 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
1075 session = &sessionsapi.SessionState{
1076 AccessToken: rawBearerToken,
1077 IDToken: rawBearerToken,
1078 RefreshToken: "",
1079 ExpiresOn: bearerToken.Expiry,
1080 Email: claims.Email,
1081 User: claims.Email,
1083 return session, nil
1085 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1088// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1089func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1090 auth := req.Header.Get("Authorization")
1091 s := strings.SplitN(auth, " ", 2)
1092 if len(s) != 2 {
1093 return "", fmt.Errorf("invalid authorization header %s", auth)
1095 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1096 var rawBearerToken string
1097 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1098 rawBearerToken = s[1]
1099 } else if s[0] == "Basic" {
1100 // Check if we have a Bearer token masquerading in Basic
1101 b, err := b64.StdEncoding.DecodeString(s[1])
1102 if err != nil {
1103 return "", err
1105 pair := strings.SplitN(string(b), ":", 2)
1106 if len(pair) != 2 {
1107 return "", fmt.Errorf("invalid format %s", b)
1109 user, password := pair[0], pair[1]
1111 // check user, user+password, or just password for a token
1112 if jwtRegex.MatchString(user) {
1113 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1114 if password == "" || password == "x-oauth-basic" {
1115 rawBearerToken = user
1117 } else if jwtRegex.MatchString(password) {
1118 // support passwords and ignore user
1119 rawBearerToken = password
1122 if rawBearerToken == "" {
1123 return "", fmt.Errorf("no valid bearer token found in authorization header")
1126 return rawBearerToken, nil