package clientapi import ( "crypto/rand" "errors" "net/http" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/userapi" ) // registerRequest is the body of POST /register. type registerRequest struct { Username string `json:"username"` Password string `json:"password"` DeviceID string `json:"device_id"` InitialDeviceDisplayName string `json:"initial_device_display_name"` // InhibitLogin asks for the account without a session, for a client // registering on someone else's behalf. InhibitLogin bool `json:"inhibit_login"` RefreshToken bool `json:"refresh_token"` Auth *uiaRequest `json:"auth"` } // registerResponse is the body of a successful POST /register. type registerResponse struct { UserID string `json:"user_id"` // The session fields are absent when inhibit_login was set. AccessToken string `json:"access_token,omitempty"` DeviceID string `json:"device_id,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` ExpiresInMS int64 `json:"expires_in_ms,omitempty"` // HomeServer is deprecated and still read by older clients, which // treat its absence as a malformed response. HomeServer string `json:"home_server,omitempty"` } // handleRegister implements POST /_matrix/client/v3/register. // // The order of the checks matters and is not the obvious one. The spec has // the registration flow start with a 401 listing the flows, so an empty // body must get the challenge rather than a complaint about the missing // password — but a username that is unusable or already taken has to be // rejected *before* the challenge, because a client that has to complete a // flow only to be told the name was taken cannot retry without starting // over. Complement checks both halves of that. func (c *ClientAPI) handleRegister(w http.ResponseWriter, r *http.Request) error { var req registerRequest if err := httputil.DecodeJSON(w, r, &req); err != nil { return err } // Guest registration needs the room-level restrictions that come with // the roomserver, so it is refused rather than half-implemented. if kind := r.URL.Query().Get("kind"); kind != "" && kind != "user" { if kind == "guest" { return httputil.Forbidden("Guest access is not supported by this server") } return httputil.InvalidParam("Unknown registration kind %q", kind) } if !c.settings.RegistrationEnabled() { return httputil.Forbidden("Registration is disabled on this server") } localpart := userapi.NormaliseLocalpart(req.Username) if localpart != "" { if err := userapi.ValidateLocalpart(localpart, c.serverName); err != nil { return httputil.InvalidUsername("%s", err.Error()) } taken, err := c.users.AccountExists(r.Context(), localpart) if err != nil { return err } if taken { return httputil.UserInUse() } } flows := c.registrationFlows() sessionID, done, err := c.beginOrAdvanceUIA(w, flows, req.Auth, c.registrationStage) if err != nil || !done { return err } // Only now that the client has authenticated is the rest of the body // worth validating. if req.Password == "" { return httputil.MissingParam("A password is required to register an account") } if len(req.Password) > userapi.MaxPasswordLength { return httputil.NewError(http.StatusBadRequest, httputil.ErrCodeWeakPassword, "Password must be at most %d bytes", userapi.MaxPasswordLength) } if req.DeviceID != "" { if err := userapi.ValidateDeviceID(req.DeviceID); err != nil { return httputil.InvalidParam("%s", err.Error()) } } if localpart == "" { // The spec lets a client leave the name to the server. if localpart, err = generateLocalpart(); err != nil { return err } } if _, err := c.users.CreateAccount(r.Context(), userapi.NewAccountRequest{ Localpart: localpart, Password: req.Password, }); err != nil { switch { case errors.Is(err, userapi.ErrUserExists): // Lost the race against another registration of the same name // between the check above and the insert. return httputil.UserInUse() case errors.Is(err, userapi.ErrInvalidLocalpart): return httputil.InvalidUsername("%s", err.Error()) default: return err } } // The flow has been acted on; a replay of the same request must not // create a second account. c.uia.remove(sessionID) resp := registerResponse{ UserID: c.users.UserID(localpart), HomeServer: c.serverName, } if req.InhibitLogin { httputil.WriteJSON(w, http.StatusOK, resp) return nil } session, err := c.users.CreateSession(r.Context(), userapi.SessionRequest{ Localpart: localpart, DeviceID: req.DeviceID, DisplayName: req.InitialDeviceDisplayName, WithRefreshToken: req.RefreshToken, IP: c.clientIP(r), UserAgent: r.UserAgent(), }) if err != nil { return err } resp.AccessToken = session.AccessToken resp.DeviceID = session.DeviceID resp.RefreshToken = session.RefreshToken resp.ExpiresInMS = session.ExpiresInMS httputil.WriteJSON(w, http.StatusOK, resp) return nil } // registrationFlows is what a client has to complete to register. // // m.login.dummy is a stage that is satisfied by being attempted. It exists // so that the flow is never empty: the spec's registration is always // interactive, and a server with no real requirements still has to offer // something for the client to complete. func (c *ClientAPI) registrationFlows() []uiaFlow { if c.registration.RequiresToken() { return []uiaFlow{{Stages: []string{stageRegistrationToken, stageDummy}}} } return []uiaFlow{{Stages: []string{stageDummy}}} } // registrationStage checks one stage of the registration flow. func (c *ClientAPI) registrationStage(stage string, auth *uiaRequest) (*httputil.MatrixError, error) { switch stage { case stageDummy: return nil, nil case stageRegistrationToken: if auth.Token == "" { return httputil.MissingParam("A registration token is required"), nil } if !c.registration.TokenValid(auth.Token) { return httputil.Forbidden("Invalid registration token"), nil } return nil, nil default: // beginOrAdvanceUIA only calls this for stages it offered, so // reaching here means the flow list and this switch disagree. return nil, httputil.InternalError() } } // handleRegisterAvailable implements GET /register/available, the live check // a client runs while the user is typing a name. func (c *ClientAPI) handleRegisterAvailable(w http.ResponseWriter, r *http.Request) error { username := r.URL.Query().Get("username") if username == "" { return httputil.MissingParam("The username query parameter is required") } localpart := userapi.NormaliseLocalpart(username) if err := userapi.ValidateLocalpart(localpart, c.serverName); err != nil { return httputil.InvalidUsername("%s", err.Error()) } taken, err := c.users.AccountExists(r.Context(), localpart) if err != nil { return err } if taken { return httputil.UserInUse() } httputil.WriteJSON(w, http.StatusOK, map[string]bool{"available": true}) return nil } // handleRegistrationTokenValidity implements // GET /_matrix/client/v1/register/m.login.registration_token/validity, which // lets a client tell the user their token is wrong before they fill in the // rest of the form. func (c *ClientAPI) handleRegistrationTokenValidity(w http.ResponseWriter, r *http.Request) error { token := r.URL.Query().Get("token") if token == "" { return httputil.MissingParam("The token query parameter is required") } httputil.WriteJSON(w, http.StatusOK, map[string]bool{"valid": c.registration.TokenValid(token)}) return nil } // generatedLocalpartAlphabet is the character set for a server-chosen user // name: lowercase letters and digits, which are valid everywhere in a // localpart and unambiguous when read out. const generatedLocalpartAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789" // generatedLocalpartLength gives roughly 62 bits of entropy, enough that a // server-generated name will not collide with an existing one in practice. const generatedLocalpartLength = 12 // generateLocalpart invents a user name for a registration that did not ask // for one. func generateLocalpart() (string, error) { raw := make([]byte, generatedLocalpartLength) if _, err := rand.Read(raw); err != nil { return "", err } out := make([]byte, generatedLocalpartLength) for i, b := range raw { out[i] = generatedLocalpartAlphabet[int(b)%len(generatedLocalpartAlphabet)] } return string(out), nil }