@@ -0,0 +1,302 @@
8+ "github.com/bitly/go-simplejson"
18+const signInPath = "/oauth2/sign_in"
19+const oauthStartPath = "/oauth2/start"
20+const oauthCallbackPath = "/oauth2/callback"
22+type OauthProxy struct {
25+ Validator func(string) bool
27+ redirectUrl *url.URL // the url to receive requests at
28+ oauthRedemptionUrl *url.URL // endpoint to redeem the code
29+ oauthLoginUrl *url.URL // to redirect the user to
30+ oauthUserInfoUrl *url.URL
35+ HtpasswdFile *HtpasswdFile
36+ serveMux *http.ServeMux
39+func NewOauthProxy(proxyUrls []*url.URL, clientID string, clientSecret string, validator func(string) bool) *OauthProxy {
40+ login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth")
41+ redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token")
42+ info, _ := url.Parse("https://www.googleapis.com/oauth2/v2/userinfo")
43+ serveMux := http.NewServeMux()
44+ for _, u := range proxyUrls {
47+ log.Printf("mapping %s => %s", path, u)
48+ serveMux.Handle(path, httputil.NewSingleHostReverseProxy(u))
51+ CookieKey: "_oauthproxy",
52+ CookieSeed: *cookieSecret,
53+ Validator: validator,
56+ clientSecret: clientSecret,
57+ oauthScope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
58+ oauthRedemptionUrl: redeem,
59+ oauthLoginUrl: login,
60+ oauthUserInfoUrl: info,
65+func (p *OauthProxy) SetRedirectUrl(redirectUrl *url.URL) {
66+ redirectUrl.Path = oauthCallbackPath
67+ p.redirectUrl = redirectUrl
70+func (p *OauthProxy) GetLoginURL() string {
71+ params := url.Values{}
72+ params.Add("redirect_uri", p.redirectUrl.String())
73+ params.Add("approval_prompt", "force")
74+ params.Add("scope", p.oauthScope)
75+ params.Add("client_id", p.clientID)
76+ params.Add("response_type", "code")
77+ return fmt.Sprintf("%s?%s", p.oauthLoginUrl, params.Encode())
80+func apiRequest(req *http.Request) (*simplejson.Json, error) {
81+ httpclient := &http.Client{}
82+ resp, err := httpclient.Do(req)
86+ body, err := ioutil.ReadAll(resp.Body)
91+ if resp.StatusCode != 200 {
92+ log.Printf("got response code %d - %s", resp.StatusCode, body)
93+ return nil, errors.New("api request returned 200 status code")
95+ data, err := simplejson.NewJson(body)
102+func (p *OauthProxy) redeemCode(code string) (string, error) {
104+ params := url.Values{}
105+ params.Add("redirect_uri", p.redirectUrl.String())
106+ params.Add("client_id", p.clientID)
107+ params.Add("client_secret", p.clientSecret)
108+ params.Add("code", code)
109+ params.Add("grant_type", "authorization_code")
110+ log.Printf("body is %s", params.Encode())
111+ req, err := http.NewRequest("POST", p.oauthRedemptionUrl.String(), bytes.NewBufferString(params.Encode()))
113+ log.Printf("failed building request %s", err.Error())
116+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
117+ json, err := apiRequest(req)
119+ log.Printf("failed making request %s", err.Error())
122+ access_token, err := json.Get("access_token").String()
126+ return access_token, nil
128+func (p *OauthProxy) getUserInfo(token string) (string, error) {
129+ params := url.Values{}
130+ params.Add("access_token", token)
131+ endpoint := fmt.Sprintf("%s?%s", p.oauthUserInfoUrl.String(), params.Encode())
132+ log.Printf("calling %s", endpoint)
133+ req, err := http.NewRequest("GET", endpoint, nil)
135+ log.Printf("failed building request %s", err.Error())
138+ json, err := apiRequest(req)
140+ log.Printf("failed making request %s", err.Error())
143+ email, err := json.Get("email").String()
145+ log.Printf("failed getting email from response %s", err.Error())
151+func ClearCookie(rw http.ResponseWriter, req *http.Request, key string) {
152+ domain := strings.Split(req.Host, ":")[0]
153+ if *cookieDomain != "" {
154+ domain = *cookieDomain
156+ cookie := &http.Cookie{
161+ Expires: time.Now().Add(time.Duration(1) * time.Hour * -1),
164+ http.SetCookie(rw, cookie)
167+func ErrorPage(rw http.ResponseWriter, code int, title string, message string, signinmessage string) {
168+ log.Printf("ErrorPage %d %s %s %s", code, title, message, signinmessage)
169+ rw.WriteHeader(code)
174+ SignInMessage string
176+ Title: fmt.Sprintf("%d %s", code, title),
178+ SignInMessage: signinmessage,
180+ t.ExecuteTemplate(rw, "error.html", p)
183+func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
184+ // check if this is a redirect back at the end of oauth
185+ if req.URL.Path == signInPath {
186+ ClearCookie(rw, req, p.CookieKey)
188+ p := struct{ SignInMessage string }{SignInMessage: p.SignInMessage}
189+ t.ExecuteTemplate(rw, "sign_in.html", p)
192+ if req.URL.Path == oauthStartPath {
193+ http.Redirect(rw, req, p.GetLoginURL(), 302)
196+ if req.URL.Path == oauthCallbackPath {
197+ // finish the oauth cycle
198+ reqParams, err := url.ParseQuery(req.URL.RawQuery)
200+ ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
203+ errorString, ok := reqParams["error"]
204+ if ok && len(errorString) == 1 {
205+ ErrorPage(rw, 403, "Permission Denied", errorString[0], p.SignInMessage)
208+ code, ok := reqParams["code"]
209+ if !ok || len(code) != 1 {
210+ ErrorPage(rw, 500, "Internal Error", "Invalid API response", p.SignInMessage)
214+ token, err := p.redeemCode(code[0])
216+ log.Printf("error redeeming code %s", err.Error())
217+ ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
221+ email, err := p.getUserInfo(token)
223+ log.Printf("error redeeming code %s", err.Error())
224+ ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
228+ // set cookie, or deny
229+ if p.Validator(email) {
230+ log.Printf("authenticating %s completed", email)
231+ domain := strings.Split(req.Host, ":")[0]
232+ if *cookieDomain != "" {
233+ domain = *cookieDomain
236+ cookie := &http.Cookie{
238+ Value: signedCookieValue(p.CookieSeed, p.CookieKey, email),
241+ Expires: time.Now().Add(time.Duration(168) * time.Hour), // 7 days
243+ // Secure: req. ... ? set if X-Scheme: https ?
245+ http.SetCookie(rw, cookie)
246+ http.Redirect(rw, req, "/", 302)
249+ ErrorPage(rw, 403, "Permission Denied", "Invalid Account", p.SignInMessage)
253+ cookie, err := req.Cookie(p.CookieKey)
258+ email, ok = validateCookie(cookie, p.CookieSeed)
259+ user = strings.Split(email, "@")[0]
263+ user, ok = p.CheckBasicAuth(req)
267+ log.Printf("invalid cookie. redirecting to sign in")
268+ // TODO: capture state for which url to redirect to at the end
269+ http.Redirect(rw, req, "/oauth2/sign_in", 302)
273+ // At this point, the user is authenticated. proxy normally
275+ req.SetBasicAuth(user, "")
276+ req.Header["X-Forwarded-User"] = []string{user}
279+ p.serveMux.ServeHTTP(rw, req)
282+func (p *OauthProxy) CheckBasicAuth(req *http.Request) (string, bool) {
283+ if p.HtpasswdFile == nil {
286+ s := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
287+ if len(s) != 2 || s[0] != "Basic" {
290+ b, err := base64.StdEncoding.DecodeString(s[1])
294+ pair := strings.SplitN(string(b), ":", 2)
298+ if p.HtpasswdFile.Validate(pair[0], pair[1]) {
299+ return pair[0], true