diff options
author | ruben <code@rbn.im> | 2019-05-21 22:56:55 +0200 |
---|---|---|
committer | Brendan Abolivier <babolivier@matrix.org> | 2019-05-21 21:56:55 +0100 |
commit | 74827428bd3e11faab65f12204449c1b9469b0ae (patch) | |
tree | 0decafa542436a0667ed2d3e3cfd4df0f03de1e5 /clientapi/routing/register.go | |
parent | 4d588f7008afe5600219ac0930c2eee2de5c447b (diff) |
use go module for dependencies (#594)
Diffstat (limited to 'clientapi/routing/register.go')
-rw-r--r-- | clientapi/routing/register.go | 958 |
1 files changed, 958 insertions, 0 deletions
diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go new file mode 100644 index 00000000..b1522e82 --- /dev/null +++ b/clientapi/routing/register.go @@ -0,0 +1,958 @@ +// Copyright 2017 Vector Creations Ltd +// Copyright 2017 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routing + +import ( + "context" + "crypto/hmac" + "crypto/sha1" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/matrix-org/dendrite/common/config" + + "github.com/matrix-org/dendrite/clientapi/auth" + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" + "github.com/matrix-org/dendrite/clientapi/auth/storage/devices" + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/clientapi/userutil" + "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +var ( + // Prometheus metrics + amtRegUsers = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "dendrite_clientapi_reg_users_total", + Help: "Total number of registered users", + }, + ) +) + +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 + sessionIDLength = 24 +) + +func init() { + // Register prometheus metrics. They must be registered to be exposed. + prometheus.MustRegister(amtRegUsers) +} + +// sessionsDict keeps track of completed auth stages for each session. +type sessionsDict struct { + sessions map[string][]authtypes.LoginType +} + +// GetCompletedStages returns the completed stages for a session. +func (d sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType { + if completedStages, ok := d.sessions[sessionID]; ok { + return completedStages + } + // Ensure that a empty slice is returned and not nil. See #399. + return make([]authtypes.LoginType, 0) +} + +// AddCompletedStage records that a session has completed an auth stage. +func (d *sessionsDict) AddCompletedStage(sessionID string, stage authtypes.LoginType) { + d.sessions[sessionID] = append(d.GetCompletedStages(sessionID), stage) +} + +func newSessionsDict() *sessionsDict { + return &sessionsDict{ + sessions: make(map[string][]authtypes.LoginType), + } +} + +var ( + // TODO: Remove old sessions. Need to do so on a session-specific timeout. + // sessions stores the completed flow stages for all sessions. Referenced using their sessionID. + sessions = newSessionsDict() + validUsernameRegex = regexp.MustCompile(`^[0-9a-z_\-./]+$`) +) + +// 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 parameters 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"` + Admin bool `json:"admin"` + // user-interactive auth params + Auth authDict `json:"auth"` + + InitialDisplayName *string `json:"initial_device_display_name"` + + // Prevent this user from logging in + InhibitLogin common.WeakBoolean `json:"inhibit_login"` + + // Application Services place Type in the root of their registration + // request, whereas clients place it in the authDict struct. + Type authtypes.LoginType `json:"type"` +} + +type authDict struct { + Type authtypes.LoginType `json:"type"` + Session string `json:"session"` + Mac gomatrixserverlib.HexString `json:"mac"` + + // Recaptcha + Response string `json:"response"` + // 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 []authtypes.Flow `json:"flows"` + Completed []authtypes.LoginType `json:"completed"` + Params map[string]interface{} `json:"params"` + Session string `json:"session"` +} + +// legacyRegisterRequest represents the submitted registration request for v1 API. +type legacyRegisterRequest struct { + Password string `json:"password"` + Username string `json:"user"` + Admin bool `json:"admin"` + Type authtypes.LoginType `json:"type"` + Mac gomatrixserverlib.HexString `json:"mac"` +} + +// newUserInteractiveResponse will return a struct to be sent back to the client +// during registration. +func newUserInteractiveResponse( + sessionID string, + fs []authtypes.Flow, + params map[string]interface{}, +) userInteractiveResponse { + return userInteractiveResponse{ + fs, sessions.GetCompletedStages(sessionID), params, 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,omitempty"` + HomeServer gomatrixserverlib.ServerName `json:"home_server"` + DeviceID string `json:"device_id,omitempty"` +} + +// recaptchaResponse represents the HTTP response from a Google Recaptcha server +type recaptchaResponse struct { + Success bool `json:"success"` + ChallengeTS time.Time `json:"challenge_ts"` + Hostname string `json:"hostname"` + ErrorCodes []int `json:"error-codes"` +} + +// validateUsername returns an error response if the username is invalid +func validateUsername(username string) *util.JSONResponse { + // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161 + if len(username) > maxUsernameLength { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON(fmt.Sprintf("'username' >%d characters", maxUsernameLength)), + } + } else if !validUsernameRegex.MatchString(username) { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidUsername("Username can only contain characters a-z, 0-9, or '_-./'"), + } + } else if username[0] == '_' { // Regex checks its not a zero length string + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidUsername("Username cannot start with a '_'"), + } + } + return nil +} + +// validateApplicationServiceUsername returns an error response if the username is invalid for an application service +func validateApplicationServiceUsername(username string) *util.JSONResponse { + if len(username) > maxUsernameLength { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON(fmt.Sprintf("'username' >%d characters", maxUsernameLength)), + } + } else if !validUsernameRegex.MatchString(username) { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidUsername("Username can only contain characters a-z, 0-9, or '_-./'"), + } + } + return nil +} + +// validatePassword returns an error response if the password is invalid +func validatePassword(password string) *util.JSONResponse { + // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161 + if len(password) > maxPasswordLength { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON(fmt.Sprintf("'password' >%d characters", maxPasswordLength)), + } + } else if len(password) > 0 && len(password) < minPasswordLength { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", minPasswordLength)), + } + } + return nil +} + +// validateRecaptcha returns an error response if the captcha response is invalid +func validateRecaptcha( + cfg *config.Dendrite, + response string, + clientip string, +) *util.JSONResponse { + if !cfg.Matrix.RecaptchaEnabled { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON("Captcha registration is disabled"), + } + } + + if response == "" { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON("Captcha response is required"), + } + } + + // Make a POST request to Google's API to check the captcha response + resp, err := http.PostForm(cfg.Matrix.RecaptchaSiteVerifyAPI, + url.Values{ + "secret": {cfg.Matrix.RecaptchaPrivateKey}, + "response": {response}, + "remoteip": {clientip}, + }, + ) + + if err != nil { + return &util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: jsonerror.BadJSON("Error in requesting validation of captcha response"), + } + } + + // Close the request once we're finishing reading from it + defer resp.Body.Close() // nolint: errcheck + + // Grab the body of the response from the captcha server + var r recaptchaResponse + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return &util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: jsonerror.BadJSON("Error in contacting captcha server" + err.Error()), + } + } + err = json.Unmarshal(body, &r) + if err != nil { + return &util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: jsonerror.BadJSON("Error in unmarshaling captcha server's response: " + err.Error()), + } + } + + // Check that we received a "success" + if !r.Success { + return &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: jsonerror.BadJSON("Invalid captcha response. Please try again."), + } + } + return nil +} + +// UserIDIsWithinApplicationServiceNamespace checks to see if a given userID +// falls within any of the namespaces of a given Application Service. If no +// Application Service is given, it will check to see if it matches any +// Application Service's namespace. +func UserIDIsWithinApplicationServiceNamespace( + cfg *config.Dendrite, + userID string, + appservice *config.ApplicationService, +) bool { + if appservice != nil { + // Loop through given application service's namespaces and see if any match + for _, namespace := range appservice.NamespaceMap["users"] { + // AS namespaces are checked for validity in config + if namespace.RegexpObject.MatchString(userID) { + return true + } + } + return false + } + + // Loop through all known application service's namespaces and see if any match + for _, knownAppService := range cfg.Derived.ApplicationServices { + for _, namespace := range knownAppService.NamespaceMap["users"] { + // AS namespaces are checked for validity in config + if namespace.RegexpObject.MatchString(userID) { + return true + } + } + } + return false +} + +// UsernameMatchesMultipleExclusiveNamespaces will check if a given username matches +// more than one exclusive namespace. More than one is not allowed +func UsernameMatchesMultipleExclusiveNamespaces( + cfg *config.Dendrite, + username string, +) bool { + userID := userutil.MakeUserID(username, cfg.Matrix.ServerName) + + // Check namespaces and see if more than one match + matchCount := 0 + for _, appservice := range cfg.Derived.ApplicationServices { + if appservice.IsInterestedInUserID(userID) { + if matchCount++; matchCount > 1 { + return true + } + } + } + return false +} + +// UsernameMatchesExclusiveNamespaces will check if a given username matches any +// application service's exclusive users namespace +func UsernameMatchesExclusiveNamespaces( + cfg *config.Dendrite, + username string, +) bool { + userID := userutil.MakeUserID(username, cfg.Matrix.ServerName) + return cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(userID) +} + +// validateApplicationService checks if a provided application service token +// corresponds to one that is registered. If so, then it checks if the desired +// username is within that application service's namespace. As long as these +// two requirements are met, no error will be returned. +func validateApplicationService( + cfg *config.Dendrite, + username string, + accessToken string, +) (string, *util.JSONResponse) { + // Check if the token if the application service is valid with one we have + // registered in the config. + var matchedApplicationService *config.ApplicationService + for _, appservice := range cfg.Derived.ApplicationServices { + if appservice.ASToken == accessToken { + matchedApplicationService = &appservice + break + } + } + if matchedApplicationService == nil { + return "", &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: jsonerror.UnknownToken("Supplied access_token does not match any known application service"), + } + } + + userID := userutil.MakeUserID(username, cfg.Matrix.ServerName) + + // Ensure the desired username is within at least one of the application service's namespaces. + if !UserIDIsWithinApplicationServiceNamespace(cfg, userID, matchedApplicationService) { + // If we didn't find any matches, return M_EXCLUSIVE + return "", &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.ASExclusive(fmt.Sprintf( + "Supplied username %s did not match any namespaces for application service ID: %s", username, matchedApplicationService.ID)), + } + } + + // Check this user does not fit multiple application service namespaces + if UsernameMatchesMultipleExclusiveNamespaces(cfg, userID) { + return "", &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.ASExclusive(fmt.Sprintf( + "Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)), + } + } + + // Check username application service is trying to register is valid + if err := validateApplicationServiceUsername(username); err != nil { + return "", err + } + + // No errors, registration valid + return matchedApplicationService.ID, 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 *accounts.Database, + deviceDB *devices.Database, + cfg *config.Dendrite, +) util.JSONResponse { + + var r registerRequest + resErr := httputil.UnmarshalJSONRequest(req, &r) + if resErr != nil { + return *resErr + } + + // Retrieve or generate the sessionID + sessionID := r.Auth.Session + if sessionID == "" { + // Generate a new, random session ID + sessionID = util.RandomString(sessionIDLength) + } + + // Don't allow numeric usernames less than MAX_INT64. + if _, err := strconv.ParseInt(r.Username, 10, 64); err == nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidUsername("Numeric user IDs are reserved"), + } + } + // Auto generate a numeric username if r.Username is empty + if r.Username == "" { + id, err := accountDB.GetNewNumericLocalpart(req.Context()) + if err != nil { + return httputil.LogThenError(req, err) + } + + r.Username = strconv.FormatInt(id, 10) + } + + // Squash username to all lowercase letters + r.Username = strings.ToLower(r.Username) + + if resErr = validateUsername(r.Username); resErr != nil { + return *resErr + } + if resErr = validatePassword(r.Password); resErr != nil { + return *resErr + } + + // Make sure normal user isn't registering under an exclusive application + // service namespace. Skip this check if no app services are registered. + if r.Auth.Type != authtypes.LoginTypeApplicationService && + len(cfg.Derived.ApplicationServices) != 0 && + UsernameMatchesExclusiveNamespaces(cfg, r.Username) { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.ASExclusive("This username is reserved by an application service."), + } + } + + 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") + + return handleRegistrationFlow(req, r, sessionID, cfg, accountDB, deviceDB) +} + +// handleRegistrationFlow will direct and complete registration flow stages +// that the client has requested. +// nolint: gocyclo +func handleRegistrationFlow( + req *http.Request, + r registerRequest, + sessionID string, + cfg *config.Dendrite, + accountDB *accounts.Database, + deviceDB *devices.Database, +) util.JSONResponse { + // TODO: Shared secret registration (create new user scripts) + // TODO: Enable registration config flag + // TODO: Guest account upgrading + + // TODO: Handle loading of previous session parameters from database. + // TODO: Handle mapping registrationRequest parameters into session parameters + + // TODO: email / msisdn auth types. + + if cfg.Matrix.RegistrationDisabled && r.Auth.Type != authtypes.LoginTypeSharedSecret { + return util.MessageResponse(http.StatusForbidden, "Registration has been disabled") + } + + switch r.Auth.Type { + case authtypes.LoginTypeRecaptcha: + // Check given captcha response + resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr) + if resErr != nil { + return *resErr + } + + // Add Recaptcha to the list of completed registration stages + sessions.AddCompletedStage(sessionID, authtypes.LoginTypeRecaptcha) + + case authtypes.LoginTypeSharedSecret: + // Check shared secret against config + valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Auth.Mac) + + if err != nil { + return httputil.LogThenError(req, err) + } else if !valid { + return util.MessageResponse(http.StatusForbidden, "HMAC incorrect") + } + + // Add SharedSecret to the list of completed registration stages + sessions.AddCompletedStage(sessionID, authtypes.LoginTypeSharedSecret) + + case "": + // Extract the access token from the request, if there's one to extract + // (which we can know by checking whether the error is nil or not). + accessToken, err := auth.ExtractAccessToken(req) + + // A missing auth type can mean either the registration is performed by + // an AS or the request is made as the first step of a registration + // using the User-Interactive Authentication API. This can be determined + // by whether the request contains an access token. + if err == nil { + return handleApplicationServiceRegistration( + accessToken, err, req, r, cfg, accountDB, deviceDB, + ) + } + + case authtypes.LoginTypeApplicationService: + // Extract the access token from the request. + accessToken, err := auth.ExtractAccessToken(req) + // Let the AS registration handler handle the process from here. We + // don't need a condition on that call since the registration is clearly + // stated as being AS-related. + return handleApplicationServiceRegistration( + accessToken, err, req, r, cfg, accountDB, deviceDB, + ) + + case authtypes.LoginTypeDummy: + // there is nothing to do + // Add Dummy to the list of completed registration stages + sessions.AddCompletedStage(sessionID, authtypes.LoginTypeDummy) + + default: + return util.JSONResponse{ + Code: http.StatusNotImplemented, + JSON: jsonerror.Unknown("unknown/unimplemented auth type"), + } + } + + // Check if the user's registration flow has been completed successfully + // A response with current registration flow and remaining available methods + // will be returned if a flow has not been successfully completed yet + return checkAndCompleteFlow(sessions.GetCompletedStages(sessionID), + req, r, sessionID, cfg, accountDB, deviceDB) +} + +// handleApplicationServiceRegistration handles the registration of an +// application service's user by validating the AS from its access token and +// registering the user. Its two first parameters must be the two return values +// of the auth.ExtractAccessToken function. +// Returns an error if the access token couldn't be extracted from the request +// at an earlier step of the registration workflow, or if the provided access +// token doesn't belong to a valid AS, or if there was an issue completing the +// registration process. +func handleApplicationServiceRegistration( + accessToken string, + tokenErr error, + req *http.Request, + r registerRequest, + cfg *config.Dendrite, + accountDB *accounts.Database, + deviceDB *devices.Database, +) util.JSONResponse { + // Check if we previously had issues extracting the access token from the + // request. + if tokenErr != nil { + return util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: jsonerror.MissingToken(tokenErr.Error()), + } + } + + // Check application service register user request is valid. + // The application service's ID is returned if so. + appserviceID, err := validateApplicationService( + cfg, r.Username, accessToken, + ) + if err != nil { + return *err + } + + // If no error, application service was successfully validated. + // Don't need to worry about appending to registration stages as + // application service registration is entirely separate. + return completeRegistration( + req.Context(), accountDB, deviceDB, r.Username, "", appserviceID, + r.InhibitLogin, r.InitialDisplayName, + ) +} + +// checkAndCompleteFlow checks if a given registration flow is completed given +// a set of allowed flows. If so, registration is completed, otherwise a +// response with +func checkAndCompleteFlow( + flow []authtypes.LoginType, + req *http.Request, + r registerRequest, + sessionID string, + cfg *config.Dendrite, + accountDB *accounts.Database, + deviceDB *devices.Database, +) util.JSONResponse { + if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) { + // This flow was completed, registration can continue + return completeRegistration( + req.Context(), accountDB, deviceDB, r.Username, r.Password, "", + r.InhibitLogin, r.InitialDisplayName, + ) + } + + // There are still more stages to complete. + // Return the flows and those that have been completed. + return util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: newUserInteractiveResponse(sessionID, + cfg.Derived.Registration.Flows, cfg.Derived.Registration.Params), + } +} + +// LegacyRegister process register requests from the legacy v1 API +func LegacyRegister( + req *http.Request, + accountDB *accounts.Database, + deviceDB *devices.Database, + cfg *config.Dendrite, +) util.JSONResponse { + var r legacyRegisterRequest + resErr := parseAndValidateLegacyLogin(req, &r) + if resErr != nil { + return *resErr + } + + logger := util.GetLogger(req.Context()) + logger.WithFields(log.Fields{ + "username": r.Username, + "auth.type": r.Type, + }).Info("Processing registration request") + + if cfg.Matrix.RegistrationDisabled && r.Type != authtypes.LoginTypeSharedSecret { + return util.MessageResponse(http.StatusForbidden, "Registration has been disabled") + } + + switch r.Type { + case authtypes.LoginTypeSharedSecret: + if cfg.Matrix.RegistrationSharedSecret == "" { + return util.MessageResponse(http.StatusBadRequest, "Shared secret registration is disabled") + } + + valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Mac) + if err != nil { + return httputil.LogThenError(req, err) + } + + if !valid { + return util.MessageResponse(http.StatusForbidden, "HMAC incorrect") + } + + return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", false, nil) + case authtypes.LoginTypeDummy: + // there is nothing to do + return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", false, nil) + default: + return util.JSONResponse{ + Code: http.StatusNotImplemented, + JSON: jsonerror.Unknown("unknown/unimplemented auth type"), + } + } +} + +// parseAndValidateLegacyLogin parses the request into r and checks that the +// request is valid (e.g. valid user names, etc) +func parseAndValidateLegacyLogin(req *http.Request, r *legacyRegisterRequest) *util.JSONResponse { + resErr := httputil.UnmarshalJSONRequest(req, &r) + if resErr != nil { + return resErr + } + + // Squash username to all lowercase letters + r.Username = strings.ToLower(r.Username) + + if resErr = validateUsername(r.Username); resErr != nil { + return resErr + } + if resErr = validatePassword(r.Password); resErr != nil { + return resErr + } + + // All registration requests must specify what auth they are using to perform this request + if r.Type == "" { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON("invalid type"), + } + } + + return nil +} + +func completeRegistration( + ctx context.Context, + accountDB *accounts.Database, + deviceDB *devices.Database, + username, password, appserviceID string, + inhibitLogin common.WeakBoolean, + displayName *string, +) util.JSONResponse { + if username == "" { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON("missing username"), + } + } + // Blank passwords are only allowed by registered application services + if password == "" && appserviceID == "" { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON("missing password"), + } + } + + acc, err := accountDB.CreateAccount(ctx, username, password, appserviceID) + if err != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: jsonerror.Unknown("failed to create account: " + err.Error()), + } + } else if acc == nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.UserInUse("Desired user ID is already taken."), + } + } + + // Check whether inhibit_login option is set. If so, don't create an access + // token or a device for this user + if inhibitLogin { + return util.JSONResponse{ + Code: http.StatusOK, + JSON: registerResponse{ + UserID: userutil.MakeUserID(username, acc.ServerName), + HomeServer: acc.ServerName, + }, + } + } + + token, err := auth.GenerateAccessToken() + if err != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: jsonerror.Unknown("Failed to generate access token"), + } + } + + // TODO: Use the device ID in the request. + dev, err := deviceDB.CreateDevice(ctx, username, nil, token, displayName) + if err != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: jsonerror.Unknown("failed to create device: " + err.Error()), + } + } + + // Increment prometheus counter for created users + amtRegUsers.Inc() + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: registerResponse{ + UserID: dev.UserID, + AccessToken: dev.AccessToken, + HomeServer: acc.ServerName, + DeviceID: dev.ID, + }, + } +} + +// Used for shared secret registration. +// Checks if the username, password and isAdmin flag matches the given mac. +func isValidMacLogin( + cfg *config.Dendrite, + username, password string, + isAdmin bool, + givenMac []byte, +) (bool, error) { + sharedSecret := cfg.Matrix.RegistrationSharedSecret + + // Check that shared secret registration isn't disabled. + if cfg.Matrix.RegistrationSharedSecret == "" { + return false, errors.New("Shared secret registration is disabled") + } + + // Double check that username/password don't contain the HMAC delimiters. We should have + // already checked this. + if strings.Contains(username, "\x00") { + return false, errors.New("Username contains invalid character") + } + if strings.Contains(password, "\x00") { + return false, errors.New("Password contains invalid character") + } + if sharedSecret == "" { + return false, errors.New("Shared secret registration is disabled") + } + + adminString := "notadmin" + if isAdmin { + adminString = "admin" + } + joined := strings.Join([]string{username, password, adminString}, "\x00") + + mac := hmac.New(sha1.New, []byte(sharedSecret)) + _, err := mac.Write([]byte(joined)) + if err != nil { + return false, err + } + expectedMAC := mac.Sum(nil) + + return hmac.Equal(givenMac, expectedMAC), nil +} + +// checkFlows checks a single completed flow against another required one. If +// one contains at least all of the stages that the other does, checkFlows +// returns true. +func checkFlows( + completedStages []authtypes.LoginType, + requiredStages []authtypes.LoginType, +) bool { + // Create temporary slices so they originals will not be modified on sorting + completed := make([]authtypes.LoginType, len(completedStages)) + required := make([]authtypes.LoginType, len(requiredStages)) + copy(completed, completedStages) + copy(required, requiredStages) + + // Sort the slices for simple comparison + sort.Slice(completed, func(i, j int) bool { return completed[i] < completed[j] }) + sort.Slice(required, func(i, j int) bool { return required[i] < required[j] }) + + // Iterate through each slice, going to the next required slice only once + // we've found a match. + i, j := 0, 0 + for j < len(required) { + // Exit if we've reached the end of our input without being able to + // match all of the required stages. + if i >= len(completed) { + return false + } + + // If we've found a stage we want, move on to the next required stage. + if completed[i] == required[j] { + j++ + } + i++ + } + return true +} + +// checkFlowCompleted checks if a registration flow complies with any allowed flow +// dictated by the server. Order of stages does not matter. A user may complete +// extra stages as long as the required stages of at least one flow is met. +func checkFlowCompleted( + flow []authtypes.LoginType, + allowedFlows []authtypes.Flow, +) bool { + // Iterate through possible flows to check whether any have been fully completed. + for _, allowedFlow := range allowedFlows { + if checkFlows(flow, allowedFlow.Stages) { + return true + } + } + return false +} + +type availableResponse struct { + Available bool `json:"available"` +} + +// RegisterAvailable checks if the username is already taken or invalid. +func RegisterAvailable( + req *http.Request, + cfg config.Dendrite, + accountDB *accounts.Database, +) util.JSONResponse { + username := req.URL.Query().Get("username") + + // Squash username to all lowercase letters + username = strings.ToLower(username) + + if err := validateUsername(username); err != nil { + return *err + } + + // Check if this username is reserved by an application service + userID := userutil.MakeUserID(username, cfg.Matrix.ServerName) + for _, appservice := range cfg.Derived.ApplicationServices { + if appservice.IsInterestedInUserID(userID) { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.UserInUse("Desired user ID is reserved by an application service."), + } + } + } + + availability, availabilityErr := accountDB.CheckAccountAvailability(req.Context(), username) + if availabilityErr != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: jsonerror.Unknown("failed to check availability: " + availabilityErr.Error()), + } + } + if !availability { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.UserInUse("Desired User ID is already taken."), + } + } + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: availableResponse{ + Available: true, + }, + } +} |