From eb2ff2ab23a70298c65f19d52b76c794b2f4937c Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Wed, 17 Mar 2021 11:33:06 +0100 Subject: [PATCH 01/15] Some more messing around with oauth2 --- go.mod | 3 +- go.sum | 4 + internal/api/server.go | 17 ++-- internal/oauth/html.go | 68 ++++++++++++++++ internal/oauth/oauth.go | 146 ++++++++++++++++++++++++++++++++++- internal/oauth/oauth_test.go | 115 +++++++++++++++++++++++++++ 6 files changed, 341 insertions(+), 12 deletions(-) create mode 100644 internal/oauth/html.go create mode 100644 internal/oauth/oauth_test.go diff --git a/go.mod b/go.mod index 64c68ba11..daa784905 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,10 @@ require ( github.com/go-fed/activity v1.0.0 github.com/go-pg/pg/extra/pgdebug v0.2.0 github.com/go-pg/pg/v10 v10.8.0 + github.com/go-session/session v3.1.2+incompatible // indirect github.com/golang/mock v1.4.4 // indirect github.com/google/uuid v1.2.0 // indirect - github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57 + github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88 github.com/onsi/ginkgo v1.15.0 // indirect github.com/onsi/gomega v1.10.5 // indirect github.com/sirupsen/logrus v1.8.0 diff --git a/go.sum b/go.sum index b73d02c47..f86cc7417 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg= +github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -88,6 +90,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57 h1:+zKsBEkg1cbz7zJDms1KMU9vJBeBAlElS1SbK/x0Rvc= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= +github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88 h1:YJ//HmHOYJ4srm/LA6VPNjNisneMbY6TTM1xttV/ZQU= +github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= diff --git a/internal/api/server.go b/internal/api/server.go index 8af9e75fa..9073618f0 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -19,16 +19,13 @@ package api import ( - "net/http" - "github.com/gin-gonic/gin" "github.com/gotosocial/gotosocial/internal/config" "github.com/sirupsen/logrus" ) type Server interface { - AttachHTTPHandler(method string, path string, handler http.HandlerFunc) - AttachGinHandler(method string, path string, handler gin.HandlerFunc) + AttachHandler(method string, path string, handler gin.HandlerFunc) // AttachMiddleware(handler gin.HandlerFunc) GetAPIGroup() *gin.RouterGroup Start() @@ -60,12 +57,12 @@ func (s *server) Stop() { // todo: shut down gracefully } -func (s *server) AttachHTTPHandler(method string, path string, handler http.HandlerFunc) { - s.engine.Handle(method, path, gin.WrapH(handler)) -} - -func (s *server) AttachGinHandler(method string, path string, handler gin.HandlerFunc) { - s.engine.Handle(method, path, handler) +func (s *server) AttachHandler(method string, path string, handler gin.HandlerFunc) { + if method == "ANY" { + s.engine.Any(path, handler) + } else { + s.engine.Handle(method, path, handler) + } } func New(config *config.Config, logger *logrus.Logger) Server { diff --git a/internal/oauth/html.go b/internal/oauth/html.go new file mode 100644 index 000000000..06089aedd --- /dev/null +++ b/internal/oauth/html.go @@ -0,0 +1,68 @@ +package oauth + +const ( + signInHTML = ` + + + + + Login + + + + + + +
+

Login

+
+
+ + +
+
+ + +
+ +
+
+ + +` + + authorizeHTML = ` + + + + + Auth + + + + + + +
+
+
+

Authorize

+

The client would like to perform actions on your behalf.

+

+ +

+
+
+
+ +` +) diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index 050c23dab..d877022d9 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -19,7 +19,14 @@ package oauth import ( + "bytes" + "net/http" + "net/url" + "time" + + "github.com/gin-gonic/gin" "github.com/go-pg/pg/v10" + "github.com/go-session/session" "github.com/gotosocial/gotosocial/internal/api" "github.com/gotosocial/gotosocial/internal/gtsmodel" "github.com/gotosocial/oauth2/v4" @@ -30,6 +37,8 @@ "golang.org/x/crypto/bcrypt" ) +const methodAny = "ANY" + type API struct { manager *manage.Manager server *server.Server @@ -52,15 +61,24 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L log.Errorf("internal response error: %s", re.Error) }) - return &API{ + api := &API{ manager: manager, server: srv, conn: conn, log: log, } + + api.server.SetPasswordAuthorizationHandler(api.PasswordAuthorizationHandler) + api.server.SetUserAuthorizationHandler(api.UserAuthorizationHandler) + api.server.SetClientInfoHandler(server.ClientFormHandler) + return api } func (a *API) AddRoutes(s api.Server) error { + s.AttachHandler(methodAny, "/auth/sign_in", gin.WrapF(a.SignInHandler)) + s.AttachHandler(methodAny, "/oauth/token", gin.WrapF(a.TokenHandler)) + s.AttachHandler(methodAny, "/oauth/authorize", gin.WrapF(a.AuthorizeHandler)) + s.AttachHandler(methodAny, "/auth", gin.WrapF(a.AuthHandler)) return nil } @@ -68,7 +86,101 @@ func incorrectPassword() (string, error) { return "", errors.New("password/email combination was incorrect") } +/* + MAIN HANDLERS -- serve these through a server/router +*/ + +// SignInHandler should be served at https://example.org/auth/sign_in. +// The idea is to present a sign in page to the user, where they can enter their username and password. +// The handler will then redirect to the auth handler served at /auth +func (a *API) SignInHandler(w http.ResponseWriter, r *http.Request) { + store, err := session.Start(r.Context(), w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if r.Method == "POST" { + if r.Form == nil { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + store.Set("username", r.Form.Get("username")) + store.Save() + + w.Header().Set("Location", "/auth") + w.WriteHeader(http.StatusFound) + return + } + http.ServeContent(w, r, "sign_in.html", time.Unix(0, 0), bytes.NewReader([]byte(signInHTML))) +} + +// TokenHandler should be served at https://example.org/oauth/token +// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. +// See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token +func (a *API) TokenHandler(w http.ResponseWriter, r *http.Request) { + if err := a.server.HandleTokenRequest(w, r); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// AuthorizeHandler should be served at https://example.org/oauth/authorize +// The idea here is to present an oauth authorize page to the user, with a button +// that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user +func (a *API) AuthorizeHandler(w http.ResponseWriter, r *http.Request) { + store, err := session.Start(nil, w, r) + if err != nil { + + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if _, ok := store.Get("username"); !ok { + w.Header().Set("Location", "/auth/sign_in") + w.WriteHeader(http.StatusFound) + return + } + + http.ServeContent(w, r, "authorize.html", time.Unix(0, 0), bytes.NewReader([]byte(authorizeHTML))) +} + +// AuthHandler should be served at https://example.org/auth +func (a *API) AuthHandler(w http.ResponseWriter, r *http.Request) { + store, err := session.Start(r.Context(), w, r) + if err != nil { + a.log.Errorf("error creating session in authhandler: %s", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var form url.Values + if v, ok := store.Get("ReturnUri"); ok { + form = v.(url.Values) + } + r.Form = form + + store.Delete("ReturnUri") + store.Save() + + if err := a.server.HandleAuthorizeRequest(w, r); err != nil { + a.log.Errorf("error in authhandler during handleauthorizerequest: %s", err) + http.Error(w, err.Error(), http.StatusBadRequest) + } +} + +/* + SUB-HANDLERS -- don't serve these directly +*/ + +// PasswordAuthorizationHandler takes a username (in this case, we use an email address) +// and a password. The goal is to authenticate the password against the one for that email +// address stored in the database. If OK, we return the userid (a uuid) for that user, +// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. func (a *API) PasswordAuthorizationHandler(email string, password string) (userid string, err error) { + a.log.Debugf("entering password authorization handler with email: %s and password: %s", email, password) + // first we select the user from the database based on email address, bail if no user found for that email gtsUser := >smodel.User{} if err := a.conn.Model(gtsUser).Where("email = ?", email).Select(); err != nil { @@ -93,3 +205,35 @@ func (a *API) PasswordAuthorizationHandler(email string, password string) (useri userid = gtsUser.ID return } + +// UserAuthorizationHandler gets the user's email address from the session key 'username' +// or redirects to the /auth/sign_in page, if this key is not present. +func (a *API) UserAuthorizationHandler(w http.ResponseWriter, r *http.Request) (string, error) { + + a.log.Errorf("entering userauthorizationhandler") + + sessionStore, err := session.Start(r.Context(), w, r) + if err != nil { + a.log.Errorf("error starting session: %s", err) + return "", err + } + + v, ok := sessionStore.Get("username") + if !ok { + if err := r.ParseForm(); err != nil { + a.log.Errorf("error parsing form: %s", err) + return "", err + } + + sessionStore.Set("ReturnUri", r.Form) + sessionStore.Save() + + w.Header().Set("Location", "/auth/sign_in") + w.WriteHeader(http.StatusFound) + return v.(string), nil + } + + sessionStore.Delete("username") + sessionStore.Save() + return v.(string), nil +} diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go new file mode 100644 index 000000000..4d942c51d --- /dev/null +++ b/internal/oauth/oauth_test.go @@ -0,0 +1,115 @@ +package oauth + +import ( + "context" + "testing" + "time" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "github.com/gotosocial/gotosocial/internal/api" + "github.com/gotosocial/gotosocial/internal/config" + "github.com/gotosocial/gotosocial/internal/gtsmodel" + "github.com/gotosocial/oauth2/v4" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/suite" + "golang.org/x/crypto/bcrypt" +) + +type OauthTestSuite struct { + suite.Suite + tokenStore oauth2.TokenStore + clientStore oauth2.ClientStore + conn *pg.DB + testClientID string + testClientSecret string + testClientDomain string + testClientUserID string + testUser *gtsmodel.User + config *config.Config +} + +const () + +// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout +func (suite *OauthTestSuite) SetupSuite() { + suite.testClientID = "test-client-id" + suite.testClientSecret = "test-client-secret" + suite.testClientDomain = "https://example.org" + suite.testClientUserID = "test-client-user-id" + encryptedPassword, err := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.DefaultCost) + if err != nil { + logrus.Panicf("error encrypting user pass: %s", err) + } + suite.testUser = >smodel.User{ + EncryptedPassword: string(encryptedPassword), + Email: "user@example.org", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + AccountID: "whatever", + } +} + +// SetupTest creates a postgres connection and creates the oauth_clients table before each test +func (suite *OauthTestSuite) SetupTest() { + suite.conn = pg.Connect(&pg.Options{}) + if err := suite.conn.Ping(context.Background()); err != nil { + logrus.Panicf("db connection error: %s", err) + } + + models := []interface{}{ + &oauthClient{}, + &oauthToken{}, + >smodel.User{}, + } + + for _, m := range models { + if err := suite.conn.Model(m).CreateTable(&orm.CreateTableOptions{ + IfNotExists: true, + }); err != nil { + logrus.Panicf("db connection error: %s", err) + } + } + + suite.tokenStore = NewPGTokenStore(context.Background(), suite.conn, logrus.New()) + suite.clientStore = NewPGClientStore(suite.conn) + + if _, err := suite.conn.Model(suite.testUser).Insert(); err != nil { + logrus.Panicf("could not insert test user into db: %s", err) + } + +} + +// TearDownTest drops the oauth_clients table and closes the pg connection after each test +func (suite *OauthTestSuite) TearDownTest() { + models := []interface{}{ + &oauthClient{}, + &oauthToken{}, + >smodel.User{}, + } + for _, m := range models { + if err := suite.conn.Model(m).DropTable(&orm.DropTableOptions{}); err != nil { + logrus.Panicf("drop table error: %s", err) + } + } + if err := suite.conn.Close(); err != nil { + logrus.Panicf("error closing db connection: %s", err) + } + suite.conn = nil +} + +func (suite *OauthTestSuite) TestAPIInitialize() { + log := logrus.New() + log.SetLevel(logrus.DebugLevel) + + r := api.New(suite.config, log) + api := New(suite.tokenStore, suite.clientStore, suite.conn, log) + api.AddRoutes(r) + go r.Start() + time.Sleep(30 * time.Second) + // http://localhost:8080/oauth/authorize?client_id=whatever +} + +func TestOauthTestSuite(t *testing.T) { + suite.Run(t, new(OauthTestSuite)) +} From 9d5fb0785f80fddd3b4193aae25c4b1ee96eb7fa Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Wed, 17 Mar 2021 13:14:52 +0100 Subject: [PATCH 02/15] fiddling --- go.mod | 1 + go.sum | 26 ++++++++++++++++++++++++++ internal/api/server.go | 4 ++++ internal/oauth/oauth.go | 19 ++++++++----------- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index daa784905..75b82ea18 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/gotosocial/gotosocial go 1.16 require ( + github.com/gin-contrib/sessions v0.0.3 // indirect github.com/gin-gonic/gin v1.6.3 github.com/go-fed/activity v1.0.0 github.com/go-pg/pg/extra/pgdebug v0.2.0 diff --git a/go.sum b/go.sum index f86cc7417..6edb85cb4 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,9 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= +github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= @@ -24,10 +27,14 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI= +github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-fed/activity v1.0.0 h1:j7w3auHZnVCjUcgA1mE+UqSOjFBhvW2Z2res3vNol+o= github.com/go-fed/activity v1.0.0/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q= github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5 h1:WLvFZqoXnuVTBKA6U/1FnEHNQ0Rq0QM0rGhY8Tx6R1g= @@ -41,8 +48,10 @@ github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4 github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= @@ -70,6 +79,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -86,6 +96,13 @@ github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= +github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57 h1:+zKsBEkg1cbz7zJDms1KMU9vJBeBAlElS1SbK/x0Rvc= @@ -97,11 +114,13 @@ github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -111,13 +130,16 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn 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/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= @@ -141,6 +163,7 @@ github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= @@ -281,6 +304,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -343,6 +367,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/api/server.go b/internal/api/server.go index 9073618f0..0cfe23531 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -19,6 +19,8 @@ package api import ( + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/memstore" "github.com/gin-gonic/gin" "github.com/gotosocial/gotosocial/internal/config" "github.com/sirupsen/logrus" @@ -67,6 +69,8 @@ func (s *server) AttachHandler(method string, path string, handler gin.HandlerFu func New(config *config.Config, logger *logrus.Logger) Server { engine := gin.New() + store := memstore.NewStore([]byte("authentication-key"), []byte("encryption-key")) + engine.Use(sessions.Sessions("mysession", store)) return &server{ APIGroup: engine.Group("/api").Group("/v1"), logger: logger, diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index d877022d9..94258b806 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -20,10 +20,12 @@ import ( "bytes" + "fmt" "net/http" "net/url" "time" + "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/go-pg/pg/v10" "github.com/go-session/session" @@ -75,7 +77,7 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L } func (a *API) AddRoutes(s api.Server) error { - s.AttachHandler(methodAny, "/auth/sign_in", gin.WrapF(a.SignInHandler)) + s.AttachHandler(methodAny, "/auth/sign_in", a.SignInHandler) s.AttachHandler(methodAny, "/oauth/token", gin.WrapF(a.TokenHandler)) s.AttachHandler(methodAny, "/oauth/authorize", gin.WrapF(a.AuthorizeHandler)) s.AttachHandler(methodAny, "/auth", gin.WrapF(a.AuthHandler)) @@ -93,13 +95,8 @@ func incorrectPassword() (string, error) { // SignInHandler should be served at https://example.org/auth/sign_in. // The idea is to present a sign in page to the user, where they can enter their username and password. // The handler will then redirect to the auth handler served at /auth -func (a *API) SignInHandler(w http.ResponseWriter, r *http.Request) { - store, err := session.Start(r.Context(), w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - +func (a *API) SignInHandler(c *gin.Context) { + s := sessions.Default(c) if r.Method == "POST" { if r.Form == nil { if err := r.ParseForm(); err != nil { @@ -107,8 +104,8 @@ func (a *API) SignInHandler(w http.ResponseWriter, r *http.Request) { return } } - store.Set("username", r.Form.Get("username")) - store.Save() + s.Set("username", r.Form.Get("username")) + s.Save() w.Header().Set("Location", "/auth") w.WriteHeader(http.StatusFound) @@ -171,7 +168,7 @@ func (a *API) AuthHandler(w http.ResponseWriter, r *http.Request) { } /* - SUB-HANDLERS -- don't serve these directly + SUB-HANDLERS -- don't serve these directly, they should be attached to the oauth2 server */ // PasswordAuthorizationHandler takes a username (in this case, we use an email address) From 6eab00e05e6e65a2b8fc738ae25be5fa6c5283b3 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Wed, 17 Mar 2021 16:01:31 +0100 Subject: [PATCH 03/15] getting there....... --- go.mod | 4 +- go.sum | 3 +- internal/api/server.go | 4 +- internal/oauth/oauth.go | 200 ++++++++++++++++----------- internal/oauth/oauth_test.go | 39 +++--- internal/oauth/pgclientstore.go | 13 +- internal/oauth/pgclientstore_test.go | 1 - internal/oauth/pgtokenstore.go | 33 +++-- 8 files changed, 183 insertions(+), 114 deletions(-) diff --git a/go.mod b/go.mod index 75b82ea18..6f1ede9ed 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/gotosocial/gotosocial go 1.16 require ( - github.com/gin-contrib/sessions v0.0.3 // indirect + github.com/gin-contrib/sessions v0.0.3 github.com/gin-gonic/gin v1.6.3 github.com/go-fed/activity v1.0.0 github.com/go-pg/pg/extra/pgdebug v0.2.0 github.com/go-pg/pg/v10 v10.8.0 - github.com/go-session/session v3.1.2+incompatible // indirect + github.com/go-session/session v3.1.2+incompatible github.com/golang/mock v1.4.4 // indirect github.com/google/uuid v1.2.0 // indirect github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88 diff --git a/go.sum b/go.sum index 6edb85cb4..f482187af 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,6 @@ github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9R github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57 h1:+zKsBEkg1cbz7zJDms1KMU9vJBeBAlElS1SbK/x0Rvc= -github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88 h1:YJ//HmHOYJ4srm/LA6VPNjNisneMbY6TTM1xttV/ZQU= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -163,6 +161,7 @@ github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 h1:jnz/4VenymvySjE+Ez511s0pqVzkUOmr1fwCVytNNWk= github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/internal/api/server.go b/internal/api/server.go index 0cfe23531..321c932cd 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -69,8 +69,8 @@ func (s *server) AttachHandler(method string, path string, handler gin.HandlerFu func New(config *config.Config, logger *logrus.Logger) Server { engine := gin.New() - store := memstore.NewStore([]byte("authentication-key"), []byte("encryption-key")) - engine.Use(sessions.Sessions("mysession", store)) + store := memstore.NewStore([]byte("authentication-key"), []byte("encryption-keyencryption-key----")) + engine.Use(sessions.Sessions("gotosocial-session", store)) return &server{ APIGroup: engine.Group("/api").Group("/v1"), logger: logger, diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index 94258b806..d1f3bcf54 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -19,16 +19,12 @@ package oauth import ( - "bytes" - "fmt" "net/http" "net/url" - "time" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/go-pg/pg/v10" - "github.com/go-session/session" "github.com/gotosocial/gotosocial/internal/api" "github.com/gotosocial/gotosocial/internal/gtsmodel" "github.com/gotosocial/oauth2/v4" @@ -48,6 +44,19 @@ type API struct { log *logrus.Logger } +type login struct { + Username string `form:"username"` + Password string `form:"password"` +} + +type authorize struct { + ForceLogin string `form:"force_login,omitempty"` + ResponseType string `form:"response_type"` + ClientID string `form:"client_id"` + RedirectURI string `form:"redirect_uri"` + Scope string `form:"scope,omitempty"` +} + func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.Logger) *API { manager := manage.NewDefaultManager() manager.MapTokenStorage(ts) @@ -77,10 +86,11 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L } func (a *API) AddRoutes(s api.Server) error { - s.AttachHandler(methodAny, "/auth/sign_in", a.SignInHandler) - s.AttachHandler(methodAny, "/oauth/token", gin.WrapF(a.TokenHandler)) - s.AttachHandler(methodAny, "/oauth/authorize", gin.WrapF(a.AuthorizeHandler)) - s.AttachHandler(methodAny, "/auth", gin.WrapF(a.AuthHandler)) + s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler) + s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler) + s.AttachHandler(methodAny, "/oauth/token", a.TokenHandler) + s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeHandler) + s.AttachHandler(methodAny, "/auth", a.AuthHandler) return nil } @@ -92,78 +102,132 @@ func incorrectPassword() (string, error) { MAIN HANDLERS -- serve these through a server/router */ -// SignInHandler should be served at https://example.org/auth/sign_in. +// SignInGETHandler should be served at https://example.org/auth/sign_in. +// The idea is to present a sign in page to the user, where they can enter their username and password. +// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler +func (a *API) SignInGETHandler(c *gin.Context) { + c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(signInHTML)) +} + +// SignInPOSTHandler should be served at https://example.org/auth/sign_in. // The idea is to present a sign in page to the user, where they can enter their username and password. // The handler will then redirect to the auth handler served at /auth -func (a *API) SignInHandler(c *gin.Context) { +func (a *API) SignInPOSTHandler(c *gin.Context) { s := sessions.Default(c) - if r.Method == "POST" { - if r.Form == nil { - if err := r.ParseForm(); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } - s.Set("username", r.Form.Get("username")) - s.Save() - - w.Header().Set("Location", "/auth") - w.WriteHeader(http.StatusFound) + form := &login{} + if err := c.ShouldBind(form); err != nil || form.Username == "" || form.Password == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - http.ServeContent(w, r, "sign_in.html", time.Unix(0, 0), bytes.NewReader([]byte(signInHTML))) + s.Set("username", form.Username) + if err := s.Save(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.Redirect(http.StatusFound, "/auth") } // TokenHandler should be served at https://example.org/oauth/token // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. // See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token -func (a *API) TokenHandler(w http.ResponseWriter, r *http.Request) { - if err := a.server.HandleTokenRequest(w, r); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) +func (a *API) TokenHandler(c *gin.Context) { + if err := a.server.HandleTokenRequest(c.Writer, c.Request); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } } -// AuthorizeHandler should be served at https://example.org/oauth/authorize +// AuthorizeHandler should be served as GET at https://example.org/oauth/authorize // The idea here is to present an oauth authorize page to the user, with a button // that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user -func (a *API) AuthorizeHandler(w http.ResponseWriter, r *http.Request) { - store, err := session.Start(nil, w, r) - if err != nil { +func (a *API) AuthorizeHandler(c *gin.Context) { + s := sessions.Default(c) + form := &authorize{} - http.Error(w, err.Error(), http.StatusInternalServerError) + if err := c.ShouldBind(form); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if _, ok := store.Get("username"); !ok { - w.Header().Set("Location", "/auth/sign_in") - w.WriteHeader(http.StatusFound) + if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "missing one of: response_type, client_id or redirect_uri"}) return } - http.ServeContent(w, r, "authorize.html", time.Unix(0, 0), bytes.NewReader([]byte(authorizeHTML))) + s.Set("force_login", form.ForceLogin) + s.Set("response_type", form.ResponseType) + s.Set("client_id", form.ClientID) + s.Set("redirect_uri", form.RedirectURI) + s.Set("scope", form.Scope) + if err := s.Save(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + + v := s.Get("username") + if username, ok := v.(string); !ok || username == "" { + c.Redirect(http.StatusFound, "/auth/sign_in") + return + } + + c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(authorizeHTML)) } // AuthHandler should be served at https://example.org/auth -func (a *API) AuthHandler(w http.ResponseWriter, r *http.Request) { - store, err := session.Start(r.Context(), w, r) - if err != nil { - a.log.Errorf("error creating session in authhandler: %s", err) - http.Error(w, err.Error(), http.StatusInternalServerError) +func (a *API) AuthHandler(c *gin.Context) { + s := sessions.Default(c) + + values := url.Values{} + + if v, ok := s.Get("force_login").(string); !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "session missing force_login"}) + return + } else { + values.Add("force_login", v) + } + + if v, ok := s.Get("response_type").(string); !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "session missing response_type"}) + return + } else { + values.Add("response_type", v) + } + + if v, ok := s.Get("client_id").(string); !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "session missing client_id"}) + return + } else { + values.Add("client_id", v) + } + + if v, ok := s.Get("redirect_uri").(string); !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "session missing redirect_uri"}) + return + } else { + values.Add("redirect_uri", v) + } + + if v, ok := s.Get("scope").(string); !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "session missing scope"}) + return + } else { + values.Add("scope", v) + } + + if v, ok := s.Get("username").(string); !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "session missing username"}) + return + } else { + values.Add("username", v) + } + + c.Request.Form = values + + if err := s.Save(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - var form url.Values - if v, ok := store.Get("ReturnUri"); ok { - form = v.(url.Values) - } - r.Form = form - - store.Delete("ReturnUri") - store.Save() - - if err := a.server.HandleAuthorizeRequest(w, r); err != nil { - a.log.Errorf("error in authhandler during handleauthorizerequest: %s", err) - http.Error(w, err.Error(), http.StatusBadRequest) + if err := a.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } } @@ -203,34 +267,14 @@ func (a *API) PasswordAuthorizationHandler(email string, password string) (useri return } -// UserAuthorizationHandler gets the user's email address from the session key 'username' +// UserAuthorizationHandler gets the user's email address from the form key 'username' // or redirects to the /auth/sign_in page, if this key is not present. func (a *API) UserAuthorizationHandler(w http.ResponseWriter, r *http.Request) (string, error) { - a.log.Errorf("entering userauthorizationhandler") - - sessionStore, err := session.Start(r.Context(), w, r) - if err != nil { - a.log.Errorf("error starting session: %s", err) - return "", err + username := r.FormValue("username") + if username == "" { + http.Redirect(w, r, "/auth/sign_in", http.StatusFound) + return "", nil } - - v, ok := sessionStore.Get("username") - if !ok { - if err := r.ParseForm(); err != nil { - a.log.Errorf("error parsing form: %s", err) - return "", err - } - - sessionStore.Set("ReturnUri", r.Form) - sessionStore.Save() - - w.Header().Set("Location", "/auth/sign_in") - w.WriteHeader(http.StatusFound) - return v.(string), nil - } - - sessionStore.Delete("username") - sessionStore.Save() - return v.(string), nil + return username, nil } diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go index 4d942c51d..f786df396 100644 --- a/internal/oauth/oauth_test.go +++ b/internal/oauth/oauth_test.go @@ -7,6 +7,7 @@ "github.com/go-pg/pg/v10" "github.com/go-pg/pg/v10/orm" + "github.com/google/uuid" "github.com/gotosocial/gotosocial/internal/api" "github.com/gotosocial/gotosocial/internal/config" "github.com/gotosocial/gotosocial/internal/gtsmodel" @@ -18,35 +19,37 @@ type OauthTestSuite struct { suite.Suite - tokenStore oauth2.TokenStore - clientStore oauth2.ClientStore - conn *pg.DB - testClientID string - testClientSecret string - testClientDomain string - testClientUserID string - testUser *gtsmodel.User - config *config.Config + tokenStore oauth2.TokenStore + clientStore oauth2.ClientStore + conn *pg.DB + testUser *gtsmodel.User + testClient *oauthClient + config *config.Config } const () // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout func (suite *OauthTestSuite) SetupSuite() { - suite.testClientID = "test-client-id" - suite.testClientSecret = "test-client-secret" - suite.testClientDomain = "https://example.org" - suite.testClientUserID = "test-client-user-id" encryptedPassword, err := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.DefaultCost) if err != nil { logrus.Panicf("error encrypting user pass: %s", err) } + + userID := uuid.NewString() suite.testUser = >smodel.User{ + ID: userID, EncryptedPassword: string(encryptedPassword), - Email: "user@example.org", + Email: "user@localhost", CreatedAt: time.Now(), UpdatedAt: time.Now(), - AccountID: "whatever", + AccountID: "some-account-id-it-doesn't-matter-really", + } + suite.testClient = &oauthClient{ + ID: "a-known-client-id", + Secret: "some-secret", + Domain: "localhost", + UserID: userID, } } @@ -78,6 +81,10 @@ func (suite *OauthTestSuite) SetupTest() { logrus.Panicf("could not insert test user into db: %s", err) } + if _, err := suite.conn.Model(suite.testClient).Insert(); err != nil { + logrus.Panicf("could not insert test client into db: %s", err) + } + } // TearDownTest drops the oauth_clients table and closes the pg connection after each test @@ -107,7 +114,7 @@ func (suite *OauthTestSuite) TestAPIInitialize() { api.AddRoutes(r) go r.Start() time.Sleep(30 * time.Second) - // http://localhost:8080/oauth/authorize?client_id=whatever + // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code } func TestOauthTestSuite(t *testing.T) { diff --git a/internal/oauth/pgclientstore.go b/internal/oauth/pgclientstore.go index 22bb54f55..1df46fedb 100644 --- a/internal/oauth/pgclientstore.go +++ b/internal/oauth/pgclientstore.go @@ -20,6 +20,7 @@ import ( "context" + "fmt" "github.com/go-pg/pg/v10" "github.com/gotosocial/oauth2/v4" @@ -42,7 +43,7 @@ func (pcs *pgClientStore) GetByID(ctx context.Context, clientID string) (oauth2. ID: clientID, } if err := pcs.conn.WithContext(ctx).Model(poc).Where("id = ?", poc.ID).Select(); err != nil { - return nil, err + return nil, fmt.Errorf("error in clientstore getbyid searching for client %s: %s", clientID, err) } return models.New(poc.ID, poc.Secret, poc.Domain, poc.UserID), nil } @@ -55,7 +56,10 @@ func (pcs *pgClientStore) Set(ctx context.Context, id string, cli oauth2.ClientI UserID: cli.GetUserID(), } _, err := pcs.conn.WithContext(ctx).Model(poc).OnConflict("(id) DO UPDATE").Insert() - return err + if err != nil { + return fmt.Errorf("error in clientstore set: %s", err) + } + return nil } func (pcs *pgClientStore) Delete(ctx context.Context, id string) error { @@ -63,7 +67,10 @@ func (pcs *pgClientStore) Delete(ctx context.Context, id string) error { ID: id, } _, err := pcs.conn.WithContext(ctx).Model(poc).Where("id = ?", poc.ID).Delete() - return err + if err != nil { + return fmt.Errorf("error in clientstore delete: %s", err) + } + return nil } type oauthClient struct { diff --git a/internal/oauth/pgclientstore_test.go b/internal/oauth/pgclientstore_test.go index d6a76981f..eb011fe01 100644 --- a/internal/oauth/pgclientstore_test.go +++ b/internal/oauth/pgclientstore_test.go @@ -96,7 +96,6 @@ func (suite *PgClientStoreTestSuite) TestClientSetAndDelete() { deletedClient, err := cs.GetByID(context.Background(), suite.testClientID) suite.Assert().Nil(deletedClient) suite.Assert().NotNil(err) - suite.EqualValues("pg: no rows in result set", err.Error()) } func TestPgClientStoreTestSuite(t *testing.T) { diff --git a/internal/oauth/pgtokenstore.go b/internal/oauth/pgtokenstore.go index a927be862..0271afafb 100644 --- a/internal/oauth/pgtokenstore.go +++ b/internal/oauth/pgtokenstore.go @@ -21,6 +21,7 @@ import ( "context" "errors" + "fmt" "time" "github.com/go-pg/pg/v10" @@ -98,32 +99,44 @@ func (pts *pgTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) erro return errors.New("info param was not a models.Token") } _, err := pts.conn.WithContext(ctx).Model(oauthTokenToPGToken(t)).Insert() - return err + if err != nil { + return fmt.Errorf("error in tokenstore create: %s", err) + } + return nil } // RemoveByCode deletes a token from the DB based on the Code field func (pts *pgTokenStore) RemoveByCode(ctx context.Context, code string) error { _, err := pts.conn.Model(&oauthToken{}).Where("code = ?", code).Delete() - return err + if err != nil { + return fmt.Errorf("error in tokenstore removebycode: %s", err) + } + return nil } // RemoveByAccess deletes a token from the DB based on the Access field func (pts *pgTokenStore) RemoveByAccess(ctx context.Context, access string) error { _, err := pts.conn.Model(&oauthToken{}).Where("access = ?", access).Delete() - return err + if err != nil { + return fmt.Errorf("error in tokenstore removebyaccess: %s", err) + } + return nil } // RemoveByRefresh deletes a token from the DB based on the Refresh field func (pts *pgTokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { _, err := pts.conn.Model(&oauthToken{}).Where("refresh = ?", refresh).Delete() - return err + if err != nil { + return fmt.Errorf("error in tokenstore removebyrefresh: %s", err) + } + return nil } // GetByCode selects a token from the DB based on the Code field func (pts *pgTokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenInfo, error) { pgt := &oauthToken{} if err := pts.conn.Model(pgt).Where("code = ?", code).Select(); err != nil { - return nil, err + return nil, fmt.Errorf("error in tokenstore getbycode: %s", err) } return pgTokenToOauthToken(pgt), nil } @@ -132,7 +145,7 @@ func (pts *pgTokenStore) GetByCode(ctx context.Context, code string) (oauth2.Tok func (pts *pgTokenStore) GetByAccess(ctx context.Context, access string) (oauth2.TokenInfo, error) { pgt := &oauthToken{} if err := pts.conn.Model(pgt).Where("access = ?", access).Select(); err != nil { - return nil, err + return nil, fmt.Errorf("error in tokenstore getbyaccess: %s", err) } return pgTokenToOauthToken(pgt), nil } @@ -141,7 +154,7 @@ func (pts *pgTokenStore) GetByAccess(ctx context.Context, access string) (oauth2 func (pts *pgTokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.TokenInfo, error) { pgt := &oauthToken{} if err := pts.conn.Model(pgt).Where("refresh = ?", refresh).Select(); err != nil { - return nil, err + return nil, fmt.Errorf("error in tokenstore getbyrefresh: %s", err) } return pgTokenToOauthToken(pgt), nil } @@ -165,15 +178,15 @@ type oauthToken struct { UserID string RedirectURI string Scope string - Code string `pg:",pk"` + Code string `pg:"default:'',pk"` CodeChallenge string CodeChallengeMethod string CodeCreateAt time.Time `pg:"type:timestamp"` CodeExpiresAt time.Time `pg:"type:timestamp"` - Access string `pg:",pk"` + Access string `pg:"default:'',pk"` AccessCreateAt time.Time `pg:"type:timestamp"` AccessExpiresAt time.Time `pg:"type:timestamp"` - Refresh string `pg:",pk"` + Refresh string `pg:"default:'',pk"` RefreshCreateAt time.Time `pg:"type:timestamp"` RefreshExpiresAt time.Time `pg:"type:timestamp"` } From b6087cc08d0228f2304bbc3c6b97b86aa8d3c3f1 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Wed, 17 Mar 2021 19:52:36 +0100 Subject: [PATCH 04/15] almost there --- internal/oauth/oauth.go | 77 +++++++++++++++++++++++++++--------- internal/oauth/oauth_test.go | 6 +-- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index d1f3bcf54..97913549a 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -45,7 +45,7 @@ type API struct { } type login struct { - Username string `form:"username"` + Email string `form:"username"` Password string `form:"password"` } @@ -61,8 +61,27 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L manager := manage.NewDefaultManager() manager.MapTokenStorage(ts) manager.MapClientStorage(cs) + manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) + sc := &server.Config{ + TokenType: "Bearer", + // Must follow the spec. + AllowGetAccessRequest: false, + // Support only the non-implicit flow. + AllowedResponseTypes: []oauth2.ResponseType{oauth2.Code}, + // Allow: + // - Authorization Code (for first & third parties) + // - Refreshing Tokens + // + // Deny: + // - Resource owner secrets (password grant) + // - Client secrets + AllowedGrantTypes: []oauth2.GrantType{ + oauth2.AuthorizationCode, + oauth2.Refreshing, + }, + } - srv := server.NewDefaultServer(manager) + srv := server.NewServer(sc, manager) srv.SetInternalErrorHandler(func(err error) *errors.Response { log.Errorf("internal oauth error: %s", err) return nil @@ -79,7 +98,6 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L log: log, } - api.server.SetPasswordAuthorizationHandler(api.PasswordAuthorizationHandler) api.server.SetUserAuthorizationHandler(api.UserAuthorizationHandler) api.server.SetClientInfoHandler(server.ClientFormHandler) return api @@ -88,8 +106,8 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L func (a *API) AddRoutes(s api.Server) error { s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler) s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler) - s.AttachHandler(methodAny, "/oauth/token", a.TokenHandler) - s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeHandler) + s.AttachHandler(http.MethodPost, "/oauth/token", a.TokenHandler) + s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeGETHandler) s.AttachHandler(methodAny, "/auth", a.AuthHandler) return nil } @@ -106,6 +124,7 @@ func incorrectPassword() (string, error) { // The idea is to present a sign in page to the user, where they can enter their username and password. // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler func (a *API) SignInGETHandler(c *gin.Context) { + a.log.WithField("func", "SignInGETHandler").Trace("serving sign in html") c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(signInHTML)) } @@ -113,17 +132,26 @@ func (a *API) SignInGETHandler(c *gin.Context) { // The idea is to present a sign in page to the user, where they can enter their username and password. // The handler will then redirect to the auth handler served at /auth func (a *API) SignInPOSTHandler(c *gin.Context) { + l := a.log.WithField("func", "SignInPOSTHandler") s := sessions.Default(c) form := &login{} - if err := c.ShouldBind(form); err != nil || form.Username == "" || form.Password == "" { + if err := c.ShouldBind(form); err != nil || form.Email == "" || form.Password == "" { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - s.Set("username", form.Username) + l.Tracef("parsed form: %+v", form) + + userid, err := a.ValidatePassword(form.Email, form.Password); + if err != nil { + c.String(http.StatusForbidden, err.Error()) + } + + s.Set("username", userid) if err := s.Save(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + l.Trace("redirecting to auth page") c.Redirect(http.StatusFound, "/auth") } @@ -131,6 +159,8 @@ func (a *API) SignInPOSTHandler(c *gin.Context) { // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. // See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token func (a *API) TokenHandler(c *gin.Context) { + l := a.log.WithField("func", "TokenHandler") + l.Trace("entered token handler, will now go to server.HandleTokenRequest") if err := a.server.HandleTokenRequest(c.Writer, c.Request); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } @@ -139,7 +169,8 @@ func (a *API) TokenHandler(c *gin.Context) { // AuthorizeHandler should be served as GET at https://example.org/oauth/authorize // The idea here is to present an oauth authorize page to the user, with a button // that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user -func (a *API) AuthorizeHandler(c *gin.Context) { +func (a *API) AuthorizeGETHandler(c *gin.Context) { + l := a.log.WithField("func", "AuthorizeHandler") s := sessions.Default(c) form := &authorize{} @@ -147,6 +178,7 @@ func (a *API) AuthorizeHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + l.Tracef("parsed form: %+v", form) if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing one of: response_type, client_id or redirect_uri"}) @@ -164,15 +196,18 @@ func (a *API) AuthorizeHandler(c *gin.Context) { v := s.Get("username") if username, ok := v.(string); !ok || username == "" { + l.Trace("username was empty, redirecting to sign in page") c.Redirect(http.StatusFound, "/auth/sign_in") return } + l.Trace("serving authorize html") c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(authorizeHTML)) } // AuthHandler should be served at https://example.org/auth func (a *API) AuthHandler(c *gin.Context) { + l := a.log.WithField("func", "AuthHandler") s := sessions.Default(c) values := url.Values{} @@ -220,9 +255,10 @@ func (a *API) AuthHandler(c *gin.Context) { } c.Request.Form = values + l.Tracef("values on request set to %+v", c.Request.Form) if err := s.Save(); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -239,42 +275,45 @@ func (a *API) AuthHandler(c *gin.Context) { // and a password. The goal is to authenticate the password against the one for that email // address stored in the database. If OK, we return the userid (a uuid) for that user, // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. -func (a *API) PasswordAuthorizationHandler(email string, password string) (userid string, err error) { - a.log.Debugf("entering password authorization handler with email: %s and password: %s", email, password) - +func (a *API) ValidatePassword(email string, password string) (userid string, err error) { + l := a.log.WithField("func", "PasswordAuthorizationHandler") + l.Tracef("email %s password %s", email, password) // first we select the user from the database based on email address, bail if no user found for that email gtsUser := >smodel.User{} if err := a.conn.Model(gtsUser).Where("email = ?", email).Select(); err != nil { - a.log.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) + l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) return incorrectPassword() } // make sure a password is actually set and bail if not if gtsUser.EncryptedPassword == "" { - a.log.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email) + l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email) return incorrectPassword() } // compare the provided password with the encrypted one from the db, bail if they don't match if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil { - a.log.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err) + l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err) return incorrectPassword() } // If we've made it this far the email/password is correct so we need the oauth client-id of the user // This is, conveniently, the same as the user ID, so we can just return it. userid = gtsUser.ID + l.Tracef("returning (%s, %s)", userid, err) return } // UserAuthorizationHandler gets the user's email address from the form key 'username' // or redirects to the /auth/sign_in page, if this key is not present. -func (a *API) UserAuthorizationHandler(w http.ResponseWriter, r *http.Request) (string, error) { - - username := r.FormValue("username") +func (a *API) UserAuthorizationHandler(w http.ResponseWriter, r *http.Request) (username string, err error) { + l := a.log.WithField("func", "UserAuthorizationHandler") + username = r.FormValue("username") if username == "" { + l.Trace("username was empty, redirecting to sign in page") http.Redirect(w, r, "/auth/sign_in", http.StatusFound) return "", nil } - return username, nil + l.Tracef("returning (%s, %s)", username, err) + return username, err } diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go index f786df396..404ea5c20 100644 --- a/internal/oauth/oauth_test.go +++ b/internal/oauth/oauth_test.go @@ -48,7 +48,7 @@ func (suite *OauthTestSuite) SetupSuite() { suite.testClient = &oauthClient{ ID: "a-known-client-id", Secret: "some-secret", - Domain: "localhost", + Domain: "http://localhost:8080", UserID: userID, } } @@ -107,14 +107,14 @@ func (suite *OauthTestSuite) TearDownTest() { func (suite *OauthTestSuite) TestAPIInitialize() { log := logrus.New() - log.SetLevel(logrus.DebugLevel) + log.SetLevel(logrus.TraceLevel) r := api.New(suite.config, log) api := New(suite.tokenStore, suite.clientStore, suite.conn, log) api.AddRoutes(r) go r.Start() time.Sleep(30 * time.Second) - // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code + // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&redirect_uri=''&response_type=code } func TestOauthTestSuite(t *testing.T) { From 1b118841211da90381dd950cafa13ead78b7f589 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Thu, 18 Mar 2021 23:27:43 +0100 Subject: [PATCH 05/15] auth flow working for code --- .gitignore | 3 + cmd/gotosocial/main.go | 12 +++- example/config.yaml | 7 +++ go.mod | 2 +- go.sum | 6 ++ internal/api/server.go | 8 +++ internal/config/config.go | 55 ++++++++++++------ internal/config/template.go | 25 ++++++++ internal/oauth/html.go | 63 +------------------- internal/oauth/oauth.go | 110 +++++++++++++++++++++-------------- internal/oauth/oauth_test.go | 15 +++-- pkg/mastotypes/oauth.go | 37 ++++++++++++ web/template/authorize.tmpl | 33 +++++++++++ web/template/sign-in.tmpl | 28 +++++++++ 14 files changed, 275 insertions(+), 129 deletions(-) create mode 100644 internal/config/template.go create mode 100644 pkg/mastotypes/oauth.go create mode 100644 web/template/authorize.tmpl create mode 100644 web/template/sign-in.tmpl diff --git a/.gitignore b/.gitignore index fe72fe018..6f9b97023 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # exclude built documentation, since readthedocs will build it for us anyway /docs/_build + +# exclude coverage report +cp.out diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index 0b4110656..96798035e 100644 --- a/cmd/gotosocial/main.go +++ b/cmd/gotosocial/main.go @@ -95,6 +95,14 @@ func main() { Value: "postgres", EnvVars: []string{envNames.DbDatabase}, }, + + // TEMPLATE FLAGS + &cli.StringFlag{ + Name: flagNames.TemplateBaseDir, + Usage: "Basedir for html templating files for rendering pages and composing emails", + Value: "./web/template/", + EnvVars: []string{envNames.TemplateBaseDir}, + }, }, Commands: []*cli.Command{ { @@ -137,12 +145,12 @@ func main() { func runAction(c *cli.Context, a action.GTSAction) error { // create a new *config.Config based on the config path provided... - conf, err := config.New(c.String(config.GetFlagNames().ConfigPath)) + conf, err := config.FromFile(c.String(config.GetFlagNames().ConfigPath)) if err != nil { return fmt.Errorf("error creating config: %s", err) } // ... and the flags set on the *cli.Context by urfave - conf.ParseFlags(c) + conf.ParseCLIFlags(c) // create a logger with the log level, formatting, and output splitter already set log, err := log.New(conf.LogLevel) diff --git a/example/config.yaml b/example/config.yaml index e4d05c634..b65149d9a 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -60,3 +60,10 @@ db: # Examples: ["mydb","postgres","gotosocial"] # Default: "postgres" database: "postgres" + +# Config pertaining to templating of web pages/email notifications and the like +template: + # String. Directory from which gotosocial will attempt to load html templates (.tmpl files). + # Examples: ["/some/absolute/path/", "./relative/path/", "../../some/weird/path/"] + # Default: "./web/template/" + baseDir: "./web/template/" diff --git a/go.mod b/go.mod index 6f1ede9ed..473ff3ea8 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/go-session/session v3.1.2+incompatible github.com/golang/mock v1.4.4 // indirect github.com/google/uuid v1.2.0 // indirect - github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88 + github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3 github.com/onsi/ginkgo v1.15.0 // indirect github.com/onsi/gomega v1.10.5 // indirect github.com/sirupsen/logrus v1.8.0 diff --git a/go.sum b/go.sum index f482187af..0b12241f0 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,12 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88 h1:YJ//HmHOYJ4srm/LA6VPNjNisneMbY6TTM1xttV/ZQU= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= +github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132047-b7df44000ea6 h1:mWWMTK2Boy6FSCi45WB6GVCcXW3IoTVJKJiHmmdjywU= +github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132047-b7df44000ea6/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= +github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132554-68b81fe90e62 h1:duqoA9NSY+BFY2IVveXx5lSvIQliVvPsaNMdspkTJPc= +github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132554-68b81fe90e62/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= +github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3 h1:CKRz5d7mRum+UMR88Ue33tCYcej14WjUsB59C02DDqY= +github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= diff --git a/internal/api/server.go b/internal/api/server.go index 321c932cd..8e22742bb 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -19,6 +19,10 @@ package api import ( + "fmt" + "os" + "path/filepath" + "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/memstore" "github.com/gin-gonic/gin" @@ -71,6 +75,10 @@ func New(config *config.Config, logger *logrus.Logger) Server { engine := gin.New() store := memstore.NewStore([]byte("authentication-key"), []byte("encryption-keyencryption-key----")) engine.Use(sessions.Sessions("gotosocial-session", store)) + cwd, _ := os.Getwd() + tmPath := filepath.Join(cwd, fmt.Sprintf("%s*", config.TemplateConfig.BaseDir)) + logger.Debugf("loading templates from %s", tmPath) + engine.LoadHTMLGlob(tmPath) return &server{ APIGroup: engine.Group("/api").Group("/v1"), logger: logger, diff --git a/internal/config/config.go b/internal/config/config.go index 5832ed53f..8e2656e3f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,25 +27,38 @@ // Config pulls together all the configuration needed to run gotosocial type Config struct { - LogLevel string `yaml:"logLevel"` - ApplicationName string `yaml:"applicationName"` - DBConfig *DBConfig `yaml:"db"` + LogLevel string `yaml:"logLevel"` + ApplicationName string `yaml:"applicationName"` + DBConfig *DBConfig `yaml:"db"` + TemplateConfig *TemplateConfig `yaml:"template"` } -// New returns a new config, or an error if something goes amiss. -// The path parameter is optional, for loading a configuration json from the given path. -func New(path string) (*Config, error) { - config := &Config{ - DBConfig: &DBConfig{}, - } - if path != "" { - var err error - if config, err = loadFromFile(path); err != nil { - return nil, fmt.Errorf("error creating config: %s", err) - } +// FromFile returns a new config from a file, or an error if something goes amiss. +func FromFile(path string) (*Config, error) { + c, err := loadFromFile(path) + if err != nil { + return nil, fmt.Errorf("error creating config: %s", err) } + return c, nil +} - return config, nil +// Default returns a new config with default values. +// Not yet implemented. +func Default() *Config { + // TODO: find a way of doing this without code repetition, because having to + // repeat all values here and elsewhere is annoying and gonna be prone to mistakes. + return &Config{ + DBConfig: &DBConfig{}, + TemplateConfig: &TemplateConfig{}, + } +} + +// Empty just returns an empty config +func Empty() *Config { + return &Config{ + DBConfig: &DBConfig{}, + TemplateConfig: &TemplateConfig{}, + } } // loadFromFile takes a path to a yaml file and attempts to load a Config object from it @@ -63,8 +76,8 @@ func loadFromFile(path string) (*Config, error) { return config, nil } -// ParseFlags sets flags on the config using the provided Flags object -func (c *Config) ParseFlags(f KeyedFlags) { +// ParseCLIFlags sets flags on the config using the provided Flags object +func (c *Config) ParseCLIFlags(f KeyedFlags) { fn := GetFlagNames() // For all of these flags, we only want to set them on the config if: @@ -108,6 +121,11 @@ func (c *Config) ParseFlags(f KeyedFlags) { if c.DBConfig.Database == "" || f.IsSet(fn.DbDatabase) { c.DBConfig.Database = f.String(fn.DbDatabase) } + + // template flags + if c.TemplateConfig.BaseDir == "" || f.IsSet(fn.TemplateBaseDir) { + c.TemplateConfig.BaseDir = f.String(fn.TemplateBaseDir) + } } // KeyedFlags is a wrapper for any type that can store keyed flags and give them back. @@ -130,6 +148,7 @@ type Flags struct { DbUser string DbPassword string DbDatabase string + TemplateBaseDir string } // GetFlagNames returns a struct containing the names of the various flags used for @@ -145,6 +164,7 @@ func GetFlagNames() Flags { DbUser: "db-user", DbPassword: "db-password", DbDatabase: "db-database", + TemplateBaseDir: "template-basedir", } } @@ -161,5 +181,6 @@ func GetEnvNames() Flags { DbUser: "GTS_DB_USER", DbPassword: "GTS_DB_PASSWORD", DbDatabase: "GTS_DB_DATABASE", + TemplateBaseDir: "GTS_TEMPLATE_BASEDIR", } } diff --git a/internal/config/template.go b/internal/config/template.go new file mode 100644 index 000000000..eba86f8e6 --- /dev/null +++ b/internal/config/template.go @@ -0,0 +1,25 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package config + +// TemplateConfig pertains to templating of web pages/email notifications and the like +type TemplateConfig struct { + // Directory from which gotosocial will attempt to load html templates (.tmpl files). + BaseDir string `yaml:"baseDir"` +} diff --git a/internal/oauth/html.go b/internal/oauth/html.go index 06089aedd..a3ae4318a 100644 --- a/internal/oauth/html.go +++ b/internal/oauth/html.go @@ -2,67 +2,8 @@ const ( signInHTML = ` - - - - - Login - - - - - - -
-

Login

-
-
- - -
-
- - -
- -
-
- - -` +` authorizeHTML = ` - - - - - Auth - - - - - - -
-
-
-

Authorize

-

The client would like to perform actions on your behalf.

-

- -

-
-
-
- -` +` ) diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index 97913549a..c6d596761 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -27,6 +27,7 @@ "github.com/go-pg/pg/v10" "github.com/gotosocial/gotosocial/internal/api" "github.com/gotosocial/gotosocial/internal/gtsmodel" + "github.com/gotosocial/gotosocial/pkg/mastotypes" "github.com/gotosocial/oauth2/v4" "github.com/gotosocial/oauth2/v4/errors" "github.com/gotosocial/oauth2/v4/manage" @@ -45,16 +46,12 @@ type API struct { } type login struct { - Email string `form:"username"` + Email string `form:"username"` Password string `form:"password"` } -type authorize struct { - ForceLogin string `form:"force_login,omitempty"` - ResponseType string `form:"response_type"` - ClientID string `form:"client_id"` - RedirectURI string `form:"redirect_uri"` - Scope string `form:"scope,omitempty"` +type code struct { + Code string `form:"code"` } func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.Logger) *API { @@ -79,6 +76,9 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L oauth2.AuthorizationCode, oauth2.Refreshing, }, + AllowedCodeChallengeMethods: []oauth2.CodeChallengeMethod{ + oauth2.CodeChallengePlain, + }, } srv := server.NewServer(sc, manager) @@ -106,9 +106,13 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L func (a *API) AddRoutes(s api.Server) error { s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler) s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler) + s.AttachHandler(http.MethodPost, "/oauth/token", a.TokenHandler) + s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeGETHandler) - s.AttachHandler(methodAny, "/auth", a.AuthHandler) + s.AttachHandler(http.MethodPost, "/oauth/authorize", a.AuthorizePOSTHandler) + + // s.AttachHandler(http.MethodGet, "/auth", a.AuthGETHandler) return nil } @@ -125,7 +129,7 @@ func incorrectPassword() (string, error) { // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler func (a *API) SignInGETHandler(c *gin.Context) { a.log.WithField("func", "SignInGETHandler").Trace("serving sign in html") - c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(signInHTML)) + c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{}) } // SignInPOSTHandler should be served at https://example.org/auth/sign_in. @@ -135,15 +139,16 @@ func (a *API) SignInPOSTHandler(c *gin.Context) { l := a.log.WithField("func", "SignInPOSTHandler") s := sessions.Default(c) form := &login{} - if err := c.ShouldBind(form); err != nil || form.Email == "" || form.Password == "" { + if err := c.ShouldBind(form); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } l.Tracef("parsed form: %+v", form) - userid, err := a.ValidatePassword(form.Email, form.Password); + userid, err := a.ValidatePassword(form.Email, form.Password) if err != nil { c.String(http.StatusForbidden, err.Error()) + return } s.Set("username", userid) @@ -151,11 +156,12 @@ func (a *API) SignInPOSTHandler(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + l.Trace("redirecting to auth page") - c.Redirect(http.StatusFound, "/auth") + c.Redirect(http.StatusFound, "/oauth/authorize") } -// TokenHandler should be served at https://example.org/oauth/token +// TokenHandler should be served as a POST at https://example.org/oauth/token // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. // See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token func (a *API) TokenHandler(c *gin.Context) { @@ -166,50 +172,61 @@ func (a *API) TokenHandler(c *gin.Context) { } } -// AuthorizeHandler should be served as GET at https://example.org/oauth/authorize +// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize // The idea here is to present an oauth authorize page to the user, with a button // that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user func (a *API) AuthorizeGETHandler(c *gin.Context) { - l := a.log.WithField("func", "AuthorizeHandler") + l := a.log.WithField("func", "AuthorizeGETHandler") s := sessions.Default(c) - form := &authorize{} - - if err := c.ShouldBind(form); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - l.Tracef("parsed form: %+v", form) - - if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "missing one of: response_type, client_id or redirect_uri"}) - return - } - - s.Set("force_login", form.ForceLogin) - s.Set("response_type", form.ResponseType) - s.Set("client_id", form.ClientID) - s.Set("redirect_uri", form.RedirectURI) - s.Set("scope", form.Scope) - if err := s.Save(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - } v := s.Get("username") if username, ok := v.(string); !ok || username == "" { - l.Trace("username was empty, redirecting to sign in page") + l.Trace("username was empty, parsing form then redirecting to sign in page") + + form := &mastotypes.OAuthAuthorize{} + + if err := c.ShouldBind(form); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + l.Tracef("parsed form: %+v", form) + + if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "missing one of: response_type, client_id or redirect_uri"}) + return + } + + // save these values from the form so we can use them elsewhere in the session + s.Set("force_login", form.ForceLogin) + s.Set("response_type", form.ResponseType) + s.Set("client_id", form.ClientID) + s.Set("redirect_uri", form.RedirectURI) + s.Set("scope", form.Scope) + if err := s.Save(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.Redirect(http.StatusFound, "/auth/sign_in") return } l.Trace("serving authorize html") - c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(authorizeHTML)) + c.HTML(http.StatusOK, "authorize.tmpl", gin.H{}) } -// AuthHandler should be served at https://example.org/auth -func (a *API) AuthHandler(c *gin.Context) { - l := a.log.WithField("func", "AuthHandler") +// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize +// The idea here is to present an oauth authorize page to the user, with a button +// that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user +func (a *API) AuthorizePOSTHandler(c *gin.Context) { + l := a.log.WithField("func", "AuthorizePOSTHandler") s := sessions.Default(c) + v := s.Get("username") + if username, ok := v.(string); !ok || username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "you are not signed in"}) + } + values := url.Values{} if v, ok := s.Get("force_login").(string); !ok { @@ -277,7 +294,13 @@ func (a *API) AuthHandler(c *gin.Context) { // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. func (a *API) ValidatePassword(email string, password string) (userid string, err error) { l := a.log.WithField("func", "PasswordAuthorizationHandler") - l.Tracef("email %s password %s", email, password) + + // make sure an email/password was provided and bail if not + if email == "" || password == "" { + l.Debug("email or password was not provided") + return incorrectPassword() + } + // first we select the user from the database based on email address, bail if no user found for that email gtsUser := >smodel.User{} if err := a.conn.Model(gtsUser).Where("email = ?", email).Select(); err != nil { @@ -297,8 +320,7 @@ func (a *API) ValidatePassword(email string, password string) (userid string, er return incorrectPassword() } - // If we've made it this far the email/password is correct so we need the oauth client-id of the user - // This is, conveniently, the same as the user ID, so we can just return it. + // If we've made it this far the email/password is correct, so we can just return the id of the user. userid = gtsUser.ID l.Tracef("returning (%s, %s)", userid, err) return diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go index 404ea5c20..901f99b64 100644 --- a/internal/oauth/oauth_test.go +++ b/internal/oauth/oauth_test.go @@ -40,17 +40,24 @@ func (suite *OauthTestSuite) SetupSuite() { suite.testUser = >smodel.User{ ID: userID, EncryptedPassword: string(encryptedPassword), - Email: "user@localhost", + Email: "user@example.org", CreatedAt: time.Now(), UpdatedAt: time.Now(), - AccountID: "some-account-id-it-doesn't-matter-really", + AccountID: "some-account-id-it-doesn't-matter-really-since-this-user-doesn't-actually-have-an-account!", } suite.testClient = &oauthClient{ ID: "a-known-client-id", Secret: "some-secret", - Domain: "http://localhost:8080", + Domain: "https://example.org", UserID: userID, } + + // because go tests are run within the test package directory, we need to fiddle with the templateconfig + // basedir in a way that we wouldn't normally have to do when running the binary, in order to make + // the templates actually load + c := config.Empty() + c.TemplateConfig.BaseDir = "../../web/template/" + suite.config = c } // SetupTest creates a postgres connection and creates the oauth_clients table before each test @@ -114,7 +121,7 @@ func (suite *OauthTestSuite) TestAPIInitialize() { api.AddRoutes(r) go r.Start() time.Sleep(30 * time.Second) - // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&redirect_uri=''&response_type=code + // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&response_type=code&redirect_uri=https://example.org } func TestOauthTestSuite(t *testing.T) { diff --git a/pkg/mastotypes/oauth.go b/pkg/mastotypes/oauth.go new file mode 100644 index 000000000..1b45b38e0 --- /dev/null +++ b/pkg/mastotypes/oauth.go @@ -0,0 +1,37 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package mastotypes + +// OAuthAuthorize represents a request sent to https://example.org/oauth/authorize +// See here: https://docs.joinmastodon.org/methods/apps/oauth/ +type OAuthAuthorize struct { + // Forces the user to re-login, which is necessary for authorizing with multiple accounts from the same instance. + ForceLogin string `form:"force_login,omitempty"` + // Should be set equal to `code`. + ResponseType string `form:"response_type"` + // Client ID, obtained during app registration. + ClientID string `form:"client_id"` + // Set a URI to redirect the user to. + // If this parameter is set to urn:ietf:wg:oauth:2.0:oob then the authorization code will be shown instead. + // Must match one of the redirect URIs declared during app registration. + RedirectURI string `form:"redirect_uri"` + // List of requested OAuth scopes, separated by spaces (or by pluses, if using query parameters). + // Must be a subset of scopes declared during app registration. If not provided, defaults to read. + Scope string `form:"scope,omitempty"` +} diff --git a/web/template/authorize.tmpl b/web/template/authorize.tmpl new file mode 100644 index 000000000..0043e21ba --- /dev/null +++ b/web/template/authorize.tmpl @@ -0,0 +1,33 @@ + + + + + Auth + + + + + + +
+
+
+

Authorize

+

The client would like to perform actions on your behalf.

+

+ +

+
+
+
+ + diff --git a/web/template/sign-in.tmpl b/web/template/sign-in.tmpl new file mode 100644 index 000000000..b7aa7c702 --- /dev/null +++ b/web/template/sign-in.tmpl @@ -0,0 +1,28 @@ + + + + + Login + + + + + + +
+

Login

+
+
+ + +
+
+ + +
+ +
+
+ + + From 95faebe60dfcb1ba62a81932e14e876ea9993cd7 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Thu, 18 Mar 2021 23:54:07 +0100 Subject: [PATCH 06/15] extend application for use in oauth --- internal/gtsmodel/application.go | 34 ++++++++++++++++++++++++++++++++ pkg/mastotypes/application.go | 30 ++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 internal/gtsmodel/application.go diff --git a/internal/gtsmodel/application.go b/internal/gtsmodel/application.go new file mode 100644 index 000000000..f8c36ca25 --- /dev/null +++ b/internal/gtsmodel/application.go @@ -0,0 +1,34 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package gtsmodel + +type Application struct { + ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` + Name string `json:"name"` + // The website associated with your application (url) + Website string `json:"website,omitempty"` + // Where the user should be redirected after authorization. + RedirectURI string `json:"redirect_uri"` + // ClientID to use when obtaining an oauth token for this application (ie., in client_id parameter of https://docs.joinmastodon.org/methods/apps/) + ClientID string `json:"client_id"` + // Client secret to use when obtaining an auth token for this application (ie., in client_secret parameter of https://docs.joinmastodon.org/methods/apps/) + ClientSecret string `json:"client_secret"` + // Used for Push Streaming API. Returned with POST /api/v1/apps. Equivalent to https://docs.joinmastodon.org/entities/pushsubscription/#server_key + VapidKey string `json:"vapid_key"` +} diff --git a/pkg/mastotypes/application.go b/pkg/mastotypes/application.go index d2f894306..88128f71d 100644 --- a/pkg/mastotypes/application.go +++ b/pkg/mastotypes/application.go @@ -18,12 +18,38 @@ package mastotypes -// Application represents a mastodon-api Application, as defined here: https://docs.joinmastodon.org/entities/application/ +// Application represents a mastodon-api Application, as defined here: https://docs.joinmastodon.org/entities/application/. +// Primarily, application is used for allowing apps like Tusky etc to connect to Mastodon on behalf of a user. +// See https://docs.joinmastodon.org/methods/apps/ type Application struct { + // The application ID in the db + ID string `json:"id,omitempty"` // The name of your application. Name string `json:"name"` // The website associated with your application (url) - Website string `json:"website"` + Website string `json:"website,omitempty"` + // Where the user should be redirected after authorization. + RedirectURI string `json:"redirect_uri,omitempty"` + // ClientID to use when obtaining an oauth token for this application (ie., in client_id parameter of https://docs.joinmastodon.org/methods/apps/) + ClientID string `json:"client_id,omitempty"` + // Client secret to use when obtaining an auth token for this application (ie., in client_secret parameter of https://docs.joinmastodon.org/methods/apps/) + ClientSecret string `json:"client_secret,omitempty"` // Used for Push Streaming API. Returned with POST /api/v1/apps. Equivalent to https://docs.joinmastodon.org/entities/pushsubscription/#server_key VapidKey string `json:"vapid_key"` } + +// ApplicationPOSTRequest represents a POST request to https://example.org/api/v1/apps. +// See here: https://docs.joinmastodon.org/methods/apps/ +// And here: https://docs.joinmastodon.org/client/token/ +type ApplicationPOSTRequest struct { + // A name for your application + ClientName string `form:"client_name"` + // Where the user should be redirected after authorization. + // To display the authorization code to the user instead of redirecting + // to a web page, use urn:ietf:wg:oauth:2.0:oob in this parameter. + RedirectURIs string `form:"redirect_uris"` + // Space separated list of scopes. If none is provided, defaults to read. + Scopes string `form:"scopes"` + // A URL to the homepage of your app + Website string `form:"website"` +} From fd02a7dfbedc72541caefcf0c7ec4f0e08840675 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Fri, 19 Mar 2021 13:37:58 +0100 Subject: [PATCH 07/15] update --- internal/gtsmodel/application.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/gtsmodel/application.go b/internal/gtsmodel/application.go index f8c36ca25..80dc1fd06 100644 --- a/internal/gtsmodel/application.go +++ b/internal/gtsmodel/application.go @@ -20,15 +20,10 @@ type Application struct { ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` - Name string `json:"name"` - // The website associated with your application (url) - Website string `json:"website,omitempty"` - // Where the user should be redirected after authorization. + Name string + Website string RedirectURI string `json:"redirect_uri"` - // ClientID to use when obtaining an oauth token for this application (ie., in client_id parameter of https://docs.joinmastodon.org/methods/apps/) ClientID string `json:"client_id"` - // Client secret to use when obtaining an auth token for this application (ie., in client_secret parameter of https://docs.joinmastodon.org/methods/apps/) ClientSecret string `json:"client_secret"` - // Used for Push Streaming API. Returned with POST /api/v1/apps. Equivalent to https://docs.joinmastodon.org/entities/pushsubscription/#server_key VapidKey string `json:"vapid_key"` } From b915f427beca99d82ddf0014b7d4c7e849106a81 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 20 Mar 2021 00:18:44 +0100 Subject: [PATCH 08/15] allow showing just code --- internal/oauth/oauth.go | 22 +++++++++++++++------- internal/oauth/oauth_test.go | 5 +++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index c6d596761..0caf2e84b 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -36,8 +36,6 @@ "golang.org/x/crypto/bcrypt" ) -const methodAny = "ANY" - type API struct { manager *manage.Manager server *server.Server @@ -107,7 +105,7 @@ func (a *API) AddRoutes(s api.Server) error { s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler) s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler) - s.AttachHandler(http.MethodPost, "/oauth/token", a.TokenHandler) + s.AttachHandler(http.MethodPost, "/oauth/token", a.TokenPOSTHandler) s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeGETHandler) s.AttachHandler(http.MethodPost, "/oauth/authorize", a.AuthorizePOSTHandler) @@ -161,10 +159,10 @@ func (a *API) SignInPOSTHandler(c *gin.Context) { c.Redirect(http.StatusFound, "/oauth/authorize") } -// TokenHandler should be served as a POST at https://example.org/oauth/token +// TokenPOSTHandler should be served as a POST at https://example.org/oauth/token // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. // See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token -func (a *API) TokenHandler(c *gin.Context) { +func (a *API) TokenPOSTHandler(c *gin.Context) { l := a.log.WithField("func", "TokenHandler") l.Trace("entered token handler, will now go to server.HandleTokenRequest") if err := a.server.HandleTokenRequest(c.Writer, c.Request); err != nil { @@ -211,8 +209,14 @@ func (a *API) AuthorizeGETHandler(c *gin.Context) { return } - l.Trace("serving authorize html") - c.HTML(http.StatusOK, "authorize.tmpl", gin.H{}) + code := &code{} + if err := c.Bind(code); err != nil || code.Code == "" { + // no code yet, serve auth html and let the user confirm + l.Trace("serving authorize html") + c.HTML(http.StatusOK, "authorize.tmpl", gin.H{}) + return + } + c.String(http.StatusOK, code.Code) } // AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize @@ -254,6 +258,10 @@ func (a *API) AuthorizePOSTHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "session missing redirect_uri"}) return } else { + // todo: explain this little hack + if v == "urn:ietf:wg:oauth:2.0:oob" { + v = "http://localhost:8080/oauth/authorize" + } values.Add("redirect_uri", v) } diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go index 901f99b64..0c786456a 100644 --- a/internal/oauth/oauth_test.go +++ b/internal/oauth/oauth_test.go @@ -40,7 +40,7 @@ func (suite *OauthTestSuite) SetupSuite() { suite.testUser = >smodel.User{ ID: userID, EncryptedPassword: string(encryptedPassword), - Email: "user@example.org", + Email: "user@localhost", CreatedAt: time.Now(), UpdatedAt: time.Now(), AccountID: "some-account-id-it-doesn't-matter-really-since-this-user-doesn't-actually-have-an-account!", @@ -48,7 +48,7 @@ func (suite *OauthTestSuite) SetupSuite() { suite.testClient = &oauthClient{ ID: "a-known-client-id", Secret: "some-secret", - Domain: "https://example.org", + Domain: "http://localhost:8080", UserID: userID, } @@ -122,6 +122,7 @@ func (suite *OauthTestSuite) TestAPIInitialize() { go r.Start() time.Sleep(30 * time.Second) // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&response_type=code&redirect_uri=https://example.org + // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&response_type=code&redirect_uri=urn:ietf:wg:oauth:2.0:oob } func TestOauthTestSuite(t *testing.T) { From 0abe01ea225ba91a504a462b1d72a5c33655bd6f Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 20 Mar 2021 18:41:10 +0100 Subject: [PATCH 09/15] add progress list --- PROGRESS.md | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 8 +- 2 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 PROGRESS.md diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 000000000..9c6c7844b --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,208 @@ +# Progress + +* [ ] Client-To-Server (Client REST API) + * [ ] Token and sign-in + * [x] /api/v1/apps POST (Create an application) + * [ ] /api/v1/apps/verify_credentials GET (Verify an application works) + * [x] /oauth/authorize GET (Show authorize page to user) + * [x] /oauth/authorize POST (Get an oauth access code for an app/user) + * [ ] /oauth/token POST (Obtain a user-level access token) + * [ ] /oauth/revoke POST (Revoke a user-level access token) + * [x] /auth/sign_in GET (Show form for user signin) + * [x] /auth/sign_in POST (Validate username and password and sign user in) + * [ ] Accounts + * [ ] /api/v1/accounts POST (Register a new account) + * [ ] /api/v1/accounts/verify_credentials GET (Verify account credentials with a user token) + * [ ] /api/v1/accounts/update_credentials PATCH (Update user's display name/preferences) + * [ ] /api/v1/accounts/:id GET (Get account information) + * [ ] /api/v1/accounts/:id/statuses GET (Get an account's statuses) + * [ ] /api/v1/accounts/:id/followers GET (Get an account's followers) + * [ ] /api/v1/accounts/:id/following GET (Get an account's following) + * [ ] /api/v1/accounts/:id/featured_tags GET (Get an account's featured tags) + * [ ] /api/v1/accounts/:id/lists GET (Get lists containing this account) + * [ ] /api/v1/accounts/:id/identity_proofs GET (Get identity proofs for this account) + * [ ] /api/v1/accounts/:id/follow POST (Follow this account) + * [ ] /api/v1/accounts/:id/unfollow POST (Unfollow this account) + * [ ] /api/v1/accounts/:id/block POST (Block this account) + * [ ] /api/v1/accounts/:id/unblock POST (Unblock this account) + * [ ] /api/v1/accounts/:id/mute POST (Mute this account) + * [ ] /api/v1/accounts/:id/unmute POST (Unmute this account) + * [ ] /api/v1/accounts/:id/pin POST (Feature this account on profile) + * [ ] /api/v1/accounts/:id/unpin POST (Remove this account from profile) + * [ ] /api/v1/accounts/:id/note POST (Make a personal note about this account) + * [ ] /api/v1/accounts/relationships GET (Check relationships with accounts) + * [ ] /api/v1/accounts/search GET (Search for an account) + * [ ] Bookmarks + * [ ] /api/v1/bookmarks GET (See bookmarked statuses) + * [ ] Favourites + * [ ] /api/v1/favourites GET (See faved statuses) + * [ ] Mutes + * [ ] /api/v1/mutes GET (See list of muted accounts) + * [ ] Blocks + * [ ] /api/v1/blocks GET (See list of blocked accounts) + * [ ] Domain Blocks + * [ ] /api/v1/domain_blocks GET (See list of domain blocks) + * [ ] /api/v1/domain_blocks POST (Create a domain block) + * [ ] /api/v1/domain_blocks DELETE (Remove a domain block) + * [ ] Filters + * [ ] /api/v1/filters GET (Get list of filters) + * [ ] /api/v1/filters/:id GET (View a filter) + * [ ] /api/v1/filters POST (Create a filter) + * [ ] /api/v1/filters/:id PUT (Update a filter) + * [ ] /api/v1/filters/:id DELETE (Remove a filter) + * [ ] Reports + * [ ] /api/v1/reports POST (File a report) + * [ ] Follow Requests + * [ ] /api/v1/follow_requests GET (View pending follow requests) + * [ ] /api/v1/follow_requests/:id/authorize POST (Accept a follow request) + * [ ] /api/v1/follow_requests/:id/reject POST (Reject a follow request) + * [ ] Endorsements + * [ ] /api/v1/endorsements GET (View existing endorsements) + * [ ] Featured Tags + * [ ] /api/v1/featured_tags GET (View featured tags) + * [ ] /api/v1/featured_tags POST (Feature a tag) + * [ ] /api/v1/featured_tags/:id DELETE (Unfeature a tag) + * [ ] /api/v1/featured_tags/suggestions GET (See most used tags) + * [ ] Preferences + * [ ] /api/v1/preferences GET (Get user preferences) + * [ ] Suggestions + * [ ] /api/v1/suggestions GET (Get suggested accounts to follow) + * [ ] /api/v1/suggestions/:account_id DELETE (Delete a suggestion) + * [ ] Statuses + * [ ] /api/v1/statuses POST (Create a new status) + * [ ] /api/v1/statuses/:id GET (View an existing status) + * [ ] /api/v1/statuses/:id DELETE (Delete a status) + * [ ] /api/v1/statuses/:id/context GET (View statuses above and below status ID) + * [ ] /api/v1/statuses/:id/reblogged_by GET (See who has reblogged a status) + * [ ] /api/v1/statuses/:id/favourited_by GET (See who has faved a status) + * [ ] /api/v1/statuses/:id/favourite POST (Fave a status) + * [ ] /api/v1/statuses/:id/favourite POST (Unfave a status) + * [ ] /api/v1/statuses/:id/reblog POST (Reblog a status) + * [ ] /api/v1/statuses/:id/unreblog POST (Undo a reblog) + * [ ] /api/v1/statuses/:id/bookmark POST (Bookmark a status) + * [ ] /api/v1/statuses/:id/unbookmark POST (Undo a bookmark) + * [ ] /api/v1/statuses/:id/mute POST (Mute notifications on a status) + * [ ] /api/v1/statuses/:id/unmute POST (Unmute notifications on a status) + * [ ] /api/v1/statuses/:id/pin POST (Pin a status to profile) + * [ ] /api/v1/statuses/:id/unpin POST (Unpin a status from profile) + * [ ] Media + * [ ] /api/v1/media POST (Upload a media attachment) + * [ ] /api/v1/media/:id GET (Get a media attachment) + * [ ] /api/v1/media/:id PUT (Update an attachment) + * [ ] Polls + * [ ] /api/v1/polls/:id GET (Show a poll) + * [ ] /api/v1/polls/:id/votes POST (Vote on a poll) + * [ ] Scheduled Statuses + * [ ] /api/v1/scheduled_statuses GET (View scheduled statuses) + * [ ] /api/v1/scheduled_statuses/:id GET (View a scheduled status) + * [ ] /api/v1/scheduled_statuses/:id PUT (Schedule a status) + * [ ] /api/v1/scheduled_statuses/:id DELETE (Cancel a scheduled status) + * [ ] Timelines + * [ ] /api/v1/timelines/public GET (See the public/federated timeline) + * [ ] /api/v1/timelines/tag/:hashtag GET (Get public statuses that use hashtag) + * [ ] /api/v1/timelines/home GET (View statuses from followed users) + * [ ] /api/v1/timelines/list/:list_id GET (Get statuses in given list) + * [ ] Conversations + * [ ] /api/v1/conversations GET (Get a list of direct message convos) + * [ ] /api/v1/conversations/:id DELETE (Delete a direct message convo) + * [ ] /api/v1/conversations/:id POST (Mark a conversation as read) + * [ ] Lists + * [ ] /api/v1/lists GET (Show a list of lists) + * [ ] /api/v1/lists/:id GET (Show a single list) + * [ ] /api/v1/lists POST (Create a new list) + * [ ] /api/v1/lists/:id PUT (Update a list) + * [ ] /api/v1/lists/:id DELETE (Delete a list) + * [ ] /api/v1/lists/:id/accounts GET (View which accounts are in a list) + * [ ] /api/v1/lists/:id/accounts POST (Add accounts to a list) + * [ ] /api/v1/lists/:id/accounts DELETE (Remove accounts from a list) + * [ ] Markers + * [ ] /api/v1/markers GET (Get saved timeline position) + * [ ] /api/v1/markers POST (Save timeline position) + * [ ] Streaming + * [ ] /api/v1/streaming WEBSOCKETS (Stream live events to user via websockets) + * [ ] Notifications + * [ ] /api/v1/notifications GET (Get list of notifications) + * [ ] /api/v1/notifications/:id GET (Get a single notification) + * [ ] /api/v1/notifications/clear POST (Clear all notifications) + * [ ] /api/v1/notifications/:id POST (Clear a single notification) + * [ ] Push + * [ ] /api/v1/push/subscription POST (Subscribe to push notifications) + * [ ] /api/v1/push/subscription GET (Get current subscription) + * [ ] /api/v1/push/subscription PUT (Change notification types) + * [ ] /api/v1/push/subscription DELETE (Delete current subscription) + * [ ] Search + * [ ] /api/v2/search GET (Get search query results) + * [ ] Instance + * [ ] /api/v1/instance GET (Get instance information) + * [ ] /api/v1/instance PATCH (Update instance information) + * [ ] /api/v1/instance/peers GET (Get list of federated servers) + * [ ] /api/v1/instance/activity GET (Instance activity over the last 3 months, binned weekly.) + * [ ] Trends + * [ ] /api/v1/trends GET (Get a list of trending tags for the last week) + * [ ] Directory + * [ ] /api/v1/directory GET (Show profiles this server is aware of.) + * [ ] Custom Emojis + * [ ] /api/v1/custom_emojis GET (Show this server's custom emoji) + * [ ] Admin + * [ ] /api/v1/admin/accounts GET (View accounts filtered by criteria) + * [ ] /api/v1/admin/accounts/:id GET (View admin level info about an account) + * [ ] /api/v1/admin/accounts/:id/action POST (Perform an admin action on account) + * [ ] /api/v1/admin/accounts/:id/approve POST (Approve pending account) + * [ ] /api/v1/admin/accounts/:id/reject POST (Deny pending account) + * [ ] /api/v1/admin/accounts/:id/enable POST (Reenable a disabled account) + * [ ] /api/v1/admin/accounts/:id/unsilence POST (Unsilence a silenced account) + * [ ] /api/v1/admin/accounts/:id/unsuspend POST (Unsuspend a suspended account) + * [ ] /api/v1/admin/reports GET (View all reports) + * [ ] /api/v1/admin/reports/:id GET (View a single report) + * [ ] /api/v1/admin/reports/:id/assign_to_self POST (Assign a report to the current admin account) + * [ ] /api/v1/admin/reports/:id/unassign POST (Unassign a report) + * [ ] /api/v1/admin/reports/:id/resolve POST (Mark a report as resolved) + * [ ] /api/v1/admin/reports/:id/reopen POST (Reopen a closed report) + * [ ] Announcements + * [ ] /api/v1/announcements GET (Show all current announcements) + * [ ] /api/v1/announcements/:id/dismiss POST (Mark an announcement as read) + * [ ] /api/v1/announcements/:id/reactions/:name PUT (Add a reaction to an announcement) + * [ ] /api/v1/announcements/:id/reactions/:name DELETE (Remove a reaction from an announcement) + * [ ] Proofs + * [ ] /api/proofs GET (View identity proofs) + * [ ] Oembed + * [ ] /api/oembed GET (Get oembed metadata for a status URL) +* [ ] Server-To-Server (Federation protocol) + * [ ] Mechanism to trigger side effects from client AP + * [ ] Federation modes + * [ ] 'Slow' federation + * [ ] Reputation scoring system for instances + * [ ] 'Greedy' federation + * [ ] No federation (insulate this instance from the Fediverse) + * [ ] Allowlist +* [ ] Storage + * [x] Internal/statuses/preferences etc + * [x] Postgres interface + * [ ] Media storage + * [ ] Local storage interface + * [ ] S3 storage interface +* [ ] Cache + * [ ] In-memory cache +* [ ] Security features + * [ ] Authorization middleware + * [ ] Rate limiting middleware + * [ ] Scope middleware + * [ ] Permissions/acl middleware for admins+moderators +* [ ] Documentation + * [ ] Swagger API documentation + * [ ] ReadTheDocs.io documentation + * [ ] Deployment documentation + * [ ] App creation guide +* [ ] Tooling + * [ ] Database migration tool + * [ ] Admin CLI tool +* [ ] Build + * [ ] Docker containerization + * [ ] Dockerfile + * [ ] docker-compose.yml +* [ ] Tests + * [ ] Unit/integration + * [ ] 25% coverage + * [ ] 50% coverage + * [ ] 90%+ coverage + * [ ] Benchmarking diff --git a/README.md b/README.md index fa55a2aa8..a5d21a6c9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GoToSocial - +![patrons](https://img.shields.io/liberapay/patrons/dumpsterqueer.svg?logo=liberapay) ![receives](https://img.shields.io/liberapay/receives/dumpsterqueer.svg?logo=liberapay) Federated social media software. @@ -30,9 +30,13 @@ Among other things: * Easily-configurable character limit. * Groups and group posting. +## Implementation Status + +For an up-to-date view on progress made towards a v1.0.0 release, see [here](./PROGRESS.md). + ## Contact -For questions and comments, you can reach out to Tobi on the Fediverse here or mail admin@gotosocial.org. +For questions and comments, you can reach out to Tobi on the Fediverse [here](https://ondergrond.org/@dumpsterqueer) or mail admin@gotosocial.org. ## Sponsorship From 81760963b04501e237a2822570fb580104aacd0a Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 20 Mar 2021 19:04:27 +0100 Subject: [PATCH 10/15] formatting,comments --- internal/config/config.go | 4 +- internal/gtsmodel/account.go | 158 +++++++++++++++++++++++++---------- internal/gtsmodel/status.go | 8 +- internal/gtsmodel/user.go | 121 +++++++++++++++++++-------- pkg/mastotypes/oauth.go | 8 +- 5 files changed, 212 insertions(+), 87 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 8e2656e3f..ce194cd52 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -48,7 +48,7 @@ func Default() *Config { // TODO: find a way of doing this without code repetition, because having to // repeat all values here and elsewhere is annoying and gonna be prone to mistakes. return &Config{ - DBConfig: &DBConfig{}, + DBConfig: &DBConfig{}, TemplateConfig: &TemplateConfig{}, } } @@ -56,7 +56,7 @@ func Default() *Config { // Empty just returns an empty config func Empty() *Config { return &Config{ - DBConfig: &DBConfig{}, + DBConfig: &DBConfig{}, TemplateConfig: &TemplateConfig{}, } } diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 84ba027b2..7bc8118a3 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -26,60 +26,130 @@ "time" ) -// Account represents a GoToSocial user account +// Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc) type Account struct { + /* + BASIC INFO + */ + + // id of this account in the local database; the end-user will never need to know this, it's strictly internal + ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` + // Username of the account, should just be a string of [a-z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org`` + Username string `pg:",notnull,unique:userdomain"` // username and domain should be unique *with* each other + // Domain of the account, will be empty if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username. + Domain string `pg:",unique:userdomain"` // username and domain + + /* + ACCOUNT METADATA + */ + + // Avatar image for this account Avatar + // Header image for this account Header - URI string - URL string - ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` - Username string - Domain string - Secret string - PrivateKey string - PublicKey string - RemoteURL string - CreatedAt time.Time `pg:"type:timestamp,notnull"` - UpdatedAt time.Time `pg:"type:timestamp,notnull"` - Note string - DisplayName string + // DisplayName for this account. Can be empty, then just the Username will be used for display purposes. + DisplayName string + // a key/value map of fields that this account has added to their profile + Fields map[string]string + // A note that this account has on their profile (ie., the account's bio/description of themselves) + Note string + // Is this a memorial account, ie., has the user passed away? + Memorial bool + // This account has moved this account id in the database + MovedToAccountID int + // When was this account created? + CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + // When was this account last updated? + UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + // When should this account function until SubscriptionExpiresAt time.Time `pg:"type:timestamp"` - Locked bool - LastWebfingeredAt time.Time `pg:"type:timestamp"` - InboxURL string - OutboxURL string - SharedInboxURL string - FollowersURL string - Protocol int - Memorial bool - MovedToAccountID int - FeaturedCollectionURL string - Fields map[string]string - ActorType string - Discoverable bool - AlsoKnownAs string - SilencedAt time.Time `pg:"type:timestamp"` - SuspendedAt time.Time `pg:"type:timestamp"` - TrustLevel int - HideCollections bool - SensitizedAt time.Time `pg:"type:timestamp"` - SuspensionOrigin int + + /* + PRIVACY SETTINGS + */ + + // Does this account need an approval for new followers? + Locked bool + // Should this account be shown in the instance's profile directory? + Discoverable bool + + /* + ACTIVITYPUB THINGS + */ + + // What is the activitypub URI for this account discovered by webfinger? + URI string `pg:",unique"` + // At which URL can we see the user account in a web browser? + URL string `pg:",unique"` + // RemoteURL where this account is located. Will be empty if this is a local account. + RemoteURL string `pg:",unique"` + // Last time this account was located using the webfinger API. + LastWebfingeredAt time.Time `pg:"type:timestamp"` + // Address of this account's activitypub inbox, for sending activity to + InboxURL string `pg:",unique"` + // Address of this account's activitypub outbox + OutboxURL string `pg:",unique"` + // Don't support shared inbox right now so this is just a stub for a future implementation + SharedInboxURL string `pg:",unique"` + // URL for getting the followers list of this account + FollowersURL string `pg:",unique"` + // URL for getting the featured collection list of this account + FeaturedCollectionURL string `pg:",unique"` + // What type of activitypub actor is this account? + ActorType string + // This account is associated with x account id + AlsoKnownAs string + + /* + CRYPTO FIELDS + */ + + Secret string + // Privatekey for validating activitypub requests, will obviously only be defined for local accounts + PrivateKey string + // Publickey for encoding activitypub requests, will be defined for both local and remote accounts + PublicKey string + + /* + ADMIN FIELDS + */ + + // When was this account set to have all its media shown as sensitive? + SensitizedAt time.Time `pg:"type:timestamp"` + // When was this account silenced (eg., statuses only visible to followers, not public)? + SilencedAt time.Time `pg:"type:timestamp"` + // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account) + SuspendedAt time.Time `pg:"type:timestamp"` + // How much do we trust this account 🤔 + TrustLevel int + // Should we hide this account's collections? + HideCollections bool + // id of the user that suspended this account through an admin action + SuspensionOrigin int } +// Avatar represents the avatar for the account for display purposes type Avatar struct { - AvatarFileName string - AvatarContentType string - AvatarFileSize int - AvatarUpdatedAt *time.Time `pg:"type:timestamp"` - AvatarRemoteURL *url.URL `pg:"type:text"` + // File name of the avatar on local storage + AvatarFileName string + // Gif? png? jpeg? + AvatarContentType string + AvatarFileSize int + AvatarUpdatedAt *time.Time `pg:"type:timestamp"` + // Where can we retrieve the avatar? + AvatarRemoteURL *url.URL `pg:"type:text"` AvatarStorageSchemaVersion int } +// Header represents the header of the account for display purposes type Header struct { - HeaderFileName string - HeaderContentType string - HeaderFileSize int - HeaderUpdatedAt *time.Time `pg:"type:timestamp"` - HeaderRemoteURL *url.URL `pg:"type:text"` + // File name of the header on local storage + HeaderFileName string + // Gif? png? jpeg? + HeaderContentType string + HeaderFileSize int + HeaderUpdatedAt *time.Time `pg:"type:timestamp"` + // Where can we retrieve the header? + HeaderRemoteURL *url.URL `pg:"type:text"` HeaderStorageSchemaVersion int } diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index 39c450934..22e88c08e 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -22,11 +22,11 @@ type Status struct { ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` - URI string - URL string + URI string `pg:",unique"` + URL string `pg:",unique"` Content string - CreatedAt time.Time `pg:"type:timestamp,notnull"` - UpdatedAt time.Time `pg:"type:timestamp,notnull"` + CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` Local bool AccountID string InReplyToID string diff --git a/internal/gtsmodel/user.go b/internal/gtsmodel/user.go index 577590ddf..c105899b8 100644 --- a/internal/gtsmodel/user.go +++ b/internal/gtsmodel/user.go @@ -23,43 +23,98 @@ "time" ) +// User represents an actual human user of gotosocial. Note, this is a LOCAL gotosocial user, not a remote account. +// To cross reference this local user with their account (which can be local or remote), use the AccountID field. type User struct { - ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` - Email string `pg:",notnull"` - CreatedAt time.Time `pg:"type:timestamp,notnull"` - UpdatedAt time.Time `pg:"type:timestamp,notnull"` - EncryptedPassword string `pg:",notnull"` - ResetPasswordToken string - ResetPasswordSentAt time.Time `pg:"type:timestamp"` - SignInCount int - CurrentSignInAt time.Time `pg:"type:timestamp"` - LastSignInAt time.Time `pg:"type:timestamp"` - CurrentSignInIP net.IP - LastSignInIP net.IP - Admin bool - ConfirmationToken string - ConfirmedAt time.Time `pg:"type:timestamp"` - ConfirmationSentAt time.Time `pg:"type:timestamp"` - UnconfirmedEmail string - Locale string + /* + BASIC INFO + */ + + // id of this user in the local database; the end-user will never need to know this, it's strictly internal + ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` + // confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported + Email string `pg:",notnull,unique"` + // The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet) + AccountID string `pg:"default:'',notnull,unique"` + // The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables + EncryptedPassword string `pg:",notnull"` + + /* + USER METADATA + */ + + // When was this user created? + CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + // From what IP was this user created? + SignUpIP net.IP + // When was this user updated (eg., password changed, email address changed)? + UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + // When did this user sign in for their current session? + CurrentSignInAt time.Time `pg:"type:timestamp"` + // What's the most recent IP of this user + CurrentSignInIP net.IP + // When did this user last sign in? + LastSignInAt time.Time `pg:"type:timestamp"` + // What's the previous IP of this user? + LastSignInIP net.IP + // How many times has this user signed in? + SignInCount int + // id of the user who invited this user (who let this guy in?) + InviteID string + // What languages does this user want to see? + ChosenLanguages []string + // What languages does this user not want to see? + FilteredLanguages []string + // In what timezone/locale is this user located? + Locale string + // Which application id created this user? See gtsmodel.Application + CreatedByApplicationID string + // When did we last contact this user + LastEmailedAt time.Time `pg:"type:timestamp"` + + /* + USER CONFIRMATION + */ + + // What confirmation token did we send this user/what are we expecting back? + ConfirmationToken string + // When did the user confirm their email address + ConfirmedAt time.Time `pg:"type:timestamp"` + // When did we send email confirmation to this user? + ConfirmationSentAt time.Time `pg:"type:timestamp"` + // Email address that hasn't yet been confirmed + UnconfirmedEmail string + + /* + ACL FLAGS + */ + + // Is this user a moderator? + Moderator bool + // Is this user an admin? + Admin bool + // Is this user disabled from posting? + Disabled bool + // Has this user been approved by a moderator? + Approved bool + + /* + USER SECURITY + */ + + // The generated token that the user can use to reset their password + ResetPasswordToken string + // When did we email the user their reset-password email? + ResetPasswordSentAt time.Time `pg:"type:timestamp"` + EncryptedOTPSecret string EncryptedOTPSecretIv string EncryptedOTPSecretSalt string - ConsumedTimestamp int OTPRequiredForLogin bool - LastEmailedAt time.Time `pg:"type:timestamp"` OTPBackupCodes []string - FilteredLanguages []string - AccountID string `pg:",notnull"` - Disabled bool - Moderator bool - InviteID string - RememberToken string - ChosenLanguages []string - CreatedByApplicationID string - Approved bool - SignInToken string - SignInTokenSentAt time.Time `pg:"type:timestamp"` - WebauthnID string - SignUpIP net.IP + ConsumedTimestamp int + RememberToken string + SignInToken string + SignInTokenSentAt time.Time `pg:"type:timestamp"` + WebauthnID string } diff --git a/pkg/mastotypes/oauth.go b/pkg/mastotypes/oauth.go index 1b45b38e0..d93ea079f 100644 --- a/pkg/mastotypes/oauth.go +++ b/pkg/mastotypes/oauth.go @@ -22,16 +22,16 @@ // See here: https://docs.joinmastodon.org/methods/apps/oauth/ type OAuthAuthorize struct { // Forces the user to re-login, which is necessary for authorizing with multiple accounts from the same instance. - ForceLogin string `form:"force_login,omitempty"` + ForceLogin string `form:"force_login,omitempty"` // Should be set equal to `code`. ResponseType string `form:"response_type"` // Client ID, obtained during app registration. - ClientID string `form:"client_id"` + ClientID string `form:"client_id"` // Set a URI to redirect the user to. // If this parameter is set to urn:ietf:wg:oauth:2.0:oob then the authorization code will be shown instead. // Must match one of the redirect URIs declared during app registration. - RedirectURI string `form:"redirect_uri"` + RedirectURI string `form:"redirect_uri"` // List of requested OAuth scopes, separated by spaces (or by pluses, if using query parameters). // Must be a subset of scopes declared during app registration. If not provided, defaults to read. - Scope string `form:"scope,omitempty"` + Scope string `form:"scope,omitempty"` } From b3d13623700ab340e6f1bd786998cc92c175a9ae Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 20 Mar 2021 19:04:35 +0100 Subject: [PATCH 11/15] add fields --- internal/gtsmodel/application.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/gtsmodel/application.go b/internal/gtsmodel/application.go index 80dc1fd06..c0d6dddaf 100644 --- a/internal/gtsmodel/application.go +++ b/internal/gtsmodel/application.go @@ -19,11 +19,12 @@ package gtsmodel type Application struct { - ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` - Name string - Website string - RedirectURI string `json:"redirect_uri"` - ClientID string `json:"client_id"` + ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` + Name string + Website string + RedirectURI string `json:"redirect_uri"` + ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` - VapidKey string `json:"vapid_key"` + Scopes string `json:"scopes"` + VapidKey string `json:"vapid_key"` } From 2b7b562a438f49fa8ebf204c50cf255ccf42c111 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 20 Mar 2021 19:05:37 +0100 Subject: [PATCH 12/15] add apps handler --- internal/oauth/oauth.go | 83 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index 0caf2e84b..39fe2ddce 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -122,6 +122,89 @@ func incorrectPassword() (string, error) { MAIN HANDLERS -- serve these through a server/router */ +// AppsPOSTHandler should be served at https://example.org/api/v1/apps +// It is equivalent to: https://docs.joinmastodon.org/methods/apps/ +func (a *API) AppsPOSTHandler(c *gin.Context) { + l := a.log.WithField("func", "AppsPOSTHandler") + l.Trace("entering AppsPOSTHandler") + + form := &mastotypes.ApplicationPOSTRequest{} + if err := c.ShouldBind(form); err != nil { + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + return + } + + // permitted length for most fields + permittedLength := 64 + // redirect can be a bit bigger because we probably need to encode data in the redirect uri + permittedRedirect := 256 + + // check lengths of fields before proceeding so the user can't spam huge entries into the database + if len(form.ClientName) > permittedLength { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", permittedLength)}) + return + } + if len(form.Website) > permittedLength { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", permittedLength)}) + return + } + if len(form.RedirectURIs) > permittedRedirect { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", permittedRedirect)}) + return + } + if len(form.Scopes) > permittedLength { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", permittedLength)}) + return + } + + // set default 'read' for scopes if it's not set + var scopes string + if form.Scopes == "" { + scopes = "read" + } else { + scopes = form.Scopes + } + + // generate new IDs for this application and its associated client + clientID := uuid.NewString() + clientSecret := uuid.NewString() + vapidKey := uuid.NewString() + + // generate the application to put in the database + app := >smodel.Application{ + Name: form.ClientName, + Website: form.Website, + RedirectURI: form.RedirectURIs, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopes, + VapidKey: vapidKey, + } + + // chuck it in the db + if _, err := a.conn.Model(app).Insert(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // now we need to model an oauth client from the application that the oauth library can use + oc := &oauthClient{ + ID: clientID, + Secret: clientSecret, + Domain: form.RedirectURIs, + UserID: "", // This client isn't yet associated with a specific user, it's just an app client right now + } + + // chuck it in the db + if _, err := a.conn.Model(oc).Insert(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // done, return the new app information per the spec here: https://docs.joinmastodon.org/methods/apps/ + c.JSON(http.StatusOK, app) +} + // SignInGETHandler should be served at https://example.org/auth/sign_in. // The idea is to present a sign in page to the user, where they can enter their username and password. // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler From d0e6625d6e2a1aac1f68ea0808abb3b1b711a8e5 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 20 Mar 2021 19:06:28 +0100 Subject: [PATCH 13/15] tidying up --- internal/oauth/oauth.go | 40 ++++++++++++++++++++++++------------ internal/oauth/oauth_test.go | 14 +++++++------ web/template/authorize.tmpl | 11 ++++++++++ 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index 39fe2ddce..49e04a905 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -19,12 +19,14 @@ package oauth import ( + "fmt" "net/http" "net/url" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/go-pg/pg/v10" + "github.com/google/uuid" "github.com/gotosocial/gotosocial/internal/api" "github.com/gotosocial/gotosocial/internal/gtsmodel" "github.com/gotosocial/gotosocial/pkg/mastotypes" @@ -102,6 +104,8 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L } func (a *API) AddRoutes(s api.Server) error { + s.AttachHandler(http.MethodPost, "/api/v1/apps", a.AppsPOSTHandler) + s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler) s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler) @@ -110,7 +114,6 @@ func (a *API) AddRoutes(s api.Server) error { s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeGETHandler) s.AttachHandler(http.MethodPost, "/oauth/authorize", a.AuthorizePOSTHandler) - // s.AttachHandler(http.MethodGet, "/auth", a.AuthGETHandler) return nil } @@ -260,18 +263,21 @@ func (a *API) AuthorizeGETHandler(c *gin.Context) { l := a.log.WithField("func", "AuthorizeGETHandler") s := sessions.Default(c) + // Username will be set in the session by AuthorizePOSTHandler if the caller has already gone through the authentication flow + // If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page. v := s.Get("username") if username, ok := v.(string); !ok || username == "" { l.Trace("username was empty, parsing form then redirecting to sign in page") + // first make sure they've filled out the authorize form with the required values form := &mastotypes.OAuthAuthorize{} - if err := c.ShouldBind(form); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } l.Tracef("parsed form: %+v", form) + // these fields are *required* so check 'em if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing one of: response_type, client_id or redirect_uri"}) return @@ -288,18 +294,26 @@ func (a *API) AuthorizeGETHandler(c *gin.Context) { return } + // send them to the sign in page so we can tell who they are c.Redirect(http.StatusFound, "/auth/sign_in") return } + // Check if we have a code already. If we do, it means the user used urn:ietf:wg:oauth:2.0:oob as their redirect URI + // and were sent here, which means they just want the code displayed so they can use it out of band. code := &code{} - if err := c.Bind(code); err != nil || code.Code == "" { - // no code yet, serve auth html and let the user confirm - l.Trace("serving authorize html") - c.HTML(http.StatusOK, "authorize.tmpl", gin.H{}) + if err := c.Bind(code); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.String(http.StatusOK, code.Code) + + // the authorize template will either: + // 1. Display the code to the user if they're already authorized and were redirected here because they selected urn:ietf:wg:oauth:2.0:oob. + // 2. Display a form where they can get some information about the app that's trying to authorize, and approve it, which will then go to AuthorizePOSTHandler + l.Trace("serving authorize html") + c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ + "code": code.Code, + }) } // AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize @@ -417,16 +431,16 @@ func (a *API) ValidatePassword(email string, password string) (userid string, er return } -// UserAuthorizationHandler gets the user's email address from the form key 'username' +// UserAuthorizationHandler gets the user's ID from the 'username' field of the request form, // or redirects to the /auth/sign_in page, if this key is not present. -func (a *API) UserAuthorizationHandler(w http.ResponseWriter, r *http.Request) (username string, err error) { +func (a *API) UserAuthorizationHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) { l := a.log.WithField("func", "UserAuthorizationHandler") - username = r.FormValue("username") - if username == "" { + userID = r.FormValue("username") + if userID == "" { l.Trace("username was empty, redirecting to sign in page") http.Redirect(w, r, "/auth/sign_in", http.StatusFound) return "", nil } - l.Tracef("returning (%s, %s)", username, err) - return username, err + l.Tracef("returning (%s, %s)", userID, err) + return userID, err } diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go index 0c786456a..2e5be121b 100644 --- a/internal/oauth/oauth_test.go +++ b/internal/oauth/oauth_test.go @@ -7,7 +7,6 @@ "github.com/go-pg/pg/v10" "github.com/go-pg/pg/v10/orm" - "github.com/google/uuid" "github.com/gotosocial/gotosocial/internal/api" "github.com/gotosocial/gotosocial/internal/config" "github.com/gotosocial/gotosocial/internal/gtsmodel" @@ -22,6 +21,7 @@ type OauthTestSuite struct { tokenStore oauth2.TokenStore clientStore oauth2.ClientStore conn *pg.DB + testAccount *gtsmodel.Account testUser *gtsmodel.User testClient *oauthClient config *config.Config @@ -36,20 +36,18 @@ func (suite *OauthTestSuite) SetupSuite() { logrus.Panicf("error encrypting user pass: %s", err) } - userID := uuid.NewString() + suite.testAccount = >smodel.Account{ + + } suite.testUser = >smodel.User{ - ID: userID, EncryptedPassword: string(encryptedPassword), Email: "user@localhost", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), AccountID: "some-account-id-it-doesn't-matter-really-since-this-user-doesn't-actually-have-an-account!", } suite.testClient = &oauthClient{ ID: "a-known-client-id", Secret: "some-secret", Domain: "http://localhost:8080", - UserID: userID, } // because go tests are run within the test package directory, we need to fiddle with the templateconfig @@ -71,6 +69,8 @@ func (suite *OauthTestSuite) SetupTest() { &oauthClient{}, &oauthToken{}, >smodel.User{}, + >smodel.Account{}, + >smodel.Application{}, } for _, m := range models { @@ -100,6 +100,8 @@ func (suite *OauthTestSuite) TearDownTest() { &oauthClient{}, &oauthToken{}, >smodel.User{}, + >smodel.Account{}, + >smodel.Application{}, } for _, m := range models { if err := suite.conn.Model(m).DropTable(&orm.DropTableOptions{}); err != nil { diff --git a/web/template/authorize.tmpl b/web/template/authorize.tmpl index 0043e21ba..2b3b3ab07 100644 --- a/web/template/authorize.tmpl +++ b/web/template/authorize.tmpl @@ -11,6 +11,7 @@ +{{if len .code | eq 0 }}
@@ -30,4 +31,14 @@
+{{else}} + +
+
+ {{.code}} +
+
+ +{{end}} + From 044c0df42813697a0923e6de87ac8e2243d6025c Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 20 Mar 2021 19:08:17 +0100 Subject: [PATCH 14/15] go fmt --- internal/gtsmodel/account.go | 2 +- internal/gtsmodel/user.go | 10 +++++----- internal/oauth/oauth_test.go | 4 +--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 7bc8118a3..67860146e 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -121,7 +121,7 @@ type Account struct { // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account) SuspendedAt time.Time `pg:"type:timestamp"` // How much do we trust this account 🤔 - TrustLevel int + TrustLevel int // Should we hide this account's collections? HideCollections bool // id of the user that suspended this account through an admin action diff --git a/internal/gtsmodel/user.go b/internal/gtsmodel/user.go index c105899b8..551cbe2a4 100644 --- a/internal/gtsmodel/user.go +++ b/internal/gtsmodel/user.go @@ -112,9 +112,9 @@ type User struct { EncryptedOTPSecretSalt string OTPRequiredForLogin bool OTPBackupCodes []string - ConsumedTimestamp int - RememberToken string - SignInToken string - SignInTokenSentAt time.Time `pg:"type:timestamp"` - WebauthnID string + ConsumedTimestamp int + RememberToken string + SignInToken string + SignInTokenSentAt time.Time `pg:"type:timestamp"` + WebauthnID string } diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go index 2e5be121b..9ee5ac9a8 100644 --- a/internal/oauth/oauth_test.go +++ b/internal/oauth/oauth_test.go @@ -36,9 +36,7 @@ func (suite *OauthTestSuite) SetupSuite() { logrus.Panicf("error encrypting user pass: %s", err) } - suite.testAccount = >smodel.Account{ - - } + suite.testAccount = >smodel.Account{} suite.testUser = >smodel.User{ EncryptedPassword: string(encryptedPassword), Email: "user@localhost", From 3388c870aedd5d61157c6551416ddee7446a6191 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 20 Mar 2021 19:08:25 +0100 Subject: [PATCH 15/15] Go mod tidy --- go.mod | 3 +-- go.sum | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 473ff3ea8..4d13b11d8 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,8 @@ require ( github.com/go-fed/activity v1.0.0 github.com/go-pg/pg/extra/pgdebug v0.2.0 github.com/go-pg/pg/v10 v10.8.0 - github.com/go-session/session v3.1.2+incompatible github.com/golang/mock v1.4.4 // indirect - github.com/google/uuid v1.2.0 // indirect + github.com/google/uuid v1.2.0 github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3 github.com/onsi/ginkgo v1.15.0 // indirect github.com/onsi/gomega v1.10.5 // indirect diff --git a/go.sum b/go.sum index 0b12241f0..c7f3098c8 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,6 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg= -github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -105,12 +103,6 @@ github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9R github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88 h1:YJ//HmHOYJ4srm/LA6VPNjNisneMbY6TTM1xttV/ZQU= -github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= -github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132047-b7df44000ea6 h1:mWWMTK2Boy6FSCi45WB6GVCcXW3IoTVJKJiHmmdjywU= -github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132047-b7df44000ea6/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= -github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132554-68b81fe90e62 h1:duqoA9NSY+BFY2IVveXx5lSvIQliVvPsaNMdspkTJPc= -github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132554-68b81fe90e62/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3 h1:CKRz5d7mRum+UMR88Ue33tCYcej14WjUsB59C02DDqY= github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=