Last active
December 10, 2015 00:09
-
-
Save imjasonh/4349047 to your computer and use it in GitHub Desktop.
Recipe to require that a user log in and go through an OAuth flow before reaching an http Handler func. This is similar to google-api-python-client's OAuth2Decorator (https://developers.google.com/api-client-library/python/platforms/google_app_engine#Decorators) This is based on the mustlogin.go gist here: https://gist.github.com/4337383
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package mustoauth | |
import ( | |
"appengine" | |
"appengine/datastore" | |
"appengine/memcache" | |
"appengine/urlfetch" | |
"appengine/user" | |
"encoding/json" | |
"fmt" | |
"html/template" | |
"io" | |
"net/http" | |
"strings" | |
"code.google.com/p/goauth2/oauth" | |
) | |
var myConfig = &oauth.Config{ | |
ClientId: "YOUR_CLIENT_ID", | |
ClientSecret: "YOUR_CLIENT_SECRET", | |
Scope: "https://www.googleapis.com/auth/userinfo.email", | |
AuthURL: "https://accounts.google.com/o/oauth2/auth", | |
TokenURL: "https://accounts.google.com/o/oauth2/token", | |
} | |
func init() { | |
http.HandleFunc("/mustlogin", MustLogin(loggedIn)) | |
http.HandleFunc("/mustoauth", MustOAuth(myConfig, oauthed)) | |
http.HandleFunc(RedirectPath, NewOAuthCallbackHandlerFunc(*myConfig)) | |
} | |
const loggedInTmpl = ` | |
<html><body> | |
You are {{.Email}}<br /> | |
<a href="{{.LogoutURL}}">Log out</a> | |
</body></html> | |
` | |
func loggedIn(w http.ResponseWriter, r *http.Request, u user.User) { | |
url, _ := user.LogoutURL(appengine.NewContext(r), r.URL.String()) | |
t := template.Must(template.New("loggedIn").Parse(loggedInTmpl)) | |
t.Execute(w, map[string]interface{}{ | |
"Email": u.Email, | |
"LogoutURL": url, | |
}) | |
} | |
func oauthed(w http.ResponseWriter, r *http.Request, u user.User, t *oauth.Transport) { | |
resp, _ := t.Client().Get("https://www.googleapis.com/oauth2/v2/userinfo") | |
io.Copy(w, resp.Body) | |
} | |
///////////////// CUT HERE FOR LIBRARY CODE /////////////////// | |
const kind = "oauth.Token" | |
var RedirectPath = "/oauthcallback" | |
type LoggedInHandlerFunc func(http.ResponseWriter, *http.Request, user.User) | |
func MustLogin(handler LoggedInHandlerFunc) http.HandlerFunc { | |
return func(w http.ResponseWriter, r *http.Request) { | |
c := appengine.NewContext(r) | |
if u := user.Current(c); u == nil { | |
// If the user isn't logged in, redirect to login form. | |
url, _ := user.LoginURL(c, r.URL.String()) | |
http.Redirect(w, r, url, http.StatusFound) | |
} else { | |
handler(w, r, *u) | |
} | |
} | |
} | |
type OAuthedHandlerFunc func(http.ResponseWriter, *http.Request, user.User, *oauth.Transport) | |
func MustOAuth(conf *oauth.Config, handler OAuthedHandlerFunc) http.HandlerFunc { | |
return MustLogin(func(w http.ResponseWriter, r *http.Request, u user.User) { | |
c := appengine.NewContext(r) | |
conf.RedirectURL = getRedirectURL(c) | |
conf.TokenCache = datastoreCache{c, u} | |
if t, _ := conf.TokenCache.Token(); t == nil || t.Expired() { | |
http.Redirect(w, r, conf.AuthCodeURL(r.URL.String()), http.StatusFound) | |
return | |
} | |
trans := &oauth.Transport{ | |
Config: conf, | |
Transport: &urlfetch.Transport{Context: c}, | |
} | |
handler(w, r, u, trans) | |
}) | |
} | |
func getRedirectURL(c appengine.Context) string { | |
if appengine.IsDevAppServer() { | |
return "http://localhost:8080" + RedirectPath | |
} | |
v := strings.Split(appengine.VersionID(c), ".")[0] | |
appid := appengine.AppID(c) | |
return fmt.Sprintf("http://%s.%s.appspot.com%s", v, appid, RedirectPath) | |
} | |
func NewOAuthCallbackHandlerFunc(config oauth.Config) http.HandlerFunc { | |
return func(w http.ResponseWriter, r *http.Request) { | |
if e := r.FormValue("error"); e != "" { | |
fmt.Fprintf(w, "Authorization request failed:", e) | |
return | |
} | |
c := appengine.NewContext(r) | |
// Make a local copy of the config. | |
conf := &oauth.Config{ | |
ClientId: config.ClientId, | |
ClientSecret: config.ClientSecret, | |
Scope: config.Scope, | |
AuthURL: config.AuthURL, | |
TokenURL: config.TokenURL, | |
RedirectURL: getRedirectURL(c), | |
} | |
u := *user.Current(c) | |
conf.TokenCache = datastoreCache{c, u} | |
t := &oauth.Transport{ | |
Config: conf, | |
Transport: &urlfetch.Transport{Context: c}, | |
} | |
_, err := t.Exchange(r.FormValue("code")) | |
if err != nil { | |
// TODO: Not sure why this occasionally happens. The error is "parse : empty url" | |
fmt.Fprintf(w, "Error exchanging code: %v", err) | |
return | |
} | |
back := r.FormValue("state") | |
http.Redirect(w, r, back, http.StatusFound) | |
} | |
} | |
type datastoreCache struct { | |
c appengine.Context | |
u user.User | |
} | |
func (d datastoreCache) Token() (*oauth.Token, error) { | |
k := datastore.NewKey(d.c, kind, d.u.ID, 0, nil) | |
t := new(oauth.Token) | |
// Try to get it from memcache first. | |
item, e := memcache.Get(d.c, d.u.ID) | |
if e == nil { | |
b := item.Value | |
if e = json.Unmarshal(b, t); e == nil { | |
return t, nil | |
} | |
} | |
// Fall back to the datastore | |
err := datastore.Get(d.c, k, t) | |
if err == datastore.ErrNoSuchEntity { | |
return nil, nil | |
} | |
return t, err | |
} | |
func (d datastoreCache) PutToken(t *oauth.Token) (err error) { | |
k := datastore.NewKey(d.c, kind, d.u.ID, 0, nil) | |
if _, err = datastore.Put(d.c, k, t); err != nil { | |
d.c.Errorf(err.Error()) | |
return | |
} | |
// JSONify and store in memcache too (don't care if it fails) | |
encoded, e := json.Marshal(t) | |
if e == nil { | |
memcache.Set(d.c, &memcache.Item{ | |
Key: d.u.ID, | |
Value: encoded, | |
}) | |
} | |
return | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment