From 55f24b2de2199d80030d6a6e3a5f2fb9f9b0a609 Mon Sep 17 00:00:00 2001 From: Adhityaa Chandrasekar Date: Wed, 30 Jan 2019 21:15:16 -0500 Subject: [PATCH] api: add github oauth Closes https://gitlab.com/commento/commento/issues/20 --- api/Gopkg.lock | 4 +- api/config.go | 3 + api/oauth.go | 4 ++ api/oauth_github.go | 43 +++++++++++++ api/oauth_github_callback.go | 115 +++++++++++++++++++++++++++++++++++ api/oauth_github_redirect.go | 25 ++++++++ api/router_api.go | 3 + frontend/js/commento.js | 4 ++ 8 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 api/oauth_github.go create mode 100644 api/oauth_github_callback.go create mode 100644 api/oauth_github_redirect.go diff --git a/api/Gopkg.lock b/api/Gopkg.lock index 046164f..bd83984 100644 --- a/api/Gopkg.lock +++ b/api/Gopkg.lock @@ -116,10 +116,11 @@ [[projects]] branch = "master" - digest = "1:bea0314c10bd362ab623af4880d853b5bad3b63d0ab9945c47e461b8d04203ed" + digest = "1:82e6e4dc5ab71680d89684e4649be630fdeeaf81feb8e88e4a56273a0cd4d966" name = "golang.org/x/oauth2" packages = [ ".", + "github", "google", "internal", "jws", @@ -161,6 +162,7 @@ "github.com/russross/blackfriday", "golang.org/x/crypto/bcrypt", "golang.org/x/oauth2", + "golang.org/x/oauth2/github", "golang.org/x/oauth2/google", ] solver-name = "gps-cdcl" diff --git a/api/config.go b/api/config.go index 333856f..6886338 100644 --- a/api/config.go +++ b/api/config.go @@ -47,6 +47,9 @@ func configParse() error { "GOOGLE_KEY": "", "GOOGLE_SECRET": "", + + "GITHUB_KEY": "", + "GITHUB_SECRET": "", } for key, value := range defaults { diff --git a/api/oauth.go b/api/oauth.go index bb20e14..845caec 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -11,5 +11,9 @@ func oauthConfigure() error { return err } + if err := githubOauthConfigure(); err != nil { + return err + } + return nil } diff --git a/api/oauth_github.go b/api/oauth_github.go new file mode 100644 index 0000000..d92a017 --- /dev/null +++ b/api/oauth_github.go @@ -0,0 +1,43 @@ +package main + +import ( + "golang.org/x/oauth2" + "golang.org/x/oauth2/github" + "os" +) + +var githubConfig *oauth2.Config + +func githubOauthConfigure() error { + githubConfig = nil + if os.Getenv("GITHUB_KEY") == "" && os.Getenv("GITHUB_SECRET") == "" { + return nil + } + + if os.Getenv("GITHUB_KEY") == "" { + logger.Errorf("COMMENTO_GITHUB_KEY not configured, but COMMENTO_GITHUB_SECRET is set") + return errorOauthMisconfigured + } + + if os.Getenv("GITHUB_SECRET") == "" { + logger.Errorf("COMMENTO_GITHUB_SECRET not configured, but COMMENTO_GITHUB_KEY is set") + return errorOauthMisconfigured + } + + logger.Infof("loading github OAuth config") + + githubConfig = &oauth2.Config{ + RedirectURL: os.Getenv("ORIGIN") + "/api/oauth/github/callback", + ClientID: os.Getenv("GITHUB_KEY"), + ClientSecret: os.Getenv("GITHUB_SECRET"), + Scopes: []string{ + "read:user", + "user:email", + }, + Endpoint: github.Endpoint, + } + + configuredOauths = append(configuredOauths, "github") + + return nil +} diff --git a/api/oauth_github_callback.go b/api/oauth_github_callback.go new file mode 100644 index 0000000..848ae87 --- /dev/null +++ b/api/oauth_github_callback.go @@ -0,0 +1,115 @@ +package main + +import ( + "encoding/json" + "fmt" + "golang.org/x/oauth2" + "io/ioutil" + "net/http" +) + +func githubGetPrimaryEmail(accessToken string) (string, error) { + resp, err := http.Get("https://api.github.com/user/emails?access_token=" + accessToken) + defer resp.Body.Close() + + contents, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errorCannotReadResponse + } + + user := []map[string]interface{}{} + if err := json.Unmarshal(contents, &user); err != nil { + logger.Errorf("error unmarshaling github user: %v", err) + return "", errorInternal + } + + nonPrimaryEmail := "" + for _, email := range(user) { + nonPrimaryEmail = email["email"].(string) + if email["primary"].(bool) { + return email["email"].(string), nil + } + } + + return nonPrimaryEmail, nil +} + +func githubCallbackHandler(w http.ResponseWriter, r *http.Request) { + commenterToken := r.FormValue("state") + code := r.FormValue("code") + + _, err := commenterGetByCommenterToken(commenterToken) + if err != nil && err != errorNoSuchToken { + fmt.Fprintf(w, "Error: %s\n", err.Error()) + return + } + + token, err := githubConfig.Exchange(oauth2.NoContext, code) + if err != nil { + fmt.Fprintf(w, "Error: %s", err.Error()) + return + } + + email, err := githubGetPrimaryEmail(token.AccessToken) + if err != nil { + fmt.Fprintf(w, "Error: %s", err.Error()) + return + } + + resp, err := http.Get("https://api.github.com/user?access_token=" + token.AccessToken) + defer resp.Body.Close() + + contents, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Fprintf(w, "Error: %s", errorCannotReadResponse.Error()) + return + } + + user := make(map[string]interface{}) + if err := json.Unmarshal(contents, &user); err != nil { + fmt.Fprintf(w, "Error: %s", errorInternal.Error()) + return + } + + if email == "" { + if user["email"] == nil { + fmt.Fprintf(w, "Error: no email address returned by Github") + return + } + + email = user["email"].(string) + } + + c, err := commenterGetByEmail("github", email) + if err != nil && err != errorNoSuchCommenter { + fmt.Fprintf(w, "Error: %s", err.Error()) + return + } + + var commenterHex string + + // TODO: in case of returning users, update the information we have on record? + if err == errorNoSuchCommenter { + var link string + if val, ok := user["html_url"]; ok { + link = val.(string) + } else { + link = "undefined" + } + + commenterHex, err = commenterNew(email, user["name"].(string), link, user["avatar_url"].(string), "github", "") + if err != nil { + fmt.Fprintf(w, "Error: %s", err.Error()) + return + } + } else { + commenterHex = c.CommenterHex + } + + if err := commenterSessionUpdate(commenterToken, commenterHex); err != nil { + fmt.Fprintf(w, "Error: %s", err.Error()) + return + } + + fmt.Fprintf(w, "") +} diff --git a/api/oauth_github_redirect.go b/api/oauth_github_redirect.go new file mode 100644 index 0000000..53a9474 --- /dev/null +++ b/api/oauth_github_redirect.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "net/http" +) + +func githubRedirectHandler(w http.ResponseWriter, r *http.Request) { + if githubConfig == nil { + logger.Errorf("github oauth access attempt without configuration") + fmt.Fprintf(w, "error: this website has not configured github OAuth") + return + } + + commenterToken := r.FormValue("commenterToken") + + _, err := commenterGetByCommenterToken(commenterToken) + if err != nil && err != errorNoSuchToken { + fmt.Fprintf(w, "error: %s\n", err.Error()) + return + } + + url := githubConfig.AuthCodeURL(commenterToken) + http.Redirect(w, r, url, http.StatusFound) +} diff --git a/api/router_api.go b/api/router_api.go index 58ae50f..59bae75 100644 --- a/api/router_api.go +++ b/api/router_api.go @@ -29,6 +29,9 @@ func apiRouterInit(router *mux.Router) error { router.HandleFunc("/api/oauth/google/redirect", googleRedirectHandler).Methods("GET") router.HandleFunc("/api/oauth/google/callback", googleCallbackHandler).Methods("GET") + router.HandleFunc("/api/oauth/github/redirect", githubRedirectHandler).Methods("GET") + router.HandleFunc("/api/oauth/github/callback", githubCallbackHandler).Methods("GET") + router.HandleFunc("/api/comment/new", commentNewHandler).Methods("POST") router.HandleFunc("/api/comment/list", commentListHandler).Methods("POST") router.HandleFunc("/api/comment/count", commentCountHandler).Methods("POST") diff --git a/frontend/js/commento.js b/frontend/js/commento.js index 0791576..1637976 100644 --- a/frontend/js/commento.js +++ b/frontend/js/commento.js @@ -269,6 +269,8 @@ avatar = create("img"); if (resp.commenter.provider === "google") { attrSet(avatar, "src", resp.commenter.photo + "?sz=50"); + } else if (resp.commenter.provider === "github") { + attrSet(avatar, "src", resp.commenter.photo + "&s=50"); } else { attrSet(avatar, "src", resp.commenter.photo); } @@ -699,6 +701,8 @@ avatar = create("img"); if (commenter.provider === "google") { attrSet(avatar, "src", commenter.photo + "?sz=50"); + } else if (commenter.provider === "github") { + attrSet(avatar, "src", commenter.photo + "&s=50"); } else { attrSet(avatar, "src", commenter.photo); }