Files
remark42/app/rest/auth/provider.go
T
2018-05-24 23:33:12 -05:00

211 lines
5.4 KiB
Go

package auth
import (
"context"
"crypto/rand"
"crypto/sha1"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-chi/chi"
"github.com/go-chi/render"
"golang.org/x/oauth2"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/proxy"
"github.com/umputun/remark/app/store"
)
// Provider represents oauth2 provider
type Provider struct {
Params
Name string
RedirectURL string
InfoURL string
Endpoint oauth2.Endpoint
Scopes []string
MapUser func(userData, []byte) store.User // map info from InfoURL to User
conf oauth2.Config
}
// Params to make initialized and ready to use provider
type Params struct {
RemarkURL string
AvatarProxy *proxy.Avatar
JwtService *JWT
SecretKey string
Admins []string
Cid string
Csecret string
}
type userData map[string]interface{}
func (u userData) value(key string) string {
if val, ok := u[key]; ok {
return fmt.Sprintf("%v", val)
}
return ""
}
// newProvider makes auth for given provider
func initProvider(p Params, provider Provider) Provider {
log.Printf("[INFO] init auth provider %s", provider.Name)
provider.Params = p
provider.conf = oauth2.Config{
ClientID: provider.Cid,
ClientSecret: provider.Csecret,
RedirectURL: provider.RedirectURL,
Scopes: provider.Scopes,
Endpoint: provider.Endpoint,
}
log.Printf("[DEBUG] created %s auth, id=%s, redir=%s, endpoint=%s",
provider.Name, provider.Cid, provider.Endpoint, provider.RedirectURL)
return provider
}
// Routes returns auth routes for given provider
func (p Provider) Routes() chi.Router {
router := chi.NewRouter()
router.Get("/login", p.loginHandler)
router.Get("/callback", p.authHandler)
router.Get("/logout", p.LogoutHandler)
return router
}
// loginHandler - GET /login?from=redirect-back-url
func (p Provider) loginHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] login with %s", p.Name)
// make state (random) and store in session
state := p.randToken()
claims := CustomClaims{
State: state,
From: r.URL.Query().Get("from"),
StandardClaims: jwt.StandardClaims{
Id: p.randToken(),
Issuer: "remark42",
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
},
}
if err := p.JwtService.Set(w, &claims); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set jwt")
return
}
// return login url
loginURL := p.conf.AuthCodeURL(state)
log.Printf("[DEBUG] login url %s", loginURL)
http.Redirect(w, r, loginURL, http.StatusFound)
}
// authHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser
// GET /callback
func (p Provider) authHandler(w http.ResponseWriter, r *http.Request) {
oauthClaims, err := p.JwtService.Get(r)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to get jwt")
return
}
retrievedState := oauthClaims.State
if retrievedState == "" || retrievedState != r.URL.Query().Get("state") {
http.Error(w, fmt.Sprintf("unexpected state %v", retrievedState), http.StatusUnauthorized)
return
}
log.Printf("[DEBUG] auth with state %s", retrievedState)
tok, err := p.conf.Exchange(context.Background(), r.URL.Query().Get("code"))
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "exchange failed")
return
}
client := p.conf.Client(context.Background(), tok)
uinfo, err := client.Get(p.InfoURL)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, fmt.Sprintf("failed to get client info via %s", p.InfoURL))
return
}
defer func() {
if e := uinfo.Body.Close(); e != nil {
log.Printf("[WARN] failed to close response body, %s", e)
}
}()
data, err := ioutil.ReadAll(uinfo.Body)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to read user info")
return
}
jData := map[string]interface{}{}
if e := json.Unmarshal(data, &jData); e != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to unmarshal user info")
return
}
log.Printf("[DEBUG] got raw user info %+v", jData)
u := p.MapUser(jData, data)
if p.AvatarProxy != nil {
if avatarURL, e := p.AvatarProxy.Put(u); e == nil {
u.Picture = avatarURL
} else {
log.Printf("[WARN] failed to proxy avatar, %s", e)
}
}
u.Admin = isAdmin(u.ID, p.Admins)
authClaims := &CustomClaims{
User: &u,
StandardClaims: jwt.StandardClaims{
Issuer: "remark42",
Id: p.randToken(),
},
}
if err = p.JwtService.Set(w, authClaims); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save user info")
return
}
log.Printf("[DEBUG] user info %+v", u)
// redirect to back url if presented in login query params
if oauthClaims.From != "" {
http.Redirect(w, r, oauthClaims.From, http.StatusTemporaryRedirect)
return
}
render.JSON(w, r, &u)
}
// LogoutHandler - GET /logout
func (p Provider) LogoutHandler(w http.ResponseWriter, r *http.Request) {
p.JwtService.Reset(w)
log.Printf("[DEBUG] logout")
}
func (p Provider) randToken() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
log.Fatalf("[ERROR] can't get randoms, %s", err)
}
s := sha1.New()
if _, err := s.Write(b); err != nil {
log.Printf("[WARN] can't write randoms, %s", err)
}
return fmt.Sprintf("%x", s.Sum(nil))
}