Skip to content

Instantly share code, notes, and snippets.

@imjasonh
Last active December 10, 2015 00:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save imjasonh/4349047 to your computer and use it in GitHub Desktop.
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
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