common provider code

This commit is contained in:
Eugene
2017-12-21 23:52:52 -06:00
parent 9ea4c305e2
commit e22b5f0db7
6 changed files with 169 additions and 247 deletions
+2
View File
@@ -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"`
+105
View File
@@ -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)
-121
View File
@@ -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
}
-123
View File
@@ -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
}
+58
View File
@@ -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
},
})
}
+4 -3
View File
@@ -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)