oauth2 with google and github

This commit is contained in:
Eugene
2017-12-21 20:41:34 -06:00
parent 4a99c704a4
commit 3c74c3f214
8 changed files with 351 additions and 18 deletions
+27 -1
View File
@@ -5,8 +5,10 @@ import (
"log"
"os"
"github.com/gorilla/sessions"
"github.com/hashicorp/logutils"
"github.com/jessevdk/go-flags"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/rest"
@@ -14,7 +16,17 @@ import (
var opts struct {
DBFile string `long:"db" env:"BOLTDB_FILE" default:"/tmp/remark.db" description:"bolt file name"`
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
SessionStore string `long:"session" env:"SESSION_STORE" default:"/tmp" description:"path to session store directory"`
StoreKey string `long:"store-key" env:"STORE_KEY" default:"secure-store-key" description:"store key"`
GoogleCID string `long:"google-cid" env:"REMARK_GOOGLE_CID" description:"Google OAuth client ID"`
GoogleCSEC string `long:"google-csec" env:"REMARK_GOOGLE_CSEC" description:"Google OAuth client secret"`
GithubCID string `long:"github-cid" env:"REMARK_GITHUB_CID" description:"Github OAuth client ID"`
GithubCSEC string `long:"github-csec" env:"REMARK_GITHUB_CSEC" description:"Github OAuth client secret"`
Admins []string `long:"admin" env:"ADMIN" default:"umputun@gmail.com" description:"admin(s) names" env-delim:","`
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
}
var revision = "unknown"
@@ -32,10 +44,24 @@ func main() {
log.Fatalf("[ERROR] can't initialize data store, %+v", err)
}
sessionStore := sessions.NewFilesystemStore(opts.SessionStore, []byte(opts.StoreKey))
srv := rest.Server{
Version: revision,
Store: dataStore,
AuthGoogle: auth.NewGoogle(auth.Params{
Cid: opts.GoogleCID,
Csecret: opts.GoogleCSEC,
SessionStore: sessionStore,
Admins: opts.Admins,
}),
AuthGithub: auth.NewGithub(auth.Params{
Cid: opts.GithubCID,
Csecret: opts.GithubCSEC,
SessionStore: sessionStore,
Admins: opts.Admins,
}),
}
srv.Run()
}
+43
View File
@@ -0,0 +1,43 @@
package auth
import (
"crypto/rand"
"crypto/sha1"
"encoding/gob"
"fmt"
"sync"
"github.com/gorilla/sessions"
"github.com/umputun/remark/app/store"
)
type Params struct {
Cid string
Csecret string
SessionStore *sessions.FilesystemStore
Admins []string
}
type SessionStore struct {
StorePath string
StoreKey string
store *sessions.FilesystemStore
once sync.Once
}
func (s *SessionStore) GetSession(name string) {
}
func randToken() string {
b := make([]byte, 32)
rand.Read(b)
s := sha1.New()
s.Write(b)
return fmt.Sprintf("%x", s.Sum(nil))
}
func init() {
gob.Register(store.User{})
}
+121
View File
@@ -0,0 +1,121 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/go-chi/render"
"github.com/gorilla/sessions"
"github.com/umputun/remark/app/store"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)
// Github provides oauth2 and session store
type Github struct {
*sessions.FilesystemStore
conf *oauth2.Config
}
// NewGithub makes auth with github
func NewGithub(p Params) *Github {
log.Printf("[INFO] create gihub auth, id=%s", p.Cid)
conf := oauth2.Config{
ClientID: p.Cid,
ClientSecret: p.Csecret,
RedirectURL: "http://remark.umputun.com:8080/auth/github",
Scopes: []string{
"user:email",
},
Endpoint: github.Endpoint,
}
return &Github{conf: &conf, FilesystemStore: p.SessionStore}
}
// LoginHandler - GET /login/github
func (a Github) LoginHandler(w http.ResponseWriter, r *http.Request) {
// make state (random) and store in session
state := randToken()
session, err := a.Get(r, "remark")
if err != nil {
log.Printf("[WARN] %s", err)
}
session.Values["state-github"] = state
session.Save(r, w)
// return login url
log.Printf("[DEBUG] login url %s", a.conf.AuthCodeURL(state))
http.Redirect(w, r, a.conf.AuthCodeURL(state), http.StatusTemporaryRedirect)
}
// AuthHandler is redirect url. Should check state to prevent CSRF.
func (a Github) AuthHandler(w http.ResponseWriter, r *http.Request) {
session, err := a.Get(r, "remark")
if err != nil {
http.Error(w, fmt.Sprintf("failed to get session, %s", err.Error()), http.StatusInternalServerError)
return
}
// compare saved state to the one from redirect url
retrievedState, ok := session.Values["state-github"]
if !ok || retrievedState != r.URL.Query().Get("state") {
http.Error(w, fmt.Sprintf("unexpected state %s", retrievedState.(string)), http.StatusUnauthorized)
return
}
tok, err := a.conf.Exchange(context.Background(), r.URL.Query().Get("code"))
if err != nil {
http.Error(w, fmt.Sprintf("exchange failed, %s", err), http.StatusInternalServerError)
return
}
client := a.conf.Client(context.Background(), tok)
uinfo, err := client.Get("https://api.github.com/user")
if err != nil {
http.Error(w, fmt.Sprintf("failed to get client info, %s", err), http.StatusBadRequest)
return
}
defer uinfo.Body.Close()
data, err := ioutil.ReadAll(uinfo.Body)
if err != nil {
http.Error(w, fmt.Sprintf("failed to read user info, %s", err), http.StatusInternalServerError)
return
}
jData := map[string]interface{}{}
if e := json.Unmarshal(data, &jData); e != nil {
http.Error(w, fmt.Sprintf("failed to unmarshal user info, %s", err), http.StatusInternalServerError)
return
}
log.Printf("[DEBUG] got raw user info %+v", jData)
session.Values["uinfo"] = a.makeUserInfo(jData)
if err = session.Save(r, w); err != nil {
http.Error(w, fmt.Sprintf("failed to save user info, %s", err), http.StatusInternalServerError)
return
}
log.Printf("[DEBUG] %+v", jData)
render.JSON(w, r, jData)
}
func (a Github) makeUserInfo(jData map[string]interface{}) store.User {
userInfo := store.User{
ID: jData["login"].(string),
Name: jData["name"].(string),
Picture: jData["avatar_url"].(string),
Profile: jData["html_url"].(string),
}
if userInfo.Name == "" {
userInfo.Name = userInfo.ID
}
return userInfo
}
+123
View File
@@ -0,0 +1,123 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/go-chi/render"
"github.com/gorilla/sessions"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"github.com/umputun/remark/app/store"
)
// Google provides oauth2 and session store
type Google struct {
*sessions.FilesystemStore
conf *oauth2.Config
}
// NewGoogle makes auth with google
func NewGoogle(p Params) *Google {
log.Printf("[INFO] create google auth, id=%s", p.Cid)
conf := oauth2.Config{
ClientID: p.Cid,
ClientSecret: p.Csecret,
RedirectURL: "http://remark.umputun.com:8080/auth/google",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
},
Endpoint: google.Endpoint,
}
return &Google{conf: &conf, FilesystemStore: p.SessionStore}
}
// LoginHandler - GET /login/google
func (a Google) LoginHandler(w http.ResponseWriter, r *http.Request) {
// make state (random) and store in session
state := randToken()
session, err := a.Get(r, "remark")
if err != nil {
log.Printf("[WARN] %s", err)
}
session.Values["state-google"] = state
session.Save(r, w)
// return login url
log.Printf("[DEBUG] login url %s", a.conf.AuthCodeURL(state))
http.Redirect(w, r, a.conf.AuthCodeURL(state), http.StatusTemporaryRedirect)
}
// AuthHandler is redirect url. Should check state to prevent CSRF.
func (a Google) AuthHandler(w http.ResponseWriter, r *http.Request) {
session, err := a.Get(r, "remark")
if err != nil {
http.Error(w, fmt.Sprintf("failed to get session, %s", err.Error()), http.StatusInternalServerError)
return
}
// compare saved state to the one from redirect url
retrievedState, ok := session.Values["state-google"]
if !ok || retrievedState != r.URL.Query().Get("state") {
http.Error(w, fmt.Sprintf("unexpected state %s", retrievedState.(string)), http.StatusUnauthorized)
return
}
tok, err := a.conf.Exchange(context.Background(), r.URL.Query().Get("code"))
if err != nil {
http.Error(w, fmt.Sprintf("exchange failed, %s", err), http.StatusInternalServerError)
return
}
client := a.conf.Client(context.Background(), tok)
uinfo, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
if err != nil {
http.Error(w, fmt.Sprintf("failed to get client info, %s", err), http.StatusBadRequest)
return
}
defer uinfo.Body.Close()
data, err := ioutil.ReadAll(uinfo.Body)
if err != nil {
http.Error(w, fmt.Sprintf("failed to read user info, %s", err), http.StatusInternalServerError)
return
}
jData := map[string]interface{}{}
if e := json.Unmarshal(data, &jData); e != nil {
http.Error(w, fmt.Sprintf("failed to unmarshal user info, %s", err), http.StatusInternalServerError)
return
}
log.Printf("[DEBUG] got raw user info %+v", jData)
session.Values["uinfo"] = a.makeUserInfo(jData)
if err = session.Save(r, w); err != nil {
http.Error(w, fmt.Sprintf("failed to save user info, %s", err), http.StatusInternalServerError)
return
}
log.Printf("[DEBUG] %+v", jData)
render.JSON(w, r, jData)
}
func (a Google) makeUserInfo(jData map[string]interface{}) store.User {
userInfo := store.User{
Name: jData["name"].(string),
ID: jData["email"].(string),
Picture: jData["picture"].(string),
Profile: jData["profile"].(string),
}
if userInfo.Name == "" {
userInfo.Name = strings.Split(userInfo.ID, "@")[0]
}
return userInfo
}
+25 -5
View File
@@ -11,13 +11,16 @@ import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/store"
)
// Server is a rest access server
type Server struct {
Version string
Store store.Interface
Version string
Store store.Interface
AuthGoogle *auth.Google
AuthGithub *auth.Github
}
// Run the lister and request's router, activate rest server
@@ -28,11 +31,18 @@ func (s *Server) Run() {
router.Use(middleware.Throttle(100), middleware.Timeout(60*time.Second))
router.Use(Limiter(10), AppInfo("remark", s.Version), Ping)
router.Get("/login/google", s.AuthGoogle.LoginHandler)
router.Get("/auth/google", s.AuthGoogle.AuthHandler)
router.Get("/login/github", s.AuthGithub.LoginHandler)
router.Get("/auth/github", s.AuthGithub.AuthHandler)
router.Post("/comment", s.createCommentCtrl)
router.Delete("/comment/{id}", s.deleteCommentCtrl)
router.Get("/find", s.getURLComments)
router.Get("/last/{max}", s.getLastComments)
router.Get("/id/{id}", s.getByID)
log.Fatal(http.ListenAndServe(":8080", router))
}
@@ -101,7 +111,7 @@ func (s *Server) getURLComments(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, comments)
}
// GET /last/{max}
// GET /last/{max}?url=abc
func (s *Server) getLastComments(w http.ResponseWriter, r *http.Request) {
max, err := strconv.Atoi(chi.URLParam(r, "max"))
@@ -109,8 +119,18 @@ func (s *Server) getLastComments(w http.ResponseWriter, r *http.Request) {
max = 0
}
url := r.URL.Query().Get("url")
log.Printf("[INFO] get comments for %s", url)
session, err := s.AuthGoogle.Get(r, "remark")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
uinfoData, ok := session.Values["uinfo"]
if !ok {
http.Error(w, "login required", http.StatusUnauthorized)
return
}
log.Printf("[DEBUG] user: %+v", uinfoData.(store.User))
comments, err := s.Store.Last(store.Locator{}, max)
if err != nil {
+2 -2
View File
@@ -26,9 +26,9 @@ var lastBucketName = "last"
func NewBoltDB(dbFile string) (*BoltDB, error) {
log.Printf("[INFO] bolt store, %s", dbFile)
result := BoltDB{}
db, err := bolt.Open(dbFile, 0600, &bolt.Options{Timeout: 1 * time.Second})
db, err := bolt.Open(dbFile, 0600, &bolt.Options{Timeout: 5 * time.Second})
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "failed to make boltdb for %s", dbFile)
}
result.DB = db
return &result, err
+9 -10
View File
@@ -4,13 +4,10 @@ import "time"
// Comment represents a single comment with reference to its parent
type Comment struct {
ID int64 `json:"id"`
ParentID int64 `json:"pid"`
Text string `json:"text"`
User User `json:"user"`
ID int64 `json:"id"`
ParentID int64 `json:"pid"`
Text string `json:"text"`
User User `json:"user"`
Locator Locator `json:"locator"`
Score int `json:"score"`
Timestamp time.Time `json:"time"`
@@ -24,9 +21,11 @@ type Locator struct {
// User holds user-related info
type User struct {
Name string `json:"name"`
ID string `json:"id"`
IP string `json:"-"`
Name string `json:"name"`
ID string `json:"id"`
Picture string `json:"picture"`
Profile string `json:"profile"`
IP string `json:"-"`
}
// Request is a container for all finds
+1
View File
@@ -3,6 +3,7 @@ version: '2'
services:
remark:
build: .
image: umputun/remark:develop
container_name: "remark"
hostname: "remark"