diff --git a/app/main.go b/app/main.go index 04f72d5b..ad3c17d7 100644 --- a/app/main.go +++ b/app/main.go @@ -24,6 +24,8 @@ var opts struct { 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"` + YandexCID string `long:"yandex-cid" env:"REMARK_YANDEX_CID" description:"Yandex OAuth client ID"` + YandexCSEC string `long:"yandex-csec" env:"REMARK_YANDEX_CSEC" description:"Yandex 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"` diff --git a/app/rest/auth/auth.go b/app/rest/auth/auth.go index 78531192..58bc3fd7 100644 --- a/app/rest/auth/auth.go +++ b/app/rest/auth/auth.go @@ -1,16 +1,35 @@ package auth import ( + "context" "crypto/rand" "crypto/sha1" "encoding/gob" + "encoding/json" "fmt" + "io/ioutil" + "log" + "net/http" + "github.com/go-chi/render" "github.com/gorilla/sessions" + "golang.org/x/oauth2" "github.com/umputun/remark/app/store" ) +type Provider struct { + Name string + RedirectURL string + InfoURL string + Endpoint oauth2.Endpoint + Scopes []string + MapUser func(map[string]interface{}) store.User + + *sessions.FilesystemStore + conf *oauth2.Config +} + type Params struct { Cid string Csecret string @@ -18,6 +37,92 @@ type Params struct { Admins []string } +// newProvider makes auth for given provider +func initProvider(p Params, provider Provider) *Provider { + log.Printf("[INFO] create %s auth, id=%s", provider.Name, p.Cid) + + conf := oauth2.Config{ + ClientID: p.Cid, + ClientSecret: p.Csecret, + RedirectURL: provider.RedirectURL, + Scopes: provider.Scopes, + Endpoint: provider.Endpoint, + } + + provider.conf = &conf + provider.FilesystemStore = p.SessionStore + return &provider +} + +// LoginHandler - GET /login/github +func (p Provider) LoginHandler(w http.ResponseWriter, r *http.Request) { + + // make state (random) and store in session + state := randToken() + session, err := p.Get(r, "remark") + if err != nil { + log.Printf("[WARN] %s", err) + } + session.Values["state-"+p.Name] = state + session.Save(r, w) + + // return login url + log.Printf("[DEBUG] login url %s", p.conf.AuthCodeURL(state)) + http.Redirect(w, r, p.conf.AuthCodeURL(state), http.StatusTemporaryRedirect) +} + +// AuthHandler is redirect url. Should check state to prevent CSRF. +func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) { + session, err := p.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-"+p.Name] + if !ok || retrievedState != r.URL.Query().Get("state") { + http.Error(w, fmt.Sprintf("unexpected state %s", retrievedState.(string)), http.StatusUnauthorized) + return + } + + tok, err := p.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 := p.conf.Client(context.Background(), tok) + uinfo, err := client.Get(p.InfoURL) + if err != nil { + http.Error(w, fmt.Sprintf("failed to get client info via %s, %s", p.InfoURL, 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"] = p.MapUser(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 randToken() string { b := make([]byte, 32) rand.Read(b) diff --git a/app/rest/auth/github.go b/app/rest/auth/github.go deleted file mode 100644 index c1e92b06..00000000 --- a/app/rest/auth/github.go +++ /dev/null @@ -1,121 +0,0 @@ -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 -} diff --git a/app/rest/auth/google.go b/app/rest/auth/google.go deleted file mode 100644 index e098dfb7..00000000 --- a/app/rest/auth/google.go +++ /dev/null @@ -1,123 +0,0 @@ -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 -} diff --git a/app/rest/auth/providers.go b/app/rest/auth/providers.go new file mode 100644 index 00000000..a448faf8 --- /dev/null +++ b/app/rest/auth/providers.go @@ -0,0 +1,58 @@ +package auth + +import ( + "strings" + + "golang.org/x/oauth2/github" + "golang.org/x/oauth2/google" + + "github.com/umputun/remark/app/store" +) + +// NewGoogle makes google oauth2 provider +func NewGoogle(p Params) *Provider { + return initProvider(p, Provider{ + Name: "google", + Endpoint: google.Endpoint, + RedirectURL: "http://remark.umputun.com:8080/auth/google", + Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"}, + InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo", + FilesystemStore: p.SessionStore, + MapUser: func(data map[string]interface{}) store.User { + userInfo := store.User{ + Name: data["name"].(string), + ID: data["email"].(string), + Picture: data["picture"].(string), + Profile: data["profile"].(string), + } + if userInfo.Name == "" { + userInfo.Name = strings.Split(userInfo.ID, "@")[0] + } + return userInfo + }, + }) +} + +// NewGithub makes github oauth2 provider +func NewGithub(p Params) *Provider { + return initProvider(p, Provider{ + Name: "github", + Endpoint: github.Endpoint, + RedirectURL: "http://remark.umputun.com:8080/auth/github", + Scopes: []string{"user:email"}, + InfoURL: "https://api.github.com/user", + FilesystemStore: p.SessionStore, + MapUser: func(data map[string]interface{}) store.User { + userInfo := store.User{ + ID: data["login"].(string), + Name: data["name"].(string), + Picture: data["avatar_url"].(string), + Profile: data["html_url"].(string), + } + if userInfo.Name == "" { + userInfo.Name = userInfo.ID + } + return userInfo + }, + }) +} diff --git a/app/rest/server.go b/app/rest/server.go index f26ef866..6d880483 100644 --- a/app/rest/server.go +++ b/app/rest/server.go @@ -20,8 +20,8 @@ import ( type Server struct { Version string Store store.Interface - AuthGoogle *auth.Google - AuthGithub *auth.Github + AuthGoogle *auth.Provider + AuthGithub *auth.Provider SessionStore *sessions.FilesystemStore } @@ -29,7 +29,8 @@ type Server struct { func (s *Server) Run() { log.Print("[INFO] activate rest server") router := chi.NewRouter() - router.Use(middleware.RealIP, Recoverer) + //router.Use(middleware.RealIP, Recoverer) + router.Use(middleware.RealIP) router.Use(middleware.Throttle(100), middleware.Timeout(60*time.Second)) router.Use(Limiter(10), AppInfo("remark", s.Version), Ping)