From b537f01b41b6c9b3923c966610ef9f66cae08152 Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 27 Dec 2018 14:59:45 -0600 Subject: [PATCH 01/21] all api package compilable with auth lib --- backend/app/rest/api/admin.go | 16 +- backend/app/rest/api/admin_test.go | 45 +-- backend/app/rest/api/migrator_test.go | 19 +- backend/app/rest/api/rest.go | 62 +++-- backend/app/rest/api/rest_private.go | 17 +- backend/app/rest/api/rest_private_test.go | 2 +- backend/app/rest/api/rest_public.go | 14 +- backend/app/rest/api/rest_test.go | 22 +- backend/app/rest/auth/auth.go | 200 -------------- backend/app/rest/auth/auth_test.go | 247 ----------------- backend/app/rest/auth/dev_provider.go | 196 ------------- backend/app/rest/auth/dev_provider_test.go | 77 ------ backend/app/rest/auth/jwt.go | 200 -------------- backend/app/rest/auth/jwt_test.go | 258 ------------------ backend/app/rest/auth/provider.go | 229 ---------------- backend/app/rest/auth/provider_test.go | 238 ---------------- backend/app/rest/auth/providers.go | 126 --------- backend/app/rest/auth/providers_test.go | 83 ------ .../github.com/dgrijalva/jwt-go/.gitignore | 4 - .../github.com/dgrijalva/jwt-go/.travis.yml | 13 - .../github.com/dgrijalva/jwt-go/LICENSE | 8 - .../dgrijalva/jwt-go/MIGRATION_GUIDE.md | 97 ------- .../github.com/dgrijalva/jwt-go/README.md | 100 ------- .../dgrijalva/jwt-go/VERSION_HISTORY.md | 118 -------- .../github.com/dgrijalva/jwt-go/claims.go | 134 --------- .../vendor/github.com/dgrijalva/jwt-go/doc.go | 4 - .../github.com/dgrijalva/jwt-go/ecdsa.go | 148 ---------- .../dgrijalva/jwt-go/ecdsa_utils.go | 67 ----- .../github.com/dgrijalva/jwt-go/errors.go | 59 ---- .../github.com/dgrijalva/jwt-go/hmac.go | 95 ------- .../github.com/dgrijalva/jwt-go/map_claims.go | 94 ------- .../github.com/dgrijalva/jwt-go/none.go | 52 ---- .../github.com/dgrijalva/jwt-go/parser.go | 148 ---------- .../vendor/github.com/dgrijalva/jwt-go/rsa.go | 101 ------- .../github.com/dgrijalva/jwt-go/rsa_pss.go | 126 --------- .../github.com/dgrijalva/jwt-go/rsa_utils.go | 101 ------- .../dgrijalva/jwt-go/signing_method.go | 35 --- .../github.com/dgrijalva/jwt-go/token.go | 108 -------- 38 files changed, 113 insertions(+), 3550 deletions(-) delete mode 100644 backend/app/rest/auth/auth.go delete mode 100644 backend/app/rest/auth/auth_test.go delete mode 100644 backend/app/rest/auth/dev_provider.go delete mode 100644 backend/app/rest/auth/dev_provider_test.go delete mode 100644 backend/app/rest/auth/jwt.go delete mode 100644 backend/app/rest/auth/jwt_test.go delete mode 100644 backend/app/rest/auth/provider.go delete mode 100644 backend/app/rest/auth/provider_test.go delete mode 100644 backend/app/rest/auth/providers.go delete mode 100644 backend/app/rest/auth/providers_test.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/.gitignore delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/.travis.yml delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/LICENSE delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/README.md delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/claims.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/doc.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/ecdsa.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/errors.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/hmac.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/map_claims.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/none.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/parser.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/rsa.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/signing_method.go delete mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/token.go diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index d2be7c9e..8f6c3a2f 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -9,11 +9,11 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + "github.com/go-pkgz/auth" R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/umputun/remark/backend/app/rest" - "github.com/umputun/remark/backend/app/rest/auth" "github.com/umputun/remark/backend/app/rest/proxy" "github.com/umputun/remark/backend/app/store" "github.com/umputun/remark/backend/app/store/service" @@ -23,7 +23,7 @@ import ( type admin struct { dataService *service.DataStore cache cache.LoadingCache - authenticator auth.Authenticator + authenticator auth.Service readOnlyAge int avatarProxy *proxy.Avatar migrator *Migrator @@ -102,21 +102,21 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { token := r.URL.Query().Get("token") - claims, err := a.authenticator.JWTService.Parse(token) + claims, err := a.authenticator.TokenService().Parse(token) if err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't process token") return } - log.Printf("[INFO] delete all user comments by request for %s, site %s", claims.User.ID, claims.SiteID) + log.Printf("[INFO] delete all user comments by request for %s, site %s", claims.User.ID, claims.Audience) // deleteme set by deleteMeCtrl, this check just to make sure we not trying to delete with leaked token - if !claims.Flags.DeleteMe { + if val, err := claims.User.BoolAttr("delete_me"); err != nil || !val { rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("forbidden"), "can't use provided token") return } - if err := a.dataService.DeleteUser(claims.SiteID, claims.User.ID); err != nil { + if err := a.dataService.DeleteUser(claims.Audience, claims.User.ID); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user") return } @@ -128,9 +128,9 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { } } - a.cache.Flush(cache.Flusher(claims.SiteID).Scopes(claims.SiteID, claims.User.ID, lastCommentsScope)) + a.cache.Flush(cache.Flusher(claims.Audience).Scopes(claims.Audience, claims.User.ID, lastCommentsScope)) render.Status(r, http.StatusOK) - render.JSON(w, r, R.JSON{"user_id": claims.User.ID, "site_id": claims.SiteID}) + render.JSON(w, r, R.JSON{"user_id": claims.User.ID, "site_id": claims.Audience}) } // PUT /user/{userid}?site=side-id&block=1&ttl=7d - block or unblock user diff --git a/backend/app/rest/api/admin_test.go b/backend/app/rest/api/admin_test.go index 2652ccb4..a0dd5cb4 100644 --- a/backend/app/rest/api/admin_test.go +++ b/backend/app/rest/api/admin_test.go @@ -13,11 +13,12 @@ import ( "time" "github.com/dgrijalva/jwt-go" + "github.com/go-pkgz/auth/token" R "github.com/go-pkgz/rest" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/umputun/remark/backend/app/rest/auth" "github.com/umputun/remark/backend/app/store" ) @@ -512,31 +513,33 @@ func TestAdmin_DeleteMeRequest(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 1, len(comments), "a comment for user1") - claims := auth.CustomClaims{ - SiteID: "radio-t", + claims := token.Claims{ SessionOnly: true, StandardClaims: jwt.StandardClaims{ + Audience: "radio-t", Id: "1234567", Issuer: "remark42", NotBefore: time.Now().Add(-1 * time.Minute).Unix(), ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), }, - User: &store.User{ + User: &token.User{ ID: "user1", Picture: "pic.image", + Attributes: map[string]interface{}{ + "delete_me": true, + }, }, } - claims.Flags.DeleteMe = true _ = os.MkdirAll("/tmp/42", 0700) - defer func(){_ = os.RemoveAll("/tmp/42")}() - require.NoError(t,ioutil.WriteFile("/tmp/42/pic.image", []byte("some image data"), 0600)) + defer func() { _ = os.RemoveAll("/tmp/42") }() + require.NoError(t, ioutil.WriteFile("/tmp/42/pic.image", []byte("some image data"), 0600)) - token, err := srv.Authenticator.JWTService.Token(&claims) + tkn, err := srv.Authenticator.TokenService().Token(claims) assert.Nil(t, err) client := http.Client{} - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) req.SetBasicAuth("dev", "password") resp, err := client.Do(req) @@ -572,24 +575,26 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { assert.Equal(t, 400, resp.StatusCode) // try with bad auth - claims := auth.CustomClaims{ - SiteID: "radio-t", + claims := token.Claims{ SessionOnly: true, StandardClaims: jwt.StandardClaims{ + Audience: "radio-t", Id: "1234567", Issuer: "remark42", NotBefore: time.Now().Add(-1 * time.Minute).Unix(), ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), }, - User: &store.User{ + User: &token.User{ ID: "user1", + Attributes: map[string]interface{}{ + "delete_me": true, + }, }, } - claims.Flags.DeleteMe = true - token, err := srv.Authenticator.JWTService.Token(&claims) + tkn, err := srv.Authenticator.TokenService().Token(claims) assert.Nil(t, err) - req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil) + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) req.SetBasicAuth("dev", "bad-password") resp, err = client.Do(req) @@ -599,9 +604,9 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { // try bad user badClaims := claims badClaims.User.ID = "no-such-id" - token, err = srv.Authenticator.JWTService.Token(&badClaims) + tkn, err = srv.Authenticator.TokenService().Token(badClaims) assert.Nil(t, err) - req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil) + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) req.SetBasicAuth("dev", "password") resp, err = client.Do(req) @@ -610,10 +615,10 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { // try without deleteme flag badClaims2 := claims - badClaims2.Flags.DeleteMe = false - token, err = srv.Authenticator.JWTService.Token(&badClaims2) + badClaims2.User.SetBoolAttr("delete_me", true) + tkn, err = srv.Authenticator.TokenService().Token(badClaims2) assert.Nil(t, err) - req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil) + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) req.SetBasicAuth("dev", "password") resp, err = client.Do(req) diff --git a/backend/app/rest/api/migrator_test.go b/backend/app/rest/api/migrator_test.go index b9440c5e..9d856b6d 100644 --- a/backend/app/rest/api/migrator_test.go +++ b/backend/app/rest/api/migrator_test.go @@ -5,6 +5,7 @@ import ( "compress/gzip" "encoding/json" "fmt" + "github.com/go-pkgz/auth/token" "io" "io/ioutil" "mime/multipart" @@ -17,12 +18,12 @@ import ( bolt "github.com/coreos/bbolt" "github.com/go-chi/chi" + "github.com/go-pkgz/auth" "github.com/go-pkgz/rest/cache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/umputun/remark/backend/app/migrator" - "github.com/umputun/remark/backend/app/rest/auth" "github.com/umputun/remark/backend/app/store" adminstore "github.com/umputun/remark/backend/app/store/admin" "github.com/umputun/remark/backend/app/store/engine" @@ -272,13 +273,15 @@ func prepImportSrv(t *testing.T) (svc *Migrator, ds *service.DataStore, ts *http Cache: &cache.Nop{}, KeyStore: adminStore, } - a := auth.Authenticator{ - DevPasswd: "password", - Providers: nil, - KeyStore: adminStore, - JWTService: auth.NewJWT(adminStore, false, time.Minute, time.Hour), - } - routes := svc.withRoutes(chi.NewRouter().With(a.Auth(true)).With(a.AdminOnly)) + + a := auth.NewService(auth.Opts{ + DevPasswd: "password", + SecretReader: token.SecretFunc(func(id string) (string, error) { return "123456", nil }), + Issuer: "test", + }) + + am := a.Middleware() + routes := svc.withRoutes(chi.NewRouter().With(am.Auth).With(am.AdminOnly)) ts = httptest.NewServer(routes) return svc, dataStore, ts } diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 5544e500..c33b29e8 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -19,15 +19,16 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/cors" "github.com/go-chi/render" - R "github.com/go-pkgz/rest" - "github.com/go-pkgz/rest/cache" - "github.com/go-pkgz/rest/logger" + "github.com/go-pkgz/auth" "github.com/pkg/errors" "github.com/rakyll/statik/fs" + R "github.com/go-pkgz/rest" + "github.com/go-pkgz/rest/cache" + "github.com/go-pkgz/rest/logger" + "github.com/umputun/remark/backend/app/notify" "github.com/umputun/remark/backend/app/rest" - "github.com/umputun/remark/backend/app/rest/auth" "github.com/umputun/remark/backend/app/rest/proxy" "github.com/umputun/remark/backend/app/store" "github.com/umputun/remark/backend/app/store/service" @@ -38,7 +39,7 @@ type Rest struct { Version string DataService *service.DataStore - Authenticator auth.Authenticator + Authenticator auth.Service Cache cache.LoadingCache AvatarProxy *proxy.Avatar ImageProxy *proxy.Image @@ -181,32 +182,47 @@ func (s *Rest) routes() chi.Router { ipFn := func(ip string) string { return store.HashValue(ip, s.SharedSecret)[:12] } // logger uses it for anonymization - // auth routes for all providers - router.Route("/auth", func(r chi.Router) { + authHandler, avatarHandler := s.Authenticator.Handlers() + + router.Group(func(r chi.Router) { l := logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)) r.Use(l.Handler, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(5, nil))) - for _, provider := range s.Authenticator.Providers { - r.Mount("/"+provider.Name, provider.Routes()) // mount auth providers as /auth/{name} - } - if len(s.Authenticator.Providers) > 0 { - // shortcut, can be any of providers, all logouts do the same - removes cookie - r.Get("/logout", s.Authenticator.Providers[0].LogoutHandler) - } + r.Mount("/auth", authHandler) }) - avatarMiddlewares := []func(http.Handler) http.Handler{ - logger.New(logger.Flags(logger.None)).Handler, - tollbooth_chi.LimitHandler(tollbooth.NewLimiter(100, nil)), - } - router.Mount(s.AvatarProxy.Routes(avatarMiddlewares...)) // mount avatars to /api/v1/avatar/{file.img} + router.Group(func(r chi.Router) { + r.Use(logger.New(logger.Flags(logger.None)).Handler, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(100, nil))) + r.Mount("/avatar", avatarHandler) + }) + + authMiddleware := s.Authenticator.Middleware() + + //// auth routes for all providers + //router.Route("/auth", func(r chi.Router) { + // l := logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)) + // r.Use(l.Handler, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(5, nil))) + // + // for _, provider := range s.Authenticator.Providers { + // r.Mount("/"+provider.Name, provider.Routes()) // mount auth providers as /auth/{name} + // } + // if len(s.Authenticator.Providers) > 0 { + // // shortcut, can be any of providers, all logouts do the same - removes cookie + // r.Get("/logout", s.Authenticator.Providers[0].LogoutHandler) + // } + //}) + + //avatarMiddlewares := []func(http.Handler) http.Handler{ + // logger.New(logger.Flags(logger.None)).Handler, + // tollbooth_chi.LimitHandler(tollbooth.NewLimiter(100, nil)), + //} + //router.Mount(s.AvatarProxy.Routes(avatarMiddlewares...)) // mount avatars to /api/v1/avatar/{file.img} // api routes router.Route("/api/v1", func(rapi chi.Router) { rapi.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil))) - // open routes rapi.Group(func(ropen chi.Router) { - ropen.Use(s.Authenticator.Auth(false)) + ropen.Use(authMiddleware.Trace) ropen.Use(logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)).Handler) ropen.Get("/find", s.findCommentsCtrl) ropen.Get("/id/{id}", s.commentByIDCtrl) @@ -225,7 +241,7 @@ func (s *Rest) routes() chi.Router { // protected routes, require auth rapi.Group(func(rauth chi.Router) { - rauth.Use(s.Authenticator.Auth(true)) + rauth.Use(authMiddleware.Auth) rauth.Use(logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)).Handler) rauth.Post("/comment", s.createCommentCtrl) rauth.Put("/comment/{id}", s.updateCommentCtrl) @@ -235,7 +251,7 @@ func (s *Rest) routes() chi.Router { rauth.Post("/deleteme", s.deleteMeCtrl) // admin routes, admin users only - rauth.Mount("/admin", s.adminService.routes(s.Authenticator.AdminOnly)) + rauth.Mount("/admin", s.adminService.routes(authMiddleware.AdminOnly)) }) }) diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index ceb1c4ff..d0492348 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/go-pkgz/auth/token" "log" "net/http" "strings" @@ -18,7 +19,6 @@ import ( "github.com/hashicorp/go-multierror" "github.com/umputun/remark/backend/app/rest" - "github.com/umputun/remark/backend/app/rest/auth" "github.com/umputun/remark/backend/app/store" "github.com/umputun/remark/backend/app/store/service" ) @@ -231,18 +231,23 @@ func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) { user := rest.MustGetUserInfo(r) siteID := r.URL.Query().Get("site") - claims := auth.CustomClaims{ - SiteID: siteID, + claims := token.Claims{ StandardClaims: jwt.StandardClaims{ + Audience: siteID, Issuer: "remark42", ExpiresAt: time.Now().AddDate(0, 3, 0).Unix(), NotBefore: time.Now().Add(-1 * time.Minute).Unix(), }, - User: &user, + User: &token.User{ + ID: user.ID, + Name: user.Name, + Attributes: map[string]interface{}{ + "delete_me": true, // prevents this token from being used for login + }, + }, } - claims.Flags.DeleteMe = true // prevent this token from being used for login - tokenStr, err := s.Authenticator.JWTService.Token(&claims) + tokenStr, err := s.Authenticator.TokenService().Token(claims) if err != nil { rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make token") return diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 9f439580..84f4b543 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -413,7 +413,7 @@ func TestRest_DeleteMe(t *testing.T) { assert.Equal(t, "dev", m["user_id"]) token := m["token"] - claims, err := srv.Authenticator.JWTService.Parse(token) + claims, err := srv.Authenticator.TokenService().Parse(token) assert.Nil(t, err) assert.Equal(t, "dev", claims.User.ID) assert.Equal(t, "https://demo.remark42.com/web/deleteme.html?token="+token, m["link"]) diff --git a/backend/app/rest/api/rest_public.go b/backend/app/rest/api/rest_public.go index 77644c3b..fa801950 100644 --- a/backend/app/rest/api/rest_public.go +++ b/backend/app/rest/api/rest_public.go @@ -58,7 +58,7 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { } if err = R.RenderJSONFromBytes(w, r, data); err != nil { - log.Printf("[WARN] can't render comments for post %+v",locator) + log.Printf("[WARN] can't render comments for post %+v", locator) } } @@ -106,7 +106,7 @@ func (s *Rest) infoCtrl(w http.ResponseWriter, r *http.Request) { } if err = R.RenderJSONFromBytes(w, r, data); err != nil { - log.Printf("[WARN] can't render info for post %+v",locator) + log.Printf("[WARN] can't render info for post %+v", locator) } } @@ -138,7 +138,7 @@ func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { } if err = R.RenderJSONFromBytes(w, r, data); err != nil { - log.Printf("[WARN] can't render last comments for site %s",siteID) + log.Printf("[WARN] can't render last comments for site %s", siteID) } } @@ -160,7 +160,7 @@ func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) if err = R.RenderJSONWithHTML(w, r, comment); err != nil { - log.Printf("[WARN] can't render last comments for url=%s, id=%s",url, id) + log.Printf("[WARN] can't render last comments for url=%s, id=%s", url, id) } } @@ -236,7 +236,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { } cnf.Auth = []string{} - for _, ap := range s.Authenticator.Providers { + for _, ap := range s.Authenticator.Providers() { cnf.Auth = append(cnf.Auth, ap.Name) } @@ -290,7 +290,7 @@ func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) { } if err = R.RenderJSONFromBytes(w, r, data); err != nil { - log.Printf("[WARN] can't render comments counters site %s",siteID) + log.Printf("[WARN] can't render comments counters site %s", siteID) } } @@ -322,6 +322,6 @@ func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) { } if err = R.RenderJSONFromBytes(w, r, data); err != nil { - log.Printf("[WARN] can't render posts lits for site %s",siteID) + log.Printf("[WARN] can't render posts lits for site %s", siteID) } } diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index 4586299d..476b08f5 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -13,13 +13,13 @@ import ( "time" "github.com/coreos/bbolt" + "github.com/go-pkgz/auth" R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/umputun/remark/backend/app/migrator" - "github.com/umputun/remark/backend/app/rest/auth" "github.com/umputun/remark/backend/app/rest/proxy" "github.com/umputun/remark/backend/app/store" adminstore "github.com/umputun/remark/backend/app/store/admin" @@ -61,7 +61,7 @@ func TestRest_GetStarted(t *testing.T) { } func TestRest_Shutdown(t *testing.T) { - srv := Rest{Authenticator: auth.Authenticator{}, AvatarProxy: &proxy.Avatar{Store: avatar.NewLocalFS("/tmp", 300), + srv := Rest{Authenticator: auth.Service{}, AvatarProxy: &proxy.Avatar{Store: avatar.NewLocalFS("/tmp", 300), RoutePath: "/api/v1/avatar"}, ImageProxy: &proxy.Image{}} go func() { @@ -91,7 +91,7 @@ func TestRest_filterComments(t *testing.T) { func TestRest_RunStaticSSLMode(t *testing.T) { srv := Rest{ - Authenticator: auth.Authenticator{}, + Authenticator: auth.Service{}, AvatarProxy: &proxy.Avatar{ Store: avatar.NewLocalFS("/tmp", 300), RoutePath: "/api/v1/avatar", @@ -143,7 +143,7 @@ func TestRest_RunStaticSSLMode(t *testing.T) { func TestRest_RunAutocertModeHTTPOnly(t *testing.T) { srv := Rest{ - Authenticator: auth.Authenticator{}, + Authenticator: auth.Service{}, AvatarProxy: &proxy.Avatar{ Store: avatar.NewLocalFS("/tmp", 300), RoutePath: "/api/v1/avatar", @@ -192,14 +192,14 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { AdminStore: adminStore, MaxVotes: service.UnlimitedVotes, } + + //DevPasswd: "password", + // Providers: nil, + // KeyStore: adminStore, + // JWTService: auth.NewJWT(adminStore, false, time.Minute, time.Hour), srv = &Rest{ - DataService: dataStore, - Authenticator: auth.Authenticator{ - DevPasswd: "password", - Providers: nil, - KeyStore: adminStore, - JWTService: auth.NewJWT(adminStore, false, time.Minute, time.Hour), - }, + DataService: dataStore, + Authenticator: *auth.NewService(auth.Opts{}), Cache: &cache.Nop{}, WebRoot: "/tmp", RemarkURL: "https://demo.remark42.com", diff --git a/backend/app/rest/auth/auth.go b/backend/app/rest/auth/auth.go deleted file mode 100644 index 61b0e35c..00000000 --- a/backend/app/rest/auth/auth.go +++ /dev/null @@ -1,200 +0,0 @@ -// Package auth provides oauth2 support as well as related middlewares. -package auth - -import ( - "encoding/base64" - "log" - "net/http" - "strings" - - "github.com/umputun/remark/backend/app/rest" - "github.com/umputun/remark/backend/app/store" -) - -// Authenticator is top level auth object providing middlewares -type Authenticator struct { - JWTService *JWT - Providers []Provider - KeyStore KeyStore - DevPasswd string - PermissionChecker PermissionChecker -} - -// KeyStore defines sub-interface for consumers needed just a key -type KeyStore interface { - Key(siteID string) (key string, err error) -} - -var devUser = store.User{ - ID: "dev", - Name: "developer one", - Picture: "/api/v1/avatar/remark.image", - Admin: true, -} - -var adminUser = store.User{ - ID: "admin", - Name: "admin", - Picture: "/api/v1/avatar/remark.image", - Admin: true, -} - -// PermissionChecker defines interface to check user flags -type PermissionChecker interface { - IsVerified(siteID, userID string) bool - IsBlocked(siteID, userID string) bool - IsAdmin(siteID, userID string) bool -} - -// Auth middleware adds auth from session and populates user info -func (a *Authenticator) Auth(reqAuth bool) func(http.Handler) http.Handler { - - f := func(h http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - - // if secret key matches for given site (from request) return admin user - if a.checkSecretKey(r) { - r = rest.SetUserInfo(r, adminUser) - h.ServeHTTP(w, r) - return - } - - // use dev user basic auth if enabled - if a.basicDevUser(r) { - r = rest.SetUserInfo(r, devUser) - h.ServeHTTP(w, r) - return - } - - claims, err := a.JWTService.Get(r) - if err != nil { - if reqAuth { // in full auth lack of token causes Unauthorized - log.Printf("[DEBUG] failed auth, %s", err) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - // if !reqAuth just pass it to the next handler, used for information only, like logs - h.ServeHTTP(w, r) - return - } - - if claims.User == nil && reqAuth { - log.Print("[DEBUG] failed auth, no user info presented in the claim") - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if claims.User != nil { // if uinfo in token populate it to context - if claims.User.Blocked { - log.Printf("[DEBUG] user %s/%s blocked", claims.User.Name, claims.User.ID) - a.JWTService.Reset(w) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if a.JWTService.HasFlags(claims) { // flags in token indicate special use cases, not for login - log.Printf("[DEBUG] invalid token flags for %s/%s", claims.User.Name, claims.User.ID) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if a.JWTService.IsExpired(claims) { - if claims, err = a.refreshExpiredToken(w, claims); err != nil { - log.Printf("[DEBUG] can't refresh jwt, %s", err) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - } - log.Printf("[DEBUG] token refreshed for %+v", claims.User) - } - r = rest.SetUserInfo(r, *claims.User) // populate user info to request context - } - - h.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) - } - return f -} - -func (a *Authenticator) checkSecretKey(r *http.Request) bool { - if a.KeyStore == nil { - return false - } - - siteID := r.URL.Query().Get("site") - secret := r.URL.Query().Get("secret") - - skey, err := a.KeyStore.Key(siteID) - if err != nil { - return false - } - - if strings.TrimSpace(secret) == "" || secret != skey { - return false - } - return true -} - -// refreshExpiredToken makes new token with passed claims, but only if permission allowed -func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims *CustomClaims) (*CustomClaims, error) { - if a.PermissionChecker != nil { - claims.User.Admin = a.PermissionChecker.IsAdmin(claims.SiteID, claims.User.ID) - claims.User.Blocked = a.PermissionChecker.IsBlocked(claims.SiteID, claims.User.ID) - claims.User.Verified = a.PermissionChecker.IsVerified(claims.SiteID, claims.User.ID) - } - // refresh token - if err := a.JWTService.Set(w, claims, false); err != nil { - return nil, err - } - return claims, nil -} - -// AdminOnly middleware allows access for admins only -func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - - user, err := rest.GetUserInfo(r) - if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if !user.Admin { - http.Error(w, "Access denied", http.StatusForbidden) - return - } - - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) -} - -func (a *Authenticator) basicDevUser(r *http.Request) bool { - - if a.DevPasswd == "" { - return false - } - - s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) - if len(s) != 2 { - return false - } - - b, err := base64.StdEncoding.DecodeString(s[1]) - if err != nil { - log.Printf("[WARN] dev user auth failed, failed to decode %s, %s", s[1], err) - return false - } - - pair := strings.SplitN(string(b), ":", 2) - if len(pair) != 2 { - log.Printf("[WARN] dev user auth failed, failed to split %s", string(b)) - return false - } - - if pair[0] != "dev" || pair[1] != a.DevPasswd { - log.Printf("[WARN] dev user auth failed, user/passwd mismatch %+v", pair) - return false - } - - return true -} diff --git a/backend/app/rest/auth/auth_test.go b/backend/app/rest/auth/auth_test.go deleted file mode 100644 index 7b901835..00000000 --- a/backend/app/rest/auth/auth_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package auth - -import ( - "encoding/base64" - "net/http" - "net/http/cookiejar" - "net/http/httptest" - "testing" - "time" - - "github.com/go-chi/chi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/umputun/remark/backend/app/store/admin" -) - -var testJwtUserBlocked = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZSI6IiIsImFkbWluIjpmYWxzZSwiYmxvY2siOnRydWV9LCJzdGF0ZSI6IjEyMzQ1NiIsImZyb20iOiJmcm9tIn0.6P_OwGf8CUJRtvNSlW20GmaMb5pFvCNemP94fHCqb5Q" - -var testJwtDeleteMe = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZSI6IiIsImFkbWluIjpmYWxzZSwiYmxvY2siOmZhbHNlfSwiZmxhZ3MiOnsiZGVsZXRlbWUiOnRydWV9fQ.SLh1QpFytWZqcT99VgcdAOtgFKhvpKCcZwqWTvAd63g" - -var testJwtNoUser = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyfQ.sBpblkbBRzZsBSPPNrTWqA5h7h54solrw5L4IypJT_o" - -func TestAuthJWTCookie(t *testing.T) { - a := Authenticator{DevPasswd: "123456", JWTService: NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, time.Hour), - PermissionChecker: &mockUserPermissions{}} - router := chi.NewRouter() - router.With(a.Auth(true)).Get("/auth", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) - }) - server := httptest.NewServer(router) - defer server.Close() - - expiration := int(time.Duration(365 * 24 * time.Hour).Seconds()) - req, err := http.NewRequest("GET", server.URL+"/auth", nil) - require.Nil(t, err) - req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtValid, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false}) - req.Header.Add("X-XSRF-TOKEN", "random id") - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "valid auth user") - - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.Nil(t, err) - req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtValid, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false}) - req.Header.Add("X-XSRF-TOKEN", "wrong id") - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 401, resp.StatusCode, "xsrf mismatch") - - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.Nil(t, err) - req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtExpired, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false}) - req.Header.Add("X-XSRF-TOKEN", "random id") - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "token expired and refreshed") - - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.Nil(t, err) - req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtNoUser, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false}) - req.Header.Add("X-XSRF-TOKEN", "random id") - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 401, resp.StatusCode, "no user info in the token") -} - -func TestAuthJWTHeader(t *testing.T) { - a := Authenticator{DevPasswd: "123456", JWTService: NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, time.Hour)} - router := chi.NewRouter() - router.With(a.Auth(true)).Get("/auth", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) - }) - server := httptest.NewServer(router) - defer server.Close() - - jar, err := cookiejar.New(nil) - require.Nil(t, err) - client := &http.Client{Jar: jar, Timeout: 5 * time.Second} - req, err := http.NewRequest("GET", server.URL+"/auth", nil) - require.Nil(t, err) - req.Header.Add("X-JWT", testJwtValid) - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "valid auth user") - - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.Nil(t, err) - req.Header.Add("X-JWT", testJwtExpired) - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "token expired and refreshed") -} - -func TestAuthJWtBlocked(t *testing.T) { - a := Authenticator{DevPasswd: "123456", JWTService: NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, time.Hour)} - router := chi.NewRouter() - router.With(a.Auth(true)).Get("/auth", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) - }) - server := httptest.NewServer(router) - defer server.Close() - - jar, err := cookiejar.New(nil) - require.Nil(t, err) - client := &http.Client{Jar: jar, Timeout: 5 * time.Second} - req, err := http.NewRequest("GET", server.URL+"/auth", nil) - require.Nil(t, err) - req.Header.Add("X-JWT", testJwtUserBlocked) - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, 401, resp.StatusCode, "blocked user") -} - -func TestAuthJWtFlags(t *testing.T) { - a := Authenticator{DevPasswd: "123456", JWTService: NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, time.Hour)} - router := chi.NewRouter() - router.With(a.Auth(true)).Get("/auth", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) - }) - server := httptest.NewServer(router) - defer server.Close() - - jar, err := cookiejar.New(nil) - require.Nil(t, err) - client := &http.Client{Jar: jar, Timeout: 5 * time.Second} - req, err := http.NewRequest("GET", server.URL+"/auth", nil) - require.Nil(t, err) - req.Header.Add("X-JWT", testJwtDeleteMe) - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, 401, resp.StatusCode, "blocked user") -} - -func TestAuthRequired(t *testing.T) { - a := Authenticator{DevPasswd: "123456"} - router := chi.NewRouter() - router.With(a.Auth(true)).Get("/auth", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) - }) - server := httptest.NewServer(router) - defer server.Close() - - client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("GET", server.URL+"/auth", nil) - require.NoError(t, err) - req = withBasicAuth(req, "dev", "123456") - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "valid auth user") - - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.NoError(t, err) - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 401, resp.StatusCode, "no auth user") - - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.NoError(t, err) - req = withBasicAuth(req, "dev", "xyz") - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 401, resp.StatusCode, "wrong auth creds") -} - -func TestAuthNotRequired(t *testing.T) { - a := Authenticator{DevPasswd: "123456"} - router := chi.NewRouter() - router.With(a.Auth(false)).Get("/auth", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) - }) - server := httptest.NewServer(router) - defer server.Close() - - client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("GET", server.URL+"/auth", nil) - require.NoError(t, err) - req = withBasicAuth(req, "dev", "123456") - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "valid auth user") - - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.NoError(t, err) - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "no auth user") - - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.NoError(t, err) - req = withBasicAuth(req, "dev", "ZZZZ123456") - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "wrong auth creds") -} - -func TestAdminRequired(t *testing.T) { - a := Authenticator{DevPasswd: "123456"} - router := chi.NewRouter() - router.With(a.Auth(true), a.AdminOnly).Get("/auth", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) - }) - server := httptest.NewServer(router) - defer server.Close() - - client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("GET", server.URL+"/auth", nil) - require.NoError(t, err) - req = withBasicAuth(req, "dev", "123456") - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "valid auth user, admin") - - devUser.Admin = false - req, err = http.NewRequest("GET", server.URL+"/auth", nil) - require.NoError(t, err) - req = withBasicAuth(req, "dev", "123456") - resp, err = client.Do(req) - require.NoError(t, err) - assert.Equal(t, 403, resp.StatusCode, "valid auth user, not admin") - -} - -func TestAuthWithSecret(t *testing.T) { - a := Authenticator{DevPasswd: "123456", KeyStore: admin.NewStaticKeyStore("secretkey")} - router := chi.NewRouter() - router.With(a.Auth(true), a.AdminOnly).Get("/auth", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) - }) - server := httptest.NewServer(router) - defer server.Close() - - resp, err := http.Get(server.URL + "/auth?secret=secretkey") - require.NoError(t, err) - assert.Equal(t, 201, resp.StatusCode, "valid auth user with secret, admin") - - resp, err = http.Get(server.URL + "/auth?secret=badsecret") - require.NoError(t, err) - assert.Equal(t, 401, resp.StatusCode, "invalid auth with bad secret") -} - -func withBasicAuth(r *http.Request, username, password string) *http.Request { - auth := username + ":" + password - r.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) - return r -} diff --git a/backend/app/rest/auth/dev_provider.go b/backend/app/rest/auth/dev_provider.go deleted file mode 100644 index 1c07ffe7..00000000 --- a/backend/app/rest/auth/dev_provider.go +++ /dev/null @@ -1,196 +0,0 @@ -package auth - -import ( - "bytes" - "context" - "fmt" - "log" - "net/http" - "strings" - "sync" - "time" - - "github.com/nullrocks/identicon" - "github.com/pkg/errors" - "golang.org/x/oauth2" - - "github.com/umputun/remark/backend/app/store" -) - -const devAuthPort = 8084 - -// DevAuthServer is a fake oauth server for development -// it provides stand-alone server running on its own port and pretending to be the real oauth2. It also provides -// Dev Provider the same way as normal providers do, i.e. like github, google and others. -// can run in interactive and non-interactive mode. In interactive mode login attempts will show login form to select -// desired user name, this is the mode used for development. Non-interactive mode for tests only. -type DevAuthServer struct { - Provider Provider - - username string // unsafe, but fine for dev - nonInteractive bool - iconGen *identicon.Generator - httpServer *http.Server - lock sync.Mutex -} - -// Run oauth2 dev server on port devAuthPort -func (d *DevAuthServer) Run() { - log.Printf("[INFO] run local oauth2 dev server on %d", devAuthPort) - d.lock.Lock() - var err error - d.iconGen, err = identicon.New("github", 5, 3) - if err != nil { - log.Printf("[WARN] can't create identicon, %s", err) - } - - d.httpServer = &http.Server{ - Addr: fmt.Sprintf(":%d", devAuthPort), - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("[DEBUG] dev oauth request %s %s %+v", r.Method, r.URL, r.Header) - switch { - - case strings.HasPrefix(r.URL.Path, "/login/oauth/authorize"): - // first time it will be called without username and will ask for one - if !d.nonInteractive && (r.ParseForm() != nil || r.Form.Get("username") == "") { - if _, err = w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil { - log.Printf("[WARN] can't write, %s", err) - } - return - } - - if !d.nonInteractive { - d.username = r.Form.Get("username") - } - - state := r.URL.Query().Get("state") - callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.RedirectURL, state) - log.Printf("[DEBUG] callback url=%s", callbackURL) - w.Header().Add("Location", callbackURL) - w.WriteHeader(http.StatusFound) - - case strings.HasPrefix(r.URL.Path, "/login/oauth/access_token"): - res := `{ - "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", - "token_type":"bearer", - "expires_in":3600, - "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", - "scope":"create", - "state":"12345678" - }` - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if _, err = w.Write([]byte(res)); err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - case strings.HasPrefix(r.URL.Path, "/user"): - ava := fmt.Sprintf("http://127.0.0.1:%d/avatar?user=%s", devAuthPort, d.username) - res := fmt.Sprintf(`{ - "id": "%s", - "name":"%s", - "picture":"%s" - }`, d.username, d.username, ava) - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if _, err = w.Write([]byte(res)); err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - case strings.HasPrefix(r.URL.Path, "/avatar"): - user := r.URL.Query().Get("user") - b, e := d.genAvatar(user) - if e != nil { - w.WriteHeader(http.StatusNotFound) - return - } - if _, err = w.Write(b); err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - default: - w.WriteHeader(http.StatusBadRequest) - } - }), - } - d.lock.Unlock() - - err = d.httpServer.ListenAndServe() - log.Printf("[WARN] dev oauth2 server terminated, %s", err) -} - -// Shutdown oauth2 dev server -func (d *DevAuthServer) Shutdown() { - log.Print("[WARN] shutdown oauth2 dev server") - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - d.lock.Lock() - if d.httpServer != nil { - if err := d.httpServer.Shutdown(ctx); err != nil { - log.Printf("[DEBUG] oauth2 dev shutdown error, %s", err) - } - } - log.Print("[DEBUG] shutdown dev oauth2 server completed") - d.lock.Unlock() -} - -// NewDev makes dev oauth2 provider for admin user -func NewDev(p Params) Provider { - return initProvider(p, Provider{ - Name: "dev", - Endpoint: oauth2.Endpoint{ - AuthURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/authorize", devAuthPort), - TokenURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/access_token", devAuthPort), - }, - RedirectURL: p.RemarkURL + "/auth/dev/callback", - Scopes: []string{"user:email"}, - InfoURL: fmt.Sprintf("http://127.0.0.1:%d/user", devAuthPort), - MapUser: func(data userData, _ []byte) store.User { - userInfo := store.User{ - ID: data.value("id"), - Name: data.value("name"), - Picture: data.value("picture"), - } - return userInfo - }, - }) -} - -func (d *DevAuthServer) genAvatar(user string) ([]byte, error) { - if d.iconGen == nil { - return nil, errors.Errorf("no iconGen, skip avatar generation for %s", user) - } - - ii, err := d.iconGen.Draw(user) // Generate an IdentIcon - if err != nil { - return nil, errors.Wrapf(err, "failed to draw avatar for %s", user) - } - - buf := &bytes.Buffer{} - err = ii.Png(300, buf) - return buf.Bytes(), err -} - -var devUserForm = ` - - - Remark42 Dev User - - - -
- username: - -
- - -` diff --git a/backend/app/rest/auth/dev_provider_test.go b/backend/app/rest/auth/dev_provider_test.go deleted file mode 100644 index 6a732bfe..00000000 --- a/backend/app/rest/auth/dev_provider_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package auth - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/cookiejar" - "testing" - "time" - - "github.com/go-chi/chi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/umputun/remark/backend/app/store" - "github.com/umputun/remark/backend/app/store/admin" -) - -func TestDevProvider(t *testing.T) { - params := Params{RemarkURL: "http://127.0.0.1:8080", Cid: "cid", Csecret: "csecret", - JwtService: NewJWT(admin.NewStaticKeyStore("12345"), false, time.Hour, time.Hour*24*31), - PermissionChecker: &mockUserPermissions{admin: "dev_user"}, - } - srv := DevAuthServer{Provider: NewDev(params), nonInteractive: true, username: "dev_user"} - - // auth routes for all providers - router := chi.NewRouter() - router.Route("/auth", func(r chi.Router) { - r.Mount("/dev", srv.Provider.Routes()) // mount auth providers as /auth/{name} - }) - - ts := &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", 8080), Handler: router} - go srv.Run() - go ts.ListenAndServe() - defer func() { - srv.Shutdown() - _ = ts.Shutdown(context.TODO()) - }() - - time.Sleep(100 * time.Millisecond) - - jar, err := cookiejar.New(nil) - require.Nil(t, err) - client := &http.Client{Jar: jar, Timeout: 5 * time.Second} - - // check non-admin, permanent - resp, err := client.Get("http://127.0.0.1:8080/auth/dev/login?site=remark") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - body, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - t.Logf("resp %s", string(body)) - t.Logf("headers: %+v", resp.Header) - - assert.Equal(t, 2, len(resp.Cookies())) - assert.Equal(t, "JWT", resp.Cookies()[0].Name) - assert.NotEqual(t, "", resp.Cookies()[0].Value, "jwt set") - assert.Equal(t, 2678400, resp.Cookies()[0].MaxAge) - assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name) - assert.NotEqual(t, "", resp.Cookies()[1].Value, "xsrf cookie set") - - claims, err := params.JwtService.Parse(resp.Cookies()[0].Value) - assert.Nil(t, err) - - u := *claims.User - assert.Equal(t, store.User{Name: "dev_user", ID: "dev_user", Picture: "http://127.0.0.1:8084/avatar?user=dev_user", IP: "", - Admin: true, Blocked: false, Verified: false}, u) - - // check avatar - resp, err = client.Get("http://127.0.0.1:8084/avatar?user=dev_user") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - body, err = ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - assert.Equal(t, 985, len(body)) - t.Logf("headers: %+v", resp.Header) -} diff --git a/backend/app/rest/auth/jwt.go b/backend/app/rest/auth/jwt.go deleted file mode 100644 index 0326ce48..00000000 --- a/backend/app/rest/auth/jwt.go +++ /dev/null @@ -1,200 +0,0 @@ -package auth - -import ( - "net/http" - "time" - - "github.com/dgrijalva/jwt-go" - "github.com/pkg/errors" - - "github.com/umputun/remark/backend/app/store" -) - -// JWT wraps jwt operations -// supports both header and cookie jwt -type JWT struct { - keyStore KeyStore - secureCookies bool - tokenDuration time.Duration - cookieDuration time.Duration -} - -// CustomClaims stores user info for auth and state & from from login -type CustomClaims struct { - jwt.StandardClaims - User *store.User `json:"user,omitempty"` - - // used for oauth handshake - State string `json:"state,omitempty"` - From string `json:"from,omitempty"` - SiteID string `json:"site_id,omitempty"` - SessionOnly bool `json:"sess_only,omitempty"` - - // flags indicate different uses - Flags struct { - Login bool `json:"login,omitempty"` - DeleteMe bool `json:"deleteme,omitempty"` - } `json:"flags,omitempty"` -} - -const jwtCookieName = "JWT" -const jwtHeaderKey = "X-JWT" -const xsrfCookieName = "XSRF-TOKEN" -const xsrfHeaderKey = "X-XSRF-TOKEN" - -// NewJWT makes JWT service -func NewJWT(keyStore KeyStore, secureCookies bool, tokenDuration time.Duration, cookieDuration time.Duration) *JWT { - res := JWT{ - keyStore: keyStore, - secureCookies: secureCookies, - tokenDuration: tokenDuration, - cookieDuration: cookieDuration, - } - return &res -} - -// Token makes jwt with claims -func (j *JWT) Token(claims *CustomClaims) (string, error) { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - secret, err := j.keyStore.Key(claims.SiteID) - if err != nil { - return "", errors.Wrap(err, "can't get secret") - } - - tokenString, err := token.SignedString([]byte(secret)) - if err != nil { - return "", errors.Wrap(err, "can't sign jwt token") - } - return tokenString, nil -} - -// HasFlags indicates presence of special flags -func (j *JWT) HasFlags(claims *CustomClaims) bool { - return claims.Flags.DeleteMe || claims.Flags.Login -} - -// Parse token string and verify. Not checking for expiration -func (j *JWT) Parse(tokenString string) (*CustomClaims, error) { - parser := jwt.Parser{SkipClaimsValidation: true} // allow parsing of expired tokens - - getSiteID := func() (siteID string, err error) { // parse token without signature check to get siteID - preToken, _, err := parser.ParseUnverified(tokenString, &CustomClaims{}) - if err != nil { - return "", errors.Wrap(err, "can't pre-parse jwt") - } - preClaims, ok := preToken.Claims.(*CustomClaims) - if !ok { - return "", errors.New("invalid jwt") - } - return preClaims.SiteID, nil - } - - siteID, err := getSiteID() - if err != nil { - return nil, errors.Wrap(err, "failed to get siteID from jwt token") - } - - secret, err := j.keyStore.Key(siteID) - if err != nil { - return nil, errors.Wrap(err, "can't get secret") - } - - token, err := parser.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(secret), nil - }) - if err != nil { - return nil, errors.Wrap(err, "can't parse jwt") - } - - claims, ok := token.Claims.(*CustomClaims) - if !ok || !token.Valid { - return nil, errors.New("invalid jwt") - } - - return claims, nil -} - -// Set creates jwt cookie with xsrf cookie and put it to ResponseWriter -// accepts claims and sets expiration if none defined. permanent flag means long-living cookie, false makes it session only. -func (j *JWT) Set(w http.ResponseWriter, claims *CustomClaims, sessionOnly bool) error { - if claims.ExpiresAt == 0 { - claims.ExpiresAt = time.Now().Add(j.tokenDuration).Unix() - } - - tokenString, err := j.Token(claims) - if err != nil { - return errors.Wrap(err, "failed to make jwt token") - } - - cookieExpiration := 0 // session cookie - if !sessionOnly { - cookieExpiration = int(j.cookieDuration.Seconds()) - } - - jwtCookie := http.Cookie{Name: jwtCookieName, Value: tokenString, HttpOnly: true, Path: "/", - MaxAge: cookieExpiration, Secure: j.secureCookies} - http.SetCookie(w, &jwtCookie) - - xsrfCookie := http.Cookie{Name: xsrfCookieName, Value: claims.Id, HttpOnly: false, Path: "/", - MaxAge: cookieExpiration, Secure: j.secureCookies} - http.SetCookie(w, &xsrfCookie) - - return nil -} - -// Get jwt from header or cookie -// if cookie used, verify xsrf token to match -func (j *JWT) Get(r *http.Request) (*CustomClaims, error) { - - fromCookie := false - tokenString := "" - - // try to get from X-JWT header - if tokenHeader := r.Header.Get(jwtHeaderKey); tokenHeader != "" { - tokenString = tokenHeader - } - - // try to get from JWT cookie - if tokenString == "" { - fromCookie = true - jc, err := r.Cookie(jwtCookieName) - if err != nil { - return nil, errors.Wrap(err, "jwt cookie was not presented") - } - tokenString = jc.Value - } - - claims, err := j.Parse(tokenString) - if err != nil { - return nil, errors.Wrap(err, "failed to get jwt") - } - - if fromCookie && claims.User != nil { - xsrf := r.Header.Get(xsrfHeaderKey) - if claims.Id != xsrf { - return nil, errors.New("xsrf mismatch") - } - } - - return claims, nil -} - -// IsExpired returns true if claims expired -func (j *JWT) IsExpired(claims *CustomClaims) bool { - return !claims.VerifyExpiresAt(time.Now().Unix(), true) -} - -// Reset token's cookies -func (j *JWT) Reset(w http.ResponseWriter) { - jwtCookie := http.Cookie{Name: jwtCookieName, Value: "", HttpOnly: false, Path: "/", - MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.secureCookies} - http.SetCookie(w, &jwtCookie) - - xsrfCookie := http.Cookie{Name: xsrfCookieName, Value: "", HttpOnly: false, Path: "/", - MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.secureCookies} - http.SetCookie(w, &xsrfCookie) -} diff --git a/backend/app/rest/auth/jwt_test.go b/backend/app/rest/auth/jwt_test.go deleted file mode 100644 index f1e654d8..00000000 --- a/backend/app/rest/auth/jwt_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package auth - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/dgrijalva/jwt-go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/umputun/remark/backend/app/store/admin" - - "github.com/umputun/remark/backend/app/store" -) - -var testJwtValid = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlb" + "WFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZSI6IiIsImFkbWluIjpmYWxzZX0" + "sInN0YXRlIjoiMTIzNDU2IiwiZnJvbSI6ImZyb20iLCJmbGFncyI6e319.E2Blxqo1wsY855q258c0obxFJ1lgJciv1av1ewzlJBs" - -var testJwtValidSess = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIs" + "ImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZSI6IiIsImFk" + "bWluIjpmYWxzZX0sInN0YXRlIjoiMTIzNDU2IiwiZnJvbSI6ImZyb20iLCJzZXNzX29ubHkiOnRydWUsImZsYWdzIjp7fX0." + "nKhehF1Xiome1yK1ewfOiIsrATvq7Tx7p1BCSJqKHuo" - -var testJwtExpired = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjY4ODc4MjIsImp0aSI6InJhbmRvbSBpZCIs" + - "ImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZSI6IiI" + - "sImFkbWluIjpmYWxzZX0sInN0YXRlIjoiMTIzNDU2IiwiZnJvbSI6ImZyb20ifQ.4_dCrY9ihyfZIedz-kZwBTxmxU1a52V7IqeJrOqTzE4" - -var testJwtBadSign = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCI" + - "sImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZS" + - "I6IiIsImFkbWluIjpmYWxzZX0sInN0YXRlIjoiMTIzNDU2IiwiZnJvbSI6ImZyb20ifQ._loFgh3g45gr9TtGqvM3N584I_6EHEOJnYb6Py84st" - -var days31 = time.Hour * 24 * 31 - -func TestJWT_Token(t *testing.T) { - j := NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, days31) - - claims := &CustomClaims{ - State: "123456", - From: "from", - User: &store.User{ - ID: "id1", - Name: "name1", - }, - StandardClaims: jwt.StandardClaims{ - Id: "random id", - Issuer: "remark42", - ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - }, - } - - res, err := j.Token(claims) - assert.Nil(t, err) - assert.Equal(t, testJwtValid, res) -} - -func TestJWT_Parse(t *testing.T) { - j := NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, days31) - claims, err := j.Parse(testJwtValid) - assert.NoError(t, err) - assert.False(t, j.IsExpired(claims)) - assert.Equal(t, &store.User{Name: "name1", ID: "id1"}, claims.User) - - claims, err = j.Parse(testJwtExpired) - assert.NoError(t, err) - assert.True(t, j.IsExpired(claims)) - - _, err = j.Parse("bad") - assert.NotNil(t, err, "bad token") - - _, err = j.Parse(testJwtBadSign) - assert.EqualError(t, err, "can't parse jwt: signature is invalid") -} - -func TestJWT_Set(t *testing.T) { - j := NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, days31) - - claims := &CustomClaims{ - State: "123456", - From: "from", - User: &store.User{ - ID: "id1", - Name: "name1", - }, - StandardClaims: jwt.StandardClaims{ - Id: "random id", - Issuer: "remark42", - ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - }, - SessionOnly: false, - } - - rr := httptest.NewRecorder() - err := j.Set(rr, claims, claims.SessionOnly) - assert.Nil(t, err) - cookies := rr.Result().Cookies() - t.Log(cookies) - require.Equal(t, 2, len(cookies)) - assert.Equal(t, "JWT", cookies[0].Name) - assert.Equal(t, testJwtValid, cookies[0].Value) - assert.Equal(t, 31*24*3600, cookies[0].MaxAge) - assert.Equal(t, "XSRF-TOKEN", cookies[1].Name) - assert.Equal(t, "random id", cookies[1].Value) - - claims.SessionOnly = true - rr = httptest.NewRecorder() - err = j.Set(rr, claims, claims.SessionOnly) - assert.Nil(t, err) - cookies = rr.Result().Cookies() - t.Log(cookies) - require.Equal(t, 2, len(cookies)) - assert.Equal(t, "JWT", cookies[0].Name) - assert.Equal(t, testJwtValidSess, cookies[0].Value) - assert.Equal(t, 0, cookies[0].MaxAge) - assert.Equal(t, "XSRF-TOKEN", cookies[1].Name) - assert.Equal(t, "random id", cookies[1].Value) -} - -func TestJWT_GetFromHeader(t *testing.T) { - j := NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, days31) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Add(jwtHeaderKey, testJwtValid) - claims, err := j.Get(req) - assert.Nil(t, err) - assert.False(t, j.IsExpired(claims)) - assert.Equal(t, &store.User{Name: "name1", ID: "id1", Picture: "", Admin: false, Blocked: false, IP: ""}, claims.User) - assert.Equal(t, "remark42", claims.Issuer) - - req = httptest.NewRequest("GET", "/", nil) - req.Header.Add(jwtHeaderKey, testJwtExpired) - claims, err = j.Get(req) - assert.Nil(t, err) - assert.True(t, j.IsExpired(claims)) - - req = httptest.NewRequest("GET", "/", nil) - req.Header.Add(jwtHeaderKey, "bad bad token") - _, err = j.Get(req) - require.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), "can't pre-parse jwt: token contains an invalid number of segments"), err.Error()) - -} - -func TestJWT_SetAndGetWithCookies(t *testing.T) { - j := NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, days31) - - claims := &CustomClaims{ - State: "123456", - From: "from", - SessionOnly: true, - User: &store.User{ - ID: "id1", - Name: "name1", - }, - StandardClaims: jwt.StandardClaims{ - Id: "random id", - Issuer: "remark42", - ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - }, - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/valid" { - assert.Nil(t, j.Set(w, claims, true)) - w.WriteHeader(200) - } - })) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/valid") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - - req := httptest.NewRequest("GET", "/valid", nil) - req.AddCookie(resp.Cookies()[0]) - req.Header.Add(xsrfHeaderKey, "random id") - claims, err = j.Get(req) - assert.Nil(t, err) - assert.Equal(t, &store.User{Name: "name1", ID: "id1", Picture: "", Admin: false, Blocked: false, IP: ""}, claims.User) - assert.Equal(t, "remark42", claims.Issuer) - assert.Equal(t, true, claims.SessionOnly) - t.Log(resp.Cookies()) -} - -func TestJWT_SetAndGetWithXsrfMismatch(t *testing.T) { - j := NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, days31) - - claims := &CustomClaims{ - State: "123456", - From: "from", - User: &store.User{ - ID: "id1", - Name: "name1", - }, - StandardClaims: jwt.StandardClaims{ - Id: "random id", - Issuer: "remark42", - ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - }, - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/valid" { - assert.Nil(t, j.Set(w, claims, true)) - w.WriteHeader(200) - } - })) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/valid") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - - req := httptest.NewRequest("GET", "/valid", nil) - req.AddCookie(resp.Cookies()[0]) - req.Header.Add(xsrfHeaderKey, "random id wrong") - claims, err = j.Get(req) - assert.EqualError(t, err, "xsrf mismatch") -} - -func TestJWT_SetAndGetWithCookiesExpired(t *testing.T) { - j := NewJWT(admin.NewStaticKeyStore("xyz 12345"), false, time.Hour, days31) - - claims := &CustomClaims{ - State: "123456", - From: "from", - User: &store.User{ - ID: "id1", - Name: "name1", - }, - StandardClaims: jwt.StandardClaims{ - Id: "random id", - Issuer: "remark42", - ExpiresAt: time.Date(2018, 5, 21, 1, 35, 22, 0, time.Local).Unix(), - NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(), - }, - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/expired" { - assert.Nil(t, j.Set(w, claims, true)) - w.WriteHeader(200) - } - })) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/expired") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - - req := httptest.NewRequest("GET", "/expired", nil) - req.AddCookie(resp.Cookies()[0]) - req.Header.Add(xsrfHeaderKey, "random id") - claims, err = j.Get(req) - assert.Nil(t, err) - assert.True(t, j.IsExpired(claims)) -} diff --git a/backend/app/rest/auth/provider.go b/backend/app/rest/auth/provider.go deleted file mode 100644 index 4558384c..00000000 --- a/backend/app/rest/auth/provider.go +++ /dev/null @@ -1,229 +0,0 @@ -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/backend/app/rest" - "github.com/umputun/remark/backend/app/rest/proxy" - "github.com/umputun/remark/backend/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 - PermissionChecker PermissionChecker - Cid string - Csecret string -} - -type userData map[string]interface{} - -func (u userData) value(key string) string { - // json.Unmarshal converts json "null" value to go's "nil", in this case return empty string - if val, ok := u[key]; ok && val != nil { - 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&site=siteID&session=1 -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"), - SiteID: r.URL.Query().Get("site"), - SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0", - StandardClaims: jwt.StandardClaims{ - Id: p.randToken(), - Issuer: "remark42", - ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), - NotBefore: time.Now().Add(-1 * time.Minute).Unix(), - }, - } - claims.Flags.Login = true - - if err := p.JwtService.Set(w, &claims, false); 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, claims=%+v", loginURL, claims) - - 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) - u = p.setPermissions(u, oauthClaims.SiteID) - u = p.setAvatar(u) - - claims := &CustomClaims{ - User: &u, - StandardClaims: jwt.StandardClaims{ - Issuer: "remark42", - Id: p.randToken(), - }, - SiteID: oauthClaims.SiteID, - SessionOnly: oauthClaims.SessionOnly, - } - - if err = p.JwtService.Set(w, claims, oauthClaims.SessionOnly); 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) -} - -// setAvatar saves avatar and puts proxied URL to u.Picture -func (p Provider) setAvatar(u store.User) store.User { - 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) - } - } - return u -} - -// setPermissions sets permission fields not handled by provider's MapUser, things like admin, verified and blocked -func (p Provider) setPermissions(u store.User, siteID string) store.User { - u.Admin = p.PermissionChecker.IsAdmin(siteID, u.ID) - u.Verified = p.PermissionChecker.IsVerified(siteID, u.ID) - u.Blocked = p.PermissionChecker.IsBlocked(siteID, u.ID) - log.Printf("[DEBUG] set permissions for user %s, site %s - %+v", u.ID, siteID, u) - return 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)) -} diff --git a/backend/app/rest/auth/provider_test.go b/backend/app/rest/auth/provider_test.go deleted file mode 100644 index c2b41616..00000000 --- a/backend/app/rest/auth/provider_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/http/cookiejar" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2" - - "github.com/umputun/remark/backend/app/store" - "github.com/umputun/remark/backend/app/store/admin" -) - -func TestLogin(t *testing.T) { - - ts, ots := mockProvider(t, 8981, 8982) - defer func() { - ts.Close() - ots.Close() - }() - - jar, err := cookiejar.New(nil) - require.Nil(t, err) - client := &http.Client{Jar: jar, Timeout: 5 * time.Second} - - // check non-admin, permanent - resp, err := client.Get("http://localhost:8981/login?site=remark") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - body, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - t.Logf("resp %s", string(body)) - t.Logf("headers: %+v", resp.Header) - - assert.Equal(t, 2, len(resp.Cookies())) - assert.Equal(t, "JWT", resp.Cookies()[0].Name) - assert.NotEqual(t, "", resp.Cookies()[0].Value, "jwt set") - assert.Equal(t, 2678400, resp.Cookies()[0].MaxAge) - assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name) - assert.NotEqual(t, "", resp.Cookies()[1].Value, "xsrf cookie set") - - u := store.User{} - err = json.Unmarshal(body, &u) - assert.Nil(t, err) - assert.Equal(t, store.User{Name: "blah", ID: "mock_myuser1", Picture: "http://exmple.com/pic1.png", - Admin: false, Blocked: true, IP: ""}, u) - - token := resp.Cookies()[0].Value - jwtSvc := NewJWT(admin.NewStaticKeyStore("12345"), false, time.Hour, time.Hour*24*31) - - claims, err := jwtSvc.Parse(token) - require.NoError(t, err) - assert.Equal(t, "remark42", claims.Issuer) - assert.Equal(t, "remark", claims.SiteID) - - // check admin user - resp, err = client.Get("http://localhost:8981/login?site=remark") - assert.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - body, err = ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - u = store.User{} - err = json.Unmarshal(body, &u) - assert.Nil(t, err) - assert.Equal(t, store.User{Name: "blah", ID: "mock_myuser2", Picture: "http://exmple.com/pic1.png", - Admin: true, Blocked: false, IP: "", Verified: true}, u) -} - -func TestLoginSessionOnly(t *testing.T) { - - ts, ots := mockProvider(t, 8981, 8982) - defer func() { - ts.Close() - ots.Close() - }() - - jar, err := cookiejar.New(nil) - require.Nil(t, err) - client := &http.Client{Jar: jar, Timeout: 5 * time.Second} - - // check non-admin, session - resp, err := client.Get("http://localhost:8981/login?site=remark&session=1") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - assert.Equal(t, 2, len(resp.Cookies())) - assert.Equal(t, "JWT", resp.Cookies()[0].Name) - assert.NotEqual(t, "", resp.Cookies()[0].Value, "jwt set") - assert.Equal(t, 0, resp.Cookies()[0].MaxAge) - assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name) - assert.NotEqual(t, "", resp.Cookies()[1].Value, "xsrf cookie set") - - req, err := http.NewRequest("GET", "http://example.com", nil) - require.Nil(t, err) - req.AddCookie(resp.Cookies()[0]) - req.AddCookie(resp.Cookies()[1]) - req.Header.Add("X-XSRF-TOKEN", resp.Cookies()[1].Value) - - jwtService := NewJWT(admin.NewStaticKeyStore("12345"), false, time.Hour, time.Hour) - res, err := jwtService.Get(req) - require.Nil(t, err) - assert.Equal(t, true, res.SessionOnly) - t.Logf("%+v", res) -} - -func TestLogout(t *testing.T) { - - ts, ots := mockProvider(t, 8691, 8692) - defer func() { - ts.Close() - ots.Close() - }() - - jar, err := cookiejar.New(nil) - require.Nil(t, err) - client := &http.Client{Jar: jar, Timeout: 5 * time.Second} - - resp, err := client.Get("http://localhost:8691/login") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - assert.Equal(t, 2, len(resp.Cookies())) - resp, err = client.Get("http://localhost:8691/logout") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - - assert.Equal(t, 2, len(resp.Cookies())) - assert.Equal(t, "JWT", resp.Cookies()[0].Name, "jwt cookie cleared") - assert.Equal(t, "", resp.Cookies()[0].Value) - assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name, "xsrf cookie cleared") - assert.Equal(t, "", resp.Cookies()[1].Value) -} - -func TestInitProvider(t *testing.T) { - params := Params{RemarkURL: "url", Cid: "cid", Csecret: "csecret"} - provider := Provider{Name: "test", RedirectURL: "redir"} - res := initProvider(params, provider) - assert.Equal(t, "cid", res.conf.ClientID) - assert.Equal(t, "csecret", res.conf.ClientSecret) - assert.Equal(t, "redir", res.RedirectURL) - assert.Equal(t, "test", res.Name) -} - -func mockProvider(t *testing.T, loginPort, authPort int) (*http.Server, *http.Server) { - - provider := Provider{ - Name: "mock", - Endpoint: oauth2.Endpoint{ - AuthURL: fmt.Sprintf("http://localhost:%d/login/oauth/authorize", authPort), - TokenURL: fmt.Sprintf("http://localhost:%d/login/oauth/access_token", authPort), - }, - RedirectURL: fmt.Sprintf("http://localhost:%d/callback", loginPort), - Scopes: []string{"user:email"}, - InfoURL: fmt.Sprintf("http://localhost:%d/user", authPort), - MapUser: func(data userData, _ []byte) store.User { - userInfo := store.User{ - ID: "mock_" + data.value("id"), - Name: data.value("name"), - Picture: data.value("picture"), - } - return userInfo - }, - } - - params := Params{RemarkURL: "url", Cid: "cid", Csecret: "csecret", - JwtService: NewJWT(admin.NewStaticKeyStore("12345"), false, time.Hour, time.Hour*24*31), - // AvatarProxy: &proxy.Avatar{Store: &mockAvatarStore, RoutePath: "/v1/avatar"}, - PermissionChecker: &mockUserPermissions{admin: "mock_myuser2", verified: "mock_myuser2", blocked: "mock_myuser1"}, - } - provider = initProvider(params, provider) - - ts := &http.Server{Addr: fmt.Sprintf(":%d", loginPort), Handler: provider.Routes()} - - count := 0 - useIds := []string{"myuser1", "myuser2"} // user for first ans second calls - - oauth := &http.Server{ - Addr: fmt.Sprintf(":%d", authPort), - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("[MOCK OAUTH] request %s %s %+v", r.Method, r.URL, r.Header) - switch { - case strings.HasPrefix(r.URL.Path, "/login/oauth/authorize"): - state := r.URL.Query().Get("state") - w.Header().Add("Location", fmt.Sprintf("http://localhost:%d/callback?code=g0ZGZmNjVmOWI&state=%s", - loginPort, state)) - w.WriteHeader(302) - case strings.HasPrefix(r.URL.Path, "/login/oauth/access_token"): - res := `{ - "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", - "token_type":"bearer", - "expires_in":3600, - "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", - "scope":"create", - "state":"12345678" - }` - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(200) - _, err := w.Write([]byte(res)) - assert.NoError(t, err) - case strings.HasPrefix(r.URL.Path, "/user"): - res := fmt.Sprintf(`{ - "id": "%s", - "name":"blah", - "picture":"http://exmple.com/pic1.png" - }`, useIds[count]) - count++ - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(200) - _, err := w.Write([]byte(res)) - assert.NoError(t, err) - default: - t.Fatalf("unexpected oauth request %s %s", r.Method, r.URL) - } - }), - } - - go func() { _ = oauth.ListenAndServe() }() - go func() { _ = ts.ListenAndServe() }() - - time.Sleep(time.Millisecond * 100) // let them start - return ts, oauth -} - -type mockUserPermissions struct { - admin string - verified string - blocked string -} - -func (m *mockUserPermissions) IsAdmin(siteID, userID string) bool { return userID == m.admin } -func (m *mockUserPermissions) IsVerified(siteID, userID string) bool { return userID == m.verified } -func (m *mockUserPermissions) IsBlocked(siteID, userID string) bool { return userID == m.blocked } diff --git a/backend/app/rest/auth/providers.go b/backend/app/rest/auth/providers.go deleted file mode 100644 index 4ed89a9b..00000000 --- a/backend/app/rest/auth/providers.go +++ /dev/null @@ -1,126 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - - "golang.org/x/oauth2/facebook" - "golang.org/x/oauth2/github" - "golang.org/x/oauth2/google" - "golang.org/x/oauth2/yandex" - - "github.com/umputun/remark/backend/app/store" -) - -// NewGoogle makes google oauth2 provider -func NewGoogle(p Params) Provider { - return initProvider(p, Provider{ - Name: "google", - Endpoint: google.Endpoint, - RedirectURL: p.RemarkURL + "/auth/google/callback", - Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"}, - InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo", - MapUser: func(data userData, _ []byte) store.User { - userInfo := store.User{ - // encode email with provider name to avoid collision if same id returned by other provider - ID: "google_" + store.EncodeID(data.value("sub")), - Name: data.value("name"), - Picture: data.value("picture"), - } - if userInfo.Name == "" { - userInfo.Name = "noname_" + userInfo.ID[8:12] - } - return userInfo - }, - }) -} - -// NewGithub makes github oauth2 provider -func NewGithub(p Params) Provider { - return initProvider(p, Provider{ - Name: "github", - Endpoint: github.Endpoint, - RedirectURL: p.RemarkURL + "/auth/github/callback", - Scopes: []string{}, - InfoURL: "https://api.github.com/user", - MapUser: func(data userData, _ []byte) store.User { - userInfo := store.User{ - ID: "github_" + store.EncodeID(data.value("login")), - Name: data.value("name"), - Picture: data.value("avatar_url"), - } - // github may have no user name, use login in this case - if userInfo.Name == "" { - userInfo.Name = data.value("login") - } - return userInfo - }, - }) -} - -// NewFacebook makes facebook oauth2 provider -func NewFacebook(p Params) Provider { - - // response format for fb /me call - type uinfo struct { - ID string `json:"id"` - Name string `json:"name"` - Picture struct { - Data struct { - URL string `json:"url"` - } `json:"data"` - } `json:"picture"` - } - - return initProvider(p, Provider{ - Name: "facebook", - Endpoint: facebook.Endpoint, - RedirectURL: p.RemarkURL + "/auth/facebook/callback", - Scopes: []string{"public_profile"}, - InfoURL: "https://graph.facebook.com/me?fields=id,name,picture", - MapUser: func(data userData, bdata []byte) store.User { - userInfo := store.User{ - ID: "facebook_" + store.EncodeID(data.value("id")), - Name: data.value("name"), - } - if userInfo.Name == "" { - userInfo.Name = userInfo.ID[0:16] - } - - uinfoJSON := uinfo{} - if err := json.Unmarshal(bdata, &uinfoJSON); err == nil { - userInfo.Picture = uinfoJSON.Picture.Data.URL - } - return userInfo - }, - }) -} - -// NewYandex makes yandex oauth2 provider -func NewYandex(p Params) Provider { - return initProvider(p, Provider{ - Name: "yandex", - Endpoint: yandex.Endpoint, - RedirectURL: p.RemarkURL + "/auth/yandex/callback", - Scopes: []string{}, - // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ - InfoURL: "https://login.yandex.ru/info?format=json", - MapUser: func(data userData, _ []byte) store.User { - userInfo := store.User{ - ID: "yandex_" + store.EncodeID(data.value("id")), - Name: data.value("display_name"), // using Display Name by default - } - if userInfo.Name == "" { - userInfo.Name = data.value("real_name") // using Real Name (== full name) if Display Name is empty - } - if userInfo.Name == "" { - userInfo.Name = data.value("login") // otherwise using login - } - - if data.value("default_avatar_id") != "" { - userInfo.Picture = fmt.Sprintf("https://avatars.yandex.net/get-yapic/%s/islands-200", data.value("default_avatar_id")) - } - return userInfo - }, - }) -} diff --git a/backend/app/rest/auth/providers_test.go b/backend/app/rest/auth/providers_test.go deleted file mode 100644 index 9867d94c..00000000 --- a/backend/app/rest/auth/providers_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package auth - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/umputun/remark/backend/app/store" -) - -func TestProviders_NewGoogle(t *testing.T) { - r := NewGoogle(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"}) - assert.Equal(t, "google", r.Name) - - udata := userData{"sub": "1234567890", "name": "test user", "picture": "http://demo.remark42.com/blah.png"} - user := r.MapUser(udata, nil) - assert.Equal(t, store.User{Name: "test user", ID: "google_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", - Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) - - // no name in data - udata = userData{"sub": "1234567890", "picture": "http://demo.remark42.com/blah.png"} - user = r.MapUser(udata, nil) - assert.Equal(t, store.User{Name: "noname_1b30", ID: "google_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", - Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) -} - -func TestProviders_NewGithub(t *testing.T) { - r := NewGithub(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"}) - assert.Equal(t, "github", r.Name) - - udata := userData{"login": "lll", "name": "test user", "avatar_url": "http://demo.remark42.com/blah.png"} - user := r.MapUser(udata, nil) - assert.Equal(t, store.User{Name: "test user", ID: "github_e80b2d2608711cbb3312db7c4727a46fbad9601a", - Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) - - // nil name in data (json response contains `"name": null`); using login, it's always required - udata = userData{"login": "lll", "name": nil, "avatar_url": "http://demo.remark42.com/blah.png"} - user = r.MapUser(udata, nil) - assert.Equal(t, store.User{Name: "lll", ID: "github_e80b2d2608711cbb3312db7c4727a46fbad9601a", - Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) -} - -func TestProviders_NewFacebook(t *testing.T) { - r := NewFacebook(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"}) - assert.Equal(t, "facebook", r.Name) - - udata := userData{"id": "myid", "name": "test user"} - user := r.MapUser(udata, []byte(`{"picture": {"data": {"url": "http://demo.remark42.com/blah.png"} }}`)) - assert.Equal(t, store.User{Name: "test user", ID: "facebook_6e34471f84557e1713012d64a7477c71bfdac631", - Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) - - udata = userData{"id": "myid", "name": ""} - user = r.MapUser(udata, []byte(`{"picture": {"data": {"url": "http://demo.remark42.com/blah.png"} }}`)) - assert.Equal(t, store.User{Name: "facebook_6e34471", ID: "facebook_6e34471f84557e1713012d64a7477c71bfdac631", - Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) -} - -func TestProviders_NewYandex(t *testing.T) { - r := NewYandex(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"}) - assert.Equal(t, "yandex", r.Name) - - udata := userData{"id": "1234567890", "display_name": "Vasya P", "default_avatar_id": "131652443"} - user := r.MapUser(udata, nil) - assert.Equal(t, store.User{Name: "Vasya P", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", - Picture: "https://avatars.yandex.net/get-yapic/131652443/islands-200", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) - - // "display_name": null, "default_avatar_id": null - udata = userData{"id": "1234567890", "login": "vasya", "display_name": nil, "real_name": "Vasya Pupkin", "default_avatar_id": nil} - user = r.MapUser(udata, nil) - assert.Equal(t, store.User{Name: "Vasya Pupkin", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", - Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) - - // empty "display_name", empty "default_avatar_id", empty "real_name" - udata = userData{"id": "1234567890", "login": "vasya", "display_name": "", "real_name": "", "default_avatar_id": ""} - user = r.MapUser(udata, nil) - assert.Equal(t, store.User{Name: "vasya", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", - Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) - - // "real_name": null - udata = userData{"id": "1234567890", "login": "vasya", "real_name": nil, "default_avatar_id": ""} - user = r.MapUser(udata, nil) - assert.Equal(t, store.User{Name: "vasya", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", - Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/.gitignore b/backend/vendor/github.com/dgrijalva/jwt-go/.gitignore deleted file mode 100644 index 80bed650..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.DS_Store -bin - - diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/.travis.yml b/backend/vendor/github.com/dgrijalva/jwt-go/.travis.yml deleted file mode 100644 index 1027f56c..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: go - -script: - - go vet ./... - - go test -v ./... - -go: - - 1.3 - - 1.4 - - 1.5 - - 1.6 - - 1.7 - - tip diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/LICENSE b/backend/vendor/github.com/dgrijalva/jwt-go/LICENSE deleted file mode 100644 index df83a9c2..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/LICENSE +++ /dev/null @@ -1,8 +0,0 @@ -Copyright (c) 2012 Dave Grijalva - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md b/backend/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md deleted file mode 100644 index 7fc1f793..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md +++ /dev/null @@ -1,97 +0,0 @@ -## Migration Guide from v2 -> v3 - -Version 3 adds several new, frequently requested features. To do so, it introduces a few breaking changes. We've worked to keep these as minimal as possible. This guide explains the breaking changes and how you can quickly update your code. - -### `Token.Claims` is now an interface type - -The most requested feature from the 2.0 verison of this library was the ability to provide a custom type to the JSON parser for claims. This was implemented by introducing a new interface, `Claims`, to replace `map[string]interface{}`. We also included two concrete implementations of `Claims`: `MapClaims` and `StandardClaims`. - -`MapClaims` is an alias for `map[string]interface{}` with built in validation behavior. It is the default claims type when using `Parse`. The usage is unchanged except you must type cast the claims property. - -The old example for parsing a token looked like this.. - -```go - if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil { - fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) - } -``` - -is now directly mapped to... - -```go - if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil { - claims := token.Claims.(jwt.MapClaims) - fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"]) - } -``` - -`StandardClaims` is designed to be embedded in your custom type. You can supply a custom claims type with the new `ParseWithClaims` function. Here's an example of using a custom claims type. - -```go - type MyCustomClaims struct { - User string - *StandardClaims - } - - if token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, keyLookupFunc); err == nil { - claims := token.Claims.(*MyCustomClaims) - fmt.Printf("Token for user %v expires %v", claims.User, claims.StandardClaims.ExpiresAt) - } -``` - -### `ParseFromRequest` has been moved - -To keep this library focused on the tokens without becoming overburdened with complex request processing logic, `ParseFromRequest` and its new companion `ParseFromRequestWithClaims` have been moved to a subpackage, `request`. The method signatues have also been augmented to receive a new argument: `Extractor`. - -`Extractors` do the work of picking the token string out of a request. The interface is simple and composable. - -This simple parsing example: - -```go - if token, err := jwt.ParseFromRequest(tokenString, req, keyLookupFunc); err == nil { - fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) - } -``` - -is directly mapped to: - -```go - if token, err := request.ParseFromRequest(req, request.OAuth2Extractor, keyLookupFunc); err == nil { - claims := token.Claims.(jwt.MapClaims) - fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"]) - } -``` - -There are several concrete `Extractor` types provided for your convenience: - -* `HeaderExtractor` will search a list of headers until one contains content. -* `ArgumentExtractor` will search a list of keys in request query and form arguments until one contains content. -* `MultiExtractor` will try a list of `Extractors` in order until one returns content. -* `AuthorizationHeaderExtractor` will look in the `Authorization` header for a `Bearer` token. -* `OAuth2Extractor` searches the places an OAuth2 token would be specified (per the spec): `Authorization` header and `access_token` argument -* `PostExtractionFilter` wraps an `Extractor`, allowing you to process the content before it's parsed. A simple example is stripping the `Bearer ` text from a header - - -### RSA signing methods no longer accept `[]byte` keys - -Due to a [critical vulnerability](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/), we've decided the convenience of accepting `[]byte` instead of `rsa.PublicKey` or `rsa.PrivateKey` isn't worth the risk of misuse. - -To replace this behavior, we've added two helper methods: `ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error)` and `ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error)`. These are just simple helpers for unpacking PEM encoded PKCS1 and PKCS8 keys. If your keys are encoded any other way, all you need to do is convert them to the `crypto/rsa` package's types. - -```go - func keyLookupFunc(*Token) (interface{}, error) { - // Don't forget to validate the alg is what you expect: - if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - - // Look up key - key, err := lookupPublicKey(token.Header["kid"]) - if err != nil { - return nil, err - } - - // Unpack key from PEM encoded PKCS8 - return jwt.ParseRSAPublicKeyFromPEM(key) - } -``` diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/README.md b/backend/vendor/github.com/dgrijalva/jwt-go/README.md deleted file mode 100644 index d358d881..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# jwt-go - -[![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go) -[![GoDoc](https://godoc.org/github.com/dgrijalva/jwt-go?status.svg)](https://godoc.org/github.com/dgrijalva/jwt-go) - -A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) - -**NEW VERSION COMING:** There have been a lot of improvements suggested since the version 3.0.0 released in 2016. I'm working now on cutting two different releases: 3.2.0 will contain any non-breaking changes or enhancements. 4.0.0 will follow shortly which will include breaking changes. See the 4.0.0 milestone to get an idea of what's coming. If you have other ideas, or would like to participate in 4.0.0, now's the time. If you depend on this library and don't want to be interrupted, I recommend you use your dependency mangement tool to pin to version 3. - -**SECURITY NOTICE:** Some older versions of Go have a security issue in the cryotp/elliptic. Recommendation is to upgrade to at least 1.8.3. See issue #216 for more detail. - -**SECURITY NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided. - -## What the heck is a JWT? - -JWT.io has [a great introduction](https://jwt.io/introduction) to JSON Web Tokens. - -In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way. - -The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used. - -The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [the RFC](http://self-issued.info/docs/draft-jones-json-web-token.html) for information about reserved keys and the proper way to add your own. - -## What's in the box? - -This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are HMAC SHA, RSA, RSA-PSS, and ECDSA, though hooks are present for adding your own. - -## Examples - -See [the project documentation](https://godoc.org/github.com/dgrijalva/jwt-go) for examples of usage: - -* [Simple example of parsing and validating a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac) -* [Simple example of building and signing a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-New--Hmac) -* [Directory of Examples](https://godoc.org/github.com/dgrijalva/jwt-go#pkg-examples) - -## Extensions - -This library publishes all the necessary components for adding your own signing methods. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod`. - -Here's an example of an extension that integrates with the Google App Engine signing tools: https://github.com/someone1/gcp-jwt-go - -## Compliance - -This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences: - -* In order to protect against accidental use of [Unsecured JWTs](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#UnsecuredJWT), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key. - -## Project Status & Versioning - -This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason). - -This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases). - -While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v3`. It will do the right thing WRT semantic versioning. - -**BREAKING CHANGES:*** -* Version 3.0.0 includes _a lot_ of changes from the 2.x line, including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code. - -## Usage Tips - -### Signing vs Encryption - -A token is simply a JSON object that is signed by its author. this tells you exactly two things about the data: - -* The author of the token was in the possession of the signing secret -* The data has not been modified since it was signed - -It's important to know that JWT does not provide encryption, which means anyone who has access to the token can read its contents. If you need to protect (encrypt) the data, there is a companion spec, `JWE`, that provides this functionality. JWE is currently outside the scope of this library. - -### Choosing a Signing Method - -There are several signing methods available, and you should probably take the time to learn about the various options before choosing one. The principal design decision is most likely going to be symmetric vs asymmetric. - -Symmetric signing methods, such as HSA, use only a single secret. This is probably the simplest signing method to use since any `[]byte` can be used as a valid secret. They are also slightly computationally faster to use, though this rarely is enough to matter. Symmetric signing methods work the best when both producers and consumers of tokens are trusted, or even the same system. Since the same secret is used to both sign and validate tokens, you can't easily distribute the key for validation. - -Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification. - -### Signing Methods and Key Types - -Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones: - -* The [HMAC signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation -* The [RSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation -* The [ECDSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation - -### JWT and OAuth - -It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication. - -Without going too far down the rabbit hole, here's a description of the interaction of these technologies: - -* OAuth is a protocol for allowing an identity provider to be separate from the service a user is logging in to. For example, whenever you use Facebook to log into a different service (Yelp, Spotify, etc), you are using OAuth. -* OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token. -* Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL. - -## More - -Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go). - -The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation. diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md b/backend/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md deleted file mode 100644 index 63702983..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md +++ /dev/null @@ -1,118 +0,0 @@ -## `jwt-go` Version History - -#### 3.2.0 - -* Added method `ParseUnverified` to allow users to split up the tasks of parsing and validation -* HMAC signing method returns `ErrInvalidKeyType` instead of `ErrInvalidKey` where appropriate -* Added options to `request.ParseFromRequest`, which allows for an arbitrary list of modifiers to parsing behavior. Initial set include `WithClaims` and `WithParser`. Existing usage of this function will continue to work as before. -* Deprecated `ParseFromRequestWithClaims` to simplify API in the future. - -#### 3.1.0 - -* Improvements to `jwt` command line tool -* Added `SkipClaimsValidation` option to `Parser` -* Documentation updates - -#### 3.0.0 - -* **Compatibility Breaking Changes**: See MIGRATION_GUIDE.md for tips on updating your code - * Dropped support for `[]byte` keys when using RSA signing methods. This convenience feature could contribute to security vulnerabilities involving mismatched key types with signing methods. - * `ParseFromRequest` has been moved to `request` subpackage and usage has changed - * The `Claims` property on `Token` is now type `Claims` instead of `map[string]interface{}`. The default value is type `MapClaims`, which is an alias to `map[string]interface{}`. This makes it possible to use a custom type when decoding claims. -* Other Additions and Changes - * Added `Claims` interface type to allow users to decode the claims into a custom type - * Added `ParseWithClaims`, which takes a third argument of type `Claims`. Use this function instead of `Parse` if you have a custom type you'd like to decode into. - * Dramatically improved the functionality and flexibility of `ParseFromRequest`, which is now in the `request` subpackage - * Added `ParseFromRequestWithClaims` which is the `FromRequest` equivalent of `ParseWithClaims` - * Added new interface type `Extractor`, which is used for extracting JWT strings from http requests. Used with `ParseFromRequest` and `ParseFromRequestWithClaims`. - * Added several new, more specific, validation errors to error type bitmask - * Moved examples from README to executable example files - * Signing method registry is now thread safe - * Added new property to `ValidationError`, which contains the raw error returned by calls made by parse/verify (such as those returned by keyfunc or json parser) - -#### 2.7.0 - -This will likely be the last backwards compatible release before 3.0.0, excluding essential bug fixes. - -* Added new option `-show` to the `jwt` command that will just output the decoded token without verifying -* Error text for expired tokens includes how long it's been expired -* Fixed incorrect error returned from `ParseRSAPublicKeyFromPEM` -* Documentation updates - -#### 2.6.0 - -* Exposed inner error within ValidationError -* Fixed validation errors when using UseJSONNumber flag -* Added several unit tests - -#### 2.5.0 - -* Added support for signing method none. You shouldn't use this. The API tries to make this clear. -* Updated/fixed some documentation -* Added more helpful error message when trying to parse tokens that begin with `BEARER ` - -#### 2.4.0 - -* Added new type, Parser, to allow for configuration of various parsing parameters - * You can now specify a list of valid signing methods. Anything outside this set will be rejected. - * You can now opt to use the `json.Number` type instead of `float64` when parsing token JSON -* Added support for [Travis CI](https://travis-ci.org/dgrijalva/jwt-go) -* Fixed some bugs with ECDSA parsing - -#### 2.3.0 - -* Added support for ECDSA signing methods -* Added support for RSA PSS signing methods (requires go v1.4) - -#### 2.2.0 - -* Gracefully handle a `nil` `Keyfunc` being passed to `Parse`. Result will now be the parsed token and an error, instead of a panic. - -#### 2.1.0 - -Backwards compatible API change that was missed in 2.0.0. - -* The `SignedString` method on `Token` now takes `interface{}` instead of `[]byte` - -#### 2.0.0 - -There were two major reasons for breaking backwards compatibility with this update. The first was a refactor required to expand the width of the RSA and HMAC-SHA signing implementations. There will likely be no required code changes to support this change. - -The second update, while unfortunately requiring a small change in integration, is required to open up this library to other signing methods. Not all keys used for all signing methods have a single standard on-disk representation. Requiring `[]byte` as the type for all keys proved too limiting. Additionally, this implementation allows for pre-parsed tokens to be reused, which might matter in an application that parses a high volume of tokens with a small set of keys. Backwards compatibilty has been maintained for passing `[]byte` to the RSA signing methods, but they will also accept `*rsa.PublicKey` and `*rsa.PrivateKey`. - -It is likely the only integration change required here will be to change `func(t *jwt.Token) ([]byte, error)` to `func(t *jwt.Token) (interface{}, error)` when calling `Parse`. - -* **Compatibility Breaking Changes** - * `SigningMethodHS256` is now `*SigningMethodHMAC` instead of `type struct` - * `SigningMethodRS256` is now `*SigningMethodRSA` instead of `type struct` - * `KeyFunc` now returns `interface{}` instead of `[]byte` - * `SigningMethod.Sign` now takes `interface{}` instead of `[]byte` for the key - * `SigningMethod.Verify` now takes `interface{}` instead of `[]byte` for the key -* Renamed type `SigningMethodHS256` to `SigningMethodHMAC`. Specific sizes are now just instances of this type. - * Added public package global `SigningMethodHS256` - * Added public package global `SigningMethodHS384` - * Added public package global `SigningMethodHS512` -* Renamed type `SigningMethodRS256` to `SigningMethodRSA`. Specific sizes are now just instances of this type. - * Added public package global `SigningMethodRS256` - * Added public package global `SigningMethodRS384` - * Added public package global `SigningMethodRS512` -* Moved sample private key for HMAC tests from an inline value to a file on disk. Value is unchanged. -* Refactored the RSA implementation to be easier to read -* Exposed helper methods `ParseRSAPrivateKeyFromPEM` and `ParseRSAPublicKeyFromPEM` - -#### 1.0.2 - -* Fixed bug in parsing public keys from certificates -* Added more tests around the parsing of keys for RS256 -* Code refactoring in RS256 implementation. No functional changes - -#### 1.0.1 - -* Fixed panic if RS256 signing method was passed an invalid key - -#### 1.0.0 - -* First versioned release -* API stabilized -* Supports creating, signing, parsing, and validating JWT tokens -* Supports RS256 and HS256 signing methods \ No newline at end of file diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/claims.go b/backend/vendor/github.com/dgrijalva/jwt-go/claims.go deleted file mode 100644 index f0228f02..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/claims.go +++ /dev/null @@ -1,134 +0,0 @@ -package jwt - -import ( - "crypto/subtle" - "fmt" - "time" -) - -// For a type to be a Claims object, it must just have a Valid method that determines -// if the token is invalid for any supported reason -type Claims interface { - Valid() error -} - -// Structured version of Claims Section, as referenced at -// https://tools.ietf.org/html/rfc7519#section-4.1 -// See examples for how to use this with your own claim types -type StandardClaims struct { - Audience string `json:"aud,omitempty"` - ExpiresAt int64 `json:"exp,omitempty"` - Id string `json:"jti,omitempty"` - IssuedAt int64 `json:"iat,omitempty"` - Issuer string `json:"iss,omitempty"` - NotBefore int64 `json:"nbf,omitempty"` - Subject string `json:"sub,omitempty"` -} - -// Validates time based claims "exp, iat, nbf". -// There is no accounting for clock skew. -// As well, if any of the above claims are not in the token, it will still -// be considered a valid claim. -func (c StandardClaims) Valid() error { - vErr := new(ValidationError) - now := TimeFunc().Unix() - - // The claims below are optional, by default, so if they are set to the - // default value in Go, let's not fail the verification for them. - if c.VerifyExpiresAt(now, false) == false { - delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) - vErr.Inner = fmt.Errorf("token is expired by %v", delta) - vErr.Errors |= ValidationErrorExpired - } - - if c.VerifyIssuedAt(now, false) == false { - vErr.Inner = fmt.Errorf("Token used before issued") - vErr.Errors |= ValidationErrorIssuedAt - } - - if c.VerifyNotBefore(now, false) == false { - vErr.Inner = fmt.Errorf("token is not valid yet") - vErr.Errors |= ValidationErrorNotValidYet - } - - if vErr.valid() { - return nil - } - - return vErr -} - -// Compares the aud claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { - return verifyAud(c.Audience, cmp, req) -} - -// Compares the exp claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { - return verifyExp(c.ExpiresAt, cmp, req) -} - -// Compares the iat claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { - return verifyIat(c.IssuedAt, cmp, req) -} - -// Compares the iss claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { - return verifyIss(c.Issuer, cmp, req) -} - -// Compares the nbf claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { - return verifyNbf(c.NotBefore, cmp, req) -} - -// ----- helpers - -func verifyAud(aud string, cmp string, required bool) bool { - if aud == "" { - return !required - } - if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { - return true - } else { - return false - } -} - -func verifyExp(exp int64, now int64, required bool) bool { - if exp == 0 { - return !required - } - return now <= exp -} - -func verifyIat(iat int64, now int64, required bool) bool { - if iat == 0 { - return !required - } - return now >= iat -} - -func verifyIss(iss string, cmp string, required bool) bool { - if iss == "" { - return !required - } - if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { - return true - } else { - return false - } -} - -func verifyNbf(nbf int64, now int64, required bool) bool { - if nbf == 0 { - return !required - } - return now >= nbf -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/doc.go b/backend/vendor/github.com/dgrijalva/jwt-go/doc.go deleted file mode 100644 index a86dc1a3..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package jwt is a Go implementation of JSON Web Tokens: http://self-issued.info/docs/draft-jones-json-web-token.html -// -// See README.md for more info. -package jwt diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa.go b/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa.go deleted file mode 100644 index f9773812..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa.go +++ /dev/null @@ -1,148 +0,0 @@ -package jwt - -import ( - "crypto" - "crypto/ecdsa" - "crypto/rand" - "errors" - "math/big" -) - -var ( - // Sadly this is missing from crypto/ecdsa compared to crypto/rsa - ErrECDSAVerification = errors.New("crypto/ecdsa: verification error") -) - -// Implements the ECDSA family of signing methods signing methods -// Expects *ecdsa.PrivateKey for signing and *ecdsa.PublicKey for verification -type SigningMethodECDSA struct { - Name string - Hash crypto.Hash - KeySize int - CurveBits int -} - -// Specific instances for EC256 and company -var ( - SigningMethodES256 *SigningMethodECDSA - SigningMethodES384 *SigningMethodECDSA - SigningMethodES512 *SigningMethodECDSA -) - -func init() { - // ES256 - SigningMethodES256 = &SigningMethodECDSA{"ES256", crypto.SHA256, 32, 256} - RegisterSigningMethod(SigningMethodES256.Alg(), func() SigningMethod { - return SigningMethodES256 - }) - - // ES384 - SigningMethodES384 = &SigningMethodECDSA{"ES384", crypto.SHA384, 48, 384} - RegisterSigningMethod(SigningMethodES384.Alg(), func() SigningMethod { - return SigningMethodES384 - }) - - // ES512 - SigningMethodES512 = &SigningMethodECDSA{"ES512", crypto.SHA512, 66, 521} - RegisterSigningMethod(SigningMethodES512.Alg(), func() SigningMethod { - return SigningMethodES512 - }) -} - -func (m *SigningMethodECDSA) Alg() string { - return m.Name -} - -// Implements the Verify method from SigningMethod -// For this verify method, key must be an ecdsa.PublicKey struct -func (m *SigningMethodECDSA) Verify(signingString, signature string, key interface{}) error { - var err error - - // Decode the signature - var sig []byte - if sig, err = DecodeSegment(signature); err != nil { - return err - } - - // Get the key - var ecdsaKey *ecdsa.PublicKey - switch k := key.(type) { - case *ecdsa.PublicKey: - ecdsaKey = k - default: - return ErrInvalidKeyType - } - - if len(sig) != 2*m.KeySize { - return ErrECDSAVerification - } - - r := big.NewInt(0).SetBytes(sig[:m.KeySize]) - s := big.NewInt(0).SetBytes(sig[m.KeySize:]) - - // Create hasher - if !m.Hash.Available() { - return ErrHashUnavailable - } - hasher := m.Hash.New() - hasher.Write([]byte(signingString)) - - // Verify the signature - if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), r, s); verifystatus == true { - return nil - } else { - return ErrECDSAVerification - } -} - -// Implements the Sign method from SigningMethod -// For this signing method, key must be an ecdsa.PrivateKey struct -func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string, error) { - // Get the key - var ecdsaKey *ecdsa.PrivateKey - switch k := key.(type) { - case *ecdsa.PrivateKey: - ecdsaKey = k - default: - return "", ErrInvalidKeyType - } - - // Create the hasher - if !m.Hash.Available() { - return "", ErrHashUnavailable - } - - hasher := m.Hash.New() - hasher.Write([]byte(signingString)) - - // Sign the string and return r, s - if r, s, err := ecdsa.Sign(rand.Reader, ecdsaKey, hasher.Sum(nil)); err == nil { - curveBits := ecdsaKey.Curve.Params().BitSize - - if m.CurveBits != curveBits { - return "", ErrInvalidKey - } - - keyBytes := curveBits / 8 - if curveBits%8 > 0 { - keyBytes += 1 - } - - // We serialize the outpus (r and s) into big-endian byte arrays and pad - // them with zeros on the left to make sure the sizes work out. Both arrays - // must be keyBytes long, and the output must be 2*keyBytes long. - rBytes := r.Bytes() - rBytesPadded := make([]byte, keyBytes) - copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) - - sBytes := s.Bytes() - sBytesPadded := make([]byte, keyBytes) - copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) - - out := append(rBytesPadded, sBytesPadded...) - - return EncodeSegment(out), nil - } else { - return "", err - } -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go b/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go deleted file mode 100644 index d19624b7..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go +++ /dev/null @@ -1,67 +0,0 @@ -package jwt - -import ( - "crypto/ecdsa" - "crypto/x509" - "encoding/pem" - "errors" -) - -var ( - ErrNotECPublicKey = errors.New("Key is not a valid ECDSA public key") - ErrNotECPrivateKey = errors.New("Key is not a valid ECDSA private key") -) - -// Parse PEM encoded Elliptic Curve Private Key Structure -func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) { - var err error - - // Parse PEM block - var block *pem.Block - if block, _ = pem.Decode(key); block == nil { - return nil, ErrKeyMustBePEMEncoded - } - - // Parse the key - var parsedKey interface{} - if parsedKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { - return nil, err - } - - var pkey *ecdsa.PrivateKey - var ok bool - if pkey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { - return nil, ErrNotECPrivateKey - } - - return pkey, nil -} - -// Parse PEM encoded PKCS1 or PKCS8 public key -func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) { - var err error - - // Parse PEM block - var block *pem.Block - if block, _ = pem.Decode(key); block == nil { - return nil, ErrKeyMustBePEMEncoded - } - - // Parse the key - var parsedKey interface{} - if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { - if cert, err := x509.ParseCertificate(block.Bytes); err == nil { - parsedKey = cert.PublicKey - } else { - return nil, err - } - } - - var pkey *ecdsa.PublicKey - var ok bool - if pkey, ok = parsedKey.(*ecdsa.PublicKey); !ok { - return nil, ErrNotECPublicKey - } - - return pkey, nil -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/errors.go b/backend/vendor/github.com/dgrijalva/jwt-go/errors.go deleted file mode 100644 index 1c93024a..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/errors.go +++ /dev/null @@ -1,59 +0,0 @@ -package jwt - -import ( - "errors" -) - -// Error constants -var ( - ErrInvalidKey = errors.New("key is invalid") - ErrInvalidKeyType = errors.New("key is of invalid type") - ErrHashUnavailable = errors.New("the requested hash function is unavailable") -) - -// The errors that might occur when parsing and validating a token -const ( - ValidationErrorMalformed uint32 = 1 << iota // Token is malformed - ValidationErrorUnverifiable // Token could not be verified because of signing problems - ValidationErrorSignatureInvalid // Signature validation failed - - // Standard Claim validation errors - ValidationErrorAudience // AUD validation failed - ValidationErrorExpired // EXP validation failed - ValidationErrorIssuedAt // IAT validation failed - ValidationErrorIssuer // ISS validation failed - ValidationErrorNotValidYet // NBF validation failed - ValidationErrorId // JTI validation failed - ValidationErrorClaimsInvalid // Generic claims validation error -) - -// Helper for constructing a ValidationError with a string error message -func NewValidationError(errorText string, errorFlags uint32) *ValidationError { - return &ValidationError{ - text: errorText, - Errors: errorFlags, - } -} - -// The error from Parse if token is not valid -type ValidationError struct { - Inner error // stores the error returned by external dependencies, i.e.: KeyFunc - Errors uint32 // bitfield. see ValidationError... constants - text string // errors that do not have a valid error just have text -} - -// Validation error is an error type -func (e ValidationError) Error() string { - if e.Inner != nil { - return e.Inner.Error() - } else if e.text != "" { - return e.text - } else { - return "token is invalid" - } -} - -// No errors -func (e *ValidationError) valid() bool { - return e.Errors == 0 -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/hmac.go b/backend/vendor/github.com/dgrijalva/jwt-go/hmac.go deleted file mode 100644 index addbe5d4..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/hmac.go +++ /dev/null @@ -1,95 +0,0 @@ -package jwt - -import ( - "crypto" - "crypto/hmac" - "errors" -) - -// Implements the HMAC-SHA family of signing methods signing methods -// Expects key type of []byte for both signing and validation -type SigningMethodHMAC struct { - Name string - Hash crypto.Hash -} - -// Specific instances for HS256 and company -var ( - SigningMethodHS256 *SigningMethodHMAC - SigningMethodHS384 *SigningMethodHMAC - SigningMethodHS512 *SigningMethodHMAC - ErrSignatureInvalid = errors.New("signature is invalid") -) - -func init() { - // HS256 - SigningMethodHS256 = &SigningMethodHMAC{"HS256", crypto.SHA256} - RegisterSigningMethod(SigningMethodHS256.Alg(), func() SigningMethod { - return SigningMethodHS256 - }) - - // HS384 - SigningMethodHS384 = &SigningMethodHMAC{"HS384", crypto.SHA384} - RegisterSigningMethod(SigningMethodHS384.Alg(), func() SigningMethod { - return SigningMethodHS384 - }) - - // HS512 - SigningMethodHS512 = &SigningMethodHMAC{"HS512", crypto.SHA512} - RegisterSigningMethod(SigningMethodHS512.Alg(), func() SigningMethod { - return SigningMethodHS512 - }) -} - -func (m *SigningMethodHMAC) Alg() string { - return m.Name -} - -// Verify the signature of HSXXX tokens. Returns nil if the signature is valid. -func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error { - // Verify the key is the right type - keyBytes, ok := key.([]byte) - if !ok { - return ErrInvalidKeyType - } - - // Decode signature, for comparison - sig, err := DecodeSegment(signature) - if err != nil { - return err - } - - // Can we use the specified hashing method? - if !m.Hash.Available() { - return ErrHashUnavailable - } - - // This signing method is symmetric, so we validate the signature - // by reproducing the signature from the signing string and key, then - // comparing that against the provided signature. - hasher := hmac.New(m.Hash.New, keyBytes) - hasher.Write([]byte(signingString)) - if !hmac.Equal(sig, hasher.Sum(nil)) { - return ErrSignatureInvalid - } - - // No validation errors. Signature is good. - return nil -} - -// Implements the Sign method from SigningMethod for this signing method. -// Key must be []byte -func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) { - if keyBytes, ok := key.([]byte); ok { - if !m.Hash.Available() { - return "", ErrHashUnavailable - } - - hasher := hmac.New(m.Hash.New, keyBytes) - hasher.Write([]byte(signingString)) - - return EncodeSegment(hasher.Sum(nil)), nil - } - - return "", ErrInvalidKeyType -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/map_claims.go b/backend/vendor/github.com/dgrijalva/jwt-go/map_claims.go deleted file mode 100644 index 291213c4..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/map_claims.go +++ /dev/null @@ -1,94 +0,0 @@ -package jwt - -import ( - "encoding/json" - "errors" - // "fmt" -) - -// Claims type that uses the map[string]interface{} for JSON decoding -// This is the default claims type if you don't supply one -type MapClaims map[string]interface{} - -// Compares the aud claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaims) VerifyAudience(cmp string, req bool) bool { - aud, _ := m["aud"].(string) - return verifyAud(aud, cmp, req) -} - -// Compares the exp claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { - switch exp := m["exp"].(type) { - case float64: - return verifyExp(int64(exp), cmp, req) - case json.Number: - v, _ := exp.Int64() - return verifyExp(v, cmp, req) - } - return req == false -} - -// Compares the iat claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { - switch iat := m["iat"].(type) { - case float64: - return verifyIat(int64(iat), cmp, req) - case json.Number: - v, _ := iat.Int64() - return verifyIat(v, cmp, req) - } - return req == false -} - -// Compares the iss claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaims) VerifyIssuer(cmp string, req bool) bool { - iss, _ := m["iss"].(string) - return verifyIss(iss, cmp, req) -} - -// Compares the nbf claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { - switch nbf := m["nbf"].(type) { - case float64: - return verifyNbf(int64(nbf), cmp, req) - case json.Number: - v, _ := nbf.Int64() - return verifyNbf(v, cmp, req) - } - return req == false -} - -// Validates time based claims "exp, iat, nbf". -// There is no accounting for clock skew. -// As well, if any of the above claims are not in the token, it will still -// be considered a valid claim. -func (m MapClaims) Valid() error { - vErr := new(ValidationError) - now := TimeFunc().Unix() - - if m.VerifyExpiresAt(now, false) == false { - vErr.Inner = errors.New("Token is expired") - vErr.Errors |= ValidationErrorExpired - } - - if m.VerifyIssuedAt(now, false) == false { - vErr.Inner = errors.New("Token used before issued") - vErr.Errors |= ValidationErrorIssuedAt - } - - if m.VerifyNotBefore(now, false) == false { - vErr.Inner = errors.New("Token is not valid yet") - vErr.Errors |= ValidationErrorNotValidYet - } - - if vErr.valid() { - return nil - } - - return vErr -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/none.go b/backend/vendor/github.com/dgrijalva/jwt-go/none.go deleted file mode 100644 index f04d189d..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/none.go +++ /dev/null @@ -1,52 +0,0 @@ -package jwt - -// Implements the none signing method. This is required by the spec -// but you probably should never use it. -var SigningMethodNone *signingMethodNone - -const UnsafeAllowNoneSignatureType unsafeNoneMagicConstant = "none signing method allowed" - -var NoneSignatureTypeDisallowedError error - -type signingMethodNone struct{} -type unsafeNoneMagicConstant string - -func init() { - SigningMethodNone = &signingMethodNone{} - NoneSignatureTypeDisallowedError = NewValidationError("'none' signature type is not allowed", ValidationErrorSignatureInvalid) - - RegisterSigningMethod(SigningMethodNone.Alg(), func() SigningMethod { - return SigningMethodNone - }) -} - -func (m *signingMethodNone) Alg() string { - return "none" -} - -// Only allow 'none' alg type if UnsafeAllowNoneSignatureType is specified as the key -func (m *signingMethodNone) Verify(signingString, signature string, key interface{}) (err error) { - // Key must be UnsafeAllowNoneSignatureType to prevent accidentally - // accepting 'none' signing method - if _, ok := key.(unsafeNoneMagicConstant); !ok { - return NoneSignatureTypeDisallowedError - } - // If signing method is none, signature must be an empty string - if signature != "" { - return NewValidationError( - "'none' signing method with non-empty signature", - ValidationErrorSignatureInvalid, - ) - } - - // Accept 'none' signing method. - return nil -} - -// Only allow 'none' signing if UnsafeAllowNoneSignatureType is specified as the key -func (m *signingMethodNone) Sign(signingString string, key interface{}) (string, error) { - if _, ok := key.(unsafeNoneMagicConstant); ok { - return "", nil - } - return "", NoneSignatureTypeDisallowedError -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/parser.go b/backend/vendor/github.com/dgrijalva/jwt-go/parser.go deleted file mode 100644 index d6901d9a..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/parser.go +++ /dev/null @@ -1,148 +0,0 @@ -package jwt - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" -) - -type Parser struct { - ValidMethods []string // If populated, only these methods will be considered valid - UseJSONNumber bool // Use JSON Number format in JSON decoder - SkipClaimsValidation bool // Skip claims validation during token parsing -} - -// Parse, validate, and return a token. -// keyFunc will receive the parsed token and should return the key for validating. -// If everything is kosher, err will be nil -func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc) -} - -func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { - token, parts, err := p.ParseUnverified(tokenString, claims) - if err != nil { - return token, err - } - - // Verify signing method is in the required set - if p.ValidMethods != nil { - var signingMethodValid = false - var alg = token.Method.Alg() - for _, m := range p.ValidMethods { - if m == alg { - signingMethodValid = true - break - } - } - if !signingMethodValid { - // signing method is not in the listed set - return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid) - } - } - - // Lookup key - var key interface{} - if keyFunc == nil { - // keyFunc was not provided. short circuiting validation - return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable) - } - if key, err = keyFunc(token); err != nil { - // keyFunc returned an error - if ve, ok := err.(*ValidationError); ok { - return token, ve - } - return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable} - } - - vErr := &ValidationError{} - - // Validate Claims - if !p.SkipClaimsValidation { - if err := token.Claims.Valid(); err != nil { - - // If the Claims Valid returned an error, check if it is a validation error, - // If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set - if e, ok := err.(*ValidationError); !ok { - vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid} - } else { - vErr = e - } - } - } - - // Perform validation - token.Signature = parts[2] - if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil { - vErr.Inner = err - vErr.Errors |= ValidationErrorSignatureInvalid - } - - if vErr.valid() { - token.Valid = true - return token, nil - } - - return token, vErr -} - -// WARNING: Don't use this method unless you know what you're doing -// -// This method parses the token but doesn't validate the signature. It's only -// ever useful in cases where you know the signature is valid (because it has -// been checked previously in the stack) and you want to extract values from -// it. -func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) { - parts = strings.Split(tokenString, ".") - if len(parts) != 3 { - return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed) - } - - token = &Token{Raw: tokenString} - - // parse Header - var headerBytes []byte - if headerBytes, err = DecodeSegment(parts[0]); err != nil { - if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") { - return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed) - } - return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} - } - if err = json.Unmarshal(headerBytes, &token.Header); err != nil { - return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} - } - - // parse Claims - var claimBytes []byte - token.Claims = claims - - if claimBytes, err = DecodeSegment(parts[1]); err != nil { - return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} - } - dec := json.NewDecoder(bytes.NewBuffer(claimBytes)) - if p.UseJSONNumber { - dec.UseNumber() - } - // JSON Decode. Special case for map type to avoid weird pointer behavior - if c, ok := token.Claims.(MapClaims); ok { - err = dec.Decode(&c) - } else { - err = dec.Decode(&claims) - } - // Handle decode error - if err != nil { - return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} - } - - // Lookup signature method - if method, ok := token.Header["alg"].(string); ok { - if token.Method = GetSigningMethod(method); token.Method == nil { - return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable) - } - } else { - return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable) - } - - return token, parts, nil -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/rsa.go b/backend/vendor/github.com/dgrijalva/jwt-go/rsa.go deleted file mode 100644 index e4caf1ca..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/rsa.go +++ /dev/null @@ -1,101 +0,0 @@ -package jwt - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" -) - -// Implements the RSA family of signing methods signing methods -// Expects *rsa.PrivateKey for signing and *rsa.PublicKey for validation -type SigningMethodRSA struct { - Name string - Hash crypto.Hash -} - -// Specific instances for RS256 and company -var ( - SigningMethodRS256 *SigningMethodRSA - SigningMethodRS384 *SigningMethodRSA - SigningMethodRS512 *SigningMethodRSA -) - -func init() { - // RS256 - SigningMethodRS256 = &SigningMethodRSA{"RS256", crypto.SHA256} - RegisterSigningMethod(SigningMethodRS256.Alg(), func() SigningMethod { - return SigningMethodRS256 - }) - - // RS384 - SigningMethodRS384 = &SigningMethodRSA{"RS384", crypto.SHA384} - RegisterSigningMethod(SigningMethodRS384.Alg(), func() SigningMethod { - return SigningMethodRS384 - }) - - // RS512 - SigningMethodRS512 = &SigningMethodRSA{"RS512", crypto.SHA512} - RegisterSigningMethod(SigningMethodRS512.Alg(), func() SigningMethod { - return SigningMethodRS512 - }) -} - -func (m *SigningMethodRSA) Alg() string { - return m.Name -} - -// Implements the Verify method from SigningMethod -// For this signing method, must be an *rsa.PublicKey structure. -func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error { - var err error - - // Decode the signature - var sig []byte - if sig, err = DecodeSegment(signature); err != nil { - return err - } - - var rsaKey *rsa.PublicKey - var ok bool - - if rsaKey, ok = key.(*rsa.PublicKey); !ok { - return ErrInvalidKeyType - } - - // Create hasher - if !m.Hash.Available() { - return ErrHashUnavailable - } - hasher := m.Hash.New() - hasher.Write([]byte(signingString)) - - // Verify the signature - return rsa.VerifyPKCS1v15(rsaKey, m.Hash, hasher.Sum(nil), sig) -} - -// Implements the Sign method from SigningMethod -// For this signing method, must be an *rsa.PrivateKey structure. -func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) { - var rsaKey *rsa.PrivateKey - var ok bool - - // Validate type of key - if rsaKey, ok = key.(*rsa.PrivateKey); !ok { - return "", ErrInvalidKey - } - - // Create the hasher - if !m.Hash.Available() { - return "", ErrHashUnavailable - } - - hasher := m.Hash.New() - hasher.Write([]byte(signingString)) - - // Sign the string and return the encoded bytes - if sigBytes, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil)); err == nil { - return EncodeSegment(sigBytes), nil - } else { - return "", err - } -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go b/backend/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go deleted file mode 100644 index 10ee9db8..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go +++ /dev/null @@ -1,126 +0,0 @@ -// +build go1.4 - -package jwt - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" -) - -// Implements the RSAPSS family of signing methods signing methods -type SigningMethodRSAPSS struct { - *SigningMethodRSA - Options *rsa.PSSOptions -} - -// Specific instances for RS/PS and company -var ( - SigningMethodPS256 *SigningMethodRSAPSS - SigningMethodPS384 *SigningMethodRSAPSS - SigningMethodPS512 *SigningMethodRSAPSS -) - -func init() { - // PS256 - SigningMethodPS256 = &SigningMethodRSAPSS{ - &SigningMethodRSA{ - Name: "PS256", - Hash: crypto.SHA256, - }, - &rsa.PSSOptions{ - SaltLength: rsa.PSSSaltLengthAuto, - Hash: crypto.SHA256, - }, - } - RegisterSigningMethod(SigningMethodPS256.Alg(), func() SigningMethod { - return SigningMethodPS256 - }) - - // PS384 - SigningMethodPS384 = &SigningMethodRSAPSS{ - &SigningMethodRSA{ - Name: "PS384", - Hash: crypto.SHA384, - }, - &rsa.PSSOptions{ - SaltLength: rsa.PSSSaltLengthAuto, - Hash: crypto.SHA384, - }, - } - RegisterSigningMethod(SigningMethodPS384.Alg(), func() SigningMethod { - return SigningMethodPS384 - }) - - // PS512 - SigningMethodPS512 = &SigningMethodRSAPSS{ - &SigningMethodRSA{ - Name: "PS512", - Hash: crypto.SHA512, - }, - &rsa.PSSOptions{ - SaltLength: rsa.PSSSaltLengthAuto, - Hash: crypto.SHA512, - }, - } - RegisterSigningMethod(SigningMethodPS512.Alg(), func() SigningMethod { - return SigningMethodPS512 - }) -} - -// Implements the Verify method from SigningMethod -// For this verify method, key must be an rsa.PublicKey struct -func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error { - var err error - - // Decode the signature - var sig []byte - if sig, err = DecodeSegment(signature); err != nil { - return err - } - - var rsaKey *rsa.PublicKey - switch k := key.(type) { - case *rsa.PublicKey: - rsaKey = k - default: - return ErrInvalidKey - } - - // Create hasher - if !m.Hash.Available() { - return ErrHashUnavailable - } - hasher := m.Hash.New() - hasher.Write([]byte(signingString)) - - return rsa.VerifyPSS(rsaKey, m.Hash, hasher.Sum(nil), sig, m.Options) -} - -// Implements the Sign method from SigningMethod -// For this signing method, key must be an rsa.PrivateKey struct -func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (string, error) { - var rsaKey *rsa.PrivateKey - - switch k := key.(type) { - case *rsa.PrivateKey: - rsaKey = k - default: - return "", ErrInvalidKeyType - } - - // Create the hasher - if !m.Hash.Available() { - return "", ErrHashUnavailable - } - - hasher := m.Hash.New() - hasher.Write([]byte(signingString)) - - // Sign the string and return the encoded bytes - if sigBytes, err := rsa.SignPSS(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil), m.Options); err == nil { - return EncodeSegment(sigBytes), nil - } else { - return "", err - } -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go b/backend/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go deleted file mode 100644 index a5ababf9..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go +++ /dev/null @@ -1,101 +0,0 @@ -package jwt - -import ( - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "errors" -) - -var ( - ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key") - ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key") - ErrNotRSAPublicKey = errors.New("Key is not a valid RSA public key") -) - -// Parse PEM encoded PKCS1 or PKCS8 private key -func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) { - var err error - - // Parse PEM block - var block *pem.Block - if block, _ = pem.Decode(key); block == nil { - return nil, ErrKeyMustBePEMEncoded - } - - var parsedKey interface{} - if parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { - if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { - return nil, err - } - } - - var pkey *rsa.PrivateKey - var ok bool - if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok { - return nil, ErrNotRSAPrivateKey - } - - return pkey, nil -} - -// Parse PEM encoded PKCS1 or PKCS8 private key protected with password -func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.PrivateKey, error) { - var err error - - // Parse PEM block - var block *pem.Block - if block, _ = pem.Decode(key); block == nil { - return nil, ErrKeyMustBePEMEncoded - } - - var parsedKey interface{} - - var blockDecrypted []byte - if blockDecrypted, err = x509.DecryptPEMBlock(block, []byte(password)); err != nil { - return nil, err - } - - if parsedKey, err = x509.ParsePKCS1PrivateKey(blockDecrypted); err != nil { - if parsedKey, err = x509.ParsePKCS8PrivateKey(blockDecrypted); err != nil { - return nil, err - } - } - - var pkey *rsa.PrivateKey - var ok bool - if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok { - return nil, ErrNotRSAPrivateKey - } - - return pkey, nil -} - -// Parse PEM encoded PKCS1 or PKCS8 public key -func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) { - var err error - - // Parse PEM block - var block *pem.Block - if block, _ = pem.Decode(key); block == nil { - return nil, ErrKeyMustBePEMEncoded - } - - // Parse the key - var parsedKey interface{} - if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { - if cert, err := x509.ParseCertificate(block.Bytes); err == nil { - parsedKey = cert.PublicKey - } else { - return nil, err - } - } - - var pkey *rsa.PublicKey - var ok bool - if pkey, ok = parsedKey.(*rsa.PublicKey); !ok { - return nil, ErrNotRSAPublicKey - } - - return pkey, nil -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/signing_method.go b/backend/vendor/github.com/dgrijalva/jwt-go/signing_method.go deleted file mode 100644 index ed1f212b..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/signing_method.go +++ /dev/null @@ -1,35 +0,0 @@ -package jwt - -import ( - "sync" -) - -var signingMethods = map[string]func() SigningMethod{} -var signingMethodLock = new(sync.RWMutex) - -// Implement SigningMethod to add new methods for signing or verifying tokens. -type SigningMethod interface { - Verify(signingString, signature string, key interface{}) error // Returns nil if signature is valid - Sign(signingString string, key interface{}) (string, error) // Returns encoded signature or error - Alg() string // returns the alg identifier for this method (example: 'HS256') -} - -// Register the "alg" name and a factory function for signing method. -// This is typically done during init() in the method's implementation -func RegisterSigningMethod(alg string, f func() SigningMethod) { - signingMethodLock.Lock() - defer signingMethodLock.Unlock() - - signingMethods[alg] = f -} - -// Get a signing method from an "alg" string -func GetSigningMethod(alg string) (method SigningMethod) { - signingMethodLock.RLock() - defer signingMethodLock.RUnlock() - - if methodF, ok := signingMethods[alg]; ok { - method = methodF() - } - return -} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/token.go b/backend/vendor/github.com/dgrijalva/jwt-go/token.go deleted file mode 100644 index d637e086..00000000 --- a/backend/vendor/github.com/dgrijalva/jwt-go/token.go +++ /dev/null @@ -1,108 +0,0 @@ -package jwt - -import ( - "encoding/base64" - "encoding/json" - "strings" - "time" -) - -// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time). -// You can override it to use another time value. This is useful for testing or if your -// server uses a different time zone than your tokens. -var TimeFunc = time.Now - -// Parse methods use this callback function to supply -// the key for verification. The function receives the parsed, -// but unverified Token. This allows you to use properties in the -// Header of the token (such as `kid`) to identify which key to use. -type Keyfunc func(*Token) (interface{}, error) - -// A JWT Token. Different fields will be used depending on whether you're -// creating or parsing/verifying a token. -type Token struct { - Raw string // The raw token. Populated when you Parse a token - Method SigningMethod // The signing method used or to be used - Header map[string]interface{} // The first segment of the token - Claims Claims // The second segment of the token - Signature string // The third segment of the token. Populated when you Parse a token - Valid bool // Is the token valid? Populated when you Parse/Verify a token -} - -// Create a new Token. Takes a signing method -func New(method SigningMethod) *Token { - return NewWithClaims(method, MapClaims{}) -} - -func NewWithClaims(method SigningMethod, claims Claims) *Token { - return &Token{ - Header: map[string]interface{}{ - "typ": "JWT", - "alg": method.Alg(), - }, - Claims: claims, - Method: method, - } -} - -// Get the complete, signed token -func (t *Token) SignedString(key interface{}) (string, error) { - var sig, sstr string - var err error - if sstr, err = t.SigningString(); err != nil { - return "", err - } - if sig, err = t.Method.Sign(sstr, key); err != nil { - return "", err - } - return strings.Join([]string{sstr, sig}, "."), nil -} - -// Generate the signing string. This is the -// most expensive part of the whole deal. Unless you -// need this for something special, just go straight for -// the SignedString. -func (t *Token) SigningString() (string, error) { - var err error - parts := make([]string, 2) - for i, _ := range parts { - var jsonValue []byte - if i == 0 { - if jsonValue, err = json.Marshal(t.Header); err != nil { - return "", err - } - } else { - if jsonValue, err = json.Marshal(t.Claims); err != nil { - return "", err - } - } - - parts[i] = EncodeSegment(jsonValue) - } - return strings.Join(parts, "."), nil -} - -// Parse, validate, and return a token. -// keyFunc will receive the parsed token and should return the key for validating. -// If everything is kosher, err will be nil -func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - return new(Parser).Parse(tokenString, keyFunc) -} - -func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { - return new(Parser).ParseWithClaims(tokenString, claims, keyFunc) -} - -// Encode JWT specific base64url encoding with padding stripped -func EncodeSegment(seg []byte) string { - return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=") -} - -// Decode JWT specific base64url encoding with padding stripped -func DecodeSegment(seg string) ([]byte, error) { - if l := len(seg) % 4; l > 0 { - seg += strings.Repeat("=", 4-l) - } - - return base64.URLEncoding.DecodeString(seg) -} From 59cdfc3d040fe39d0d0e5335cbd8c3e10e99f037 Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 27 Dec 2018 20:16:06 -0600 Subject: [PATCH 02/21] rest/api passes --- backend/app/rest/api/admin.go | 7 +- backend/app/rest/api/admin_test.go | 4 +- backend/app/rest/api/rest.go | 13 +- backend/app/rest/api/rest_private.go | 7 +- backend/app/rest/api/rest_private_test.go | 10 +- backend/app/rest/api/rest_public_test.go | 3 +- backend/app/rest/api/rest_test.go | 57 +++--- backend/app/rest/proxy/avatar.go | 118 ------------ backend/app/rest/proxy/avatar_test.go | 172 ----------------- backend/app/rest/proxy/image.go | 4 +- backend/app/rest/user.go | 43 +++-- backend/app/store/avatar/bolt.go | 144 -------------- backend/app/store/avatar/bolt_test.go | 99 ---------- backend/app/store/avatar/gridfs.go | 125 ------------ backend/app/store/avatar/gridfs_test.go | 97 ---------- backend/app/store/avatar/localfs.go | 129 ------------- backend/app/store/avatar/localfs_test.go | 178 ------------------ backend/app/store/avatar/store.go | 110 ----------- backend/app/store/avatar/store_test.go | 109 ----------- backend/app/store/avatar/testdata/circles.jpg | Bin 23983 -> 0 bytes backend/app/store/avatar/testdata/circles.png | Bin 11392 -> 0 bytes 21 files changed, 83 insertions(+), 1346 deletions(-) delete mode 100644 backend/app/rest/proxy/avatar.go delete mode 100644 backend/app/rest/proxy/avatar_test.go delete mode 100644 backend/app/store/avatar/bolt.go delete mode 100644 backend/app/store/avatar/bolt_test.go delete mode 100644 backend/app/store/avatar/gridfs.go delete mode 100644 backend/app/store/avatar/gridfs_test.go delete mode 100644 backend/app/store/avatar/localfs.go delete mode 100644 backend/app/store/avatar/localfs_test.go delete mode 100644 backend/app/store/avatar/store.go delete mode 100644 backend/app/store/avatar/store_test.go delete mode 100644 backend/app/store/avatar/testdata/circles.jpg delete mode 100644 backend/app/store/avatar/testdata/circles.png diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index 8f6c3a2f..2cfe1479 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -14,7 +14,6 @@ import ( "github.com/go-pkgz/rest/cache" "github.com/umputun/remark/backend/app/rest" - "github.com/umputun/remark/backend/app/rest/proxy" "github.com/umputun/remark/backend/app/store" "github.com/umputun/remark/backend/app/store/service" ) @@ -25,7 +24,6 @@ type admin struct { cache cache.LoadingCache authenticator auth.Service readOnlyAge int - avatarProxy *proxy.Avatar migrator *Migrator } @@ -111,7 +109,7 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[INFO] delete all user comments by request for %s, site %s", claims.User.ID, claims.Audience) // deleteme set by deleteMeCtrl, this check just to make sure we not trying to delete with leaked token - if val, err := claims.User.BoolAttr("delete_me"); err != nil || !val { + if !claims.User.BoolAttr("delete_me") { rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("forbidden"), "can't use provided token") return } @@ -122,7 +120,8 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { } if claims.User.Picture != "" { - if err := a.avatarProxy.Store.Remove(path.Base(claims.User.Picture)); err != nil { + avatartStore := a.authenticator.AvatarProxy().Store + if err := avatartStore.Remove(path.Base(claims.User.Picture)); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user's avatar") return } diff --git a/backend/app/rest/api/admin_test.go b/backend/app/rest/api/admin_test.go index a0dd5cb4..9ebb64af 100644 --- a/backend/app/rest/api/admin_test.go +++ b/backend/app/rest/api/admin_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/dgrijalva/jwt-go" + jwt "github.com/dgrijalva/jwt-go" "github.com/go-pkgz/auth/token" R "github.com/go-pkgz/rest" @@ -615,7 +615,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { // try without deleteme flag badClaims2 := claims - badClaims2.User.SetBoolAttr("delete_me", true) + badClaims2.User.SetBoolAttr("delete_me", false) tkn, err = srv.Authenticator.TokenService().Token(badClaims2) assert.Nil(t, err) req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index c33b29e8..a4257685 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -41,7 +41,6 @@ type Rest struct { DataService *service.DataStore Authenticator auth.Service Cache cache.LoadingCache - AvatarProxy *proxy.Avatar ImageProxy *proxy.Image CommentFormatter *store.CommentFormatter Migrator *Migrator @@ -167,7 +166,6 @@ func (s *Rest) routes() chi.Router { cache: s.Cache, authenticator: s.Authenticator, readOnlyAge: s.ReadOnlyAge, - avatarProxy: s.AvatarProxy, } corsMiddleware := cors.New(cors.Options{ @@ -219,9 +217,15 @@ func (s *Rest) routes() chi.Router { // api routes router.Route("/api/v1", func(rapi chi.Router) { - rapi.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil))) + + rapi.Group(func(rava chi.Router) { + rava.Use(logger.New(logger.Flags(logger.None)).Handler, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(100, nil))) + rava.Mount("/avatar", avatarHandler) + }) + // open routes rapi.Group(func(ropen chi.Router) { + ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil))) ropen.Use(authMiddleware.Trace) ropen.Use(logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)).Handler) ropen.Get("/find", s.findCommentsCtrl) @@ -241,6 +245,7 @@ func (s *Rest) routes() chi.Router { // protected routes, require auth rapi.Group(func(rauth chi.Router) { + rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil))) rauth.Use(authMiddleware.Auth) rauth.Use(logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)).Handler) rauth.Post("/comment", s.createCommentCtrl) @@ -255,7 +260,7 @@ func (s *Rest) routes() chi.Router { }) }) - // respond to /robots.tx with the list of allowed paths + // respond to /robots.txt with the list of allowed paths router.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil))). Get("/robots.txt", func(w http.ResponseWriter, r *http.Request) { allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/img", "/avatar"} diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index d0492348..a0fd94b4 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -5,18 +5,19 @@ import ( "encoding/json" "errors" "fmt" - "github.com/go-pkgz/auth/token" "log" "net/http" "strings" "time" - "github.com/dgrijalva/jwt-go" + "github.com/go-pkgz/auth/token" + + jwt "github.com/dgrijalva/jwt-go" "github.com/go-chi/chi" "github.com/go-chi/render" R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" - "github.com/hashicorp/go-multierror" + multierror "github.com/hashicorp/go-multierror" "github.com/umputun/remark/backend/app/rest" "github.com/umputun/remark/backend/app/store" diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 84f4b543..8c4a4c7f 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -142,8 +142,8 @@ func TestRest_CreateAndGet(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "

test 123

\n\n

http://radio-t.com

\n", comment.Text) assert.Equal(t, "**test** *123*\n\n http://radio-t.com", comment.Orig) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", - Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "dbc7c999343f003f189f70aaf52cc04443f90790"}, + assert.Equal(t, store.User{Name: "developer one", ID: "dev", Admin: true, Blocked: false, + IP: "dbc7c999343f003f189f70aaf52cc04443f90790"}, comment.User) t.Logf("%+v", comment) } @@ -336,7 +336,7 @@ func TestRest_UserAllData(t *testing.T) { ungzBody, err := ioutil.ReadAll(ungzReader) assert.NoError(t, err) assert.True(t, strings.HasPrefix(string(ungzBody), - `{"info": {"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","admin":true}, "comments":[{`)) + `{"info": {"name":"developer one","id":"dev","picture":"","admin":true}, "comments":[{`)) assert.Equal(t, 3, strings.Count(string(ungzBody), `"text":`), "3 comments inside") t.Logf("%s", string(ungzBody)) @@ -347,7 +347,7 @@ func TestRest_UserAllData(t *testing.T) { err = json.Unmarshal(ungzBody, &parsed) assert.Nil(t, err) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "/api/v1/avatar/remark.image", Admin: true}, parsed.Info) + assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "", Admin: true}, parsed.Info) assert.Equal(t, 3, len(parsed.Comments)) req, err = http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) @@ -387,7 +387,7 @@ func TestRest_UserAllDataManyComments(t *testing.T) { ungzBody, err := ioutil.ReadAll(ungzReader) assert.NoError(t, err) assert.True(t, strings.HasPrefix(string(ungzBody), - `{"info": {"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","admin":true}, "comments":[{`)) + `{"info": {"name":"developer one","id":"dev","picture":"","admin":true}, "comments":[{`)) assert.Equal(t, 478, strings.Count(string(ungzBody), `"text":`), "478 comments inside") } diff --git a/backend/app/rest/api/rest_public_test.go b/backend/app/rest/api/rest_public_test.go index 042ba13f..9f40a7f3 100644 --- a/backend/app/rest/api/rest_public_test.go +++ b/backend/app/rest/api/rest_public_test.go @@ -289,8 +289,7 @@ func TestRest_UserInfo(t *testing.T) { user := store.User{} err := json.Unmarshal([]byte(body), &user) assert.Nil(t, err) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", - Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: ""}, user) + assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "", Admin: true, Blocked: false, IP: ""}, user) } func TestRest_Count(t *testing.T) { diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index 476b08f5..7ed3be28 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -12,8 +12,10 @@ import ( "testing" "time" - "github.com/coreos/bbolt" + bolt "github.com/coreos/bbolt" "github.com/go-pkgz/auth" + "github.com/go-pkgz/auth/avatar" + "github.com/go-pkgz/auth/token" R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/stretchr/testify/assert" @@ -23,7 +25,6 @@ import ( "github.com/umputun/remark/backend/app/rest/proxy" "github.com/umputun/remark/backend/app/store" adminstore "github.com/umputun/remark/backend/app/store/admin" - "github.com/umputun/remark/backend/app/store/avatar" "github.com/umputun/remark/backend/app/store/engine" "github.com/umputun/remark/backend/app/store/service" ) @@ -61,8 +62,7 @@ func TestRest_GetStarted(t *testing.T) { } func TestRest_Shutdown(t *testing.T) { - srv := Rest{Authenticator: auth.Service{}, AvatarProxy: &proxy.Avatar{Store: avatar.NewLocalFS("/tmp", 300), - RoutePath: "/api/v1/avatar"}, ImageProxy: &proxy.Image{}} + srv := Rest{Authenticator: auth.Service{}, ImageProxy: &proxy.Image{}} go func() { time.Sleep(100 * time.Millisecond) @@ -91,11 +91,11 @@ func TestRest_filterComments(t *testing.T) { func TestRest_RunStaticSSLMode(t *testing.T) { srv := Rest{ - Authenticator: auth.Service{}, - AvatarProxy: &proxy.Avatar{ - Store: avatar.NewLocalFS("/tmp", 300), - RoutePath: "/api/v1/avatar", - }, + Authenticator: *auth.NewService(auth.Opts{ + AvatarStore: avatar.NewLocalFS("/tmp"), + AvatarResizeLimit: 300, + }), + ImageProxy: &proxy.Image{}, SSLConfig: SSLConfig{ SSLMode: Static, @@ -144,11 +144,7 @@ func TestRest_RunStaticSSLMode(t *testing.T) { func TestRest_RunAutocertModeHTTPOnly(t *testing.T) { srv := Rest{ Authenticator: auth.Service{}, - AvatarProxy: &proxy.Avatar{ - Store: avatar.NewLocalFS("/tmp", 300), - RoutePath: "/api/v1/avatar", - }, - ImageProxy: &proxy.Image{}, + ImageProxy: &proxy.Image{}, SSLConfig: SSLConfig{ SSLMode: Auto, Port: 8443, @@ -193,17 +189,18 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { MaxVotes: service.UnlimitedVotes, } - //DevPasswd: "password", - // Providers: nil, - // KeyStore: adminStore, - // JWTService: auth.NewJWT(adminStore, false, time.Minute, time.Hour), srv = &Rest{ - DataService: dataStore, - Authenticator: *auth.NewService(auth.Opts{}), - Cache: &cache.Nop{}, - WebRoot: "/tmp", - RemarkURL: "https://demo.remark42.com", - AvatarProxy: &proxy.Avatar{Store: avatar.NewLocalFS("/tmp", 300), RoutePath: "/api/v1/avatar"}, + DataService: dataStore, + Authenticator: *auth.NewService(auth.Opts{ + DevPasswd: "password", + SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), + AvatarStore: avatar.NewLocalFS("/tmp"), + AvatarResizeLimit: 300, + }), + Cache: &cache.Nop{}, + WebRoot: "/tmp", + RemarkURL: "https://demo.remark42.com", + ImageProxy: &proxy.Image{}, ReadOnlyAge: 10, CommentFormatter: store.NewCommentFormatter(&proxy.Image{}), @@ -257,21 +254,21 @@ func post(t *testing.T, url string, body string) (*http.Response, error) { func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string { b, err := json.Marshal(c) - assert.Nil(t, err, "can't marshal comment %+v", c) + require.Nil(t, err, "can't marshal comment %+v", c) client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b)) - assert.Nil(t, err) + require.Nil(t, err) req.SetBasicAuth("dev", "password") resp, err := client.Do(req) - assert.Nil(t, err) - assert.Equal(t, http.StatusCreated, resp.StatusCode) + require.Nil(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) b, err = ioutil.ReadAll(resp.Body) - assert.Nil(t, err) + require.Nil(t, err) crResp := R.JSON{} err = json.Unmarshal(b, &crResp) - assert.Nil(t, err) + require.Nil(t, err) time.Sleep(time.Nanosecond * 10) return crResp["id"].(string) } diff --git a/backend/app/rest/proxy/avatar.go b/backend/app/rest/proxy/avatar.go deleted file mode 100644 index 7b71bcf1..00000000 --- a/backend/app/rest/proxy/avatar.go +++ /dev/null @@ -1,118 +0,0 @@ -package proxy - -import ( - "io" - "log" - "net/http" - "strconv" - "strings" - "time" - - "github.com/go-chi/chi" - "github.com/pkg/errors" - - "github.com/umputun/remark/backend/app/rest" - "github.com/umputun/remark/backend/app/store" - "github.com/umputun/remark/backend/app/store/avatar" -) - -// Avatar provides http handler for avatars from avatar.Store -// On user login auth will call Put and it will retrieve and save picture locally. -type Avatar struct { - Store avatar.Store - RoutePath string - RemarkURL string -} - -// Put stores retrieved avatar to avatar.Store. Gets image from user info. Returns proxied url -func (p *Avatar) Put(u store.User) (avatarURL string, err error) { - - // no picture for user, try default avatar - if u.Picture == "" { - return "", errors.Errorf("no picture for %s", u.ID) - } - - // load avatar from remote location - client := http.Client{Timeout: 10 * time.Second} - var resp *http.Response - err = retry(5, time.Second, func() error { - var e error - resp, e = client.Get(u.Picture) - return e - }) - if err != nil { - return "", errors.Wrap(err, "failed to fetch avatar from the orig") - } - - defer func() { - if e := resp.Body.Close(); e != nil { - log.Printf("[WARN] can't close response body, %s", e) - } - }() - - if resp.StatusCode != http.StatusOK { - return "", errors.Errorf("failed to get avatar from the orig, status %s", resp.Status) - } - - avatarID, err := p.Store.Put(u.ID, resp.Body) // put returns avatar base name, like 123456.image - if err != nil { - return "", err - } - - log.Printf("[DEBUG] saved avatar from %s to %s, user %q", u.Picture, avatarID, u.Name) - return p.RemarkURL + p.RoutePath + "/" + avatarID, nil -} - -// Routes returns auth routes for given provider -func (p *Avatar) Routes(middlewares ...func(http.Handler) http.Handler) (string, chi.Router) { - router := chi.NewRouter() - router.Use(middlewares...) - - // GET /123456789.image - router.Get("/{avatar}", func(w http.ResponseWriter, r *http.Request) { - - avatarID := chi.URLParam(r, "avatar") - - // enforce client-side caching - etag := `"` + p.Store.ID(avatarID) + `"` - w.Header().Set("Etag", etag) - w.Header().Set("Cache-Control", "max-age=604800") // 7 days - if match := r.Header.Get("If-None-Match"); match != "" { - if strings.Contains(match, etag) { - w.WriteHeader(http.StatusNotModified) - return - } - } - - avReader, size, err := p.Store.Get(avatarID) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load avatar") - return - } - - defer func() { - if e := avReader.Close(); e != nil { - log.Printf("[WARN] can't close avatar reader for %s, %s", avatarID, e) - } - }() - - w.Header().Set("Content-Type", "image/*") - w.Header().Set("Content-Length", strconv.Itoa(size)) - w.WriteHeader(http.StatusOK) - if _, err = io.Copy(w, avReader); err != nil { - log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err) - } - }) - - return p.RoutePath, router -} - -func retry(retries int, delay time.Duration, fn func() error) (err error) { - for i := 0; i < retries; i++ { - if err = fn(); err == nil { - return nil - } - time.Sleep(delay) - } - return errors.Wrap(err, "retry failed") -} diff --git a/backend/app/rest/proxy/avatar_test.go b/backend/app/rest/proxy/avatar_test.go deleted file mode 100644 index 53eba9a6..00000000 --- a/backend/app/rest/proxy/avatar_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package proxy - -import ( - "bytes" - "errors" - "fmt" - "io" - "log" - "net/http" - "net/http/httptest" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/umputun/remark/backend/app/store" - "github.com/umputun/remark/backend/app/store/avatar" -) - -func TestAvatar_Put(t *testing.T) { - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/pic.png" { - w.Header().Set("Content-Type", "image/*") - fmt.Fprint(w, "some picture bin data") - return - } - http.Error(w, "not found", http.StatusNotFound) - })) - defer ts.Close() - - p := Avatar{RoutePath: "/avatar", RemarkURL: "http://localhost:8080", Store: avatar.NewLocalFS("/tmp/avatars.test", 300)} - os.MkdirAll("/tmp/avatars.test", 0700) - defer os.RemoveAll("/tmp/avatars.test") - - u := store.User{ID: "user1", Name: "user1 name", Picture: ts.URL + "/pic.png"} - res, err := p.Put(u) - assert.NoError(t, err) - assert.Equal(t, "http://localhost:8080/avatar/b3daa77b4c04a9551b8781d03191fe098f325e67.image", res) - fi, err := os.Stat("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image") - assert.NoError(t, err) - assert.Equal(t, int64(21), fi.Size()) - - u.ID = "user2" - res, err = p.Put(u) - assert.NoError(t, err) - assert.Equal(t, "http://localhost:8080/avatar/a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image", res) - fi, err = os.Stat("/tmp/avatars.test/84/a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image") - assert.NoError(t, err) - assert.Equal(t, int64(21), fi.Size()) -} - -func TestAvatar_PutFailed(t *testing.T) { - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Print("request: ", r.URL.Path) - w.WriteHeader(http.StatusNotFound) - })) - defer ts.Close() - - p := Avatar{RoutePath: "/avatar", Store: avatar.NewLocalFS("/tmp/avatars.test", 300)} - - u := store.User{ID: "user1", Name: "user1 name"} - _, err := p.Put(u) - assert.EqualError(t, err, "no picture for user1") - - u = store.User{ID: "user1", Name: "user1 name", Picture: "http://127.0.0.1:22345/avater/pic"} - _, err = p.Put(u) - require.Error(t, err) - assert.Contains(t, err.Error(), "connect: connection refused") - - u = store.User{ID: "user1", Name: "user1 name", Picture: ts.URL + "/avatar/pic"} - _, err = p.Put(u) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get avatar from the orig") -} - -func TestAvatar_Routes(t *testing.T) { - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/pic.png" { - w.Header().Set("Content-Type", "image/*") - w.Header().Set("Custom-Header", "xyz") - fmt.Fprint(w, "some picture bin data") - return - } - http.Error(w, "not found", http.StatusNotFound) - })) - defer ts.Close() - - p := Avatar{RoutePath: "/avatar", Store: avatar.NewLocalFS("/tmp/avatars.test", 300)} - os.MkdirAll("/tmp/avatars.test", 0700) - defer os.RemoveAll("/tmp/avatars.test") - - u := store.User{ID: "user1", Name: "user1 name", Picture: ts.URL + "/pic.png"} - _, err := p.Put(u) - assert.NoError(t, err) - - // status 400 - req, err := http.NewRequest("GET", "/some_random_name.image", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - _, routes := p.Routes() - handler := http.Handler(routes) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusBadRequest, rr.Code) - - // status 200 - req, err = http.NewRequest("GET", "/b3daa77b4c04a9551b8781d03191fe098f325e67.image", nil) - if err != nil { - t.Fatal(err) - } - - rr = httptest.NewRecorder() - _, routes = p.Routes() - handler = http.Handler(routes) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - assert.Equal(t, []string{"image/*"}, rr.HeaderMap["Content-Type"]) - assert.Equal(t, []string{"21"}, rr.HeaderMap["Content-Length"]) - assert.Equal(t, []string(nil), rr.HeaderMap["Custom-Header"], "strip all custom headers") - assert.NotNil(t, rr.HeaderMap["Etag"]) - - bb := bytes.Buffer{} - sz, err := io.Copy(&bb, rr.Body) - assert.NoError(t, err) - assert.Equal(t, int64(21), sz) - assert.Equal(t, "some picture bin data", bb.String()) - - // status 304 - req, err = http.NewRequest("GET", "/some_random_name.image", nil) - if err != nil { - t.Fatal(err) - } - req.Header.Add("If-None-Match", `"a008de0a2ccb3308b5d99ffff66436e15538f701"`) // hash of `some_random_name.image` since the file doesn't exist - - rr = httptest.NewRecorder() - _, routes = p.Routes() - handler = http.Handler(routes) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusNotModified, rr.Code) - assert.Equal(t, []string{`"a008de0a2ccb3308b5d99ffff66436e15538f701"`}, rr.HeaderMap["Etag"]) -} - -func TestAvatar_Retry(t *testing.T) { - i := 0 - err := retry(5, time.Millisecond, func() error { - if i == 3 { - return nil - } - i++ - return errors.New("err") - }) - assert.Nil(t, err) - assert.Equal(t, 3, i) - - st := time.Now() - err = retry(5, time.Millisecond, func() error { - return errors.New("err") - }) - assert.NotNil(t, err) - assert.True(t, time.Since(st) >= time.Microsecond*5) -} diff --git a/backend/app/rest/proxy/image.go b/backend/app/rest/proxy/image.go index 284c8e13..4d33311d 100644 --- a/backend/app/rest/proxy/image.go +++ b/backend/app/rest/proxy/image.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "git.tkginternal.com/commons/pkg/repeater" + "github.com/PuerkitoBio/goquery" "github.com/go-chi/chi" "github.com/pkg/errors" @@ -52,7 +54,7 @@ func (p Image) Routes() chi.Router { client := http.Client{Timeout: 30 * time.Second} var resp *http.Response - err = retry(5, time.Second, func() error { + err = repeater.NewDefault(5, time.Second).Do(func() error { var e error resp, e = client.Get(string(src)) return e diff --git a/backend/app/rest/user.go b/backend/app/rest/user.go index 98d0092c..7727e137 100644 --- a/backend/app/rest/user.go +++ b/backend/app/rest/user.go @@ -1,15 +1,14 @@ package rest import ( - "context" - "errors" "net/http" + "github.com/go-pkgz/auth/token" + "github.com/pkg/errors" + "github.com/umputun/remark/backend/app/store" ) -type contextKey string - // MustGetUserInfo fails if can't extract user data from the request. // should be called from authed controllers only func MustGetUserInfo(r *http.Request) store.User { @@ -23,20 +22,36 @@ func MustGetUserInfo(r *http.Request) store.User { // GetUserInfo returns user from request context func GetUserInfo(r *http.Request) (user store.User, err error) { - ctx := r.Context() - if ctx == nil { - return store.User{}, errors.New("no info about user") - } - if u, ok := ctx.Value(contextKey("user")).(store.User); ok { - return u, nil + u, err := token.GetUserInfo(r) + if err != nil { + return store.User{}, errors.Wrap(err, "can't extract user info from the token") } - return store.User{}, errors.New("user can't be parsed") + return store.User{ + Name: u.Name, + ID: u.ID, + IP: u.IP, + Picture: u.Picture, + Admin: u.IsAdmin(), + Verified: u.BoolAttr("verified"), + Blocked: u.BoolAttr("blocked"), + }, nil + } // SetUserInfo sets user into request context func SetUserInfo(r *http.Request, user store.User) *http.Request { - ctx := r.Context() - ctx = context.WithValue(ctx, contextKey("user"), user) - return r.WithContext(ctx) + u := token.User{ + ID: user.ID, + Name: user.Name, + Picture: user.Picture, + IP: user.IP, + Attributes: map[string]interface{}{ + "blocked": user.Blocked, + "verified": user.Verified, + }, + } + u.SetAdmin(user.Admin) + + return token.SetUserInfo(r, u) } diff --git a/backend/app/store/avatar/bolt.go b/backend/app/store/avatar/bolt.go deleted file mode 100644 index 826a1ded..00000000 --- a/backend/app/store/avatar/bolt.go +++ /dev/null @@ -1,144 +0,0 @@ -package avatar - -import ( - "bytes" - "crypto/sha1" - "encoding/hex" - "io" - "io/ioutil" - "log" - - "github.com/coreos/bbolt" - "github.com/pkg/errors" - - "github.com/umputun/remark/backend/app/store" -) - -// BoltDB implements avatar store with bolt -// using separate db (file) with "avatars" bucket to keep image bin and "metas" bucket -// to keep sha1 of picture. avatarID (base file name) used as a key for both. -type BoltDB struct { - fileName string // full path to boltdb - resizeLimit int - db *bolt.DB -} - -const avatarsBktName = "avatars" -const metasBktName = "metas" - -// NewBoltDB makes bolt avatar store -func NewBoltDB(fileName string, options bolt.Options, resizeLimit int) (*BoltDB, error) { - db, err := bolt.Open(fileName, 0600, &options) - if err != nil { - return nil, errors.Wrapf(err, "failed to make boltdb for %s", fileName) - } - err = db.Update(func(tx *bolt.Tx) error { - if _, e := tx.CreateBucketIfNotExists([]byte(avatarsBktName)); e != nil { - return errors.Wrapf(e, "failed to create top level bucket %s", avatarsBktName) - } - _, e := tx.CreateBucketIfNotExists([]byte(metasBktName)) - return errors.Wrapf(e, "failed to create top metas bucket %s", metasBktName) - }) - if err != nil { - return nil, errors.Wrapf(err, "failed to initialize boltdb db %q buckets", fileName) - } - return &BoltDB{db: db, fileName: fileName, resizeLimit: resizeLimit}, nil -} - -// Put avatar to bolt, key by avatarID. Trying to resize image and lso calculates sha1 of the file for ID func -func (b *BoltDB) Put(userID string, reader io.Reader) (avatar string, err error) { - id := encodeID(userID) - - // Trying to resize avatar. - if reader = resize(reader, b.resizeLimit); reader == nil { - return "", errors.New("avatar resize reader is nil") - } - - avatarID := id + imgSfx - err = b.db.Update(func(tx *bolt.Tx) error { - buf := &bytes.Buffer{} - if _, err = io.Copy(buf, reader); err != nil { - return errors.Wrapf(err, "can't read avatar %s", avatarID) - } - - if err = tx.Bucket([]byte(avatarsBktName)).Put([]byte(avatarID), buf.Bytes()); err != nil { - return errors.Wrapf(err, "can't put to bucket with %s", avatarID) - } - // store sha1 of the image - return tx.Bucket([]byte(metasBktName)).Put([]byte(avatarID), []byte(b.sha1(buf.Bytes(), avatarID))) - }) - return avatarID, err -} - -// Get avatar reader for avatar id.image, avatarID used as the direct key -func (b *BoltDB) Get(avatarID string) (reader io.ReadCloser, size int, err error) { - buf := &bytes.Buffer{} - err = b.db.View(func(tx *bolt.Tx) error { - data := tx.Bucket([]byte(avatarsBktName)).Get([]byte(avatarID)) - if data == nil { - return errors.Errorf("can't load avatar %s", avatarID) - } - size, err = buf.Write(data) - return errors.Wrapf(err, "failed to write for %s", avatarID) - }) - return ioutil.NopCloser(buf), size, err -} - -// ID returns a fingerprint of the avatar content. -func (b *BoltDB) ID(avatarID string) (id string) { - data := []byte{} - err := b.db.View(func(tx *bolt.Tx) error { - if data = tx.Bucket([]byte(metasBktName)).Get([]byte(avatarID)); data == nil { - return errors.Errorf("can't load avatar's id for %s", avatarID) - } - return nil - }) - - if err != nil { // failed to get ID, use encoded avatarID - log.Printf("[DEBUG] can't get avatar info '%s', %s", avatarID, err) - return store.EncodeID(avatarID) - } - - return string(data) -} - -// Remove avatar from bolt -func (b *BoltDB) Remove(avatarID string) (err error) { - return b.db.Update(func(tx *bolt.Tx) error { - bkt := tx.Bucket([]byte(avatarsBktName)) - if bkt.Get([]byte(avatarID)) == nil { - return errors.Errorf("avatar key not found, %s", avatarID) - } - if err = tx.Bucket([]byte(avatarsBktName)).Delete([]byte(avatarID)); err != nil { - return errors.Wrapf(err, "can't delete avatar object %s", avatarID) - } - return errors.Wrapf(tx.Bucket([]byte(metasBktName)).Delete([]byte(avatarID)), - "can't delete meta object %s", avatarID) - }) -} - -// List all avatars (ids) from metas bucket -// note: id includes .image suffix -func (b *BoltDB) List() (ids []string, err error) { - err = b.db.View(func(tx *bolt.Tx) error { - return tx.Bucket([]byte(metasBktName)).ForEach(func(k, _ []byte) error { - ids = append(ids, string(k)) - return nil - }) - }) - return ids, errors.Wrap(err, "failed to list") -} - -// Close bolt store -func (b *BoltDB) Close() error { - return errors.Wrapf(b.db.Close(), "failed to close %s", b.fileName) -} - -func (b *BoltDB) sha1(data []byte, avatarID string) (id string) { - h := sha1.New() - if _, err := h.Write(data); err != nil { - log.Printf("[DEBUG] can't apply sha1 for content of '%s', %s", avatarID, err) - return store.EncodeID(avatarID) - } - return hex.EncodeToString(h.Sum(nil)) -} diff --git a/backend/app/store/avatar/bolt_test.go b/backend/app/store/avatar/bolt_test.go deleted file mode 100644 index b9ddac23..00000000 --- a/backend/app/store/avatar/bolt_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package avatar - -import ( - "io/ioutil" - "os" - "sort" - "strings" - "testing" - - "github.com/coreos/bbolt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var testDb = "/tmp/test-remark-avatars.db" - -func TestBoltDB_PutAndGet(t *testing.T) { - var b Store = prepBoltStore(t) - defer func() { - assert.Nil(t, b.Close()) - os.Remove(testDb) - }() - - avatar, err := b.Put("user1", strings.NewReader("some picture bin data")) - require.Nil(t, err) - assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", avatar) - - rd, size, err := b.Get(avatar) - require.Nil(t, err) - assert.Equal(t, 21, size) - data, err := ioutil.ReadAll(rd) - require.Nil(t, err) - assert.Equal(t, "some picture bin data", string(data)) - - _, _, err = b.Get("bad avatar") - assert.NotNil(t, err) - - // check IDs - assert.Equal(t, "fddae9ce556712a6ece0e8951a6e7a05c51ed6bf", b.ID(avatar)) - assert.Equal(t, "70c881d4a26984ddce795f6f71817c9cf4480e79", b.ID("aaaa"), "no data, encoded avatar id") - - l, err := b.List() - require.Nil(t, err) - assert.Equal(t, 1, len(l)) - assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", l[0]) -} - -func TestBoltDB_Remove(t *testing.T) { - b := prepBoltStore(t) - defer func() { - assert.Nil(t, b.Close()) - os.Remove(testDb) - }() - - assert.NotNil(t, b.Remove("no-such-thing.image")) - - avatar, err := b.Put("user1", strings.NewReader("some picture bin data")) - require.Nil(t, err) - assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", avatar) - assert.NoError(t, b.Remove("b3daa77b4c04a9551b8781d03191fe098f325e67.image"), "remove real one") - assert.NotNil(t, b.Remove("b3daa77b4c04a9551b8781d03191fe098f325e67.image"), "already removed") -} - -func TestBoltDB_List(t *testing.T) { - b := prepBoltStore(t) - defer func() { - assert.Nil(t, b.Close()) - os.Remove(testDb) - }() - - // write some avatars - _, err := b.Put("user1", strings.NewReader("some picture bin data 1")) - require.Nil(t, err) - _, err = b.Put("user2", strings.NewReader("some picture bin data 2")) - require.Nil(t, err) - _, err = b.Put("user3", strings.NewReader("some picture bin data 3")) - require.Nil(t, err) - - l, err := b.List() - assert.NoError(t, err) - assert.Equal(t, 3, len(l), "3 avatars listed") - sort.Strings(l) - assert.Equal(t, []string{"0b7f849446d3383546d15a480966084442cd2193.image", "a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image", "b3daa77b4c04a9551b8781d03191fe098f325e67.image"}, l) - - r, size, err := b.Get("0b7f849446d3383546d15a480966084442cd2193.image") - assert.Nil(t, err) - assert.Equal(t, 23, size) - data, err := ioutil.ReadAll(r) - assert.Nil(t, err) - assert.Equal(t, "some picture bin data 3", string(data)) -} - -// makes new boltdb, put two records -func prepBoltStore(t *testing.T) *BoltDB { - os.Remove(testDb) - boltStore, err := NewBoltDB(testDb, bolt.Options{}, 0) - require.Nil(t, err) - return boltStore -} diff --git a/backend/app/store/avatar/gridfs.go b/backend/app/store/avatar/gridfs.go deleted file mode 100644 index 6a20253e..00000000 --- a/backend/app/store/avatar/gridfs.go +++ /dev/null @@ -1,125 +0,0 @@ -package avatar - -import ( - "bytes" - "io" - "io/ioutil" - "log" - "time" - - "github.com/globalsign/mgo" - "github.com/go-pkgz/mongo" - "github.com/pkg/errors" - - "github.com/umputun/remark/backend/app/store" -) - -// NewGridFS makes gridfs (mongo) avatar store -func NewGridFS(conn *mongo.Connection, resizeLimit int) *GridFS { - return &GridFS{Connection: conn, resizeLimit: resizeLimit} -} - -// GridFS implements Store for GridFS -type GridFS struct { - Connection *mongo.Connection - resizeLimit int -} - -// Put avatar to gridfs object, try to resize -func (gf *GridFS) Put(userID string, reader io.Reader) (avatar string, err error) { - id := encodeID(userID) - err = gf.Connection.WithDB(func(dbase *mgo.Database) error { - fh, e := dbase.GridFS("fs").Create(id + imgSfx) - if e != nil { - return e - } - defer func() { - if err = fh.Close(); err != nil { - log.Printf("[WARN] can't close avatar file %v, %s", fh, err) - } - }() - - // Trying to resize avatar. - if reader = resize(reader, gf.resizeLimit); reader == nil { - return errors.New("avatar resize reader is nil") - } - _, e = io.Copy(fh, reader) - return e - }) - return id + imgSfx, err -} - -// Get avatar reader for avatar id.image -func (gf *GridFS) Get(avatar string) (reader io.ReadCloser, size int, err error) { - buf := &bytes.Buffer{} - err = gf.Connection.WithDB(func(dbase *mgo.Database) error { - fh, e := dbase.GridFS("fs").Open(avatar) - if e != nil { - return errors.Wrapf(e, "can't load avatar %s", avatar) - } - if _, e = io.Copy(buf, fh); e != nil { - return errors.Wrapf(e, "can't copy avatar %s", avatar) - } - size = int(fh.Size()) - return fh.Close() - }) - return ioutil.NopCloser(buf), size, err -} - -// ID returns a fingerprint of the avatar content. Uses MD5 because gridfs provides it directly -func (gf *GridFS) ID(avatar string) (id string) { - err := gf.Connection.WithDB(func(dbase *mgo.Database) error { - fh, e := dbase.GridFS("fs").Open(avatar) - if e != nil { - return errors.Wrapf(e, "can't open avatar %s", avatar) - } - id = fh.MD5() - return errors.Wrapf(fh.Close(), "can't close avatar") - }) - if err != nil { - log.Printf("[DEBUG] can't get file info '%s', %s", avatar, err) - return store.EncodeID(avatar) - } - return id -} - -// Remove avatar from gridfs -func (gf *GridFS) Remove(avatar string) error { - return gf.Connection.WithDB(func(dbase *mgo.Database) error { - fh, e := dbase.GridFS("fs").Open(avatar) - if e != nil { - return errors.Wrapf(e, "can't get avatar %s", avatar) - } - if e = fh.Close(); e != nil { - log.Printf("[WARN] can't close avatar %s, %s", avatar, e) - } - return dbase.GridFS("fs").Remove(avatar) - }) -} - -// List all avatars (ids) on gfs -// note: id includes .image suffix -func (gf *GridFS) List() (ids []string, err error) { - - type gfsFile struct { - UploadDate time.Time `bson:"uploadDate"` - Length int64 `bson:",minsize"` - MD5 string - Filename string `bson:",omitempty"` - } - - files := []gfsFile{} - err = gf.Connection.WithDB(func(dbase *mgo.Database) error { - return dbase.GridFS("fs").Find(nil).All(&files) - }) - - for _, f := range files { - ids = append(ids, f.Filename) - } - return ids, errors.Wrap(err, "can't list avatars") -} - -// Close gridfs does nothing but satisfies interface -func (gf *GridFS) Close() error { - return nil -} diff --git a/backend/app/store/avatar/gridfs_test.go b/backend/app/store/avatar/gridfs_test.go deleted file mode 100644 index 4ca28741..00000000 --- a/backend/app/store/avatar/gridfs_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package avatar - -import ( - "io/ioutil" - "sort" - "strings" - "testing" - - "github.com/globalsign/mgo" - "github.com/go-pkgz/mongo" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGridFS_PutAndGet(t *testing.T) { - p, skip := prepGFStore(t) - if skip { - return - } - avatar, err := p.Put("user1", strings.NewReader("some picture bin data")) - require.Nil(t, err) - assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", avatar) - - rd, size, err := p.Get(avatar) - require.Nil(t, err) - assert.Equal(t, 21, size) - data, err := ioutil.ReadAll(rd) - require.Nil(t, err) - assert.Equal(t, "some picture bin data", string(data)) - - _, _, err = p.Get("bad avatar") - assert.NotNil(t, err) - - assert.Equal(t, "8ce5568f7f9a1c9da5b897bc8642e397", p.ID(avatar)) - assert.Equal(t, "70c881d4a26984ddce795f6f71817c9cf4480e79", p.ID("aaaa"), "no data, encode avatar id") - - l, err := p.List() - require.Nil(t, err) - assert.Equal(t, 1, len(l)) - assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", l[0]) -} - -func TestGridFS_Remove(t *testing.T) { - p, skip := prepGFStore(t) - if skip { - return - } - - assert.NotNil(t, p.Remove("no-such-thing.image")) - - avatar, err := p.Put("user1", strings.NewReader("some picture bin data")) - require.Nil(t, err) - assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", avatar) - assert.NoError(t, p.Remove("b3daa77b4c04a9551b8781d03191fe098f325e67.image"), "remove real one") - assert.NotNil(t, p.Remove("b3daa77b4c04a9551b8781d03191fe098f325e67.image"), "already removed") -} - -func TestGridFS_List(t *testing.T) { - p, skip := prepGFStore(t) - if skip { - return - } - // write some avatars - _, err := p.Put("user1", strings.NewReader("some picture bin data 1")) - require.Nil(t, err) - _, err = p.Put("user2", strings.NewReader("some picture bin data 2")) - require.Nil(t, err) - _, err = p.Put("user3", strings.NewReader("some picture bin data 3")) - require.Nil(t, err) - - l, err := p.List() - assert.NoError(t, err) - assert.Equal(t, 3, len(l), "3 avatars listed") - sort.Strings(l) - assert.Equal(t, []string{"0b7f849446d3383546d15a480966084442cd2193.image", "a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image", "b3daa77b4c04a9551b8781d03191fe098f325e67.image"}, l) - - r, size, err := p.Get("0b7f849446d3383546d15a480966084442cd2193.image") - assert.Nil(t, err) - assert.Equal(t, 23, size) - data, err := ioutil.ReadAll(r) - assert.Nil(t, err) - assert.Equal(t, "some picture bin data 3", string(data)) -} - -func prepGFStore(t *testing.T) (Store, bool) { - conn, err := mongo.MakeTestConnection(t) - if err != nil { - return nil, true - } - _ = conn.WithCustomCollection("fs.chunks", func(coll *mgo.Collection) error { - return coll.DropCollection() - }) - _ = conn.WithCustomCollection("fs.files", func(coll *mgo.Collection) error { - return coll.DropCollection() - }) - return NewGridFS(conn, 0), false -} diff --git a/backend/app/store/avatar/localfs.go b/backend/app/store/avatar/localfs.go deleted file mode 100644 index 9b34ac3d..00000000 --- a/backend/app/store/avatar/localfs.go +++ /dev/null @@ -1,129 +0,0 @@ -package avatar - -import ( - "fmt" - "hash/crc64" - "io" - "log" - "os" - "path" - "path/filepath" - "strconv" - "strings" - "sync" - - "github.com/pkg/errors" - - "github.com/umputun/remark/backend/app/store" -) - -// LocalFS implements Store for local file system -type LocalFS struct { - storePath string - resizeLimit int - ctcTable *crc64.Table - once sync.Once -} - -// NewLocalFS makes file-system avatar store -func NewLocalFS(storePath string, resizeLimit int) *LocalFS { - return &LocalFS{storePath: storePath, resizeLimit: resizeLimit} -} - -// Put avatar for userID to file and return avatar's file name (base), like 12345678.image -// userID can be avatarID as well, in this case encoding just strip .image prefix -func (fs *LocalFS) Put(userID string, reader io.Reader) (avatar string, err error) { - id := encodeID(userID) - location := fs.location(id) // location adds partition to path - - if _, err = os.Stat(location); os.IsNotExist(err) { - if e := os.Mkdir(location, 0700); e != nil { - return "", errors.Wrapf(e, "failed to mkdir avatar location %s", location) - } - } - - avFile := path.Join(location, id+imgSfx) - fh, err := os.Create(avFile) - if err != nil { - return "", errors.Wrapf(err, "can't create file %s", avFile) - } - defer func() { - if e := fh.Close(); e != nil { - log.Printf("[WARN] can't close avatar file %s, %s", avFile, e) - } - }() - - // Trying to resize avatar. - if reader = resize(reader, fs.resizeLimit); reader == nil { - return "", errors.New("avatar resize reader is nil") - } - - if _, err = io.Copy(fh, reader); err != nil { - return "", errors.Wrapf(err, "can't save file %s", avFile) - } - return id + imgSfx, nil -} - -// Get avatar reader for avatar id.image -func (fs *LocalFS) Get(avatar string) (reader io.ReadCloser, size int, err error) { - location := fs.location(strings.TrimSuffix(avatar, imgSfx)) - avFile := path.Join(location, avatar) - fh, err := os.Open(avFile) - if err != nil { - return nil, 0, errors.Wrapf(err, "can't load avatar %s, id", avatar) - } - if fi, e := fh.Stat(); e == nil { - size = int(fi.Size()) - } - return fh, size, nil -} - -// ID returns a fingerprint of the avatar content. -func (fs *LocalFS) ID(avatar string) (id string) { - location := fs.location(strings.TrimSuffix(avatar, imgSfx)) - avFile := path.Join(location, avatar) - fi, err := os.Stat(avFile) - if err != nil { - log.Printf("[DEBUG] can't get file info '%s', %s", avFile, err) - return store.EncodeID(avatar) - } - return store.EncodeID(avatar + strconv.FormatInt(fi.ModTime().Unix(), 10)) -} - -// Remove avatar file -func (fs *LocalFS) Remove(avatar string) error { - location := fs.location(strings.TrimSuffix(avatar, imgSfx)) - avFile := path.Join(location, avatar) - return os.Remove(avFile) -} - -// List all avatars (ids) on local file system -// note: id includes .image suffix -func (fs *LocalFS) List() (ids []string, err error) { - err = filepath.Walk(fs.storePath, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && strings.HasSuffix(info.Name(), imgSfx) { - ids = append(ids, info.Name()) - } - return nil - }) - return ids, errors.Wrap(err, "can't list avatars") -} - -// Close gridfs does nothing but satisfies interface -func (fs *LocalFS) Close() error { - return nil -} - -// get location (directory) for user id by adding partition to final path in order to keep files -// in different subdirectories and avoid too many files in a single place. -// the end result is a full path like this - /tmp/avatars.test/92 -func (fs *LocalFS) location(id string) string { - fs.once.Do(func() { fs.ctcTable = crc64.MakeTable(crc64.ECMA) }) - checksum64 := crc64.Checksum([]byte(id), fs.ctcTable) - partition := checksum64 % 100 - return path.Join(fs.storePath, fmt.Sprintf("%02d", partition)) -} diff --git a/backend/app/store/avatar/localfs_test.go b/backend/app/store/avatar/localfs_test.go deleted file mode 100644 index 922c420f..00000000 --- a/backend/app/store/avatar/localfs_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package avatar - -import ( - "io/ioutil" - "os" - "sort" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAvatarStoreFS_Put(t *testing.T) { - p := NewLocalFS("/tmp/avatars.test", 300) - err := os.MkdirAll("/tmp/avatars.test", 0700) - require.NoError(t, err) - defer os.RemoveAll("/tmp/avatars.test") - - avatar, err := p.Put("user1", nil) - assert.Equal(t, "", avatar) - assert.EqualError(t, err, "avatar resize reader is nil") - - avatar, err = p.Put("user1", strings.NewReader("some picture bin data")) - require.Nil(t, err) - assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", avatar) - fi, err := os.Stat("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image") - assert.NoError(t, err) - assert.Equal(t, int64(21), fi.Size()) - - avatar, err = p.Put("user2", strings.NewReader("some picture bin data 123")) - require.Nil(t, err) - assert.Equal(t, "a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image", avatar) - fi, err = os.Stat("/tmp/avatars.test/84/a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image") - assert.NoError(t, err) - assert.Equal(t, int64(25), fi.Size()) - - // with encoded id - avatar, err = p.Put("f1881c06eec96db9901c7bbfe41c42a3f08e9cb8.image", strings.NewReader("some picture bin data 123")) - require.Nil(t, err) - assert.Equal(t, "f1881c06eec96db9901c7bbfe41c42a3f08e9cb8.image", avatar) - fi, err = os.Stat("/tmp/avatars.test/56/f1881c06eec96db9901c7bbfe41c42a3f08e9cb8.image") - assert.NoError(t, err) - assert.Equal(t, int64(25), fi.Size()) - - // with resize - file, e := os.Open("testdata/circles.png") - require.Nil(t, e) - avatar, err = p.Put("user3", file) - require.Nil(t, err) - assert.Equal(t, "0b7f849446d3383546d15a480966084442cd2193.image", avatar) - fi, err = os.Stat("/tmp/avatars.test/60/0b7f849446d3383546d15a480966084442cd2193.image") - assert.NoError(t, err) - assert.Equal(t, int64(6986), fi.Size()) - - p = NewLocalFS("/dev/null", 300) - _, err = p.Put("user1", strings.NewReader("some picture bin data")) - assert.EqualError(t, err, "can't create file /dev/null/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image: open /dev/null/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image: not a directory") -} - -func TestAvatarStoreFS_Get(t *testing.T) { - p := NewLocalFS("/tmp/avatars.test", 300) - err := os.MkdirAll("/tmp/avatars.test/30", 0700) - require.NoError(t, err) - defer os.RemoveAll("/tmp/avatars.test") - - // file not exists - r, size, err := p.Get("some_random_name.image") - // nil, 0, errors.Wrapf(err, "can't load avatar %s, id") - assert.Nil(t, r) - assert.Equal(t, 0, size) - assert.EqualError(t, err, "can't load avatar some_random_name.image, id: open /tmp/avatars.test/91/some_random_name.image: no such file or directory") - // file exists - err = ioutil.WriteFile("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image", []byte("something"), 0666) - assert.Nil(t, err) - r, size, err = p.Get("b3daa77b4c04a9551b8781d03191fe098f325e67.image") - - assert.Nil(t, err) - assert.Equal(t, 9, size) - data, err := ioutil.ReadAll(r) - assert.Nil(t, err) - assert.Equal(t, "something", string(data)) -} - -func TestAvatarStoreFS_Location(t *testing.T) { - p := NewLocalFS("/tmp/avatars.test", 300) - - tbl := []struct { - id string - res string - }{ - {"abc", "/tmp/avatars.test/35"}, - {"xyz", "/tmp/avatars.test/69"}, - {"blah blah", "/tmp/avatars.test/29"}, - {"f1881c06eec96db9901c7bbfe41c42a3f08e9cb8", "/tmp/avatars.test/56"}, - } - - for i, tt := range tbl { - assert.Equal(t, tt.res, p.location(tt.id), "test #%d", i) - } -} - -func TestAvatarStoreFS_ID(t *testing.T) { - p := NewLocalFS("/tmp/avatars.test", 300) - err := os.MkdirAll("/tmp/avatars.test/30", 0700) - require.NoError(t, err) - defer os.RemoveAll("/tmp/avatars.test") - - // file not exists - id := p.ID("some_random_name.image") - assert.Equal(t, "a008de0a2ccb3308b5d99ffff66436e15538f701", id) // store.EncodeID("some_random_name.image") - // file exists - err = ioutil.WriteFile("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image", []byte("something"), 0666) - require.NoError(t, err) - touch := time.Date(2017, 7, 14, 2, 40, 0, 0, time.UTC) // 1500000000 - err = os.Chtimes("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image", touch, touch) - require.NoError(t, err) - id = p.ID("b3daa77b4c04a9551b8781d03191fe098f325e67.image") - assert.Equal(t, "325d5b451f32c2f8e7f30a9fd65bff6a42954d9a", id) // store.EncodeID("b3daa77b4c04a9551b8781d03191fe098f325e67.image1500000000") -} - -func TestAvatarStoreFS_Remove(t *testing.T) { - p := NewLocalFS("/tmp/avatars.test", 300) - err := os.MkdirAll("/tmp/avatars.test/30", 0700) - require.NoError(t, err) - defer os.RemoveAll("/tmp/avatars.test") - - assert.NotNil(t, p.Remove("no-such-avatar"), "remove non-existing avatar") - err = ioutil.WriteFile("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image", []byte("something"), 0666) - require.NoError(t, err) - - assert.NoError(t, p.Remove("b3daa77b4c04a9551b8781d03191fe098f325e67.image")) - _, err = os.Stat("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image") - assert.NotNil(t, err, "removed for real") - t.Log(err) -} - -func TestAvatarStoreFS_List(t *testing.T) { - p := NewLocalFS("/tmp/avatars.test", 300) - err := os.MkdirAll("/tmp/avatars.test", 0700) - require.NoError(t, err) - defer os.RemoveAll("/tmp/avatars.test") - - // write some avatars - _, err = p.Put("user1", strings.NewReader("some picture bin data 1")) - require.Nil(t, err) - _, err = p.Put("user2", strings.NewReader("some picture bin data 2")) - require.Nil(t, err) - _, err = p.Put("user3", strings.NewReader("some picture bin data 3")) - require.Nil(t, err) - - l, err := p.List() - assert.NoError(t, err) - assert.Equal(t, 3, len(l), "3 avatars listed") - sort.Strings(l) - assert.Equal(t, []string{"0b7f849446d3383546d15a480966084442cd2193.image", "a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image", "b3daa77b4c04a9551b8781d03191fe098f325e67.image"}, l) - - r, size, err := p.Get("0b7f849446d3383546d15a480966084442cd2193.image") - assert.Nil(t, err) - assert.Equal(t, 23, size) - data, err := ioutil.ReadAll(r) - assert.Nil(t, err) - assert.Equal(t, "some picture bin data 3", string(data)) -} - -func BenchmarkAvatarStoreFS_ID(b *testing.B) { - p := NewLocalFS("/tmp/avatars.test", 300) - os.MkdirAll("/tmp/avatars.test/30", 0700) - defer os.RemoveAll("/tmp/avatars.test") - err := ioutil.WriteFile("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image", []byte("something"), 0666) - require.NoError(b, err) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - p.ID("b3daa77b4c04a9551b8781d03191fe098f325e67.image") - } -} diff --git a/backend/app/store/avatar/store.go b/backend/app/store/avatar/store.go deleted file mode 100644 index eb9a3aed..00000000 --- a/backend/app/store/avatar/store.go +++ /dev/null @@ -1,110 +0,0 @@ -// Package avatar defines store interface and implements local (fs), gridfs (mongo) and boltdb stores. -// -package avatar - -//go:generate sh -c "mockery -inpkg -name Store -print > /tmp/mock.tmp && mv /tmp/mock.tmp store_mock.go" - -import ( - "bytes" - "image" - "strings" - - // Initializing packages for supporting GIF and JPEG formats. - _ "image/gif" - _ "image/jpeg" - "image/png" - "io" - "log" - "regexp" - - "github.com/umputun/remark/backend/app/store" - "golang.org/x/image/draw" -) - -// imgSfx for avatars -const imgSfx = ".image" - -var reValidAvatarID = regexp.MustCompile(`^[a-fA-F0-9]{40}\.image$`) - -// Store defines interface to store and and load avatars -type Store interface { - Put(userID string, reader io.Reader) (avatarID string, err error) // save avatar data from the reader and return base name - Get(avatarID string) (reader io.ReadCloser, size int, err error) // load avatar via reader - ID(avatarID string) (id string) // unique id of stored avatar's data - Remove(avatarID string) error // remove avatar data - List() (ids []string, err error) // list all avatar ids - Close() error // close store -} - -// Migrate avatars between stores -func Migrate(dst Store, src Store) (int, error) { - ids, err := src.List() - if err != nil { - return 0, err - } - for _, id := range ids { - srcReader, _, err := src.Get(id) - if err != nil { - log.Printf("[WARN] can't get reader for avatar %s", id) - continue - } - if _, err = dst.Put(id, srcReader); err != nil { - log.Printf("[WARN] can't put avatar %s", id) - } - if err = srcReader.Close(); err != nil { - log.Printf("[WARN] failed to close avatar %s", id) - } - } - return len(ids), nil -} - -// resize an image of supported format (PNG, JPG, GIF) to the size of "limit" px of the biggest side -// (width or height) preserving aspect ratio. -// Returns original reader if resizing is not needed or failed. -func resize(reader io.Reader, limit int) io.Reader { - if reader == nil { - log.Print("[WARN] avatar resize(): reader is nil") - return nil - } - if limit <= 0 { - log.Print("[DEBUG] avatar resize(): limit should be greater than 0") - return reader - } - - var teeBuf bytes.Buffer - tee := io.TeeReader(reader, &teeBuf) - src, _, err := image.Decode(tee) - if err != nil { - log.Printf("[WARN] avatar resize(): can't decode avatar image, %s", err) - return &teeBuf - } - - bounds := src.Bounds() - w, h := bounds.Dx(), bounds.Dy() - if w <= limit && h <= limit || w <= 0 || h <= 0 { - log.Print("[DEBUG] resizing image is smaller that the limit or has 0 size") - return &teeBuf - } - newW, newH := w*limit/h, limit - if w > h { - newW, newH = limit, h*limit/w - } - m := image.NewRGBA(image.Rect(0, 0, newW, newH)) - // Slower than `draw.ApproxBiLinear.Scale()` but better quality. - draw.BiLinear.Scale(m, m.Bounds(), src, src.Bounds(), draw.Src, nil) - - var out bytes.Buffer - if err = png.Encode(&out, m); err != nil { - log.Printf("[WARN] avatar resize(): can't encode resized avatar to PNG, %s", err) - return &teeBuf - } - return &out -} - -// encodeID converts string to encoded id unless already encoded and valid avatar id (with .image) passed -func encodeID(val string) string { - if reValidAvatarID.MatchString(val) { - return strings.TrimSuffix(val, imgSfx) // already encoded, strip .image - } - return store.EncodeID(val) -} diff --git a/backend/app/store/avatar/store_test.go b/backend/app/store/avatar/store_test.go deleted file mode 100644 index 377af11a..00000000 --- a/backend/app/store/avatar/store_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package avatar - -import ( - "bytes" - "image" - "io" - "io/ioutil" - "os" - "sort" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAvatarStore_resize(t *testing.T) { - checkC := func(t *testing.T, r io.Reader, cExp []byte) { - content, err := ioutil.ReadAll(r) - require.NoError(t, err) - assert.Equal(t, cExp, content) - } - - // Reader is nil. - resizedR := resize(nil, 100) - // assert.EqualError(t, err, "limit should be greater than 0") - assert.Nil(t, resizedR) - - // Negative limit error. - resizedR = resize(strings.NewReader("some picture bin data"), -1) - require.NotNil(t, resizedR) - checkC(t, resizedR, []byte("some picture bin data")) - - // Decode error. - resizedR = resize(strings.NewReader("invalid image content"), 100) - assert.NotNil(t, resizedR) - checkC(t, resizedR, []byte("invalid image content")) - - cases := []struct { - file string - wr, hr int - }{ - {"testdata/circles.png", 400, 300}, // full size: 800x600 px - {"testdata/circles.jpg", 300, 400}, // full size: 600x800 px - } - - for _, c := range cases { - img, err := ioutil.ReadFile(c.file) - require.Nil(t, err, "can't open test file %s", c.file) - - // No need for resize, avatar dimensions are smaller than resize limit. - resizedR = resize(bytes.NewReader(img), 800) - assert.NotNilf(t, resizedR, "file %s", c.file) - checkC(t, resizedR, img) - - // Resizing to half of width. Check resizedR avatar format PNG. - resizedR = resize(bytes.NewReader(img), 400) - assert.NotNilf(t, resizedR, "file %s", c.file) - - imgRz, format, err := image.Decode(resizedR) - assert.Nilf(t, err, "file %s", c.file) - assert.Equalf(t, "png", format, "file %s", c.file) - bounds := imgRz.Bounds() - assert.Equalf(t, c.wr, bounds.Dx(), "file %s", c.file) - assert.Equalf(t, c.hr, bounds.Dy(), "file %s", c.file) - } -} - -func TestAvatarStore_Migrate(t *testing.T) { - // prep localfs - plocal := NewLocalFS("/tmp/avatars.test", 300) - err := os.MkdirAll("/tmp/avatars.test", 0700) - require.NoError(t, err) - defer os.RemoveAll("/tmp/avatars.test") - - // prep gridfs - pgfs, skip := prepGFStore(t) - if skip { - return - } - - // write to localfs - _, err = plocal.Put("user1", strings.NewReader("some picture bin data 1")) - require.Nil(t, err) - _, err = plocal.Put("user2", strings.NewReader("some picture bin data 2")) - require.Nil(t, err) - _, err = plocal.Put("user3", strings.NewReader("some picture bin data 3")) - require.Nil(t, err) - - // migrate and check reported count - count, err := Migrate(pgfs, plocal) - require.NoError(t, err) - assert.Equal(t, 3, count, "all 3 recs migrated") - - // list avatars - l, err := pgfs.List() - assert.NoError(t, err) - assert.Equal(t, 3, len(l), "3 avatars listed in destination store") - sort.Strings(l) - assert.Equal(t, []string{"0b7f849446d3383546d15a480966084442cd2193.image", "a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image", "b3daa77b4c04a9551b8781d03191fe098f325e67.image"}, l) - - // try to read one of migrated avatars - r, size, err := pgfs.Get("0b7f849446d3383546d15a480966084442cd2193.image") - assert.Nil(t, err) - assert.Equal(t, 23, size) - data, err := ioutil.ReadAll(r) - assert.Nil(t, err) - assert.Equal(t, "some picture bin data 3", string(data)) -} diff --git a/backend/app/store/avatar/testdata/circles.jpg b/backend/app/store/avatar/testdata/circles.jpg deleted file mode 100644 index 2c7048c3a4b524ba2009a6820bd7fced0af45378..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23983 zcmd5^2|Uzm_a9VBB8sFkqR3v^g}LQM2-&xo5JHh;H>MQI8bbE$S;xLJWzCYEv4pH+ zUuF!3G4r49d+&XPx^>^~=YPlNWA^8F&Uw!Hp6@x&^9-Sf@Ck5OPFhwPKtcilTqFJg z2*Uuas}5!+0Dyu5fD-@!><5sN&;rPak4T7r08cZ(p5Gn=0Bpp20D$Z@$$#H{P5Q^X zq*|}Z{&-Br{P}}~Ie^HGJ5~>^Z0=ayXXfWU4-mN`tFY(uXvEuZkI(-0@NnlTpI(1J z&Yr192VTvtU2!G61<>p#Vm1~d-sv= zKR|Ji_<{VxMA6B}NcWKK-TV16BoN~JfIT#OX-{9cyzhvz0Xd8H(Tk4*V)wIN$$m$t z(zbYp-|&Ik0SfwK496MIvaz4z;1m!P5*85^llt|lw9GYGIaM`v4NWa=9i!XECZ>1H z%xxap+SxleIz4&n?&10DxmRG&%ixgES7G6C@d=4Z$!}6pb8_?Y3kr*hODe0XYijG> z*Eh6xbar(kdV2eYM@GlSCyB^s7tshLq^*&9AMIsjas%rlEEgZ|KYAq~Hv8QHR(_R5I>QHT6!d2VhR-g2 zR_!;6yT85oWo zrKe{+br#5Yg7qXlJu^2mD;qlpC&zIh&v~A6=g*$wI7c83LPAbXPO+cj2nEHFbBy$i z=YIG`7$CZ(JfROjNk&3+CNdfT2(X^?hyrj9@S61h@s5np`tJ~+3 zo&OFoy8azvbpJcVK>Rzz_(ySL|CsI7kF!Uq5|)#KoLEdn-r30Z9xr5^53_o)8W4nk z$A`<+ts>WVmRT0RDMK^<%H9kC=5t0B%PG-My^#bjt+F7^!3NI3K8VqIZUL7gsowj5lB-lR~+3KIf=8i|2vglO-d zQ654`l3f^eQ7{>O;?FXVfsVm}s?o(IMa2-r5!urOfZ6jJr}N1a=B*l%m*{+8UZH}H z#USAaoUNn3r#IjpRDZ?VS%;x6ttZN2*5P@<&FPXXnM^mAZ;xyw8$p$pjOQxoCl5Z` zRB9M{g^w#+?==>kIdmKo24yt5zv6eAGhicT5U!3E`@p4>+xc?8aX+u9@0t}JKCG|c z!XpQy?I>Q~ry27JEoiQi2M?Yk06yV-osbe)yXRJF!aoD^`}p^&Xni7yjP5*ZEhoph zVC0K%QTJ^%^JI^|4qX8+!N3PY!GjsA&@k+vW362|B^=3zm%$A+7Qb&~m1h;t)b1I} zWm0gaar4RnUCPj~H9;yD49?99y+M9S6aY2XjL*=#XU%7W#S8dEf(7)#Iw$ox9lYM9 z&@L?>fzob5pfTNLk%~7H7gX9-BsW+`Ioi!VK!g5hTmAA)d(0)RL|G%5=fMFUQdCbh zea{t@O0=}6sJq|c=&<+h&3Hb==)Tia$qD~NS|UBJ$0BWi>rMQb=truWK{@;>Dgtpx z;oJFl(F?Q|D08Y--3#)+STC>_udKkJ2i(*`#qoUaaiPzO!jp{HrKsj)At(%Lw!*4$ zU-^;IUAfdDe`<@sDVsem zYs6;2a@_?P-fAL-pz&B@Xv(cFid>a-mc@Ysu%M&XC$l>oZt`#T4G;kQ1OV^A>YSxh zIqPuc*LY?3;zZWEUiZn60vj|ec|&Tj$oiG~Grt&G$#~D)sd2#!3A0Aq{tu3Li!rC` zD%{y=@%yBY?|H~lVVe{cw!tG7khWLkQgW*6ubsZz?F}OQ1IcI$42kqlTFY7Xx}a8#v{f27|TR z!&AsjqB_tLGFL0~WR$$dZt+u|KNV5#Qtrg-^O1TW$EQ-%kCq|0bimF6I+LBK#q|1% zG)6-B>8wNq%rs$mi%N&Ij=AgYkeO&9v{L~RX7FKyb_a}6x)L%yI3P` z!kcKn5&`9nd8}U`{%M;VZjV21vqH9x8*ZPVjlz_!Q3QZoZ8^wHp{jUtyeRoejcR5^ zqtZBHQR3b;W}5ecD9?qdq)g8n=eG{}vzZ7boJL{nPH3P#>OD}k4wJE%J&{2-;ef@n zPGfByVdEAH9T@SFiZ?ihznH+!s$zKdS%GA7Uc8X?A$5%ej)S?pJkM(L@voNo!MiW6 z(=p*vT)O>H0yAXYTQlZQPxw?eb+mWeCKfR z)Do~b(8;Y*Y@e&&-;+4@?xIzzA$XpjZzg62Ec~jrtAD|0#g#IxCAbpQFa|$?PriIp zrfX|V|MXN$WmHE1p!Rv@Mot^DsbJ~C?;;ZC|Lra zLZ|#>?pjL+MyAHP&x+3-9_6Yc!nsa{`oiOP_dBvy7m5pUv`}OkuvN3g1X_y)W-VT^ z;3=J9Uq5x$5!v(QeEf~Hx9{4tcj&G7!JHJ8qg@m2`iA#-8gk6(rw9NZVc?2|Tc(wJ z@(OHfQ!7;CT;_TXHO4v@T4t>eQ9PXl_uRLs2XFd-(4#px-5!9deKHe19&=a&N~y9j zgr%Li$eMC9+K;@IVj{%WJEUEH46f{~O(|RmLU(pk3)FzH4mERUKN^%2fl$2!fY^r# zhGYVO4>(xU!aKa;?T?*f-&`?+*dHMPKxbF8kfHZCuGZmJg76yx5a<8Am~GQo6gN-A z8B?N}Q$virz^J9oQDms~2!8D0OFXki+lTCcKByZ3aC$@5{*0c=C)+;PG+i0-nmT7t zD3-eaTClA>8S=SOTQ**>i|X{73K90nIlM9FLMpqz`ZNEiQ&64RrXC3uN+yPRg;)oMcw!F-<44w74eVap^OyCK9b_u0@%%6*uz{ zVJMt|eBZb_5S2#&oN!H!M6?6Fx6a!xrPyb;W(pc+D1%e?jZqdLaXmy7#YR4a>C4QO zvCH%ttHHV#7Oa+qxn8XQ@&$1I7$IN@xifq`xN_4Xx_~WsuT#!7MOn)m5$ab*j`GYq z(2XEkrt0J_z%yeIpG;RJ_YY1t(SR-4&ad(2Wqy81JiRIeNxV+8Iwb zYarrzB$qy(?N(j&?HPBYJZkh(>E z71qUy!ddM%y1yEHLQP)>RE+lxYiF&kxvimb>KQCCUMI={-5wY?AJq~DyOMa|fx5T% zY}eBazx!JaxFZe$q>ALKmt}=!{dGEqHw1mlBu0Hzmq;t5)3Hbnd(&kmQqQjYdBkdu zTV*4_^7ZUiu%-S1MnalcGkc!&prrwhOW%@9plUeJav_IBo%P7YQI+uPPdEEEY;iM4 zYtccLf|r|J24u9t8IxZ6koDI3HM2@Y^{|Ev)2KYnyvv) zy}GZ*F{mZ_pn_=hdH4~=>2?dF=kdm;{EgNS{kL+(dBwzg9FxbpTnZ&wPlOm|T^z5z z43qRnW*>55f3+2Nzl;J#ea1Xtr-2-!bG{RxR-5%`F8>p%y=_4YvCHD3j~zbNzGQ5Z zG@Z=H>4i37#IW(JUv{m(yNk@Fr&nVsX%jLVpK;$1O(CMbQ0~lA|FYBm!o1aZ1oNrq zlb{q>aLoi}N!V-EsMj@pB3P3p-%;9NXUk;Q^aHp-c3dh&1CTO(=q5Yo1Jx(FdN-04 z1zCm#AFlJWZ2anXy*#C7Xh7l+Bt*9clcE!S!KSWc2p99I#kXlq(pR@-KlW_z9NuC{ zUz)C_tY9JM_H9th6xi`;oxxJp$}2|>zQ(SHl4QpoRo_Z=7XS4m7&*BffUrio@2r+T zLyNuS-fYypGiBE3>-XXF{!5^SvXjk)0$qZwo4SutNEvGG+ugZsPJC!+pjZQ%x^$}p zlRg4{4;3Tvn|`MKj6y8DIq_PDcjQ3ezPaK6$CQwU{zXvv$4##BrQVOp4hDI8>$p;= zE`wtJ^7C-nGl373Y``E6(M#n`L;h+~oyG#YAWSC0HxUk2~>MJ~z0OcN2(p)%Qd?)*a}o(l4Mw z`CFzDTXNIJn!U+3BRW~Chj)++`sIjYomTOM{*UUohX#~8yBWM1e1(OaiZ%4a8|f&; z#pXA7Ve8XQQDw^gl>ySF%77c>x46c~K1Ku$#FQ$>Qyd+Y3(qr5FQ<`^&H7?=lJ95) zW!T%E$}w)%T2}5v?BfT@A2{)0rp=a3RrzC8adF^q4sv`p{x!_E)+lvk+_7AY*ONPG zyqM1V@YjRw%h3Ds*>R_k&{N&|pg=4A4;WZdy+dFH*jybl4Jm;sq54~~kux4|OoY?q z2U3ROF$93brlO_8?ZTG{4eIW$5l?H{YE{(VG%P{8%fqmK{N%cO)!SevWT86tQe0WX zU(O=n;Q1RDW#5vOv(4%#DmH4gTwSaU*o-i%P{s%G9*mjyYs3qpUo~kKMuk@T_pX_q z(bfls$8FB8qGsVOEp4avS_2ig=}7XI4I=&pSO{f;gBkR)KATa5)WMAsr{anqQ3z@QZ|jn%M{;!kc-lI1}VI|O7+ zxCp`enekvpPK#r?pF4|Kx-^9Nt*tW6xo`}x6d0FR+?4MYX<2lRfsX6bgHE(;q&nN0 zPDk=o@lkg3)0Z#gG7Va_L8QET`J-S!H48q6O) zQBY1n-GzTvo2dA#2^K2)71p%XoxbI2pWpNQRh#v;EoHLXp?lJKlM3nyb7yn;Go+)O zEQ8e@%G(;vOPsYU#_HYu$vb)w)4lW%%7&9c?QsM4XfVtOa0r-jTa47lkQ6jL3}V$(Qx-t~_W8Jr<@EN_|MIFb)iAGq-nQ zdwehHbr6$nvuJ>)(yEO4Vm3oPqQkgnQ4>*~Qz6u-R4-!$6%1#(aS^0JI&VpwRu$Mo>ibNK*Kk;gakj!xn2A2F- zog}8XG}%6RwWgWYgprQcP3y05zpp@`fJX-cb#u5!i&JVAKYCDF_1=^^XtO8 z99?C`wF{{X@4xbsJ8bi$uVJCBzi>o1cd;VT+HEPFYlaniIS7~5mh!62qZ#k+Q@ZNdf(~=qwuT4)LS5)&m1Q`$uNTCzClw_L>k?DT4 zL$A+dj`$+^?u?8i{oQ5uN1|*=D$0r~y(BH*n-O8TI5Ky-#V|j?QBE`@xDYe(K+&bi zt!(r=IuAbr*az1Ynrl%vaS6DJ+6wP8btw5Bg~=7n92)mwN^x*TXl91$(biP;heMNhxH*DI`ggu zi-@^O8t>DFo2f`)*03|CMj;Xw==J=w8kRN#k-h^Txmd33UNQd<%8^1OM`u^b*vCK| zFnOXf8YNc4P9QXvQ8koHOl+ened|R0_c~;|Qh@4i_95oyN(bxL5$};j6;570c7AyH zB9&8nY4(*0m$$o*^(~`+$9rTpaxu*XfS525kYd*gR+6iKxZSx<2_-fqH(9P<{Q3(b z{d+M+vP%MHiN1VP5gF%0WbLW$9O|LJa$MqyY`kE{U3b<`!PQ>0B#>ARj#!R#?*B*B zAZ-p44thue;=(1BUyrB5xLi>(yF&m-$vqekXhjU1`tBrn(KaP3?XZNa(K0N&Wl6~A z4sQv(V;yxNgV#gPCA;ZsknuZm{Ovn6@~ftNF_!)x7hUsL*C=JW??}fF!BYAU*l>k# z<-Oh|ws%P{qNP~z{uFA7x-JPX6HQr+?7M#Gy$id~1+h3eMU`3OENk1ml-Z zbgg*a*pY^S+X@)c6Y<_=N*nB0P2LOr)XK!xD*o3-6B}D>Z}l!1^bM)ZG|xeAvSdOVXcc?~>v9Zzo){BJVy zmpkl8MLi}Xot|hR`~^8=N(`M(esI0td^lmJ-~U7>IVSKp;@#{aXVH^Z-by(pd9JKZ z6`j*NqK0e-FQ-zu+BaL_Nr%n2_CTR7yRz$5{?DwBwt(>O!tmXR7p8(7TSt^vEtNY; zHe#fg?S;8!cc~>nJLor+`KFz|oCF$+cx$4z8O<4oh7YCG37TEx{KWi^;+c{Z81rG7 zv?9hc7AL#T*EIRM(4-t9TC@v9Y~QX$$zyim;fXFA@QIj?@#+-bG(1V8wsQ^VVw2mB zX#I&g2;~{~n+4&nD?mc7D%2fnT8FQZ>(R}J)6MT}9_-}Slo{6iN3ABUv502tLtK?~ zmo7mj=t7={siQnacIf;QfsI_oJX$Z$Y%*SOosA+iyQ=b3;3dQKK=#bIoyOV*#(>up z;Xba$kxzhFEz;5xTP@Hy=D`*Swx!J2KZ+Ii&74we#^!F^i zUHCJ(o-IYc&UIA;_dco;S8(WE8lvCzyYhKIP5j74HYBk_HWkh%f`m{L3{i^6Hz80V?9}-@=~uj8govixd8j zctf54^rK?7pJ&Q4U6)&xJe{@*7<46`$M6{ZVKcWXwi`3#X((IQR4n-1oQ%s~Cy!0}y>cXzW{ZB6*%HSNT_jaZx zZpga~HVwK}0h8}jaBj!>rQXOXj@9pdvr7~ZZA!k4a{nHqq#PsKakni!vZ{wipCwHx znQ-tLo7mV$x4wTraMcz1ELL1XM$tLmOFp>wxzH2X#(^6r248cn>%)49ZZ&J1_>5gk}o&>FHtT`R2R#O`;J*gU$BX-F_V}J0G z*D!dML99noiqvhpT29XQ;U)XN=Hlc#^2F~tkv_U$t1IDS>T}<9aal4S<=Cw^+h47s zBY`VxcFHO&^I6As{5;TQ+;@#E&@7=phs;?q|M1n~M`#9{xjFE?;9KpDcxV7Z z@nl-l*E)CqY2ZP(vHC6YF}^*|jG|UnJ(t2I@#t;my+gV4`i54-WgvEeMtxeB)-~Yp z*aJ0Bt`5U|M>d16dBE4K_?09(jf3;jjbxr+ z)Ewl!QvOHce$wip)!t-`4)A@&7${|!06>0p9TNfHgvkyq3G(f|y!rIkECw^HJ=eoC z`ZlM?{B7xca+<_TbSG?D*efOCts=Nvb&~TYNmJV4|RLbA<@}^!wdR=GW z56;=2_P;Y#a4B1$Y8+1|u{kq!F)}T@YbJx(1dKX5g)#X6QU2D{ea61s&gfKi*>328 z&Yk!W2_;=w4Bm}B;$agpKOP-)U6q7wC$nY$*d_Bh{9e~_Fx+Po>6qBB>UXsvxhA43jh$D34J1~T8i^>&%1%&hW;3-QRvW0lC1 z#t`$Sz}&x+-rG9T?G3d(8@tq2*AZ>wj!F_~nmP~la6_TU$%m@R5ba#B#%?XtKaKxe z_SoJbqgd6X(Wy3$s+1NB((X!QltY~~9Q(WiUu~g(HheKp$kB@`49^CWOc>W4ZL=c) zB=HeW(fA`=KKI1dD+WNH@X~wv`@WJ1L%Jt)O$Uw`bty?{8J_Q4qUexeV<(qC!ldJk z>>SFf@S3t-HLl~dn8BWVURpm9poSJ2CS3;QPd53qmyq7yNWke5YyOrJD zN&Ux7EkW)w8j@WtIyniYg*o^V#Ifoyt)K~oCvAl)0WDrH!X zv}|H_5SmR_p$@AslYmGDuN-&Pq~x3k$;Fj}nx@?aPdSCA^$2{&@Y~M#B&L9?l6CUX z(c}QCbjV(V{}@XkQBK_?mM%?vNEIG&qr@y(vZ4H?6UB(^K!Z-@9tw75Y@}^6nZzVMa8zI3n%K8iH{dn@R zy9h6bzjmJAn1oJlR-g;NOThXnNDi|odiADyBA*?oyEZFn?4Iu17i>MDWQbx|W|e`N zL2SLO)})V`dVt`=m*0bjBwLvdb5R)2u%q$kBW~TcoYF6ECJ01^%iB3t6sO9j>V=BZ z@U7=B14s4ka$L{@cqW<7Pg&m7hg{j=*rS~Ozw9!xUmAz;0OfuW)z67aig3t&fDi^fnbKf+BKxow@&uBSRue z4AV&?HpXPG)1V5OC1UE*DXZ^X>45j@LSrW3pQ7j0pXRKfvCtUIdgM5H$@k0wV8$%` zK*+Dt@C+jzAB)SCde<);WhL_E78BbLH1`ts^VMrG{|q@blVP5=#o);XcOq$yGk^ch z)l`@B@}ir4M;7`@?yrgUgqkbE$5%rcitn2Bo5>aNpQ_u4PlV7PIsH^(R;qNiK*;xK zp@1^oG4n@&S*{^>kq&=ux`0%T9>|^ZyVZ_cLifKjyk%=GC6v~jOsPZfrN1CvjR_BT zw;oBo#A!VU4h5sIQ6l1(VCquIV5#Nj5L5Rt6vCh)UY9G9A@O?94HfgVaGie`) z6;(kz3j))ZzUP7T%=eo?iJzPypX1nYm@g!rn|kz65N+D91&6nia+Y|}J}<3xiXzo0 znZ~{LV*;@32d6GNin=hb&gd5a&s2AHyH2BtC$81ylx(gX5@J}?va)iG33h&UHpkGi%UDOL0>{|Z8 zk<+zZF&R>R47-4q4CSiY5R+_M;A>3AhS8a(ewSkrRo?HT=0Q$LbvflTepfee%GS&_ zH!5oL-EDUWy)S;!PGV+))A*I2nX||Mn+p@yK~6zqs%l@#l+{sdjLujzV?N05kc*!t zeIadjmfMRz&B?rHkt1R7*#X)<`;F`iIIbiH?yE{3)bkuylwu7fgoaB}VVwkkhebiO ztatPjpC(?DLnVk^fq8T_2T#FO5koDAlih4w$9bz=%IVaxEwNRp+lR8NVVzWbE4|EHuAe&NZ>*2~+3O(n$hPBNoKOEK`1Tyk6TbBt`-;?I2nM^hjNX#(qR3Io!U-C_JbRQr2n+7R4zmjd8I_-@7GY zNL)U7YjOP6`S!u^4flGcszbrq1V9KYCOuly-DyK`&AYcQE2eh^j&39Xm;`}6YoIY4 z82kCvc%^(G>hr6hQ3YcL!4=RkyuoaIkFxPy(72aIK*M4{UpO$=-(X~rZi>qA9eB%Yys3%yB4?wb)k}|9V_|v%U2Q0qpIEGd{@CS0)-0|Iipr4Vf6Q)HA~P2$FPgC(&^JN1jDlSjb$7=?l$(T|T1iU$UtYTlEa14#`} zO9d*PimDjdw59#tDE~7qzxIVcIy6_TWlOoT#FLeSn&Cw@gHpJex~hQ=Z;0&%MiNd= zbzS^eAajP_U(uW|U0;H{lGZVto3%95VWx?_4^Ui zeA^E}zcUt(PjhanlL`3X7OzmA;0%2nJjhsubE_5 z@6i+*N*SLFGqdHMgQ1d>9&BEU?Tvm z;lU4#Km(~zcl@s{!=^7aMOPpH*tJOd*N)*YqoD|;DfOp#?#*M+wf8#y@z&T#aHT;|8jl-rv%b7+Cdw(DlKF{|Y?f`LDF%~|P zyqx?L&!cp8J%%p=T80aqeu$Z3y0bNRe<{olJ_G*VviG|>aDrOdt0CQS_}uChr}+AL zP2=jc&2Clr@jWx&#$z3C%AnXOyB?P&s~vZjhIQ7D8s1>wIedEhyaBLnff1K#W9*8Eiu$U z9?22kJNE9oZ%N$UcqEf}#%EXK`l-e&GqSi=E%o|wbL+t4-k}nXjUVugjF`S#yMOT; zKJ&FN{CPf@?8#CSlYackPu48AHm^G7ALB9OU4bS~twrM%RF+;|viO`rPR_CRf4_k! z04!i3waZ-*q;U_%yn`l)}KdW zsf26Sp%(^g<2M4KQu_BCoW;rp&SD9Gci@0oV?pfg$@1v5&ao-ruV3@%tzhu^nn$L( zeId?tCALQO%cnkE@0V*EnDo1;ZtIzVTy=_0+#^rP!AQwMNuuw|X&!m>nAB+hy@9~5 z@9f?_{CAjk%L*~Y>W#BqWiQ@e^b-niAZ{$CqO=b3R`HuepOR~kKUmNF64LPwbIR9o z@!k|qHxe;?;UJByf)%0@re^yc7b1Ryq~GDv^$o%7k}zv-_(LWlF-4?R$wcdS&E|pA zHjm+sjHAUh3sUNV9-y#J&DTFMD-q}8i%tFK%j6S$rCalELEC>Qs zc#GtZcDJ&v4yi`O53QrbG4E8=YZ>lep3_PSFOkf-s3ND_>wW$8Unej!qsn#JPTr@1 zeCq4>uPUGa#k#mQ+O-}2Ya0Bp(YOj;EmzV5a|L{|4 ze`6lMJ_B2-$j=c!=As^pjfL?XxkwUr*nX7`shKzn7I!@c4~xrqU8g}Q9yVz`vSeL#Wx+m2f~}#Oi|e)< zUFEsap~2G0cD48h_;5>5)Yx` zR%zitp*ZTs4l5(RCW)vNb*G0A_$T6dPIeW@&V!p^hoq0=k|L; zA)Ipr0NA?Tt48Y3!A93-%l9r;o)YUac|FAuQmvLed1SsQo5W-2bEh;O`7%6kgm7gRaGDV5@6-fLEC zd|(s-;3+<9BFSK7Yr#7-VZUzcBL*q*7&uR^`Z307K3-ZNB)qUx(mX@ED=2cH*RJsI ztSbCP!Abo`S+>Q37X=~o{VLbr3(1kw^68AHd8oLwwc%Zp6;B*frCcCSw)CV)y;JDq z9CJaqrcZrpZC#Vqk~+k^Wzp4t-BqS_IF~}pS18nU-JXAX*H$L|xrFdv0!|0C{1P7? zCVQ3mJ7y^q$`cVeVi_kYtKCCY#BC4zPn!>4v9%2?fgi(zV`4vzpfy2u9^PU87)FM-!Ju7qzC>_HTT!7wEO8oqjR^^A2DP0B`K?D1eTDIt1fmW z4(ij4H9$*S@pN_KtUB#b_ChSIu64d@LU7W|IkN@VY)B+sh?S==q90I?C@p3>%67CT z=%5FP2d9xni1_ff=%7{bFp-%>`Goqm6}(SWiak1d_N+r`^h@^RX*C0Tl@0z|btsu7 z4-A~teY=2fWdKIW*0piiz1}u6JZ(T0qTL<%QBeu0!zq{yFR56? zgZHzDk z;6aNASQqaAT_rs69u(VAf9F^EBuY6Zg;Y~Y%mwOp*0G8=G z7-}inG^C|czuA@kn#X=8P5t@~h96?GTTtWgd~Zvm`#a_SAJyLd>twrZKKncB{Lj|d TZTWk5k^jd_ee3IFgx>!HlUQuC diff --git a/backend/app/store/avatar/testdata/circles.png b/backend/app/store/avatar/testdata/circles.png deleted file mode 100644 index fb30946d44016008b5c28eed4719f6ef6e850367..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11392 zcmcI~c{G&m|L|BEiisAxYz^wM7G+8HVklc=k5HCmi!5QrUdeXbiWrKrB#~|Gk{Ggt zME0HRBU^@fuiNu{pZEQq-}~2l&U=pI%>B8p>$9)deLvCD(Lf#HJc2+VP#8^h0|bJG z4}qXIp+^Ep%EMqc1VU5;qpo7;^K)rf<({Q+Wybg)DsR1>uJ~t8&N;k#`&8e@9NQIg z1?F{ZZ8bTARIZLuhwy!24#`x`tVg|3^Zt;D32J!Je>%j&OaJt-hI0mOpEWc1PPnU6 zTP<8#b5Bq=@XUI!Y5J&eHmKZn^iqx%!FgrFaiz3xg&Kj#;ICWP2Ar@fgMT#i2n3Fb z7J-n&{huIf1l2zvB=bMJ4nT~2;0XrRBLJpnqsF zcBM`R%6C}l4&|;)D6Sq(>5lWy86%Fn0#h*Zom|cjaVd8glWkY_*lov(AHevwZdxeL z4M#JcYTj5*rM$HIQ^*YzulmuW^YizRz|pnL$%;VAj8a|AY1k-2b3Jn%vqqETYYR8( zF2ER`X)L*3s%D`A#_hrl`2-NH-QS3ZMJ>R)b*8mLnoa1ArS?&Qo!sML4YsdB9_xOD z&wY+zTeiH2mhPT*Yv2c#%(C%aC~u!`cfDCRUd{spga{Jj!MCmREa`XE;1etrPHR?% z#(PP$fR?BJe#xFC!@j^HnQ9bF+_U9=B>AT2psNvZIL*}QC)#Xci{{^(+zeQO4`qgt z;(3HEO1H8^@pe>zHM7a^EB?YB783BHk2uTX$aEOY^3(WTq`2t^?_%xSR`mAkYKu(^ zCIN#wqF>s4ewS_dxUFK-@DBIfx9lnDgKLZSlxw?m6gEJ<C@m9%KEmsh;jNNspkBzHytE zYFhhdQ*IMtM}QMIMlkZfbnj=cZEul5M7n4HQL*hM{Sd`2k$y;G#ZZEP)i0u3v=sx@ zBdI-0haJnuN1l!IJj;H5PW>5ye4*WHkbI%TYJq%#t{(A;qegZvILv25?Xe91QrshU z4+*v9`;yMryw4CRM;Aq?Byp0#t|;E8c*SPF@tDvcHRN#+wBbw~s=uIJl*h5u)YmBP zB_2&uT_bs=n+GgD9&cRbSJQM?(@58uw##$DS0qwSaa{#gGCIo3CRXC5%J%8YZ=orB zc|z`hd9?g!^b~eW@jJC7&Tzc=P#Gitjd+bpPI=E>NvKW+wqA!3!@a1kD_FZP0@&8H zEzEYc*4v$HBG^J7DQauPe5kHij#s$&?&}9ao1QJ{$AF2y*A~b2$yF}JbmdM9dij## z2AeN*m=COP8Salz*CV=*xXAsG{WHXyZHpRx*#9F_Xfx7B1mHTGexh*7XN1_Y^1EiK zOM3Ka{{gfE#b6&6U-U&xSEQAzP%P9)b?4EySyKcuR-k{2KefAA#I@vBlyBrM7SsKa zucNO)l)d?MeKy6;>gI3z`G|fl{|75lBBhRa&7%T4kJtc%w*G$Em=(JDKW|M4K5cE| zj66>3igDcOC;=Q*=#$-UYEzVFZevJ9)by{Pd{8nMr#evjiOC)jxfE|t-*>qUY8My< zU^}k)W%`nwyLg|NhW%cio_9Sp!yJJMdK(9Qti`boeIco z^&&dv6B0PtS5&0=mCLE$)`jzyH)4Mp?bm};*Uhh3XJB6G-!Zzd6K}}Xp8YUMN!Be= z9(WzDP}J%Bt3YftSUO=CDN96pA!u3>hRbq{Ja?r;@fMh25#9G6RZfBUPv|V{EtBCh z*4W-l!&?kEh5+Rl&VGG3&uhC@}W< zLpt@)xSBzG&C_O98o)88T+HFlcI;HBC<7d(CpA{{#P$YG@j^oVW<#ZCfK9Kv(H8#f z!rOeK#&mx+kMi{n15t>na&Q-MRYcF*dxx>Bf(%5Z9?pb09}1Sl-416m@tNfVogQNPpkA%xmypY-DP>>E7 z`Of=mLi|9L?8;gAKjrdxAXXFE#UC0TC*|NnwJM z`@6$ivp#qA1#3mk z@w^2jGkZZ+Au`(1a`?nsloobJ`mv9tBY`PBd|k4@WgM zG(`8R$KUT@3oo$4vnFHK{v%rfn%&YmO+V$fv4G>mk-aQ z#AeNxv3iSX0?CsjStq`4O5Rl<3p%!nog!OSpXwBa7h)vDK3mG3!KD0QCaEo&CV;Tv ziJmIR_qMr|{+Wd-0P~lFnx6#g-+Oh%TJwM1`ub>0+aO4-Wx=HM43OvY;-TEvN~aVi z6y^kp!$3K9zW`*igNdtCFn96Rm(!8QC2>9s0^kwTKJ5bZY1ml9H$39AoGFf3a=0)f zrUmnn&$^K2e34b5S1|X=K&6z$6Z!v5zU+{0M^v*n!Q+fJCd1m5>j6;bW+)n$pb{zen%FK3C1qN?!5 zbIj7CjnCKklCq90GzA}`XIK8UDZu@0&=VVN&+`c~bl)wyPX$D-47f+Xwy6g7nk=l< zPk=~eS529ZDf%@of=LAFW+I~h<2DHE@K~Y@ulHqtLCo4;Jmzr7j5B)>GbH0ip4?}$ z#B^t5KQEdC`ywnaN(?zh|z`BF3->C1Ez z)9ngsj;SG#K!0k4Sth3nY@50ZyrCXpZrf!wQ52Dfp6^|13J;?N$ntd?_nE!1 zn$XZ;*gg-dx>Y_8^FnBE*g|LjDEz9tzGtSeDw&9m8tQ0Z2PS8K&9*jqO2D#CfA98J zpI9yE>U)rC89=H%PTC$B67Ft7EhyGO=p9A|F#m|gtkxVG9uS#W7r{WCAHbBOJtb^L zaIb0Kz2YAzi1FZL1w!NwRi{6?`jeE~huz#sL=){nsApaxy=o*|EErI!5yNCC+%hBN z2iabDD-86dYr$lri;jjq`_Hc~Ysk{S$CoaFA!5JOg_~2lR(>3Rhd65tq4BqasAy9@ z@DB2%Kv_izN?jn$yS)O5MePP8T}Gla2Q7beDd#%RbFiGK)DJIb@!E7Y8bH#0K-qMn z?8;*fgKJ-5z$P#?cP<+PPXdH4e-#1{k8fLIph=$w1a*`f83dz&So2FdF>qv-yG{g? zV&~g+*cuU3PD(h=q=|t7Kq?KoD*A;5Jam6<4bcJk7{P23@fz*;YFb4Y@PY&xf=T0_ zt#{vjsiu34CMtp~p)9pXiA7(~uXU$mP+fj zp(fgh$x0HZ_fdiM8;w5HYq&E5ng_jHg`?jZ^`I$LRe zLCi&1EY3-4RSXn02e|!GfzyMeqsk^o|Ju%%St-3f+<`Ug$D$$N9)0VI=K9!}?hNjS>fU>Ou8I9=?RXT(O2L!Ah zJ=(@VZ%hM^FaiRhCqAGtI-jU%=%s}uaHQ3E^z}BI}H5F z8WD#!G73BnpTk9@qlx;};L?KTF*-L7ST5o`yYig8#wv)xCJ=*?IFQepK##O#J}3+N8Hr-D2LW=Y#$>fV9!+2XRi}&58ywO4O9=(oWLHLGJlH^v86zt! zqA`M{K>C*rh+ryp5G6stZj5{ong*Z8Nf<$KrRCu(tEnQ&_je&*1 z0m?vvzd1|s1boLLX#iFXlyVjiv`-7vaImp{Kn(bsJO+Y!>|$h+`}-HHI}T;we+Ng- zC>{GpNGH%a9!LcX^S1>5uy-HcHW$SYg&nfVW?3q2rp{6}3vRwJP)-qk|F=oo*TAnt!&?woIFPAjkm-Pf3NV{> z#iLo`&AUL9G*9cMD6FC}*+WzZLY^vT^}hTYjcF=5)d@S2YBB+32^n^CF_&k60EAT* zm8kKV2FWb%EX;ZtoNNO604%*6>=ML?^V0&?or&l_ps=$q7A9ZxHG zLW-YGKoe+Sqq8R;(}J`d@-$YLSD5zqJ`NAhhTJ|gIJCzbGCNhlxxa9?5nBhkTXw*Q zB`L9-4v%*H>8dPL%Z!z>b6(jO>D!{nvD4rDgQcvE)=>iAIp5;??X}X_@2|L9Rv;{6 zyERTGv|-a4DU~}bQsga#YNorgIa8R#@DqUJ?tGucabe7_-ec9fyAq?dM3V(9cUu*2 zQ};4mNYlO>qJL-N=I@o!Cl%g>&ue!VCmWQE_ty&~EP2W6Gj0iL7ztKn=OXtj5;F&o z#VmRPv@=yhF+Mky7FRwm?_)pb?@>J}0-EX9MB7;UUpGl_!87?JlhV~sf>#tEa(XB$J5d-+Hy;cFIpv!CR#6R!QTmODacB1*VHaap!zU&~FR zqJ0{NJ?D!6CX%JP=ekAN-+nV`J^pf6*6GhXr|#6j^{E1K=IhQ9Mm|Zeog2rPR50Dr zy7jH``5w9vGN9kMTT(vM^=8 z|AcJE3^O5OorKAjSFBgjav9BxEVpGa)p@RikqguahAG}_dWJ3o3b6H9|s z0EQE`r$O>Jz?*X@{8W0pE!~EowzY>ri+BqT1$n8*Q~Xo}jn8+zXy~6sMa@B3Yt?H$ z60Ex`sEf(w)!v{fymRN?-PP}*kJN$StlH-$$IoKqHL$11Swl0VWqeBMP@UqoA1D+r zBe}i_V(i6S#XD~#h;}WUiTLW@mUxd2e}_YP?CY&2TPA%>w*%f{W0ZI3-F^LAE8l7w zJBuZ8^(~~F%S<;g-HvT-3l;)7qNCxW%`bb30XcalL(oPot*`c}34=aM2FP>Toj()w zU*opzLJ!g=?rSci+zo*3beh6Kp{&VpCXEHBgwv0by_G!TE2(G$Qv*OQ(Ez&N(-P~uv#FQw+T%Z z6Dp#Z#6$))2D=MhhOS^zsbJ(=jUN)7xh;x`?oSl@-WQ)5vVlZ0v$q((=4pWpn^%k0 zo5UDwuHS#~M~kwe({diC%&3#Qs+W}IQr?fnen6Bk_ercMfi*I`a{fN+ncYgwtrtM8`K$D# z6i+|Yn>pT?%9MC(MD1hQ<=F?KH1wr#-uemk#&y20-qFh+33YDY=59l%M%`-8qbo;q zC}!m@9ya}DwJ!7-0EeE6W5Ssn&G5DZ&s`9is-XNaCub!c+nc?Axwtl;*O&<6Ql;8OfXzq1C7EmiZ+xcLM#UD=feJXta1x{4NOFawzBYIemT9 zrEPE<QEg>9YjyHw-=NELHfLjHb&EWXk!%Nl9qlR(`YB&JOpiz4ROtZ=28Kb2dV zVQ&n_zf~id=~KT6=I-1m99f@F^nB_OWB5a{33y5cK#}6IK)fEQo!fl_n0+-I-uQx) zPQ#$>SfX15+F3w9s(lXX{W=n}zjui>(V&U#B&YHivj$14+is!YtI0Jj`!jS~WHXRZ zMuIgVotiar09R-BgAo*ze1=Oo!nTl{y~?>_b%eK+mmQ09$z_>Ga|v| z+Jq_;IJus`qPW}tN-yQp-QNl|JwV|2LK!h}G_ifwz;Vgn{hpi0Ti}IfItR;IVzXY4 z2EGHn#hx(q%moqT-ukgj>^vGCi7u&tGWuM>bx>6e3LGKzu|z6^Q47iZoVFPlL?U`* z7~45-48}B&2QkspiCK*_ExRH3S~G-J&(gw3L=!gr4kGA5i&VLo0(!oAkCzo|z>$0$ zT=^oI=kk6OIep_N4#w-p`oAHkWkyUJzn88`D` zUnceGb9D=T-$#C4t1^(A4p6v6mwy8I&2s=Xx{UihT0R$l#A$2X|6B7Kr?U06p;&Pu zn1D8e;CaABGue(3kp&$*Q7F>*%-{#^>0leJDSR@t7cC*Xy|)C=bAuH+vLCMq{_SETb);F zK)LDNIGe<9M9?dM=8uf-tmyA7Rp7%X3CD&(&6^5V#aL2kMI;nHg8iZ~ErBdCfLQ7f z;%=ruZa&_6I?SHc&wwEFD%s~aNTx!I;QfU&^+&z9UV;qY-j*mS>yz%Mzt*mLf=bR; zS65;Z*ISB|5i_3xGZ6^47u@Y4#ADSEC$BQ;JrCfe?OQceSwcCj+h0^4->URfMt4RU zhuh{m*M=UbHmCr>RjE0Il$Fy*UgLB+VCFryIQ{1L^z)B7adwC}={_X(7K*Q4f7A97>*Uk0C^YDWY3REcg>Av4 z1c)J`JkBTm1I($GZlTtIz$ipT>+meT2(;wj4|lbo$&%p|m>a-f21oR4BESn~B~D&d z?oJURLI4ad(n2w_7eZm)Idj7f(a>Ad$MuHoa{?Acm$N<%2FUV)>rez$$MQyl)oE5m zNQ+(B8rKAfg%UF55~NI5#MPx zh6zi?>Y(^={D56`gVP0tUz_rMoWTso5B;p8n*yQ5>SHio0GjKU8IA{FGbP;Ar!UmH zfB7Q@QvM;puAKBx=|g?|k-`#lU<-gg>j5ww%h29#5x6|(Q(9)H#Q`+x@8Z_L$oECEnTyNm+f_or-_3nP~wsaf2RyUFQ_5pCx zTyLD{u;}u^K&6UyoFc_X^BOWeD=p#4MaH(FyAV0|4COMa5w6rZ)8fPXNx8s~9) z^31K=p(LA`CA={Wy(G&{ax6gY8y;h2wpizyDa_KpwMEgNIt&s{jwe0wN@!C;mVRqz zK{TY)>vlZJaO|*&)zDcIEocc0zcLHVdkR}x%01j>-?uD=+Z0725`@O;An58CL@~lg zc?sA>wA@Q)MSSU4{E>V2AH8ou&4GcjU>=ITeAk;Y=K6RPqInuMXW*MyNdZNo+EQ8e zjd8a`%)sS>2G!3fNUdhzI_G)XabIAWa@*PUQ) z_2Sa{Yy$9HdKaDKO2Vk^&}hVLuzkRM<)IAe>7+kDzL0K$_RQ_-@U?thl9TQ7SY@9*K2!0qy9YsG|KOR)}w(B$6Px3B%O zH`6@6e~x=eSas(voLUzF<|+yczfqh;GLNB)+Q-@WwiV2Emnhc#wf7q=y1Dqe;=rok z#T5LelxX=Y?E_pVHo#@$Du%^bS}7_ms%-st{8goLX&j=d1z#~^g<}ow_H>c%>=e+B#WGbS=sCRy4UNq{nM)a$ zTZ3}9DC`|FkHPR3UdpTKe>~FqEDV&@y$FVaid|q)A1(pkg>(8loijSjxi8vh30tiQ zYPUJz%#m=-;h_ch}_Qah&?wWfDsy6~kEX2<9qUh{zZ8A@KDt6G$q1l3ck=i8#%@&xiw-gS+8Jc5*$a2MKeCTeG* zA#s1yd|mDh73~=<@%J8acZ$yLkv;Rv0$irW9)rdNB(p>d=K!4vS-I|wf|Q*xhfkS#)x`Hoyo`s*tggY|aQRSNhwn;r1M_Iu-fa*%d;%AK zzc4BYY1R{e3A&Sxo0R=DlgO&H?J97C#jc)(439~ni)kc%cw(Vxs;Bk3w9L0g3L61b za`ULb#r*v1sck48rGdz(ZO}d*%9d6I4UqXG{#+{BxtHAPNwys}#!V=mowE1g?bG&g z47nD1rY2f%{dbEV<=?>40W>--u5FeMImgAB7bh((*}INRZgDi*p$@}D=|fVXWpuIY4yeTfoZ%h z-UicLUVNd4SZ528B#Y}6l5p?lQwMG~x|z3ugt(M7^1xT!B2npVdBqwKBHwFSij39m zhX=>+H{0!BtK?Guv+>qAbrhTT1w`X?wpNLb~p|6iQHi>+-uBMOh(W2u!0~Y61TG zIrSKrMFx7})WA&Nlg#iwvHc!vMqVYjUZKPCdZkg(j)mT~tFWJ1eJkfQ`_`zDgzcn~ zqPwo9Vd^n{N9nQC$kw-o(fo-{Fno?Bkc!ptPNlm&dE}L@tE6JN6ypm?c-|TN$ED;w zPzhxx2jeWh&b;bVHt67r9;YbLMf*QUbp230?J$Zj0N%Mor*FBd==afR=`zS+&)oTZ zIbs%h9k=82#_QwiHj&XTEn)pX-IC+=*yFk(-w zFI^2hYCNb`B^0t}EF#64Q#?O}pstri#`}*=3CQc-KJP$Yvd8OA7ZO*oGlN!7*=%pu?jK}Rmyx%}#QXd`&l>5{N{S~=tOeFo`&-nz(CQX0)eT<&}uKugw zDRIv zTt&jqy_P^&4fQ+Ix+Uv5w_MdchmKV#5@6Av8Mz2`;N51g_puJQg6j1d%qkj-AORqR zPS-7T4))Zb^j)OATVZk;$1WAk-Pde#y?pax;eZ2d8;4tge(|nq9{Nu1BYP*_dlWW% zd_^49W8b6i-beM+j{Fj8sz3yJ{r?nS4+b=Fc~tO?Udf(Ei2g>uxV7UI@Kr zK1ic-<`&J?W9i;cs?MNM0|l{R@QloJE+Bqhf>KbY?X3sw#Ll2a7LLj3pe@R=!Oj)N z5!vakI5n%lqc9B|&CZP-i!&0gmTOP?d^aDG7XsgHxu7;5P8GE*ufo3Q7CbI&rhQJz zM+7Q=w|i}2s`e47v)`hUqh|P0h>X_eL)bxNL;OX~b6+LNj)JjGvB&S@TiaP;b4aFb z0i`GtlwU-B#dN`tuZ6RCS%j;G2Vd^QQcFYqIsdjeE@A}Uc;Fgriwypq6BLWkS@&Y? zy?d@oKU!_?4%T9)c02busz5h{d{;Vb9ug!HBl*zkfOS6tU~a12mJe=AlS?P-UTB* x*mVFpzyO25ebGPP+5U_40Ca#92H^zASe-`>amiT^u>b=Cb45q}y{dKa{{lfs0S*8F From c4b10a395a8c95eb2e3c844cadcb23debadcc897 Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 27 Dec 2018 23:11:20 -0600 Subject: [PATCH 03/21] most test passes with ext auth --- backend/Gopkg.lock | 37 ++- backend/app/cmd/avatar.go | 9 +- backend/app/cmd/avatar_test.go | 2 +- backend/app/cmd/server.go | 113 +++---- backend/app/cmd/server_test.go | 10 +- .../commons/pkg/repeater/.gitlab-ci.yml | 25 ++ .../commons/pkg/repeater/README.md | 41 +++ .../commons/pkg/repeater/repeater.go | 60 ++++ .../commons/pkg/repeater/strategy/backoff.go | 56 ++++ .../commons/pkg/repeater/strategy/fixed.go | 41 +++ .../commons/pkg/repeater/strategy/strategy.go | 28 ++ .../github.com/dgrijalva/jwt-go/.gitignore | 4 + .../github.com/dgrijalva/jwt-go/.travis.yml | 13 + .../github.com/dgrijalva/jwt-go/LICENSE | 8 + .../dgrijalva/jwt-go/MIGRATION_GUIDE.md | 97 ++++++ .../github.com/dgrijalva/jwt-go/README.md | 100 +++++++ .../dgrijalva/jwt-go/VERSION_HISTORY.md | 118 ++++++++ .../github.com/dgrijalva/jwt-go/claims.go | 134 +++++++++ .../vendor/github.com/dgrijalva/jwt-go/doc.go | 4 + .../github.com/dgrijalva/jwt-go/ecdsa.go | 148 +++++++++ .../dgrijalva/jwt-go/ecdsa_utils.go | 67 +++++ .../github.com/dgrijalva/jwt-go/errors.go | 59 ++++ .../github.com/dgrijalva/jwt-go/hmac.go | 95 ++++++ .../github.com/dgrijalva/jwt-go/map_claims.go | 94 ++++++ .../github.com/dgrijalva/jwt-go/none.go | 52 ++++ .../github.com/dgrijalva/jwt-go/parser.go | 148 +++++++++ .../vendor/github.com/dgrijalva/jwt-go/rsa.go | 101 +++++++ .../github.com/dgrijalva/jwt-go/rsa_pss.go | 126 ++++++++ .../github.com/dgrijalva/jwt-go/rsa_utils.go | 101 +++++++ .../dgrijalva/jwt-go/signing_method.go | 35 +++ .../github.com/dgrijalva/jwt-go/token.go | 108 +++++++ .../vendor/github.com/go-pkgz/auth/.gitignore | 14 + .../github.com/go-pkgz/auth/.travis.yml | 23 ++ .../vendor/github.com/go-pkgz/auth/LICENSE | 21 ++ .../vendor/github.com/go-pkgz/auth/README.md | 128 ++++++++ .../vendor/github.com/go-pkgz/auth/auth.go | 201 +++++++++++++ .../github.com/go-pkgz/auth/avatar/avatar.go | 162 ++++++++++ .../github.com/go-pkgz/auth/avatar/bolt.go | 136 +++++++++ .../github.com/go-pkgz/auth/avatar/gridfs.go | 118 ++++++++ .../github.com/go-pkgz/auth/avatar/localfs.go | 123 ++++++++ .../github.com/go-pkgz/auth/avatar/store.go | 62 ++++ backend/vendor/github.com/go-pkgz/auth/go.mod | 23 ++ backend/vendor/github.com/go-pkgz/auth/go.sum | 50 ++++ .../go-pkgz/auth/middleware/auth.go | 200 +++++++++++++ .../go-pkgz/auth/provider/dev_provider.go | 199 ++++++++++++ .../go-pkgz/auth/provider/providers.go | 127 ++++++++ .../go-pkgz/auth/provider/service.go | 247 +++++++++++++++ .../github.com/go-pkgz/auth/token/jwt.go | 282 ++++++++++++++++++ .../github.com/go-pkgz/auth/token/user.go | 126 ++++++++ 49 files changed, 4210 insertions(+), 66 deletions(-) create mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/.gitlab-ci.yml create mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/README.md create mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/repeater.go create mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/backoff.go create mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/fixed.go create mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/strategy.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/.gitignore create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/.travis.yml create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/LICENSE create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/README.md create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/claims.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/doc.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/ecdsa.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/errors.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/hmac.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/map_claims.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/none.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/parser.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/rsa.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/signing_method.go create mode 100644 backend/vendor/github.com/dgrijalva/jwt-go/token.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/.gitignore create mode 100644 backend/vendor/github.com/go-pkgz/auth/.travis.yml create mode 100644 backend/vendor/github.com/go-pkgz/auth/LICENSE create mode 100644 backend/vendor/github.com/go-pkgz/auth/README.md create mode 100644 backend/vendor/github.com/go-pkgz/auth/auth.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/avatar/avatar.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/avatar/bolt.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/avatar/gridfs.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/avatar/localfs.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/avatar/store.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/go.mod create mode 100644 backend/vendor/github.com/go-pkgz/auth/go.sum create mode 100644 backend/vendor/github.com/go-pkgz/auth/middleware/auth.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/provider/providers.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/provider/service.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/token/jwt.go create mode 100644 backend/vendor/github.com/go-pkgz/auth/token/user.go diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index add74efb..69c8931e 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -9,6 +9,17 @@ revision = "767c40d6a2e058483c25fa193e963a22da17236d" version = "v0.18.0" +[[projects]] + digest = "1:6f958db63973bc397ef72acacbd56e045b4a0160af1224d6eb0f20deb860c0cd" + name = "git.tkginternal.com/commons/pkg/repeater" + packages = [ + ".", + "strategy", + ] + pruneopts = "UT" + revision = "a207227f9303dc677c4d9644f709ad1e29bd0940" + version = "v1.0.0" + [[projects]] digest = "1:bff7b2530f02b143623e260c11df5cbf34e0faeaca6aa001a8be31f333518ca9" name = "github.com/PuerkitoBio/goquery" @@ -111,6 +122,20 @@ revision = "9f855fadd4b8cde7773f9ef51f6b2705af239519" version = "v1.0.0" +[[projects]] + branch = "master" + digest = "1:5ef69525e5e62fb771f3f6910c94030a86b542eff1cb9d9350803b6dae147144" + name = "github.com/go-pkgz/auth" + packages = [ + ".", + "avatar", + "middleware", + "provider", + "token", + ] + pruneopts = "UT" + revision = "8d5238712a320d972f9d658e2fd1d4468ef81c3e" + [[projects]] digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574" name = "github.com/go-pkgz/mongo" @@ -375,6 +400,7 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "git.tkginternal.com/commons/pkg/repeater", "github.com/PuerkitoBio/goquery", "github.com/coreos/bbolt", "github.com/dgrijalva/jwt-go", @@ -386,6 +412,10 @@ "github.com/go-chi/chi/middleware", "github.com/go-chi/cors", "github.com/go-chi/render", + "github.com/go-pkgz/auth", + "github.com/go-pkgz/auth/avatar", + "github.com/go-pkgz/auth/provider", + "github.com/go-pkgz/auth/token", "github.com/go-pkgz/mongo", "github.com/go-pkgz/repeater", "github.com/go-pkgz/rest", @@ -397,19 +427,12 @@ "github.com/hashicorp/logutils", "github.com/jessevdk/go-flags", "github.com/microcosm-cc/bluemonday", - "github.com/nullrocks/identicon", "github.com/patrickmn/go-cache", "github.com/pkg/errors", "github.com/rakyll/statik/fs", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", "golang.org/x/crypto/acme/autocert", - "golang.org/x/image/draw", - "golang.org/x/oauth2", - "golang.org/x/oauth2/facebook", - "golang.org/x/oauth2/github", - "golang.org/x/oauth2/google", - "golang.org/x/oauth2/yandex", "gopkg.in/russross/blackfriday.v2", ] solver-name = "gps-cdcl" diff --git a/backend/app/cmd/avatar.go b/backend/app/cmd/avatar.go index 52174fe5..f623cc99 100644 --- a/backend/app/cmd/avatar.go +++ b/backend/app/cmd/avatar.go @@ -6,10 +6,9 @@ import ( "time" "github.com/coreos/bbolt" + "github.com/go-pkgz/auth/avatar" "github.com/go-pkgz/mongo" "github.com/pkg/errors" - - "github.com/umputun/remark/backend/app/store/avatar" ) // AvatarCommand set of flags and command for avatar migration @@ -76,19 +75,19 @@ func (ac *AvatarCommand) makeAvatarStore(gr AvatarGroup) (avatar.Store, error) { if err := makeDirs(gr.FS.Path); err != nil { return nil, err } - return avatar.NewLocalFS(gr.FS.Path, gr.RszLmt), nil + return avatar.NewLocalFS(gr.FS.Path), nil case "mongo": mgServer, err := ac.makeMongo() if err != nil { return nil, errors.Wrap(err, "failed to create mongo server") } conn := mongo.NewConnection(mgServer, ac.Mongo.DB, "") - return avatar.NewGridFS(conn, gr.RszLmt), nil + return avatar.NewGridFS(conn), nil case "bolt": if err := makeDirs(path.Dir(gr.Bolt.File)); err != nil { return nil, err } - return avatar.NewBoltDB(gr.Bolt.File, bolt.Options{}, gr.RszLmt) + return avatar.NewBoltDB(gr.Bolt.File, bolt.Options{}) } return nil, errors.Errorf("unsupported avatar store type %s", gr.Type) } diff --git a/backend/app/cmd/avatar_test.go b/backend/app/cmd/avatar_test.go index c7c62c91..e0df7193 100644 --- a/backend/app/cmd/avatar_test.go +++ b/backend/app/cmd/avatar_test.go @@ -5,10 +5,10 @@ import ( "os" "testing" + "github.com/go-pkgz/auth/avatar" flags "github.com/jessevdk/go-flags" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/umputun/remark/backend/app/store/avatar" ) func TestAvatar_Execute(t *testing.T) { diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 16bc019a..db073e68 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -12,19 +12,23 @@ import ( "syscall" "time" + "github.com/go-pkgz/auth/token" + bolt "github.com/coreos/bbolt" + "github.com/pkg/errors" + + "github.com/go-pkgz/auth" + "github.com/go-pkgz/auth/avatar" + "github.com/go-pkgz/auth/provider" "github.com/go-pkgz/mongo" "github.com/go-pkgz/rest/cache" - "github.com/pkg/errors" "github.com/umputun/remark/backend/app/migrator" "github.com/umputun/remark/backend/app/notify" "github.com/umputun/remark/backend/app/rest/api" - "github.com/umputun/remark/backend/app/rest/auth" "github.com/umputun/remark/backend/app/rest/proxy" "github.com/umputun/remark/backend/app/store" "github.com/umputun/remark/backend/app/store/admin" - "github.com/umputun/remark/backend/app/store/avatar" "github.com/umputun/remark/backend/app/store/engine" "github.com/umputun/remark/backend/app/store/service" ) @@ -148,7 +152,7 @@ type serverApp struct { restSrv *api.Rest migratorSrv *api.Migrator exporter migrator.Exporter - devAuth *auth.DevAuthServer + devAuth *provider.DevAuthServer dataService *service.DataStore avatarStore avatar.Store notifyService *notify.Service @@ -217,18 +221,38 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { return nil, errors.Wrap(err, "failed to make cache") } - // token TTL is 5 minutes, inactivity interval 7+ days by default - jwtService := auth.NewJWT(adminStore, strings.HasPrefix(s.RemarkURL, "https://"), s.Auth.TTL.JWT, s.Auth.TTL.Cookie) - avatarStore, err := s.makeAvatarStore() if err != nil { return nil, errors.Wrap(err, "failed to make avatar store") } - avatarProxy := &proxy.Avatar{ - Store: avatarStore, - RoutePath: "/api/v1/avatar", - RemarkURL: strings.TrimSuffix(s.RemarkURL, "/"), - } + + authenticator := auth.NewService(auth.Opts{ + TokenDuration: s.Auth.TTL.JWT, + CookieDuration: s.Auth.TTL.Cookie, + SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"), + SecretReader: token.SecretFunc(func(id string) (string, error) { + return adminStore.Key(id) + }), + ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { + c.User.SetAdmin(dataService.IsAdmin(c.Audience, c.User.ID)) + return c + }), + DevPasswd: s.DevPasswd, + //Validator: dataService, + AvatarStore: avatarStore, + AvatarResizeLimit: s.Avatar.RszLmt, + AvatarRoutePath: "/api/v1/avatar", + }) + s.addAuthProviders(authenticator) + + // token TTL is 5 minutes, inactivity interval 7+ days by default + // jwtService := auth.NewJWT(adminStore, strings.HasPrefix(s.RemarkURL, "https://"), s.Auth.TTL.JWT, s.Auth.TTL.Cookie) + + // avatarProxy := &proxy.Avatar{ + // Store: avatarStore, + // RoutePath: "/api/v1/avatar", + // RemarkURL: strings.TrimSuffix(s.RemarkURL, "/"), + // } exporter := &migrator.Native{DataStore: dataService} @@ -247,7 +271,6 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { notifyService = notify.NopService // disable notifier } - authProviders := s.makeAuthProviders(jwtService, avatarProxy, dataService) imgProxy := &proxy.Image{Enabled: s.ImageProxy, RoutePath: "/api/v1/img", RemarkURL: s.RemarkURL} commentFormatter := store.NewCommentFormatter(imgProxy) @@ -263,27 +286,24 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { RemarkURL: s.RemarkURL, ImageProxy: imgProxy, CommentFormatter: commentFormatter, - AvatarProxy: avatarProxy, Migrator: migr, ReadOnlyAge: s.ReadOnlyAge, SharedSecret: s.SharedSecret, - Authenticator: auth.Authenticator{ - JWTService: jwtService, - KeyStore: adminStore, - Providers: authProviders, - DevPasswd: s.DevPasswd, - PermissionChecker: dataService, - }, - Cache: loadingCache, - NotifyService: notifyService, - SSLConfig: sslConfig, + Authenticator: *authenticator, + Cache: loadingCache, + NotifyService: notifyService, + SSLConfig: sslConfig, } srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore - var devAuth *auth.DevAuthServer + var devAuth provider.DevAuthServer if s.Auth.Dev { - devAuth = &auth.DevAuthServer{Provider: authProviders[len(authProviders)-1]} + p, err := authenticator.Provider("dev") + if err != nil { + return nil, errors.Wrap(err, "can't pick dev provider") + } + devAuth = provider.DevAuthServer{Provider: p} } return &serverApp{ @@ -291,7 +311,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { restSrv: srv, migratorSrv: migr, exporter: exporter, - devAuth: devAuth, + devAuth: &devAuth, dataService: dataService, avatarStore: avatarStore, notifyService: notifyService, @@ -385,19 +405,19 @@ func (s *ServerCommand) makeAvatarStore() (avatar.Store, error) { if err := makeDirs(s.Avatar.FS.Path); err != nil { return nil, err } - return avatar.NewLocalFS(s.Avatar.FS.Path, s.Avatar.RszLmt), nil + return avatar.NewLocalFS(s.Avatar.FS.Path), nil case "mongo": mgServer, err := s.makeMongo() if err != nil { return nil, errors.Wrap(err, "failed to create mongo server") } conn := mongo.NewConnection(mgServer, s.Mongo.DB, "") - return avatar.NewGridFS(conn, s.Avatar.RszLmt), nil + return avatar.NewGridFS(conn), nil case "bolt": if err := makeDirs(path.Dir(s.Avatar.Bolt.File)); err != nil { return nil, err } - return avatar.NewBoltDB(s.Avatar.Bolt.File, bolt.Options{}, s.Avatar.RszLmt) + return avatar.NewBoltDB(s.Avatar.Bolt.File, bolt.Options{}) } return nil, errors.Errorf("unsupported avatar store type %s", s.Avatar.Type) } @@ -452,40 +472,33 @@ func (s *ServerCommand) makeMongo() (result *mongo.Server, err error) { return mongo.NewServerWithURL(s.Mongo.URL, 10*time.Second) } -func (s *ServerCommand) makeAuthProviders(jwt *auth.JWT, ap *proxy.Avatar, ds *service.DataStore) []auth.Provider { +func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) { - makeParams := func(cid, secret string) auth.Params { - return auth.Params{ - JwtService: jwt, - AvatarProxy: ap, - RemarkURL: s.RemarkURL, - Cid: cid, - Csecret: secret, - PermissionChecker: ds, - } - } - - providers := []auth.Provider{} + providers := 0 if s.Auth.Google.CID != "" && s.Auth.Google.CSEC != "" { - providers = append(providers, auth.NewGoogle(makeParams(s.Auth.Google.CID, s.Auth.Google.CSEC))) + authenticator.AddProvider("google", s.Auth.Google.CID, s.Auth.Google.CSEC) + providers++ } if s.Auth.Github.CID != "" && s.Auth.Github.CSEC != "" { - providers = append(providers, auth.NewGithub(makeParams(s.Auth.Github.CID, s.Auth.Github.CSEC))) + authenticator.AddProvider("github", s.Auth.Github.CID, s.Auth.Github.CSEC) + providers++ } if s.Auth.Facebook.CID != "" && s.Auth.Facebook.CSEC != "" { - providers = append(providers, auth.NewFacebook(makeParams(s.Auth.Facebook.CID, s.Auth.Facebook.CSEC))) + authenticator.AddProvider("facebook", s.Auth.Facebook.CID, s.Auth.Facebook.CSEC) + providers++ } if s.Auth.Yandex.CID != "" && s.Auth.Yandex.CSEC != "" { - providers = append(providers, auth.NewYandex(makeParams(s.Auth.Yandex.CID, s.Auth.Yandex.CSEC))) + authenticator.AddProvider("yandex", s.Auth.Yandex.CID, s.Auth.Yandex.CSEC) + providers++ } if s.Auth.Dev { - providers = append(providers, auth.NewDev(makeParams("", ""))) + authenticator.AddProvider("dev", "", "") + providers++ } - if len(providers) == 0 { + if providers == 0 { log.Printf("[WARN] no auth providers defined") } - return providers } func (s *ServerCommand) makeNotify(dataStore *service.DataStore) (*notify.Service, error) { diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 82284d54..9fcfb546 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -39,8 +39,12 @@ func TestServerApp(t *testing.T) { assert.Equal(t, "pong", string(body)) // add comment - resp, err = http.Post("http://dev:password@localhost:18080/api/v1/comment", "json", + client := http.Client{Timeout: 5 * time.Second} + req, err := http.NewRequest("POST", "http://localhost:18080/api/v1/comment", strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`)) + req.SetBasicAuth("dev", "password") + require.Nil(t, err) + resp, err = client.Do(req) require.Nil(t, err) assert.Equal(t, http.StatusCreated, resp.StatusCode) body, _ = ioutil.ReadAll(resp.Body) @@ -62,8 +66,8 @@ func TestServerApp_DevMode(t *testing.T) { go func() { _ = app.run(ctx) }() time.Sleep(100 * time.Millisecond) // let server start - assert.Equal(t, 4+1, len(app.restSrv.Authenticator.Providers), "extra auth provider") - assert.Equal(t, "dev", app.restSrv.Authenticator.Providers[4].Name, "dev auth provider") + assert.Equal(t, 4+1, len(app.restSrv.Authenticator.Providers()), "extra auth provider") + assert.Equal(t, "dev", app.restSrv.Authenticator.Providers()[4].Name, "dev auth provider") // send ping resp, err := http.Get("http://localhost:18085/api/v1/ping") require.Nil(t, err) diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/.gitlab-ci.yml b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/.gitlab-ci.yml new file mode 100644 index 00000000..383db2ea --- /dev/null +++ b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/.gitlab-ci.yml @@ -0,0 +1,25 @@ +image: docker.tkginternal.com/system/buildimage-go:1.1-master + +stages: + - build + +variables: + PROJ: "repeater" + GROUP: "commons/pkg" + PKG: "git.tkginternal.com" + +build_app: + stage: build + script: + - mkdir -p /go/src/$PKG/$GROUP && cp -fR $CI_PROJECT_DIR /go/src/$PKG/$GROUP/$PROJ + - mkdir -p $CI_PROJECT_DIR/target && ln -s $CI_PROJECT_DIR/target /go/src/$PKG/$GROUP/$PROJ/target + - cd /go/src/$PKG/$GROUP/$PROJ + - go get -v && go get -t $(go list -e ./... | grep -v vendor) && go test -v $(go list -e ./... | grep -v vendor) + - gometalinter --exclude=test --vendored-linters --disable-all --vendor --enable=vet --enable=vetshadow --enable=golint --enable=ineffassign --enable=goconst --enable=gas --enable=staticcheck --enable=errcheck --deadline=120s ./... + - go build -ldflags "-X main.revision=$REV" -o $CI_PROJECT_DIR/target/$PROJ + - cd /go/src/$PKG/$GROUP/$PROJ && /script/coverage.sh + tags: + - gobuilder + artifacts: + paths: + - target/ \ No newline at end of file diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/README.md b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/README.md new file mode 100644 index 00000000..03ef147d --- /dev/null +++ b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/README.md @@ -0,0 +1,41 @@ +# Repeater + +[![pipeline status](https://git.tkginternal.com/commons/pkg/repeater/badges/master/pipeline.svg)](https://git.tkginternal.com/commons/pkg/repeater/commits/master) +[![coverage report](https://git.tkginternal.com/commons/pkg/repeater/badges/master/coverage.svg)](https://git.tkginternal.com/commons/pkg/repeater/commits/master) +[![GoDoc](https://godoc.tkginternal.com/godoc.svg)](https://godoc.tkginternal.com/pkg/git.tkginternal.com/commons/pkg/repeater/) + + +Package repeater call fun till it returns no error, up to repeat some number of iterations and delays defined by strategy. +Repeats number and delays defined by strategy.Interface. Terminates immediately on err from provided, optional list of critical errors + +## Install and update + +`go get -u git.tkginternal.com/commons/pkg/repeater` + +## How to use + +New Repeater created by `New(strtg strategy.Interface)` or shortcut for defaults - `NewDefault(repeats int, delay time.Duration) *Repeater`. + +To activate use `Do` method. Do repeats fun till no error. Predefined (optional) errors terminate immediately + +`func (r Repeater) Do(fun func() error, errors ...error) (err error)` + +### Repeating strategy + +User can provide his own strategy implementing this interface: + +```go +type Interface interface { + Start(ctx context.Context) chan struct{} +} +``` + +Returned channels used as "ticks", i.e. for each repeat (or initial) operation one read from this channel needed. Closing this channel indicates "done with retries". This is pretty much the same idea as `time.Timer` or `time.Tick` implements. Note - the first (technically not-repeated-yet) call won't happen **until something sent to the channel**. This is why typical strategy sends first "tick" prior to first wait/sleep. + +Three mist common strategies provided by package and ready to use: +1. **Fixed delay**, up to max number of attempts - `NewFixedDelay(repeats int, delay time.Duration)`. +This is default strategy used by `repeater.NewDefault` constructor +2. **BackOff** with jitter provides exponential backoff. It starts from 100ms interval and goes in steps with `last * math.Pow(factor, attempt)`. Optional jitter randomizes intervals a little bit. The strategy created by `NewBackoff(repeats int, factor float64, jitter bool)`. _Factor = 1 effectively makes this strategy fixed with 100ms delay._ + +3. **Once** strategy does not do any repeats and mainly useful for tests - `NewOnce()` + diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/repeater.go b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/repeater.go new file mode 100644 index 00000000..a95e8219 --- /dev/null +++ b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/repeater.go @@ -0,0 +1,60 @@ +// Package repeater call fun till it returns no error, up to repeat some number of iterations and delays defined by strategy. +// Repeats number and delays defined by strategy.Interface. Terminates immediately on err from +// provided, optional list of critical errors +package repeater + +import ( + "context" + "time" + + "git.tkginternal.com/commons/pkg/repeater/strategy" +) + +// Repeater is the main object, should be made by New or NewDefault, embeds strategy +type Repeater struct { + strategy.Interface +} + +// New repeater with a given strategy. If strategy=nil initializes with FixedDelay 5sec, 10 times. +func New(strtg strategy.Interface) *Repeater { + if strtg == nil { + strtg = strategy.NewFixedDelay(10, time.Second*5) + } + result := Repeater{Interface: strtg} + return &result +} + +// NewDefault makes repeater with FixedDelay strategy +func NewDefault(repeats int, delay time.Duration) *Repeater { + return New(strategy.NewFixedDelay(repeats, delay)) +} + +// Do repeats fun till no error. Predefined (optional) errors terminate immediately +func (r Repeater) Do(fun func() error, errors ...error) (err error) { + + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() // ensure strategy's channel termination + + inErrors := func(err error) bool { + for _, e := range errors { + if e == err { + return true + } + } + return false + } + + ch := r.Start(ctx) // channel of ticks-like events provided by strategy + + // closed channel indicates completion or early termination, set by strategy + for range ch { + + if err = fun(); err == nil { + return nil + } + if err != nil && inErrors(err) { //terminate on critical error from provided list + return err + } + } + return err +} diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/backoff.go b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/backoff.go new file mode 100644 index 00000000..c660c95c --- /dev/null +++ b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/backoff.go @@ -0,0 +1,56 @@ +package strategy + +import ( + "context" + "math" + "math/rand" + "time" +) + +// Backoff implements Interface for exponential-backoff +// it starts from 100ms and goes in steps with last * math.Pow(factor, attempt) +// optional jitter randomize intervals a little bit. +type Backoff struct { + repeats int + factor float64 + jitter bool +} + +// NewBackoff makes Backoff strategy with given factor and optional jitter +func NewBackoff(repeats int, factor float64, jitter bool) Interface { + if repeats == 0 { + repeats = 1 + } + if factor <= 0 { + factor = 1 + } + result := Backoff{repeats: repeats, factor: factor, jitter: jitter} + return &result +} + +// Start returns channel, similar to time.Timer +// then publishing signals to channel ch for retries attempt. Closed ch indicates "done" event +// consumer (repeater) should stop it explicitly after completion +func (b *Backoff) Start(ctx context.Context) (ch chan struct{}) { + ch = make(chan struct{}) + go func() { + defer close(ch) + rnd := rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) + minDelay := 100 * time.Millisecond // starts 100ms + for i := 0; i < b.repeats; i++ { + select { + case <-ctx.Done(): + return + default: + ch <- struct{}{} + delay := float64(minDelay) * math.Pow(b.factor, float64(i)) + if b.jitter { + delay = rnd.Float64()*(float64(2*minDelay)) + (delay - float64(minDelay)) + } + // log.Printf("%v", time.Duration(delay)) + time.Sleep(time.Duration(delay)) + } + } + }() + return ch +} diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/fixed.go b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/fixed.go new file mode 100644 index 00000000..d9c30ecb --- /dev/null +++ b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/fixed.go @@ -0,0 +1,41 @@ +package strategy + +import ( + "context" + "time" +) + +// FixedDelay implements Interface for fixed intervals up to max repeats +type FixedDelay struct { + repeats int + delay time.Duration +} + +// NewFixedDelay makes a Interface +func NewFixedDelay(repeats int, delay time.Duration) Interface { + if repeats == 0 { + repeats = 1 + } + result := FixedDelay{repeats: repeats, delay: delay} + return &result +} + +// Start returns channel, similar to time.Timer +// then publishing signals to channel ch for retries attempt. +// can be terminated (canceled) via context. +func (s *FixedDelay) Start(ctx context.Context) (ch chan struct{}) { + ch = make(chan struct{}) + go func() { + defer close(ch) + for i := 0; i < s.repeats; i++ { + select { + case <-ctx.Done(): + return + default: + ch <- struct{}{} + time.Sleep(s.delay) + } + } + }() + return ch +} diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/strategy.go b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/strategy.go new file mode 100644 index 00000000..8d6a2a69 --- /dev/null +++ b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/strategy.go @@ -0,0 +1,28 @@ +// Package strategy defines repeater's strategy and implements some. Strategy result +// is channel acting like time.Timer ot time.Tick +package strategy + +import "context" + +// Interface for repeats strategy. Returns channel with ticks +type Interface interface { + Start(ctx context.Context) chan struct{} +} + +// Once strategy eliminate repeats and makes a single try only +type Once struct{} + +// NewOnce makes no-repeat strategy +func NewOnce() Interface { + return &Once{} +} + +// Start returns closed channel with a single element to prevent any repeats +func (s *Once) Start(ctx context.Context) (ch chan struct{}) { + ch = make(chan struct{}) + go func() { + ch <- struct{}{} + close(ch) + }() + return ch +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/.gitignore b/backend/vendor/github.com/dgrijalva/jwt-go/.gitignore new file mode 100644 index 00000000..80bed650 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +bin + + diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/.travis.yml b/backend/vendor/github.com/dgrijalva/jwt-go/.travis.yml new file mode 100644 index 00000000..1027f56c --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/.travis.yml @@ -0,0 +1,13 @@ +language: go + +script: + - go vet ./... + - go test -v ./... + +go: + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - tip diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/LICENSE b/backend/vendor/github.com/dgrijalva/jwt-go/LICENSE new file mode 100644 index 00000000..df83a9c2 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/LICENSE @@ -0,0 +1,8 @@ +Copyright (c) 2012 Dave Grijalva + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md b/backend/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md new file mode 100644 index 00000000..7fc1f793 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md @@ -0,0 +1,97 @@ +## Migration Guide from v2 -> v3 + +Version 3 adds several new, frequently requested features. To do so, it introduces a few breaking changes. We've worked to keep these as minimal as possible. This guide explains the breaking changes and how you can quickly update your code. + +### `Token.Claims` is now an interface type + +The most requested feature from the 2.0 verison of this library was the ability to provide a custom type to the JSON parser for claims. This was implemented by introducing a new interface, `Claims`, to replace `map[string]interface{}`. We also included two concrete implementations of `Claims`: `MapClaims` and `StandardClaims`. + +`MapClaims` is an alias for `map[string]interface{}` with built in validation behavior. It is the default claims type when using `Parse`. The usage is unchanged except you must type cast the claims property. + +The old example for parsing a token looked like this.. + +```go + if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil { + fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) + } +``` + +is now directly mapped to... + +```go + if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil { + claims := token.Claims.(jwt.MapClaims) + fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"]) + } +``` + +`StandardClaims` is designed to be embedded in your custom type. You can supply a custom claims type with the new `ParseWithClaims` function. Here's an example of using a custom claims type. + +```go + type MyCustomClaims struct { + User string + *StandardClaims + } + + if token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, keyLookupFunc); err == nil { + claims := token.Claims.(*MyCustomClaims) + fmt.Printf("Token for user %v expires %v", claims.User, claims.StandardClaims.ExpiresAt) + } +``` + +### `ParseFromRequest` has been moved + +To keep this library focused on the tokens without becoming overburdened with complex request processing logic, `ParseFromRequest` and its new companion `ParseFromRequestWithClaims` have been moved to a subpackage, `request`. The method signatues have also been augmented to receive a new argument: `Extractor`. + +`Extractors` do the work of picking the token string out of a request. The interface is simple and composable. + +This simple parsing example: + +```go + if token, err := jwt.ParseFromRequest(tokenString, req, keyLookupFunc); err == nil { + fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) + } +``` + +is directly mapped to: + +```go + if token, err := request.ParseFromRequest(req, request.OAuth2Extractor, keyLookupFunc); err == nil { + claims := token.Claims.(jwt.MapClaims) + fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"]) + } +``` + +There are several concrete `Extractor` types provided for your convenience: + +* `HeaderExtractor` will search a list of headers until one contains content. +* `ArgumentExtractor` will search a list of keys in request query and form arguments until one contains content. +* `MultiExtractor` will try a list of `Extractors` in order until one returns content. +* `AuthorizationHeaderExtractor` will look in the `Authorization` header for a `Bearer` token. +* `OAuth2Extractor` searches the places an OAuth2 token would be specified (per the spec): `Authorization` header and `access_token` argument +* `PostExtractionFilter` wraps an `Extractor`, allowing you to process the content before it's parsed. A simple example is stripping the `Bearer ` text from a header + + +### RSA signing methods no longer accept `[]byte` keys + +Due to a [critical vulnerability](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/), we've decided the convenience of accepting `[]byte` instead of `rsa.PublicKey` or `rsa.PrivateKey` isn't worth the risk of misuse. + +To replace this behavior, we've added two helper methods: `ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error)` and `ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error)`. These are just simple helpers for unpacking PEM encoded PKCS1 and PKCS8 keys. If your keys are encoded any other way, all you need to do is convert them to the `crypto/rsa` package's types. + +```go + func keyLookupFunc(*Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + // Look up key + key, err := lookupPublicKey(token.Header["kid"]) + if err != nil { + return nil, err + } + + // Unpack key from PEM encoded PKCS8 + return jwt.ParseRSAPublicKeyFromPEM(key) + } +``` diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/README.md b/backend/vendor/github.com/dgrijalva/jwt-go/README.md new file mode 100644 index 00000000..d358d881 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/README.md @@ -0,0 +1,100 @@ +# jwt-go + +[![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go) +[![GoDoc](https://godoc.org/github.com/dgrijalva/jwt-go?status.svg)](https://godoc.org/github.com/dgrijalva/jwt-go) + +A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) + +**NEW VERSION COMING:** There have been a lot of improvements suggested since the version 3.0.0 released in 2016. I'm working now on cutting two different releases: 3.2.0 will contain any non-breaking changes or enhancements. 4.0.0 will follow shortly which will include breaking changes. See the 4.0.0 milestone to get an idea of what's coming. If you have other ideas, or would like to participate in 4.0.0, now's the time. If you depend on this library and don't want to be interrupted, I recommend you use your dependency mangement tool to pin to version 3. + +**SECURITY NOTICE:** Some older versions of Go have a security issue in the cryotp/elliptic. Recommendation is to upgrade to at least 1.8.3. See issue #216 for more detail. + +**SECURITY NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided. + +## What the heck is a JWT? + +JWT.io has [a great introduction](https://jwt.io/introduction) to JSON Web Tokens. + +In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way. + +The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used. + +The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [the RFC](http://self-issued.info/docs/draft-jones-json-web-token.html) for information about reserved keys and the proper way to add your own. + +## What's in the box? + +This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are HMAC SHA, RSA, RSA-PSS, and ECDSA, though hooks are present for adding your own. + +## Examples + +See [the project documentation](https://godoc.org/github.com/dgrijalva/jwt-go) for examples of usage: + +* [Simple example of parsing and validating a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac) +* [Simple example of building and signing a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-New--Hmac) +* [Directory of Examples](https://godoc.org/github.com/dgrijalva/jwt-go#pkg-examples) + +## Extensions + +This library publishes all the necessary components for adding your own signing methods. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod`. + +Here's an example of an extension that integrates with the Google App Engine signing tools: https://github.com/someone1/gcp-jwt-go + +## Compliance + +This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences: + +* In order to protect against accidental use of [Unsecured JWTs](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#UnsecuredJWT), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key. + +## Project Status & Versioning + +This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason). + +This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases). + +While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v3`. It will do the right thing WRT semantic versioning. + +**BREAKING CHANGES:*** +* Version 3.0.0 includes _a lot_ of changes from the 2.x line, including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code. + +## Usage Tips + +### Signing vs Encryption + +A token is simply a JSON object that is signed by its author. this tells you exactly two things about the data: + +* The author of the token was in the possession of the signing secret +* The data has not been modified since it was signed + +It's important to know that JWT does not provide encryption, which means anyone who has access to the token can read its contents. If you need to protect (encrypt) the data, there is a companion spec, `JWE`, that provides this functionality. JWE is currently outside the scope of this library. + +### Choosing a Signing Method + +There are several signing methods available, and you should probably take the time to learn about the various options before choosing one. The principal design decision is most likely going to be symmetric vs asymmetric. + +Symmetric signing methods, such as HSA, use only a single secret. This is probably the simplest signing method to use since any `[]byte` can be used as a valid secret. They are also slightly computationally faster to use, though this rarely is enough to matter. Symmetric signing methods work the best when both producers and consumers of tokens are trusted, or even the same system. Since the same secret is used to both sign and validate tokens, you can't easily distribute the key for validation. + +Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification. + +### Signing Methods and Key Types + +Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones: + +* The [HMAC signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation +* The [RSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation +* The [ECDSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation + +### JWT and OAuth + +It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication. + +Without going too far down the rabbit hole, here's a description of the interaction of these technologies: + +* OAuth is a protocol for allowing an identity provider to be separate from the service a user is logging in to. For example, whenever you use Facebook to log into a different service (Yelp, Spotify, etc), you are using OAuth. +* OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token. +* Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL. + +## More + +Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go). + +The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation. diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md b/backend/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md new file mode 100644 index 00000000..63702983 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md @@ -0,0 +1,118 @@ +## `jwt-go` Version History + +#### 3.2.0 + +* Added method `ParseUnverified` to allow users to split up the tasks of parsing and validation +* HMAC signing method returns `ErrInvalidKeyType` instead of `ErrInvalidKey` where appropriate +* Added options to `request.ParseFromRequest`, which allows for an arbitrary list of modifiers to parsing behavior. Initial set include `WithClaims` and `WithParser`. Existing usage of this function will continue to work as before. +* Deprecated `ParseFromRequestWithClaims` to simplify API in the future. + +#### 3.1.0 + +* Improvements to `jwt` command line tool +* Added `SkipClaimsValidation` option to `Parser` +* Documentation updates + +#### 3.0.0 + +* **Compatibility Breaking Changes**: See MIGRATION_GUIDE.md for tips on updating your code + * Dropped support for `[]byte` keys when using RSA signing methods. This convenience feature could contribute to security vulnerabilities involving mismatched key types with signing methods. + * `ParseFromRequest` has been moved to `request` subpackage and usage has changed + * The `Claims` property on `Token` is now type `Claims` instead of `map[string]interface{}`. The default value is type `MapClaims`, which is an alias to `map[string]interface{}`. This makes it possible to use a custom type when decoding claims. +* Other Additions and Changes + * Added `Claims` interface type to allow users to decode the claims into a custom type + * Added `ParseWithClaims`, which takes a third argument of type `Claims`. Use this function instead of `Parse` if you have a custom type you'd like to decode into. + * Dramatically improved the functionality and flexibility of `ParseFromRequest`, which is now in the `request` subpackage + * Added `ParseFromRequestWithClaims` which is the `FromRequest` equivalent of `ParseWithClaims` + * Added new interface type `Extractor`, which is used for extracting JWT strings from http requests. Used with `ParseFromRequest` and `ParseFromRequestWithClaims`. + * Added several new, more specific, validation errors to error type bitmask + * Moved examples from README to executable example files + * Signing method registry is now thread safe + * Added new property to `ValidationError`, which contains the raw error returned by calls made by parse/verify (such as those returned by keyfunc or json parser) + +#### 2.7.0 + +This will likely be the last backwards compatible release before 3.0.0, excluding essential bug fixes. + +* Added new option `-show` to the `jwt` command that will just output the decoded token without verifying +* Error text for expired tokens includes how long it's been expired +* Fixed incorrect error returned from `ParseRSAPublicKeyFromPEM` +* Documentation updates + +#### 2.6.0 + +* Exposed inner error within ValidationError +* Fixed validation errors when using UseJSONNumber flag +* Added several unit tests + +#### 2.5.0 + +* Added support for signing method none. You shouldn't use this. The API tries to make this clear. +* Updated/fixed some documentation +* Added more helpful error message when trying to parse tokens that begin with `BEARER ` + +#### 2.4.0 + +* Added new type, Parser, to allow for configuration of various parsing parameters + * You can now specify a list of valid signing methods. Anything outside this set will be rejected. + * You can now opt to use the `json.Number` type instead of `float64` when parsing token JSON +* Added support for [Travis CI](https://travis-ci.org/dgrijalva/jwt-go) +* Fixed some bugs with ECDSA parsing + +#### 2.3.0 + +* Added support for ECDSA signing methods +* Added support for RSA PSS signing methods (requires go v1.4) + +#### 2.2.0 + +* Gracefully handle a `nil` `Keyfunc` being passed to `Parse`. Result will now be the parsed token and an error, instead of a panic. + +#### 2.1.0 + +Backwards compatible API change that was missed in 2.0.0. + +* The `SignedString` method on `Token` now takes `interface{}` instead of `[]byte` + +#### 2.0.0 + +There were two major reasons for breaking backwards compatibility with this update. The first was a refactor required to expand the width of the RSA and HMAC-SHA signing implementations. There will likely be no required code changes to support this change. + +The second update, while unfortunately requiring a small change in integration, is required to open up this library to other signing methods. Not all keys used for all signing methods have a single standard on-disk representation. Requiring `[]byte` as the type for all keys proved too limiting. Additionally, this implementation allows for pre-parsed tokens to be reused, which might matter in an application that parses a high volume of tokens with a small set of keys. Backwards compatibilty has been maintained for passing `[]byte` to the RSA signing methods, but they will also accept `*rsa.PublicKey` and `*rsa.PrivateKey`. + +It is likely the only integration change required here will be to change `func(t *jwt.Token) ([]byte, error)` to `func(t *jwt.Token) (interface{}, error)` when calling `Parse`. + +* **Compatibility Breaking Changes** + * `SigningMethodHS256` is now `*SigningMethodHMAC` instead of `type struct` + * `SigningMethodRS256` is now `*SigningMethodRSA` instead of `type struct` + * `KeyFunc` now returns `interface{}` instead of `[]byte` + * `SigningMethod.Sign` now takes `interface{}` instead of `[]byte` for the key + * `SigningMethod.Verify` now takes `interface{}` instead of `[]byte` for the key +* Renamed type `SigningMethodHS256` to `SigningMethodHMAC`. Specific sizes are now just instances of this type. + * Added public package global `SigningMethodHS256` + * Added public package global `SigningMethodHS384` + * Added public package global `SigningMethodHS512` +* Renamed type `SigningMethodRS256` to `SigningMethodRSA`. Specific sizes are now just instances of this type. + * Added public package global `SigningMethodRS256` + * Added public package global `SigningMethodRS384` + * Added public package global `SigningMethodRS512` +* Moved sample private key for HMAC tests from an inline value to a file on disk. Value is unchanged. +* Refactored the RSA implementation to be easier to read +* Exposed helper methods `ParseRSAPrivateKeyFromPEM` and `ParseRSAPublicKeyFromPEM` + +#### 1.0.2 + +* Fixed bug in parsing public keys from certificates +* Added more tests around the parsing of keys for RS256 +* Code refactoring in RS256 implementation. No functional changes + +#### 1.0.1 + +* Fixed panic if RS256 signing method was passed an invalid key + +#### 1.0.0 + +* First versioned release +* API stabilized +* Supports creating, signing, parsing, and validating JWT tokens +* Supports RS256 and HS256 signing methods \ No newline at end of file diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/claims.go b/backend/vendor/github.com/dgrijalva/jwt-go/claims.go new file mode 100644 index 00000000..f0228f02 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/claims.go @@ -0,0 +1,134 @@ +package jwt + +import ( + "crypto/subtle" + "fmt" + "time" +) + +// For a type to be a Claims object, it must just have a Valid method that determines +// if the token is invalid for any supported reason +type Claims interface { + Valid() error +} + +// Structured version of Claims Section, as referenced at +// https://tools.ietf.org/html/rfc7519#section-4.1 +// See examples for how to use this with your own claim types +type StandardClaims struct { + Audience string `json:"aud,omitempty"` + ExpiresAt int64 `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` +} + +// Validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (c StandardClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + // The claims below are optional, by default, so if they are set to the + // default value in Go, let's not fail the verification for them. + if c.VerifyExpiresAt(now, false) == false { + delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) + vErr.Inner = fmt.Errorf("token is expired by %v", delta) + vErr.Errors |= ValidationErrorExpired + } + + if c.VerifyIssuedAt(now, false) == false { + vErr.Inner = fmt.Errorf("Token used before issued") + vErr.Errors |= ValidationErrorIssuedAt + } + + if c.VerifyNotBefore(now, false) == false { + vErr.Inner = fmt.Errorf("token is not valid yet") + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { + return verifyAud(c.Audience, cmp, req) +} + +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { + return verifyExp(c.ExpiresAt, cmp, req) +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { + return verifyIat(c.IssuedAt, cmp, req) +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { + return verifyIss(c.Issuer, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { + return verifyNbf(c.NotBefore, cmp, req) +} + +// ----- helpers + +func verifyAud(aud string, cmp string, required bool) bool { + if aud == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { + return true + } else { + return false + } +} + +func verifyExp(exp int64, now int64, required bool) bool { + if exp == 0 { + return !required + } + return now <= exp +} + +func verifyIat(iat int64, now int64, required bool) bool { + if iat == 0 { + return !required + } + return now >= iat +} + +func verifyIss(iss string, cmp string, required bool) bool { + if iss == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { + return true + } else { + return false + } +} + +func verifyNbf(nbf int64, now int64, required bool) bool { + if nbf == 0 { + return !required + } + return now >= nbf +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/doc.go b/backend/vendor/github.com/dgrijalva/jwt-go/doc.go new file mode 100644 index 00000000..a86dc1a3 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/doc.go @@ -0,0 +1,4 @@ +// Package jwt is a Go implementation of JSON Web Tokens: http://self-issued.info/docs/draft-jones-json-web-token.html +// +// See README.md for more info. +package jwt diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa.go b/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa.go new file mode 100644 index 00000000..f9773812 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa.go @@ -0,0 +1,148 @@ +package jwt + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "errors" + "math/big" +) + +var ( + // Sadly this is missing from crypto/ecdsa compared to crypto/rsa + ErrECDSAVerification = errors.New("crypto/ecdsa: verification error") +) + +// Implements the ECDSA family of signing methods signing methods +// Expects *ecdsa.PrivateKey for signing and *ecdsa.PublicKey for verification +type SigningMethodECDSA struct { + Name string + Hash crypto.Hash + KeySize int + CurveBits int +} + +// Specific instances for EC256 and company +var ( + SigningMethodES256 *SigningMethodECDSA + SigningMethodES384 *SigningMethodECDSA + SigningMethodES512 *SigningMethodECDSA +) + +func init() { + // ES256 + SigningMethodES256 = &SigningMethodECDSA{"ES256", crypto.SHA256, 32, 256} + RegisterSigningMethod(SigningMethodES256.Alg(), func() SigningMethod { + return SigningMethodES256 + }) + + // ES384 + SigningMethodES384 = &SigningMethodECDSA{"ES384", crypto.SHA384, 48, 384} + RegisterSigningMethod(SigningMethodES384.Alg(), func() SigningMethod { + return SigningMethodES384 + }) + + // ES512 + SigningMethodES512 = &SigningMethodECDSA{"ES512", crypto.SHA512, 66, 521} + RegisterSigningMethod(SigningMethodES512.Alg(), func() SigningMethod { + return SigningMethodES512 + }) +} + +func (m *SigningMethodECDSA) Alg() string { + return m.Name +} + +// Implements the Verify method from SigningMethod +// For this verify method, key must be an ecdsa.PublicKey struct +func (m *SigningMethodECDSA) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + // Get the key + var ecdsaKey *ecdsa.PublicKey + switch k := key.(type) { + case *ecdsa.PublicKey: + ecdsaKey = k + default: + return ErrInvalidKeyType + } + + if len(sig) != 2*m.KeySize { + return ErrECDSAVerification + } + + r := big.NewInt(0).SetBytes(sig[:m.KeySize]) + s := big.NewInt(0).SetBytes(sig[m.KeySize:]) + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Verify the signature + if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), r, s); verifystatus == true { + return nil + } else { + return ErrECDSAVerification + } +} + +// Implements the Sign method from SigningMethod +// For this signing method, key must be an ecdsa.PrivateKey struct +func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string, error) { + // Get the key + var ecdsaKey *ecdsa.PrivateKey + switch k := key.(type) { + case *ecdsa.PrivateKey: + ecdsaKey = k + default: + return "", ErrInvalidKeyType + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return r, s + if r, s, err := ecdsa.Sign(rand.Reader, ecdsaKey, hasher.Sum(nil)); err == nil { + curveBits := ecdsaKey.Curve.Params().BitSize + + if m.CurveBits != curveBits { + return "", ErrInvalidKey + } + + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes += 1 + } + + // We serialize the outpus (r and s) into big-endian byte arrays and pad + // them with zeros on the left to make sure the sizes work out. Both arrays + // must be keyBytes long, and the output must be 2*keyBytes long. + rBytes := r.Bytes() + rBytesPadded := make([]byte, keyBytes) + copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) + + sBytes := s.Bytes() + sBytesPadded := make([]byte, keyBytes) + copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) + + out := append(rBytesPadded, sBytesPadded...) + + return EncodeSegment(out), nil + } else { + return "", err + } +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go b/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go new file mode 100644 index 00000000..d19624b7 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go @@ -0,0 +1,67 @@ +package jwt + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrNotECPublicKey = errors.New("Key is not a valid ECDSA public key") + ErrNotECPrivateKey = errors.New("Key is not a valid ECDSA private key") +) + +// Parse PEM encoded Elliptic Curve Private Key Structure +func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { + return nil, err + } + + var pkey *ecdsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { + return nil, ErrNotECPrivateKey + } + + return pkey, nil +} + +// Parse PEM encoded PKCS1 or PKCS8 public key +func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + var pkey *ecdsa.PublicKey + var ok bool + if pkey, ok = parsedKey.(*ecdsa.PublicKey); !ok { + return nil, ErrNotECPublicKey + } + + return pkey, nil +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/errors.go b/backend/vendor/github.com/dgrijalva/jwt-go/errors.go new file mode 100644 index 00000000..1c93024a --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/errors.go @@ -0,0 +1,59 @@ +package jwt + +import ( + "errors" +) + +// Error constants +var ( + ErrInvalidKey = errors.New("key is invalid") + ErrInvalidKeyType = errors.New("key is of invalid type") + ErrHashUnavailable = errors.New("the requested hash function is unavailable") +) + +// The errors that might occur when parsing and validating a token +const ( + ValidationErrorMalformed uint32 = 1 << iota // Token is malformed + ValidationErrorUnverifiable // Token could not be verified because of signing problems + ValidationErrorSignatureInvalid // Signature validation failed + + // Standard Claim validation errors + ValidationErrorAudience // AUD validation failed + ValidationErrorExpired // EXP validation failed + ValidationErrorIssuedAt // IAT validation failed + ValidationErrorIssuer // ISS validation failed + ValidationErrorNotValidYet // NBF validation failed + ValidationErrorId // JTI validation failed + ValidationErrorClaimsInvalid // Generic claims validation error +) + +// Helper for constructing a ValidationError with a string error message +func NewValidationError(errorText string, errorFlags uint32) *ValidationError { + return &ValidationError{ + text: errorText, + Errors: errorFlags, + } +} + +// The error from Parse if token is not valid +type ValidationError struct { + Inner error // stores the error returned by external dependencies, i.e.: KeyFunc + Errors uint32 // bitfield. see ValidationError... constants + text string // errors that do not have a valid error just have text +} + +// Validation error is an error type +func (e ValidationError) Error() string { + if e.Inner != nil { + return e.Inner.Error() + } else if e.text != "" { + return e.text + } else { + return "token is invalid" + } +} + +// No errors +func (e *ValidationError) valid() bool { + return e.Errors == 0 +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/hmac.go b/backend/vendor/github.com/dgrijalva/jwt-go/hmac.go new file mode 100644 index 00000000..addbe5d4 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/hmac.go @@ -0,0 +1,95 @@ +package jwt + +import ( + "crypto" + "crypto/hmac" + "errors" +) + +// Implements the HMAC-SHA family of signing methods signing methods +// Expects key type of []byte for both signing and validation +type SigningMethodHMAC struct { + Name string + Hash crypto.Hash +} + +// Specific instances for HS256 and company +var ( + SigningMethodHS256 *SigningMethodHMAC + SigningMethodHS384 *SigningMethodHMAC + SigningMethodHS512 *SigningMethodHMAC + ErrSignatureInvalid = errors.New("signature is invalid") +) + +func init() { + // HS256 + SigningMethodHS256 = &SigningMethodHMAC{"HS256", crypto.SHA256} + RegisterSigningMethod(SigningMethodHS256.Alg(), func() SigningMethod { + return SigningMethodHS256 + }) + + // HS384 + SigningMethodHS384 = &SigningMethodHMAC{"HS384", crypto.SHA384} + RegisterSigningMethod(SigningMethodHS384.Alg(), func() SigningMethod { + return SigningMethodHS384 + }) + + // HS512 + SigningMethodHS512 = &SigningMethodHMAC{"HS512", crypto.SHA512} + RegisterSigningMethod(SigningMethodHS512.Alg(), func() SigningMethod { + return SigningMethodHS512 + }) +} + +func (m *SigningMethodHMAC) Alg() string { + return m.Name +} + +// Verify the signature of HSXXX tokens. Returns nil if the signature is valid. +func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error { + // Verify the key is the right type + keyBytes, ok := key.([]byte) + if !ok { + return ErrInvalidKeyType + } + + // Decode signature, for comparison + sig, err := DecodeSegment(signature) + if err != nil { + return err + } + + // Can we use the specified hashing method? + if !m.Hash.Available() { + return ErrHashUnavailable + } + + // This signing method is symmetric, so we validate the signature + // by reproducing the signature from the signing string and key, then + // comparing that against the provided signature. + hasher := hmac.New(m.Hash.New, keyBytes) + hasher.Write([]byte(signingString)) + if !hmac.Equal(sig, hasher.Sum(nil)) { + return ErrSignatureInvalid + } + + // No validation errors. Signature is good. + return nil +} + +// Implements the Sign method from SigningMethod for this signing method. +// Key must be []byte +func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) { + if keyBytes, ok := key.([]byte); ok { + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := hmac.New(m.Hash.New, keyBytes) + hasher.Write([]byte(signingString)) + + return EncodeSegment(hasher.Sum(nil)), nil + } + + return "", ErrInvalidKeyType +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/map_claims.go b/backend/vendor/github.com/dgrijalva/jwt-go/map_claims.go new file mode 100644 index 00000000..291213c4 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/map_claims.go @@ -0,0 +1,94 @@ +package jwt + +import ( + "encoding/json" + "errors" + // "fmt" +) + +// Claims type that uses the map[string]interface{} for JSON decoding +// This is the default claims type if you don't supply one +type MapClaims map[string]interface{} + +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyAudience(cmp string, req bool) bool { + aud, _ := m["aud"].(string) + return verifyAud(aud, cmp, req) +} + +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { + switch exp := m["exp"].(type) { + case float64: + return verifyExp(int64(exp), cmp, req) + case json.Number: + v, _ := exp.Int64() + return verifyExp(v, cmp, req) + } + return req == false +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { + switch iat := m["iat"].(type) { + case float64: + return verifyIat(int64(iat), cmp, req) + case json.Number: + v, _ := iat.Int64() + return verifyIat(v, cmp, req) + } + return req == false +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyIssuer(cmp string, req bool) bool { + iss, _ := m["iss"].(string) + return verifyIss(iss, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { + switch nbf := m["nbf"].(type) { + case float64: + return verifyNbf(int64(nbf), cmp, req) + case json.Number: + v, _ := nbf.Int64() + return verifyNbf(v, cmp, req) + } + return req == false +} + +// Validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (m MapClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if m.VerifyExpiresAt(now, false) == false { + vErr.Inner = errors.New("Token is expired") + vErr.Errors |= ValidationErrorExpired + } + + if m.VerifyIssuedAt(now, false) == false { + vErr.Inner = errors.New("Token used before issued") + vErr.Errors |= ValidationErrorIssuedAt + } + + if m.VerifyNotBefore(now, false) == false { + vErr.Inner = errors.New("Token is not valid yet") + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/none.go b/backend/vendor/github.com/dgrijalva/jwt-go/none.go new file mode 100644 index 00000000..f04d189d --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/none.go @@ -0,0 +1,52 @@ +package jwt + +// Implements the none signing method. This is required by the spec +// but you probably should never use it. +var SigningMethodNone *signingMethodNone + +const UnsafeAllowNoneSignatureType unsafeNoneMagicConstant = "none signing method allowed" + +var NoneSignatureTypeDisallowedError error + +type signingMethodNone struct{} +type unsafeNoneMagicConstant string + +func init() { + SigningMethodNone = &signingMethodNone{} + NoneSignatureTypeDisallowedError = NewValidationError("'none' signature type is not allowed", ValidationErrorSignatureInvalid) + + RegisterSigningMethod(SigningMethodNone.Alg(), func() SigningMethod { + return SigningMethodNone + }) +} + +func (m *signingMethodNone) Alg() string { + return "none" +} + +// Only allow 'none' alg type if UnsafeAllowNoneSignatureType is specified as the key +func (m *signingMethodNone) Verify(signingString, signature string, key interface{}) (err error) { + // Key must be UnsafeAllowNoneSignatureType to prevent accidentally + // accepting 'none' signing method + if _, ok := key.(unsafeNoneMagicConstant); !ok { + return NoneSignatureTypeDisallowedError + } + // If signing method is none, signature must be an empty string + if signature != "" { + return NewValidationError( + "'none' signing method with non-empty signature", + ValidationErrorSignatureInvalid, + ) + } + + // Accept 'none' signing method. + return nil +} + +// Only allow 'none' signing if UnsafeAllowNoneSignatureType is specified as the key +func (m *signingMethodNone) Sign(signingString string, key interface{}) (string, error) { + if _, ok := key.(unsafeNoneMagicConstant); ok { + return "", nil + } + return "", NoneSignatureTypeDisallowedError +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/parser.go b/backend/vendor/github.com/dgrijalva/jwt-go/parser.go new file mode 100644 index 00000000..d6901d9a --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/parser.go @@ -0,0 +1,148 @@ +package jwt + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +type Parser struct { + ValidMethods []string // If populated, only these methods will be considered valid + UseJSONNumber bool // Use JSON Number format in JSON decoder + SkipClaimsValidation bool // Skip claims validation during token parsing +} + +// Parse, validate, and return a token. +// keyFunc will receive the parsed token and should return the key for validating. +// If everything is kosher, err will be nil +func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { + return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc) +} + +func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { + token, parts, err := p.ParseUnverified(tokenString, claims) + if err != nil { + return token, err + } + + // Verify signing method is in the required set + if p.ValidMethods != nil { + var signingMethodValid = false + var alg = token.Method.Alg() + for _, m := range p.ValidMethods { + if m == alg { + signingMethodValid = true + break + } + } + if !signingMethodValid { + // signing method is not in the listed set + return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid) + } + } + + // Lookup key + var key interface{} + if keyFunc == nil { + // keyFunc was not provided. short circuiting validation + return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable) + } + if key, err = keyFunc(token); err != nil { + // keyFunc returned an error + if ve, ok := err.(*ValidationError); ok { + return token, ve + } + return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable} + } + + vErr := &ValidationError{} + + // Validate Claims + if !p.SkipClaimsValidation { + if err := token.Claims.Valid(); err != nil { + + // If the Claims Valid returned an error, check if it is a validation error, + // If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set + if e, ok := err.(*ValidationError); !ok { + vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid} + } else { + vErr = e + } + } + } + + // Perform validation + token.Signature = parts[2] + if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil { + vErr.Inner = err + vErr.Errors |= ValidationErrorSignatureInvalid + } + + if vErr.valid() { + token.Valid = true + return token, nil + } + + return token, vErr +} + +// WARNING: Don't use this method unless you know what you're doing +// +// This method parses the token but doesn't validate the signature. It's only +// ever useful in cases where you know the signature is valid (because it has +// been checked previously in the stack) and you want to extract values from +// it. +func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) { + parts = strings.Split(tokenString, ".") + if len(parts) != 3 { + return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed) + } + + token = &Token{Raw: tokenString} + + // parse Header + var headerBytes []byte + if headerBytes, err = DecodeSegment(parts[0]); err != nil { + if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") { + return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed) + } + return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + if err = json.Unmarshal(headerBytes, &token.Header); err != nil { + return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + + // parse Claims + var claimBytes []byte + token.Claims = claims + + if claimBytes, err = DecodeSegment(parts[1]); err != nil { + return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + dec := json.NewDecoder(bytes.NewBuffer(claimBytes)) + if p.UseJSONNumber { + dec.UseNumber() + } + // JSON Decode. Special case for map type to avoid weird pointer behavior + if c, ok := token.Claims.(MapClaims); ok { + err = dec.Decode(&c) + } else { + err = dec.Decode(&claims) + } + // Handle decode error + if err != nil { + return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + + // Lookup signature method + if method, ok := token.Header["alg"].(string); ok { + if token.Method = GetSigningMethod(method); token.Method == nil { + return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable) + } + } else { + return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable) + } + + return token, parts, nil +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/rsa.go b/backend/vendor/github.com/dgrijalva/jwt-go/rsa.go new file mode 100644 index 00000000..e4caf1ca --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/rsa.go @@ -0,0 +1,101 @@ +package jwt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" +) + +// Implements the RSA family of signing methods signing methods +// Expects *rsa.PrivateKey for signing and *rsa.PublicKey for validation +type SigningMethodRSA struct { + Name string + Hash crypto.Hash +} + +// Specific instances for RS256 and company +var ( + SigningMethodRS256 *SigningMethodRSA + SigningMethodRS384 *SigningMethodRSA + SigningMethodRS512 *SigningMethodRSA +) + +func init() { + // RS256 + SigningMethodRS256 = &SigningMethodRSA{"RS256", crypto.SHA256} + RegisterSigningMethod(SigningMethodRS256.Alg(), func() SigningMethod { + return SigningMethodRS256 + }) + + // RS384 + SigningMethodRS384 = &SigningMethodRSA{"RS384", crypto.SHA384} + RegisterSigningMethod(SigningMethodRS384.Alg(), func() SigningMethod { + return SigningMethodRS384 + }) + + // RS512 + SigningMethodRS512 = &SigningMethodRSA{"RS512", crypto.SHA512} + RegisterSigningMethod(SigningMethodRS512.Alg(), func() SigningMethod { + return SigningMethodRS512 + }) +} + +func (m *SigningMethodRSA) Alg() string { + return m.Name +} + +// Implements the Verify method from SigningMethod +// For this signing method, must be an *rsa.PublicKey structure. +func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + var rsaKey *rsa.PublicKey + var ok bool + + if rsaKey, ok = key.(*rsa.PublicKey); !ok { + return ErrInvalidKeyType + } + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Verify the signature + return rsa.VerifyPKCS1v15(rsaKey, m.Hash, hasher.Sum(nil), sig) +} + +// Implements the Sign method from SigningMethod +// For this signing method, must be an *rsa.PrivateKey structure. +func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) { + var rsaKey *rsa.PrivateKey + var ok bool + + // Validate type of key + if rsaKey, ok = key.(*rsa.PrivateKey); !ok { + return "", ErrInvalidKey + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return the encoded bytes + if sigBytes, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil)); err == nil { + return EncodeSegment(sigBytes), nil + } else { + return "", err + } +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go b/backend/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go new file mode 100644 index 00000000..10ee9db8 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go @@ -0,0 +1,126 @@ +// +build go1.4 + +package jwt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" +) + +// Implements the RSAPSS family of signing methods signing methods +type SigningMethodRSAPSS struct { + *SigningMethodRSA + Options *rsa.PSSOptions +} + +// Specific instances for RS/PS and company +var ( + SigningMethodPS256 *SigningMethodRSAPSS + SigningMethodPS384 *SigningMethodRSAPSS + SigningMethodPS512 *SigningMethodRSAPSS +) + +func init() { + // PS256 + SigningMethodPS256 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS256", + Hash: crypto.SHA256, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA256, + }, + } + RegisterSigningMethod(SigningMethodPS256.Alg(), func() SigningMethod { + return SigningMethodPS256 + }) + + // PS384 + SigningMethodPS384 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS384", + Hash: crypto.SHA384, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA384, + }, + } + RegisterSigningMethod(SigningMethodPS384.Alg(), func() SigningMethod { + return SigningMethodPS384 + }) + + // PS512 + SigningMethodPS512 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS512", + Hash: crypto.SHA512, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA512, + }, + } + RegisterSigningMethod(SigningMethodPS512.Alg(), func() SigningMethod { + return SigningMethodPS512 + }) +} + +// Implements the Verify method from SigningMethod +// For this verify method, key must be an rsa.PublicKey struct +func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + var rsaKey *rsa.PublicKey + switch k := key.(type) { + case *rsa.PublicKey: + rsaKey = k + default: + return ErrInvalidKey + } + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + return rsa.VerifyPSS(rsaKey, m.Hash, hasher.Sum(nil), sig, m.Options) +} + +// Implements the Sign method from SigningMethod +// For this signing method, key must be an rsa.PrivateKey struct +func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (string, error) { + var rsaKey *rsa.PrivateKey + + switch k := key.(type) { + case *rsa.PrivateKey: + rsaKey = k + default: + return "", ErrInvalidKeyType + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return the encoded bytes + if sigBytes, err := rsa.SignPSS(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil), m.Options); err == nil { + return EncodeSegment(sigBytes), nil + } else { + return "", err + } +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go b/backend/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go new file mode 100644 index 00000000..a5ababf9 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go @@ -0,0 +1,101 @@ +package jwt + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key") + ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key") + ErrNotRSAPublicKey = errors.New("Key is not a valid RSA public key") +) + +// Parse PEM encoded PKCS1 or PKCS8 private key +func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + var parsedKey interface{} + if parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { + return nil, err + } + } + + var pkey *rsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, ErrNotRSAPrivateKey + } + + return pkey, nil +} + +// Parse PEM encoded PKCS1 or PKCS8 private key protected with password +func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + var parsedKey interface{} + + var blockDecrypted []byte + if blockDecrypted, err = x509.DecryptPEMBlock(block, []byte(password)); err != nil { + return nil, err + } + + if parsedKey, err = x509.ParsePKCS1PrivateKey(blockDecrypted); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(blockDecrypted); err != nil { + return nil, err + } + } + + var pkey *rsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, ErrNotRSAPrivateKey + } + + return pkey, nil +} + +// Parse PEM encoded PKCS1 or PKCS8 public key +func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + var pkey *rsa.PublicKey + var ok bool + if pkey, ok = parsedKey.(*rsa.PublicKey); !ok { + return nil, ErrNotRSAPublicKey + } + + return pkey, nil +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/signing_method.go b/backend/vendor/github.com/dgrijalva/jwt-go/signing_method.go new file mode 100644 index 00000000..ed1f212b --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/signing_method.go @@ -0,0 +1,35 @@ +package jwt + +import ( + "sync" +) + +var signingMethods = map[string]func() SigningMethod{} +var signingMethodLock = new(sync.RWMutex) + +// Implement SigningMethod to add new methods for signing or verifying tokens. +type SigningMethod interface { + Verify(signingString, signature string, key interface{}) error // Returns nil if signature is valid + Sign(signingString string, key interface{}) (string, error) // Returns encoded signature or error + Alg() string // returns the alg identifier for this method (example: 'HS256') +} + +// Register the "alg" name and a factory function for signing method. +// This is typically done during init() in the method's implementation +func RegisterSigningMethod(alg string, f func() SigningMethod) { + signingMethodLock.Lock() + defer signingMethodLock.Unlock() + + signingMethods[alg] = f +} + +// Get a signing method from an "alg" string +func GetSigningMethod(alg string) (method SigningMethod) { + signingMethodLock.RLock() + defer signingMethodLock.RUnlock() + + if methodF, ok := signingMethods[alg]; ok { + method = methodF() + } + return +} diff --git a/backend/vendor/github.com/dgrijalva/jwt-go/token.go b/backend/vendor/github.com/dgrijalva/jwt-go/token.go new file mode 100644 index 00000000..d637e086 --- /dev/null +++ b/backend/vendor/github.com/dgrijalva/jwt-go/token.go @@ -0,0 +1,108 @@ +package jwt + +import ( + "encoding/base64" + "encoding/json" + "strings" + "time" +) + +// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time). +// You can override it to use another time value. This is useful for testing or if your +// server uses a different time zone than your tokens. +var TimeFunc = time.Now + +// Parse methods use this callback function to supply +// the key for verification. The function receives the parsed, +// but unverified Token. This allows you to use properties in the +// Header of the token (such as `kid`) to identify which key to use. +type Keyfunc func(*Token) (interface{}, error) + +// A JWT Token. Different fields will be used depending on whether you're +// creating or parsing/verifying a token. +type Token struct { + Raw string // The raw token. Populated when you Parse a token + Method SigningMethod // The signing method used or to be used + Header map[string]interface{} // The first segment of the token + Claims Claims // The second segment of the token + Signature string // The third segment of the token. Populated when you Parse a token + Valid bool // Is the token valid? Populated when you Parse/Verify a token +} + +// Create a new Token. Takes a signing method +func New(method SigningMethod) *Token { + return NewWithClaims(method, MapClaims{}) +} + +func NewWithClaims(method SigningMethod, claims Claims) *Token { + return &Token{ + Header: map[string]interface{}{ + "typ": "JWT", + "alg": method.Alg(), + }, + Claims: claims, + Method: method, + } +} + +// Get the complete, signed token +func (t *Token) SignedString(key interface{}) (string, error) { + var sig, sstr string + var err error + if sstr, err = t.SigningString(); err != nil { + return "", err + } + if sig, err = t.Method.Sign(sstr, key); err != nil { + return "", err + } + return strings.Join([]string{sstr, sig}, "."), nil +} + +// Generate the signing string. This is the +// most expensive part of the whole deal. Unless you +// need this for something special, just go straight for +// the SignedString. +func (t *Token) SigningString() (string, error) { + var err error + parts := make([]string, 2) + for i, _ := range parts { + var jsonValue []byte + if i == 0 { + if jsonValue, err = json.Marshal(t.Header); err != nil { + return "", err + } + } else { + if jsonValue, err = json.Marshal(t.Claims); err != nil { + return "", err + } + } + + parts[i] = EncodeSegment(jsonValue) + } + return strings.Join(parts, "."), nil +} + +// Parse, validate, and return a token. +// keyFunc will receive the parsed token and should return the key for validating. +// If everything is kosher, err will be nil +func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { + return new(Parser).Parse(tokenString, keyFunc) +} + +func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { + return new(Parser).ParseWithClaims(tokenString, claims, keyFunc) +} + +// Encode JWT specific base64url encoding with padding stripped +func EncodeSegment(seg []byte) string { + return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=") +} + +// Decode JWT specific base64url encoding with padding stripped +func DecodeSegment(seg string) ([]byte, error) { + if l := len(seg) % 4; l > 0 { + seg += strings.Repeat("=", 4-l) + } + + return base64.URLEncoding.DecodeString(seg) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/.gitignore b/backend/vendor/github.com/go-pkgz/auth/.gitignore new file mode 100644 index 00000000..4ee8214d --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/.gitignore @@ -0,0 +1,14 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +.vscode +*.cov \ No newline at end of file diff --git a/backend/vendor/github.com/go-pkgz/auth/.travis.yml b/backend/vendor/github.com/go-pkgz/auth/.travis.yml new file mode 100644 index 00000000..db0e0d50 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/.travis.yml @@ -0,0 +1,23 @@ +language: go + +services: + - mongodb + +go: + - "1.11.x" + +install: true + +before_install: + - export TZ=America/Chicago + - curl -L https://git.io/vp6lP | sh + - go get github.com/mattn/goveralls + - export MONGO_TEST=mongodb://127.0.0.1:27017 + - export PATH=$(pwd)/bin:$PATH + +script: + - GO111MODULE=on go get ./... + - GO111MODULE=on go mod vendor + - GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1; + - ./bin/gometalinter --deadline=120s --exclude=test --exclude=mock --exclude=vendor --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=megacheck --enable=ineffassign --enable=varcheck --enable=unconvert --enable=deadcode --enable=interfacer --enable=gotype ./... || travis_terminate 1; + - $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/backend/vendor/github.com/go-pkgz/auth/LICENSE b/backend/vendor/github.com/go-pkgz/auth/LICENSE new file mode 100644 index 00000000..ca125214 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Umputun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/vendor/github.com/go-pkgz/auth/README.md b/backend/vendor/github.com/go-pkgz/auth/README.md new file mode 100644 index 00000000..ebb5c881 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/README.md @@ -0,0 +1,128 @@ +# auth - authentication via oauth2 [![Build Status](https://travis-ci.org/go-pkgz/auth.svg?branch=master)](https://travis-ci.org/go-pkgz/auth) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/auth/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/auth?branch=master) + +This library provides "social login" with Github, Google, Facebook and Yandex. + +- Multiple oauth2 providers can be used at the same time +- Special `dev` provider allows local testing and development +- JWT stored in a secure cookie and with XSRF protection. Cookies can be session-only +- Minimal scopes with user name, id and picture (avatar) only +- Integrated avatar proxy with FS, boltdb or gridfs storage +- Support of user-defined storages +- Black list with user-defined validator +- Multiple aud (audience) supported +- Secure key with customizable `SecretReader` +- Ability to store extra information to token and retrieve on login +- Middleware for easy integration into http routers + +## Install + +`go install github.com/go-pkgz/auth` + +## Usage + +Example with chi router: + +```go +func main() { + /// define options + options := auth.Opts{ + SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), // secret key for JWT + TokenDuration: time.Hour, + CookieDuration: time.Hour * 24, + Issuer: "my-test-app", + URL: "http://127.0.0.1:8080", + AvatarStore: avatar.NewLocalFS("/tmp", 120), + Validator: middleware.ValidatorFunc(func(_ string, claims token.Claims) bool { + return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_") // allow only dev_ names + }), + } + + // create auth service + service, err := auth.NewService(options) + if err != nil { + log.Fatal(err) + } + service.AddProvider("github", "", "") // add github provider + service.AddProvider("facebook", "", "") // add facebook provider + + // retrieve auth middleware + m := service.Middleware() + + // setup http server + router := chi.NewRouter() + router.Get("/open", openRouteHandler) // open api + router.With(m.Auth).Get("/private", protectedRouteHandler) // protected api + + // setup auth routes + authRoutes, avaRoutes := service.Handlers() + router.Mount("/auth", authRoutes) // add auth handlers + router.Mount("/avatar", avaRoutes) // add avatar handler + + log.Fatal(http.ListenAndServe(":8080", router)) +} +``` + +## Middleware + +`github.com/go-pkgz/auth/middleware` provides ready-to-use middleware. + +- `middleware.Auth` - requires authenticated user +- `middleware.Admin` - requires authenticated and admin user +- `middleware.Trace` - doesn't require authenticated user, but adds user info to request + +## Register oauth2 providers + +Authentication handled by external providers. You should setup oauth2 for all (or some) of them to allow users to authenticate. It is not mandatory to have all of them, but at least one should be correctly configured. + +#### Google Auth Provider + +1. Create a new project: https://console.developers.google.com/project +1. Choose the new project from the top right project dropdown (only if another project is selected) +1. In the project Dashboard center pane, choose **"API Manager"** +1. In the left Nav pane, choose **"Credentials"** +1. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save. +1. In the center pane, choose **"Credentials"** tab. + * Open the **"New credentials"** drop down + * Choose **"OAuth client ID"** + * Choose **"Web application"** + * Application name is freeform, choose something appropriate + * Authorized origins is your domain ex: `https://example.mysite.com` + * Authorized redirect URIs is the location of oauth2/callback constructed as domain + `/auth/google/callback`, ex: `https://example.mysite.com/auth/google/callback` + * Choose **"Create"** +2. Take note of the **Client ID** and **Client Secret** + +_instructions for google oauth2 setup borrowed from [oauth2_proxy](https://github.com/bitly/oauth2_proxy)_ + +#### GitHub Auth Provider + +1. Create a new **"OAuth App"**: https://github.com/settings/developers +1. Fill **"Application Name"** and **"Homepage URL"** for your site +1. Under **"Authorization callback URL"** enter the correct url constructed as domain + `/auth/github/callback`. ie `https://example.mysite.com/auth/github/callback` +1. Take note of the **Client ID** and **Client Secret** + +#### Facebook Auth Provider + +1. From https://developers.facebook.com select **"My Apps"** / **"Add a new App"** +1. Set **"Display Name"** and **"Contact email"** +1. Choose **"Facebook Login"** and then **"Web"** +1. Set "Site URL" to your domain, ex: `https://example.mysite.com` +1. Under **"Facebook login"** / **"Settings"** fill "Valid OAuth redirect URIs" with your callback url constructed as domain + `/auth/facebook/callback` +1. Select **"App Review"** and turn public flag on. This step may ask you to provide a link to your privacy policy. + +#### Yandex Auth Provider + +1. Create a new **"OAuth App"**: https://oauth.yandex.com/client/new +1. Fill **"App name"** for your site +1. Under **Platforms** select **"Web services"** and enter **"Callback URI #1"** constructed as domain + `/auth/yandex/callback`. ie `https://example.mysite.com/auth/yandex/callback` +1. Select **Permissions**. You need following permissions only from the **"Yandex.Passport API"** section: + * Access to user avatar + * Access to username, first name and surname, gender +1. Fill out the rest of fields if needed +1. Take note of the **ID** and **Password** + +For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/concepts/about-docpage/) and [Yandex.Passport](https://tech.yandex.com/passport/doc/dg/index-docpage/) API documentation. + + +## Status + +The library extracted from [remark42](https://github.com/umputun/remark) project. The code in production use on multiple sites and seems to work fine. \ No newline at end of file diff --git a/backend/vendor/github.com/go-pkgz/auth/auth.go b/backend/vendor/github.com/go-pkgz/auth/auth.go new file mode 100644 index 00000000..a87534e4 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/auth.go @@ -0,0 +1,201 @@ +package auth + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-pkgz/rest" + "github.com/pkg/errors" + + "github.com/go-pkgz/auth/avatar" + "github.com/go-pkgz/auth/middleware" + "github.com/go-pkgz/auth/provider" + "github.com/go-pkgz/auth/token" +) + +// Service provides higher level wrapper allowing to construct everything and get back token middleware +type Service struct { + opts Opts + jwtService *token.Service + providers []provider.Service + authMiddleware middleware.Authenticator + avatarProxy *avatar.Proxy + issuer string +} + +// Opts is a full set of all parameters to initialize Service +type Opts struct { + SecretReader token.Secret // reader returns secret for given site id (aud) + ClaimsUpd token.ClaimsUpdater // updater for jwt to add/modify values stored in the token + SecureCookies bool // makes jwt cookie secure + TokenDuration time.Duration // token's TTL, refreshed automatically + CookieDuration time.Duration // cookie's TTL. This cookie stores JWT token + DisableXSRF bool // disable XSRF protection, useful for testing/debugging + + // optional (custom) names for cookies and headers + JWTCookieName string // default "JWT" + JWTHeaderKey string // default "X-JWT" + XSRFCookieName string // default "XSRF-TOKEN" + XSRFHeaderKey string // default "X-XSRF-TOKEN" + + Issuer string // optional value for iss claim, usually the application name, default "go-pkgz/auth" + + URL string // root url for the rest service, i.e. http://blah.example.com + Validator token.Validator // validator allows to reject some valid tokens with user-defined logic + + AvatarStore avatar.Store // store to save/load avatars + AvatarResizeLimit int // resize avatar's limit in pixels + AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar" + + DevPasswd string // if presented, allows basic auth with user dev and given password +} + +// NewService initializes everything +func NewService(opts Opts) *Service { + + jwtService := token.NewService(token.Opts{ + SecretReader: opts.SecretReader, + ClaimsUpd: opts.ClaimsUpd, + SecureCookies: opts.SecureCookies, + TokenDuration: opts.TokenDuration, + CookieDuration: opts.CookieDuration, + DisableXSRF: opts.DisableXSRF, + JWTCookieName: opts.JWTCookieName, + JWTHeaderKey: opts.JWTHeaderKey, + XSRFCookieName: opts.XSRFCookieName, + XSRFHeaderKey: opts.XSRFHeaderKey, + Issuer: opts.Issuer, + }) + + if opts.SecretReader == nil { + jwtService.SecretReader = token.SecretFunc(func(id string) (string, error) { + return "", errors.New("secrets reader not avalibale") + }) + } + + res := Service{ + opts: opts, + jwtService: jwtService, + authMiddleware: middleware.Authenticator{ + JWTService: jwtService, + Validator: opts.Validator, + DevPasswd: opts.DevPasswd, + }, + } + + if opts.Issuer == "" { + res.issuer = "go-pkgz/auth" + } + + if opts.AvatarStore != nil { + res.avatarProxy = &avatar.Proxy{ + Store: opts.AvatarStore, + URL: opts.URL, + RoutePath: opts.AvatarRoutePath, + ResizeLimit: opts.AvatarResizeLimit, + } + } + + return &res +} + +// Handlers gets http.Handler for all providers and avatars +func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handler) { + + providerHandler := func(w http.ResponseWriter, r *http.Request) { + elems := strings.Split(r.URL.Path, "/") + if len(elems) < 2 { + w.WriteHeader(http.StatusBadRequest) + return + } + + // list all providers + if elems[len(elems)-1] == "list" { + list := []string{} + for _, p := range s.providers { + list = append(list, p.Name) + } + rest.RenderJSON(w, r, list) + return + } + + // allow logout without specifying provider + if elems[len(elems)-1] == "logout" { + s.providers[0].Handler(w, r) + return + } + + provName := elems[len(elems)-2] + p, err := s.Provider(provName) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + rest.RenderJSON(w, r, rest.JSON{"error": fmt.Sprintf("provider %s not supported", provName)}) + return + } + p.Handler(w, r) + } + + return http.HandlerFunc(providerHandler), http.HandlerFunc(s.avatarProxy.Handler) +} + +// Middleware returns token middleware +func (s *Service) Middleware() middleware.Authenticator { + return s.authMiddleware +} + +// AddProvider adds provider for given name +func (s *Service) AddProvider(name string, cid string, csecret string) { + + p := provider.Params{ + URL: s.opts.URL, + JwtService: s.jwtService, + Issuer: s.issuer, + AvatarProxy: s.avatarProxy, + Cid: cid, + Csecret: csecret, + } + + switch strings.ToLower(name) { + case "github": + s.providers = append(s.providers, provider.NewGithub(p)) + case "google": + s.providers = append(s.providers, provider.NewGoogle(p)) + case "facebook": + s.providers = append(s.providers, provider.NewFacebook(p)) + case "yandex": + s.providers = append(s.providers, provider.NewFacebook(p)) + case "dev": + s.providers = append(s.providers, provider.NewDev(p)) + default: + return + } + + s.authMiddleware.Providers = s.providers +} + +// Provider gets provider by name +func (s *Service) Provider(name string) (provider.Service, error) { + for _, p := range s.providers { + if p.Name == name { + return p, nil + } + } + return provider.Service{}, errors.Errorf("provider %s not found", name) +} + +// Providers gets all registered providers +func (s *Service) Providers() []provider.Service { + return s.providers +} + +// TokenService returns token.Service +func (s *Service) TokenService() *token.Service { + return s.jwtService +} + +// AvatarProxy returns stored in service +func (s *Service) AvatarProxy() *avatar.Proxy { + return s.avatarProxy +} diff --git a/backend/vendor/github.com/go-pkgz/auth/avatar/avatar.go b/backend/vendor/github.com/go-pkgz/auth/avatar/avatar.go new file mode 100644 index 00000000..0140f952 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/avatar/avatar.go @@ -0,0 +1,162 @@ +// Package avatar implements avatart proxy for oauth and +// defines store interface and implements local (fs), gridfs (mongo) and boltdb stores. +package avatar + +import ( + "bytes" + "image" + "image/png" + "io" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/go-pkgz/rest" + "github.com/pkg/errors" + "golang.org/x/image/draw" + + "github.com/go-pkgz/auth/token" +) + +// Proxy provides http handler for avatars from avatar.Store +// On user login token will call Put and it will retrieve and save picture locally. +type Proxy struct { + Store Store + RoutePath string + URL string + ResizeLimit int +} + +// Put stores retrieved avatar to avatar.Store. Gets image from user info. Returns proxied url +func (p *Proxy) Put(u token.User) (avatarURL string, err error) { + + // no picture for user, try default avatar + if u.Picture == "" { + return "", errors.Errorf("no picture for %s", u.ID) + } + + // load avatar from remote location + client := http.Client{Timeout: 10 * time.Second} + var resp *http.Response + err = retry(5, time.Second, func() error { + var e error + resp, e = client.Get(u.Picture) + return e + }) + if err != nil { + return "", errors.Wrap(err, "failed to fetch avatar from the orig") + } + + defer func() { + if e := resp.Body.Close(); e != nil { + log.Printf("[WARN] can't close response body, %s", e) + } + }() + + if resp.StatusCode != http.StatusOK { + return "", errors.Errorf("failed to get avatar from the orig, status %s", resp.Status) + } + + avatarID, err := p.Store.Put(u.ID, p.resize(resp.Body, p.ResizeLimit)) // put returns avatar base name, like 123456.image + if err != nil { + return "", err + } + + log.Printf("[DEBUG] saved avatar from %s to %s, user %q", u.Picture, avatarID, u.Name) + return p.URL + p.RoutePath + "/" + avatarID, nil +} + +// Handler returns token routes for given provider +func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) { + + if r.Method != "GET" { + w.WriteHeader(http.StatusMethodNotAllowed) + } + elems := strings.Split(r.URL.Path, "/") + avatarID := elems[len(elems)-1] + + // enforce client-side caching + etag := `"` + p.Store.ID(avatarID) + `"` + w.Header().Set("Etag", etag) + w.Header().Set("Cache-Control", "max-age=604800") // 7 days + if match := r.Header.Get("If-None-Match"); match != "" { + if strings.Contains(match, etag) { + w.WriteHeader(http.StatusNotModified) + return + } + } + + avReader, size, err := p.Store.Get(avatarID) + if err != nil { + + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load avatar") + return + } + + defer func() { + if e := avReader.Close(); e != nil { + log.Printf("[WARN] can't close avatar reader for %s, %s", avatarID, e) + } + }() + + w.Header().Set("Content-Type", "image/*") + w.Header().Set("Content-Length", strconv.Itoa(size)) + w.WriteHeader(http.StatusOK) + if _, err = io.Copy(w, avReader); err != nil { + log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err) + } +} + +// resize an image of supported format (PNG, JPG, GIF) to the size of "limit" px of the biggest side +// (width or height) preserving aspect ratio. +// Returns original reader if resizing is not needed or failed. +func (p *Proxy) resize(reader io.Reader, limit int) io.Reader { + if reader == nil { + log.Print("[WARN] avatar resize(): reader is nil") + return nil + } + if limit <= 0 { + log.Print("[DEBUG] avatar resize(): limit should be greater than 0") + return reader + } + + var teeBuf bytes.Buffer + tee := io.TeeReader(reader, &teeBuf) + src, _, err := image.Decode(tee) + if err != nil { + log.Printf("[WARN] avatar resize(): can't decode avatar image, %s", err) + return &teeBuf + } + + bounds := src.Bounds() + w, h := bounds.Dx(), bounds.Dy() + if w <= limit && h <= limit || w <= 0 || h <= 0 { + log.Print("[DEBUG] resizing image is smaller that the limit or has 0 size") + return &teeBuf + } + newW, newH := w*limit/h, limit + if w > h { + newW, newH = limit, h*limit/w + } + m := image.NewRGBA(image.Rect(0, 0, newW, newH)) + // Slower than `draw.ApproxBiLinear.Scale()` but better quality. + draw.BiLinear.Scale(m, m.Bounds(), src, src.Bounds(), draw.Src, nil) + + var out bytes.Buffer + if err = png.Encode(&out, m); err != nil { + log.Printf("[WARN] avatar resize(): can't encode resized avatar to PNG, %s", err) + return &teeBuf + } + return &out +} +func retry(retries int, delay time.Duration, fn func() error) (err error) { + for i := 0; i < retries; i++ { + if err = fn(); err == nil { + return nil + } + time.Sleep(delay) + } + return errors.Wrap(err, "retry failed") +} diff --git a/backend/vendor/github.com/go-pkgz/auth/avatar/bolt.go b/backend/vendor/github.com/go-pkgz/auth/avatar/bolt.go new file mode 100644 index 00000000..e1d3d1c2 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/avatar/bolt.go @@ -0,0 +1,136 @@ +package avatar + +import ( + "bytes" + "crypto/sha1" + "encoding/hex" + "io" + "io/ioutil" + "log" + + bolt "github.com/coreos/bbolt" + "github.com/pkg/errors" +) + +// BoltDB implements avatar store with bolt +// using separate db (file) with "avatars" bucket to keep image bin and "metas" bucket +// to keep sha1 of picture. avatarID (base file name) used as a key for both. +type BoltDB struct { + fileName string // full path to boltdb + db *bolt.DB +} + +const avatarsBktName = "avatars" +const metasBktName = "metas" + +// NewBoltDB makes bolt avatar store +func NewBoltDB(fileName string, options bolt.Options) (*BoltDB, error) { + db, err := bolt.Open(fileName, 0600, &options) + if err != nil { + return nil, errors.Wrapf(err, "failed to make boltdb for %s", fileName) + } + err = db.Update(func(tx *bolt.Tx) error { + if _, e := tx.CreateBucketIfNotExists([]byte(avatarsBktName)); e != nil { + return errors.Wrapf(e, "failed to create top level bucket %s", avatarsBktName) + } + _, e := tx.CreateBucketIfNotExists([]byte(metasBktName)) + return errors.Wrapf(e, "failed to create top metas bucket %s", metasBktName) + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to initialize boltdb db %q buckets", fileName) + } + return &BoltDB{db: db, fileName: fileName}, nil +} + +// Put avatar to bolt, key by avatarID. Trying to resize image and lso calculates sha1 of the file for ID func +func (b *BoltDB) Put(userID string, reader io.Reader) (avatar string, err error) { + id := encodeID(userID) + + avatarID := id + imgSfx + err = b.db.Update(func(tx *bolt.Tx) error { + buf := &bytes.Buffer{} + if _, err = io.Copy(buf, reader); err != nil { + return errors.Wrapf(err, "can't read avatar %s", avatarID) + } + + if err = tx.Bucket([]byte(avatarsBktName)).Put([]byte(avatarID), buf.Bytes()); err != nil { + return errors.Wrapf(err, "can't put to bucket with %s", avatarID) + } + // store sha1 of the image + return tx.Bucket([]byte(metasBktName)).Put([]byte(avatarID), []byte(b.sha1(buf.Bytes(), avatarID))) + }) + return avatarID, err +} + +// Get avatar reader for avatar id.image, avatarID used as the direct key +func (b *BoltDB) Get(avatarID string) (reader io.ReadCloser, size int, err error) { + buf := &bytes.Buffer{} + err = b.db.View(func(tx *bolt.Tx) error { + data := tx.Bucket([]byte(avatarsBktName)).Get([]byte(avatarID)) + if data == nil { + return errors.Errorf("can't load avatar %s", avatarID) + } + size, err = buf.Write(data) + return errors.Wrapf(err, "failed to write for %s", avatarID) + }) + return ioutil.NopCloser(buf), size, err +} + +// ID returns a fingerprint of the avatar content. +func (b *BoltDB) ID(avatarID string) (id string) { + data := []byte{} + err := b.db.View(func(tx *bolt.Tx) error { + if data = tx.Bucket([]byte(metasBktName)).Get([]byte(avatarID)); data == nil { + return errors.Errorf("can't load avatar's id for %s", avatarID) + } + return nil + }) + + if err != nil { // failed to get ID, use encoded avatarID + log.Printf("[DEBUG] can't get avatar info '%s', %s", avatarID, err) + return encodeID(avatarID) + } + + return string(data) +} + +// Remove avatar from bolt +func (b *BoltDB) Remove(avatarID string) (err error) { + return b.db.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(avatarsBktName)) + if bkt.Get([]byte(avatarID)) == nil { + return errors.Errorf("avatar key not found, %s", avatarID) + } + if err = tx.Bucket([]byte(avatarsBktName)).Delete([]byte(avatarID)); err != nil { + return errors.Wrapf(err, "can't delete avatar object %s", avatarID) + } + return errors.Wrapf(tx.Bucket([]byte(metasBktName)).Delete([]byte(avatarID)), + "can't delete meta object %s", avatarID) + }) +} + +// List all avatars (ids) from metas bucket +// note: id includes .image suffix +func (b *BoltDB) List() (ids []string, err error) { + err = b.db.View(func(tx *bolt.Tx) error { + return tx.Bucket([]byte(metasBktName)).ForEach(func(k, _ []byte) error { + ids = append(ids, string(k)) + return nil + }) + }) + return ids, errors.Wrap(err, "failed to list") +} + +// Close bolt store +func (b *BoltDB) Close() error { + return errors.Wrapf(b.db.Close(), "failed to close %s", b.fileName) +} + +func (b *BoltDB) sha1(data []byte, avatarID string) (id string) { + h := sha1.New() + if _, err := h.Write(data); err != nil { + log.Printf("[DEBUG] can't apply sha1 for content of '%s', %s", avatarID, err) + return encodeID(avatarID) + } + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/avatar/gridfs.go b/backend/vendor/github.com/go-pkgz/auth/avatar/gridfs.go new file mode 100644 index 00000000..5eeb7214 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/avatar/gridfs.go @@ -0,0 +1,118 @@ +package avatar + +import ( + "bytes" + "io" + "io/ioutil" + "log" + "time" + + "github.com/globalsign/mgo" + "github.com/go-pkgz/mongo" + "github.com/pkg/errors" +) + +// NewGridFS makes gridfs (mongo) avatar store +func NewGridFS(conn *mongo.Connection) *GridFS { + return &GridFS{Connection: conn} +} + +// GridFS implements Store for GridFS +type GridFS struct { + Connection *mongo.Connection +} + +// Put avatar to gridfs object, try to resize +func (gf *GridFS) Put(userID string, reader io.Reader) (avatar string, err error) { + id := encodeID(userID) + err = gf.Connection.WithDB(func(dbase *mgo.Database) error { + fh, e := dbase.GridFS("fs").Create(id + imgSfx) + if e != nil { + return e + } + defer func() { + if err = fh.Close(); err != nil { + log.Printf("[WARN] can't close avatar file %v, %s", fh, err) + } + }() + + _, e = io.Copy(fh, reader) + return e + }) + return id + imgSfx, err +} + +// Get avatar reader for avatar id.image +func (gf *GridFS) Get(avatar string) (reader io.ReadCloser, size int, err error) { + buf := &bytes.Buffer{} + err = gf.Connection.WithDB(func(dbase *mgo.Database) error { + fh, e := dbase.GridFS("fs").Open(avatar) + if e != nil { + return errors.Wrapf(e, "can't load avatar %s", avatar) + } + if _, e = io.Copy(buf, fh); e != nil { + return errors.Wrapf(e, "can't copy avatar %s", avatar) + } + size = int(fh.Size()) + return fh.Close() + }) + return ioutil.NopCloser(buf), size, err +} + +// ID returns a fingerprint of the avatar content. Uses MD5 because gridfs provides it directly +func (gf *GridFS) ID(avatar string) (id string) { + err := gf.Connection.WithDB(func(dbase *mgo.Database) error { + fh, e := dbase.GridFS("fs").Open(avatar) + if e != nil { + return errors.Wrapf(e, "can't open avatar %s", avatar) + } + id = fh.MD5() + return errors.Wrapf(fh.Close(), "can't close avatar") + }) + if err != nil { + log.Printf("[DEBUG] can't get file info '%s', %s", avatar, err) + return encodeID(avatar) + } + return id +} + +// Remove avatar from gridfs +func (gf *GridFS) Remove(avatar string) error { + return gf.Connection.WithDB(func(dbase *mgo.Database) error { + fh, e := dbase.GridFS("fs").Open(avatar) + if e != nil { + return errors.Wrapf(e, "can't get avatar %s", avatar) + } + if e = fh.Close(); e != nil { + log.Printf("[WARN] can't close avatar %s, %s", avatar, e) + } + return dbase.GridFS("fs").Remove(avatar) + }) +} + +// List all avatars (ids) on gfs +// note: id includes .image suffix +func (gf *GridFS) List() (ids []string, err error) { + + type gfsFile struct { + UploadDate time.Time `bson:"uploadDate"` + Length int64 `bson:",minsize"` + MD5 string + Filename string `bson:",omitempty"` + } + + files := []gfsFile{} + err = gf.Connection.WithDB(func(dbase *mgo.Database) error { + return dbase.GridFS("fs").Find(nil).All(&files) + }) + + for _, f := range files { + ids = append(ids, f.Filename) + } + return ids, errors.Wrap(err, "can't list avatars") +} + +// Close gridfs does nothing but satisfies interface +func (gf *GridFS) Close() error { + return nil +} diff --git a/backend/vendor/github.com/go-pkgz/auth/avatar/localfs.go b/backend/vendor/github.com/go-pkgz/auth/avatar/localfs.go new file mode 100644 index 00000000..8317c4ee --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/avatar/localfs.go @@ -0,0 +1,123 @@ +package avatar + +import ( + "fmt" + "hash/crc64" + "io" + "log" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "sync" + + "github.com/pkg/errors" +) + +// LocalFS implements Store for local file system +type LocalFS struct { + storePath string + ctcTable *crc64.Table + once sync.Once +} + +// NewLocalFS makes file-system avatar store +func NewLocalFS(storePath string) *LocalFS { + return &LocalFS{storePath: storePath} +} + +// Put avatar for userID to file and return avatar's file name (base), like 12345678.image +// userID can be avatarID as well, in this case encoding just strip .image prefix +func (fs *LocalFS) Put(userID string, reader io.Reader) (avatar string, err error) { + if reader == nil { + return "", errors.New("empty reader") + } + id := encodeID(userID) + location := fs.location(id) // location adds partition to path + + if e := os.MkdirAll(location, 0755); e != nil { + return "", errors.Wrapf(e, "failed to mkdir avatar location %s", location) + } + + avFile := path.Join(location, id+imgSfx) + fh, err := os.Create(avFile) + if err != nil { + return "", errors.Wrapf(err, "can't create file %s", avFile) + } + defer func() { + if e := fh.Close(); e != nil { + log.Printf("[WARN] can't close avatar file %s, %s", avFile, e) + } + }() + + if _, err = io.Copy(fh, reader); err != nil { + return "", errors.Wrapf(err, "can't save file %s", avFile) + } + log.Printf("[DEBUG] put avatar for %s to %s completed", userID, fh.Name()) + return id + imgSfx, nil +} + +// Get avatar reader for avatar id.image +func (fs *LocalFS) Get(avatar string) (reader io.ReadCloser, size int, err error) { + location := fs.location(strings.TrimSuffix(avatar, imgSfx)) + avFile := path.Join(location, avatar) + fh, err := os.Open(avFile) + if err != nil { + return nil, 0, errors.Wrapf(err, "can't load avatar %s, id", avatar) + } + if fi, e := fh.Stat(); e == nil { + size = int(fi.Size()) + } + return fh, size, nil +} + +// ID returns a fingerprint of the avatar content. +func (fs *LocalFS) ID(avatar string) (id string) { + location := fs.location(strings.TrimSuffix(avatar, imgSfx)) + avFile := path.Join(location, avatar) + fi, err := os.Stat(avFile) + if err != nil { + log.Printf("[DEBUG] can't get file info '%s', %s", avFile, err) + return encodeID(avatar) + } + return encodeID(avatar + strconv.FormatInt(fi.ModTime().Unix(), 10)) +} + +// Remove avatar file +func (fs *LocalFS) Remove(avatar string) error { + location := fs.location(strings.TrimSuffix(avatar, imgSfx)) + avFile := path.Join(location, avatar) + return os.Remove(avFile) +} + +// List all avatars (ids) on local file system +// note: id includes .image suffix +func (fs *LocalFS) List() (ids []string, err error) { + err = filepath.Walk(fs.storePath, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(info.Name(), imgSfx) { + ids = append(ids, info.Name()) + } + return nil + }) + return ids, errors.Wrap(err, "can't list avatars") +} + +// Close gridfs does nothing but satisfies interface +func (fs *LocalFS) Close() error { + return nil +} + +// get location (directory) for user id by adding partition to final path in order to keep files +// in different subdirectories and avoid too many files in a single place. +// the end result is a full path like this - /tmp/avatars.test/92 +func (fs *LocalFS) location(id string) string { + fs.once.Do(func() { fs.ctcTable = crc64.MakeTable(crc64.ECMA) }) + checksum64 := crc64.Checksum([]byte(id), fs.ctcTable) + partition := checksum64 % 100 + return path.Join(fs.storePath, fmt.Sprintf("%02d", partition)) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/avatar/store.go b/backend/vendor/github.com/go-pkgz/auth/avatar/store.go new file mode 100644 index 00000000..8c19fbdb --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/avatar/store.go @@ -0,0 +1,62 @@ +package avatar + +//go:generate sh -c "mockery -inpkg -name Store -print > /tmp/mock.tmp && mv /tmp/mock.tmp store_mock.go" + +import ( + "crypto/sha1" + "strings" + + // Initializing packages for supporting GIF and JPEG formats. + _ "image/gif" + _ "image/jpeg" + "io" + "log" + "regexp" + + "github.com/go-pkgz/auth/token" +) + +// imgSfx for avatars +const imgSfx = ".image" + +var reValidAvatarID = regexp.MustCompile(`^[a-fA-F0-9]{40}\.image$`) + +// Store defines interface to store and and load avatars +type Store interface { + Put(userID string, reader io.Reader) (avatarID string, err error) // save avatar data from the reader and return base name + Get(avatarID string) (reader io.ReadCloser, size int, err error) // load avatar via reader + ID(avatarID string) (id string) // unique id of stored avatar's data + Remove(avatarID string) error // remove avatar data + List() (ids []string, err error) // list all avatar ids + Close() error // close store +} + +// Migrate avatars between stores +func Migrate(dst Store, src Store) (int, error) { + ids, err := src.List() + if err != nil { + return 0, err + } + for _, id := range ids { + srcReader, _, err := src.Get(id) + if err != nil { + log.Printf("[WARN] can't get reader for avatar %s", id) + continue + } + if _, err = dst.Put(id, srcReader); err != nil { + log.Printf("[WARN] can't put avatar %s", id) + } + if err = srcReader.Close(); err != nil { + log.Printf("[WARN] failed to close avatar %s", id) + } + } + return len(ids), nil +} + +// encodeID hashes id to sha1. Skip encoding for already processed +func encodeID(id string) string { + if reValidAvatarID.MatchString(id) { + return strings.TrimSuffix(id, imgSfx) // already encoded, strip .image + } + return token.HashID(sha1.New(), id) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/go.mod b/backend/vendor/github.com/go-pkgz/auth/go.mod new file mode 100644 index 00000000..2369b3bd --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/go.mod @@ -0,0 +1,23 @@ +module github.com/go-pkgz/auth + +require ( + cloud.google.com/go v0.34.0 // indirect + github.com/boltdb/bolt v1.3.1 // indirect + github.com/coreos/bbolt v1.3.0 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 + github.com/go-errors/errors v1.0.1 + github.com/go-pkgz/mongo v1.0.0 + github.com/go-pkgz/rest v1.1.1 + github.com/kr/pretty v0.1.0 // indirect + github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 + github.com/pkg/errors v0.8.0 + github.com/stretchr/testify v1.2.2 + golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b + golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect + golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 + golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect + golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 // indirect + google.golang.org/appengine v1.4.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect +) diff --git a/backend/vendor/github.com/go-pkgz/auth/go.sum b/backend/vendor/github.com/go-pkgz/auth/go.sum new file mode 100644 index 00000000..2676c7cb --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/go.sum @@ -0,0 +1,50 @@ +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/coreos/bbolt v1.3.0 h1:HIgH5xUWXT914HCI671AxuTTqjj64UOFr7pHn48LUTI= +github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-pkgz/mongo v1.0.0 h1:9jijAK7prCRMetiyTu3c1rv/2lMypzuf2DWcVpTlwzw= +github.com/go-pkgz/mongo v1.0.0/go.mod h1:R9si/F2aJsjz4MUxhzuppIHY8yLV3YCeuCpgcI50cu4= +github.com/go-pkgz/rest v1.1.1 h1:YuLe+wOJwcE+Y0SkJ+AtvUOPGjTMe4Q4vg98Uqs9CKc= +github.com/go-pkgz/rest v1.1.1/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc= +github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM= +golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 h1:IcgEB62HYgAhX0Nd/QrVgZlxlcyxbGQHElLUhW2X4Fo= +golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go new file mode 100644 index 00000000..80537ea6 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go @@ -0,0 +1,200 @@ +// Package middleware provides oauth2 support as well as related middlewares. +package middleware + +import ( + "encoding/base64" + "log" + "net/http" + "strings" + + "github.com/pkg/errors" + + "github.com/go-pkgz/auth/provider" + "github.com/go-pkgz/auth/token" +) + +// Authenticator is top level token object providing middlewares +type Authenticator struct { + JWTService *token.Service + Providers []provider.Service + Validator token.Validator + DevPasswd string +} + +var devUser = token.User{ + ID: "dev", + Name: "developer one", + Attributes: map[string]interface{}{ + "admin": true, + }, +} + +var adminUser = token.User{ + ID: "admin", + Name: "admin", + Attributes: map[string]interface{}{ + "admin": true, + }, +} + +// Auth middleware adds token from session and populates user info +func (a *Authenticator) Auth(next http.Handler) http.Handler { + return a.auth(true)(next) +} + +// Trace middleware doesn't require valid user but if user info presented populates info +func (a *Authenticator) Trace(next http.Handler) http.Handler { + return a.auth(false)(next) +} + +func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { + + onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) { + if err == nil { + return + } + if !reqAuth { + h.ServeHTTP(w, r) + return + } + log.Printf("[DEBUG] failed token, %s", err) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + + f := func(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + + // if secret key matches for given site (from request) return admin user + if a.checkSecretKey(r) { + r = token.SetUserInfo(r, adminUser) + h.ServeHTTP(w, r) + return + } + + // use dev user basic token if enabled + if a.basicDevUser(r) { + r = token.SetUserInfo(r, devUser) + h.ServeHTTP(w, r) + return + } + + claims, tkn, err := a.JWTService.Get(r) + if err != nil { + onError(h, w, r, errors.Wrap(err, "can't get token")) + return + } + + if claims.Handshake != nil { // handshake in token indicate special use cases, not for login + onError(h, w, r, errors.Errorf("invalid kind of token for %s/%s", claims.User.Name, claims.User.ID)) + return + } + + if claims.User == nil { + onError(h, w, r, errors.New("failed token, no user info presented in the claim")) + return + } + + if claims.User != nil { // if uinfo in token populate it to context + // validator passed by client and performs check on token or/and claims + if a.Validator != nil && !a.Validator.Validate(tkn, claims) { + onError(h, w, r, errors.Errorf("user %s/%s blocked", claims.User.Name, claims.User.ID)) + a.JWTService.Reset(w) + return + } + + if a.JWTService.IsExpired(claims) { + if claims, err = a.refreshExpiredToken(w, claims); err != nil { + a.JWTService.Reset(w) + onError(h, w, r, errors.Wrap(err, "can't refresh token")) + return + } + log.Printf("[DEBUG] token refreshed for %+v", claims.User) + } + + r = token.SetUserInfo(r, *claims.User) // populate user info to request context + } + + h.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } + return f +} + +func (a *Authenticator) checkSecretKey(r *http.Request) bool { + if a.JWTService.SecretReader == nil { + return false + } + + aud := r.URL.Query().Get("aud") + secret := r.URL.Query().Get("secret") + + skey, err := a.JWTService.SecretReader.Get(aud) + if err != nil { + return false + } + + if strings.TrimSpace(secret) == "" || secret != skey { + return false + } + return true +} + +// refreshExpiredToken makes new token with passed claims, but only if permission allowed +func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token.Claims) (token.Claims, error) { + // refresh token + if err := a.JWTService.Set(w, claims, false); err != nil { + return token.Claims{}, err + } + return claims, nil +} + +// AdminOnly middleware allows access for admins only +func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + + user, err := token.GetUserInfo(r) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if !user.IsAdmin() { + http.Error(w, "Access denied", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +func (a *Authenticator) basicDevUser(r *http.Request) bool { + + if a.DevPasswd == "" { + return false + } + + s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) + if len(s) != 2 { + return false + } + + b, err := base64.StdEncoding.DecodeString(s[1]) + if err != nil { + log.Printf("[WARN] dev user token failed, failed to decode %s, %s", s[1], err) + return false + } + + pair := strings.SplitN(string(b), ":", 2) + if len(pair) != 2 { + log.Printf("[WARN] dev user token failed, failed to split %s", string(b)) + return false + } + + if pair[0] != "dev" || pair[1] != a.DevPasswd { + log.Printf("[WARN] dev user token failed, user/passwd mismatch %+v", pair) + return false + } + + return true +} diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go new file mode 100644 index 00000000..514a35e9 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go @@ -0,0 +1,199 @@ +package provider + +import ( + "bytes" + "context" + "fmt" + "log" + "net/http" + "strings" + "sync" + "time" + + "github.com/nullrocks/identicon" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/go-pkgz/auth/token" +) + +const devAuthPort = 8084 + +// DevAuthServer is a fake oauth server for development +// it provides stand-alone server running on its own port and pretending to be the real oauth2. It also provides +// Dev Provider the same way as normal providers do, i.e. like github, google and others. +// can run in interactive and non-interactive mode. In interactive mode login attempts will show login form to select +// desired user name, this is the mode used for development. Non-interactive mode for tests only. +type DevAuthServer struct { + Provider Service + Automatic bool + + username string // unsafe, but fine for dev + + iconGen *identicon.Generator + httpServer *http.Server + lock sync.Mutex +} + +// Run oauth2 dev server on port devAuthPort +func (d *DevAuthServer) Run() { + d.username = "dev_user" + log.Printf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.RedirectURL) + d.lock.Lock() + var err error + d.iconGen, err = identicon.New("github", 5, 3) + if err != nil { + log.Printf("[WARN] can't create identicon, %s", err) + } + + d.httpServer = &http.Server{ + Addr: fmt.Sprintf(":%d", devAuthPort), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("[DEBUG] dev oauth request %s %s %+v", r.Method, r.URL, r.Header) + switch { + + case strings.HasPrefix(r.URL.Path, "/login/oauth/authorize"): + + // first time it will be called without username and will ask for one + if !d.Automatic && (r.ParseForm() != nil || r.Form.Get("username") == "") { + if _, err = w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil { + log.Printf("[WARN] can't write, %s", err) + } + return + } + + if !d.Automatic { + d.username = r.Form.Get("username") + } + + state := r.URL.Query().Get("state") + callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.RedirectURL, state) + log.Printf("[DEBUG] callback url=%s", callbackURL) + w.Header().Add("Location", callbackURL) + w.WriteHeader(http.StatusFound) + + case strings.HasPrefix(r.URL.Path, "/login/oauth/access_token"): + res := `{ + "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", + "token_type":"bearer", + "expires_in":3600, + "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", + "scope":"create", + "state":"12345678" + }` + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, err = w.Write([]byte(res)); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + case strings.HasPrefix(r.URL.Path, "/user"): + ava := fmt.Sprintf("http://127.0.0.1:%d/avatar?user=%s", devAuthPort, d.username) + res := fmt.Sprintf(`{ + "id": "%s", + "name":"%s", + "picture":"%s" + }`, d.username, d.username, ava) + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, err = w.Write([]byte(res)); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + case strings.HasPrefix(r.URL.Path, "/avatar"): + user := r.URL.Query().Get("user") + b, e := d.genAvatar(user) + if e != nil { + w.WriteHeader(http.StatusNotFound) + return + } + if _, err = w.Write(b); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + default: + w.WriteHeader(http.StatusBadRequest) + } + }), + } + d.lock.Unlock() + + err = d.httpServer.ListenAndServe() + log.Printf("[WARN] dev oauth2 server terminated, %s", err) +} + +// Shutdown oauth2 dev server +func (d *DevAuthServer) Shutdown() { + log.Print("[WARN] shutdown oauth2 dev server") + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + d.lock.Lock() + if d.httpServer != nil { + if err := d.httpServer.Shutdown(ctx); err != nil { + log.Printf("[DEBUG] oauth2 dev shutdown error, %s", err) + } + } + log.Print("[DEBUG] shutdown dev oauth2 server completed") + d.lock.Unlock() +} + +// NewDev makes dev oauth2 provider for admin user +func NewDev(p Params) Service { + return initService(p, Service{ + Name: "dev", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/authorize", devAuthPort), + TokenURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/access_token", devAuthPort), + }, + RedirectURL: p.URL + "/auth/dev/callback", + Scopes: []string{"user:email"}, + InfoURL: fmt.Sprintf("http://127.0.0.1:%d/user", devAuthPort), + MapUser: func(data userData, _ []byte) token.User { + userInfo := token.User{ + ID: data.value("id"), + Name: data.value("name"), + Picture: data.value("picture"), + } + return userInfo + }, + }) +} + +func (d *DevAuthServer) genAvatar(user string) ([]byte, error) { + if d.iconGen == nil { + return nil, errors.Errorf("no iconGen, skip avatar generation for %s", user) + } + + ii, err := d.iconGen.Draw(user) // Generate an IdentIcon + if err != nil { + return nil, errors.Wrapf(err, "failed to draw avatar for %s", user) + } + + buf := &bytes.Buffer{} + err = ii.Png(300, buf) + return buf.Bytes(), err +} + +var devUserForm = ` + + + Dev User + + + +
+ username: + +
+ + +` diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go new file mode 100644 index 00000000..0241b347 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go @@ -0,0 +1,127 @@ +package provider + +import ( + "crypto/sha1" + "encoding/json" + "fmt" + + "golang.org/x/oauth2/facebook" + "golang.org/x/oauth2/github" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/yandex" + + "github.com/go-pkgz/auth/token" +) + +// NewGoogle makes google oauth2 provider +func NewGoogle(p Params) Service { + return initService(p, Service{ + Name: "google", + Endpoint: google.Endpoint, + RedirectURL: p.URL + "/token/google/callback", + Scopes: []string{"https://www.googleapis.com/token/userinfo.profile"}, + InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo", + MapUser: func(data userData, _ []byte) token.User { + userInfo := token.User{ + // encode email with provider name to avoid collision if same id returned by other provider + ID: "google_" + token.HashID(sha1.New(), data.value("sub")), + Name: data.value("name"), + Picture: data.value("picture"), + } + if userInfo.Name == "" { + userInfo.Name = "noname_" + userInfo.ID[8:12] + } + return userInfo + }, + }) +} + +// NewGithub makes github oauth2 provider +func NewGithub(p Params) Service { + return initService(p, Service{ + Name: "github", + Endpoint: github.Endpoint, + RedirectURL: p.URL + "/token/github/callback", + Scopes: []string{}, + InfoURL: "https://api.github.com/user", + MapUser: func(data userData, _ []byte) token.User { + userInfo := token.User{ + ID: "github_" + token.HashID(sha1.New(), data.value("login")), + Name: data.value("name"), + Picture: data.value("avatar_url"), + } + // github may have no user name, use login in this case + if userInfo.Name == "" { + userInfo.Name = data.value("login") + } + return userInfo + }, + }) +} + +// NewFacebook makes facebook oauth2 provider +func NewFacebook(p Params) Service { + + // response format for fb /me call + type uinfo struct { + ID string `json:"id"` + Name string `json:"name"` + Picture struct { + Data struct { + URL string `json:"url"` + } `json:"data"` + } `json:"picture"` + } + + return initService(p, Service{ + Name: "facebook", + Endpoint: facebook.Endpoint, + RedirectURL: p.URL + "/token/facebook/callback", + Scopes: []string{"public_profile"}, + InfoURL: "https://graph.facebook.com/me?fields=id,name,picture", + MapUser: func(data userData, bdata []byte) token.User { + userInfo := token.User{ + ID: "facebook_" + token.HashID(sha1.New(), data.value("id")), + Name: data.value("name"), + } + if userInfo.Name == "" { + userInfo.Name = userInfo.ID[0:16] + } + + uinfoJSON := uinfo{} + if err := json.Unmarshal(bdata, &uinfoJSON); err == nil { + userInfo.Picture = uinfoJSON.Picture.Data.URL + } + return userInfo + }, + }) +} + +// NewYandex makes yandex oauth2 provider +func NewYandex(p Params) Service { + return initService(p, Service{ + Name: "yandex", + Endpoint: yandex.Endpoint, + RedirectURL: p.URL + "/token/yandex/callback", + Scopes: []string{}, + // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ + InfoURL: "https://login.yandex.ru/info?format=json", + MapUser: func(data userData, _ []byte) token.User { + userInfo := token.User{ + ID: "yandex_" + token.HashID(sha1.New(), data.value("id")), + Name: data.value("display_name"), // using Display Name by default + } + if userInfo.Name == "" { + userInfo.Name = data.value("real_name") // using Real Name (== full name) if Display Name is empty + } + if userInfo.Name == "" { + userInfo.Name = data.value("login") // otherwise using login + } + + if data.value("default_avatar_id") != "" { + userInfo.Picture = fmt.Sprintf("https://avatars.yandex.net/get-yapic/%s/islands-200", data.value("default_avatar_id")) + } + return userInfo + }, + }) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/service.go b/backend/vendor/github.com/go-pkgz/auth/provider/service.go new file mode 100644 index 00000000..9d05b828 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/provider/service.go @@ -0,0 +1,247 @@ +package provider + +import ( + "context" + "crypto/rand" + "crypto/sha1" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/go-pkgz/rest" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/go-pkgz/auth/avatar" + "github.com/go-pkgz/auth/token" +) + +// Service represents oauth2 provider +type Service struct { + Params + Name string + RedirectURL string + InfoURL string + Endpoint oauth2.Endpoint + Scopes []string + MapUser func(userData, []byte) token.User // map info from InfoURL to User + conf oauth2.Config +} + +// Params to make initialized and ready to use provider +type Params struct { + URL string + JwtService *token.Service + AvatarProxy *avatar.Proxy + Cid string + Csecret string + Issuer string +} + +type userData map[string]interface{} + +func (u userData) value(key string) string { + // json.Unmarshal converts json "null" value to go's "nil", in this case return empty string + if val, ok := u[key]; ok && val != nil { + return fmt.Sprintf("%v", val) + } + return "" +} + +// initService makes token service for given provider +func initService(p Params, service Service) Service { + log.Printf("[INFO] init token service %s", service.Name) + service.Params = p + service.conf = oauth2.Config{ + ClientID: service.Cid, + ClientSecret: service.Csecret, + RedirectURL: service.RedirectURL, + Scopes: service.Scopes, + Endpoint: service.Endpoint, + } + + log.Printf("[DEBUG] created %s token, id=%s, redir=%s, endpoint=%s", + service.Name, service.Cid, service.Endpoint, service.RedirectURL) + return service +} + +// Handler returns auth routes for given provider +func (p Service) Handler(w http.ResponseWriter, r *http.Request) { + + if r.Method != "GET" { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + if strings.HasSuffix(r.URL.Path, "/login") { + p.loginHandler(w, r) + return + } + if strings.HasSuffix(r.URL.Path, "/callback") { + p.authHandler(w, r) + return + } + if strings.HasSuffix(r.URL.Path, "/logout") { + p.LogoutHandler(w, r) + return + } + w.WriteHeader(http.StatusNotFound) +} + +// loginHandler - GET /login?from=redirect-back-url&site=siteID&session=1 +func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) { + + log.Printf("[DEBUG] login with %s", p.Name) + // make state (random) and store in session + state, err := p.randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make oauth2 state") + return + } + + cid, err := p.randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") + return + } + + claims := token.Claims{ + Handshake: &token.Handshake{ + State: state, + From: r.URL.Query().Get("from"), + }, + SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0", + StandardClaims: jwt.StandardClaims{ + Id: cid, + Audience: r.URL.Query().Get("site"), + ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), + NotBefore: time.Now().Add(-1 * time.Minute).Unix(), + }, + } + + if err := p.JwtService.Set(w, claims, false); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") + return + } + + // return login url + loginURL := p.conf.AuthCodeURL(state) + log.Printf("[DEBUG] login url %s, claims=%+v", loginURL, claims) + + 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 Service) 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 token") + return + } + + retrievedState := oauthClaims.Handshake.State + if retrievedState == "" || retrievedState != r.URL.Query().Get("state") { + http.Error(w, fmt.Sprintf("unexpected state %v", retrievedState), http.StatusUnauthorized) + return + } + + log.Printf("[DEBUG] token 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) + u = p.setAvatar(u) + + cid, err := p.randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") + return + } + claims := token.Claims{ + User: &u, + StandardClaims: jwt.StandardClaims{ + Issuer: p.Issuer, + Id: cid, + Audience: oauthClaims.Audience, + }, + SessionOnly: oauthClaims.SessionOnly, + } + + if err = p.JwtService.Set(w, claims, oauthClaims.SessionOnly); 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.Handshake != nil && oauthClaims.Handshake.From != "" { + http.Redirect(w, r, oauthClaims.Handshake.From, http.StatusTemporaryRedirect) + return + } + rest.RenderJSON(w, r, &u) +} + +// setAvatar saves avatar and puts proxied URL to u.Picture +func (p Service) setAvatar(u token.User) token.User { + if p.AvatarProxy != nil { + if avatarURL, e := p.AvatarProxy.Put(u); e == nil { + u.Picture = avatarURL + } else { + log.Printf("[WARN] failed to set avatar for %+v, %+v", u, e) + } + } + return u +} + +// LogoutHandler - GET /logout +func (p Service) LogoutHandler(w http.ResponseWriter, r *http.Request) { + p.JwtService.Reset(w) + log.Printf("[DEBUG] logout") +} + +func (p Service) randToken() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", errors.Wrap(err, "can't get random") + } + s := sha1.New() + if _, err := s.Write(b); err != nil { + return "", errors.Wrap(err, "can't write randoms to sha1") + } + return fmt.Sprintf("%x", s.Sum(nil)), nil +} diff --git a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go new file mode 100644 index 00000000..97f5f2bc --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go @@ -0,0 +1,282 @@ +package token + +import ( + "net/http" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/pkg/errors" +) + +// Service wraps jwt operations +// supports both header and cookie tokens +type Service struct { + Opts +} + +// Claims stores user info for token and state & from from login +type Claims struct { + jwt.StandardClaims + User *User `json:"user,omitempty"` // user info + SessionOnly bool `json:"sess_only,omitempty"` + Handshake *Handshake `json:"handshake,omitempty"` // used for oauth handshake +} + +// Handshake used for oauth handshake +type Handshake struct { + State string `json:"state,omitempty"` + From string `json:"from,omitempty"` + ID string `json:"id,omitempty"` +} + +// default names for cookies and headers +const ( + jwtCookieName = "JWT" + jwtHeaderKey = "X-JWT" + xsrfCookieName = "XSRF-TOKEN" + xsrfHeaderKey = "X-XSRF-TOKEN" + issuer = "go-pkgz/auth" + tokenDuration = time.Minute * 15 + cookieDuration = time.Hour * 24 * 31 +) + +// Opts holds constructor params +type Opts struct { + SecretReader Secret + ClaimsUpd ClaimsUpdater + SecureCookies bool + TokenDuration time.Duration + CookieDuration time.Duration + DisableXSRF bool + + // optional (custom) names for cookies and headers + JWTCookieName string + JWTHeaderKey string + XSRFCookieName string + XSRFHeaderKey string + + Issuer string // optional value for iss claim, usually application name +} + +// NewService makes JWT service +func NewService(opts Opts) *Service { + res := Service{Opts: opts} + + setDefault := func(fld *string, def string) { + if *fld == "" { + *fld = def + } + } + + setDefault(&res.JWTCookieName, jwtCookieName) + setDefault(&res.JWTHeaderKey, jwtHeaderKey) + setDefault(&res.XSRFCookieName, xsrfCookieName) + setDefault(&res.XSRFHeaderKey, xsrfHeaderKey) + setDefault(&res.Issuer, issuer) + + if opts.TokenDuration == 0 { + res.TokenDuration = tokenDuration + } + + if opts.CookieDuration == 0 { + res.CookieDuration = cookieDuration + } + + return &res +} + +// Token makes token with claims +func (j *Service) Token(claims Claims) (string, error) { + + // update claims with ClaimsUpdFunc defined by consumer + if j.ClaimsUpd != nil { + claims = j.ClaimsUpd.Update(claims) + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secret, err := j.SecretReader.Get(claims.Audience) // get secret via consumer defined SecretReader + if err != nil { + return "", errors.Wrap(err, "can't get secret") + } + + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return "", errors.Wrap(err, "can't sign token token") + } + return tokenString, nil +} + +// Parse token string and verify. Not checking for expiration +func (j *Service) Parse(tokenString string) (Claims, error) { + parser := jwt.Parser{SkipClaimsValidation: true} // allow parsing of expired tokens + + getAud := func() (aud string, err error) { // parse token without signature check to get id (aud) + preToken, _, err := parser.ParseUnverified(tokenString, &Claims{}) + if err != nil { + return "", errors.Wrap(err, "can't pre-parse token") + } + preClaims, ok := preToken.Claims.(*Claims) + if !ok { + return "", errors.New("invalid token") + } + return preClaims.Audience, nil + } + + aud, err := getAud() + if err != nil { + return Claims{}, errors.Wrap(err, "failed to get aud from token token") + } + + secret, err := j.SecretReader.Get(aud) + if err != nil { + return Claims{}, errors.Wrap(err, "can't get secret") + } + + token, err := parser.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(secret), nil + }) + if err != nil { + return Claims{}, errors.Wrap(err, "can't parse token") + } + + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return Claims{}, errors.New("invalid token") + } + + return *claims, nil +} + +// Set creates token cookie with xsrf cookie and put it to ResponseWriter +// accepts claims and sets expiration if none defined. permanent flag means long-living cookie, +// false makes it session only. +func (j *Service) Set(w http.ResponseWriter, claims Claims, sessionOnly bool) error { + if claims.ExpiresAt == 0 { + claims.ExpiresAt = time.Now().Add(j.TokenDuration).Unix() + } + + claims.Issuer = j.Issuer + + tokenString, err := j.Token(claims) + if err != nil { + return errors.Wrap(err, "failed to make token token") + } + + cookieExpiration := 0 // session cookie + if !sessionOnly { + cookieExpiration = int(j.CookieDuration.Seconds()) + } + + jwtCookie := http.Cookie{Name: jwtCookieName, Value: tokenString, HttpOnly: true, Path: "/", + MaxAge: cookieExpiration, Secure: j.SecureCookies} + http.SetCookie(w, &jwtCookie) + + xsrfCookie := http.Cookie{Name: xsrfCookieName, Value: claims.Id, HttpOnly: false, Path: "/", + MaxAge: cookieExpiration, Secure: j.SecureCookies} + http.SetCookie(w, &xsrfCookie) + + return nil +} + +// Get token from header or cookie +// if cookie used, verify xsrf token to match +func (j *Service) Get(r *http.Request) (Claims, string, error) { + + fromCookie := false + tokenString := "" + + // try to get from X-JWT header + if tokenHeader := r.Header.Get(jwtHeaderKey); tokenHeader != "" { + tokenString = tokenHeader + } + + // try to get from JWT cookie + if tokenString == "" { + fromCookie = true + jc, err := r.Cookie(jwtCookieName) + if err != nil { + return Claims{}, "", errors.Wrap(err, "token cookie was not presented") + } + tokenString = jc.Value + } + + claims, err := j.Parse(tokenString) + if err != nil { + return Claims{}, "", errors.Wrap(err, "failed to get token") + } + + if j.DisableXSRF { + return claims, tokenString, nil + } + + if fromCookie && claims.User != nil { + xsrf := r.Header.Get(xsrfHeaderKey) + if claims.Id != xsrf { + return Claims{}, "", errors.New("xsrf mismatch") + } + } + return claims, tokenString, nil +} + +// IsExpired returns true if claims expired +func (j *Service) IsExpired(claims Claims) bool { + return !claims.VerifyExpiresAt(time.Now().Unix(), true) +} + +// Reset token's cookies +func (j *Service) Reset(w http.ResponseWriter) { + jwtCookie := http.Cookie{Name: jwtCookieName, Value: "", HttpOnly: false, Path: "/", + MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.SecureCookies} + http.SetCookie(w, &jwtCookie) + + xsrfCookie := http.Cookie{Name: xsrfCookieName, Value: "", HttpOnly: false, Path: "/", + MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.SecureCookies} + http.SetCookie(w, &xsrfCookie) +} + +// Secret defines interface returning secret key for given id (aud) +type Secret interface { + Get(id string) (string, error) +} + +// SecretFunc type is an adapter to allow the use of ordinary functions as Secret. If f is a function +// with the appropriate signature, SecretFunc(f) is a Handler that calls f. +type SecretFunc func(id string) (string, error) + +// Get calls f(id) +func (f SecretFunc) Get(id string) (string, error) { + return f(id) +} + +// ClaimsUpdater defines interface adding extras to claims +type ClaimsUpdater interface { + Update(claims Claims) Claims +} + +// ClaimsUpdFunc type is an adapter to allow the use of ordinary functions as ClaimsUpdater. If f is a function +// with the appropriate signature, ClaimsUpdFunc(f) is a Handler that calls f. +type ClaimsUpdFunc func(claims Claims) Claims + +// Update calls f(id) +func (f ClaimsUpdFunc) Update(claims Claims) Claims { + return f(claims) +} + +// Validator defines interface to accept o reject claims with consumer defined logic +// It works with valid token and allows to reject some, based on token match or user's fields +type Validator interface { + Validate(token string, claims Claims) bool +} + +// ValidatorFunc type is an adapter to allow the use of ordinary functions as Validator. If f is a function +// with the appropriate signature, ValidatorFunc(f) is a Validator that calls f. +type ValidatorFunc func(token string, claims Claims) bool + +// Validate calls f(id) +func (f ValidatorFunc) Validate(token string, claims Claims) bool { + return f(token, claims) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/token/user.go b/backend/vendor/github.com/go-pkgz/auth/token/user.go new file mode 100644 index 00000000..afbeb0d5 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/token/user.go @@ -0,0 +1,126 @@ +package token + +import ( + "context" + "encoding/hex" + "fmt" + "hash" + "hash/crc64" + "io" + "log" + "net/http" + "regexp" + + "github.com/pkg/errors" +) + +var reValidSha = regexp.MustCompile("^[a-fA-F0-9]{40}$") +var reValidCrc64 = regexp.MustCompile("^[a-fA-F0-9]{16}$") + +const adminAttr = "admin" // predefined attribute key for bool isAdmin status + +// User is the basic part of oauth data provided by service +type User struct { + Name string `json:"name"` + ID string `json:"id"` + Picture string `json:"picture"` + IP string `json:"ip,omitempty"` + Email string `json:"email,omitempty"` + + Attributes map[string]interface{} `json:"attrs,omitempty"` +} + +// SetBoolAttr sets boolean attribute +func (u *User) SetBoolAttr(key string, val bool) { + if u.Attributes == nil { + u.Attributes = map[string]interface{}{} + } + u.Attributes[key] = val +} + +// SetStrAttr sets string attribute +func (u *User) SetStrAttr(key string, val string) { + if u.Attributes == nil { + u.Attributes = map[string]interface{}{} + } + u.Attributes[key] = val +} + +// BoolAttr gets boolean attribute +func (u *User) BoolAttr(key string) bool { + r, ok := u.Attributes[key].(bool) + if !ok { + return false + } + return r +} + +// StrAttr gets string attribute +func (u *User) StrAttr(key string) string { + r, ok := u.Attributes[key].(string) + if !ok { + return "" + } + return r +} + +// SetAdmin is a shortcut to set "admin" attribute +func (u *User) SetAdmin(val bool) { + u.SetBoolAttr(adminAttr, val) +} + +// IsAdmin is a shortcut to get admin attribute +func (u *User) IsAdmin() bool { + return u.BoolAttr(adminAttr) +} + +// HashID tries to has val with hash.Hash and fallback to crc if needed +func HashID(h hash.Hash, val string) string { + + if reValidSha.MatchString(val) { + return val // already hashed or empty + } + + if _, err := io.WriteString(h, val); err != nil { + // fail back to crc64 + log.Printf("[WARN] can't hash id %s, %s", val, err) + if reValidCrc64.MatchString(val) { + return val // already crced + } + return fmt.Sprintf("%x", crc64.Checksum([]byte(val), crc64.MakeTable(crc64.ECMA))) + } + return hex.EncodeToString(h.Sum(nil)) +} + +type contextKey string + +// MustGetUserInfo fails if can't extract user data from the request. +// should be called from authenticated controllers only +func MustGetUserInfo(r *http.Request) User { + user, err := GetUserInfo(r) + if err != nil { + panic(err) + } + return user +} + +// GetUserInfo returns user from request context +func GetUserInfo(r *http.Request) (user User, err error) { + + ctx := r.Context() + if ctx == nil { + return User{}, errors.New("no info about user") + } + if u, ok := ctx.Value(contextKey("user")).(User); ok { + return u, nil + } + + return User{}, errors.New("user can't be parsed") +} + +// SetUserInfo sets user into request context +func SetUserInfo(r *http.Request, user User) *http.Request { + ctx := r.Context() + ctx = context.WithValue(ctx, contextKey("user"), user) + return r.WithContext(ctx) +} From 085468e788797e36fe72262827b764707e13457f Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 27 Dec 2018 23:49:06 -0600 Subject: [PATCH 04/21] missing remark url in auth init --- backend/app/cmd/server.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index db073e68..84dd3102 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -227,6 +227,8 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { } authenticator := auth.NewService(auth.Opts{ + URL: s.RemarkURL, + Issuer: "remark42", TokenDuration: s.Auth.TTL.JWT, CookieDuration: s.Auth.TTL.Cookie, SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"), @@ -234,6 +236,9 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { return adminStore.Key(id) }), ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { + if c.User == nil { + return c + } c.User.SetAdmin(dataService.IsAdmin(c.Audience, c.User.ID)) return c }), From 09de97ffd62e6f5fb48cac00658ff793853e9d2d Mon Sep 17 00:00:00 2001 From: Umputun Date: Fri, 28 Dec 2018 00:02:35 -0600 Subject: [PATCH 05/21] resort imports --- backend/Gopkg.lock | 12 ---- backend/app/cmd/avatar.go | 5 +- backend/app/cmd/server.go | 3 +- backend/app/rest/api/admin.go | 1 + backend/app/rest/api/migrator.go | 1 + backend/app/rest/api/rest.go | 2 +- backend/app/rest/api/rest_private.go | 6 +- backend/app/rest/api/rest_public.go | 1 + backend/app/rest/api/rss.go | 3 +- backend/app/rest/api/ssl.go | 3 +- backend/app/rest/proxy/image.go | 4 +- backend/app/store/admin/mongo.go | 3 +- backend/app/store/engine/mongo.go | 3 +- backend/app/store/service/service.go | 3 +- .../commons/pkg/repeater/.gitlab-ci.yml | 25 -------- .../commons/pkg/repeater/README.md | 41 ------------- .../commons/pkg/repeater/repeater.go | 60 ------------------- .../commons/pkg/repeater/strategy/backoff.go | 56 ----------------- .../commons/pkg/repeater/strategy/fixed.go | 41 ------------- .../commons/pkg/repeater/strategy/strategy.go | 28 --------- 20 files changed, 22 insertions(+), 279 deletions(-) delete mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/.gitlab-ci.yml delete mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/README.md delete mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/repeater.go delete mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/backoff.go delete mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/fixed.go delete mode 100644 backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/strategy.go diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index 69c8931e..7beda51c 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -9,17 +9,6 @@ revision = "767c40d6a2e058483c25fa193e963a22da17236d" version = "v0.18.0" -[[projects]] - digest = "1:6f958db63973bc397ef72acacbd56e045b4a0160af1224d6eb0f20deb860c0cd" - name = "git.tkginternal.com/commons/pkg/repeater" - packages = [ - ".", - "strategy", - ] - pruneopts = "UT" - revision = "a207227f9303dc677c4d9644f709ad1e29bd0940" - version = "v1.0.0" - [[projects]] digest = "1:bff7b2530f02b143623e260c11df5cbf34e0faeaca6aa001a8be31f333518ca9" name = "github.com/PuerkitoBio/goquery" @@ -400,7 +389,6 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ - "git.tkginternal.com/commons/pkg/repeater", "github.com/PuerkitoBio/goquery", "github.com/coreos/bbolt", "github.com/dgrijalva/jwt-go", diff --git a/backend/app/cmd/avatar.go b/backend/app/cmd/avatar.go index f623cc99..82093821 100644 --- a/backend/app/cmd/avatar.go +++ b/backend/app/cmd/avatar.go @@ -5,10 +5,11 @@ import ( "path" "time" - "github.com/coreos/bbolt" + bolt "github.com/coreos/bbolt" + "github.com/pkg/errors" + "github.com/go-pkgz/auth/avatar" "github.com/go-pkgz/mongo" - "github.com/pkg/errors" ) // AvatarCommand set of flags and command for avatar migration diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 84dd3102..d32f4968 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -12,14 +12,13 @@ import ( "syscall" "time" - "github.com/go-pkgz/auth/token" - bolt "github.com/coreos/bbolt" "github.com/pkg/errors" "github.com/go-pkgz/auth" "github.com/go-pkgz/auth/avatar" "github.com/go-pkgz/auth/provider" + "github.com/go-pkgz/auth/token" "github.com/go-pkgz/mongo" "github.com/go-pkgz/rest/cache" diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index 2cfe1479..107a6675 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -9,6 +9,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + "github.com/go-pkgz/auth" R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" diff --git a/backend/app/rest/api/migrator.go b/backend/app/rest/api/migrator.go index 7dad7c04..90ec7bd6 100644 --- a/backend/app/rest/api/migrator.go +++ b/backend/app/rest/api/migrator.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/pkg/errors" diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index a4257685..86b826bf 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -19,10 +19,10 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/cors" "github.com/go-chi/render" - "github.com/go-pkgz/auth" "github.com/pkg/errors" "github.com/rakyll/statik/fs" + "github.com/go-pkgz/auth" R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/go-pkgz/rest/logger" diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index a0fd94b4..0006b01b 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -10,14 +10,14 @@ import ( "strings" "time" - "github.com/go-pkgz/auth/token" - jwt "github.com/dgrijalva/jwt-go" "github.com/go-chi/chi" "github.com/go-chi/render" + multierror "github.com/hashicorp/go-multierror" + + "github.com/go-pkgz/auth/token" R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" - multierror "github.com/hashicorp/go-multierror" "github.com/umputun/remark/backend/app/rest" "github.com/umputun/remark/backend/app/store" diff --git a/backend/app/rest/api/rest_public.go b/backend/app/rest/api/rest_public.go index fa801950..49134e3c 100644 --- a/backend/app/rest/api/rest_public.go +++ b/backend/app/rest/api/rest_public.go @@ -10,6 +10,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" diff --git a/backend/app/rest/api/rss.go b/backend/app/rest/api/rss.go index 4c5b1256..003337bb 100644 --- a/backend/app/rest/api/rss.go +++ b/backend/app/rest/api/rss.go @@ -7,10 +7,11 @@ import ( "time" "github.com/go-chi/chi" - "github.com/go-pkgz/rest/cache" "github.com/gorilla/feeds" "github.com/pkg/errors" + "github.com/go-pkgz/rest/cache" + "github.com/umputun/remark/backend/app/rest" "github.com/umputun/remark/backend/app/store" ) diff --git a/backend/app/rest/api/ssl.go b/backend/app/rest/api/ssl.go index a52771b2..3310a4a3 100644 --- a/backend/app/rest/api/ssl.go +++ b/backend/app/rest/api/ssl.go @@ -9,8 +9,9 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" - R "github.com/go-pkgz/rest" "golang.org/x/crypto/acme/autocert" + + R "github.com/go-pkgz/rest" ) // sslMode defines ssl mode for rest server diff --git a/backend/app/rest/proxy/image.go b/backend/app/rest/proxy/image.go index 4d33311d..c66c7054 100644 --- a/backend/app/rest/proxy/image.go +++ b/backend/app/rest/proxy/image.go @@ -8,12 +8,12 @@ import ( "strings" "time" - "git.tkginternal.com/commons/pkg/repeater" - "github.com/PuerkitoBio/goquery" "github.com/go-chi/chi" "github.com/pkg/errors" + "github.com/go-pkgz/repeater" + "github.com/umputun/remark/backend/app/rest" ) diff --git a/backend/app/store/admin/mongo.go b/backend/app/store/admin/mongo.go index ddd98ac8..f519c482 100644 --- a/backend/app/store/admin/mongo.go +++ b/backend/app/store/admin/mongo.go @@ -5,8 +5,9 @@ import ( "github.com/globalsign/mgo" "github.com/globalsign/mgo/bson" - "github.com/go-pkgz/mongo" "github.com/pkg/errors" + + "github.com/go-pkgz/mongo" ) // MongoStore implements admin.Store with mongo backend diff --git a/backend/app/store/engine/mongo.go b/backend/app/store/engine/mongo.go index 587e7bda..772b22eb 100644 --- a/backend/app/store/engine/mongo.go +++ b/backend/app/store/engine/mongo.go @@ -5,10 +5,11 @@ import ( "github.com/globalsign/mgo" "github.com/globalsign/mgo/bson" - "github.com/go-pkgz/mongo" multierror "github.com/hashicorp/go-multierror" "github.com/pkg/errors" + "github.com/go-pkgz/mongo" + "github.com/umputun/remark/backend/app/store" ) diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go index 6a5d94c3..c67ebe26 100644 --- a/backend/app/store/service/service.go +++ b/backend/app/store/service/service.go @@ -5,9 +5,8 @@ import ( "sync" "time" - multierror "github.com/hashicorp/go-multierror" - "github.com/google/uuid" + multierror "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "github.com/umputun/remark/backend/app/store" diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/.gitlab-ci.yml b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/.gitlab-ci.yml deleted file mode 100644 index 383db2ea..00000000 --- a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/.gitlab-ci.yml +++ /dev/null @@ -1,25 +0,0 @@ -image: docker.tkginternal.com/system/buildimage-go:1.1-master - -stages: - - build - -variables: - PROJ: "repeater" - GROUP: "commons/pkg" - PKG: "git.tkginternal.com" - -build_app: - stage: build - script: - - mkdir -p /go/src/$PKG/$GROUP && cp -fR $CI_PROJECT_DIR /go/src/$PKG/$GROUP/$PROJ - - mkdir -p $CI_PROJECT_DIR/target && ln -s $CI_PROJECT_DIR/target /go/src/$PKG/$GROUP/$PROJ/target - - cd /go/src/$PKG/$GROUP/$PROJ - - go get -v && go get -t $(go list -e ./... | grep -v vendor) && go test -v $(go list -e ./... | grep -v vendor) - - gometalinter --exclude=test --vendored-linters --disable-all --vendor --enable=vet --enable=vetshadow --enable=golint --enable=ineffassign --enable=goconst --enable=gas --enable=staticcheck --enable=errcheck --deadline=120s ./... - - go build -ldflags "-X main.revision=$REV" -o $CI_PROJECT_DIR/target/$PROJ - - cd /go/src/$PKG/$GROUP/$PROJ && /script/coverage.sh - tags: - - gobuilder - artifacts: - paths: - - target/ \ No newline at end of file diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/README.md b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/README.md deleted file mode 100644 index 03ef147d..00000000 --- a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Repeater - -[![pipeline status](https://git.tkginternal.com/commons/pkg/repeater/badges/master/pipeline.svg)](https://git.tkginternal.com/commons/pkg/repeater/commits/master) -[![coverage report](https://git.tkginternal.com/commons/pkg/repeater/badges/master/coverage.svg)](https://git.tkginternal.com/commons/pkg/repeater/commits/master) -[![GoDoc](https://godoc.tkginternal.com/godoc.svg)](https://godoc.tkginternal.com/pkg/git.tkginternal.com/commons/pkg/repeater/) - - -Package repeater call fun till it returns no error, up to repeat some number of iterations and delays defined by strategy. -Repeats number and delays defined by strategy.Interface. Terminates immediately on err from provided, optional list of critical errors - -## Install and update - -`go get -u git.tkginternal.com/commons/pkg/repeater` - -## How to use - -New Repeater created by `New(strtg strategy.Interface)` or shortcut for defaults - `NewDefault(repeats int, delay time.Duration) *Repeater`. - -To activate use `Do` method. Do repeats fun till no error. Predefined (optional) errors terminate immediately - -`func (r Repeater) Do(fun func() error, errors ...error) (err error)` - -### Repeating strategy - -User can provide his own strategy implementing this interface: - -```go -type Interface interface { - Start(ctx context.Context) chan struct{} -} -``` - -Returned channels used as "ticks", i.e. for each repeat (or initial) operation one read from this channel needed. Closing this channel indicates "done with retries". This is pretty much the same idea as `time.Timer` or `time.Tick` implements. Note - the first (technically not-repeated-yet) call won't happen **until something sent to the channel**. This is why typical strategy sends first "tick" prior to first wait/sleep. - -Three mist common strategies provided by package and ready to use: -1. **Fixed delay**, up to max number of attempts - `NewFixedDelay(repeats int, delay time.Duration)`. -This is default strategy used by `repeater.NewDefault` constructor -2. **BackOff** with jitter provides exponential backoff. It starts from 100ms interval and goes in steps with `last * math.Pow(factor, attempt)`. Optional jitter randomizes intervals a little bit. The strategy created by `NewBackoff(repeats int, factor float64, jitter bool)`. _Factor = 1 effectively makes this strategy fixed with 100ms delay._ - -3. **Once** strategy does not do any repeats and mainly useful for tests - `NewOnce()` - diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/repeater.go b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/repeater.go deleted file mode 100644 index a95e8219..00000000 --- a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/repeater.go +++ /dev/null @@ -1,60 +0,0 @@ -// Package repeater call fun till it returns no error, up to repeat some number of iterations and delays defined by strategy. -// Repeats number and delays defined by strategy.Interface. Terminates immediately on err from -// provided, optional list of critical errors -package repeater - -import ( - "context" - "time" - - "git.tkginternal.com/commons/pkg/repeater/strategy" -) - -// Repeater is the main object, should be made by New or NewDefault, embeds strategy -type Repeater struct { - strategy.Interface -} - -// New repeater with a given strategy. If strategy=nil initializes with FixedDelay 5sec, 10 times. -func New(strtg strategy.Interface) *Repeater { - if strtg == nil { - strtg = strategy.NewFixedDelay(10, time.Second*5) - } - result := Repeater{Interface: strtg} - return &result -} - -// NewDefault makes repeater with FixedDelay strategy -func NewDefault(repeats int, delay time.Duration) *Repeater { - return New(strategy.NewFixedDelay(repeats, delay)) -} - -// Do repeats fun till no error. Predefined (optional) errors terminate immediately -func (r Repeater) Do(fun func() error, errors ...error) (err error) { - - ctx, cancelFunc := context.WithCancel(context.Background()) - defer cancelFunc() // ensure strategy's channel termination - - inErrors := func(err error) bool { - for _, e := range errors { - if e == err { - return true - } - } - return false - } - - ch := r.Start(ctx) // channel of ticks-like events provided by strategy - - // closed channel indicates completion or early termination, set by strategy - for range ch { - - if err = fun(); err == nil { - return nil - } - if err != nil && inErrors(err) { //terminate on critical error from provided list - return err - } - } - return err -} diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/backoff.go b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/backoff.go deleted file mode 100644 index c660c95c..00000000 --- a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/backoff.go +++ /dev/null @@ -1,56 +0,0 @@ -package strategy - -import ( - "context" - "math" - "math/rand" - "time" -) - -// Backoff implements Interface for exponential-backoff -// it starts from 100ms and goes in steps with last * math.Pow(factor, attempt) -// optional jitter randomize intervals a little bit. -type Backoff struct { - repeats int - factor float64 - jitter bool -} - -// NewBackoff makes Backoff strategy with given factor and optional jitter -func NewBackoff(repeats int, factor float64, jitter bool) Interface { - if repeats == 0 { - repeats = 1 - } - if factor <= 0 { - factor = 1 - } - result := Backoff{repeats: repeats, factor: factor, jitter: jitter} - return &result -} - -// Start returns channel, similar to time.Timer -// then publishing signals to channel ch for retries attempt. Closed ch indicates "done" event -// consumer (repeater) should stop it explicitly after completion -func (b *Backoff) Start(ctx context.Context) (ch chan struct{}) { - ch = make(chan struct{}) - go func() { - defer close(ch) - rnd := rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) - minDelay := 100 * time.Millisecond // starts 100ms - for i := 0; i < b.repeats; i++ { - select { - case <-ctx.Done(): - return - default: - ch <- struct{}{} - delay := float64(minDelay) * math.Pow(b.factor, float64(i)) - if b.jitter { - delay = rnd.Float64()*(float64(2*minDelay)) + (delay - float64(minDelay)) - } - // log.Printf("%v", time.Duration(delay)) - time.Sleep(time.Duration(delay)) - } - } - }() - return ch -} diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/fixed.go b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/fixed.go deleted file mode 100644 index d9c30ecb..00000000 --- a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/fixed.go +++ /dev/null @@ -1,41 +0,0 @@ -package strategy - -import ( - "context" - "time" -) - -// FixedDelay implements Interface for fixed intervals up to max repeats -type FixedDelay struct { - repeats int - delay time.Duration -} - -// NewFixedDelay makes a Interface -func NewFixedDelay(repeats int, delay time.Duration) Interface { - if repeats == 0 { - repeats = 1 - } - result := FixedDelay{repeats: repeats, delay: delay} - return &result -} - -// Start returns channel, similar to time.Timer -// then publishing signals to channel ch for retries attempt. -// can be terminated (canceled) via context. -func (s *FixedDelay) Start(ctx context.Context) (ch chan struct{}) { - ch = make(chan struct{}) - go func() { - defer close(ch) - for i := 0; i < s.repeats; i++ { - select { - case <-ctx.Done(): - return - default: - ch <- struct{}{} - time.Sleep(s.delay) - } - } - }() - return ch -} diff --git a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/strategy.go b/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/strategy.go deleted file mode 100644 index 8d6a2a69..00000000 --- a/backend/vendor/git.tkginternal.com/commons/pkg/repeater/strategy/strategy.go +++ /dev/null @@ -1,28 +0,0 @@ -// Package strategy defines repeater's strategy and implements some. Strategy result -// is channel acting like time.Timer ot time.Tick -package strategy - -import "context" - -// Interface for repeats strategy. Returns channel with ticks -type Interface interface { - Start(ctx context.Context) chan struct{} -} - -// Once strategy eliminate repeats and makes a single try only -type Once struct{} - -// NewOnce makes no-repeat strategy -func NewOnce() Interface { - return &Once{} -} - -// Start returns closed channel with a single element to prevent any repeats -func (s *Once) Start(ctx context.Context) (ch chan struct{}) { - ch = make(chan struct{}) - go func() { - ch <- struct{}{} - close(ch) - }() - return ch -} From 60700d96c7a918b41995915b262a97289814bbdd Mon Sep 17 00:00:00 2001 From: Umputun Date: Fri, 28 Dec 2018 00:37:02 -0600 Subject: [PATCH 06/21] isolate authenticator creation to a separate methos --- backend/app/cmd/server.go | 67 ++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index d32f4968..22c6902d 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -224,39 +224,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { if err != nil { return nil, errors.Wrap(err, "failed to make avatar store") } - - authenticator := auth.NewService(auth.Opts{ - URL: s.RemarkURL, - Issuer: "remark42", - TokenDuration: s.Auth.TTL.JWT, - CookieDuration: s.Auth.TTL.Cookie, - SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"), - SecretReader: token.SecretFunc(func(id string) (string, error) { - return adminStore.Key(id) - }), - ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { - if c.User == nil { - return c - } - c.User.SetAdmin(dataService.IsAdmin(c.Audience, c.User.ID)) - return c - }), - DevPasswd: s.DevPasswd, - //Validator: dataService, - AvatarStore: avatarStore, - AvatarResizeLimit: s.Avatar.RszLmt, - AvatarRoutePath: "/api/v1/avatar", - }) - s.addAuthProviders(authenticator) - - // token TTL is 5 minutes, inactivity interval 7+ days by default - // jwtService := auth.NewJWT(adminStore, strings.HasPrefix(s.RemarkURL, "https://"), s.Auth.TTL.JWT, s.Auth.TTL.Cookie) - - // avatarProxy := &proxy.Avatar{ - // Store: avatarStore, - // RoutePath: "/api/v1/avatar", - // RemarkURL: strings.TrimSuffix(s.RemarkURL, "/"), - // } + authenticator := s.makeAuthenticator(dataService, avatarStore, adminStore) exporter := &migrator.Native{DataStore: dataService} @@ -550,3 +518,36 @@ func (s *ServerCommand) makeSSLConfig() (config api.SSLConfig, err error) { } return config, err } + +func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Store, admns admin.Store) *auth.Service { + authenticator := auth.NewService(auth.Opts{ + URL: strings.TrimSuffix(s.RemarkURL, "/"), + Issuer: "remark42", + TokenDuration: s.Auth.TTL.JWT, + CookieDuration: s.Auth.TTL.Cookie, + SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"), + SecretReader: token.SecretFunc(func(id string) (string, error) { + return admns.Key(id) + }), + ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { + if c.User == nil { + return c + } + c.User.SetAdmin(ds.IsAdmin(c.Audience, c.User.ID)) + c.User.SetBoolAttr("blocked", ds.IsBlocked(c.Audience, c.User.ID)) + return c + }), + DevPasswd: s.DevPasswd, + Validator: token.ValidatorFunc(func(token string, claims token.Claims) bool { + if claims.User == nil { + return false + } + return !claims.User.BoolAttr("blocked") + }), + AvatarStore: avas, + AvatarResizeLimit: s.Avatar.RszLmt, + AvatarRoutePath: "/api/v1/avatar", + }) + s.addAuthProviders(authenticator) + return authenticator +} From d33997c742c5cf51ecd087ea68c9af8cc87b604d Mon Sep 17 00:00:00 2001 From: Umputun Date: Fri, 28 Dec 2018 00:49:46 -0600 Subject: [PATCH 07/21] pass auth by reference --- backend/app/cmd/server.go | 2 +- backend/app/rest/api/admin.go | 2 +- backend/app/rest/api/rest.go | 2 +- backend/app/rest/api/rest_test.go | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 22c6902d..82aa5dd4 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -261,7 +261,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { Migrator: migr, ReadOnlyAge: s.ReadOnlyAge, SharedSecret: s.SharedSecret, - Authenticator: *authenticator, + Authenticator: authenticator, Cache: loadingCache, NotifyService: notifyService, SSLConfig: sslConfig, diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index 107a6675..34db4411 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -23,7 +23,7 @@ import ( type admin struct { dataService *service.DataStore cache cache.LoadingCache - authenticator auth.Service + authenticator *auth.Service readOnlyAge int migrator *Migrator } diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 86b826bf..4ab25249 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -39,7 +39,7 @@ type Rest struct { Version string DataService *service.DataStore - Authenticator auth.Service + Authenticator *auth.Service Cache cache.LoadingCache ImageProxy *proxy.Image CommentFormatter *store.CommentFormatter diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index 7ed3be28..11143548 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -62,7 +62,7 @@ func TestRest_GetStarted(t *testing.T) { } func TestRest_Shutdown(t *testing.T) { - srv := Rest{Authenticator: auth.Service{}, ImageProxy: &proxy.Image{}} + srv := Rest{Authenticator: &auth.Service{}, ImageProxy: &proxy.Image{}} go func() { time.Sleep(100 * time.Millisecond) @@ -91,7 +91,7 @@ func TestRest_filterComments(t *testing.T) { func TestRest_RunStaticSSLMode(t *testing.T) { srv := Rest{ - Authenticator: *auth.NewService(auth.Opts{ + Authenticator: auth.NewService(auth.Opts{ AvatarStore: avatar.NewLocalFS("/tmp"), AvatarResizeLimit: 300, }), @@ -143,7 +143,7 @@ func TestRest_RunStaticSSLMode(t *testing.T) { func TestRest_RunAutocertModeHTTPOnly(t *testing.T) { srv := Rest{ - Authenticator: auth.Service{}, + Authenticator: &auth.Service{}, ImageProxy: &proxy.Image{}, SSLConfig: SSLConfig{ SSLMode: Auto, @@ -191,7 +191,7 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { srv = &Rest{ DataService: dataStore, - Authenticator: *auth.NewService(auth.Opts{ + Authenticator: auth.NewService(auth.Opts{ DevPasswd: "password", SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), AvatarStore: avatar.NewLocalFS("/tmp"), From acf19e73cf5e84f1b4000a43a67d7e7a4a987491 Mon Sep 17 00:00:00 2001 From: Umputun Date: Sat, 29 Dec 2018 18:36:34 -0600 Subject: [PATCH 08/21] minor formatting --- backend/app/cmd/server.go | 6 +++--- backend/app/store/formatter.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 82aa5dd4..9d983c69 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -526,10 +526,10 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto TokenDuration: s.Auth.TTL.JWT, CookieDuration: s.Auth.TTL.Cookie, SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"), - SecretReader: token.SecretFunc(func(id string) (string, error) { + SecretReader: token.SecretFunc(func(id string) (string, error) { // get secret per site return admns.Key(id) }), - ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { + ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { // set attributes, on new token or refresh if c.User == nil { return c } @@ -538,7 +538,7 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto return c }), DevPasswd: s.DevPasswd, - Validator: token.ValidatorFunc(func(token string, claims token.Claims) bool { + Validator: token.ValidatorFunc(func(token string, claims token.Claims) bool { // check on each auth call (in middleware) if claims.User == nil { return false } diff --git a/backend/app/store/formatter.go b/backend/app/store/formatter.go index 3543395a..61a386e6 100644 --- a/backend/app/store/formatter.go +++ b/backend/app/store/formatter.go @@ -44,9 +44,9 @@ func (f *CommentFormatter) FormatText(txt string) (res string) { blackfriday.Strikethrough | blackfriday.SpaceHeadings | blackfriday.HardLineBreak | blackfriday.BackslashLineBreak | blackfriday.Autolink res = string(blackfriday.Run([]byte(txt), blackfriday.WithExtensions(mdExt))) + for _, conv := range f.converters { res = conv.Convert(res) - } res = f.shortenAutoLinks(res, shortURLLen) return res From 195e65ca60788ac08646f021046c41e1bf90190d Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 30 Dec 2018 13:31:03 -0600 Subject: [PATCH 09/21] adjusted tests for new auth lib --- README.md | 2 +- backend/Gopkg.lock | 10 +- backend/app/cmd/server.go | 8 +- backend/app/cmd/server_test.go | 10 +- backend/app/rest/api/admin.go | 2 +- backend/app/rest/api/admin_test.go | 140 ++++++++--------- backend/app/rest/api/migrator_test.go | 49 +++--- backend/app/rest/api/rest_private_test.go | 105 ++++++------- backend/app/rest/api/rest_public_test.go | 81 ++++------ backend/app/rest/api/rest_test.go | 56 ++++--- backend/app/rest/api/rss_test.go | 20 +-- .../github.com/go-pkgz/auth/.travis.yml | 2 +- .../vendor/github.com/go-pkgz/auth/README.md | 147 +++++++++++++++--- .../vendor/github.com/go-pkgz/auth/auth.go | 42 +++-- backend/vendor/github.com/go-pkgz/auth/go.mod | 10 +- backend/vendor/github.com/go-pkgz/auth/go.sum | 29 +--- .../go-pkgz/auth/middleware/auth.go | 77 +++------ .../go-pkgz/auth/provider/dev_provider.go | 141 +++++++++++++++-- .../go-pkgz/auth/provider/providers.go | 10 +- .../go-pkgz/auth/provider/service.go | 34 ++-- .../github.com/go-pkgz/auth/token/jwt.go | 4 +- .../vendor/github.com/go-pkgz/rest/README.md | 3 +- .../github.com/go-pkgz/rest/httperrors.go | 16 +- .../github.com/go-pkgz/rest/logger/logger.go | 7 + 24 files changed, 598 insertions(+), 407 deletions(-) diff --git a/README.md b/README.md index e275701f..a90a5f03 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,8 @@ _this is the recommended way to run remark42_ | edit-time | EDIT_TIME | `5m` | edit window | | read-age | READONLY_AGE | | read-only age of comments, days | | img-proxy | IMG_PROXY | `false` | enable http->https proxy for images | +| admin-passwd | ADMIN_PASSWD | | password for `admin` basic auth | | dbg | DEBUG | `false` | debug mode | -| dev-passwd | DEV_PASSWD | | password for `dev` user | * command line parameters are long form `--=value`, i.e. `--site=https://demo.remark42.com` * _multi_ parameters separated by `,` in the environment or repeated with command line key, like `--site=s1 --site=s2 ...` diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index 7beda51c..9121d90d 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -113,7 +113,7 @@ [[projects]] branch = "master" - digest = "1:5ef69525e5e62fb771f3f6910c94030a86b542eff1cb9d9350803b6dae147144" + digest = "1:b117a0a0b46dad26254a48c11a511d6c697038e591a1d7ce11a229e1c8e0a237" name = "github.com/go-pkgz/auth" packages = [ ".", @@ -123,7 +123,7 @@ "token", ] pruneopts = "UT" - revision = "8d5238712a320d972f9d658e2fd1d4468ef81c3e" + revision = "b95cb645615503dba4d5fced3b77d97d4f0dcc81" [[projects]] digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574" @@ -145,7 +145,7 @@ version = "v1.0.0" [[projects]] - digest = "1:71dc1e5b19e179495d2e2ca63454a9204753c5ecb3faa4a842ea5859355a968f" + digest = "1:e133aa7be09588b02198e4ddb98df5033b0319b56533881d0163ee51b903305b" name = "github.com/go-pkgz/rest" packages = [ ".", @@ -153,8 +153,8 @@ "logger", ] pruneopts = "UT" - revision = "c0e09a7a640e54001aed8bad117d60ad8971958e" - version = "v1.1.1" + revision = "553c0e1b55b215f8f55da4682ac57aff9aec8b6d" + version = "v1.1.5" [[projects]] digest = "1:ffc060c551980d37ee9e428ef528ee2813137249ccebb0bfc412ef83071cac91" diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 9d983c69..62951d2c 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -43,7 +43,7 @@ type ServerCommand struct { SSL SSLGroup `group:"ssl" namespace:"ssl" env-namespace:"SSL"` Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","` - DevPasswd string `long:"dev-passwd" env:"DEV_PASSWD" default:"" description:"development mode password"` + AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"` BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"` MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"` ImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"enable image proxy"` @@ -293,8 +293,8 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { // Run all application objects func (a *serverApp) run(ctx context.Context) error { - if a.DevPasswd != "" { - log.Printf("[WARN] running in dev mode") + if a.AdminPasswd != "" { + log.Printf("[WARN] admin basic auth enabled") } go func() { @@ -537,7 +537,7 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto c.User.SetBoolAttr("blocked", ds.IsBlocked(c.Audience, c.User.ID)) return c }), - DevPasswd: s.DevPasswd, + AdminPasswd: s.AdminPasswd, Validator: token.ValidatorFunc(func(token string, claims token.Claims) bool { // check on each auth call (in middleware) if claims.User == nil { return false diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 9fcfb546..c73985cf 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -42,7 +42,7 @@ func TestServerApp(t *testing.T) { client := http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("POST", "http://localhost:18080/api/v1/comment", strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`)) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") require.Nil(t, err) resp, err = client.Do(req) require.Nil(t, err) @@ -58,7 +58,7 @@ func TestServerApp(t *testing.T) { func TestServerApp_DevMode(t *testing.T) { app, ctx := prepServerApp(t, 500*time.Millisecond, func(o ServerCommand) ServerCommand { o.Port = 18085 - o.DevPasswd = "password" + o.AdminPasswd = "password" o.Auth.Dev = true return o }) @@ -95,7 +95,7 @@ func TestServerApp_WithMongo(t *testing.T) { // prepare options p := flags.NewParser(&opts, flags.Default) - _, err := p.ParseArgs([]string{"--dev-passwd=password", "--cache.type=none", "--store.type=mongo", + _, err := p.ParseArgs([]string{"--admin-passwd=password", "--cache.type=none", "--store.type=mongo", "--avatar.type=mongo", "--mongo.url=" + mongoURL, "--mongo.db=test_remark", "--port=12345", "--admin.type=mongo"}) require.Nil(t, err) opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid" @@ -142,7 +142,7 @@ func TestServerApp_WithSSL(t *testing.T) { // prepare options p := flags.NewParser(&opts, flags.Default) - _, err := p.ParseArgs([]string{"--dev-passwd=password", "--port=18080", "--store.bolt.path=/tmp/xyz", "--backup=/tmp", "--avatar.type=bolt", "--avatar.bolt.file=/tmp/ava-test.db", "--notify.type=none", + _, err := p.ParseArgs([]string{"--admin-passwd=password", "--port=18080", "--store.bolt.path=/tmp/xyz", "--backup=/tmp", "--avatar.type=bolt", "--avatar.bolt.file=/tmp/ava-test.db", "--notify.type=none", "--ssl.type=static", "--ssl.cert=testdata/cert.pem", "--ssl.key=testdata/key.pem", "--ssl.port=18443"}) require.Nil(t, err) @@ -318,7 +318,7 @@ func prepServerApp(t *testing.T, duration time.Duration, fn func(o ServerCommand // prepare options p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--dev-passwd=password"}) + _, err := p.ParseArgs([]string{"--admin-passwd=password"}) require.Nil(t, err) cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation = "/tmp", "fs", "/tmp" cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port) diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index 34db4411..b4a60988 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -120,7 +120,7 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { return } - if claims.User.Picture != "" { + if claims.User.Picture != "" && a.authenticator.AvatarProxy() != nil { avatartStore := a.authenticator.AvatarProxy().Store if err := avatartStore.Remove(path.Base(claims.User.Picture)); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user's avatar") diff --git a/backend/app/rest/api/admin_test.go b/backend/app/rest/api/admin_test.go index 9ebb64af..002e6fcd 100644 --- a/backend/app/rest/api/admin_test.go +++ b/backend/app/rest/api/admin_test.go @@ -23,9 +23,8 @@ import ( ) func TestAdmin_Delete(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", User: store.User{ID: "id", Name: "name"}, Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} @@ -39,12 +38,12 @@ func TestAdmin_Delete(t *testing.T) { req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/admin/comment/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) - body, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + body, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) assert.Equal(t, 200, code) cr := store.Comment{} err = json.Unmarshal([]byte(body), &cr) @@ -54,9 +53,8 @@ func TestAdmin_Delete(t *testing.T) { } func TestAdmin_DeleteUser(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Orig: "o test test #1", User: store.User{ID: "id1", Name: "name"}, Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} @@ -76,7 +74,7 @@ func TestAdmin_DeleteUser(t *testing.T) { client := http.Client{} req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t", ts.URL, "id2"), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -108,9 +106,8 @@ func TestAdmin_DeleteUser(t *testing.T) { } func TestAdmin_Pin(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} @@ -125,7 +122,7 @@ func TestAdmin_Pin(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/pin/%s?site=radio-t&url=https://radio-t.com/blah&pin=%d", ts.URL, id1, val), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) return resp.StatusCode @@ -152,9 +149,8 @@ func TestAdmin_Pin(t *testing.T) { } func TestAdmin_Block(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -174,7 +170,7 @@ func TestAdmin_Block(t *testing.T) { } req, e := http.NewRequest(http.MethodPut, url, nil) assert.Nil(t, e) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, e := client.Do(req) require.Nil(t, e) body, e = ioutil.ReadAll(resp.Body) @@ -233,9 +229,8 @@ func TestAdmin_Block(t *testing.T) { } func TestAdmin_BlockedList(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() client := http.Client{} @@ -243,7 +238,7 @@ func TestAdmin_BlockedList(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user1", 1), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) @@ -251,33 +246,40 @@ func TestAdmin_BlockedList(t *testing.T) { req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d&ttl=50ms", ts.URL, "user2", 1), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) - res, code := getWithAuth(t, ts.URL+"/api/v1/admin/blocked?site=radio-t") - require.Equal(t, 200, code, res) + req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/blocked?site=radio-t", nil) + require.Nil(t, err) + req.SetBasicAuth("admin", "password") + res, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, 200, res.StatusCode) users := []store.BlockedUser{} - err = json.Unmarshal([]byte(res), &users) + err = json.NewDecoder(res.Body).Decode(&users) assert.Nil(t, err) assert.Equal(t, 2, len(users), "two users blocked") assert.Equal(t, "user1", users[0].ID) assert.Equal(t, "user2", users[1].ID) time.Sleep(50 * time.Millisecond) - res, code = getWithAuth(t, ts.URL+"/api/v1/admin/blocked?site=radio-t") - require.Equal(t, 200, code, res) + + req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/blocked?site=radio-t", nil) + require.Nil(t, err) + req.SetBasicAuth("admin", "password") + res, err = client.Do(req) + require.Equal(t, 200, res.StatusCode) users = []store.BlockedUser{} - err = json.Unmarshal([]byte(res), &users) + err = json.NewDecoder(res.Body).Decode(&users) assert.Nil(t, err) assert.Equal(t, 1, len(users), "one user left blocked") } func TestAdmin_ReadOnly(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -299,7 +301,7 @@ func TestAdmin_ReadOnly(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=1", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -314,7 +316,7 @@ func TestAdmin_ReadOnly(t *testing.T) { assert.Nil(t, err, "can't marshal comment %+v", c) req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusForbidden, resp.StatusCode) @@ -323,7 +325,7 @@ func TestAdmin_ReadOnly(t *testing.T) { req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) require.Nil(t, err) @@ -338,16 +340,15 @@ func TestAdmin_ReadOnly(t *testing.T) { assert.Nil(t, err, "can't marshal comment %+v", c) req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusCreated, resp.StatusCode) } func TestAdmin_ReadOnlyWithAge(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}, @@ -365,7 +366,7 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=1", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -377,7 +378,7 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) { req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Equal(t, 403, resp.StatusCode) require.Nil(t, err) @@ -387,9 +388,8 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) { } func TestAdmin_Verify(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -408,7 +408,7 @@ func TestAdmin_Verify(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=1", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) verified = srv.DataService.IsVerified("radio-t", "user1") @@ -426,7 +426,7 @@ func TestAdmin_Verify(t *testing.T) { req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=0", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) verified = srv.DataService.IsVerified("radio-t", "user1") @@ -443,9 +443,8 @@ func TestAdmin_Verify(t *testing.T) { } func TestAdmin_ExportStream(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -455,7 +454,7 @@ func TestAdmin_ExportStream(t *testing.T) { addComment(t, c1, ts) addComment(t, c2, ts) - body, code := getWithAuth(t, ts.URL+"/api/v1/admin/export?site=radio-t&mode=stream") + body, code := getWithAdminAuth(t, ts.URL+"/api/v1/admin/export?site=radio-t&mode=stream") assert.Equal(t, 200, code) assert.Equal(t, 3, strings.Count(body, "\n")) assert.Equal(t, 2, strings.Count(body, "\"text\"")) @@ -463,9 +462,8 @@ func TestAdmin_ExportStream(t *testing.T) { } func TestAdmin_ExportFile(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -478,7 +476,7 @@ func TestAdmin_ExportFile(t *testing.T) { client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("GET", ts.URL+"/api/v1/admin/export?site=radio-t&mode=file", nil) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) require.Nil(t, err) @@ -495,9 +493,8 @@ func TestAdmin_ExportFile(t *testing.T) { } func TestAdmin_DeleteMeRequest(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -531,9 +528,8 @@ func TestAdmin_DeleteMeRequest(t *testing.T) { }, } - _ = os.MkdirAll("/tmp/42", 0700) - defer func() { _ = os.RemoveAll("/tmp/42") }() - require.NoError(t, ioutil.WriteFile("/tmp/42/pic.image", []byte("some image data"), 0600)) + require.NoError(t, os.MkdirAll("/tmp/ava-remark42/42", 0700)) + require.NoError(t, ioutil.WriteFile("/tmp/ava-remark42/42/pic.image", []byte("some image data"), 0600)) tkn, err := srv.Authenticator.TokenService().Token(claims) assert.Nil(t, err) @@ -541,9 +537,9 @@ func TestAdmin_DeleteMeRequest(t *testing.T) { client := http.Client{} req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) - assert.Nil(t, err) + require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) _, err = srv.DataService.User("radio-t", "user1", 0, 0) @@ -551,9 +547,8 @@ func TestAdmin_DeleteMeRequest(t *testing.T) { } func TestAdmin_DeleteMeRequestFailed(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -569,7 +564,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { client := http.Client{} req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, "bad token"), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, 400, resp.StatusCode) @@ -596,7 +591,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { assert.Nil(t, err) req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "bad-password") + req.SetBasicAuth("admin", "bad-password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, 401, resp.StatusCode) @@ -608,7 +603,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { assert.Nil(t, err) req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, 400, resp.StatusCode, resp.Status) @@ -620,7 +615,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { assert.Nil(t, err) req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, 403, resp.StatusCode) @@ -630,9 +625,8 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { } func TestAdmin_GetUserInfo(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -644,7 +638,7 @@ func TestAdmin_GetUserInfo(t *testing.T) { _, err = srv.DataService.Create(c2) assert.Nil(t, err) - body, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL)) + body, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL)) assert.Equal(t, 200, code) u := store.User{} err = json.Unmarshal([]byte(body), &u) @@ -655,6 +649,6 @@ func TestAdmin_GetUserInfo(t *testing.T) { _, code = get(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL)) assert.Equal(t, 401, code, "no auth") - _, code = getWithAuth(t, fmt.Sprintf("%s/api/v1/admin/user/userX?site=radio-t&url=https://radio-t.com/blah", ts.URL)) + _, code = getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/admin/user/userX?site=radio-t&url=https://radio-t.com/blah", ts.URL)) assert.Equal(t, 400, code, "no info about user") } diff --git a/backend/app/rest/api/migrator_test.go b/backend/app/rest/api/migrator_test.go index 9d856b6d..8ca343e3 100644 --- a/backend/app/rest/api/migrator_test.go +++ b/backend/app/rest/api/migrator_test.go @@ -5,7 +5,6 @@ import ( "compress/gzip" "encoding/json" "fmt" - "github.com/go-pkgz/auth/token" "io" "io/ioutil" "mime/multipart" @@ -16,6 +15,8 @@ import ( "testing" "time" + "github.com/go-pkgz/auth/token" + bolt "github.com/coreos/bbolt" "github.com/go-chi/chi" "github.com/go-pkgz/auth" @@ -39,7 +40,8 @@ func TestMigrator_Import(t *testing.T) { {"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"

test test #2

","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`) client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) + req.SetBasicAuth("admin", "password") assert.Nil(t, err) resp, err := client.Do(req) assert.Nil(t, err) @@ -51,7 +53,7 @@ func TestMigrator_Import(t *testing.T) { client = &http.Client{Timeout: 10 * time.Second} req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) @@ -74,7 +76,8 @@ func TestMigrator_ImportForm(t *testing.T) { contentType := bodyWriter.FormDataContentType() require.NoError(t, bodyWriter.Close()) - resp, err := http.Post(ts.URL+"/import/form?site=radio-t&provider=native&secret=123456", contentType, bodyBuf) + authts := strings.Replace(ts.URL, "http://", "http://admin:password@", 1) + resp, err := http.Post(authts+"/import/form?site=radio-t&provider=native", contentType, bodyBuf) assert.Nil(t, err) assert.Equal(t, http.StatusAccepted, resp.StatusCode) @@ -84,7 +87,7 @@ func TestMigrator_ImportForm(t *testing.T) { client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) @@ -97,9 +100,10 @@ func TestMigrator_ImportFromWP(t *testing.T) { r := strings.NewReader(strings.Replace(xmlTestWP, "'", "`", -1)) client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=wordpress&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=wordpress", r) assert.Nil(t, err) req.Header.Add("Content-Type", "application/xml; charset=utf-8") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusAccepted, resp.StatusCode) @@ -110,16 +114,15 @@ func TestMigrator_ImportFromWP(t *testing.T) { client = &http.Client{Timeout: 10 * time.Second} req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) assert.NoError(t, ds.Interface.Close()) - srvAccess, tsAccess := prep(t) - require.NotNil(t, srvAccess) - defer cleanup(ts, srvAccess) + tsAccess, _, teardownAccess := startupT(t) + defer teardownAccess() res, code := get(t, tsAccess.URL+"/api/v1/last/10?site=radio-t") require.Equal(t, 200, code) @@ -160,14 +163,16 @@ func TestMigrator_ImportDouble(t *testing.T) { } r := strings.NewReader(`{"version":1}` + strings.Join(recs, "\n")) // reader with 10k records client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) + req.SetBasicAuth("admin", "password") assert.Nil(t, err) resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusAccepted, resp.StatusCode) client = &http.Client{Timeout: 1 * time.Second} - req, err = http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err = http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) + req.SetBasicAuth("admin", "password") assert.Nil(t, err) resp, err = client.Do(req) assert.Nil(t, err) @@ -187,7 +192,8 @@ func TestMigrator_ImportWaitExpired(t *testing.T) { } r := strings.NewReader(`{"version":1}` + strings.Join(recs, "\n")) // reader with 10k records client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) + req.SetBasicAuth("admin", "password") require.Nil(t, err) resp, err := client.Do(req) assert.Nil(t, err) @@ -195,7 +201,7 @@ func TestMigrator_ImportWaitExpired(t *testing.T) { client = &http.Client{Timeout: 10 * time.Second} req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t&timeout=100ms", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, http.StatusGatewayTimeout, resp.StatusCode) @@ -211,21 +217,23 @@ func TestMigrator_Export(t *testing.T) { // import comments first client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) require.Nil(t, err) + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) require.Nil(t, err) require.Equal(t, http.StatusAccepted, resp.StatusCode) client = &http.Client{Timeout: 10 * time.Second} req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) // check file mode - req, err = http.NewRequest("GET", ts.URL+"/export?mode=file&site=radio-t&secret=123456", nil) + req, err = http.NewRequest("GET", ts.URL+"/export?mode=file&site=radio-t", nil) require.Nil(t, err) + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) require.Nil(t, err) require.Equal(t, 200, resp.StatusCode) @@ -240,8 +248,9 @@ func TestMigrator_Export(t *testing.T) { t.Logf("%s", string(ungzBody)) // check stream mode - req, err = http.NewRequest("GET", ts.URL+"/export?mode=stream&site=radio-t&secret=123456", nil) + req, err = http.NewRequest("GET", ts.URL+"/export?mode=stream&site=radio-t", nil) require.Nil(t, err) + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) require.Nil(t, err) require.Equal(t, 200, resp.StatusCode) @@ -253,7 +262,7 @@ func TestMigrator_Export(t *testing.T) { assert.Equal(t, 2, strings.Count(string(body), "\"text\"")) t.Logf("%s", string(body)) - req, err = http.NewRequest("GET", ts.URL+"/export?site=radio-t&secret=bad", nil) + req, err = http.NewRequest("GET", ts.URL+"/export?site=radio-t", nil) require.Nil(t, err) resp, err = client.Do(req) require.Nil(t, err) @@ -275,7 +284,7 @@ func prepImportSrv(t *testing.T) (svc *Migrator, ds *service.DataStore, ts *http } a := auth.NewService(auth.Opts{ - DevPasswd: "password", + AdminPasswd: "password", SecretReader: token.SecretFunc(func(id string) (string, error) { return "123456", nil }), Issuer: "test", }) diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 8c4a4c7f..fcf201d0 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -18,9 +18,8 @@ import ( ) func TestRest_Create(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() resp, err := post(t, ts.URL+"/api/v1/comment", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) @@ -29,6 +28,7 @@ func TestRest_Create(t *testing.T) { assert.Nil(t, err) require.Equal(t, http.StatusCreated, resp.StatusCode, string(b)) + t.Log(string(b)) c := R.JSON{} err = json.Unmarshal(b, &c) assert.Nil(t, err) @@ -39,9 +39,8 @@ func TestRest_Create(t *testing.T) { } func TestRest_CreateOldPost(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() // make old, but not too old comment old := store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5), @@ -73,9 +72,8 @@ func TestRest_CreateOldPost(t *testing.T) { } func TestRest_CreateTooBig(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() longComment := fmt.Sprintf(`{"text": "%4001s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "Щ") @@ -105,9 +103,8 @@ func TestRest_CreateTooBig(t *testing.T) { func TestRest_CreateRejected(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() body := `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}` // try to create without auth @@ -117,9 +114,8 @@ func TestRest_CreateRejected(t *testing.T) { } func TestRest_CreateAndGet(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() // create comment resp, err := post(t, ts.URL+"/api/v1/comment", @@ -134,24 +130,31 @@ func TestRest_CreateAndGet(t *testing.T) { id := c["id"].(string) - // get created comment by id - res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + // get created comment by id as admin + res, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) assert.Equal(t, 200, code) comment := store.Comment{} err = json.Unmarshal([]byte(res), &comment) assert.Nil(t, err) assert.Equal(t, "

test 123

\n\n

http://radio-t.com

\n", comment.Text) assert.Equal(t, "**test** *123*\n\n http://radio-t.com", comment.Orig) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", Admin: true, Blocked: false, + assert.Equal(t, store.User{Name: "admin", ID: "admin", Admin: true, Blocked: false, IP: "dbc7c999343f003f189f70aaf52cc04443f90790"}, comment.User) t.Logf("%+v", comment) + + // get created comment by id as non-admin + res, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + assert.Equal(t, 200, code) + comment = store.Comment{} + err = json.Unmarshal([]byte(res), &comment) + assert.Nil(t, err) + assert.Equal(t, store.User{Name: "admin", ID: "admin", Admin: true, Blocked: false, IP: ""}, comment.User, "no ip") } func TestRest_Update(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -161,7 +164,7 @@ func TestRest_Update(t *testing.T) { req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) b, err := client.Do(req) assert.Nil(t, err) body, err := ioutil.ReadAll(b.Body) @@ -179,7 +182,7 @@ func TestRest_Update(t *testing.T) { assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second) // read updated comment - res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + res, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) assert.Equal(t, 200, code) c3 := store.Comment{} err = json.Unmarshal([]byte(res), &c3) @@ -188,9 +191,8 @@ func TestRest_Update(t *testing.T) { } func TestRest_UpdateDelete(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -200,7 +202,7 @@ func TestRest_UpdateDelete(t *testing.T) { req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"delete": true, "summary":"removed by user"}`)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) b, err := client.Do(req) assert.Nil(t, err) body, err := ioutil.ReadAll(b.Body) @@ -215,7 +217,7 @@ func TestRest_UpdateDelete(t *testing.T) { assert.True(t, c2.Deleted) // read updated comment - res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + res, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) assert.Equal(t, 200, code) c3 := store.Comment{} err = json.Unmarshal([]byte(res), &c3) @@ -223,13 +225,11 @@ func TestRest_UpdateDelete(t *testing.T) { assert.Equal(t, "", c3.Text) assert.Equal(t, "", c3.Orig) assert.True(t, c3.Deleted) - } func TestRest_UpdateNotOwner(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "xyz"}} @@ -240,7 +240,7 @@ func TestRest_UpdateNotOwner(t *testing.T) { req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+ "?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) b, err := client.Do(req) assert.Nil(t, err) body, err := ioutil.ReadAll(b.Body) @@ -252,16 +252,15 @@ func TestRest_UpdateNotOwner(t *testing.T) { req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+ "?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) b, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, 400, b.StatusCode, string(body), "update is not json") } func TestRest_Vote(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} @@ -276,7 +275,7 @@ func TestRest_Vote(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) return resp.StatusCode @@ -290,7 +289,7 @@ func TestRest_Vote(t *testing.T) { err := json.Unmarshal([]byte(body), &cr) assert.Nil(t, err) assert.Equal(t, 1, cr.Score) - assert.Equal(t, map[string]bool{"dev": true}, cr.Votes) + assert.Equal(t, map[string]bool{"admin": true}, cr.Votes) assert.Equal(t, 200, vote(-1), "opposite vote allowed") body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) @@ -303,9 +302,8 @@ func TestRest_Vote(t *testing.T) { } func TestRest_UserAllData(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() // write 3 comments user := store.User{ID: "dev", Name: "user name 1"} @@ -325,7 +323,7 @@ func TestRest_UserAllData(t *testing.T) { client := &http.Client{Timeout: 1 * time.Second} req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) require.Nil(t, err) require.Equal(t, 200, resp.StatusCode) @@ -336,7 +334,7 @@ func TestRest_UserAllData(t *testing.T) { ungzBody, err := ioutil.ReadAll(ungzReader) assert.NoError(t, err) assert.True(t, strings.HasPrefix(string(ungzBody), - `{"info": {"name":"developer one","id":"dev","picture":"","admin":true}, "comments":[{`)) + `{"info": {"name":"developer one","id":"dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false}, "comments":[{`)) assert.Equal(t, 3, strings.Count(string(ungzBody), `"text":`), "3 comments inside") t.Logf("%s", string(ungzBody)) @@ -347,7 +345,8 @@ func TestRest_UserAllData(t *testing.T) { err = json.Unmarshal(ungzBody, &parsed) assert.Nil(t, err) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "", Admin: true}, parsed.Info) + assert.Equal(t, store.User{Name: "developer one", ID: "dev", + Picture: "http://example.com/pic.png", IP: "127.0.0.1"}, parsed.Info) assert.Equal(t, 3, len(parsed.Comments)) req, err = http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) @@ -358,9 +357,8 @@ func TestRest_UserAllData(t *testing.T) { } func TestRest_UserAllDataManyComments(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() user := store.User{ID: "dev", Name: "user name 1"} c := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", @@ -376,7 +374,7 @@ func TestRest_UserAllDataManyComments(t *testing.T) { client := &http.Client{Timeout: 1 * time.Second} req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) require.Nil(t, err) require.Equal(t, 200, resp.StatusCode) @@ -387,19 +385,18 @@ func TestRest_UserAllDataManyComments(t *testing.T) { ungzBody, err := ioutil.ReadAll(ungzReader) assert.NoError(t, err) assert.True(t, strings.HasPrefix(string(ungzBody), - `{"info": {"name":"developer one","id":"dev","picture":"","admin":true}, "comments":[{`)) + `{"info": {"name":"developer one","id":"dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false}, "comments":[{`)) assert.Equal(t, 478, strings.Count(string(ungzBody), `"text":`), "478 comments inside") } func TestRest_DeleteMe(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() client := http.Client{} req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/deleteme?site=radio-t", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) diff --git a/backend/app/rest/api/rest_public_test.go b/backend/app/rest/api/rest_public_test.go index 9f40a7f3..48760bdc 100644 --- a/backend/app/rest/api/rest_public_test.go +++ b/backend/app/rest/api/rest_public_test.go @@ -18,9 +18,8 @@ import ( ) func TestRest_Ping(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() res, code := get(t, ts.URL+"/api/v1/ping") assert.Equal(t, "pong", res) @@ -28,9 +27,8 @@ func TestRest_Ping(t *testing.T) { } func TestRest_Preview(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() resp, err := post(t, ts.URL+"/api/v1/preview", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) assert.Nil(t, err) @@ -41,9 +39,8 @@ func TestRest_Preview(t *testing.T) { } func TestRest_PreviewWithMD(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() text := ` # h1 @@ -69,9 +66,8 @@ BKT } func TestRest_Find(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() _, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1") assert.Equal(t, 400, code, "nothing in") @@ -123,9 +119,8 @@ func TestRest_Find(t *testing.T) { } func TestRest_FindAge(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5), Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} @@ -155,9 +150,8 @@ func TestRest_FindAge(t *testing.T) { } func TestRest_FindReadOnly(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -1), Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} @@ -175,7 +169,7 @@ func TestRest_FindReadOnly(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah1&ro=1", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) @@ -197,9 +191,8 @@ func TestRest_FindReadOnly(t *testing.T) { } func TestRest_Last(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -243,9 +236,8 @@ func TestRest_Last(t *testing.T) { } func TestRest_FindUserComments(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -280,22 +272,20 @@ func TestRest_FindUserComments(t *testing.T) { } func TestRest_UserInfo(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() - body, code := getWithAuth(t, ts.URL+"/api/v1/user?site=radio-t") + body, code := getWithDevAuth(t, ts.URL+"/api/v1/user?site=radio-t") assert.Equal(t, 200, code) user := store.User{} err := json.Unmarshal([]byte(body), &user) assert.Nil(t, err) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "", Admin: true, Blocked: false, IP: ""}, user) + assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "http://example.com/pic.png", IP: "127.0.0.1"}, user) } func TestRest_Count(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -323,9 +313,8 @@ func TestRest_Count(t *testing.T) { } func TestRest_Counts(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -353,9 +342,8 @@ func TestRest_Counts(t *testing.T) { } func TestRest_List(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -380,9 +368,8 @@ func TestRest_List(t *testing.T) { } func TestRest_Config(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() body, code := get(t, ts.URL+"/api/v1/config?site=radio-t") assert.Equal(t, 200, code) @@ -400,9 +387,8 @@ func TestRest_Config(t *testing.T) { } func TestRest_Info(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() srv.ReadOnlyAge = 10000000 // make sure we don't hit read-only @@ -438,9 +424,8 @@ func TestRest_Info(t *testing.T) { } func TestRest_Robots(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() body, code := get(t, ts.URL+"/robots.txt") assert.Equal(t, 200, code) diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index 11143548..de740492 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -33,10 +33,11 @@ var testDb = "/tmp/test-remark.db" var testHTML = "/tmp/test-remark.html" var getStartedHTML = "/tmp/getstarted.html" +var devToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg" + func TestRest_FileServer(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() body, code := get(t, ts.URL+"/web/test-remark.html") assert.Equal(t, 200, code) @@ -44,9 +45,8 @@ func TestRest_FileServer(t *testing.T) { } func TestRest_GetStarted(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() err := ioutil.WriteFile(getStartedHTML, []byte("some html blah"), 0700) assert.Nil(t, err) @@ -175,7 +175,7 @@ func TestRest_RunAutocertModeHTTPOnly(t *testing.T) { srv.Shutdown() } -func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { +func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) { b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"}) require.Nil(t, err) @@ -192,10 +192,9 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { srv = &Rest{ DataService: dataStore, Authenticator: auth.NewService(auth.Opts{ - DevPasswd: "password", - SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), - AvatarStore: avatar.NewLocalFS("/tmp"), - AvatarResizeLimit: 300, + AdminPasswd: "password", + SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), + AvatarStore: avatar.NewLocalFS("/tmp/ava-remark42"), }), Cache: &cache.Nop{}, WebRoot: "/tmp", @@ -217,8 +216,18 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { err = ioutil.WriteFile(testHTML, []byte("some html"), 0700) assert.Nil(t, err) + ts = httptest.NewServer(srv.routes()) - return srv, ts + + teardown = func() { + ts.Close() + srv.DataService.Close() + os.Remove(testDb) + os.Remove(testHTML) + os.RemoveAll("/tmp/ava-remark42") + } + + return ts, srv, teardown } func get(t *testing.T, url string) (string, int) { @@ -230,11 +239,24 @@ func get(t *testing.T, url string) (string, int) { return string(body), r.StatusCode } -func getWithAuth(t *testing.T, url string) (string, int) { +func getWithDevAuth(t *testing.T, url string) (body string, code int) { client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("GET", url, nil) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) + r, err := client.Do(req) + require.Nil(t, err) + defer r.Body.Close() + b, err := ioutil.ReadAll(r.Body) + assert.Nil(t, err) + return string(b), r.StatusCode +} + +func getWithAdminAuth(t *testing.T, url string) (string, int) { + client := &http.Client{Timeout: 5 * time.Second} + req, err := http.NewRequest("GET", url, nil) + require.Nil(t, err) + req.SetBasicAuth("admin", "password") r, err := client.Do(req) require.Nil(t, err) defer r.Body.Close() @@ -242,24 +264,22 @@ func getWithAuth(t *testing.T, url string) (string, int) { assert.Nil(t, err) return string(body), r.StatusCode } - func post(t *testing.T, url string, body string) (*http.Response, error) { client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("POST", url, strings.NewReader(body)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") return client.Do(req) } func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string { - b, err := json.Marshal(c) require.Nil(t, err, "can't marshal comment %+v", c) client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b)) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) require.Nil(t, err) require.Equal(t, http.StatusCreated, resp.StatusCode) diff --git a/backend/app/rest/api/rss_test.go b/backend/app/rest/api/rss_test.go index 3eaffe14..201ac3cd 100644 --- a/backend/app/rest/api/rss_test.go +++ b/backend/app/rest/api/rss_test.go @@ -12,9 +12,8 @@ import ( ) func TestServer_RssPost(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() waitOnSecChange() @@ -53,9 +52,8 @@ func TestServer_RssPost(t *testing.T) { } func TestServer_RssSite(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() waitOnSecChange() @@ -107,9 +105,8 @@ func TestServer_RssSite(t *testing.T) { } func TestServer_RssWithReply(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() waitOnSecChange() @@ -159,9 +156,8 @@ func TestServer_RssWithReply(t *testing.T) { } func TestServer_RssReplies(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() waitOnSecChange() diff --git a/backend/vendor/github.com/go-pkgz/auth/.travis.yml b/backend/vendor/github.com/go-pkgz/auth/.travis.yml index db0e0d50..f8747c76 100644 --- a/backend/vendor/github.com/go-pkgz/auth/.travis.yml +++ b/backend/vendor/github.com/go-pkgz/auth/.travis.yml @@ -19,5 +19,5 @@ script: - GO111MODULE=on go get ./... - GO111MODULE=on go mod vendor - GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1; - - ./bin/gometalinter --deadline=120s --exclude=test --exclude=mock --exclude=vendor --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=megacheck --enable=ineffassign --enable=varcheck --enable=unconvert --enable=deadcode --enable=interfacer --enable=gotype ./... || travis_terminate 1; + - ./bin/gometalinter --deadline=120s --exclude=test --exclude=mock --exclude=vendor --exclude=_example --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=megacheck --enable=ineffassign --enable=varcheck --enable=unconvert --enable=deadcode --enable=interfacer --enable=gotype ./... || travis_terminate 1; - $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/backend/vendor/github.com/go-pkgz/auth/README.md b/backend/vendor/github.com/go-pkgz/auth/README.md index ebb5c881..ccba903f 100644 --- a/backend/vendor/github.com/go-pkgz/auth/README.md +++ b/backend/vendor/github.com/go-pkgz/auth/README.md @@ -1,17 +1,20 @@ -# auth - authentication via oauth2 [![Build Status](https://travis-ci.org/go-pkgz/auth.svg?branch=master)](https://travis-ci.org/go-pkgz/auth) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/auth/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/auth?branch=master) +# auth - authentication via oauth2 [![Build Status](https://travis-ci.org/go-pkgz/auth.svg?branch=master)](https://travis-ci.org/go-pkgz/auth) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/auth/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/auth?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/auth?status.svg)](https://godoc.org/github.com/go-pkgz/auth) + + This library provides "social login" with Github, Google, Facebook and Yandex. - Multiple oauth2 providers can be used at the same time - Special `dev` provider allows local testing and development -- JWT stored in a secure cookie and with XSRF protection. Cookies can be session-only +- JWT stored in a secure cookie with XSRF protection. Cookies can be session-only - Minimal scopes with user name, id and picture (avatar) only -- Integrated avatar proxy with FS, boltdb or gridfs storage -- Support of user-defined storages +- Integrated avatar proxy with FS, boltdb and gridfs storages +- Support of user-defined storages for avatars - Black list with user-defined validator - Multiple aud (audience) supported - Secure key with customizable `SecretReader` - Ability to store extra information to token and retrieve on login +- Pre-auth and post-auth hooks to handle custom use cases. - Middleware for easy integration into http routers ## Install @@ -23,25 +26,26 @@ This library provides "social login" with Github, Google, Facebook and Yandex. Example with chi router: ```go + func main() { /// define options options := auth.Opts{ - SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), // secret key for JWT + SecretReader: token.SecretFunc(func(id string) (string, error) { // secret key for JWT + return "secret", nil + }), TokenDuration: time.Hour, CookieDuration: time.Hour * 24, Issuer: "my-test-app", URL: "http://127.0.0.1:8080", - AvatarStore: avatar.NewLocalFS("/tmp", 120), - Validator: middleware.ValidatorFunc(func(_ string, claims token.Claims) bool { - return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_") // allow only dev_ names - }), + AvatarStore: avatar.NewLocalFS("/tmp"), + Validator: token.ValidatorFunc(func(_ string, claims token.Claims) bool { + // allow only dev_* names + return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_") + }), } - // create auth service - service, err := auth.NewService(options) - if err != nil { - log.Fatal(err) - } + // create auth service with providers + service := auth.NewService(options) service.AddProvider("github", "", "") // add github provider service.AddProvider("facebook", "", "") // add facebook provider @@ -69,7 +73,104 @@ func main() { - `middleware.Auth` - requires authenticated user - `middleware.Admin` - requires authenticated and admin user - `middleware.Trace` - doesn't require authenticated user, but adds user info to request - + +## Details + +Generally, adding support of `auth` includes a few relatively simple steps: + +1. Setup `auth.Opts` structure with all parameters. Each of them [documented](https://github.com/go-pkgz/auth/blob/master/auth.go#L29) and most of parameters are optional and have sane defaults. +2. [Create](https://github.com/go-pkgz/auth/blob/master/auth.go#L56) the new `auth.Service` with provided options. +3. [Add all](https://github.com/go-pkgz/auth/blob/master/auth.go#L149) desirable authentication providers. Currently supported Github, Google, Facebook and Yandex +4. Retrieve [middleware](https://github.com/go-pkgz/auth/blob/master/auth.go#L144) and [http handlers](https://github.com/go-pkgz/auth/blob/master/auth.go#L105) from `auth.Service` +5. Wire auth and avatar handlers into http router as sub–routes. + +### API + +For the example above authentication handlers wired as `/auth` and provides: + +- `/auth//login?id=&from=` - site_id used as `aud` claim for the token and can be processed by `SecretReader` to load/retrieve/define different secrets. redirect_url is the url to redirect after successful login. +- `/avatar/` - returns the avatar (image). Links to those pictures added into user info automatically, for details see "Avatar proxy" +- `/auth//logout` and `/auth/logout` - invalidate "session" by removing JWT cookie +- `/auth/list` - gives a json list of active providers +- `/auth/user` - returns `token.User` (json) + +### User info + +Middleware populates `token.User` to request's context. It can be loaded with `token.GetUserInfo(r *http.Request) (user User, err error)` or `token.MustGetUserInfo(r *http.Request) User` functions. + +`token.User` object includes all fields retrieved from oauth2 provider: +- `Name` - user name +- `ID` - hash of user id +- `Picture` - full link to proxied avatar (see "Avatar proxy") + +It also has placeholders for fields application can populate with custom `token.ClaimsUpdater` (see "Customization") + +- `IP` - hash of user's IP address +- `Email` - user's email +- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provides, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details. + + +### Avatar proxy + +Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides and automatic proxy + +- On each login the proxy will retrieve user's picture and save it to `AvatarStore` +- Local (proxied) link to avatar included in user's info (jwt token) +- API for avatar removal provided as a part of `AvatarStore` +- User can leverage one of provided stores: + - `avatar.LocalFS` - file system, each avatar in a separate file + - `avatar.BoltDB` - a single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store). + - `avatar.GridFS` - external [GridFS](https://docs.mongodb.com/manual/core/gridfs/) (mongo db). +- In case of need a custom implementation of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25). +- All avatar-related setup done as a part of `auth.Opts` and needs: + - `AvatarStore` - avatar store to use, i.e. `avatar.NewLocalFS("/tmp/avatars")` + - `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links links this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info") + - `AvatarResizeLimit` - size (in pixel) used to resize avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing. + +### Customization + +There are several ways to adjust functionality of the library: + +1. `SecretReader` - interface with a single method `Get(aud string) string` to return secret used for JWT signing and verification +1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on. +2. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users. + +All of interfaces have corresponding Func wrappers (adapters) - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`. + +### Implementing black list logic or some other filters + +Restricting some users or some tokens is two step process: + +- `ClaimsUpdater` sets an attribute, like `blocked` (or `allowed`) +- `Validator` checks the attribute and returns true/false + +_This technic used in the [example](https://github.com/go-pkgz/auth/blob/master/_example/backend/main.go#L36) code_ + +The process can be simplified by doing all checks directly in `Validator`, but depends on particular case such solution +can be too expensive because `Validator` runs on each request as a part of auth middleware. In contrast, `ClaimsUpdater` called on token creation/refresh only. + + +### Dev provider + +Working with oauth2 providers can be a pain, especially during development phase. A special, development-only provider `dev` can make it less painful. This one can be registered directly, i.e. `service.AddProvider("dev", "", "")` and should be activated like this: + +```go + // runs dev oauth2 server on :8084 + go func() { + p, err := service.Provider("dev") + if err != nil { + log.Fatal(err) + } + devAuthServer := provider.DevAuthServer{Provider: p} + devAuthServer.Run() + }() +``` + +It will run fake aouth2 "server" on port :8084 and user could login with any user name. See [example](https://github.com/go-pkgz/auth/blob/master/_example/backend/main.go) for more details. + +_Warning: this is not the real oauth2 server but just a small fake thing for development and testing only. Don't use `dev` provider with any production code._ + + ## Register oauth2 providers Authentication handled by external providers. You should setup oauth2 for all (or some) of them to allow users to authenticate. It is not mandatory to have all of them, but at least one should be correctly configured. @@ -77,11 +178,11 @@ Authentication handled by external providers. You should setup oauth2 for all (o #### Google Auth Provider 1. Create a new project: https://console.developers.google.com/project -1. Choose the new project from the top right project dropdown (only if another project is selected) -1. In the project Dashboard center pane, choose **"API Manager"** -1. In the left Nav pane, choose **"Credentials"** -1. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save. -1. In the center pane, choose **"Credentials"** tab. +2. Choose the new project from the top right project dropdown (only if another project is selected) +3. In the project Dashboard center pane, choose **"API Manager"** +4. In the left Nav pane, choose **"Credentials"** +5. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save. +6. In the center pane, choose **"Credentials"** tab. * Open the **"New credentials"** drop down * Choose **"OAuth client ID"** * Choose **"Web application"** @@ -89,7 +190,7 @@ Authentication handled by external providers. You should setup oauth2 for all (o * Authorized origins is your domain ex: `https://example.mysite.com` * Authorized redirect URIs is the location of oauth2/callback constructed as domain + `/auth/google/callback`, ex: `https://example.mysite.com/auth/google/callback` * Choose **"Create"** -2. Take note of the **Client ID** and **Client Secret** +7. Take note of the **Client ID** and **Client Secret** _instructions for google oauth2 setup borrowed from [oauth2_proxy](https://github.com/bitly/oauth2_proxy)_ @@ -125,4 +226,6 @@ For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/co ## Status -The library extracted from [remark42](https://github.com/umputun/remark) project. The code in production use on multiple sites and seems to work fine. \ No newline at end of file +The library extracted from [remark42](https://github.com/umputun/remark) project. The original code in production use on multiple sites and seems to work fine. + +`go-pkgz/auth` library still in beta and until version 1 released some breaking changes still possible. \ No newline at end of file diff --git a/backend/vendor/github.com/go-pkgz/auth/auth.go b/backend/vendor/github.com/go-pkgz/auth/auth.go index a87534e4..d1ef0305 100644 --- a/backend/vendor/github.com/go-pkgz/auth/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/auth.go @@ -27,7 +27,7 @@ type Service struct { // Opts is a full set of all parameters to initialize Service type Opts struct { - SecretReader token.Secret // reader returns secret for given site id (aud) + SecretReader token.Secret // reader returns secret for given site id (aud), required ClaimsUpd token.ClaimsUpdater // updater for jwt to add/modify values stored in the token SecureCookies bool // makes jwt cookie secure TokenDuration time.Duration // token's TTL, refreshed automatically @@ -42,14 +42,14 @@ type Opts struct { Issuer string // optional value for iss claim, usually the application name, default "go-pkgz/auth" - URL string // root url for the rest service, i.e. http://blah.example.com + URL string // root url for the rest service, i.e. http://blah.example.com, required Validator token.Validator // validator allows to reject some valid tokens with user-defined logic - AvatarStore avatar.Store // store to save/load avatars + AvatarStore avatar.Store // store to save/load avatars, required AvatarResizeLimit int // resize avatar's limit in pixels - AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar" + AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar", default `/avatar` - DevPasswd string // if presented, allows basic auth with user dev and given password + AdminPasswd string // if presented, allows basic auth with user admin and given password } // NewService initializes everything @@ -71,7 +71,7 @@ func NewService(opts Opts) *Service { if opts.SecretReader == nil { jwtService.SecretReader = token.SecretFunc(func(id string) (string, error) { - return "", errors.New("secrets reader not avalibale") + return "", errors.New("secrets reader not available") }) } @@ -79,9 +79,9 @@ func NewService(opts Opts) *Service { opts: opts, jwtService: jwtService, authMiddleware: middleware.Authenticator{ - JWTService: jwtService, - Validator: opts.Validator, - DevPasswd: opts.DevPasswd, + JWTService: jwtService, + Validator: opts.Validator, + AdminPasswd: opts.AdminPasswd, }, } @@ -96,6 +96,9 @@ func NewService(opts Opts) *Service { RoutePath: opts.AvatarRoutePath, ResizeLimit: opts.AvatarResizeLimit, } + if res.avatarProxy.RoutePath == "" { + res.avatarProxy.RoutePath = "/avatar" + } } return &res @@ -104,7 +107,7 @@ func NewService(opts Opts) *Service { // Handlers gets http.Handler for all providers and avatars func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handler) { - providerHandler := func(w http.ResponseWriter, r *http.Request) { + ah := func(w http.ResponseWriter, r *http.Request) { elems := strings.Split(r.URL.Path, "/") if len(elems) < 2 { w.WriteHeader(http.StatusBadRequest) @@ -127,6 +130,19 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl return } + // show user info + if elems[len(elems)-1] == "user" { + claims, _, err := s.jwtService.Get(r) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + rest.RenderJSON(w, r, rest.JSON{"error": err.Error()}) + return + } + rest.RenderJSON(w, r, claims.User) + return + } + + // regular auth handlers provName := elems[len(elems)-2] p, err := s.Provider(provName) if err != nil { @@ -137,10 +153,10 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl p.Handler(w, r) } - return http.HandlerFunc(providerHandler), http.HandlerFunc(s.avatarProxy.Handler) + return http.HandlerFunc(ah), http.HandlerFunc(s.avatarProxy.Handler) } -// Middleware returns token middleware +// Middleware returns auth middleware func (s *Service) Middleware() middleware.Authenticator { return s.authMiddleware } @@ -152,7 +168,7 @@ func (s *Service) AddProvider(name string, cid string, csecret string) { URL: s.opts.URL, JwtService: s.jwtService, Issuer: s.issuer, - AvatarProxy: s.avatarProxy, + AvatarSaver: s.avatarProxy, Cid: cid, Csecret: csecret, } diff --git a/backend/vendor/github.com/go-pkgz/auth/go.mod b/backend/vendor/github.com/go-pkgz/auth/go.mod index 2369b3bd..736ae648 100644 --- a/backend/vendor/github.com/go-pkgz/auth/go.mod +++ b/backend/vendor/github.com/go-pkgz/auth/go.mod @@ -2,22 +2,14 @@ module github.com/go-pkgz/auth require ( cloud.google.com/go v0.34.0 // indirect - github.com/boltdb/bolt v1.3.1 // indirect github.com/coreos/bbolt v1.3.0 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 - github.com/go-errors/errors v1.0.1 github.com/go-pkgz/mongo v1.0.0 - github.com/go-pkgz/rest v1.1.1 - github.com/kr/pretty v0.1.0 // indirect + github.com/go-pkgz/rest v1.1.5 github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 github.com/pkg/errors v0.8.0 - github.com/stretchr/testify v1.2.2 golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 - golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect - golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 // indirect - google.golang.org/appengine v1.4.0 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/backend/vendor/github.com/go-pkgz/auth/go.sum b/backend/vendor/github.com/go-pkgz/auth/go.sum index 2676c7cb..63a7900c 100644 --- a/backend/vendor/github.com/go-pkgz/auth/go.sum +++ b/backend/vendor/github.com/go-pkgz/auth/go.sum @@ -1,7 +1,5 @@ cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= -github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/coreos/bbolt v1.3.0 h1:HIgH5xUWXT914HCI671AxuTTqjj64UOFr7pHn48LUTI= github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -10,20 +8,15 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-pkgz/mongo v1.0.0 h1:9jijAK7prCRMetiyTu3c1rv/2lMypzuf2DWcVpTlwzw= github.com/go-pkgz/mongo v1.0.0/go.mod h1:R9si/F2aJsjz4MUxhzuppIHY8yLV3YCeuCpgcI50cu4= -github.com/go-pkgz/rest v1.1.1 h1:YuLe+wOJwcE+Y0SkJ+AtvUOPGjTMe4Q4vg98Uqs9CKc= -github.com/go-pkgz/rest v1.1.1/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/go-pkgz/rest v1.1.3 h1:rMf+xJn8i1Ip9OKohusZsRxwntM0BwYu8OX8BuEwN80= +github.com/go-pkgz/rest v1.1.3/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= +github.com/go-pkgz/rest v1.1.4 h1:/Lrg9kBWBjNah7nmCDHLszRAfVVBIy5ajf0vVgpHPi0= +github.com/go-pkgz/rest v1.1.4/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= +github.com/go-pkgz/rest v1.1.5 h1:5br4mnscfLb27yxv5hJFLBVmAt09PrmIBP+meA3CfHc= +github.com/go-pkgz/rest v1.1.5/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc= github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= @@ -34,17 +27,7 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 h1:IcgEB62HYgAhX0Nd/QrVgZlxlcyxbGQHElLUhW2X4Fo= -golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go index 80537ea6..164a35c0 100644 --- a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go @@ -13,20 +13,12 @@ import ( "github.com/go-pkgz/auth/token" ) -// Authenticator is top level token object providing middlewares +// Authenticator is top level auth object providing middlewares type Authenticator struct { - JWTService *token.Service - Providers []provider.Service - Validator token.Validator - DevPasswd string -} - -var devUser = token.User{ - ID: "dev", - Name: "developer one", - Attributes: map[string]interface{}{ - "admin": true, - }, + JWTService *token.Service + Providers []provider.Service + Validator token.Validator + AdminPasswd string } var adminUser = token.User{ @@ -37,7 +29,7 @@ var adminUser = token.User{ }, } -// Auth middleware adds token from session and populates user info +// Auth middleware adds auth from session and populates user info func (a *Authenticator) Auth(next http.Handler) http.Handler { return a.auth(true)(next) } @@ -47,6 +39,7 @@ func (a *Authenticator) Trace(next http.Handler) http.Handler { return a.auth(false)(next) } +// auth implements all logic for authentication (reqAuth=true) and tracing (reqAuth=false) func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) { @@ -57,27 +50,20 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { h.ServeHTTP(w, r) return } - log.Printf("[DEBUG] failed token, %s", err) + log.Printf("[DEBUG] auth failed, %s", err) http.Error(w, "Unauthorized", http.StatusUnauthorized) } f := func(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - // if secret key matches for given site (from request) return admin user - if a.checkSecretKey(r) { + // use admin user basic auth if enabled + if a.basicAdminUser(r) { r = token.SetUserInfo(r, adminUser) h.ServeHTTP(w, r) return } - // use dev user basic token if enabled - if a.basicDevUser(r) { - r = token.SetUserInfo(r, devUser) - h.ServeHTTP(w, r) - return - } - claims, tkn, err := a.JWTService.Get(r) if err != nil { onError(h, w, r, errors.Wrap(err, "can't get token")) @@ -85,12 +71,12 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { } if claims.Handshake != nil { // handshake in token indicate special use cases, not for login - onError(h, w, r, errors.Errorf("invalid kind of token for %s/%s", claims.User.Name, claims.User.ID)) + onError(h, w, r, errors.New("invalid kind of token")) return } if claims.User == nil { - onError(h, w, r, errors.New("failed token, no user info presented in the claim")) + onError(h, w, r, errors.New("failed auth, no user info presented in the claim")) return } @@ -121,29 +107,11 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { return f } -func (a *Authenticator) checkSecretKey(r *http.Request) bool { - if a.JWTService.SecretReader == nil { - return false - } - - aud := r.URL.Query().Get("aud") - secret := r.URL.Query().Get("secret") - - skey, err := a.JWTService.SecretReader.Get(aud) - if err != nil { - return false - } - - if strings.TrimSpace(secret) == "" || secret != skey { - return false - } - return true -} - -// refreshExpiredToken makes new token with passed claims, but only if permission allowed +// refreshExpiredToken makes a new token with passed claims func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token.Claims) (token.Claims, error) { - // refresh token - if err := a.JWTService.Set(w, claims, false); err != nil { + + claims.ExpiresAt = 0 // this will cause now+duration for refreshed token + if err := a.JWTService.Set(w, claims); err != nil { return token.Claims{}, err } return claims, nil @@ -168,9 +136,10 @@ func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { return http.HandlerFunc(fn) } -func (a *Authenticator) basicDevUser(r *http.Request) bool { +// basic auth for admin user +func (a *Authenticator) basicAdminUser(r *http.Request) bool { - if a.DevPasswd == "" { + if a.AdminPasswd == "" { return false } @@ -181,18 +150,18 @@ func (a *Authenticator) basicDevUser(r *http.Request) bool { b, err := base64.StdEncoding.DecodeString(s[1]) if err != nil { - log.Printf("[WARN] dev user token failed, failed to decode %s, %s", s[1], err) + log.Printf("[WARN] admin user auth failed, can't to decode %s, %s", s[1], err) return false } pair := strings.SplitN(string(b), ":", 2) if len(pair) != 2 { - log.Printf("[WARN] dev user token failed, failed to split %s", string(b)) + log.Printf("[WARN] admin user auth failed, can't split basic auth %s", string(b)) return false } - if pair[0] != "dev" || pair[1] != a.DevPasswd { - log.Printf("[WARN] dev user token failed, user/passwd mismatch %+v", pair) + if pair[0] != "admin" || pair[1] != a.AdminPasswd { + log.Printf("[WARN] dev user auth failed, user/passwd mismatch %+v", pair) return false } diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go index 514a35e9..9dc6226b 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" "sync" + "text/template" "time" "github.com/nullrocks/identicon" @@ -46,6 +47,12 @@ func (d *DevAuthServer) Run() { log.Printf("[WARN] can't create identicon, %s", err) } + userFormTmpl, err := template.New("page").Parse(devUserFormTmpl) + if err != nil { + log.Printf("[WARN] can't parse user form template, %s", err) + return + } + d.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", devAuthPort), Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -56,7 +63,10 @@ func (d *DevAuthServer) Run() { // first time it will be called without username and will ask for one if !d.Automatic && (r.ParseForm() != nil || r.Form.Get("username") == "") { - if _, err = w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil { + + formData := struct{ Query string }{Query: r.URL.RawQuery} + + if err = userFormTmpl.Execute(w, formData); err != nil { log.Printf("[WARN] can't write, %s", err) } return @@ -176,24 +186,121 @@ func (d *DevAuthServer) genAvatar(user string) ([]byte, error) { return buf.Bytes(), err } -var devUserForm = ` +var devUserFormTmpl = ` - - Dev User - - + + Dev OAuth + + -
- username: - + +
+

GO-PKGZ/AUTH

+

Dev Provider

+
+ + +

Not for production use

- + + ` diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go index 0241b347..cb5ee7db 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go @@ -18,8 +18,8 @@ func NewGoogle(p Params) Service { return initService(p, Service{ Name: "google", Endpoint: google.Endpoint, - RedirectURL: p.URL + "/token/google/callback", - Scopes: []string{"https://www.googleapis.com/token/userinfo.profile"}, + RedirectURL: p.URL + "/auth/google/callback", + Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"}, InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo", MapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ @@ -41,7 +41,7 @@ func NewGithub(p Params) Service { return initService(p, Service{ Name: "github", Endpoint: github.Endpoint, - RedirectURL: p.URL + "/token/github/callback", + RedirectURL: p.URL + "/auth/github/callback", Scopes: []string{}, InfoURL: "https://api.github.com/user", MapUser: func(data userData, _ []byte) token.User { @@ -76,7 +76,7 @@ func NewFacebook(p Params) Service { return initService(p, Service{ Name: "facebook", Endpoint: facebook.Endpoint, - RedirectURL: p.URL + "/token/facebook/callback", + RedirectURL: p.URL + "/auth/facebook/callback", Scopes: []string{"public_profile"}, InfoURL: "https://graph.facebook.com/me?fields=id,name,picture", MapUser: func(data userData, bdata []byte) token.User { @@ -102,7 +102,7 @@ func NewYandex(p Params) Service { return initService(p, Service{ Name: "yandex", Endpoint: yandex.Endpoint, - RedirectURL: p.URL + "/token/yandex/callback", + RedirectURL: p.URL + "/auth/yandex/callback", Scopes: []string{}, // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ InfoURL: "https://login.yandex.ru/info?format=json", diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/service.go b/backend/vendor/github.com/go-pkgz/auth/provider/service.go index 9d05b828..df98d6a6 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/service.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/service.go @@ -17,7 +17,6 @@ import ( "github.com/pkg/errors" "golang.org/x/oauth2" - "github.com/go-pkgz/auth/avatar" "github.com/go-pkgz/auth/token" ) @@ -37,12 +36,17 @@ type Service struct { type Params struct { URL string JwtService *token.Service - AvatarProxy *avatar.Proxy + AvatarSaver AvatarSaver Cid string Csecret string Issuer string } +// AvatarSaver defines minimal interface to save avatar +type AvatarSaver interface { + Put(u token.User) (avatarURL string, err error) +} + type userData map[string]interface{} func (u userData) value(key string) string { @@ -53,9 +57,9 @@ func (u userData) value(key string) string { return "" } -// initService makes token service for given provider +// initService makes oauth2 service for given provider func initService(p Params, service Service) Service { - log.Printf("[INFO] init token service %s", service.Name) + log.Printf("[INFO] init oauth2 service %s", service.Name) service.Params = p service.conf = oauth2.Config{ ClientID: service.Cid, @@ -65,7 +69,7 @@ func initService(p Params, service Service) Service { Endpoint: service.Endpoint, } - log.Printf("[DEBUG] created %s token, id=%s, redir=%s, endpoint=%s", + log.Printf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s", service.Name, service.Cid, service.Endpoint, service.RedirectURL) return service } @@ -123,7 +127,7 @@ func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) { }, } - if err := p.JwtService.Set(w, claims, false); err != nil { + if err := p.JwtService.Set(w, claims); err != nil { rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") return } @@ -144,9 +148,14 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { return } + if oauthClaims.Handshake == nil { + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "finvalid handshake token") + return + } + retrievedState := oauthClaims.Handshake.State if retrievedState == "" || retrievedState != r.URL.Query().Get("state") { - http.Error(w, fmt.Sprintf("unexpected state %v", retrievedState), http.StatusUnauthorized) + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state") return } @@ -160,7 +169,7 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { 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)) + rest.SendErrorJSON(w, r, http.StatusServiceUnavailable, err, "failed to get client info") return } @@ -201,8 +210,8 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { SessionOnly: oauthClaims.SessionOnly, } - if err = p.JwtService.Set(w, claims, oauthClaims.SessionOnly); err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save user info") + if err = p.JwtService.Set(w, claims); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") return } @@ -218,8 +227,8 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { // setAvatar saves avatar and puts proxied URL to u.Picture func (p Service) setAvatar(u token.User) token.User { - if p.AvatarProxy != nil { - if avatarURL, e := p.AvatarProxy.Put(u); e == nil { + if p.AvatarSaver != nil { + if avatarURL, e := p.AvatarSaver.Put(u); e == nil { u.Picture = avatarURL } else { log.Printf("[WARN] failed to set avatar for %+v, %+v", u, e) @@ -231,7 +240,6 @@ func (p Service) setAvatar(u token.User) token.User { // LogoutHandler - GET /logout func (p Service) LogoutHandler(w http.ResponseWriter, r *http.Request) { p.JwtService.Reset(w) - log.Printf("[DEBUG] logout") } func (p Service) randToken() (string, error) { diff --git a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go index 97f5f2bc..96e918b6 100644 --- a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go +++ b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go @@ -154,7 +154,7 @@ func (j *Service) Parse(tokenString string) (Claims, error) { // Set creates token cookie with xsrf cookie and put it to ResponseWriter // accepts claims and sets expiration if none defined. permanent flag means long-living cookie, // false makes it session only. -func (j *Service) Set(w http.ResponseWriter, claims Claims, sessionOnly bool) error { +func (j *Service) Set(w http.ResponseWriter, claims Claims) error { if claims.ExpiresAt == 0 { claims.ExpiresAt = time.Now().Add(j.TokenDuration).Unix() } @@ -167,7 +167,7 @@ func (j *Service) Set(w http.ResponseWriter, claims Claims, sessionOnly bool) er } cookieExpiration := 0 // session cookie - if !sessionOnly { + if !claims.SessionOnly && claims.Handshake == nil { cookieExpiration = int(j.CookieDuration.Seconds()) } diff --git a/backend/vendor/github.com/go-pkgz/rest/README.md b/backend/vendor/github.com/go-pkgz/rest/README.md index 6e80e899..8a6c6f05 100644 --- a/backend/vendor/github.com/go-pkgz/rest/README.md +++ b/backend/vendor/github.com/go-pkgz/rest/README.md @@ -1,4 +1,5 @@ -## REST helpers and middleware [![Build Status](https://travis-ci.org/go-pkgz/rest.svg?branch=master)](https://travis-ci.org/go-pkgz/rest) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/rest)](https://goreportcard.com/report/github.com/go-pkgz/rest) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/rest/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/rest?branch=master) +## REST helpers and middleware [![Build Status](https://travis-ci.org/go-pkgz/rest.svg?branch=master)](https://travis-ci.org/go-pkgz/rest) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/rest)](https://goreportcard.com/report/github.com/go-pkgz/rest) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/rest/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/rest?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/rest?status.svg)](https://godoc.org/github.com/go-pkgz/rest) + ## Install and update diff --git a/backend/vendor/github.com/go-pkgz/rest/httperrors.go b/backend/vendor/github.com/go-pkgz/rest/httperrors.go index c9730ffc..e5d20588 100644 --- a/backend/vendor/github.com/go-pkgz/rest/httperrors.go +++ b/backend/vendor/github.com/go-pkgz/rest/httperrors.go @@ -1,6 +1,7 @@ package rest import ( + "errors" "fmt" "log" "net/http" @@ -9,14 +10,14 @@ import ( "strings" ) -// SendErrorJSON makes {error: blah, details: blah} json body and responds with error code -func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, details string) { - log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, details)) +// SendErrorJSON sends {error: msg} with error code and logging error and caller +func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, msg string) { + log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, msg)) w.WriteHeader(code) - RenderJSON(w, r, map[string]interface{}{"error": err.Error(), "details": details}) + RenderJSON(w, r, JSON{"error": msg}) } -func errDetailsMsg(r *http.Request, code int, err error, details string) string { +func errDetailsMsg(r *http.Request, code int, err error, msg string) string { q := r.URL.String() if qun, e := url.QueryUnescape(q); e == nil { @@ -35,5 +36,8 @@ func errDetailsMsg(r *http.Request, code int, err error, details string) string if pos := strings.Index(remoteIP, ":"); pos >= 0 { remoteIP = remoteIP[:pos] } - return fmt.Sprintf("%s - %v - %d - %s - %s%s", details, err, code, remoteIP, q, srcFileInfo) + if err == nil { + err = errors.New("no error") + } + return fmt.Sprintf("%s - %v - %d - %s - %s%s", msg, err, code, remoteIP, q, srcFileInfo) } diff --git a/backend/vendor/github.com/go-pkgz/rest/logger/logger.go b/backend/vendor/github.com/go-pkgz/rest/logger/logger.go index d3746101..2bdbc882 100644 --- a/backend/vendor/github.com/go-pkgz/rest/logger/logger.go +++ b/backend/vendor/github.com/go-pkgz/rest/logger/logger.go @@ -36,6 +36,13 @@ const ( None ) +// Logger returns default logger middleware +func Logger(next http.Handler) http.Handler { + l := New(Flags(All), Prefix("[REST]")) + return l.Handler(next) + +} + // New makes rest Logger with given options func New(options ...Option) *Middleware { res := Middleware{ From 55cae69d49973ae696b7d13b510ae2ebf8a0a865 Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 30 Dec 2018 16:17:00 -0600 Subject: [PATCH 10/21] add test triggeting auth hooks --- backend/app/cmd/server_test.go | 74 +++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index c73985cf..61ed2f0a 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -13,9 +13,12 @@ import ( "testing" "time" + jwt "github.com/dgrijalva/jwt-go" "github.com/globalsign/mgo" + "github.com/go-pkgz/auth/token" "github.com/go-pkgz/mongo" flags "github.com/jessevdk/go-flags" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -312,13 +315,80 @@ func Test_ACMEEmail(t *testing.T) { assert.Equal(t, "admin@remark.com", cfg.ACMEEmail) } +func TestServerAuthHooks(t *testing.T) { + app, ctx := prepServerApp(t, 2500*time.Millisecond, func(o ServerCommand) ServerCommand { + o.Port = 18080 + return o + }) + + go func() { _ = app.run(ctx) }() + time.Sleep(100 * time.Millisecond) // let server start + + // make a token for user dev + tkService := app.restSrv.Authenticator.TokenService() + tkService.TokenDuration = time.Second + + claims := token.Claims{ + StandardClaims: jwt.StandardClaims{ + Audience: "remark", + Issuer: "remark", + ExpiresAt: time.Now().Add(time.Second).Unix(), + NotBefore: time.Now().Add(-1 * time.Minute).Unix(), + }, + User: &token.User{ + ID: "dev", + Name: "developer one", + }, + } + tk, err := tkService.Token(claims) + require.NoError(t, err) + t.Log(tk) + + // add comment + client := http.Client{Timeout: 5 * time.Second} + req, err := http.NewRequest("POST", "http://localhost:18080/api/v1/comment", + strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`)) + req.Header.Set("X-JWT", tk) + require.Nil(t, err) + resp, err := client.Do(req) + require.Nil(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusCreated, resp.StatusCode, "non-blocked user able to post") + + // block user dev as admin + req, e := http.NewRequest(http.MethodPut, "http://localhost:18080/api/v1/admin/user/dev?site=remark&block=1&ttl=10d", nil) + assert.Nil(t, e) + req.SetBasicAuth("admin", "password") + resp, e = client.Do(req) + require.Nil(t, e) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode, "user dev blocked") + b, err := ioutil.ReadAll(resp.Body) + require.Nil(t, e) + t.Log(string(b)) + + time.Sleep(2 * time.Second) // make sure token expired and refresh happened + + // try add a comment with blocked user + req, err = http.NewRequest("POST", "http://localhost:18080/api/v1/comment", + strings.NewReader(`{"text": "test 123 blah", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`)) + req.Header.Set("X-JWT", tk) + require.Nil(t, err) + resp, err = client.Do(req) + require.Nil(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusForbidden, resp.StatusCode, "blocked user can't post") + + app.Wait() +} + func prepServerApp(t *testing.T, duration time.Duration, fn func(o ServerCommand) ServerCommand) (*serverApp, context.Context) { cmd := ServerCommand{} - cmd.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"}) + cmd.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "secret"}) // prepare options p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--admin-passwd=password"}) + _, err := p.ParseArgs([]string{"--admin-passwd=password", "--site=remark"}) require.Nil(t, err) cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation = "/tmp", "fs", "/tmp" cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port) From a7c0041d8c0c5b307105e34b1b9de67fe60ca98b Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 30 Dec 2018 16:36:08 -0600 Subject: [PATCH 11/21] test non-0 crash --- backend/app/main_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/app/main_test.go b/backend/app/main_test.go index 7981e1d6..6d5b3320 100644 --- a/backend/app/main_test.go +++ b/backend/app/main_test.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "os" + "os/exec" "strings" "sync" "syscall" @@ -56,3 +57,19 @@ func TestGetDump(t *testing.T) { assert.True(t, strings.Contains(dump, "backend/app/main.go")) log.Print("\n dump:" + dump) } + +func TestCrasher(t *testing.T) { + if os.Getenv("BE_CRASHER") == "1" { + main() + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestCrasher") + cmd.Env = append(os.Environ(), "BE_CRASHER=1") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + err := cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + t.Fatalf("process ran with err %v, want exit status 1", err) +} From c21f8757ddc774615446eaa02fed7bea18f4f790 Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 30 Dec 2018 17:16:57 -0600 Subject: [PATCH 12/21] Revert "test non-0 crash" This reverts commit a7c0041d8c0c5b307105e34b1b9de67fe60ca98b. --- backend/app/main_test.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/backend/app/main_test.go b/backend/app/main_test.go index 6d5b3320..7981e1d6 100644 --- a/backend/app/main_test.go +++ b/backend/app/main_test.go @@ -5,7 +5,6 @@ import ( "log" "net/http" "os" - "os/exec" "strings" "sync" "syscall" @@ -57,19 +56,3 @@ func TestGetDump(t *testing.T) { assert.True(t, strings.Contains(dump, "backend/app/main.go")) log.Print("\n dump:" + dump) } - -func TestCrasher(t *testing.T) { - if os.Getenv("BE_CRASHER") == "1" { - main() - return - } - cmd := exec.Command(os.Args[0], "-test.run=TestCrasher") - cmd.Env = append(os.Environ(), "BE_CRASHER=1") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stdout - err := cmd.Run() - if e, ok := err.(*exec.ExitError); ok && !e.Success() { - return - } - t.Fatalf("process ran with err %v, want exit status 1", err) -} From 4215db8d89fcada86b77494d7797094c7a7fc91d Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 30 Dec 2018 17:42:54 -0600 Subject: [PATCH 13/21] change to ADMIN --- compose-dev-backend.yml | 2 +- compose-dev-frontend.yml | 4 ++-- docker-compose.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compose-dev-backend.yml b/compose-dev-backend.yml index a213f5ca..5cc72f6b 100644 --- a/compose-dev-backend.yml +++ b/compose-dev-backend.yml @@ -40,7 +40,7 @@ services: - STORE_BOLT_PATH=/srv/var/db - BACKUP_PATH=/srv/var/backup - DEBUG=true - - DEV_PASSWD=password + - ADMIN_PASSWD=password - AUTH_DEV=true # activate local oauth "dev" - ADMIN_SHARED_ID=dev_user # set admin flag for default user on local ouath2 - NOTIFY_TYPE diff --git a/compose-dev-frontend.yml b/compose-dev-frontend.yml index f2760302..f21c504a 100644 --- a/compose-dev-frontend.yml +++ b/compose-dev-frontend.yml @@ -31,9 +31,9 @@ services: - REMARK_URL=http://127.0.0.1:8080 - SECRET=12345 - STORE_BOLT_PATH=/srv/var/db - - BACKUP_PATH=/srv/var/backup + - BACKUP_PATH=/srv/var/backupang - DEBUG=true - - DEV_PASSWD=password + - ADMIN_PASSWD=password - AUTH_DEV=true # activate local oauth "dev" - ADMIN_SHARED_ID=dev_user # set admin flag for default user on local ouath2 volumes: diff --git a/docker-compose.yml b/docker-compose.yml index cce74474..87234c00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,6 @@ services: - AUTH_FACEBOOK_CSEC - AUTH_DISQUS_CID - AUTH_DISQUS_CSEC - # - DEV_PASSWD=password # development mode, be careful! + # - ADMIN_PASSWD=password volumes: - ./var:/srv/var From 80467c39af21b262cb474c0e840113342c365607 Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 30 Dec 2018 18:00:32 -0600 Subject: [PATCH 14/21] switch utils commands to basic auth, remove secret passing --- backend/app/cmd/backup.go | 14 ++++++++------ backend/app/cmd/backup_test.go | 9 +++++---- backend/app/cmd/cleanup.go | 18 ++++++++++-------- backend/app/cmd/cleanup_test.go | 6 +++--- backend/app/cmd/import.go | 15 ++++++++------- backend/app/cmd/import_test.go | 12 ++++++------ backend/app/cmd/restore.go | 18 ++++++++++-------- backend/app/cmd/restore_test.go | 2 +- backend/app/cmd/server.go | 2 +- 9 files changed, 52 insertions(+), 44 deletions(-) diff --git a/backend/app/cmd/backup.go b/backend/app/cmd/backup.go index 584dab33..c0cae341 100644 --- a/backend/app/cmd/backup.go +++ b/backend/app/cmd/backup.go @@ -15,17 +15,18 @@ import ( // BackupCommand set of flags and command for export // ExportPath used as a separate element to leverage BACKUP_PATH. If ExportFile has a path (i.e. with /) BACKUP_PATH ignored. type BackupCommand struct { - ExportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"` - ExportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.TS}}.gz" description:"file name"` - Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"` - Timeout time.Duration `long:"timeout" default:"15m" description:"export (backup) timeout"` + ExportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"` + ExportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.TS}}.gz" description:"file name"` + Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"` + Timeout time.Duration `long:"timeout" default:"15m" description:"export (backup) timeout"` + AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"` CommonOpts } // Execute runs export with ExportCommand parameters, entry point for "export" command func (ec *BackupCommand) Execute(args []string) error { log.Printf("[INFO] export to %s, site %s", ec.ExportPath, ec.Site) - resetEnv("SECRET") + resetEnv("SECRET", "ADMIN_PASSWD") fp := fileParser{site: ec.Site, path: ec.ExportPath, file: ec.ExportFile} fname, err := fp.parse(time.Now()) @@ -39,11 +40,12 @@ func (ec *BackupCommand) Execute(args []string) error { client := http.Client{} ctx, cancel := context.WithTimeout(context.Background(), ec.Timeout) defer cancel() - exportURL := fmt.Sprintf("%s/api/v1/admin/export?mode=file&site=%s&secret=%s", ec.RemarkURL, ec.Site, ec.SharedSecret) + exportURL := fmt.Sprintf("%s/api/v1/admin/export?mode=file&site=%s", ec.RemarkURL, ec.Site) req, err := http.NewRequest(http.MethodGet, exportURL, nil) if err != nil { return errors.Wrapf(err, "can't make export request for %s", exportURL) } + req.SetBasicAuth("admin", ec.AdminPasswd) // get with timeout resp, err := client.Do(req.WithContext(ctx)) diff --git a/backend/app/cmd/backup_test.go b/backend/app/cmd/backup_test.go index e163d0f4..612db960 100644 --- a/backend/app/cmd/backup_test.go +++ b/backend/app/cmd/backup_test.go @@ -8,7 +8,7 @@ import ( "os" "testing" - "github.com/jessevdk/go-flags" + flags "github.com/jessevdk/go-flags" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,7 +24,7 @@ func TestBackup_Execute(t *testing.T) { cmd := BackupCommand{} cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--path=/tmp", "--file={{.SITE}}-test.export"}) + _, err := p.ParseArgs([]string{"--site=remark", "--path=/tmp", "--file={{.SITE}}-test.export", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) assert.NoError(t, err) @@ -48,7 +48,7 @@ func TestBackup_ExecuteFailedStatus(t *testing.T) { cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--path=/tmp", "--file={{.SITE}}-test.export"}) + _, err := p.ParseArgs([]string{"--site=remark", "--path=/tmp", "--file={{.SITE}}-test.export", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) assert.EqualError(t, err, `error response "400 Bad Request", some error`) @@ -66,7 +66,8 @@ func TestBackup_ExecuteFailedWrite(t *testing.T) { cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--path=/tmp", "--file=/tmp/no-such-dir/{{.SITE}}-test.export"}) + _, err := p.ParseArgs([]string{"--site=remark", "--path=/tmp", + "--file=/tmp/no-such-dir/{{.SITE}}-test.export", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) assert.EqualError(t, err, `can't create backup file /tmp/no-such-dir/remark-test.export: open /tmp/no-such-dir/remark-test.export: no such file or directory`) diff --git a/backend/app/cmd/cleanup.go b/backend/app/cmd/cleanup.go index f1211bad..cb06dad9 100644 --- a/backend/app/cmd/cleanup.go +++ b/backend/app/cmd/cleanup.go @@ -15,12 +15,13 @@ import ( // CleanupCommand set of flags and command for cleanup type CleanupCommand struct { - Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"` - Dry bool `long:"dry" description:"dry mode, will not remove comments"` - From string `long:"from" description:"from yyyymmdd"` - To string `long:"to" description:"from yyyymmdd"` - BadWords []string `short:"w" long:"bword" description:"bad word(s)"` - BadUsers []string `short:"u" long:"buser" description:"bad user(s)"` + Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"` + Dry bool `long:"dry" description:"dry mode, will not remove comments"` + From string `long:"from" description:"from yyyymmdd"` + To string `long:"to" description:"from yyyymmdd"` + BadWords []string `short:"w" long:"bword" description:"bad word(s)"` + BadUsers []string `short:"u" long:"buser" description:"bad user(s)"` + AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"` CommonOpts } @@ -160,12 +161,13 @@ func (cc *CleanupCommand) listComments(postURL string) ([]store.Comment, error) // deleteComment with DELETE /admin/comment/{id}?site=siteID&url=post-url func (cc *CleanupCommand) deleteComment(c store.Comment) error { - deleteURL := fmt.Sprintf("%s/api/v1/admin/comment/%s?site=%s&url=%s&format=plain&secret=%s", - cc.RemarkURL, c.ID, cc.Site, c.Locator.URL, cc.SharedSecret) + deleteURL := fmt.Sprintf("%s/api/v1/admin/comment/%s?site=%s&url=%s&format=plain", cc.RemarkURL, c.ID, cc.Site, c.Locator.URL) req, err := http.NewRequest("DELETE", deleteURL, nil) if err != nil { return errors.Wrapf(err, "failed to make delete request for comment %s, %s", c.ID, c.Locator.URL) } + req.SetBasicAuth("admin", cc.AdminPasswd) + client := http.Client{} r, err := client.Do(req) if err != nil { diff --git a/backend/app/cmd/cleanup_test.go b/backend/app/cmd/cleanup_test.go index 9cc8154c..1603ab40 100644 --- a/backend/app/cmd/cleanup_test.go +++ b/backend/app/cmd/cleanup_test.go @@ -67,7 +67,7 @@ func TestCleanup_postsInRange(t *testing.T) { cmd := CleanupCommand{} cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--bword=bad1", "--bword=bad2", "--buser=bu_"}) + _, err := p.ParseArgs([]string{"--site=remark", "--bword=bad1", "--bword=bad2", "--buser=bu_", "--admin-passwd=secret"}) require.Nil(t, err) posts, err := cmd.postsInRange("20181218", "20181219") assert.NoError(t, err) @@ -90,7 +90,7 @@ func TestCleanup_listComments(t *testing.T) { cmd := CleanupCommand{} cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--bword=bad1", "--bword=bad2", "--buser=bu_"}) + _, err := p.ParseArgs([]string{"--site=remark", "--bword=bad1", "--bword=bad2", "--buser=bu_", "--admin-passwd=secret"}) require.Nil(t, err) comments, err := cmd.listComments("http://test.com/post1") @@ -117,7 +117,7 @@ func TestCleanup_Execute(t *testing.T) { cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) _, err := p.ParseArgs([]string{"--site=remark", "--bword=bad1", "--bword=bad2", "--buser=bu_", - "--from=20181217", "--to=20181218"}) + "--from=20181217", "--to=20181218", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) assert.NoError(t, err) diff --git a/backend/app/cmd/import.go b/backend/app/cmd/import.go index 2e34c445..3b3d85ee 100644 --- a/backend/app/cmd/import.go +++ b/backend/app/cmd/import.go @@ -17,17 +17,18 @@ import ( // ImportCommand set of flags and command for import type ImportCommand struct { - InputFile string `short:"f" long:"file" description:"input file name" required:"true"` - Provider string `short:"p" long:"provider" default:"disqus" choice:"disqus" choice:"wordpress" description:"import format"` - Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"` - Timeout time.Duration `long:"timeout" default:"15m" description:"import timeout"` + InputFile string `short:"f" long:"file" description:"input file name" required:"true"` + Provider string `short:"p" long:"provider" default:"disqus" choice:"disqus" choice:"wordpress" description:"import format"` + Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"` + Timeout time.Duration `long:"timeout" default:"15m" description:"import timeout"` + AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"` CommonOpts } // Execute runs import with ImportCommand parameters, entry point for "import" command func (ic *ImportCommand) Execute(args []string) error { log.Printf("[INFO] import %s (%s), site %s", ic.InputFile, ic.Provider, ic.Site) - resetEnv("SECRET") + resetEnv("SECRET", "ADMIN_PASSWD") reader, err := ic.reader(ic.InputFile) if err != nil { @@ -37,12 +38,12 @@ func (ic *ImportCommand) Execute(args []string) error { client := http.Client{} ctx, cancel := context.WithTimeout(context.Background(), ic.Timeout) defer cancel() - importURL := fmt.Sprintf("%s/api/v1/admin/import?site=%s&provider=%s&secret=%s", - ic.RemarkURL, ic.Site, ic.Provider, ic.SharedSecret) + importURL := fmt.Sprintf("%s/api/v1/admin/import?site=%s&provider=%s", ic.RemarkURL, ic.Site, ic.Provider) req, err := http.NewRequest(http.MethodPost, importURL, reader) if err != nil { return errors.Wrapf(err, "can't make import request for %s", importURL) } + req.SetBasicAuth("admin", ic.AdminPasswd) resp, err := client.Do(req.WithContext(ctx)) // closes request's reader if err != nil { diff --git a/backend/app/cmd/import_test.go b/backend/app/cmd/import_test.go index 7e5a9ddb..9c50355b 100644 --- a/backend/app/cmd/import_test.go +++ b/backend/app/cmd/import_test.go @@ -33,7 +33,7 @@ func TestImport_Execute(t *testing.T) { cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt"}) + _, err := p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) assert.NoError(t, err) @@ -42,7 +42,7 @@ func TestImport_Execute(t *testing.T) { cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p = flags.NewParser(&cmd, flags.Default) - _, err = p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt.gz"}) + _, err = p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt.gz", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) assert.NoError(t, err) @@ -60,7 +60,7 @@ func TestImport_ExecuteFailed(t *testing.T) { cmd := ImportCommand{} cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--file=testdata/import-no.txt"}) + _, err := p.ParseArgs([]string{"--site=remark", "--file=testdata/import-no.txt", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) t.Log(err) @@ -70,7 +70,7 @@ func TestImport_ExecuteFailed(t *testing.T) { cmd = ImportCommand{} cmd.SetCommon(CommonOpts{RemarkURL: "http://127.0.0.1:12345", SharedSecret: "123456"}) p = flags.NewParser(&cmd, flags.Default) - _, err = p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt"}) + _, err = p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) t.Log(err) @@ -86,7 +86,7 @@ func TestImport_ExecuteFailed(t *testing.T) { cmd = ImportCommand{} cmd.SetCommon(CommonOpts{RemarkURL: ts2.URL, SharedSecret: "123456"}) p = flags.NewParser(&cmd, flags.Default) - _, err = p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt"}) + _, err = p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) t.Log(err) @@ -111,7 +111,7 @@ func TestImport_ExecuteTimeout(t *testing.T) { cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt", "--timeout=300ms"}) + _, err := p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt", "--timeout=300ms", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) assert.NotNil(t, err) diff --git a/backend/app/cmd/restore.go b/backend/app/cmd/restore.go index fe54ea33..708e1975 100644 --- a/backend/app/cmd/restore.go +++ b/backend/app/cmd/restore.go @@ -10,8 +10,9 @@ type RestoreCommand struct { ImportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"` ImportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.YYYYMMDD}}.gz" description:"file name" required:"true"` - Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"` - Timeout time.Duration `long:"timeout" default:"15m" description:"import timeout"` + Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"` + Timeout time.Duration `long:"timeout" default:"15m" description:"import timeout"` + AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"` CommonOpts } @@ -19,7 +20,7 @@ type RestoreCommand struct { // uses ImportCommand with constructed full file name func (rc *RestoreCommand) Execute(args []string) error { log.Printf("[INFO] restore %s, site %s", rc.ImportFile, rc.Site) - resetEnv("SECRET") + resetEnv("SECRET", "ADMIN_PASSWD") fp := fileParser{site: rc.Site, path: rc.ImportPath, file: rc.ImportFile} fname, err := fp.parse(time.Now()) @@ -27,11 +28,12 @@ func (rc *RestoreCommand) Execute(args []string) error { return err } importer := ImportCommand{ - InputFile: fname, - Site: rc.Site, - Provider: "native", - Timeout: rc.Timeout, - CommonOpts: rc.CommonOpts, + InputFile: fname, + Site: rc.Site, + Provider: "native", + Timeout: rc.Timeout, + AdminPasswd: rc.AdminPasswd, + CommonOpts: rc.CommonOpts, } return importer.Execute(args) } diff --git a/backend/app/cmd/restore_test.go b/backend/app/cmd/restore_test.go index 5bd5e8fc..0e425476 100644 --- a/backend/app/cmd/restore_test.go +++ b/backend/app/cmd/restore_test.go @@ -31,7 +31,7 @@ func TestRestore_Execute(t *testing.T) { cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--site=remark", "--path=testdata", "--file=import.txt"}) + _, err := p.ParseArgs([]string{"--site=remark", "--path=testdata", "--file=import.txt", "--admin-passwd=secret"}) require.Nil(t, err) err = cmd.Execute(nil) assert.NoError(t, err) diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 62951d2c..7a751c1c 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -161,7 +161,7 @@ type serverApp struct { // Execute is the entry point for "server" command, called by flag parser func (s *ServerCommand) Execute(args []string) error { log.Printf("[INFO] start server on port %d", s.Port) - resetEnv("SECRET", "AUTH_GOOGLE_CSEC", "AUTH_GITHUB_CSEC", "AUTH_FACEBOOK_CSEC", "AUTH_YANDEX_CSEC") + resetEnv("SECRET", "AUTH_GOOGLE_CSEC", "AUTH_GITHUB_CSEC", "AUTH_FACEBOOK_CSEC", "AUTH_YANDEX_CSEC", "ADMIN_PASSWD") ctx, cancel := context.WithCancel(context.Background()) go func() { // catch signal and invoke graceful termination From 7deee9aaa12ee5996d7722b959189bf75e7735cd Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 30 Dec 2018 23:06:20 -0600 Subject: [PATCH 15/21] revendor with lateas auth lib, sets logger to stdout --- backend/Gopkg.lock | 5 +- backend/app/cmd/server.go | 14 +++-- .../vendor/github.com/go-pkgz/auth/README.md | 17 +++++- .../vendor/github.com/go-pkgz/auth/auth.go | 58 +++++++++++++------ .../github.com/go-pkgz/auth/avatar/avatar.go | 21 +++---- .../github.com/go-pkgz/auth/avatar/localfs.go | 5 +- .../github.com/go-pkgz/auth/logger/logger.go | 20 +++++++ .../go-pkgz/auth/middleware/auth.go | 13 +++-- .../go-pkgz/auth/provider/dev_provider.go | 28 ++++----- .../go-pkgz/auth/provider/service.go | 25 ++++---- 10 files changed, 134 insertions(+), 72 deletions(-) create mode 100644 backend/vendor/github.com/go-pkgz/auth/logger/logger.go diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index 9121d90d..59f9778a 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -113,17 +113,18 @@ [[projects]] branch = "master" - digest = "1:b117a0a0b46dad26254a48c11a511d6c697038e591a1d7ce11a229e1c8e0a237" + digest = "1:9240838c072f500013fa68b709050692dd6ba0095131046639618a296d4f4400" name = "github.com/go-pkgz/auth" packages = [ ".", "avatar", + "logger", "middleware", "provider", "token", ] pruneopts = "UT" - revision = "b95cb645615503dba4d5fced3b77d97d4f0dcc81" + revision = "c322626ae89af60b2ad5a8f96ade761b5ed361bb" [[projects]] digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574" diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 7a751c1c..906a214d 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -17,6 +17,7 @@ import ( "github.com/go-pkgz/auth" "github.com/go-pkgz/auth/avatar" + "github.com/go-pkgz/auth/logger" "github.com/go-pkgz/auth/provider" "github.com/go-pkgz/auth/token" "github.com/go-pkgz/mongo" @@ -269,13 +270,13 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore - var devAuth provider.DevAuthServer + var devAuth *provider.DevAuthServer if s.Auth.Dev { - p, err := authenticator.Provider("dev") + da, err := authenticator.DevAuth() if err != nil { - return nil, errors.Wrap(err, "can't pick dev provider") + return nil, errors.Wrap(err, "can't make dev oauth2 server") } - devAuth = provider.DevAuthServer{Provider: p} + devAuth = da } return &serverApp{ @@ -283,7 +284,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { restSrv: srv, migratorSrv: migr, exporter: exporter, - devAuth: &devAuth, + devAuth: devAuth, dataService: dataService, avatarStore: avatarStore, notifyService: notifyService, @@ -316,7 +317,7 @@ func (a *serverApp) run(ctx context.Context) error { }() a.activateBackup(ctx) // runs in goroutine for each site if a.Auth.Dev { - go a.devAuth.Run() // dev oauth2 server on :8084 + go a.devAuth.Run(context.Background()) // dev oauth2 server on :8084 } a.restSrv.Run(a.Port) close(a.terminated) @@ -547,6 +548,7 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto AvatarStore: avas, AvatarResizeLimit: s.Avatar.RszLmt, AvatarRoutePath: "/api/v1/avatar", + Logger: logger.Std, }) s.addAuthProviders(authenticator) return authenticator diff --git a/backend/vendor/github.com/go-pkgz/auth/README.md b/backend/vendor/github.com/go-pkgz/auth/README.md index ccba903f..f1b6a1d3 100644 --- a/backend/vendor/github.com/go-pkgz/auth/README.md +++ b/backend/vendor/github.com/go-pkgz/auth/README.md @@ -33,8 +33,8 @@ func main() { SecretReader: token.SecretFunc(func(id string) (string, error) { // secret key for JWT return "secret", nil }), - TokenDuration: time.Hour, - CookieDuration: time.Hour * 24, + TokenDuration: time.Minute * 5, // token expires in 5 minutes + CookieDuration: time.Hour * 24, // cookie expires in 1 day and will enforce re-login Issuer: "my-test-app", URL: "http://127.0.0.1:8080", AvatarStore: avatar.NewLocalFS("/tmp"), @@ -170,7 +170,18 @@ It will run fake aouth2 "server" on port :8084 and user could login with any use _Warning: this is not the real oauth2 server but just a small fake thing for development and testing only. Don't use `dev` provider with any production code._ - +### Other ways to authenticate + +In addition to the primary method (i.e. JWT cookie with XSRF header) there are two more ways to authenticate: + +1. Send JWT header as `X-JWT`. This shouldn't be used for web application, however can be helpful for service-to-service authentication. +2. [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). This mode by default disabled and will be enabled it `Opts.AdminPasswd` defined. This will allow access with basic auth admin: with user [admin](https://github.com/go-pkgz/auth/blob/master/middleware/auth.go#L24). Such method can be used for automation scripts. + +### Logging + +By default this library doesn't print anything to stdout/stderr, however user can pass a logger implementing `logger.L` interface with a single method `Logf(format string, args ...interface{})`. Functional adapter for this interface included as `logger.Func`. There are two predefined implementations in the `logger` package - `NoOp` (prints nothing, default) and `Std` wrapping `log.Printf` from stdlib. + + ## Register oauth2 providers Authentication handled by external providers. You should setup oauth2 for all (or some) of them to allow users to authenticate. It is not mandatory to have all of them, but at least one should be correctly configured. diff --git a/backend/vendor/github.com/go-pkgz/auth/auth.go b/backend/vendor/github.com/go-pkgz/auth/auth.go index d1ef0305..3b2161ec 100644 --- a/backend/vendor/github.com/go-pkgz/auth/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/auth.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/go-pkgz/auth/avatar" + "github.com/go-pkgz/auth/logger" "github.com/go-pkgz/auth/middleware" "github.com/go-pkgz/auth/provider" "github.com/go-pkgz/auth/token" @@ -17,6 +18,7 @@ import ( // Service provides higher level wrapper allowing to construct everything and get back token middleware type Service struct { + logger logger.L opts Opts jwtService *token.Service providers []provider.Service @@ -49,11 +51,30 @@ type Opts struct { AvatarResizeLimit int // resize avatar's limit in pixels AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar", default `/avatar` - AdminPasswd string // if presented, allows basic auth with user admin and given password + AdminPasswd string // if presented, allows basic auth with user admin and given password + Logger logger.L // logger interface, default is no logging at all } // NewService initializes everything -func NewService(opts Opts) *Service { +func NewService(opts Opts) (res *Service) { + + res = &Service{ + opts: opts, + logger: opts.Logger, + authMiddleware: middleware.Authenticator{ + Validator: opts.Validator, + AdminPasswd: opts.AdminPasswd, + }, + issuer: opts.Issuer, + } + + if opts.Issuer == "" { + res.issuer = "go-pkgz/auth" + } + + if opts.Logger == nil { + res.logger = logger.Func(func(fmt string, args ...interface{}) {}) // do-nothing logger + } jwtService := token.NewService(token.Opts{ SecretReader: opts.SecretReader, @@ -66,28 +87,19 @@ func NewService(opts Opts) *Service { JWTHeaderKey: opts.JWTHeaderKey, XSRFCookieName: opts.XSRFCookieName, XSRFHeaderKey: opts.XSRFHeaderKey, - Issuer: opts.Issuer, + Issuer: res.issuer, }) if opts.SecretReader == nil { jwtService.SecretReader = token.SecretFunc(func(id string) (string, error) { return "", errors.New("secrets reader not available") }) + res.logger.Logf("[WARN] no secret reader defined") } - res := Service{ - opts: opts, - jwtService: jwtService, - authMiddleware: middleware.Authenticator{ - JWTService: jwtService, - Validator: opts.Validator, - AdminPasswd: opts.AdminPasswd, - }, - } - - if opts.Issuer == "" { - res.issuer = "go-pkgz/auth" - } + res.jwtService = jwtService + res.authMiddleware.JWTService = jwtService + res.authMiddleware.L = res.logger if opts.AvatarStore != nil { res.avatarProxy = &avatar.Proxy{ @@ -95,13 +107,14 @@ func NewService(opts Opts) *Service { URL: opts.URL, RoutePath: opts.AvatarRoutePath, ResizeLimit: opts.AvatarResizeLimit, + L: res.logger, } if res.avatarProxy.RoutePath == "" { res.avatarProxy.RoutePath = "/avatar" } } - return &res + return res } // Handlers gets http.Handler for all providers and avatars @@ -171,6 +184,7 @@ func (s *Service) AddProvider(name string, cid string, csecret string) { AvatarSaver: s.avatarProxy, Cid: cid, Csecret: csecret, + L: s.logger, } switch strings.ToLower(name) { @@ -191,6 +205,16 @@ func (s *Service) AddProvider(name string, cid string, csecret string) { s.authMiddleware.Providers = s.providers } +// DevAuth makes dev oauth2 server, for testing and development only! +func (s *Service) DevAuth() (*provider.DevAuthServer, error) { + p, err := s.Provider("dev") // peak dev provider + if err != nil { + return nil, errors.Wrap(err, "dev provider not registered") + } + // make and start dev auth server + return &provider.DevAuthServer{Provider: p, L: s.logger}, nil +} + // Provider gets provider by name func (s *Service) Provider(name string) (provider.Service, error) { for _, p := range s.providers { diff --git a/backend/vendor/github.com/go-pkgz/auth/avatar/avatar.go b/backend/vendor/github.com/go-pkgz/auth/avatar/avatar.go index 0140f952..645e744f 100644 --- a/backend/vendor/github.com/go-pkgz/auth/avatar/avatar.go +++ b/backend/vendor/github.com/go-pkgz/auth/avatar/avatar.go @@ -7,7 +7,6 @@ import ( "image" "image/png" "io" - "log" "net/http" "strconv" "strings" @@ -17,12 +16,14 @@ import ( "github.com/pkg/errors" "golang.org/x/image/draw" + "github.com/go-pkgz/auth/logger" "github.com/go-pkgz/auth/token" ) // Proxy provides http handler for avatars from avatar.Store // On user login token will call Put and it will retrieve and save picture locally. type Proxy struct { + logger.L Store Store RoutePath string URL string @@ -51,7 +52,7 @@ func (p *Proxy) Put(u token.User) (avatarURL string, err error) { defer func() { if e := resp.Body.Close(); e != nil { - log.Printf("[WARN] can't close response body, %s", e) + p.Logf("[WARN] can't close response body, %s", e) } }() @@ -64,7 +65,7 @@ func (p *Proxy) Put(u token.User) (avatarURL string, err error) { return "", err } - log.Printf("[DEBUG] saved avatar from %s to %s, user %q", u.Picture, avatarID, u.Name) + p.Logf("[DEBUG] saved avatar from %s to %s, user %q", u.Picture, avatarID, u.Name) return p.URL + p.RoutePath + "/" + avatarID, nil } @@ -97,7 +98,7 @@ func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) { defer func() { if e := avReader.Close(); e != nil { - log.Printf("[WARN] can't close avatar reader for %s, %s", avatarID, e) + p.Logf("[WARN] can't close avatar reader for %s, %s", avatarID, e) } }() @@ -105,7 +106,7 @@ func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", strconv.Itoa(size)) w.WriteHeader(http.StatusOK) if _, err = io.Copy(w, avReader); err != nil { - log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err) + p.Logf("[WARN] can't send response to %s, %s", r.RemoteAddr, err) } } @@ -114,11 +115,11 @@ func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) { // Returns original reader if resizing is not needed or failed. func (p *Proxy) resize(reader io.Reader, limit int) io.Reader { if reader == nil { - log.Print("[WARN] avatar resize(): reader is nil") + p.Logf("[WARN] avatar resize(): reader is nil") return nil } if limit <= 0 { - log.Print("[DEBUG] avatar resize(): limit should be greater than 0") + p.Logf("[DEBUG] avatar resize(): limit should be greater than 0") return reader } @@ -126,14 +127,14 @@ func (p *Proxy) resize(reader io.Reader, limit int) io.Reader { tee := io.TeeReader(reader, &teeBuf) src, _, err := image.Decode(tee) if err != nil { - log.Printf("[WARN] avatar resize(): can't decode avatar image, %s", err) + p.Logf("[WARN] avatar resize(): can't decode avatar image, %s", err) return &teeBuf } bounds := src.Bounds() w, h := bounds.Dx(), bounds.Dy() if w <= limit && h <= limit || w <= 0 || h <= 0 { - log.Print("[DEBUG] resizing image is smaller that the limit or has 0 size") + p.Logf("[DEBUG] resizing image is smaller that the limit or has 0 size") return &teeBuf } newW, newH := w*limit/h, limit @@ -146,7 +147,7 @@ func (p *Proxy) resize(reader io.Reader, limit int) io.Reader { var out bytes.Buffer if err = png.Encode(&out, m); err != nil { - log.Printf("[WARN] avatar resize(): can't encode resized avatar to PNG, %s", err) + p.Logf("[WARN] avatar resize(): can't encode resized avatar to PNG, %s", err) return &teeBuf } return &out diff --git a/backend/vendor/github.com/go-pkgz/auth/avatar/localfs.go b/backend/vendor/github.com/go-pkgz/auth/avatar/localfs.go index 8317c4ee..1692a6bb 100644 --- a/backend/vendor/github.com/go-pkgz/auth/avatar/localfs.go +++ b/backend/vendor/github.com/go-pkgz/auth/avatar/localfs.go @@ -4,7 +4,6 @@ import ( "fmt" "hash/crc64" "io" - "log" "os" "path" "path/filepath" @@ -47,14 +46,13 @@ func (fs *LocalFS) Put(userID string, reader io.Reader) (avatar string, err erro } defer func() { if e := fh.Close(); e != nil { - log.Printf("[WARN] can't close avatar file %s, %s", avFile, e) + err = errors.Wrapf(err, "can't close avatar file %s", avFile) } }() if _, err = io.Copy(fh, reader); err != nil { return "", errors.Wrapf(err, "can't save file %s", avFile) } - log.Printf("[DEBUG] put avatar for %s to %s completed", userID, fh.Name()) return id + imgSfx, nil } @@ -78,7 +76,6 @@ func (fs *LocalFS) ID(avatar string) (id string) { avFile := path.Join(location, avatar) fi, err := os.Stat(avFile) if err != nil { - log.Printf("[DEBUG] can't get file info '%s', %s", avFile, err) return encodeID(avatar) } return encodeID(avatar + strconv.FormatInt(fi.ModTime().Unix(), 10)) diff --git a/backend/vendor/github.com/go-pkgz/auth/logger/logger.go b/backend/vendor/github.com/go-pkgz/auth/logger/logger.go new file mode 100644 index 00000000..5c62407d --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/logger/logger.go @@ -0,0 +1,20 @@ +package logger + +import "log" + +// L defines minimal interface used to log things +type L interface { + Logf(format string, args ...interface{}) +} + +// Func type is an adapter to allow the use of ordinary functions as Logger. +type Func func(format string, args ...interface{}) + +// Logf calls f(id) +func (f Func) Logf(format string, args ...interface{}) { f(format, args...) } + +// NoOp logger +var NoOp = Func(func(format string, args ...interface{}) {}) + +// Std logger +var Std = Func(func(format string, args ...interface{}) { log.Printf(format, args...) }) diff --git a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go index 164a35c0..404fad40 100644 --- a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go @@ -3,18 +3,19 @@ package middleware import ( "encoding/base64" - "log" "net/http" "strings" "github.com/pkg/errors" + "github.com/go-pkgz/auth/logger" "github.com/go-pkgz/auth/provider" "github.com/go-pkgz/auth/token" ) // Authenticator is top level auth object providing middlewares type Authenticator struct { + logger.L JWTService *token.Service Providers []provider.Service Validator token.Validator @@ -50,7 +51,7 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { h.ServeHTTP(w, r) return } - log.Printf("[DEBUG] auth failed, %s", err) + a.Logf("[DEBUG] auth failed, %s", err) http.Error(w, "Unauthorized", http.StatusUnauthorized) } @@ -94,7 +95,7 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { onError(h, w, r, errors.Wrap(err, "can't refresh token")) return } - log.Printf("[DEBUG] token refreshed for %+v", claims.User) + a.Logf("[DEBUG] token refreshed for %+v", claims.User) } r = token.SetUserInfo(r, *claims.User) // populate user info to request context @@ -150,18 +151,18 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool { b, err := base64.StdEncoding.DecodeString(s[1]) if err != nil { - log.Printf("[WARN] admin user auth failed, can't to decode %s, %s", s[1], err) + a.Logf("[WARN] admin user auth failed, can't to decode %s, %s", s[1], err) return false } pair := strings.SplitN(string(b), ":", 2) if len(pair) != 2 { - log.Printf("[WARN] admin user auth failed, can't split basic auth %s", string(b)) + a.Logf("[WARN] admin user auth failed, can't split basic auth %s", string(b)) return false } if pair[0] != "admin" || pair[1] != a.AdminPasswd { - log.Printf("[WARN] dev user auth failed, user/passwd mismatch %+v", pair) + a.Logf("[WARN] admin basic auth failed, user/passwd mismatch %+v", pair) return false } diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go index 9dc6226b..98549af1 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "log" "net/http" "strings" "sync" @@ -15,6 +14,7 @@ import ( "github.com/pkg/errors" "golang.org/x/oauth2" + "github.com/go-pkgz/auth/logger" "github.com/go-pkgz/auth/token" ) @@ -26,10 +26,10 @@ const devAuthPort = 8084 // can run in interactive and non-interactive mode. In interactive mode login attempts will show login form to select // desired user name, this is the mode used for development. Non-interactive mode for tests only. type DevAuthServer struct { + logger.L Provider Service Automatic bool - - username string // unsafe, but fine for dev + username string // unsafe, but fine for dev iconGen *identicon.Generator httpServer *http.Server @@ -37,26 +37,26 @@ type DevAuthServer struct { } // Run oauth2 dev server on port devAuthPort -func (d *DevAuthServer) Run() { +func (d *DevAuthServer) Run(ctx context.Context) { d.username = "dev_user" - log.Printf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.RedirectURL) + d.Logf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.RedirectURL) d.lock.Lock() var err error d.iconGen, err = identicon.New("github", 5, 3) if err != nil { - log.Printf("[WARN] can't create identicon, %s", err) + d.Logf("[WARN] can't create identicon, %s", err) } userFormTmpl, err := template.New("page").Parse(devUserFormTmpl) if err != nil { - log.Printf("[WARN] can't parse user form template, %s", err) + d.Logf("[WARN] can't parse user form template, %s", err) return } d.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", devAuthPort), Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("[DEBUG] dev oauth request %s %s %+v", r.Method, r.URL, r.Header) + d.Logf("[DEBUG] dev oauth request %s %s %+v", r.Method, r.URL, r.Header) switch { case strings.HasPrefix(r.URL.Path, "/login/oauth/authorize"): @@ -67,7 +67,7 @@ func (d *DevAuthServer) Run() { formData := struct{ Query string }{Query: r.URL.RawQuery} if err = userFormTmpl.Execute(w, formData); err != nil { - log.Printf("[WARN] can't write, %s", err) + d.Logf("[WARN] can't write, %s", err) } return } @@ -78,7 +78,7 @@ func (d *DevAuthServer) Run() { state := r.URL.Query().Get("state") callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.RedirectURL, state) - log.Printf("[DEBUG] callback url=%s", callbackURL) + d.Logf("[DEBUG] callback url=%s", callbackURL) w.Header().Add("Location", callbackURL) w.WriteHeader(http.StatusFound) @@ -131,21 +131,21 @@ func (d *DevAuthServer) Run() { d.lock.Unlock() err = d.httpServer.ListenAndServe() - log.Printf("[WARN] dev oauth2 server terminated, %s", err) + d.Logf("[WARN] dev oauth2 server terminated, %s", err) } // Shutdown oauth2 dev server func (d *DevAuthServer) Shutdown() { - log.Print("[WARN] shutdown oauth2 dev server") + d.Logf("[WARN] shutdown oauth2 dev server") ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() d.lock.Lock() if d.httpServer != nil { if err := d.httpServer.Shutdown(ctx); err != nil { - log.Printf("[DEBUG] oauth2 dev shutdown error, %s", err) + d.Logf("[DEBUG] oauth2 dev shutdown error, %s", err) } } - log.Print("[DEBUG] shutdown dev oauth2 server completed") + d.Logf("[DEBUG] shutdown dev oauth2 server completed") d.lock.Unlock() } diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/service.go b/backend/vendor/github.com/go-pkgz/auth/provider/service.go index df98d6a6..756a2ebb 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/service.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/service.go @@ -7,11 +7,12 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "net/http" "strings" "time" + "github.com/go-pkgz/auth/logger" + jwt "github.com/dgrijalva/jwt-go" "github.com/go-pkgz/rest" "github.com/pkg/errors" @@ -34,6 +35,7 @@ type Service struct { // Params to make initialized and ready to use provider type Params struct { + logger.L URL string JwtService *token.Service AvatarSaver AvatarSaver @@ -59,7 +61,10 @@ func (u userData) value(key string) string { // initService makes oauth2 service for given provider func initService(p Params, service Service) Service { - log.Printf("[INFO] init oauth2 service %s", service.Name) + if p.L == nil { + p.L = logger.Func(func(fmt string, args ...interface{}) {}) + } + p.Logf("[INFO] init oauth2 service %s", service.Name) service.Params = p service.conf = oauth2.Config{ ClientID: service.Cid, @@ -69,7 +74,7 @@ func initService(p Params, service Service) Service { Endpoint: service.Endpoint, } - log.Printf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s", + p.Logf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s", service.Name, service.Cid, service.Endpoint, service.RedirectURL) return service } @@ -99,7 +104,7 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) { // loginHandler - GET /login?from=redirect-back-url&site=siteID&session=1 func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("[DEBUG] login with %s", p.Name) + p.Logf("[DEBUG] login with %s", p.Name) // make state (random) and store in session state, err := p.randToken() if err != nil { @@ -134,7 +139,7 @@ func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) { // return login url loginURL := p.conf.AuthCodeURL(state) - log.Printf("[DEBUG] login url %s, claims=%+v", loginURL, claims) + p.Logf("[DEBUG] login url %s, claims=%+v", loginURL, claims) http.Redirect(w, r, loginURL, http.StatusFound) } @@ -159,7 +164,7 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { return } - log.Printf("[DEBUG] token with state %s", retrievedState) + p.Logf("[DEBUG] token 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") @@ -175,7 +180,7 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { defer func() { if e := uinfo.Body.Close(); e != nil { - log.Printf("[WARN] failed to close response body, %s", e) + p.Logf("[WARN] failed to close response body, %s", e) } }() @@ -190,7 +195,7 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to unmarshal user info") return } - log.Printf("[DEBUG] got raw user info %+v", jData) + p.Logf("[DEBUG] got raw user info %+v", jData) u := p.MapUser(jData, data) u = p.setAvatar(u) @@ -215,7 +220,7 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { return } - log.Printf("[DEBUG] user info %+v", u) + p.Logf("[DEBUG] user info %+v", u) // redirect to back url if presented in login query params if oauthClaims.Handshake != nil && oauthClaims.Handshake.From != "" { @@ -231,7 +236,7 @@ func (p Service) setAvatar(u token.User) token.User { if avatarURL, e := p.AvatarSaver.Put(u); e == nil { u.Picture = avatarURL } else { - log.Printf("[WARN] failed to set avatar for %+v, %+v", u, e) + p.Logf("[WARN] failed to set avatar for %+v, %+v", u, e) } } return u From fdef63c61d0634dd350835729061a9e077b74281 Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 30 Dec 2018 23:29:13 -0600 Subject: [PATCH 16/21] make import expired test to run longer --- backend/app/rest/api/migrator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/rest/api/migrator_test.go b/backend/app/rest/api/migrator_test.go index 8ca343e3..44642f30 100644 --- a/backend/app/rest/api/migrator_test.go +++ b/backend/app/rest/api/migrator_test.go @@ -187,7 +187,7 @@ func TestMigrator_ImportWaitExpired(t *testing.T) { tmpl := `{"id":"%d","pid":"","text":"

test test #1

","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah1"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.849053725-05:00"}` recs := []string{} - for i := 0; i < 1000; i++ { + for i := 0; i < 5000; i++ { recs = append(recs, fmt.Sprintf(tmpl, i)) } r := strings.NewReader(`{"version":1}` + strings.Join(recs, "\n")) // reader with 10k records From 189df5b0a4a60ad4d867ded0c6ce4ded7dd955ed Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 31 Dec 2018 01:05:29 -0600 Subject: [PATCH 17/21] revendor with latest auth lib --- backend/Gopkg.lock | 5 +-- .../go-pkgz/auth/middleware/auth.go | 35 ++++++++----------- .../go-pkgz/auth/provider/service.go | 10 +++++- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index 59f9778a..f843d289 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -113,7 +113,7 @@ [[projects]] branch = "master" - digest = "1:9240838c072f500013fa68b709050692dd6ba0095131046639618a296d4f4400" + digest = "1:495f89256b4fef47e64160d109454685afa4949f400d02833207e2af383d1173" name = "github.com/go-pkgz/auth" packages = [ ".", @@ -124,7 +124,7 @@ "token", ] pruneopts = "UT" - revision = "c322626ae89af60b2ad5a8f96ade761b5ed361bb" + revision = "46d3a954882f41fc0efdf551e2c1b070799e0f54" [[projects]] digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574" @@ -403,6 +403,7 @@ "github.com/go-chi/render", "github.com/go-pkgz/auth", "github.com/go-pkgz/auth/avatar", + "github.com/go-pkgz/auth/logger", "github.com/go-pkgz/auth/provider", "github.com/go-pkgz/auth/token", "github.com/go-pkgz/mongo", diff --git a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go index 404fad40..dc7067a5 100644 --- a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go @@ -2,9 +2,7 @@ package middleware import ( - "encoding/base64" "net/http" - "strings" "github.com/pkg/errors" @@ -16,12 +14,21 @@ import ( // Authenticator is top level auth object providing middlewares type Authenticator struct { logger.L - JWTService *token.Service + JWTService TokenService Providers []provider.Service Validator token.Validator AdminPasswd string } +// TokenService defines interface accessing tokens +type TokenService interface { + Parse(tokenString string) (claims token.Claims, err error) + Set(w http.ResponseWriter, claims token.Claims) error + Get(r *http.Request) (claims token.Claims, token string, err error) + IsExpired(claims token.Claims) bool + Reset(w http.ResponseWriter) +} + var adminUser = token.User{ ID: "admin", Name: "admin", @@ -51,7 +58,7 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { h.ServeHTTP(w, r) return } - a.Logf("[DEBUG] auth failed, %s", err) + a.Logf("[DEBUG] auth failed, %v", err) http.Error(w, "Unauthorized", http.StatusUnauthorized) } @@ -144,25 +151,13 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool { return false } - s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) - if len(s) != 2 { + user, passwd, ok := r.BasicAuth() + if !ok { return false } - b, err := base64.StdEncoding.DecodeString(s[1]) - if err != nil { - a.Logf("[WARN] admin user auth failed, can't to decode %s, %s", s[1], err) - return false - } - - pair := strings.SplitN(string(b), ":", 2) - if len(pair) != 2 { - a.Logf("[WARN] admin user auth failed, can't split basic auth %s", string(b)) - return false - } - - if pair[0] != "admin" || pair[1] != a.AdminPasswd { - a.Logf("[WARN] admin basic auth failed, user/passwd mismatch %+v", pair) + if user != "admin" || passwd != a.AdminPasswd { + a.Logf("[WARN] admin basic auth failed, user/passwd mismatch, %s:%s", user, passwd) return false } diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/service.go b/backend/vendor/github.com/go-pkgz/auth/provider/service.go index 756a2ebb..e6d55d5e 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/service.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/service.go @@ -37,7 +37,7 @@ type Service struct { type Params struct { logger.L URL string - JwtService *token.Service + JwtService TokenService AvatarSaver AvatarSaver Cid string Csecret string @@ -49,6 +49,14 @@ type AvatarSaver interface { Put(u token.User) (avatarURL string, err error) } +// TokenService defines interface accessing tokens +type TokenService interface { + Parse(tokenString string) (claims token.Claims, err error) + Set(w http.ResponseWriter, claims token.Claims) error + Get(r *http.Request) (claims token.Claims, token string, err error) + Reset(w http.ResponseWriter) +} + type userData map[string]interface{} func (u userData) value(key string) string { From ca1943f9099188aadadf7044deba7fb60b03b248 Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 31 Dec 2018 17:47:25 -0600 Subject: [PATCH 18/21] revendor with auth:0.1.1 --- backend/Gopkg.lock | 6 +- backend/app/cmd/server_test.go | 2 +- backend/app/rest/api/rest_public.go | 2 +- .../vendor/github.com/go-pkgz/auth/README.md | 28 +-- .../vendor/github.com/go-pkgz/auth/auth.go | 16 +- .../go-pkgz/auth/provider/dev_provider.go | 22 +- .../go-pkgz/auth/provider/oauth2.go | 213 +++++++++++++++++ .../go-pkgz/auth/provider/providers.go | 64 ++--- .../go-pkgz/auth/provider/service.go | 221 ++---------------- 9 files changed, 305 insertions(+), 269 deletions(-) create mode 100644 backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index f843d289..a4e0cb32 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -112,8 +112,7 @@ version = "v1.0.0" [[projects]] - branch = "master" - digest = "1:495f89256b4fef47e64160d109454685afa4949f400d02833207e2af383d1173" + digest = "1:2e6b942dd80c33bba11b9567f3f8a4338e80d597959e4a5d182d5251953bebe6" name = "github.com/go-pkgz/auth" packages = [ ".", @@ -124,7 +123,8 @@ "token", ] pruneopts = "UT" - revision = "46d3a954882f41fc0efdf551e2c1b070799e0f54" + revision = "a4dab49e2656a32ab7eb1255980c24f8560dc298" + version = "v0.1.1" [[projects]] digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574" diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 61ed2f0a..5f91ae12 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -70,7 +70,7 @@ func TestServerApp_DevMode(t *testing.T) { time.Sleep(100 * time.Millisecond) // let server start assert.Equal(t, 4+1, len(app.restSrv.Authenticator.Providers()), "extra auth provider") - assert.Equal(t, "dev", app.restSrv.Authenticator.Providers()[4].Name, "dev auth provider") + assert.Equal(t, "dev", app.restSrv.Authenticator.Providers()[4].Name(), "dev auth provider") // send ping resp, err := http.Get("http://localhost:18085/api/v1/ping") require.Nil(t, err) diff --git a/backend/app/rest/api/rest_public.go b/backend/app/rest/api/rest_public.go index 49134e3c..cfba32f4 100644 --- a/backend/app/rest/api/rest_public.go +++ b/backend/app/rest/api/rest_public.go @@ -238,7 +238,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { cnf.Auth = []string{} for _, ap := range s.Authenticator.Providers() { - cnf.Auth = append(cnf.Auth, ap.Name) + cnf.Auth = append(cnf.Auth, ap.Name()) } if cnf.Admins == nil { // prevent json serialization to nil diff --git a/backend/vendor/github.com/go-pkgz/auth/README.md b/backend/vendor/github.com/go-pkgz/auth/README.md index f1b6a1d3..f6f4b8b5 100644 --- a/backend/vendor/github.com/go-pkgz/auth/README.md +++ b/backend/vendor/github.com/go-pkgz/auth/README.md @@ -13,9 +13,10 @@ This library provides "social login" with Github, Google, Facebook and Yandex. - Black list with user-defined validator - Multiple aud (audience) supported - Secure key with customizable `SecretReader` -- Ability to store extra information to token and retrieve on login +- Ability to store an extra information to token and retrieve on login - Pre-auth and post-auth hooks to handle custom use cases. - Middleware for easy integration into http routers +- Wrappers to extract user info from the request ## Install @@ -107,35 +108,35 @@ It also has placeholders for fields application can populate with custom `token. - `IP` - hash of user's IP address - `Email` - user's email -- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provides, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details. +- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provided, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details. ### Avatar proxy -Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides and automatic proxy +Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides an automatic proxy - On each login the proxy will retrieve user's picture and save it to `AvatarStore` - Local (proxied) link to avatar included in user's info (jwt token) - API for avatar removal provided as a part of `AvatarStore` -- User can leverage one of provided stores: +- User can leverage one of the provided stores: - `avatar.LocalFS` - file system, each avatar in a separate file - - `avatar.BoltDB` - a single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store). + - `avatar.BoltDB` - single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store). - `avatar.GridFS` - external [GridFS](https://docs.mongodb.com/manual/core/gridfs/) (mongo db). -- In case of need a custom implementation of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25). +- In case of need custom implementations of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25). - All avatar-related setup done as a part of `auth.Opts` and needs: - `AvatarStore` - avatar store to use, i.e. `avatar.NewLocalFS("/tmp/avatars")` - - `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links links this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info") - - `AvatarResizeLimit` - size (in pixel) used to resize avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing. + - `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links like this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info") + - `AvatarResizeLimit` - size (in pixels) used to resize the avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing. ### Customization There are several ways to adjust functionality of the library: -1. `SecretReader` - interface with a single method `Get(aud string) string` to return secret used for JWT signing and verification +1. `SecretReader` - interface with a single method `Get(aud string) string` to return the secret used for JWT signing and verification 1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on. 2. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users. -All of interfaces have corresponding Func wrappers (adapters) - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`. +All of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`. ### Implementing black list logic or some other filters @@ -157,11 +158,10 @@ Working with oauth2 providers can be a pain, especially during development phase ```go // runs dev oauth2 server on :8084 go func() { - p, err := service.Provider("dev") + devAuthServer, err := service.DevAuth() if err != nil { log.Fatal(err) } - devAuthServer := provider.DevAuthServer{Provider: p} devAuthServer.Run() }() ``` @@ -175,7 +175,7 @@ _Warning: this is not the real oauth2 server but just a small fake thing for dev In addition to the primary method (i.e. JWT cookie with XSRF header) there are two more ways to authenticate: 1. Send JWT header as `X-JWT`. This shouldn't be used for web application, however can be helpful for service-to-service authentication. -2. [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). This mode by default disabled and will be enabled it `Opts.AdminPasswd` defined. This will allow access with basic auth admin: with user [admin](https://github.com/go-pkgz/auth/blob/master/middleware/auth.go#L24). Such method can be used for automation scripts. +2. [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). This mode disabled by default and will be enabled if `Opts.AdminPasswd` defined. This will allow access with basic auth admin: with user [admin](https://github.com/go-pkgz/auth/blob/master/middleware/auth.go#L24). Such method can be used for automation scripts. ### Logging @@ -239,4 +239,4 @@ For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/co The library extracted from [remark42](https://github.com/umputun/remark) project. The original code in production use on multiple sites and seems to work fine. -`go-pkgz/auth` library still in beta and until version 1 released some breaking changes still possible. \ No newline at end of file +`go-pkgz/auth` library still in development and until version 1 released some breaking changes possible. \ No newline at end of file diff --git a/backend/vendor/github.com/go-pkgz/auth/auth.go b/backend/vendor/github.com/go-pkgz/auth/auth.go index 3b2161ec..a5b04f8a 100644 --- a/backend/vendor/github.com/go-pkgz/auth/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/auth.go @@ -131,7 +131,7 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl if elems[len(elems)-1] == "list" { list := []string{} for _, p := range s.providers { - list = append(list, p.Name) + list = append(list, p.Name()) } rest.RenderJSON(w, r, list) return @@ -189,15 +189,15 @@ func (s *Service) AddProvider(name string, cid string, csecret string) { switch strings.ToLower(name) { case "github": - s.providers = append(s.providers, provider.NewGithub(p)) + s.providers = append(s.providers, provider.NewService(provider.NewGithub(p))) case "google": - s.providers = append(s.providers, provider.NewGoogle(p)) + s.providers = append(s.providers, provider.NewService(provider.NewGoogle(p))) case "facebook": - s.providers = append(s.providers, provider.NewFacebook(p)) + s.providers = append(s.providers, provider.NewService(provider.NewFacebook(p))) case "yandex": - s.providers = append(s.providers, provider.NewFacebook(p)) + s.providers = append(s.providers, provider.NewService(provider.NewFacebook(p))) case "dev": - s.providers = append(s.providers, provider.NewDev(p)) + s.providers = append(s.providers, provider.NewService(provider.NewDev(p))) default: return } @@ -212,13 +212,13 @@ func (s *Service) DevAuth() (*provider.DevAuthServer, error) { return nil, errors.Wrap(err, "dev provider not registered") } // make and start dev auth server - return &provider.DevAuthServer{Provider: p, L: s.logger}, nil + return &provider.DevAuthServer{Provider: p.Provider.(provider.Oauth2Handler), L: s.logger}, nil } // Provider gets provider by name func (s *Service) Provider(name string) (provider.Service, error) { for _, p := range s.providers { - if p.Name == name { + if p.Name() == name { return p, nil } } diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go index 98549af1..8d93fc96 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go @@ -27,7 +27,7 @@ const devAuthPort = 8084 // desired user name, this is the mode used for development. Non-interactive mode for tests only. type DevAuthServer struct { logger.L - Provider Service + Provider Oauth2Handler Automatic bool username string // unsafe, but fine for dev @@ -39,7 +39,7 @@ type DevAuthServer struct { // Run oauth2 dev server on port devAuthPort func (d *DevAuthServer) Run(ctx context.Context) { d.username = "dev_user" - d.Logf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.RedirectURL) + d.Logf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.redirectURL) d.lock.Lock() var err error d.iconGen, err = identicon.New("github", 5, 3) @@ -77,7 +77,7 @@ func (d *DevAuthServer) Run(ctx context.Context) { } state := r.URL.Query().Get("state") - callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.RedirectURL, state) + callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.redirectURL, state) d.Logf("[DEBUG] callback url=%s", callbackURL) w.Header().Add("Location", callbackURL) w.WriteHeader(http.StatusFound) @@ -150,17 +150,17 @@ func (d *DevAuthServer) Shutdown() { } // NewDev makes dev oauth2 provider for admin user -func NewDev(p Params) Service { - return initService(p, Service{ - Name: "dev", - Endpoint: oauth2.Endpoint{ +func NewDev(p Params) Oauth2Handler { + return initOauth2Handler(p, Oauth2Handler{ + name: "dev", + endpoint: oauth2.Endpoint{ AuthURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/authorize", devAuthPort), TokenURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/access_token", devAuthPort), }, - RedirectURL: p.URL + "/auth/dev/callback", - Scopes: []string{"user:email"}, - InfoURL: fmt.Sprintf("http://127.0.0.1:%d/user", devAuthPort), - MapUser: func(data userData, _ []byte) token.User { + redirectURL: p.URL + "/auth/dev/callback", + scopes: []string{"user:email"}, + infoURL: fmt.Sprintf("http://127.0.0.1:%d/user", devAuthPort), + mapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ ID: data.value("id"), Name: data.value("name"), diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go b/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go new file mode 100644 index 00000000..17ec0134 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go @@ -0,0 +1,213 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/go-pkgz/rest" + "golang.org/x/oauth2" + + "github.com/go-pkgz/auth/logger" + "github.com/go-pkgz/auth/token" +) + +// Oauth2Handler implements /login, /callback and /logout handlers from aouth2 flow +type Oauth2Handler struct { + Params + + // all of these fields specific to particular oauth2 provider + name string + redirectURL string + infoURL string + endpoint oauth2.Endpoint + scopes []string + mapUser func(userData, []byte) token.User // map info from InfoURL to User + conf oauth2.Config +} + +// Params to make initialized and ready to use provider +type Params struct { + logger.L + URL string + JwtService TokenService + Cid string + Csecret string + Issuer string + AvatarSaver AvatarSaver +} + +type userData map[string]interface{} + +func (u userData) value(key string) string { + // json.Unmarshal converts json "null" value to go's "nil", in this case return empty string + if val, ok := u[key]; ok && val != nil { + return fmt.Sprintf("%v", val) + } + return "" +} + +// initOauth2Handler makes oauth2 handler for given provider +func initOauth2Handler(p Params, service Oauth2Handler) Oauth2Handler { + if p.L == nil { + p.L = logger.Func(func(fmt string, args ...interface{}) {}) + } + p.Logf("[INFO] init oauth2 service %s", service.name) + service.Params = p + service.conf = oauth2.Config{ + ClientID: service.Cid, + ClientSecret: service.Csecret, + RedirectURL: service.redirectURL, + Scopes: service.scopes, + Endpoint: service.endpoint, + } + + p.Logf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s", + service.name, service.Cid, service.endpoint, service.redirectURL) + return service +} + +// Name returns provider name +func (p Oauth2Handler) Name() string { return p.name } + +// LoginHandler - GET /login?from=redirect-back-url&site=siteID&session=1 +func (p Oauth2Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { + + p.Logf("[DEBUG] login with %s", p.Name) + // make state (random) and store in session + state, err := randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make oauth2 state") + return + } + + cid, err := randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") + return + } + + claims := token.Claims{ + Handshake: &token.Handshake{ + State: state, + From: r.URL.Query().Get("from"), + }, + SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0", + StandardClaims: jwt.StandardClaims{ + Id: cid, + Audience: r.URL.Query().Get("site"), + 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 token") + return + } + + // return login url + loginURL := p.conf.AuthCodeURL(state) + p.Logf("[DEBUG] login url %s, claims=%+v", loginURL, claims) + + 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 Oauth2Handler) 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 token") + return + } + + if oauthClaims.Handshake == nil { + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "invalid handshake token") + return + } + + retrievedState := oauthClaims.Handshake.State + if retrievedState == "" || retrievedState != r.URL.Query().Get("state") { + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state") + return + } + + p.Logf("[DEBUG] token 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.StatusServiceUnavailable, err, "failed to get client info") + return + } + + defer func() { + if e := uinfo.Body.Close(); e != nil { + p.Logf("[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 + } + p.Logf("[DEBUG] got raw user info %+v", jData) + + u := p.mapUser(jData, data) + u, err = setAvatar(p.AvatarSaver, u) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save avatar to proxy") + return + } + + cid, err := randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") + return + } + claims := token.Claims{ + User: &u, + StandardClaims: jwt.StandardClaims{ + Issuer: p.Issuer, + Id: cid, + Audience: oauthClaims.Audience, + }, + SessionOnly: oauthClaims.SessionOnly, + } + + if err = p.JwtService.Set(w, claims); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") + return + } + + p.Logf("[DEBUG] user info %+v", u) + + // redirect to back url if presented in login query params + if oauthClaims.Handshake != nil && oauthClaims.Handshake.From != "" { + http.Redirect(w, r, oauthClaims.Handshake.From, http.StatusTemporaryRedirect) + return + } + rest.RenderJSON(w, r, &u) +} + +// LogoutHandler - GET /logout +func (p Oauth2Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) { + p.JwtService.Reset(w) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go index cb5ee7db..9474bc7d 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go @@ -14,14 +14,14 @@ import ( ) // NewGoogle makes google oauth2 provider -func NewGoogle(p Params) Service { - return initService(p, Service{ - Name: "google", - Endpoint: google.Endpoint, - RedirectURL: p.URL + "/auth/google/callback", - Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"}, - InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo", - MapUser: func(data userData, _ []byte) token.User { +func NewGoogle(p Params) Oauth2Handler { + return initOauth2Handler(p, Oauth2Handler{ + name: "google", + endpoint: google.Endpoint, + redirectURL: p.URL + "/auth/google/callback", + scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"}, + infoURL: "https://www.googleapis.com/oauth2/v3/userinfo", + mapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ // encode email with provider name to avoid collision if same id returned by other provider ID: "google_" + token.HashID(sha1.New(), data.value("sub")), @@ -37,14 +37,14 @@ func NewGoogle(p Params) Service { } // NewGithub makes github oauth2 provider -func NewGithub(p Params) Service { - return initService(p, Service{ - Name: "github", - Endpoint: github.Endpoint, - RedirectURL: p.URL + "/auth/github/callback", - Scopes: []string{}, - InfoURL: "https://api.github.com/user", - MapUser: func(data userData, _ []byte) token.User { +func NewGithub(p Params) Oauth2Handler { + return initOauth2Handler(p, Oauth2Handler{ + name: "github", + endpoint: github.Endpoint, + redirectURL: p.URL + "/auth/github/callback", + scopes: []string{}, + infoURL: "https://api.github.com/user", + mapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ ID: "github_" + token.HashID(sha1.New(), data.value("login")), Name: data.value("name"), @@ -60,7 +60,7 @@ func NewGithub(p Params) Service { } // NewFacebook makes facebook oauth2 provider -func NewFacebook(p Params) Service { +func NewFacebook(p Params) Oauth2Handler { // response format for fb /me call type uinfo struct { @@ -73,13 +73,13 @@ func NewFacebook(p Params) Service { } `json:"picture"` } - return initService(p, Service{ - Name: "facebook", - Endpoint: facebook.Endpoint, - RedirectURL: p.URL + "/auth/facebook/callback", - Scopes: []string{"public_profile"}, - InfoURL: "https://graph.facebook.com/me?fields=id,name,picture", - MapUser: func(data userData, bdata []byte) token.User { + return initOauth2Handler(p, Oauth2Handler{ + name: "facebook", + endpoint: facebook.Endpoint, + redirectURL: p.URL + "/auth/facebook/callback", + scopes: []string{"public_profile"}, + infoURL: "https://graph.facebook.com/me?fields=id,name,picture", + mapUser: func(data userData, bdata []byte) token.User { userInfo := token.User{ ID: "facebook_" + token.HashID(sha1.New(), data.value("id")), Name: data.value("name"), @@ -98,15 +98,15 @@ func NewFacebook(p Params) Service { } // NewYandex makes yandex oauth2 provider -func NewYandex(p Params) Service { - return initService(p, Service{ - Name: "yandex", - Endpoint: yandex.Endpoint, - RedirectURL: p.URL + "/auth/yandex/callback", - Scopes: []string{}, +func NewYandex(p Params) Oauth2Handler { + return initOauth2Handler(p, Oauth2Handler{ + name: "yandex", + endpoint: yandex.Endpoint, + redirectURL: p.URL + "/auth/yandex/callback", + scopes: []string{}, // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ - InfoURL: "https://login.yandex.ru/info?format=json", - MapUser: func(data userData, _ []byte) token.User { + infoURL: "https://login.yandex.ru/info?format=json", + mapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ ID: "yandex_" + token.HashID(sha1.New(), data.value("id")), Name: data.value("display_name"), // using Display Name by default diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/service.go b/backend/vendor/github.com/go-pkgz/auth/provider/service.go index e6d55d5e..0915e2f1 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/service.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/service.go @@ -1,47 +1,25 @@ package provider import ( - "context" "crypto/rand" "crypto/sha1" - "encoding/json" "fmt" - "io/ioutil" "net/http" "strings" - "time" - "github.com/go-pkgz/auth/logger" - - jwt "github.com/dgrijalva/jwt-go" - "github.com/go-pkgz/rest" "github.com/pkg/errors" - "golang.org/x/oauth2" "github.com/go-pkgz/auth/token" ) -// Service represents oauth2 provider +// Service represents oauth2 provider. Adds Handler method multiplexing login, auth and logout requests type Service struct { - Params - Name string - RedirectURL string - InfoURL string - Endpoint oauth2.Endpoint - Scopes []string - MapUser func(userData, []byte) token.User // map info from InfoURL to User - conf oauth2.Config + Provider } -// Params to make initialized and ready to use provider -type Params struct { - logger.L - URL string - JwtService TokenService - AvatarSaver AvatarSaver - Cid string - Csecret string - Issuer string +// NewService makes service for given provider +func NewService(p Provider) Service { + return Service{Provider: p} } // AvatarSaver defines minimal interface to save avatar @@ -57,34 +35,12 @@ type TokenService interface { Reset(w http.ResponseWriter) } -type userData map[string]interface{} - -func (u userData) value(key string) string { - // json.Unmarshal converts json "null" value to go's "nil", in this case return empty string - if val, ok := u[key]; ok && val != nil { - return fmt.Sprintf("%v", val) - } - return "" -} - -// initService makes oauth2 service for given provider -func initService(p Params, service Service) Service { - if p.L == nil { - p.L = logger.Func(func(fmt string, args ...interface{}) {}) - } - p.Logf("[INFO] init oauth2 service %s", service.Name) - service.Params = p - service.conf = oauth2.Config{ - ClientID: service.Cid, - ClientSecret: service.Csecret, - RedirectURL: service.RedirectURL, - Scopes: service.Scopes, - Endpoint: service.Endpoint, - } - - p.Logf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s", - service.Name, service.Cid, service.Endpoint, service.RedirectURL) - return service +// Provider defines interface for auth handler +type Provider interface { + Name() string + LoginHandler(w http.ResponseWriter, r *http.Request) + AuthHandler(w http.ResponseWriter, r *http.Request) + LogoutHandler(w http.ResponseWriter, r *http.Request) } // Handler returns auth routes for given provider @@ -95,11 +51,11 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) { return } if strings.HasSuffix(r.URL.Path, "/login") { - p.loginHandler(w, r) + p.LoginHandler(w, r) return } if strings.HasSuffix(r.URL.Path, "/callback") { - p.authHandler(w, r) + p.AuthHandler(w, r) return } if strings.HasSuffix(r.URL.Path, "/logout") { @@ -109,153 +65,20 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) } -// loginHandler - GET /login?from=redirect-back-url&site=siteID&session=1 -func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) { - - p.Logf("[DEBUG] login with %s", p.Name) - // make state (random) and store in session - state, err := p.randToken() - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make oauth2 state") - return - } - - cid, err := p.randToken() - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") - return - } - - claims := token.Claims{ - Handshake: &token.Handshake{ - State: state, - From: r.URL.Query().Get("from"), - }, - SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0", - StandardClaims: jwt.StandardClaims{ - Id: cid, - Audience: r.URL.Query().Get("site"), - 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 token") - return - } - - // return login url - loginURL := p.conf.AuthCodeURL(state) - p.Logf("[DEBUG] login url %s, claims=%+v", loginURL, claims) - - 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 Service) 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 token") - return - } - - if oauthClaims.Handshake == nil { - rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "finvalid handshake token") - return - } - - retrievedState := oauthClaims.Handshake.State - if retrievedState == "" || retrievedState != r.URL.Query().Get("state") { - rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state") - return - } - - p.Logf("[DEBUG] token 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.StatusServiceUnavailable, err, "failed to get client info") - return - } - - defer func() { - if e := uinfo.Body.Close(); e != nil { - p.Logf("[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 - } - p.Logf("[DEBUG] got raw user info %+v", jData) - - u := p.MapUser(jData, data) - u = p.setAvatar(u) - - cid, err := p.randToken() - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") - return - } - claims := token.Claims{ - User: &u, - StandardClaims: jwt.StandardClaims{ - Issuer: p.Issuer, - Id: cid, - Audience: oauthClaims.Audience, - }, - SessionOnly: oauthClaims.SessionOnly, - } - - if err = p.JwtService.Set(w, claims); err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") - return - } - - p.Logf("[DEBUG] user info %+v", u) - - // redirect to back url if presented in login query params - if oauthClaims.Handshake != nil && oauthClaims.Handshake.From != "" { - http.Redirect(w, r, oauthClaims.Handshake.From, http.StatusTemporaryRedirect) - return - } - rest.RenderJSON(w, r, &u) -} - // setAvatar saves avatar and puts proxied URL to u.Picture -func (p Service) setAvatar(u token.User) token.User { - if p.AvatarSaver != nil { - if avatarURL, e := p.AvatarSaver.Put(u); e == nil { - u.Picture = avatarURL - } else { - p.Logf("[WARN] failed to set avatar for %+v, %+v", u, e) +func setAvatar(ava AvatarSaver, u token.User) (token.User, error) { + if ava != nil { + avatarURL, e := ava.Put(u) + if e != nil { + return u, errors.Wrap(e, "failed to save avatar for") } + u.Picture = avatarURL + return u, nil } - return u + return u, nil // empty AvatarSaver ok, just skipped } -// LogoutHandler - GET /logout -func (p Service) LogoutHandler(w http.ResponseWriter, r *http.Request) { - p.JwtService.Reset(w) -} - -func (p Service) randToken() (string, error) { +func randToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", errors.Wrap(err, "can't get random") From 17acbc63224477e51baa5b4bcc96f07798e786db Mon Sep 17 00:00:00 2001 From: Umputun Date: Wed, 2 Jan 2019 00:16:37 -0600 Subject: [PATCH 19/21] vednor auth v0.2.0 --- backend/Gopkg.lock | 6 +- .../vendor/github.com/go-pkgz/auth/README.md | 22 ++++- .../vendor/github.com/go-pkgz/auth/auth.go | 19 ++++- backend/vendor/github.com/go-pkgz/auth/go.sum | 1 + .../go-pkgz/auth/middleware/auth.go | 4 +- .../go-pkgz/auth/provider/direct.go | 81 +++++++++++++++++++ .../go-pkgz/auth/provider/oauth2.go | 2 +- .../github.com/go-pkgz/auth/token/jwt.go | 28 ++++++- .../github.com/go-pkgz/auth/token/user.go | 7 +- 9 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 backend/vendor/github.com/go-pkgz/auth/provider/direct.go diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index a4e0cb32..a58fd36d 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -112,7 +112,7 @@ version = "v1.0.0" [[projects]] - digest = "1:2e6b942dd80c33bba11b9567f3f8a4338e80d597959e4a5d182d5251953bebe6" + digest = "1:a4ff2b649472abf046975396ac916b04527fde8d897857c2feea76498aeb762f" name = "github.com/go-pkgz/auth" packages = [ ".", @@ -123,8 +123,8 @@ "token", ] pruneopts = "UT" - revision = "a4dab49e2656a32ab7eb1255980c24f8560dc298" - version = "v0.1.1" + revision = "855a238343c3bcea84b352fdeb4393576f9eb217" + version = "v0.2.0" [[projects]] digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574" diff --git a/backend/vendor/github.com/go-pkgz/auth/README.md b/backend/vendor/github.com/go-pkgz/auth/README.md index f6f4b8b5..836344b8 100644 --- a/backend/vendor/github.com/go-pkgz/auth/README.md +++ b/backend/vendor/github.com/go-pkgz/auth/README.md @@ -2,12 +2,13 @@ -This library provides "social login" with Github, Google, Facebook and Yandex. +This library provides "social login" with Github, Google, Facebook and Yandex as well as custom auth providers. - Multiple oauth2 providers can be used at the same time - Special `dev` provider allows local testing and development - JWT stored in a secure cookie with XSRF protection. Cookies can be session-only - Minimal scopes with user name, id and picture (avatar) only +- Direct authentication with user's provided credential checker - Integrated avatar proxy with FS, boltdb and gridfs storages - Support of user-defined storages for avatars - Black list with user-defined validator @@ -128,13 +129,28 @@ Direct links to avatars won't survive any real-life usage if they linked from a - `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links like this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info") - `AvatarResizeLimit` - size (in pixels) used to resize the avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing. +### Direct authentication + +In addition to oauth2 providers `auth.Service` allows to use direct user-defined authentication. This is done by adding direct provider with `auth.AddDirectProvider`. + +```go + service.AddDirectProvider("local", provider.CredCheckerFunc(func(user, password string) (ok bool, err error) { + ok, err := checkUserSomehow(user, password) + return ok, err + })) +``` + +Such provider acts like any other, i.e. will be registered as `/auth/local/login`. + +The API for this provider - `GET /auth//login?user=&passwd=&aud=&session=[1|0]` + ### Customization There are several ways to adjust functionality of the library: 1. `SecretReader` - interface with a single method `Get(aud string) string` to return the secret used for JWT signing and verification -1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on. -2. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users. +2. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on. +3. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users. All of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`. diff --git a/backend/vendor/github.com/go-pkgz/auth/auth.go b/backend/vendor/github.com/go-pkgz/auth/auth.go index a5b04f8a..47954a8b 100644 --- a/backend/vendor/github.com/go-pkgz/auth/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/auth.go @@ -34,7 +34,9 @@ type Opts struct { SecureCookies bool // makes jwt cookie secure TokenDuration time.Duration // token's TTL, refreshed automatically CookieDuration time.Duration // cookie's TTL. This cookie stores JWT token - DisableXSRF bool // disable XSRF protection, useful for testing/debugging + + DisableXSRF bool // disable XSRF protection, useful for testing/debugging + DisableIAT bool // disable IssuedAt claim // optional (custom) names for cookies and headers JWTCookieName string // default "JWT" @@ -83,6 +85,7 @@ func NewService(opts Opts) (res *Service) { TokenDuration: opts.TokenDuration, CookieDuration: opts.CookieDuration, DisableXSRF: opts.DisableXSRF, + DisableIAT: opts.DisableIAT, JWTCookieName: opts.JWTCookieName, JWTHeaderKey: opts.JWTHeaderKey, XSRFCookieName: opts.XSRFCookieName, @@ -205,6 +208,20 @@ func (s *Service) AddProvider(name string, cid string, csecret string) { s.authMiddleware.Providers = s.providers } +// AddDirectProvider adds provider with direct check against data store +// it doesn't do any handshake and uses provided credChecker to verify user and password from the request +func (s *Service) AddDirectProvider(name string, credChecker provider.CredChecker) { + dh := provider.DirectHandler{ + L: s.logger, + ProviderName: name, + Issuer: s.issuer, + TokenService: s.jwtService, + CredChecker: credChecker, + } + s.providers = append(s.providers, provider.NewService(dh)) + s.authMiddleware.Providers = s.providers +} + // DevAuth makes dev oauth2 server, for testing and development only! func (s *Service) DevAuth() (*provider.DevAuthServer, error) { p, err := s.Provider("dev") // peak dev provider diff --git a/backend/vendor/github.com/go-pkgz/auth/go.sum b/backend/vendor/github.com/go-pkgz/auth/go.sum index 63a7900c..ccb2a3f2 100644 --- a/backend/vendor/github.com/go-pkgz/auth/go.sum +++ b/backend/vendor/github.com/go-pkgz/auth/go.sum @@ -16,6 +16,7 @@ github.com/go-pkgz/rest v1.1.4 h1:/Lrg9kBWBjNah7nmCDHLszRAfVVBIy5ajf0vVgpHPi0= github.com/go-pkgz/rest v1.1.4/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= github.com/go-pkgz/rest v1.1.5 h1:5br4mnscfLb27yxv5hJFLBVmAt09PrmIBP+meA3CfHc= github.com/go-pkgz/rest v1.1.5/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc= github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18= diff --git a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go index dc7067a5..f09432c1 100644 --- a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go @@ -126,9 +126,9 @@ func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token. } // AdminOnly middleware allows access for admins only +// this handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - user, err := token.GetUserInfo(r) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -141,7 +141,7 @@ func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { } next.ServeHTTP(w, r) } - return http.HandlerFunc(fn) + return a.auth(true)(http.HandlerFunc(fn)) // enforce auth } // basic auth for admin user diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/direct.go b/backend/vendor/github.com/go-pkgz/auth/provider/direct.go new file mode 100644 index 00000000..1bad3879 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/provider/direct.go @@ -0,0 +1,81 @@ +package provider + +import ( + "errors" + "net/http" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/go-pkgz/rest" + + "github.com/go-pkgz/auth/logger" + "github.com/go-pkgz/auth/token" +) + +// DirectHandler implements non-oauth2 provider authorizing user in traditional way with storage +// with users and hashes +type DirectHandler struct { + logger.L + CredChecker CredChecker + ProviderName string + TokenService TokenService + Issuer string +} + +// CredChecker defines interface to check credentials +type CredChecker interface { + Check(user, password string) (ok bool, err error) +} + +// CredCheckerFunc type is an adapter to allow the use of ordinary functions as CredsChecker. +type CredCheckerFunc func(user, password string) (ok bool, err error) + +// Check calls f(user,passwd) +func (f CredCheckerFunc) Check(user, password string) (ok bool, err error) { + return f(user, password) +} + +// Name of the handler +func (p DirectHandler) Name() string { return p.ProviderName } + +// LoginHandler checks "user" and "passwd" against data store and makes jwt if all passed +// GET /something?user=name&password=xyz&sess=[0|1] +func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { + user, password := r.URL.Query().Get("user"), r.URL.Query().Get("passwd") + aud := r.URL.Query().Get("aud") + sessOnly := r.URL.Query().Get("sess") == "1" + if p.CredChecker == nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, errors.New("empty credential store"), "no credential store") + return + } + ok, err := p.CredChecker.Check(user, password) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to access creds store") + return + } + if !ok { + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "incorrect user or password") + return + } + claims := token.Claims{ + User: &token.User{Name: user}, + StandardClaims: jwt.StandardClaims{ + Issuer: p.Issuer, + Audience: aud, + }, + SessionOnly: sessOnly, + } + + if err = p.TokenService.Set(w, claims); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") + return + } + rest.RenderJSON(w, r, claims.User) +} + +// AuthHandler doesn't do anyting for direct login as it has no callbacks +func (p DirectHandler) AuthHandler(w http.ResponseWriter, r *http.Request) {} + +// LogoutHandler - GET /logout +func (p DirectHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) { + p.TokenService.Reset(w) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go b/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go index 17ec0134..b69dcad5 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go @@ -77,7 +77,7 @@ func (p Oauth2Handler) Name() string { return p.name } // LoginHandler - GET /login?from=redirect-back-url&site=siteID&session=1 func (p Oauth2Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { - p.Logf("[DEBUG] login with %s", p.Name) + p.Logf("[DEBUG] login with %s", p.Name()) // make state (random) and store in session state, err := randToken() if err != nil { diff --git a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go index 96e918b6..13ae2b4d 100644 --- a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go +++ b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go @@ -1,6 +1,8 @@ package token import ( + "encoding/json" + "fmt" "net/http" "time" @@ -48,7 +50,7 @@ type Opts struct { TokenDuration time.Duration CookieDuration time.Duration DisableXSRF bool - + DisableIAT bool // disable IssuedAt claim // optional (custom) names for cookies and headers JWTCookieName string JWTHeaderKey string @@ -95,6 +97,10 @@ func (j *Service) Token(claims Claims) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + if j.SecretReader == nil { + return "", errors.New("secretreader not defined") + } + secret, err := j.SecretReader.Get(claims.Audience) // get secret via consumer defined SecretReader if err != nil { return "", errors.Wrap(err, "can't get secret") @@ -128,6 +134,10 @@ func (j *Service) Parse(tokenString string) (Claims, error) { return Claims{}, errors.Wrap(err, "failed to get aud from token token") } + if j.SecretReader == nil { + return Claims{}, errors.New("secretreader not defined") + } + secret, err := j.SecretReader.Get(aud) if err != nil { return Claims{}, errors.Wrap(err, "can't get secret") @@ -159,7 +169,13 @@ func (j *Service) Set(w http.ResponseWriter, claims Claims) error { claims.ExpiresAt = time.Now().Add(j.TokenDuration).Unix() } - claims.Issuer = j.Issuer + if claims.Issuer == "" { + claims.Issuer = j.Issuer + } + + if !j.DisableIAT { + claims.IssuedAt = time.Now().Unix() + } tokenString, err := j.Token(claims) if err != nil { @@ -280,3 +296,11 @@ type ValidatorFunc func(token string, claims Claims) bool func (f ValidatorFunc) Validate(token string, claims Claims) bool { return f(token, claims) } + +func (c Claims) String() string { + b, err := json.Marshal(c) + if err != nil { + return fmt.Sprintf("%+v %+v", c.StandardClaims, c.User) + } + return string(b) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/token/user.go b/backend/vendor/github.com/go-pkgz/auth/token/user.go index afbeb0d5..d91bb433 100644 --- a/backend/vendor/github.com/go-pkgz/auth/token/user.go +++ b/backend/vendor/github.com/go-pkgz/auth/token/user.go @@ -7,7 +7,6 @@ import ( "hash" "hash/crc64" "io" - "log" "net/http" "regexp" @@ -74,7 +73,7 @@ func (u *User) IsAdmin() bool { return u.BoolAttr(adminAttr) } -// HashID tries to has val with hash.Hash and fallback to crc if needed +// HashID tries to hash val with hash.Hash and fallback to crc if needed func HashID(h hash.Hash, val string) string { if reValidSha.MatchString(val) { @@ -83,7 +82,9 @@ func HashID(h hash.Hash, val string) string { if _, err := io.WriteString(h, val); err != nil { // fail back to crc64 - log.Printf("[WARN] can't hash id %s, %s", val, err) + if val == "" { + val = "!empty string!" + } if reValidCrc64.MatchString(val) { return val // already crced } From d8725caee92fb50bcfe16c0998e8d48ca4ec50c0 Mon Sep 17 00:00:00 2001 From: Umputun Date: Wed, 2 Jan 2019 11:19:42 -0600 Subject: [PATCH 20/21] chande docs to reflect auth lib usage --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a90a5f03..191c18f6 100644 --- a/README.md +++ b/README.md @@ -609,16 +609,16 @@ _all admin calls require auth and admin privilege_ * Data stored in [boltdb](https://github.com/coreos/bbolt) (embedded key/value database) files under `STORE_BOLT_PATH` * Each site stored in a separate boltbd file. -* In order to migrate/move remark42 to another host boltbd files as well as avatars directory `AVATAR_FS_PATH` should be transferred. +* In order to migrate/move remark42 to another host boltbd files as well as avatars directory `AVATAR_FS_PATH` should be transferred. Optionally, boltdb can be used to store avatars as well. * Automatic backup process runs every 24h and exports all content in json-like format to `backup-remark-YYYYMMDD.gz`. -* Authentication implemented with [jwt](https://github.com/dgrijalva/jwt-go) stored in a cookie. It uses HttpOnly, secure cookies. -* All heavy REST calls cached internally in LRU cache limited by `CACHE_MAX_ITEMS` and `CACHE_MAX_SIZE`. +* Authentication implemented with [go-pkgz/auth](https://github.com/go-pkgz/auth) stored in a cookie. It uses HttpOnly, secure cookies. +* All heavy REST calls cached internally in LRU cache limited by `CACHE_MAX_ITEMS` and `CACHE_MAX_SIZE` with [go-pkgz/rest](https://github.com/go-pkgz/rest) * User's activity throttled globally (up to 1000 simultaneous requests) and limited locally (per user, usually up to 10 req/sec) * Request timeout set to 60sec -* Development mode (`--dev-password` set) allows to test remark42 without social login and with admin privileges. Adds basic-auth for username: `dev`, password: `${DEV_PASSWD}`. **should not be used in production deployment** +* Admin authentication (`--admin-password` set) allows to hit remark42 API without social login and with admin privileges. Adds basic-auth for username: `admin`, password: `${ADMIN_PASSWD}`. * User can vote for the comment multiple times but only to change the vote. Double-voting not allowed. * User can edit comments in 5 mins (configurable) window after creation. * User ID hashed and prefixed by oauth provider name to avoid collisions and potential abuse. -* All avatars resized and cached locally to prevent rate limiters from oauth providers. +* All avatars resized and cached locally to prevent rate limiters from oauth providers, part of [go-pkgz/auth](https://github.com/go-pkgz/auth) functionality. * Images can be proxied (`IMG_PROXY=true`) to prevent mixed http/https. * Docker build uses [publicly available](https://github.com/umputun/baseimage) base images. From a6d20b15634dec33a69e55793caabba67e364f20 Mon Sep 17 00:00:00 2001 From: Umputun Date: Wed, 2 Jan 2019 11:50:36 -0600 Subject: [PATCH 21/21] lint: missing error check in tests --- backend/app/cmd/server_test.go | 2 +- backend/app/rest/api/admin_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 5f91ae12..7397aa5d 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -364,7 +364,7 @@ func TestServerAuthHooks(t *testing.T) { defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode, "user dev blocked") b, err := ioutil.ReadAll(resp.Body) - require.Nil(t, e) + require.Nil(t, err) t.Log(string(b)) time.Sleep(2 * time.Second) // make sure token expired and refresh happened diff --git a/backend/app/rest/api/admin_test.go b/backend/app/rest/api/admin_test.go index 002e6fcd..6bbde081 100644 --- a/backend/app/rest/api/admin_test.go +++ b/backend/app/rest/api/admin_test.go @@ -269,6 +269,7 @@ func TestAdmin_BlockedList(t *testing.T) { require.Nil(t, err) req.SetBasicAuth("admin", "password") res, err = client.Do(req) + require.Nil(t, err) require.Equal(t, 200, res.StatusCode) users = []store.BlockedUser{} err = json.NewDecoder(res.Body).Decode(&users)