git.iko.soy/typeable/oauth2-proxy
on v2.2 help sign in
typeable/oauth2-proxy/oauthproxy.go
720 lines 20 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 CSRFCookieName string
41 CookieDomain string
42 CookieSecure bool
43 CookieHttpOnly bool
44 CookieExpire time.Duration
45 CookieRefresh time.Duration
46 Validator func(string) bool
48 RobotsPath string
49 PingPath string
50 SignInPath string
51 SignOutPath string
52 OAuthStartPath string
53 OAuthCallbackPath string
54 AuthOnlyPath string
56 redirectURL *url.URL // the url to receive requests at
57 provider providers.Provider
58 ProxyPrefix string
59 SignInMessage string
60 HtpasswdFile *HtpasswdFile
61 DisplayHtpasswdForm bool
62 serveMux http.Handler
63 SetXAuthRequest bool
64 PassBasicAuth bool
65 SkipProviderButton bool
66 PassUserHeaders bool
67 BasicAuthPassword string
68 PassAccessToken bool
69 CookieCipher *cookie.Cipher
70 skipAuthRegex []string
71 skipAuthPreflight bool
72 compiledRegex []*regexp.Regexp
73 templates *template.Template
74 Footer string
77type UpstreamProxy struct {
78 upstream string
79 handler http.Handler
80 auth hmacauth.HmacAuth
83func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
84 w.Header().Set("GAP-Upstream-Address", u.upstream)
85 if u.auth != nil {
86 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
87 u.auth.SignRequest(r)
88 }
89 u.handler.ServeHTTP(w, r)
92func NewReverseProxy(target *url.URL) (proxy *httputil.ReverseProxy) {
93 return httputil.NewSingleHostReverseProxy(target)
95func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
96 director := proxy.Director
97 proxy.Director = func(req *http.Request) {
98 director(req)
99 // use RequestURI so that we aren't unescaping encoded slashes in the request path
100 req.Host = target.Host
101 req.URL.Opaque = req.RequestURI
102 req.URL.RawQuery = ""
103 }
105func setProxyDirector(proxy *httputil.ReverseProxy) {
106 director := proxy.Director
107 proxy.Director = func(req *http.Request) {
108 director(req)
109 // use RequestURI so that we aren't unescaping encoded slashes in the request path
110 req.URL.Opaque = req.RequestURI
111 req.URL.RawQuery = ""
112 }
114func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
115 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
118func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
119 serveMux := http.NewServeMux()
120 var auth hmacauth.HmacAuth
121 if sigData := opts.signatureData; sigData != nil {
122 auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
123 SignatureHeader, SignatureHeaders)
124 }
125 for _, u := range opts.proxyURLs {
126 path := u.Path
127 switch u.Scheme {
128 case "http", "https":
129 u.Path = ""
130 log.Printf("mapping path %q => upstream %q", path, u)
131 proxy := NewReverseProxy(u)
132 if !opts.PassHostHeader {
133 setProxyUpstreamHostHeader(proxy, u)
134 } else {
135 setProxyDirector(proxy)
136 }
137 serveMux.Handle(path,
138 &UpstreamProxy{u.Host, proxy, auth})
139 case "file":
140 if u.Fragment != "" {
141 path = u.Fragment
142 }
143 log.Printf("mapping path %q => file system %q", path, u.Path)
144 proxy := NewFileServer(path, u.Path)
145 serveMux.Handle(path, &UpstreamProxy{path, proxy, nil})
146 default:
147 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
148 }
149 }
150 for _, u := range opts.CompiledRegex {
151 log.Printf("compiled skip-auth-regex => %q", u)
152 }
154 redirectURL := opts.redirectURL
155 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
157 log.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID)
158 domain := opts.CookieDomain
159 if domain == "" {
160 domain = "<default>"
161 }
162 refresh := "disabled"
163 if opts.CookieRefresh != time.Duration(0) {
164 refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
165 }
167 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)
169 var cipher *cookie.Cipher
170 if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
171 var err error
172 cipher, err = cookie.NewCipher(secretBytes(opts.CookieSecret))
173 if err != nil {
174 log.Fatal("cookie-secret error: ", err)
175 }
176 }
178 return &OAuthProxy{
179 CookieName: opts.CookieName,
180 CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
181 CookieSeed: opts.CookieSecret,
182 CookieDomain: opts.CookieDomain,
183 CookieSecure: opts.CookieSecure,
184 CookieHttpOnly: opts.CookieHttpOnly,
185 CookieExpire: opts.CookieExpire,
186 CookieRefresh: opts.CookieRefresh,
187 Validator: validator,
189 RobotsPath: "/robots.txt",
190 PingPath: "/ping",
191 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
192 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
193 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
194 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
195 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
197 ProxyPrefix: opts.ProxyPrefix,
198 provider: opts.provider,
199 serveMux: serveMux,
200 redirectURL: redirectURL,
201 skipAuthRegex: opts.SkipAuthRegex,
202 skipAuthPreflight: opts.SkipAuthPreflight,
203 compiledRegex: opts.CompiledRegex,
204 SetXAuthRequest: opts.SetXAuthRequest,
205 PassBasicAuth: opts.PassBasicAuth,
206 PassUserHeaders: opts.PassUserHeaders,
207 BasicAuthPassword: opts.BasicAuthPassword,
208 PassAccessToken: opts.PassAccessToken,
209 SkipProviderButton: opts.SkipProviderButton,
210 CookieCipher: cipher,
211 templates: loadTemplates(opts.CustomTemplatesDir),
212 Footer: opts.Footer,
213 }
216func (p *OAuthProxy) GetRedirectURI(host string) string {
217 // default to the request Host if not set
218 if p.redirectURL.Host != "" {
219 return p.redirectURL.String()
220 }
221 var u url.URL
222 u = *p.redirectURL
223 if u.Scheme == "" {
224 if p.CookieSecure {
225 u.Scheme = "https"
226 } else {
227 u.Scheme = "http"
228 }
229 }
230 u.Host = host
231 return u.String()
234func (p *OAuthProxy) displayCustomLoginForm() bool {
235 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
238func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, err error) {
239 if code == "" {
240 return nil, errors.New("missing code")
241 }
242 redirectURI := p.GetRedirectURI(host)
243 s, err = p.provider.Redeem(redirectURI, code)
244 if err != nil {
245 return
246 }
248 if s.Email == "" {
249 s.Email, err = p.provider.GetEmailAddress(s)
250 }
251 return
254func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
255 if value != "" {
256 value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
257 if len(value) > 4096 {
258 // Cookies cannot be larger than 4kb
259 log.Printf("WARNING - Cookie Size: %d bytes", len(value))
260 }
261 }
262 return p.makeCookie(req, p.CookieName, value, expiration, now)
265func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
266 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
269func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
270 domain := req.Host
271 if h, _, err := net.SplitHostPort(domain); err == nil {
272 domain = h
273 }
274 if p.CookieDomain != "" {
275 if !strings.HasSuffix(domain, p.CookieDomain) {
276 log.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain)
277 }
278 domain = p.CookieDomain
279 }
281 return &http.Cookie{
282 Name: name,
283 Value: value,
284 Path: "/",
285 Domain: domain,
286 HttpOnly: p.CookieHttpOnly,
287 Secure: p.CookieSecure,
288 Expires: now.Add(expiration),
289 }
292func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
293 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
296func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
297 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
300func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) {
301 http.SetCookie(rw, p.MakeSessionCookie(req, "", time.Hour*-1, time.Now()))
304func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
305 http.SetCookie(rw, p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()))
308func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) {
309 var age time.Duration
310 c, err := req.Cookie(p.CookieName)
311 if err != nil {
312 // always http.ErrNoCookie
313 return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
314 }
315 val, timestamp, ok := cookie.Validate(c, p.CookieSeed, p.CookieExpire)
316 if !ok {
317 return nil, age, errors.New("Cookie Signature not valid")
318 }
320 session, err := p.provider.SessionFromCookie(val, p.CookieCipher)
321 if err != nil {
322 return nil, age, err
323 }
325 age = time.Now().Truncate(time.Second).Sub(timestamp)
326 return session, age, nil
329func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *providers.SessionState) error {
330 value, err := p.provider.CookieForSession(s, p.CookieCipher)
331 if err != nil {
332 return err
333 }
334 p.SetSessionCookie(rw, req, value)
335 return nil
338func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
339 rw.WriteHeader(http.StatusOK)
340 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
343func (p *OAuthProxy) PingPage(rw http.ResponseWriter) {
344 rw.WriteHeader(http.StatusOK)
345 fmt.Fprintf(rw, "OK")
348func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
349 log.Printf("ErrorPage %d %s %s", code, title, message)
350 rw.WriteHeader(code)
351 t := struct {
352 Title string
353 Message string
354 ProxyPrefix string
355 }{
356 Title: fmt.Sprintf("%d %s", code, title),
357 Message: message,
358 ProxyPrefix: p.ProxyPrefix,
359 }
360 p.templates.ExecuteTemplate(rw, "error.html", t)
363func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
364 p.ClearSessionCookie(rw, req)
365 rw.WriteHeader(code)
367 redirect_url := req.URL.RequestURI()
368 if req.Header.Get("X-Auth-Request-Redirect") != "" {
369 redirect_url = req.Header.Get("X-Auth-Request-Redirect")
370 }
371 if redirect_url == p.SignInPath {
372 redirect_url = "/"
373 }
375 t := struct {
376 ProviderName string
377 SignInMessage string
378 CustomLogin bool
379 Redirect string
380 Version string
381 ProxyPrefix string
382 Footer template.HTML
383 }{
384 ProviderName: p.provider.Data().ProviderName,
385 SignInMessage: p.SignInMessage,
386 CustomLogin: p.displayCustomLoginForm(),
387 Redirect: redirect_url,
388 Version: VERSION,
389 ProxyPrefix: p.ProxyPrefix,
390 Footer: template.HTML(p.Footer),
391 }
392 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
395func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
396 if req.Method != "POST" || p.HtpasswdFile == nil {
397 return "", false
398 }
399 user := req.FormValue("username")
400 passwd := req.FormValue("password")
401 if user == "" {
402 return "", false
403 }
404 // check auth
405 if p.HtpasswdFile.Validate(user, passwd) {
406 log.Printf("authenticated %q via HtpasswdFile", user)
407 return user, true
408 }
409 return "", false
412func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
413 err = req.ParseForm()
414 if err != nil {
415 return
416 }
418 redirect = req.Form.Get("rd")
419 if redirect == "" || !strings.HasPrefix(redirect, "/") || strings.HasPrefix(redirect, "//") {
420 redirect = "/"
421 }
423 return
426func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) {
427 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
428 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
431func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) {
432 for _, u := range p.compiledRegex {
433 ok = u.MatchString(path)
434 if ok {
435 return
436 }
437 }
438 return
441func getRemoteAddr(req *http.Request) (s string) {
442 s = req.RemoteAddr
443 if req.Header.Get("X-Real-IP") != "" {
444 s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
445 }
446 return
449func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
450 switch path := req.URL.Path; {
451 case path == p.RobotsPath:
452 p.RobotsTxt(rw)
453 case path == p.PingPath:
454 p.PingPage(rw)
455 case p.IsWhitelistedRequest(req):
456 p.serveMux.ServeHTTP(rw, req)
457 case path == p.SignInPath:
458 p.SignIn(rw, req)
459 case path == p.SignOutPath:
460 p.SignOut(rw, req)
461 case path == p.OAuthStartPath:
462 p.OAuthStart(rw, req)
463 case path == p.OAuthCallbackPath:
464 p.OAuthCallback(rw, req)
465 case path == p.AuthOnlyPath:
466 p.AuthenticateOnly(rw, req)
467 default:
468 p.Proxy(rw, req)
469 }
472func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
473 redirect, err := p.GetRedirect(req)
474 if err != nil {
475 p.ErrorPage(rw, 500, "Internal Error", err.Error())
476 return
477 }
479 user, ok := p.ManualSignIn(rw, req)
480 if ok {
481 session := &providers.SessionState{User: user}
482 p.SaveSession(rw, req, session)
483 http.Redirect(rw, req, redirect, 302)
484 } else {
485 p.SignInPage(rw, req, 200)
486 }
489func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
490 p.ClearSessionCookie(rw, req)
491 http.Redirect(rw, req, "/", 302)
494func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
495 nonce, err := cookie.Nonce()
496 if err != nil {
497 p.ErrorPage(rw, 500, "Internal Error", err.Error())
498 return
499 }
500 p.SetCSRFCookie(rw, req, nonce)
501 redirect, err := p.GetRedirect(req)
502 if err != nil {
503 p.ErrorPage(rw, 500, "Internal Error", err.Error())
504 return
505 }
506 redirectURI := p.GetRedirectURI(req.Host)
507 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302)
510func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
511 remoteAddr := getRemoteAddr(req)
513 // finish the oauth cycle
514 err := req.ParseForm()
515 if err != nil {
516 p.ErrorPage(rw, 500, "Internal Error", err.Error())
517 return
518 }
519 errorString := req.Form.Get("error")
520 if errorString != "" {
521 p.ErrorPage(rw, 403, "Permission Denied", errorString)
522 return
523 }
525 session, err := p.redeemCode(req.Host, req.Form.Get("code"))
526 if err != nil {
527 log.Printf("%s error redeeming code %s", remoteAddr, err)
528 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
529 return
530 }
532 s := strings.SplitN(req.Form.Get("state"), ":", 2)
533 if len(s) != 2 {
534 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
535 return
536 }
537 nonce := s[0]
538 redirect := s[1]
539 c, err := req.Cookie(p.CSRFCookieName)
540 if err != nil {
541 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
542 return
543 }
544 p.ClearCSRFCookie(rw, req)
545 if c.Value != nonce {
546 log.Printf("%s csrf token mismatch, potential attack", remoteAddr)
547 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
548 return
549 }
551 if !strings.HasPrefix(redirect, "/") || strings.HasPrefix(redirect, "//") {
552 redirect = "/"
553 }
555 // set cookie, or deny
556 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
557 log.Printf("%s authentication complete %s", remoteAddr, session)
558 err := p.SaveSession(rw, req, session)
559 if err != nil {
560 log.Printf("%s %s", remoteAddr, err)
561 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
562 return
563 }
564 http.Redirect(rw, req, redirect, 302)
565 } else {
566 log.Printf("%s Permission Denied: %q is unauthorized", remoteAddr, session.Email)
567 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
568 }
571func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
572 status := p.Authenticate(rw, req)
573 if status == http.StatusAccepted {
574 rw.WriteHeader(http.StatusAccepted)
575 } else {
576 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
577 }
580func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
581 status := p.Authenticate(rw, req)
582 if status == http.StatusInternalServerError {
583 p.ErrorPage(rw, http.StatusInternalServerError,
584 "Internal Error", "Internal Error")
585 } else if status == http.StatusForbidden {
586 if p.SkipProviderButton {
587 p.OAuthStart(rw, req)
588 } else {
589 p.SignInPage(rw, req, http.StatusForbidden)
590 }
591 } else {
592 p.serveMux.ServeHTTP(rw, req)
593 }
596func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
597 var saveSession, clearSession, revalidated bool
598 remoteAddr := getRemoteAddr(req)
600 session, sessionAge, err := p.LoadCookiedSession(req)
601 if err != nil {
602 log.Printf("%s %s", remoteAddr, err)
603 }
604 if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
605 log.Printf("%s refreshing %s old session cookie for %s (refresh after %s)", remoteAddr, sessionAge, session, p.CookieRefresh)
606 saveSession = true
607 }
609 if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
610 log.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
611 clearSession = true
612 session = nil
613 } else if ok {
614 saveSession = true
615 revalidated = true
616 }
618 if session != nil && session.IsExpired() {
619 log.Printf("%s removing session. token expired %s", remoteAddr, session)
620 session = nil
621 saveSession = false
622 clearSession = true
623 }
625 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
626 if !p.provider.ValidateSessionState(session) {
627 log.Printf("%s removing session. error validating %s", remoteAddr, session)
628 saveSession = false
629 session = nil
630 clearSession = true
631 }
632 }
634 if session != nil && session.Email != "" && !p.Validator(session.Email) {
635 log.Printf("%s Permission Denied: removing session %s", remoteAddr, session)
636 session = nil
637 saveSession = false
638 clearSession = true
639 }
641 if saveSession && session != nil {
642 err := p.SaveSession(rw, req, session)
643 if err != nil {
644 log.Printf("%s %s", remoteAddr, err)
645 return http.StatusInternalServerError
646 }
647 }
649 if clearSession {
650 p.ClearSessionCookie(rw, req)
651 }
653 if session == nil {
654 session, err = p.CheckBasicAuth(req)
655 if err != nil {
656 log.Printf("%s %s", remoteAddr, err)
657 }
658 }
660 if session == nil {
661 return http.StatusForbidden
662 }
664 // At this point, the user is authenticated. proxy normally
665 if p.PassBasicAuth {
666 req.SetBasicAuth(session.User, p.BasicAuthPassword)
667 req.Header["X-Forwarded-User"] = []string{session.User}
668 if session.Email != "" {
669 req.Header["X-Forwarded-Email"] = []string{session.Email}
670 }
671 }
672 if p.PassUserHeaders {
673 req.Header["X-Forwarded-User"] = []string{session.User}
674 if session.Email != "" {
675 req.Header["X-Forwarded-Email"] = []string{session.Email}
676 }
677 }
678 if p.SetXAuthRequest {
679 rw.Header().Set("X-Auth-Request-User", session.User)
680 if session.Email != "" {
681 rw.Header().Set("X-Auth-Request-Email", session.Email)
682 }
683 }
684 if p.PassAccessToken && session.AccessToken != "" {
685 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
686 }
687 if session.Email == "" {
688 rw.Header().Set("GAP-Auth", session.User)
689 } else {
690 rw.Header().Set("GAP-Auth", session.Email)
691 }
692 return http.StatusAccepted
695func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) {
696 if p.HtpasswdFile == nil {
697 return nil, nil
698 }
699 auth := req.Header.Get("Authorization")
700 if auth == "" {
701 return nil, nil
702 }
703 s := strings.SplitN(auth, " ", 2)
704 if len(s) != 2 || s[0] != "Basic" {
705 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
706 }
707 b, err := b64.StdEncoding.DecodeString(s[1])
708 if err != nil {
709 return nil, err
710 }
711 pair := strings.SplitN(string(b), ":", 2)
712 if len(pair) != 2 {
713 return nil, fmt.Errorf("invalid format %s", b)
714 }
715 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
716 log.Printf("authenticated %q via basic auth", pair[0])
717 return &providers.SessionState{User: pair[0]}, nil
718 }
719 return nil, fmt.Errorf("%s not in HtpasswdFile", pair[0])