@@ -0,0 +1,302 @@
1+package main
2+
3+import (
4+ "bytes"
5+ "encoding/base64"
6+ "errors"
7+ "fmt"
8+ "github.com/bitly/go-simplejson"
9+ "io/ioutil"
10+ "log"
11+ "net/http"
12+ "net/http/httputil"
13+ "net/url"
14+ "strings"
15+ "time"
16+)
17+
18+const signInPath = "/oauth2/sign_in"
19+const oauthStartPath = "/oauth2/start"
20+const oauthCallbackPath = "/oauth2/callback"
21+
22+type OauthProxy struct {
23+ CookieSeed string
24+ CookieKey string
25+ Validator func(string) bool
26+
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
31+ oauthScope string
32+ clientID string
33+ clientSecret string
34+ SignInMessage string
35+ HtpasswdFile *HtpasswdFile
36+ serveMux *http.ServeMux
37+}
38+
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 {
45+ path := u.Path
46+ u.Path = ""
47+ log.Printf("mapping %s => %s", path, u)
48+ serveMux.Handle(path, httputil.NewSingleHostReverseProxy(u))
49+ }
50+ return &OauthProxy{
51+ CookieKey: "_oauthproxy",
52+ CookieSeed: *cookieSecret,
53+ Validator: validator,
54+
55+ clientID: clientID,
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,
61+ serveMux: serveMux,
62+ }
63+}
64+
65+func (p *OauthProxy) SetRedirectUrl(redirectUrl *url.URL) {
66+ redirectUrl.Path = oauthCallbackPath
67+ p.redirectUrl = redirectUrl
68+}
69+
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())
78+}
79+
80+func apiRequest(req *http.Request) (*simplejson.Json, error) {
81+ httpclient := &http.Client{}
82+ resp, err := httpclient.Do(req)
83+ if err != nil {
84+ return nil, err
85+ }
86+ body, err := ioutil.ReadAll(resp.Body)
87+ resp.Body.Close()
88+ if err != nil {
89+ return nil, err
90+ }
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")
94+ }
95+ data, err := simplejson.NewJson(body)
96+ if err != nil {
97+ return nil, err
98+ }
99+ return data, nil
100+}
101+
102+func (p *OauthProxy) redeemCode(code string) (string, error) {
103+
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()))
112+ if err != nil {
113+ log.Printf("failed building request %s", err.Error())
114+ return "", err
115+ }
116+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
117+ json, err := apiRequest(req)
118+ if err != nil {
119+ log.Printf("failed making request %s", err.Error())
120+ return "", err
121+ }
122+ access_token, err := json.Get("access_token").String()
123+ if err != nil {
124+ return "", err
125+ }
126+ return access_token, nil
127+}
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)
134+ if err != nil {
135+ log.Printf("failed building request %s", err.Error())
136+ return "", err
137+ }
138+ json, err := apiRequest(req)
139+ if err != nil {
140+ log.Printf("failed making request %s", err.Error())
141+ return "", err
142+ }
143+ email, err := json.Get("email").String()
144+ if err != nil {
145+ log.Printf("failed getting email from response %s", err.Error())
146+ return "", err
147+ }
148+ return email, nil
149+}
150+
151+func ClearCookie(rw http.ResponseWriter, req *http.Request, key string) {
152+ domain := strings.Split(req.Host, ":")[0]
153+ if *cookieDomain != "" {
154+ domain = *cookieDomain
155+ }
156+ cookie := &http.Cookie{
157+ Name: key,
158+ Value: "",
159+ Path: "/",
160+ Domain: domain,
161+ Expires: time.Now().Add(time.Duration(1) * time.Hour * -1),
162+ HttpOnly: true,
163+ }
164+ http.SetCookie(rw, cookie)
165+}
166+
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)
170+ t := getTemplates()
171+ p := struct {
172+ Title string
173+ Message string
174+ SignInMessage string
175+ }{
176+ Title: fmt.Sprintf("%d %s", code, title),
177+ Message: message,
178+ SignInMessage: signinmessage,
179+ }
180+ t.ExecuteTemplate(rw, "error.html", p)
181+}
182+
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)
187+ t := getTemplates()
188+ p := struct{ SignInMessage string }{SignInMessage: p.SignInMessage}
189+ t.ExecuteTemplate(rw, "sign_in.html", p)
190+ return
191+ }
192+ if req.URL.Path == oauthStartPath {
193+ http.Redirect(rw, req, p.GetLoginURL(), 302)
194+ return
195+ }
196+ if req.URL.Path == oauthCallbackPath {
197+ // finish the oauth cycle
198+ reqParams, err := url.ParseQuery(req.URL.RawQuery)
199+ if err != nil {
200+ ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
201+ return
202+ }
203+ errorString, ok := reqParams["error"]
204+ if ok && len(errorString) == 1 {
205+ ErrorPage(rw, 403, "Permission Denied", errorString[0], p.SignInMessage)
206+ return
207+ }
208+ code, ok := reqParams["code"]
209+ if !ok || len(code) != 1 {
210+ ErrorPage(rw, 500, "Internal Error", "Invalid API response", p.SignInMessage)
211+ return
212+ }
213+
214+ token, err := p.redeemCode(code[0])
215+ if err != nil {
216+ log.Printf("error redeeming code %s", err.Error())
217+ ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
218+ return
219+ }
220+ // validate user
221+ email, err := p.getUserInfo(token)
222+ if err != nil {
223+ log.Printf("error redeeming code %s", err.Error())
224+ ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
225+ return
226+ }
227+
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
234+ }
235+
236+ cookie := &http.Cookie{
237+ Name: p.CookieKey,
238+ Value: signedCookieValue(p.CookieSeed, p.CookieKey, email),
239+ Path: "/",
240+ Domain: domain,
241+ Expires: time.Now().Add(time.Duration(168) * time.Hour), // 7 days
242+ HttpOnly: true,
243+ // Secure: req. ... ? set if X-Scheme: https ?
244+ }
245+ http.SetCookie(rw, cookie)
246+ http.Redirect(rw, req, "/", 302)
247+ return
248+ } else {
249+ ErrorPage(rw, 403, "Permission Denied", "Invalid Account", p.SignInMessage)
250+ return
251+ }
252+ }
253+ cookie, err := req.Cookie(p.CookieKey)
254+ var ok bool
255+ var email string
256+ var user string
257+ if err == nil {
258+ email, ok = validateCookie(cookie, p.CookieSeed)
259+ user = strings.Split(email, "@")[0]
260+ }
261+
262+ if !ok {
263+ user, ok = p.CheckBasicAuth(req)
264+ }
265+
266+ if !ok {
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)
270+ return
271+ }
272+
273+ // At this point, the user is authenticated. proxy normally
274+ if *passBasicAuth {
275+ req.SetBasicAuth(user, "")
276+ req.Header["X-Forwarded-User"] = []string{user}
277+ }
278+
279+ p.serveMux.ServeHTTP(rw, req)
280+}
281+
282+func (p *OauthProxy) CheckBasicAuth(req *http.Request) (string, bool) {
283+ if p.HtpasswdFile == nil {
284+ return "", false
285+ }
286+ s := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
287+ if len(s) != 2 || s[0] != "Basic" {
288+ return "", false
289+ }
290+ b, err := base64.StdEncoding.DecodeString(s[1])
291+ if err != nil {
292+ return "", false
293+ }
294+ pair := strings.SplitN(string(b), ":", 2)
295+ if len(pair) != 2 {
296+ return "", false
297+ }
298+ if p.HtpasswdFile.Validate(pair[0], pair[1]) {
299+ return pair[0], true
300+ }
301+ return "", false
302+}