initial code import
This commit is contained in:
		
							parent
							
								
									c6f07d54d7
								
							
						
					
					
						commit
						fb636396a3
					
				|  | @ -0,0 +1,24 @@ | ||||||
|  | google_auth_proxy | ||||||
|  | # Go.gitignore | ||||||
|  | # Compiled Object files, Static and Dynamic libs (Shared Objects) | ||||||
|  | *.o | ||||||
|  | *.a | ||||||
|  | *.so | ||||||
|  | 
 | ||||||
|  | # Folders | ||||||
|  | _obj | ||||||
|  | _test | ||||||
|  | 
 | ||||||
|  | # Architecture specific extensions/prefixes | ||||||
|  | *.[568vq] | ||||||
|  | [568vq].out | ||||||
|  | 
 | ||||||
|  | *.cgo1.go | ||||||
|  | *.cgo2.c | ||||||
|  | _cgo_defun.c | ||||||
|  | _cgo_gotypes.go | ||||||
|  | _cgo_export.* | ||||||
|  | 
 | ||||||
|  | _testmain.go | ||||||
|  | 
 | ||||||
|  | *.exe | ||||||
							
								
								
									
										21
									
								
								README.md
								
								
								
								
							
							
						
						
									
										21
									
								
								README.md
								
								
								
								
							|  | @ -28,12 +28,19 @@ individual accounts, or a whole google apps domain. | ||||||
| ## Usage | ## Usage | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| ./google_auth_proxy | Usage of ./google_auth_proxy: | ||||||
|  |   -authenticated-emails-file="": authenticate against emails via file (one per line) | ||||||
|   -client-id="": the Google OAuth Client ID: ie: "123456.apps.googleusercontent.com" |   -client-id="": the Google OAuth Client ID: ie: "123456.apps.googleusercontent.com" | ||||||
|   -client-secret="": the OAuth Client secret |   -client-secret="": the OAuth Client Secret | ||||||
|   -cookie-secret="": the seed for cookie values |   -cookie-domain="": an optional cookie domain to force cookies to | ||||||
|   -redirect-url="": the http base to redirect to. ie: https://internalapp.yourcompany.com/oauth2/callback |   -cookie-secret="": the seed string for secure cookies | ||||||
|   -htpasswd-file="": additionally lookup basic auth in a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption |   -google-apps-domain="": authenticate against the given google apps domain | ||||||
|   -pass-basic-auth=true: pass basic auth information to upstream |   -htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption | ||||||
|   -upstream=[]: the http url(s) of the upstream endpoint(s). If multiple, routing is based on URL path |   -http-address="0.0.0.0:4180": <addr>:<port> to listen on for HTTP clients | ||||||
|  |   -pass-basic-auth=true: pass HTTP Basic Auth information to upstream | ||||||
|  |   -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" | ||||||
|  |   -upstream=[]: the http url(s) of the upstream endpoint. If multiple, routing is based on path | ||||||
|  |   -version=false: print version string | ||||||
| ``` | ``` | ||||||
|  | 
 | ||||||
|  | Unauthenticated requests will be redirected to `/oauth2/sign_in` to start the sign-in process. | ||||||
|  |  | ||||||
|  | @ -0,0 +1,50 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/hmac" | ||||||
|  | 	"crypto/sha1" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func validateCookie(cookie *http.Cookie, seed string) (string, bool) { | ||||||
|  | 	// value, timestamp, sig
 | ||||||
|  | 	parts := strings.Split(cookie.Value, "|") | ||||||
|  | 	if len(parts) != 3 { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	sig := cookieSignature(seed, cookie.Name, parts[0], parts[1]) | ||||||
|  | 	if parts[2] == sig { | ||||||
|  | 		ts, err := strconv.Atoi(parts[1]) | ||||||
|  | 		if err == nil && int64(ts) > time.Now().Add(time.Duration(24)*7*time.Hour*-1).Unix() { | ||||||
|  | 			// it's a valid cookie. now get the contents
 | ||||||
|  | 			rawValue, err := base64.URLEncoding.DecodeString(parts[0]) | ||||||
|  | 			if err == nil { | ||||||
|  | 				return string(rawValue), true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "", false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func signedCookieValue(seed string, key string, value string) string { | ||||||
|  | 	encodedValue := base64.URLEncoding.EncodeToString([]byte(value)) | ||||||
|  | 	timeStr := fmt.Sprintf("%d", time.Now().Unix()) | ||||||
|  | 	sig := cookieSignature(seed, key, encodedValue, timeStr) | ||||||
|  | 	cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig) | ||||||
|  | 	return cookieVal | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func cookieSignature(args ...string) string { | ||||||
|  | 	h := hmac.New(sha1.New, []byte(args[0])) | ||||||
|  | 	for _, arg := range args[1:] { | ||||||
|  | 		h.Write([]byte(arg)) | ||||||
|  | 	} | ||||||
|  | 	var b []byte | ||||||
|  | 	b = h.Sum(b) | ||||||
|  | 	return base64.URLEncoding.EncodeToString(b) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/sha1" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/csv" | ||||||
|  | 	"log" | ||||||
|  | 	"os" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // lookup passwords in a htpasswd file
 | ||||||
|  | // The entries must have been created with -s for SHA encryption
 | ||||||
|  | 
 | ||||||
|  | type HtpasswdFile struct { | ||||||
|  | 	Users map[string]string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewHtpasswdFile(path string) *HtpasswdFile { | ||||||
|  | 	log.Printf("using htpasswd file %s", path) | ||||||
|  | 	r, err := os.Open(path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("failed opening %v, %s", path, err.Error()) | ||||||
|  | 	} | ||||||
|  | 	csv_reader := csv.NewReader(r) | ||||||
|  | 	csv_reader.Comma = ':' | ||||||
|  | 	csv_reader.Comment = '#' | ||||||
|  | 	csv_reader.TrimLeadingSpace = true | ||||||
|  | 
 | ||||||
|  | 	records, err := csv_reader.ReadAll() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("Failed reading file %s", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	h := &HtpasswdFile{Users: make(map[string]string)} | ||||||
|  | 	for _, record := range records { | ||||||
|  | 		h.Users[record[0]] = record[1] | ||||||
|  | 	} | ||||||
|  | 	return h | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (h *HtpasswdFile) Validate(user string, password string) bool { | ||||||
|  | 	realPassword, exists := h.Users[user] | ||||||
|  | 	if !exists { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	if realPassword[:5] == "{SHA}" { | ||||||
|  | 		d := sha1.New() | ||||||
|  | 		d.Write([]byte(password)) | ||||||
|  | 		if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user) | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | @ -0,0 +1,90 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"flag" | ||||||
|  | 	"log" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  | 	"fmt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const VERSION = "0.0.1" | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	showVersion             = flag.Bool("version", false, "print version string") | ||||||
|  | 	httpAddr                = flag.String("http-address", "0.0.0.0:4180", "<addr>:<port> to listen on for HTTP clients") | ||||||
|  | 	redirectUrl             = flag.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"") | ||||||
|  | 	clientID                = flag.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"") | ||||||
|  | 	clientSecret            = flag.String("client-secret", "", "the OAuth Client Secret") | ||||||
|  | 	passBasicAuth           = flag.Bool("pass-basic-auth", true, "pass HTTP Basic Auth information to upstream") | ||||||
|  | 	htpasswdFile            = flag.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption") | ||||||
|  | 	cookieSecret            = flag.String("cookie-secret", "", "the seed string for secure cookies") | ||||||
|  | 	cookieDomain            = flag.String("cookie-domain", "", "an optional cookie domain to force cookies to") | ||||||
|  | 	googleAppsDomain        = flag.String("google-apps-domain", "", "authenticate against the given google apps domain") | ||||||
|  | 	authenticatedEmailsFile = flag.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)") | ||||||
|  | 	upstreams               = StringArray{} | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	flag.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint. If multiple, routing is based on path") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	flag.Parse() | ||||||
|  | 
 | ||||||
|  | 	if *showVersion { | ||||||
|  | 		fmt.Printf("google_auth_proxy v%s\n", VERSION) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(upstreams) < 1 { | ||||||
|  | 		log.Fatal("missing --upstream") | ||||||
|  | 	} | ||||||
|  | 	if *cookieSecret == "" { | ||||||
|  | 		log.Fatal("missing --cookie-secret") | ||||||
|  | 	} | ||||||
|  | 	if *clientID == "" { | ||||||
|  | 		log.Fatal("missing --client-id") | ||||||
|  | 	} | ||||||
|  | 	if *clientSecret == "" { | ||||||
|  | 		log.Fatal("missing --client-secret") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var upstreamUrls []*url.URL | ||||||
|  | 	for _, u := range upstreams { | ||||||
|  | 		upstreamUrl, err := url.Parse(u) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatalf("error parsing --upstream %s", err.Error()) | ||||||
|  | 		} | ||||||
|  | 		upstreamUrls = append(upstreamUrls, upstreamUrl) | ||||||
|  | 	} | ||||||
|  | 	redirectUrl, err := url.Parse(*redirectUrl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("error parsing --redirect-url %s", err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	validator := NewValidator(*googleAppsDomain, *authenticatedEmailsFile) | ||||||
|  | 	oauthproxy := NewOauthProxy(upstreamUrls, *clientID, *clientSecret, validator) | ||||||
|  | 	oauthproxy.SetRedirectUrl(redirectUrl) | ||||||
|  | 	if *googleAppsDomain != "" && *authenticatedEmailsFile == "" { | ||||||
|  | 		oauthproxy.SignInMessage = fmt.Sprintf("using a %s email address", *googleAppsDomain) | ||||||
|  | 	} | ||||||
|  | 	if *htpasswdFile != "" { | ||||||
|  | 		oauthproxy.HtpasswdFile = NewHtpasswdFile(*htpasswdFile) | ||||||
|  | 	} | ||||||
|  | 	listener, err := net.Listen("tcp", *httpAddr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("FATAL: listen (%s) failed - %s", *httpAddr, err.Error()) | ||||||
|  | 	} | ||||||
|  | 	log.Printf("listening on %s", *httpAddr) | ||||||
|  | 
 | ||||||
|  | 	server := &http.Server{Handler: oauthproxy} | ||||||
|  | 	err = server.Serve(listener) | ||||||
|  | 	if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { | ||||||
|  | 		log.Printf("ERROR: http.Serve() - %s", err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Printf("HTTP: closing %s", listener.Addr().String()) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,302 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/bitly/go-simplejson" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httputil" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const signInPath = "/oauth2/sign_in" | ||||||
|  | const oauthStartPath = "/oauth2/start" | ||||||
|  | const oauthCallbackPath = "/oauth2/callback" | ||||||
|  | 
 | ||||||
|  | type OauthProxy struct { | ||||||
|  | 	CookieSeed string | ||||||
|  | 	CookieKey  string | ||||||
|  | 	Validator  func(string) bool | ||||||
|  | 
 | ||||||
|  | 	redirectUrl        *url.URL // the url to receive requests at
 | ||||||
|  | 	oauthRedemptionUrl *url.URL // endpoint to redeem the code
 | ||||||
|  | 	oauthLoginUrl      *url.URL // to redirect the user to
 | ||||||
|  | 	oauthUserInfoUrl   *url.URL | ||||||
|  | 	oauthScope         string | ||||||
|  | 	clientID           string | ||||||
|  | 	clientSecret       string | ||||||
|  | 	SignInMessage      string | ||||||
|  | 	HtpasswdFile       *HtpasswdFile | ||||||
|  | 	serveMux           *http.ServeMux | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewOauthProxy(proxyUrls []*url.URL, clientID string, clientSecret string, validator func(string) bool) *OauthProxy { | ||||||
|  | 	login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth") | ||||||
|  | 	redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token") | ||||||
|  | 	info, _ := url.Parse("https://www.googleapis.com/oauth2/v2/userinfo") | ||||||
|  | 	serveMux := http.NewServeMux() | ||||||
|  | 	for _, u := range proxyUrls { | ||||||
|  | 		path := u.Path | ||||||
|  | 		u.Path = "" | ||||||
|  | 		log.Printf("mapping %s => %s", path, u) | ||||||
|  | 		serveMux.Handle(path, httputil.NewSingleHostReverseProxy(u)) | ||||||
|  | 	} | ||||||
|  | 	return &OauthProxy{ | ||||||
|  | 		CookieKey:  "_oauthproxy", | ||||||
|  | 		CookieSeed: *cookieSecret, | ||||||
|  | 		Validator:  validator, | ||||||
|  | 
 | ||||||
|  | 		clientID:           clientID, | ||||||
|  | 		clientSecret:       clientSecret, | ||||||
|  | 		oauthScope:         "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", | ||||||
|  | 		oauthRedemptionUrl: redeem, | ||||||
|  | 		oauthLoginUrl:      login, | ||||||
|  | 		oauthUserInfoUrl:   info, | ||||||
|  | 		serveMux:           serveMux, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *OauthProxy) SetRedirectUrl(redirectUrl *url.URL) { | ||||||
|  | 	redirectUrl.Path = oauthCallbackPath | ||||||
|  | 	p.redirectUrl = redirectUrl | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *OauthProxy) GetLoginURL() string { | ||||||
|  | 	params := url.Values{} | ||||||
|  | 	params.Add("redirect_uri", p.redirectUrl.String()) | ||||||
|  | 	params.Add("approval_prompt", "force") | ||||||
|  | 	params.Add("scope", p.oauthScope) | ||||||
|  | 	params.Add("client_id", p.clientID) | ||||||
|  | 	params.Add("response_type", "code") | ||||||
|  | 	return fmt.Sprintf("%s?%s", p.oauthLoginUrl, params.Encode()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func apiRequest(req *http.Request) (*simplejson.Json, error) { | ||||||
|  | 	httpclient := &http.Client{} | ||||||
|  | 	resp, err := httpclient.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	body, err := ioutil.ReadAll(resp.Body) | ||||||
|  | 	resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if resp.StatusCode != 200 { | ||||||
|  | 		log.Printf("got response code %d - %s", resp.StatusCode, body) | ||||||
|  | 		return nil, errors.New("api request returned 200 status code") | ||||||
|  | 	} | ||||||
|  | 	data, err := simplejson.NewJson(body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return data, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *OauthProxy) redeemCode(code string) (string, error) { | ||||||
|  | 
 | ||||||
|  | 	params := url.Values{} | ||||||
|  | 	params.Add("redirect_uri", p.redirectUrl.String()) | ||||||
|  | 	params.Add("client_id", p.clientID) | ||||||
|  | 	params.Add("client_secret", p.clientSecret) | ||||||
|  | 	params.Add("code", code) | ||||||
|  | 	params.Add("grant_type", "authorization_code") | ||||||
|  | 	log.Printf("body is %s", params.Encode()) | ||||||
|  | 	req, err := http.NewRequest("POST", p.oauthRedemptionUrl.String(), bytes.NewBufferString(params.Encode())) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("failed building request %s", err.Error()) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||||||
|  | 	json, err := apiRequest(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("failed making request %s", err.Error()) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	access_token, err := json.Get("access_token").String() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return access_token, nil | ||||||
|  | } | ||||||
|  | func (p *OauthProxy) getUserInfo(token string) (string, error) { | ||||||
|  | 	params := url.Values{} | ||||||
|  | 	params.Add("access_token", token) | ||||||
|  | 	endpoint := fmt.Sprintf("%s?%s", p.oauthUserInfoUrl.String(), params.Encode()) | ||||||
|  | 	log.Printf("calling %s", endpoint) | ||||||
|  | 	req, err := http.NewRequest("GET", endpoint, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("failed building request %s", err.Error()) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	json, err := apiRequest(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("failed making request %s", err.Error()) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	email, err := json.Get("email").String() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("failed getting email from response %s", err.Error()) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return email, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ClearCookie(rw http.ResponseWriter, req *http.Request, key string) { | ||||||
|  | 	domain := strings.Split(req.Host, ":")[0] | ||||||
|  | 	if *cookieDomain != "" { | ||||||
|  | 		domain = *cookieDomain | ||||||
|  | 	} | ||||||
|  | 	cookie := &http.Cookie{ | ||||||
|  | 		Name:     key, | ||||||
|  | 		Value:    "", | ||||||
|  | 		Path:     "/", | ||||||
|  | 		Domain:   domain, | ||||||
|  | 		Expires:  time.Now().Add(time.Duration(1) * time.Hour * -1), | ||||||
|  | 		HttpOnly: true, | ||||||
|  | 	} | ||||||
|  | 	http.SetCookie(rw, cookie) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ErrorPage(rw http.ResponseWriter, code int, title string, message string, signinmessage string) { | ||||||
|  | 	log.Printf("ErrorPage %d %s %s %s", code, title, message, signinmessage) | ||||||
|  | 	rw.WriteHeader(code) | ||||||
|  | 	t := getTemplates() | ||||||
|  | 	p := struct { | ||||||
|  | 		Title         string | ||||||
|  | 		Message       string | ||||||
|  | 		SignInMessage string | ||||||
|  | 	}{ | ||||||
|  | 		Title:         fmt.Sprintf("%d %s", code, title), | ||||||
|  | 		Message:       message, | ||||||
|  | 		SignInMessage: signinmessage, | ||||||
|  | 	} | ||||||
|  | 	t.ExecuteTemplate(rw, "error.html", p) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | ||||||
|  | 	// check if this is a redirect back at the end of oauth
 | ||||||
|  | 	if req.URL.Path == signInPath { | ||||||
|  | 		ClearCookie(rw, req, p.CookieKey) | ||||||
|  | 		t := getTemplates() | ||||||
|  | 		p := struct{ SignInMessage string }{SignInMessage: p.SignInMessage} | ||||||
|  | 		t.ExecuteTemplate(rw, "sign_in.html", p) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if req.URL.Path == oauthStartPath { | ||||||
|  | 		http.Redirect(rw, req, p.GetLoginURL(), 302) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if req.URL.Path == oauthCallbackPath { | ||||||
|  | 		// finish the oauth cycle
 | ||||||
|  | 		reqParams, err := url.ParseQuery(req.URL.RawQuery) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		errorString, ok := reqParams["error"] | ||||||
|  | 		if ok && len(errorString) == 1 { | ||||||
|  | 			ErrorPage(rw, 403, "Permission Denied", errorString[0], p.SignInMessage) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		code, ok := reqParams["code"] | ||||||
|  | 		if !ok || len(code) != 1 { | ||||||
|  | 			ErrorPage(rw, 500, "Internal Error", "Invalid API response", p.SignInMessage) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		token, err := p.redeemCode(code[0]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Printf("error redeeming code %s", err.Error()) | ||||||
|  | 			ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// validate user
 | ||||||
|  | 		email, err := p.getUserInfo(token) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Printf("error redeeming code %s", err.Error()) | ||||||
|  | 			ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// set cookie, or deny
 | ||||||
|  | 		if p.Validator(email) { | ||||||
|  | 			log.Printf("authenticating %s completed", email) | ||||||
|  | 			domain := strings.Split(req.Host, ":")[0] | ||||||
|  | 			if *cookieDomain != "" { | ||||||
|  | 				domain = *cookieDomain | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			cookie := &http.Cookie{ | ||||||
|  | 				Name:     p.CookieKey, | ||||||
|  | 				Value:    signedCookieValue(p.CookieSeed, p.CookieKey, email), | ||||||
|  | 				Path:     "/", | ||||||
|  | 				Domain:   domain, | ||||||
|  | 				Expires:  time.Now().Add(time.Duration(168) * time.Hour), // 7 days
 | ||||||
|  | 				HttpOnly: true, | ||||||
|  | 				// Secure: req. ... ? set if X-Scheme: https ?
 | ||||||
|  | 			} | ||||||
|  | 			http.SetCookie(rw, cookie) | ||||||
|  | 			http.Redirect(rw, req, "/", 302) | ||||||
|  | 			return | ||||||
|  | 		} else { | ||||||
|  | 			ErrorPage(rw, 403, "Permission Denied", "Invalid Account", p.SignInMessage) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	cookie, err := req.Cookie(p.CookieKey) | ||||||
|  | 	var ok bool | ||||||
|  | 	var email string | ||||||
|  | 	var user string | ||||||
|  | 	if err == nil { | ||||||
|  | 		email, ok = validateCookie(cookie, p.CookieSeed) | ||||||
|  | 		user = strings.Split(email, "@")[0] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !ok { | ||||||
|  | 		user, ok = p.CheckBasicAuth(req) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !ok { | ||||||
|  | 		log.Printf("invalid cookie. redirecting to sign in") | ||||||
|  | 		// TODO: capture state for which url to redirect to at the end
 | ||||||
|  | 		http.Redirect(rw, req, "/oauth2/sign_in", 302) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// At this point, the user is authenticated. proxy normally
 | ||||||
|  | 	if *passBasicAuth { | ||||||
|  | 		req.SetBasicAuth(user, "") | ||||||
|  | 		req.Header["X-Forwarded-User"] = []string{user} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	p.serveMux.ServeHTTP(rw, req) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *OauthProxy) CheckBasicAuth(req *http.Request) (string, bool) { | ||||||
|  | 	if p.HtpasswdFile == nil { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	s := strings.SplitN(req.Header.Get("Authorization"), " ", 2) | ||||||
|  | 	if len(s) != 2 || s[0] != "Basic" { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	b, err := base64.StdEncoding.DecodeString(s[1]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	pair := strings.SplitN(string(b), ":", 2) | ||||||
|  | 	if len(pair) != 2 { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	if p.HtpasswdFile.Validate(pair[0], pair[1]) { | ||||||
|  | 		return pair[0], true | ||||||
|  | 	} | ||||||
|  | 	return "", false | ||||||
|  | } | ||||||
|  | @ -0,0 +1,16 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type StringArray []string | ||||||
|  | 
 | ||||||
|  | func (a *StringArray) Set(s string) error { | ||||||
|  | 	*a = append(*a, s) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *StringArray) String() string { | ||||||
|  | 	return fmt.Sprint(*a) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"html/template" | ||||||
|  | 	"log" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func getTemplates() *template.Template { | ||||||
|  | 	t, err := template.New("foo").Parse(`{{define "sign_in.html"}} | ||||||
|  | <html><head><title>Sign In</title></head> | ||||||
|  | 	<body> | ||||||
|  | 	<form method="GET" action="/oauth2/start"> | ||||||
|  | 	<button type="submit">Sign In w/ Google</button> | ||||||
|  | 	{{.SignInMessage}} | ||||||
|  | 	</form> | ||||||
|  | </body></html> | ||||||
|  | {{end}}`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("failed parsing template %s", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	t, err = t.Parse(`{{define "error.html"}} | ||||||
|  | <html><head><title>{{.Title}}</title></head> | ||||||
|  | <body> | ||||||
|  | 	<h2>{{.Title}}</h2> | ||||||
|  | 	<p>{{.Message}}</p> | ||||||
|  | 	<hr> | ||||||
|  | 	<form method="GET" action="/oauth2/start"> | ||||||
|  | 	<button type="submit">Sign In w/ Google</button> | ||||||
|  | 	{{.SignInMessage}} | ||||||
|  | 	</form> | ||||||
|  | </body> | ||||||
|  | </html>{{end}}`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("failed parsing template %s", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	return t | ||||||
|  | } | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"log" | ||||||
|  | 	"encoding/csv" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func NewValidator(domain string, usersFile string) func(string) bool { | ||||||
|  | 
 | ||||||
|  | 	validUsers := make(map[string]bool) | ||||||
|  | 	emailSuffix := "" | ||||||
|  | 	if domain != "" { | ||||||
|  | 		emailSuffix = fmt.Sprintf("@%s", domain) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if usersFile != "" { | ||||||
|  | 		r, err := os.Open(usersFile) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatalf("failed opening -authenticated-emails-file=%v, %s", usersFile, err.Error()) | ||||||
|  | 		} | ||||||
|  | 		csv_reader := csv.NewReader(r) | ||||||
|  | 		csv_reader.Comma = ',' | ||||||
|  | 		csv_reader.Comment = '#' | ||||||
|  | 		csv_reader.TrimLeadingSpace = true | ||||||
|  | 		records, err := csv_reader.ReadAll() | ||||||
|  | 		for _, r := range records { | ||||||
|  | 			validUsers[r[0]] = true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	validator := func(email string) bool { | ||||||
|  | 		var valid bool | ||||||
|  | 		if emailSuffix != "" { | ||||||
|  | 			valid = strings.HasSuffix(email, emailSuffix) | ||||||
|  | 		} | ||||||
|  | 		if !valid { | ||||||
|  | 			_, valid = validUsers[email] | ||||||
|  | 		} | ||||||
|  | 		log.Printf("validating: is %s valid? %v", email, valid) | ||||||
|  | 		return valid | ||||||
|  | 	} | ||||||
|  | 	return validator | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue