mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:22:25 -05:00 
			
		
		
		
	auth flow working for code
This commit is contained in:
		
					parent
					
						
							
								b6087cc08d
							
						
					
				
			
			
				commit
				
					
						1b11884121
					
				
			
		
					 14 changed files with 275 additions and 129 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -3,3 +3,6 @@ | ||||||
| 
 | 
 | ||||||
| # exclude built documentation, since readthedocs will build it for us anyway | # exclude built documentation, since readthedocs will build it for us anyway | ||||||
| /docs/_build | /docs/_build | ||||||
|  | 
 | ||||||
|  | # exclude coverage report | ||||||
|  | cp.out | ||||||
|  |  | ||||||
|  | @ -95,6 +95,14 @@ func main() { | ||||||
| 				Value:   "postgres", | 				Value:   "postgres", | ||||||
| 				EnvVars: []string{envNames.DbDatabase}, | 				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{ | 		Commands: []*cli.Command{ | ||||||
| 			{ | 			{ | ||||||
|  | @ -137,12 +145,12 @@ func main() { | ||||||
| func runAction(c *cli.Context, a action.GTSAction) error { | func runAction(c *cli.Context, a action.GTSAction) error { | ||||||
| 
 | 
 | ||||||
| 	// create a new *config.Config based on the config path provided... | 	// 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 { | 	if err != nil { | ||||||
| 		return fmt.Errorf("error creating config: %s", err) | 		return fmt.Errorf("error creating config: %s", err) | ||||||
| 	} | 	} | ||||||
| 	// ... and the flags set on the *cli.Context by urfave | 	// ... 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 | 	// create a logger with the log level, formatting, and output splitter already set | ||||||
| 	log, err := log.New(conf.LogLevel) | 	log, err := log.New(conf.LogLevel) | ||||||
|  |  | ||||||
|  | @ -60,3 +60,10 @@ db: | ||||||
|   # Examples: ["mydb","postgres","gotosocial"] |   # Examples: ["mydb","postgres","gotosocial"] | ||||||
|   # Default: "postgres" |   # Default: "postgres" | ||||||
|   database: "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/" | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -11,7 +11,7 @@ require ( | ||||||
| 	github.com/go-session/session v3.1.2+incompatible | 	github.com/go-session/session v3.1.2+incompatible | ||||||
| 	github.com/golang/mock v1.4.4 // indirect | 	github.com/golang/mock v1.4.4 // indirect | ||||||
| 	github.com/google/uuid v1.2.0 // 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/ginkgo v1.15.0 // indirect | ||||||
| 	github.com/onsi/gomega v1.10.5 // indirect | 	github.com/onsi/gomega v1.10.5 // indirect | ||||||
| 	github.com/sirupsen/logrus v1.8.0 | 	github.com/sirupsen/logrus v1.8.0 | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								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/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 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.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/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 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= | ||||||
| github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= | ||||||
|  |  | ||||||
|  | @ -19,6 +19,10 @@ | ||||||
| package api | package api | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 
 | ||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-contrib/sessions/memstore" | 	"github.com/gin-contrib/sessions/memstore" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | @ -71,6 +75,10 @@ func New(config *config.Config, logger *logrus.Logger) Server { | ||||||
| 	engine := gin.New() | 	engine := gin.New() | ||||||
| 	store := memstore.NewStore([]byte("authentication-key"), []byte("encryption-keyencryption-key----")) | 	store := memstore.NewStore([]byte("authentication-key"), []byte("encryption-keyencryption-key----")) | ||||||
| 	engine.Use(sessions.Sessions("gotosocial-session", store)) | 	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{ | 	return &server{ | ||||||
| 		APIGroup: engine.Group("/api").Group("/v1"), | 		APIGroup: engine.Group("/api").Group("/v1"), | ||||||
| 		logger:   logger, | 		logger:   logger, | ||||||
|  |  | ||||||
|  | @ -30,22 +30,35 @@ type Config struct { | ||||||
| 	LogLevel        string          `yaml:"logLevel"` | 	LogLevel        string          `yaml:"logLevel"` | ||||||
| 	ApplicationName string          `yaml:"applicationName"` | 	ApplicationName string          `yaml:"applicationName"` | ||||||
| 	DBConfig        *DBConfig       `yaml:"db"` | 	DBConfig        *DBConfig       `yaml:"db"` | ||||||
|  | 	TemplateConfig  *TemplateConfig `yaml:"template"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // New returns a new config, or an error if something goes amiss. | // FromFile returns a new config from a file, or an error if something goes amiss. | ||||||
| // The path parameter is optional, for loading a configuration json from the given path. | func FromFile(path string) (*Config, error) { | ||||||
| func New(path string) (*Config, error) { | 	c, err := loadFromFile(path) | ||||||
| 	config := &Config{ | 	if err != nil { | ||||||
| 		DBConfig: &DBConfig{}, |  | ||||||
| 	} |  | ||||||
| 	if path != "" { |  | ||||||
| 		var err error |  | ||||||
| 		if config, err = loadFromFile(path); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("error creating config: %s", err) | 		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 | // 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 | 	return config, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ParseFlags sets flags on the config using the provided Flags object | // ParseCLIFlags sets flags on the config using the provided Flags object | ||||||
| func (c *Config) ParseFlags(f KeyedFlags) { | func (c *Config) ParseCLIFlags(f KeyedFlags) { | ||||||
| 	fn := GetFlagNames() | 	fn := GetFlagNames() | ||||||
| 
 | 
 | ||||||
| 	// For all of these flags, we only want to set them on the config if: | 	// 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) { | 	if c.DBConfig.Database == "" || f.IsSet(fn.DbDatabase) { | ||||||
| 		c.DBConfig.Database = f.String(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. | // 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 | 	DbUser          string | ||||||
| 	DbPassword      string | 	DbPassword      string | ||||||
| 	DbDatabase      string | 	DbDatabase      string | ||||||
|  | 	TemplateBaseDir string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetFlagNames returns a struct containing the names of the various flags used for | // GetFlagNames returns a struct containing the names of the various flags used for | ||||||
|  | @ -145,6 +164,7 @@ func GetFlagNames() Flags { | ||||||
| 		DbUser:          "db-user", | 		DbUser:          "db-user", | ||||||
| 		DbPassword:      "db-password", | 		DbPassword:      "db-password", | ||||||
| 		DbDatabase:      "db-database", | 		DbDatabase:      "db-database", | ||||||
|  | 		TemplateBaseDir: "template-basedir", | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -161,5 +181,6 @@ func GetEnvNames() Flags { | ||||||
| 		DbUser:          "GTS_DB_USER", | 		DbUser:          "GTS_DB_USER", | ||||||
| 		DbPassword:      "GTS_DB_PASSWORD", | 		DbPassword:      "GTS_DB_PASSWORD", | ||||||
| 		DbDatabase:      "GTS_DB_DATABASE", | 		DbDatabase:      "GTS_DB_DATABASE", | ||||||
|  | 		TemplateBaseDir: "GTS_TEMPLATE_BASEDIR", | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										25
									
								
								internal/config/template.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								internal/config/template.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | 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"` | ||||||
|  | } | ||||||
|  | @ -2,67 +2,8 @@ package oauth | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	signInHTML = ` | 	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 = ` | 	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>` |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -27,6 +27,7 @@ import ( | ||||||
| 	"github.com/go-pg/pg/v10" | 	"github.com/go-pg/pg/v10" | ||||||
| 	"github.com/gotosocial/gotosocial/internal/api" | 	"github.com/gotosocial/gotosocial/internal/api" | ||||||
| 	"github.com/gotosocial/gotosocial/internal/gtsmodel" | 	"github.com/gotosocial/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/gotosocial/gotosocial/pkg/mastotypes" | ||||||
| 	"github.com/gotosocial/oauth2/v4" | 	"github.com/gotosocial/oauth2/v4" | ||||||
| 	"github.com/gotosocial/oauth2/v4/errors" | 	"github.com/gotosocial/oauth2/v4/errors" | ||||||
| 	"github.com/gotosocial/oauth2/v4/manage" | 	"github.com/gotosocial/oauth2/v4/manage" | ||||||
|  | @ -49,12 +50,8 @@ type login struct { | ||||||
| 	Password string `form:"password"` | 	Password string `form:"password"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type authorize struct { | type code struct { | ||||||
| 	ForceLogin   string `form:"force_login,omitempty"` | 	Code string `form:"code"` | ||||||
| 	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 { | 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.AuthorizationCode, | ||||||
| 			oauth2.Refreshing, | 			oauth2.Refreshing, | ||||||
| 		}, | 		}, | ||||||
|  | 		AllowedCodeChallengeMethods: []oauth2.CodeChallengeMethod{ | ||||||
|  | 			oauth2.CodeChallengePlain, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	srv := server.NewServer(sc, manager) | 	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 { | func (a *API) AddRoutes(s api.Server) error { | ||||||
| 	s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler) | 	s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler) | ||||||
| 	s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler) | 	s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler) | ||||||
|  | 
 | ||||||
| 	s.AttachHandler(http.MethodPost, "/oauth/token", a.TokenHandler) | 	s.AttachHandler(http.MethodPost, "/oauth/token", a.TokenHandler) | ||||||
|  | 
 | ||||||
| 	s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeGETHandler) | 	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 | 	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 | // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler | ||||||
| func (a *API) SignInGETHandler(c *gin.Context) { | func (a *API) SignInGETHandler(c *gin.Context) { | ||||||
| 	a.log.WithField("func", "SignInGETHandler").Trace("serving sign in html") | 	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. | // 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") | 	l := a.log.WithField("func", "SignInPOSTHandler") | ||||||
| 	s := sessions.Default(c) | 	s := sessions.Default(c) | ||||||
| 	form := &login{} | 	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()}) | 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("parsed form: %+v", form) | 	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 { | 	if err != nil { | ||||||
| 		c.String(http.StatusForbidden, err.Error()) | 		c.String(http.StatusForbidden, err.Error()) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	s.Set("username", userid) | 	s.Set("username", userid) | ||||||
|  | @ -151,11 +156,12 @@ func (a *API) SignInPOSTHandler(c *gin.Context) { | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	l.Trace("redirecting to auth page") | 	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. | // 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 | // See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token | ||||||
| func (a *API) TokenHandler(c *gin.Context) { | func (a *API) TokenHandler(c *gin.Context) { | ||||||
|  | @ -166,13 +172,18 @@ 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 | // 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 | // 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) { | func (a *API) AuthorizeGETHandler(c *gin.Context) { | ||||||
| 	l := a.log.WithField("func", "AuthorizeHandler") | 	l := a.log.WithField("func", "AuthorizeGETHandler") | ||||||
| 	s := sessions.Default(c) | 	s := sessions.Default(c) | ||||||
| 	form := &authorize{} | 
 | ||||||
|  | 	v := s.Get("username") | ||||||
|  | 	if username, ok := v.(string); !ok || username == "" { | ||||||
|  | 		l.Trace("username was empty, parsing form then redirecting to sign in page") | ||||||
|  | 
 | ||||||
|  | 		form := &mastotypes.OAuthAuthorize{} | ||||||
| 
 | 
 | ||||||
| 		if err := c.ShouldBind(form); err != nil { | 		if err := c.ShouldBind(form); err != nil { | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||||
|  | @ -185,6 +196,7 @@ func (a *API) AuthorizeGETHandler(c *gin.Context) { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		// save these values from the form so we can use them elsewhere in the session | ||||||
| 		s.Set("force_login", form.ForceLogin) | 		s.Set("force_login", form.ForceLogin) | ||||||
| 		s.Set("response_type", form.ResponseType) | 		s.Set("response_type", form.ResponseType) | ||||||
| 		s.Set("client_id", form.ClientID) | 		s.Set("client_id", form.ClientID) | ||||||
|  | @ -192,24 +204,29 @@ func (a *API) AuthorizeGETHandler(c *gin.Context) { | ||||||
| 		s.Set("scope", form.Scope) | 		s.Set("scope", form.Scope) | ||||||
| 		if err := s.Save(); err != nil { | 		if err := s.Save(); err != nil { | ||||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||||
|  | 			return | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 	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") | 		c.Redirect(http.StatusFound, "/auth/sign_in") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Trace("serving authorize html") | 	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 | // AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize | ||||||
| func (a *API) AuthHandler(c *gin.Context) { | // The idea here is to present an oauth authorize page to the user, with a button | ||||||
| 	l := a.log.WithField("func", "AuthHandler") | // 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) | 	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{} | 	values := url.Values{} | ||||||
| 
 | 
 | ||||||
| 	if v, ok := s.Get("force_login").(string); !ok { | 	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. | // 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) { | func (a *API) ValidatePassword(email string, password string) (userid string, err error) { | ||||||
| 	l := a.log.WithField("func", "PasswordAuthorizationHandler") | 	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 | 	// first we select the user from the database based on email address, bail if no user found for that email | ||||||
| 	gtsUser := >smodel.User{} | 	gtsUser := >smodel.User{} | ||||||
| 	if err := a.conn.Model(gtsUser).Where("email = ?", email).Select(); err != nil { | 	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() | 		return incorrectPassword() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// If we've made it this far the email/password is correct so we need the oauth client-id of the user | 	// If we've made it this far the email/password is correct, so we can just return the id of the user. | ||||||
| 	// This is, conveniently, the same as the user ID, so we can just return it. |  | ||||||
| 	userid = gtsUser.ID | 	userid = gtsUser.ID | ||||||
| 	l.Tracef("returning (%s, %s)", userid, err) | 	l.Tracef("returning (%s, %s)", userid, err) | ||||||
| 	return | 	return | ||||||
|  |  | ||||||
|  | @ -40,17 +40,24 @@ func (suite *OauthTestSuite) SetupSuite() { | ||||||
| 	suite.testUser = >smodel.User{ | 	suite.testUser = >smodel.User{ | ||||||
| 		ID:                userID, | 		ID:                userID, | ||||||
| 		EncryptedPassword: string(encryptedPassword), | 		EncryptedPassword: string(encryptedPassword), | ||||||
| 		Email:             "user@localhost", | 		Email:             "user@example.org", | ||||||
| 		CreatedAt:         time.Now(), | 		CreatedAt:         time.Now(), | ||||||
| 		UpdatedAt:         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{ | 	suite.testClient = &oauthClient{ | ||||||
| 		ID:     "a-known-client-id", | 		ID:     "a-known-client-id", | ||||||
| 		Secret: "some-secret", | 		Secret: "some-secret", | ||||||
| 		Domain: "http://localhost:8080", | 		Domain: "https://example.org", | ||||||
| 		UserID: userID, | 		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 | // 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) | 	api.AddRoutes(r) | ||||||
| 	go r.Start() | 	go r.Start() | ||||||
| 	time.Sleep(30 * time.Second) | 	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) { | func TestOauthTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
							
								
								
									
										37
									
								
								pkg/mastotypes/oauth.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								pkg/mastotypes/oauth.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | 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"` | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								web/template/authorize.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/template/authorize.tmpl
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | <!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> | ||||||
							
								
								
									
										28
									
								
								web/template/sign-in.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								web/template/sign-in.tmpl
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | <!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> | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue