mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-28 18:10:30 +00:00
Some more messing around with oauth2
This commit is contained in:
parent
a4b70269ba
commit
eb2ff2ab23
6 changed files with 341 additions and 12 deletions
3
go.mod
3
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
68
internal/oauth/html.go
Normal file
68
internal/oauth/html.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package oauth
|
||||
|
||||
const (
|
||||
signInHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
|
||||
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Login</h1>
|
||||
<form action="/auth/sign_in" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="text" class="form-control" name="username" required placeholder="Please enter your email address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" name="password" placeholder="Please enter your password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>`
|
||||
|
||||
authorizeHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Auth</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
|
||||
/>
|
||||
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="jumbotron">
|
||||
<form action="/oauth/authorize" method="POST">
|
||||
<h1>Authorize</h1>
|
||||
<p>The client would like to perform actions on your behalf.</p>
|
||||
<p>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg"
|
||||
style="width:200px;"
|
||||
>
|
||||
Allow
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
)
|
|
@ -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 @@ import (
|
|||
"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
|
||||
}
|
||||
|
|
115
internal/oauth/oauth_test.go
Normal file
115
internal/oauth/oauth_test.go
Normal file
|
@ -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))
|
||||
}
|
Loading…
Reference in a new issue