git.iko.soy/typeable/oauth2-proxy
on 6cbc339 help sign in
typeable/oauth2-proxy/oauthproxy.go
641 lines 17 kB history raw
1package main
3import (
4 b64 "encoding/base64"
5 "errors"
6 "fmt"
7 "html/template"
8 "log"
9 "net"
10 "net/http"
11 "net/http/httputil"
12 "net/url"
13 "regexp"
14 "strings"
15 "time"
17 "github.com/18F/hmacauth"
18 "github.com/bitly/oauth2_proxy/cookie"
19 "github.com/bitly/oauth2_proxy/providers"
22const SignatureHeader = "GAP-Signature"
24var SignatureHeaders []string = []string{
25 "Content-Length",
26 "Content-Md5",
27 "Content-Type",
28 "Date",
29 "Authorization",
30 "X-Forwarded-User",
31 "X-Forwarded-Email",
32 "X-Forwarded-Access-Token",
33 "Cookie",
34 "Gap-Auth",
37type OAuthProxy struct {
38 CookieSeed string
39 CookieName string
40 CookieDomain string
41 CookieSecure bool
42 CookieHttpOnly bool
43 CookieExpire time.Duration
44 CookieRefresh time.Duration
45 Validator func(string) bool
47 RobotsPath string
48 PingPath string
49 SignInPath string
50 OAuthStartPath string
51 OAuthCallbackPath string
52 AuthOnlyPath string
54 redirectURL *url.URL // the url to receive requests at
55 provider providers.Provider
56 ProxyPrefix string
57 SignInMessage string
58 HtpasswdFile *HtpasswdFile
59 DisplayHtpasswdForm bool
60 serveMux http.Handler
61 PassBasicAuth bool
62 SkipProviderButton bool
63 BasicAuthPassword string
64 PassAccessToken bool
65 CookieCipher *cookie.Cipher
66 skipAuthRegex []string
67 compiledRegex []*regexp.Regexp
68 templates *template.Template
69 Footer string
72type UpstreamProxy struct {
73 upstream string
74 handler http.Handler
75 auth hmacauth.HmacAuth
78func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
79 w.Header().Set("GAP-Upstream-Address", u.upstream)
80 if u.auth != nil {
81 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
82 u.auth.SignRequest(r)
83 }
84 u.handler.ServeHTTP(w, r)
87func NewReverseProxy(target *url.URL) (proxy *httputil.ReverseProxy) {
88 return httputil.NewSingleHostReverseProxy(target)
90func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
91 director := proxy.Director
92 proxy.Director = func(req *http.Request) {
93 director(req)
94 // use RequestURI so that we aren't unescaping encoded slashes in the request path
95 req.Host = target.Host
96 req.URL.Opaque = req.RequestURI
97 req.URL.RawQuery = ""
98 }
100func setProxyDirector(proxy *httputil.ReverseProxy) {
101 director := proxy.Director
102 proxy.Director = func(req *http.Request) {
103 director(req)
104 // use RequestURI so that we aren't unescaping encoded slashes in the request path
105 req.URL.Opaque = req.RequestURI
106 req.URL.RawQuery = ""
107 }
109func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
110 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
113func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
114 serveMux := http.NewServeMux()
115 var auth hmacauth.HmacAuth
116 if sigData := opts.signatureData; sigData != nil {
117 auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
118 SignatureHeader, SignatureHeaders)
119 }
120 for _, u := range opts.proxyURLs {
121 path := u.Path
122 switch u.Scheme {
123 case "http", "https":
124 u.Path = ""
125 log.Printf("mapping path %q => upstream %q", path, u)
126 proxy := NewReverseProxy(u)
127 if !opts.PassHostHeader {
128 setProxyUpstreamHostHeader(proxy, u)
129 } else {
130 setProxyDirector(proxy)
131 }
132 serveMux.Handle(path,
133 &UpstreamProxy{u.Host, proxy, auth})
134 case "file":
135 if u.Fragment != "" {
136 path = u.Fragment
137 }
138 log.Printf("mapping path %q => file system %q", path, u.Path)
139 proxy := NewFileServer(path, u.Path)
140 serveMux.Handle(path, &UpstreamProxy{path, proxy, nil})
141 default:
142 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
143 }
144 }
145 for _, u := range opts.CompiledRegex {
146 log.Printf("compiled skip-auth-regex => %q", u)
147 }
149 redirectURL := opts.redirectURL
150 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
152 log.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID)
153 domain := opts.CookieDomain
154 if domain == "" {
155 domain = "<default>"
156 }
157 refresh := "disabled"
158 if opts.CookieRefresh != time.Duration(0) {
159 refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
160 }
162 log.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain, refresh)
164 var cipher *cookie.Cipher
165 if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
166 var err error
167 cipher, err = cookie.NewCipher(secretBytes(opts.CookieSecret))
168 if err != nil {
169 log.Fatal("cookie-secret error: ", err)
170 }
171 }
173 return &OAuthProxy{
174 CookieName: opts.CookieName,
175 CookieSeed: opts.CookieSecret,
176 CookieDomain: opts.CookieDomain,
177 CookieSecure: opts.CookieSecure,
178 CookieHttpOnly: opts.CookieHttpOnly,
179 CookieExpire: opts.CookieExpire,
180 CookieRefresh: opts.CookieRefresh,
181 Validator: validator,
183 RobotsPath: "/robots.txt",
184 PingPath: "/ping",
185 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
186 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
187 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
188 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
190 ProxyPrefix: opts.ProxyPrefix,
191 provider: opts.provider,
192 serveMux: serveMux,
193 redirectURL: redirectURL,
194 skipAuthRegex: opts.SkipAuthRegex,
195 compiledRegex: opts.CompiledRegex,
196 PassBasicAuth: opts.PassBasicAuth,
197 BasicAuthPassword: opts.BasicAuthPassword,
198 PassAccessToken: opts.PassAccessToken,
199 SkipProviderButton: opts.SkipProviderButton,
200 CookieCipher: cipher,
201 templates: loadTemplates(opts.CustomTemplatesDir),
202 Footer: opts.Footer,
203 }
206func (p *OAuthProxy) GetRedirectURI(host string) string {
207 // default to the request Host if not set
208 if p.redirectURL.Host != "" {
209 return p.redirectURL.String()
210 }
211 var u url.URL
212 u = *p.redirectURL
213 if u.Scheme == "" {
214 if p.CookieSecure {
215 u.Scheme = "https"
216 } else {
217 u.Scheme = "http"
218 }
219 }
220 u.Host = host
221 return u.String()
224func (p *OAuthProxy) displayCustomLoginForm() bool {
225 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
228func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, err error) {
229 if code == "" {
230 return nil, errors.New("missing code")
231 }
232 redirectURI := p.GetRedirectURI(host)
233 s, err = p.provider.Redeem(redirectURI, code)
234 if err != nil {
235 return
236 }
238 if s.Email == "" {
239 s.Email, err = p.provider.GetEmailAddress(s)
240 }
241 return
244func (p *OAuthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
245 domain := req.Host
246 if h, _, err := net.SplitHostPort(domain); err == nil {
247 domain = h
248 }
249 if p.CookieDomain != "" {
250 if !strings.HasSuffix(domain, p.CookieDomain) {
251 log.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain)
252 }
253 domain = p.CookieDomain
254 }
256 if value != "" {
257 value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
258 }
259 return &http.Cookie{
260 Name: p.CookieName,
261 Value: value,
262 Path: "/",
263 Domain: domain,
264 HttpOnly: p.CookieHttpOnly,
265 Secure: p.CookieSecure,
266 Expires: now.Add(expiration),
267 }
270func (p *OAuthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
271 http.SetCookie(rw, p.MakeCookie(req, "", time.Hour*-1, time.Now()))
274func (p *OAuthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
275 http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire, time.Now()))
278func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) {
279 var age time.Duration
280 c, err := req.Cookie(p.CookieName)
281 if err != nil {
282 // always http.ErrNoCookie
283 return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
284 }
285 val, timestamp, ok := cookie.Validate(c, p.CookieSeed, p.CookieExpire)
286 if !ok {
287 return nil, age, errors.New("Cookie Signature not valid")
288 }
290 session, err := p.provider.SessionFromCookie(val, p.CookieCipher)
291 if err != nil {
292 return nil, age, err
293 }
295 age = time.Now().Truncate(time.Second).Sub(timestamp)
296 return session, age, nil
299func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *providers.SessionState) error {
300 value, err := p.provider.CookieForSession(s, p.CookieCipher)
301 if err != nil {
302 return err
303 }
304 p.SetCookie(rw, req, value)
305 return nil
308func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
309 rw.WriteHeader(http.StatusOK)
310 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
313func (p *OAuthProxy) PingPage(rw http.ResponseWriter) {
314 rw.WriteHeader(http.StatusOK)
315 fmt.Fprintf(rw, "OK")
318func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
319 log.Printf("ErrorPage %d %s %s", code, title, message)
320 rw.WriteHeader(code)
321 t := struct {
322 Title string
323 Message string
324 ProxyPrefix string
325 }{
326 Title: fmt.Sprintf("%d %s", code, title),
327 Message: message,
328 ProxyPrefix: p.ProxyPrefix,
329 }
330 p.templates.ExecuteTemplate(rw, "error.html", t)
333func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
334 p.ClearCookie(rw, req)
335 rw.WriteHeader(code)
337 redirect_url := req.URL.RequestURI()
338 if redirect_url == p.SignInPath {
339 redirect_url = "/"
340 }
342 t := struct {
343 ProviderName string
344 SignInMessage string
345 CustomLogin bool
346 Redirect string
347 Version string
348 ProxyPrefix string
349 Footer template.HTML
350 }{
351 ProviderName: p.provider.Data().ProviderName,
352 SignInMessage: p.SignInMessage,
353 CustomLogin: p.displayCustomLoginForm(),
354 Redirect: redirect_url,
355 Version: VERSION,
356 ProxyPrefix: p.ProxyPrefix,
357 Footer: template.HTML(p.Footer),
358 }
359 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
362func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
363 if req.Method != "POST" || p.HtpasswdFile == nil {
364 return "", false
365 }
366 user := req.FormValue("username")
367 passwd := req.FormValue("password")
368 if user == "" {
369 return "", false
370 }
371 // check auth
372 if p.HtpasswdFile.Validate(user, passwd) {
373 log.Printf("authenticated %q via HtpasswdFile", user)
374 return user, true
375 }
376 return "", false
379func (p *OAuthProxy) GetRedirect(req *http.Request) (string, error) {
380 err := req.ParseForm()
382 if err != nil {
383 return "", err
384 }
386 redirect := req.FormValue("rd")
388 if redirect == "" {
389 redirect = "/"
390 }
392 return redirect, err
395func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) {
396 for _, u := range p.compiledRegex {
397 ok = u.MatchString(path)
398 if ok {
399 return
400 }
401 }
402 return
405func getRemoteAddr(req *http.Request) (s string) {
406 s = req.RemoteAddr
407 if req.Header.Get("X-Real-IP") != "" {
408 s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
409 }
410 return
413func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
414 switch path := req.URL.Path; {
415 case path == p.RobotsPath:
416 p.RobotsTxt(rw)
417 case path == p.PingPath:
418 p.PingPage(rw)
419 case p.IsWhitelistedPath(path):
420 p.serveMux.ServeHTTP(rw, req)
421 case path == p.SignInPath:
422 p.SignIn(rw, req)
423 case path == p.OAuthStartPath:
424 p.OAuthStart(rw, req)
425 case path == p.OAuthCallbackPath:
426 p.OAuthCallback(rw, req)
427 case path == p.AuthOnlyPath:
428 p.AuthenticateOnly(rw, req)
429 default:
430 p.Proxy(rw, req)
431 }
434func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
435 redirect, err := p.GetRedirect(req)
436 if err != nil {
437 p.ErrorPage(rw, 500, "Internal Error", err.Error())
438 return
439 }
441 user, ok := p.ManualSignIn(rw, req)
442 if ok {
443 session := &providers.SessionState{User: user}
444 p.SaveSession(rw, req, session)
445 http.Redirect(rw, req, redirect, 302)
446 } else {
447 p.SignInPage(rw, req, 200)
448 }
451func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
452 redirect, err := p.GetRedirect(req)
453 if err != nil {
454 p.ErrorPage(rw, 500, "Internal Error", err.Error())
455 return
456 }
457 redirectURI := p.GetRedirectURI(req.Host)
458 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, redirect), 302)
461func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
462 remoteAddr := getRemoteAddr(req)
464 // finish the oauth cycle
465 err := req.ParseForm()
466 if err != nil {
467 p.ErrorPage(rw, 500, "Internal Error", err.Error())
468 return
469 }
470 errorString := req.Form.Get("error")
471 if errorString != "" {
472 p.ErrorPage(rw, 403, "Permission Denied", errorString)
473 return
474 }
476 session, err := p.redeemCode(req.Host, req.Form.Get("code"))
477 if err != nil {
478 log.Printf("%s error redeeming code %s", remoteAddr, err)
479 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
480 return
481 }
483 redirect := req.Form.Get("state")
484 if !strings.HasPrefix(redirect, "/") {
485 redirect = "/"
486 }
488 // set cookie, or deny
489 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
490 log.Printf("%s authentication complete %s", remoteAddr, session)
491 err := p.SaveSession(rw, req, session)
492 if err != nil {
493 log.Printf("%s %s", remoteAddr, err)
494 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
495 return
496 }
497 http.Redirect(rw, req, redirect, 302)
498 } else {
499 log.Printf("%s Permission Denied: %q is unauthorized", remoteAddr, session.Email)
500 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
501 }
504func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
505 status := p.Authenticate(rw, req)
506 if status == http.StatusAccepted {
507 rw.WriteHeader(http.StatusAccepted)
508 } else {
509 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
510 }
513func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
514 status := p.Authenticate(rw, req)
515 if status == http.StatusInternalServerError {
516 p.ErrorPage(rw, http.StatusInternalServerError,
517 "Internal Error", "Internal Error")
518 } else if status == http.StatusForbidden {
519 if p.SkipProviderButton {
520 p.OAuthStart(rw, req)
521 } else {
522 p.SignInPage(rw, req, http.StatusForbidden)
523 }
524 } else {
525 p.serveMux.ServeHTTP(rw, req)
526 }
529func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
530 var saveSession, clearSession, revalidated bool
531 remoteAddr := getRemoteAddr(req)
533 session, sessionAge, err := p.LoadCookiedSession(req)
534 if err != nil {
535 log.Printf("%s %s", remoteAddr, err)
536 }
537 if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
538 log.Printf("%s refreshing %s old session cookie for %s (refresh after %s)", remoteAddr, sessionAge, session, p.CookieRefresh)
539 saveSession = true
540 }
542 if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
543 log.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
544 clearSession = true
545 session = nil
546 } else if ok {
547 saveSession = true
548 revalidated = true
549 }
551 if session != nil && session.IsExpired() {
552 log.Printf("%s removing session. token expired %s", remoteAddr, session)
553 session = nil
554 saveSession = false
555 clearSession = true
556 }
558 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
559 if !p.provider.ValidateSessionState(session) {
560 log.Printf("%s removing session. error validating %s", remoteAddr, session)
561 saveSession = false
562 session = nil
563 clearSession = true
564 }
565 }
567 if session != nil && session.Email != "" && !p.Validator(session.Email) {
568 log.Printf("%s Permission Denied: removing session %s", remoteAddr, session)
569 session = nil
570 saveSession = false
571 clearSession = true
572 }
574 if saveSession && session != nil {
575 err := p.SaveSession(rw, req, session)
576 if err != nil {
577 log.Printf("%s %s", remoteAddr, err)
578 return http.StatusInternalServerError
579 }
580 }
582 if clearSession {
583 p.ClearCookie(rw, req)
584 }
586 if session == nil {
587 session, err = p.CheckBasicAuth(req)
588 if err != nil {
589 log.Printf("%s %s", remoteAddr, err)
590 }
591 }
593 if session == nil {
594 return http.StatusForbidden
595 }
597 // At this point, the user is authenticated. proxy normally
598 if p.PassBasicAuth {
599 req.SetBasicAuth(session.User, p.BasicAuthPassword)
600 req.Header["X-Forwarded-User"] = []string{session.User}
601 if session.Email != "" {
602 req.Header["X-Forwarded-Email"] = []string{session.Email}
603 }
604 }
605 if p.PassAccessToken && session.AccessToken != "" {
606 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
607 }
608 if session.Email == "" {
609 rw.Header().Set("GAP-Auth", session.User)
610 } else {
611 rw.Header().Set("GAP-Auth", session.Email)
612 }
613 return http.StatusAccepted
616func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) {
617 if p.HtpasswdFile == nil {
618 return nil, nil
619 }
620 auth := req.Header.Get("Authorization")
621 if auth == "" {
622 return nil, nil
623 }
624 s := strings.SplitN(auth, " ", 2)
625 if len(s) != 2 || s[0] != "Basic" {
626 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
627 }
628 b, err := b64.StdEncoding.DecodeString(s[1])
629 if err != nil {
630 return nil, err
631 }
632 pair := strings.SplitN(string(b), ":", 2)
633 if len(pair) != 2 {
634 return nil, fmt.Errorf("invalid format %s", b)
635 }
636 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
637 log.Printf("authenticated %q via basic auth", pair[0])
638 return &providers.SessionState{User: pair[0]}, nil
639 }
640 return nil, fmt.Errorf("%s not in HtpasswdFile", pair[0])