git.iko.soy/typeable/oauth2-proxy
on v4.1.0 help sign in
typeable/oauth2-proxy/oauthproxy.go
1069 lines 32 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/encryption"
24 "github.com/pusher/oauth2_proxy/pkg/logger"
25 "github.com/pusher/oauth2_proxy/providers"
26 "github.com/yhat/wsutil"
29const (
30 // SignatureHeader is the name of the request header containing the GAP Signature
31 // Part of hmacauth
32 SignatureHeader = "GAP-Signature"
34 httpScheme = "http"
35 httpsScheme = "https"
37 applicationJSON = "application/json"
40// SignatureHeaders contains the headers to be signed by the hmac algorithm
41// Part of hmacauth
42var SignatureHeaders = []string{
43 "Content-Length",
44 "Content-Md5",
45 "Content-Type",
46 "Date",
47 "Authorization",
48 "X-Forwarded-User",
49 "X-Forwarded-Email",
50 "X-Forwarded-Access-Token",
51 "Cookie",
52 "Gap-Auth",
55var (
56 // ErrNeedsLogin means the user should be redirected to the login page
57 ErrNeedsLogin = errors.New("redirect to login page")
60// OAuthProxy is the main authentication proxy
61type OAuthProxy struct {
62 CookieSeed string
63 CookieName string
64 CSRFCookieName string
65 CookieDomain string
66 CookiePath string
67 CookieSecure bool
68 CookieHTTPOnly bool
69 CookieExpire time.Duration
70 CookieRefresh time.Duration
71 Validator func(string) bool
73 RobotsPath string
74 PingPath string
75 SignInPath string
76 SignOutPath string
77 OAuthStartPath string
78 OAuthCallbackPath string
79 AuthOnlyPath string
80 UserInfoPath string
82 redirectURL *url.URL // the url to receive requests at
83 whitelistDomains []string
84 provider providers.Provider
85 providerNameOverride string
86 sessionStore sessionsapi.SessionStore
87 ProxyPrefix string
88 SignInMessage string
89 HtpasswdFile *HtpasswdFile
90 DisplayHtpasswdForm bool
91 serveMux http.Handler
92 SetXAuthRequest bool
93 PassBasicAuth bool
94 SkipProviderButton bool
95 PassUserHeaders bool
96 BasicAuthPassword string
97 PassAccessToken bool
98 SetAuthorization bool
99 PassAuthorization bool
100 skipAuthRegex []string
101 skipAuthPreflight bool
102 skipJwtBearerTokens bool
103 jwtBearerVerifiers []*oidc.IDTokenVerifier
104 compiledRegex []*regexp.Regexp
105 templates *template.Template
106 Banner string
107 Footer string
110// UpstreamProxy represents an upstream server to proxy to
111type UpstreamProxy struct {
112 upstream string
113 handler http.Handler
114 wsHandler http.Handler
115 auth hmacauth.HmacAuth
118// ServeHTTP proxies requests to the upstream provider while signing the
119// request headers
120func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
121 w.Header().Set("GAP-Upstream-Address", u.upstream)
122 if u.auth != nil {
123 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
124 u.auth.SignRequest(r)
125 }
126 if u.wsHandler != nil && strings.EqualFold(r.Header.Get("Connection"), "upgrade") && r.Header.Get("Upgrade") == "websocket" {
127 u.wsHandler.ServeHTTP(w, r)
128 } else {
129 u.handler.ServeHTTP(w, r)
130 }
134// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
135// servers
136func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) {
137 proxy = httputil.NewSingleHostReverseProxy(target)
138 proxy.FlushInterval = opts.FlushInterval
139 if opts.SSLUpstreamInsecureSkipVerify {
140 proxy.Transport = &http.Transport{
141 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
142 }
143 }
144 return proxy
147func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
148 director := proxy.Director
149 proxy.Director = func(req *http.Request) {
150 director(req)
151 // use RequestURI so that we aren't unescaping encoded slashes in the request path
152 req.Host = target.Host
153 req.URL.Opaque = req.RequestURI
154 req.URL.RawQuery = ""
155 }
158func setProxyDirector(proxy *httputil.ReverseProxy) {
159 director := proxy.Director
160 proxy.Director = func(req *http.Request) {
161 director(req)
162 // use RequestURI so that we aren't unescaping encoded slashes in the request path
163 req.URL.Opaque = req.RequestURI
164 req.URL.RawQuery = ""
165 }
168// NewFileServer creates a http.Handler to serve files from the filesystem
169func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
170 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
173// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
174func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
175 u.Path = ""
176 proxy := NewReverseProxy(u, opts)
177 if !opts.PassHostHeader {
178 setProxyUpstreamHostHeader(proxy, u)
179 } else {
180 setProxyDirector(proxy)
181 }
183 // this should give us a wss:// scheme if the url is https:// based.
184 var wsProxy *wsutil.ReverseProxy
185 if opts.ProxyWebSockets {
186 wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
187 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
188 wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
189 }
190 return &UpstreamProxy{
191 upstream: u.Host,
192 handler: proxy,
193 wsHandler: wsProxy,
194 auth: auth,
195 }
198// NewOAuthProxy creates a new instance of OOuthProxy from the options provided
199func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
200 serveMux := http.NewServeMux()
201 var auth hmacauth.HmacAuth
202 if sigData := opts.signatureData; sigData != nil {
203 auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
204 SignatureHeader, SignatureHeaders)
205 }
206 for _, u := range opts.proxyURLs {
207 path := u.Path
208 host := u.Host
209 switch u.Scheme {
210 case httpScheme, httpsScheme:
211 logger.Printf("mapping path %q => upstream %q", path, u)
212 proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
213 serveMux.Handle(path, proxy)
214 case "static":
215 responseCode, err := strconv.Atoi(host)
216 if err != nil {
217 logger.Printf("unable to convert %q to int, use default \"200\"", host)
218 responseCode = 200
219 }
221 serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
222 rw.WriteHeader(responseCode)
223 fmt.Fprintf(rw, "Authenticated")
224 })
225 case "file":
226 if u.Fragment != "" {
227 path = u.Fragment
228 }
229 logger.Printf("mapping path %q => file system %q", path, u.Path)
230 proxy := NewFileServer(path, u.Path)
231 uProxy := UpstreamProxy{
232 upstream: path,
233 handler: proxy,
234 wsHandler: nil,
235 auth: nil,
236 }
237 serveMux.Handle(path, &uProxy)
238 default:
239 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
240 }
241 }
242 for _, u := range opts.CompiledRegex {
243 logger.Printf("compiled skip-auth-regex => %q", u)
244 }
246 if opts.SkipJwtBearerTokens {
247 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
248 for _, issuer := range opts.ExtraJwtIssuers {
249 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
250 }
251 }
252 redirectURL := opts.redirectURL
253 if redirectURL.Path == "" {
254 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
255 }
257 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID)
258 refresh := "disabled"
259 if opts.CookieRefresh != time.Duration(0) {
260 refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
261 }
263 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, refresh)
265 return &OAuthProxy{
266 CookieName: opts.CookieName,
267 CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
268 CookieSeed: opts.CookieSecret,
269 CookieDomain: opts.CookieDomain,
270 CookiePath: opts.CookiePath,
271 CookieSecure: opts.CookieSecure,
272 CookieHTTPOnly: opts.CookieHTTPOnly,
273 CookieExpire: opts.CookieExpire,
274 CookieRefresh: opts.CookieRefresh,
275 Validator: validator,
277 RobotsPath: "/robots.txt",
278 PingPath: opts.PingPath,
279 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
280 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
281 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
282 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
283 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
284 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
286 ProxyPrefix: opts.ProxyPrefix,
287 provider: opts.provider,
288 providerNameOverride: opts.ProviderName,
289 sessionStore: opts.sessionStore,
290 serveMux: serveMux,
291 redirectURL: redirectURL,
292 whitelistDomains: opts.WhitelistDomains,
293 skipAuthRegex: opts.SkipAuthRegex,
294 skipAuthPreflight: opts.SkipAuthPreflight,
295 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
296 jwtBearerVerifiers: opts.jwtBearerVerifiers,
297 compiledRegex: opts.CompiledRegex,
298 SetXAuthRequest: opts.SetXAuthRequest,
299 PassBasicAuth: opts.PassBasicAuth,
300 PassUserHeaders: opts.PassUserHeaders,
301 BasicAuthPassword: opts.BasicAuthPassword,
302 PassAccessToken: opts.PassAccessToken,
303 SetAuthorization: opts.SetAuthorization,
304 PassAuthorization: opts.PassAuthorization,
305 SkipProviderButton: opts.SkipProviderButton,
306 templates: loadTemplates(opts.CustomTemplatesDir),
307 Banner: opts.Banner,
308 Footer: opts.Footer,
309 }
312// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
313// redirect clients to once authenticated
314func (p *OAuthProxy) GetRedirectURI(host string) string {
315 // default to the request Host if not set
316 if p.redirectURL.Host != "" {
317 return p.redirectURL.String()
318 }
319 var u url.URL
320 u = *p.redirectURL
321 if u.Scheme == "" {
322 if p.CookieSecure {
323 u.Scheme = httpsScheme
324 } else {
325 u.Scheme = httpScheme
326 }
327 }
328 u.Host = host
329 return u.String()
332func (p *OAuthProxy) displayCustomLoginForm() bool {
333 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
336func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState, err error) {
337 if code == "" {
338 return nil, errors.New("missing code")
339 }
340 redirectURI := p.GetRedirectURI(host)
341 s, err = p.provider.Redeem(redirectURI, code)
342 if err != nil {
343 return
344 }
346 if s.Email == "" {
347 s.Email, err = p.provider.GetEmailAddress(s)
348 }
350 if s.User == "" {
351 s.User, err = p.provider.GetUserName(s)
352 if err != nil && err.Error() == "not implemented" {
353 err = nil
354 }
355 }
356 return
359// MakeCSRFCookie creates a cookie for CSRF
360func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
361 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
364func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
365 if p.CookieDomain != "" {
366 domain := req.Host
367 if h, _, err := net.SplitHostPort(domain); err == nil {
368 domain = h
369 }
370 if !strings.HasSuffix(domain, p.CookieDomain) {
371 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain)
372 }
373 }
375 return &http.Cookie{
376 Name: name,
377 Value: value,
378 Path: p.CookiePath,
379 Domain: p.CookieDomain,
380 HttpOnly: p.CookieHTTPOnly,
381 Secure: p.CookieSecure,
382 Expires: now.Add(expiration),
383 }
386// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
387// session
388func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
389 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
392// SetCSRFCookie adds a CSRF cookie to the response
393func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
394 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
397// ClearSessionCookie creates a cookie to unset the user's authentication cookie
398// stored in the user's session
399func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
400 return p.sessionStore.Clear(rw, req)
403// LoadCookiedSession reads the user's authentication details from the request
404func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
405 return p.sessionStore.Load(req)
408// SaveSession creates a new session cookie value and sets this on the response
409func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
410 return p.sessionStore.Save(rw, req, s)
413// RobotsTxt disallows scraping pages from the OAuthProxy
414func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
415 rw.WriteHeader(http.StatusOK)
416 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
419// PingPage responds 200 OK to requests
420func (p *OAuthProxy) PingPage(rw http.ResponseWriter) {
421 rw.WriteHeader(http.StatusOK)
422 fmt.Fprintf(rw, "OK")
425// ErrorPage writes an error response
426func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
427 rw.WriteHeader(code)
428 t := struct {
429 Title string
430 Message string
431 ProxyPrefix string
432 }{
433 Title: fmt.Sprintf("%d %s", code, title),
434 Message: message,
435 ProxyPrefix: p.ProxyPrefix,
436 }
437 p.templates.ExecuteTemplate(rw, "error.html", t)
440// SignInPage writes the sing in template to the response
441func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
442 p.ClearSessionCookie(rw, req)
443 rw.WriteHeader(code)
445 redirecURL := req.URL.RequestURI()
446 if req.Header.Get("X-Auth-Request-Redirect") != "" {
447 redirecURL = req.Header.Get("X-Auth-Request-Redirect")
448 }
449 if redirecURL == p.SignInPath {
450 redirecURL = "/"
451 }
453 t := struct {
454 ProviderName string
455 SignInMessage string
456 CustomLogin bool
457 Redirect string
458 Version string
459 ProxyPrefix string
460 Footer template.HTML
461 }{
462 ProviderName: p.provider.Data().ProviderName,
463 SignInMessage: p.SignInMessage,
464 CustomLogin: p.displayCustomLoginForm(),
465 Redirect: redirecURL,
466 Version: VERSION,
467 ProxyPrefix: p.ProxyPrefix,
468 Footer: template.HTML(p.Footer),
469 }
470 if p.providerNameOverride != "" {
471 t.ProviderName = p.providerNameOverride
472 }
473 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
476// ManualSignIn handles basic auth logins to the proxy
477func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
478 if req.Method != "POST" || p.HtpasswdFile == nil {
479 return "", false
480 }
481 user := req.FormValue("username")
482 passwd := req.FormValue("password")
483 if user == "" {
484 return "", false
485 }
486 // check auth
487 if p.HtpasswdFile.Validate(user, passwd) {
488 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
489 return user, true
490 }
491 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
492 return "", false
495// GetRedirect reads the query parameter to get the URL to redirect clients to
496// once authenticated with the OAuthProxy
497func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
498 err = req.ParseForm()
499 if err != nil {
500 return
501 }
503 redirect = req.Header.Get("X-Auth-Request-Redirect")
504 if req.Form.Get("rd") != "" {
505 redirect = req.Form.Get("rd")
506 }
507 if !p.IsValidRedirect(redirect) {
508 redirect = req.URL.Path
509 if strings.HasPrefix(redirect, p.ProxyPrefix) {
510 redirect = "/"
511 }
512 }
514 return
517// IsValidRedirect checks whether the redirect URL is whitelisted
518func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
519 switch {
520 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//"):
521 return true
522 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
523 redirectURL, err := url.Parse(redirect)
524 if err != nil {
525 return false
526 }
527 for _, domain := range p.whitelistDomains {
528 if (redirectURL.Host == domain) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectURL.Host, domain)) {
529 return true
530 }
531 }
532 return false
533 default:
534 return false
535 }
538// IsWhitelistedRequest is used to check if auth should be skipped for this request
539func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
540 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
541 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
544// IsWhitelistedPath is used to check if the request path is allowed without auth
545func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
546 for _, u := range p.compiledRegex {
547 if u.MatchString(path) {
548 return true
549 }
550 }
551 return false
554func getRemoteAddr(req *http.Request) (s string) {
555 s = req.RemoteAddr
556 if req.Header.Get("X-Real-IP") != "" {
557 s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
558 }
559 return
562func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
563 switch path := req.URL.Path; {
564 case path == p.RobotsPath:
565 p.RobotsTxt(rw)
566 case path == p.PingPath:
567 p.PingPage(rw)
568 case p.IsWhitelistedRequest(req):
569 p.serveMux.ServeHTTP(rw, req)
570 case path == p.SignInPath:
571 p.SignIn(rw, req)
572 case path == p.SignOutPath:
573 p.SignOut(rw, req)
574 case path == p.OAuthStartPath:
575 p.OAuthStart(rw, req)
576 case path == p.OAuthCallbackPath:
577 p.OAuthCallback(rw, req)
578 case path == p.AuthOnlyPath:
579 p.AuthenticateOnly(rw, req)
580 case path == p.UserInfoPath:
581 p.UserInfo(rw, req)
582 default:
583 p.Proxy(rw, req)
584 }
587// SignIn serves a page prompting users to sign in
588func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
589 redirect, err := p.GetRedirect(req)
590 if err != nil {
591 logger.Printf("Error obtaining redirect: %s", err.Error())
592 p.ErrorPage(rw, 500, "Internal Error", err.Error())
593 return
594 }
596 user, ok := p.ManualSignIn(rw, req)
597 if ok {
598 session := &sessionsapi.SessionState{User: user}
599 p.SaveSession(rw, req, session)
600 http.Redirect(rw, req, redirect, 302)
601 } else {
602 if p.SkipProviderButton {
603 p.OAuthStart(rw, req)
604 } else {
605 p.SignInPage(rw, req, http.StatusOK)
606 }
607 }
610//UserInfo endpoint outputs session email in JSON format
611func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
613 session, err := p.getAuthenticatedSession(rw, req)
614 if err != nil {
615 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
616 return
617 }
618 userInfo := struct {
619 Email string `json:"email"`
620 }{session.Email}
621 rw.Header().Set("Content-Type", "application/json")
622 rw.WriteHeader(http.StatusOK)
623 json.NewEncoder(rw).Encode(userInfo)
626// SignOut sends a response to clear the authentication cookie
627func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
628 redirect, err := p.GetRedirect(req)
629 if err != nil {
630 logger.Printf("Error obtaining redirect: %s", err.Error())
631 p.ErrorPage(rw, 500, "Internal Error", err.Error())
632 return
633 }
634 p.ClearSessionCookie(rw, req)
635 http.Redirect(rw, req, redirect, 302)
638// OAuthStart starts the OAuth2 authentication flow
639func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
640 nonce, err := encryption.Nonce()
641 if err != nil {
642 logger.Printf("Error obtaining nonce: %s", err.Error())
643 p.ErrorPage(rw, 500, "Internal Error", err.Error())
644 return
645 }
646 p.SetCSRFCookie(rw, req, nonce)
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 }
653 redirectURI := p.GetRedirectURI(req.Host)
654 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302)
657// OAuthCallback is the OAuth2 authentication flow callback that finishes the
658// OAuth2 authentication flow
659func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
660 remoteAddr := getRemoteAddr(req)
662 // finish the oauth cycle
663 err := req.ParseForm()
664 if err != nil {
665 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
666 p.ErrorPage(rw, 500, "Internal Error", err.Error())
667 return
668 }
669 errorString := req.Form.Get("error")
670 if errorString != "" {
671 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
672 p.ErrorPage(rw, 403, "Permission Denied", errorString)
673 return
674 }
676 session, err := p.redeemCode(req.Host, req.Form.Get("code"))
677 if err != nil {
678 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
679 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
680 return
681 }
683 s := strings.SplitN(req.Form.Get("state"), ":", 2)
684 if len(s) != 2 {
685 logger.Printf("Error while parsing OAuth2 state: invalid length")
686 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
687 return
688 }
689 nonce := s[0]
690 redirect := s[1]
691 c, err := req.Cookie(p.CSRFCookieName)
692 if err != nil {
693 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
694 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
695 return
696 }
697 p.ClearCSRFCookie(rw, req)
698 if c.Value != nonce {
699 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
700 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
701 return
702 }
704 if !p.IsValidRedirect(redirect) {
705 redirect = "/"
706 }
708 // set cookie, or deny
709 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
710 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
711 err := p.SaveSession(rw, req, session)
712 if err != nil {
713 logger.Printf("%s %s", remoteAddr, err)
714 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
715 return
716 }
717 http.Redirect(rw, req, redirect, 302)
718 } else {
719 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
720 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
721 }
724// AuthenticateOnly checks whether the user is currently logged in
725func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
726 session, err := p.getAuthenticatedSession(rw, req)
727 if err != nil {
728 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
729 return
730 }
732 // we are authenticated
733 p.addHeadersForProxying(rw, req, session)
734 rw.WriteHeader(http.StatusAccepted)
737// Proxy proxies the user request if the user is authenticated else it prompts
738// them to authenticate
739func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
740 session, err := p.getAuthenticatedSession(rw, req)
741 switch err {
742 case nil:
743 // we are authenticated
744 p.addHeadersForProxying(rw, req, session)
745 p.serveMux.ServeHTTP(rw, req)
747 case ErrNeedsLogin:
748 // we need to send the user to a login screen
749 if isAjax(req) {
750 // no point redirecting an AJAX request
751 p.ErrorJSON(rw, http.StatusUnauthorized)
752 return
753 }
755 if p.SkipProviderButton {
756 p.OAuthStart(rw, req)
757 } else {
758 p.SignInPage(rw, req, http.StatusForbidden)
759 }
761 default:
762 // unknown error
763 logger.Printf("Unexpected internal error: %s", err)
764 p.ErrorPage(rw, http.StatusInternalServerError,
765 "Internal Error", "Internal Error")
766 }
770// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
771// Returns nil, ErrNeedsLogin if user needs to login.
772// Set-Cookie headers may be set on the response as a side-effect of calling this method.
773func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
774 var session *sessionsapi.SessionState
775 var err error
776 var saveSession, clearSession, revalidated bool
778 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
779 session, err = p.GetJwtSession(req)
780 if err != nil {
781 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
782 }
783 if session != nil {
784 saveSession = false
785 }
786 }
788 remoteAddr := getRemoteAddr(req)
789 if session == nil {
790 session, err = p.LoadCookiedSession(req)
791 if err != nil {
792 logger.Printf("Error loading cookied session: %s", err)
793 }
795 if session != nil {
796 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
797 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
798 saveSession = true
799 }
801 if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
802 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
803 clearSession = true
804 session = nil
805 } else if ok {
806 saveSession = true
807 revalidated = true
808 }
809 }
810 }
812 if session != nil && session.IsExpired() {
813 logger.Printf("Removing session: token expired %s", session)
814 session = nil
815 saveSession = false
816 clearSession = true
817 }
819 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
820 if !p.provider.ValidateSessionState(session) {
821 logger.Printf("Removing session: error validating %s", session)
822 saveSession = false
823 session = nil
824 clearSession = true
825 }
826 }
828 if session != nil && session.Email != "" {
829 if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
830 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
831 session = nil
832 saveSession = false
833 clearSession = true
834 }
835 }
837 if saveSession && session != nil {
838 err = p.SaveSession(rw, req, session)
839 if err != nil {
840 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
841 return nil, err
842 }
843 }
845 if clearSession {
846 p.ClearSessionCookie(rw, req)
847 }
849 if session == nil {
850 session, err = p.CheckBasicAuth(req)
851 if err != nil {
852 logger.Printf("Error during basic auth validation: %s", err)
853 }
854 }
856 if session == nil {
857 return nil, ErrNeedsLogin
858 }
860 return session, nil
863// addHeadersForProxying adds the appropriate headers the request / response for proxying
864func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
865 if p.PassBasicAuth {
866 req.SetBasicAuth(session.User, p.BasicAuthPassword)
867 req.Header["X-Forwarded-User"] = []string{session.User}
868 if session.Email != "" {
869 req.Header["X-Forwarded-Email"] = []string{session.Email}
870 } else {
871 req.Header.Del("X-Forwarded-Email")
872 }
873 }
875 if p.PassUserHeaders {
876 req.Header["X-Forwarded-User"] = []string{session.User}
877 if session.Email != "" {
878 req.Header["X-Forwarded-Email"] = []string{session.Email}
879 } else {
880 req.Header.Del("X-Forwarded-Email")
881 }
882 }
884 if p.SetXAuthRequest {
885 rw.Header().Set("X-Auth-Request-User", session.User)
886 if session.Email != "" {
887 rw.Header().Set("X-Auth-Request-Email", session.Email)
888 } else {
889 rw.Header().Del("X-Auth-Request-Email")
890 }
892 if p.PassAccessToken {
893 if session.AccessToken != "" {
894 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
895 } else {
896 rw.Header().Del("X-Auth-Request-Access-Token")
897 }
898 }
899 }
901 if p.PassAccessToken {
902 if session.AccessToken != "" {
903 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
904 } else {
905 req.Header.Del("X-Forwarded-Access-Token")
906 }
907 }
909 if p.PassAuthorization {
910 if session.IDToken != "" {
911 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
912 } else {
913 req.Header.Del("Authorization")
914 }
915 }
916 if p.SetAuthorization {
917 if session.IDToken != "" {
918 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
919 } else {
920 rw.Header().Del("Authorization")
921 }
922 }
924 if session.Email == "" {
925 rw.Header().Set("GAP-Auth", session.User)
926 } else {
927 rw.Header().Set("GAP-Auth", session.Email)
928 }
931// CheckBasicAuth checks the requests Authorization header for basic auth
932// credentials and authenticates these against the proxies HtpasswdFile
933func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
934 if p.HtpasswdFile == nil {
935 return nil, nil
936 }
937 auth := req.Header.Get("Authorization")
938 if auth == "" {
939 return nil, nil
940 }
941 s := strings.SplitN(auth, " ", 2)
942 if len(s) != 2 || s[0] != "Basic" {
943 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
944 }
945 b, err := b64.StdEncoding.DecodeString(s[1])
946 if err != nil {
947 return nil, err
948 }
949 pair := strings.SplitN(string(b), ":", 2)
950 if len(pair) != 2 {
951 return nil, fmt.Errorf("invalid format %s", b)
952 }
953 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
954 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
955 return &sessionsapi.SessionState{User: pair[0]}, nil
956 }
957 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
958 return nil, nil
961// isAjax checks if a request is an ajax request
962func isAjax(req *http.Request) bool {
963 acceptValues, ok := req.Header["accept"]
964 if !ok {
965 acceptValues = req.Header["Accept"]
966 }
967 const ajaxReq = applicationJSON
968 for _, v := range acceptValues {
969 if v == ajaxReq {
970 return true
971 }
972 }
973 return false
976// ErrorJSON returns the error code with an application/json mime type
977func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
978 rw.Header().Set("Content-Type", applicationJSON)
979 rw.WriteHeader(code)
982// GetJwtSession loads a session based on a JWT token in the authorization header.
983func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
984 rawBearerToken, err := p.findBearerToken(req)
985 if err != nil {
986 return nil, err
987 }
989 ctx := context.Background()
990 var session *sessionsapi.SessionState
991 for _, verifier := range p.jwtBearerVerifiers {
992 bearerToken, err := verifier.Verify(ctx, rawBearerToken)
994 if err != nil {
995 logger.Printf("failed to verify bearer token: %v", err)
996 continue
997 }
999 var claims struct {
1000 Subject string `json:"sub"`
1001 Email string `json:"email"`
1002 Verified *bool `json:"email_verified"`
1005 if err := bearerToken.Claims(&claims); err != nil {
1006 return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
1009 if claims.Email == "" {
1010 claims.Email = claims.Subject
1013 if claims.Verified != nil && !*claims.Verified {
1014 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
1017 session = &sessionsapi.SessionState{
1018 AccessToken: rawBearerToken,
1019 IDToken: rawBearerToken,
1020 RefreshToken: "",
1021 ExpiresOn: bearerToken.Expiry,
1022 Email: claims.Email,
1023 User: claims.Email,
1025 return session, nil
1027 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1030// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1031func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1032 auth := req.Header.Get("Authorization")
1033 s := strings.SplitN(auth, " ", 2)
1034 if len(s) != 2 {
1035 return "", fmt.Errorf("invalid authorization header %s", auth)
1037 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1038 var rawBearerToken string
1039 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1040 rawBearerToken = s[1]
1041 } else if s[0] == "Basic" {
1042 // Check if we have a Bearer token masquerading in Basic
1043 b, err := b64.StdEncoding.DecodeString(s[1])
1044 if err != nil {
1045 return "", err
1047 pair := strings.SplitN(string(b), ":", 2)
1048 if len(pair) != 2 {
1049 return "", fmt.Errorf("invalid format %s", b)
1051 user, password := pair[0], pair[1]
1053 // check user, user+password, or just password for a token
1054 if jwtRegex.MatchString(user) {
1055 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1056 if password == "" || password == "x-oauth-basic" {
1057 rawBearerToken = user
1059 } else if jwtRegex.MatchString(password) {
1060 // support passwords and ignore user
1061 rawBearerToken = password
1064 if rawBearerToken == "" {
1065 return "", fmt.Errorf("no valid bearer token found in authorization header")
1068 return rawBearerToken, nil