From bc3dd821f9006e2055e8d3487d5adf507067232c Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 5 Dec 2017 16:16:14 +0000 Subject: [PATCH] Implemented ReCaptcha registration method (#343) Signed-off-by: Andrew (anoa) --- .../clientapi/auth/authtypes/logintypes.go | 1 + .../dendrite/clientapi/routing/register.go | 113 ++++++++++++++++-- .../dendrite/common/config/config.go | 23 +++- 3 files changed, 123 insertions(+), 14 deletions(-) diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/logintypes.go b/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/logintypes.go index ca9c8b38..c4f7b046 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/logintypes.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/logintypes.go @@ -7,4 +7,5 @@ type LoginType string const ( LoginTypeDummy = "m.login.dummy" LoginTypeSharedSecret = "org.matrix.login.shared_secret" + LoginTypeRecaptcha = "m.login.recaptcha" ) diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/register.go b/src/github.com/matrix-org/dendrite/clientapi/routing/register.go index 6ef4ab05..7bd5820f 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/register.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/register.go @@ -19,12 +19,16 @@ import ( "context" "crypto/hmac" "crypto/sha1" + "encoding/json" "errors" "fmt" + "io/ioutil" "net/http" + "net/url" "regexp" "sort" "strings" + "time" "github.com/matrix-org/dendrite/common/config" @@ -74,6 +78,8 @@ type authDict struct { Session string `json:"session"` Mac gomatrixserverlib.HexString `json:"mac"` + // Recaptcha + Response string `json:"response"` // TODO: Lots of custom keys depending on the type } @@ -114,6 +120,14 @@ type registerResponse struct { DeviceID string `json:"device_id"` } +// 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 @@ -153,6 +167,72 @@ func validatePassword(password string) *util.JSONResponse { 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: 400, + JSON: jsonerror.BadJSON("Captcha registration is disabled"), + } + } + + if response == "" { + return &util.JSONResponse{ + Code: 400, + 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: 500, + 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: 500, + JSON: jsonerror.BadJSON("Error in contacting captcha server" + err.Error()), + } + } + err = json.Unmarshal(body, &r) + if err != nil { + return &util.JSONResponse{ + Code: 500, + 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: 401, + JSON: jsonerror.BadJSON("Invalid captcha response. Please try again."), + } + } + 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, @@ -221,26 +301,30 @@ func handleRegistrationFlow( // TODO: Handle loading of previous session parameters from database. // TODO: Handle mapping registrationRequest parameters into session parameters - // TODO: email / msisdn / recaptcha auth types. + // TODO: email / msisdn auth types. if cfg.Matrix.RegistrationDisabled && r.Auth.Type != authtypes.LoginTypeSharedSecret { return util.MessageResponse(403, "Registration has been disabled") } switch r.Auth.Type { - case authtypes.LoginTypeSharedSecret: - if cfg.Matrix.RegistrationSharedSecret == "" { - return util.MessageResponse(400, "Shared secret registration is disabled") + case authtypes.LoginTypeRecaptcha: + // Check given captcha response + resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr) + if resErr != nil { + return *resErr } - valid, err := isValidMacLogin(r.Username, r.Password, r.Admin, - r.Auth.Mac, cfg.Matrix.RegistrationSharedSecret) + // Add Recaptcha to the list of completed registration stages + sessions[sessionID] = append(sessions[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) - } - - if !valid { + } else if !valid { return util.MessageResponse(403, "HMAC incorrect") } @@ -303,7 +387,7 @@ func LegacyRegister( return util.MessageResponse(400, "Shared secret registration is disabled") } - valid, err := isValidMacLogin(r.Username, r.Password, r.Admin, r.Mac, cfg.Matrix.RegistrationSharedSecret) + valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Mac) if err != nil { return httputil.LogThenError(req, err) } @@ -412,11 +496,18 @@ func completeRegistration( // 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, - sharedSecret string, ) (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") { diff --git a/src/github.com/matrix-org/dendrite/common/config/config.go b/src/github.com/matrix-org/dendrite/common/config/config.go index 00217e46..d4a9a2c5 100644 --- a/src/github.com/matrix-org/dendrite/common/config/config.go +++ b/src/github.com/matrix-org/dendrite/common/config/config.go @@ -83,6 +83,18 @@ type Dendrite struct { // If set, allows registration by anyone who also has the shared // secret, even if registration is otherwise disabled. RegistrationSharedSecret string `yaml:"registration_shared_secret"` + // This Home Server's ReCAPTCHA public key. + RecaptchaPublicKey string `yaml:"recaptcha_public_key"` + // This Home Server's ReCAPTCHA private key. + RecaptchaPrivateKey string `yaml:"recaptcha_private_key"` + // Boolean stating whether catpcha registration is enabled + // and required + RecaptchaEnabled bool `yaml:"enable_registration_captcha"` + // Secret used to bypass the captcha registration entirely + RecaptchaBypassSecret string `yaml:"captcha_bypass_secret"` + // HTTP API endpoint used to verify whether the captcha response + // was successful + RecaptchaSiteVerifyAPI string `yaml:"recaptcha_siteverify_api"` // If set disables new users from registering (except via shared // secrets) RegistrationDisabled bool `yaml:"registration_disabled"` @@ -339,10 +351,15 @@ func (config *Dendrite) derive() { // TODO: Add email auth type // TODO: Add MSISDN auth type - // TODO: Add Recaptcha auth type - config.Derived.Registration.Flows = append(config.Derived.Registration.Flows, - authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeDummy}}) + if config.Matrix.RecaptchaEnabled { + config.Derived.Registration.Params[authtypes.LoginTypeRecaptcha] = map[string]string{"public_key": config.Matrix.RecaptchaPublicKey} + config.Derived.Registration.Flows = append(config.Derived.Registration.Flows, + authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeRecaptcha}}) + } else { + config.Derived.Registration.Flows = append(config.Derived.Registration.Flows, + authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeDummy}}) + } } // setDefaults sets default config values if they are not explicitly set.