Implement dummy registration and hook it up to AccountDatabase (#113)
parent
6605333f6f
commit
0325459e7f
|
@ -41,10 +41,10 @@ const insertAccountSQL = "" +
|
||||||
"INSERT INTO accounts(localpart, created_ts, password_hash) VALUES ($1, $2, $3)"
|
"INSERT INTO accounts(localpart, created_ts, password_hash) VALUES ($1, $2, $3)"
|
||||||
|
|
||||||
const selectAccountByLocalpartSQL = "" +
|
const selectAccountByLocalpartSQL = "" +
|
||||||
"SELECT localpart WHERE localpart = $1"
|
"SELECT localpart FROM accounts WHERE localpart = $1"
|
||||||
|
|
||||||
const selectPasswordHashSQL = "" +
|
const selectPasswordHashSQL = "" +
|
||||||
"SELECT password_hash WHERE localpart = $1"
|
"SELECT password_hash FROM accounts WHERE localpart = $1"
|
||||||
|
|
||||||
// TODO: Update password
|
// TODO: Update password
|
||||||
|
|
||||||
|
@ -78,10 +78,11 @@ func (s *accountsStatements) prepare(db *sql.DB, server gomatrixserverlib.Server
|
||||||
// on success.
|
// on success.
|
||||||
func (s *accountsStatements) insertAccount(localpart, hash string) (acc *types.Account, err error) {
|
func (s *accountsStatements) insertAccount(localpart, hash string) (acc *types.Account, err error) {
|
||||||
createdTimeMS := time.Now().UnixNano() / 1000000
|
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{
|
acc = &types.Account{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
UserID: makeUserID(localpart, s.serverName),
|
UserID: makeUserID(localpart, s.serverName),
|
||||||
|
ServerName: s.serverName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -97,6 +98,7 @@ func (s *accountsStatements) selectAccountByLocalpart(localpart string) (*types.
|
||||||
err := s.selectAccountByLocalpartStmt.QueryRow(localpart).Scan(&acc.Localpart)
|
err := s.selectAccountByLocalpartStmt.QueryRow(localpart).Scan(&acc.Localpart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acc.UserID = makeUserID(localpart, s.serverName)
|
acc.UserID = makeUserID(localpart, s.serverName)
|
||||||
|
acc.ServerName = s.serverName
|
||||||
}
|
}
|
||||||
return &acc, err
|
return &acc, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/types"
|
"github.com/matrix-org/dendrite/clientapi/auth/types"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
// Import the postgres database driver.
|
||||||
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccountDatabase represents an account database
|
// AccountDatabase represents an account database
|
||||||
|
|
|
@ -14,10 +14,15 @@
|
||||||
|
|
||||||
package types
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
// Account represents a Matrix account on this home server.
|
// Account represents a Matrix account on this home server.
|
||||||
type Account struct {
|
type Account struct {
|
||||||
UserID string
|
UserID string
|
||||||
Localpart string
|
Localpart string
|
||||||
|
ServerName gomatrixserverlib.ServerName
|
||||||
// TODO: Other flags like IsAdmin, IsGuest
|
// TODO: Other flags like IsAdmin, IsGuest
|
||||||
// TODO: Device IDs
|
// TODO: Device IDs
|
||||||
// TODO: Associations (e.g. with application services)
|
// TODO: Associations (e.g. with application services)
|
||||||
|
|
|
@ -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"
|
||||||
|
)
|
|
@ -79,6 +79,12 @@ func UnknownToken(msg string) *MatrixError {
|
||||||
return &MatrixError{"M_UNKNOWN_TOKEN", msg}
|
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.
|
// LimitExceededError is a rate-limiting error.
|
||||||
type LimitExceededError struct {
|
type LimitExceededError struct {
|
||||||
MatrixError
|
MatrixError
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"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/config"
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/clientapi/readers"
|
"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
|
// Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client
|
||||||
// to clients which need to make outbound HTTP requests.
|
// 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()
|
apiMux := mux.NewRouter()
|
||||||
r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
|
r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
|
||||||
r0mux.Handle("/createRoom",
|
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
|
// Stub endpoints required by Riot
|
||||||
|
|
||||||
r0mux.Handle("/login",
|
r0mux.Handle("/login",
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/storage"
|
||||||
"github.com/matrix-org/dendrite/clientapi/config"
|
"github.com/matrix-org/dendrite/clientapi/config"
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/clientapi/routing"
|
"github.com/matrix-org/dendrite/clientapi/routing"
|
||||||
|
@ -37,6 +38,7 @@ var (
|
||||||
clientAPIOutputTopic = os.Getenv("CLIENTAPI_OUTPUT_TOPIC")
|
clientAPIOutputTopic = os.Getenv("CLIENTAPI_OUTPUT_TOPIC")
|
||||||
serverName = gomatrixserverlib.ServerName(os.Getenv("SERVER_NAME"))
|
serverName = gomatrixserverlib.ServerName(os.Getenv("SERVER_NAME"))
|
||||||
serverKey = os.Getenv("SERVER_KEY")
|
serverKey = os.Getenv("SERVER_KEY")
|
||||||
|
accountDataSource = os.Getenv("ACCOUNT_DATABASE")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -79,7 +81,11 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
queryAPI := api.NewRoomserverQueryAPIHTTP(cfg.RoomserverURL, nil)
|
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))
|
log.Fatal(http.ListenAndServe(bindAddr, nil))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue