diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts_table.go b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts_table.go index aa98d8dd..891fce5b 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts_table.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts_table.go @@ -41,10 +41,10 @@ const insertAccountSQL = "" + "INSERT INTO accounts(localpart, created_ts, password_hash) VALUES ($1, $2, $3)" const selectAccountByLocalpartSQL = "" + - "SELECT localpart WHERE localpart = $1" + "SELECT localpart FROM accounts WHERE localpart = $1" const selectPasswordHashSQL = "" + - "SELECT password_hash WHERE localpart = $1" + "SELECT password_hash FROM accounts WHERE localpart = $1" // TODO: Update password @@ -78,10 +78,11 @@ func (s *accountsStatements) prepare(db *sql.DB, server gomatrixserverlib.Server // on success. func (s *accountsStatements) insertAccount(localpart, hash string) (acc *types.Account, err error) { createdTimeMS := time.Now().UnixNano() / 1000000 - if _, err = s.insertAccountStmt.Exec(localpart, createdTimeMS, hash); err != nil { + if _, err = s.insertAccountStmt.Exec(localpart, createdTimeMS, hash); err == nil { acc = &types.Account{ - Localpart: localpart, - UserID: makeUserID(localpart, s.serverName), + Localpart: localpart, + UserID: makeUserID(localpart, s.serverName), + ServerName: s.serverName, } } return @@ -97,6 +98,7 @@ func (s *accountsStatements) selectAccountByLocalpart(localpart string) (*types. err := s.selectAccountByLocalpartStmt.QueryRow(localpart).Scan(&acc.Localpart) if err != nil { acc.UserID = makeUserID(localpart, s.serverName) + acc.ServerName = s.serverName } return &acc, err } diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/storage.go b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/storage.go index 22001ca8..19bd3d93 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/storage.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/storage.go @@ -19,6 +19,8 @@ import ( "github.com/matrix-org/dendrite/clientapi/auth/types" "github.com/matrix-org/gomatrixserverlib" "golang.org/x/crypto/bcrypt" + // Import the postgres database driver. + _ "github.com/lib/pq" ) // AccountDatabase represents an account database diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/types/account.go b/src/github.com/matrix-org/dendrite/clientapi/auth/types/account.go index a6d0a834..3317d4d6 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/types/account.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/types/account.go @@ -14,10 +14,15 @@ package types +import ( + "github.com/matrix-org/gomatrixserverlib" +) + // Account represents a Matrix account on this home server. type Account struct { - UserID string - Localpart string + UserID string + Localpart string + ServerName gomatrixserverlib.ServerName // TODO: Other flags like IsAdmin, IsGuest // TODO: Device IDs // TODO: Associations (e.g. with application services) diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/types/logintypes.go b/src/github.com/matrix-org/dendrite/clientapi/auth/types/logintypes.go new file mode 100644 index 00000000..42cb477a --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/types/logintypes.go @@ -0,0 +1,9 @@ +package types + +// LoginType are specified by http://matrix.org/docs/spec/client_server/r0.2.0.html#login-types +type LoginType string + +// The relevant login types implemented in Dendrite +const ( + LoginTypeDummy = "m.login.dummy" +) diff --git a/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go b/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go index 4f2dce84..b271eeb6 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go +++ b/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go @@ -79,6 +79,12 @@ func UnknownToken(msg string) *MatrixError { return &MatrixError{"M_UNKNOWN_TOKEN", msg} } +// WeakPassword is an error which is returned when the client tries to register +// using a weak password. http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based +func WeakPassword(msg string) *MatrixError { + return &MatrixError{"M_WEAK_PASSWORD", msg} +} + // LimitExceededError is a rate-limiting error. type LimitExceededError struct { MatrixError diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index 5ad9b2bb..0e04dba8 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -19,6 +19,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/clientapi/auth/storage" "github.com/matrix-org/dendrite/clientapi/config" "github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/readers" @@ -32,7 +33,7 @@ const pathPrefixR0 = "/_matrix/client/r0" // Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client // to clients which need to make outbound HTTP requests. -func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg config.ClientAPI, producer *producers.RoomserverProducer, queryAPI api.RoomserverQueryAPI) { +func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg config.ClientAPI, producer *producers.RoomserverProducer, queryAPI api.RoomserverQueryAPI, accountDB *storage.AccountDatabase) { apiMux := mux.NewRouter() r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() r0mux.Handle("/createRoom", @@ -61,6 +62,10 @@ func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg config.ClientAPI }), ) + r0mux.Handle("/register", makeAPI("register", func(req *http.Request) util.JSONResponse { + return writers.Register(req, accountDB) + })) + // Stub endpoints required by Riot r0mux.Handle("/login", diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/register.go b/src/github.com/matrix-org/dendrite/clientapi/writers/register.go new file mode 100644 index 00000000..c2a9760b --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/register.go @@ -0,0 +1,162 @@ +package writers + +import ( + "fmt" + "net/http" + + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/dendrite/clientapi/auth/storage" + "github.com/matrix-org/dendrite/clientapi/auth/types" + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" +) + +const ( + minPasswordLength = 8 // http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based + maxPasswordLength = 512 // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161 + maxUsernameLength = 254 // http://matrix.org/speculator/spec/HEAD/intro.html#user-identifiers TODO account for domain +) + +// registerRequest represents the submitted registration request. +// It can be broken down into 2 sections: the auth dictionary and registration parameters. +// Registration parameters vary depending on the request, and will need to remembered across +// sessions. If no parameters are supplied, the server should use the parameters previously +// remembered. If ANY parameters are supplied, the server should REPLACE all knowledge of +// previous paramters with the ones supplied. This mean you cannot "build up" request params. +type registerRequest struct { + // registration parameters. + Password string `json:"password"` + Username string `json:"username"` + // user-interactive auth params + Auth authDict `json:"auth"` +} + +type authDict struct { + Type types.LoginType `json:"type"` + Session string `json:"session"` + // TODO: Lots of custom keys depending on the type +} + +// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api +type userInteractiveResponse struct { + Flows []authFlow `json:"flows"` + Completed []types.LoginType `json:"completed"` + Params map[string]interface{} `json:"params"` + Session string `json:"session"` +} + +// authFlow represents one possible way that the client can authenticate a request. +// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api +type authFlow struct { + Stages []types.LoginType `json:"stages"` +} + +func newUserInteractiveResponse(sessionID string, fs []authFlow) userInteractiveResponse { + return userInteractiveResponse{ + fs, []types.LoginType{}, make(map[string]interface{}), sessionID, + } +} + +// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register +type registerResponse struct { + UserID string `json:"user_id"` + AccessToken string `json:"access_token"` + HomeServer gomatrixserverlib.ServerName `json:"home_server"` + DeviceID string `json:"device_id"` +} + +// Validate returns an error response if the request fails to validate. +func (r *registerRequest) Validate() *util.JSONResponse { + // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161 + if len(r.Password) > maxPasswordLength { + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON(fmt.Sprintf("'password' >%d characters", maxPasswordLength)), + } + } else if len(r.Username) > maxUsernameLength { + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON(fmt.Sprintf("'username' >%d characters", maxUsernameLength)), + } + } else if len(r.Password) > 0 && len(r.Password) < minPasswordLength { + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", minPasswordLength)), + } + } + return nil +} + +// Register processes a /register request. http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register +func Register(req *http.Request, accountDB *storage.AccountDatabase) util.JSONResponse { + var r registerRequest + resErr := httputil.UnmarshalJSONRequest(req, &r) + if resErr != nil { + return *resErr + } + if resErr = r.Validate(); resErr != nil { + return *resErr + } + + logger := util.GetLogger(req.Context()) + logger.WithFields(log.Fields{ + "username": r.Username, + "auth.type": r.Auth.Type, + "session_id": r.Auth.Session, + }).Info("Processing registration request") + + // TODO: Shared secret registration (create new user scripts) + // TODO: AS API registration + // TODO: Enable registration config flag + // TODO: Guest account upgrading + + // All registration requests must specify what auth they are using to perform this request + if r.Auth.Type == "" { + return util.JSONResponse{ + Code: 401, + // TODO: Hard-coded 'dummy' auth for now with a bogus session ID. + // Server admins should be able to change things around (eg enable captcha) + JSON: newUserInteractiveResponse("totallyuniquesessionid", []authFlow{ + {[]types.LoginType{types.LoginTypeDummy}}, + }), + } + } + + // TODO: Handle loading of previous session parameters from database. + // TODO: Handle mapping registrationRequest parameters into session parameters + + // TODO: email / msisdn / recaptcha auth types. + switch r.Auth.Type { + case types.LoginTypeDummy: + // there is nothing to do + return completeRegistration(accountDB, r.Username, r.Password) + default: + return util.JSONResponse{ + Code: 501, + JSON: jsonerror.Unknown("unknown/unimplemented auth type"), + } + } +} + +func completeRegistration(accountDB *storage.AccountDatabase, username, password string) util.JSONResponse { + acc, err := accountDB.CreateAccount(username, password) + if err != nil { + return util.JSONResponse{ + Code: 500, + JSON: jsonerror.Unknown("failed to create account: " + err.Error()), + } + } + // TODO: Make and store a proper access_token + // TODO: Store the client's device information? + return util.JSONResponse{ + Code: 200, + JSON: registerResponse{ + UserID: acc.UserID, + AccessToken: acc.UserID, // FIXME + HomeServer: acc.ServerName, + DeviceID: "kindauniquedeviceid", + }, + } +} diff --git a/src/github.com/matrix-org/dendrite/cmd/dendrite-client-api-server/main.go b/src/github.com/matrix-org/dendrite/cmd/dendrite-client-api-server/main.go index bf1bd9f6..1a1a1bac 100644 --- a/src/github.com/matrix-org/dendrite/cmd/dendrite-client-api-server/main.go +++ b/src/github.com/matrix-org/dendrite/cmd/dendrite-client-api-server/main.go @@ -19,6 +19,7 @@ import ( "os" "strings" + "github.com/matrix-org/dendrite/clientapi/auth/storage" "github.com/matrix-org/dendrite/clientapi/config" "github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/routing" @@ -37,6 +38,7 @@ var ( clientAPIOutputTopic = os.Getenv("CLIENTAPI_OUTPUT_TOPIC") serverName = gomatrixserverlib.ServerName(os.Getenv("SERVER_NAME")) serverKey = os.Getenv("SERVER_KEY") + accountDataSource = os.Getenv("ACCOUNT_DATABASE") ) func main() { @@ -79,7 +81,11 @@ func main() { } queryAPI := api.NewRoomserverQueryAPIHTTP(cfg.RoomserverURL, nil) + accountDB, err := storage.NewAccountDatabase(accountDataSource, serverName) + if err != nil { + log.Panicf("Failed to setup account database(%s): %s", accountDataSource, err.Error()) + } - routing.Setup(http.DefaultServeMux, http.DefaultClient, cfg, roomserverProducer, queryAPI) + routing.Setup(http.DefaultServeMux, http.DefaultClient, cfg, roomserverProducer, queryAPI, accountDB) log.Fatal(http.ListenAndServe(bindAddr, nil)) }