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