Implement dummy registration and hook it up to AccountDatabase
MODIFY
src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts_table.go
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),
+ 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
}

MODIFY
src/github.com/matrix-org/dendrite/clientapi/auth/storage/storage.go
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

MODIFY
src/github.com/matrix-org/dendrite/clientapi/auth/types/account.go
src/github.com/matrix-org/dendrite/clientapi/auth/types/account.go
package types
+import (
+ "github.com/matrix-org/gomatrixserverlib"
+)
+
// Account represents a Matrix account on this home server.
type Account struct {
UserID string
Localpart string
+ ServerName gomatrixserverlib.ServerName
// TODO: Other flags like IsAdmin, IsGuest
// TODO: Device IDs
// TODO: Associations (e.g. with application services)

ADD
src/github.com/matrix-org/dendrite/clientapi/auth/types/logintypes.go
+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"
+)

MODIFY
src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go
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

MODIFY
src/github.com/matrix-org/dendrite/clientapi/routing/routing.go
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",

ADD
src/github.com/matrix-org/dendrite/clientapi/writers/register.go
+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",
+ },
+ }
+}

MODIFY
src/github.com/matrix-org/dendrite/cmd/dendrite-client-api-server/main.go
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))
}

Implement dummy registration and hook it up to AccountDatabase (#113)