From 6eab00e05e6e65a2b8fc738ae25be5fa6c5283b3 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Wed, 17 Mar 2021 16:01:31 +0100 Subject: [PATCH] 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 @@ import ( "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 @@ import ( 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 @@ package oauth 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 @@ package oauth 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"` }