oauth2 with google and github
This commit is contained in:
+27
-1
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@ version: '2'
|
||||
services:
|
||||
|
||||
remark:
|
||||
build: .
|
||||
image: umputun/remark:develop
|
||||
container_name: "remark"
|
||||
hostname: "remark"
|
||||
|
||||
Reference in New Issue
Block a user