mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 10:22:25 -05: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…
	
	Add table
		Add a link
		
	
		Reference in a new issue