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 ca9c8b38f..c4f7b0463 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 92c9b427e..40988a717 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/register.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/register.go @@ -19,16 +19,18 @@ import ( "context" "crypto/hmac" "crypto/sha1" + "encoding/json" "errors" "fmt" + "io/ioutil" "net/http" + "net/url" "regexp" "strings" "time" "github.com/matrix-org/dendrite/common/config" - log "github.com/sirupsen/logrus" "github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" @@ -37,6 +39,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" + log "github.com/sirupsen/logrus" ) const ( @@ -66,6 +69,9 @@ 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 } @@ -106,6 +112,13 @@ type registerResponse struct { DeviceID string `json:"device_id"` } +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 @@ -145,6 +158,64 @@ 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 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"), + } + } + + defer resp.Body.Close() + + // 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, @@ -166,6 +237,7 @@ func Register( // Server admins should be able to change things around (eg enable captcha) JSON: newUserInteractiveResponse(time.Now().String(), []authFlow{ {[]authtypes.LoginType{authtypes.LoginTypeDummy}}, + {[]authtypes.LoginType{authtypes.LoginTypeRecaptcha}}, {[]authtypes.LoginType{authtypes.LoginTypeSharedSecret}}, }), } @@ -193,8 +265,25 @@ func Register( // 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. switch r.Auth.Type { + case authtypes.LoginTypeRecaptcha: + if !cfg.Matrix.RegistrationRecaptcha { + return util.MessageResponse(400, "Captcha registration is disabled") + } + + logger := util.GetLogger(req.Context()) + logger.WithFields(log.Fields{ + "clientip": req.RemoteAddr, + "response": r.Auth.Response, + }).Info("Submitting recaptcha response") + + // Check given captcha response + if resErr = validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr); resErr != nil { + return *resErr + } + return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) + case authtypes.LoginTypeSharedSecret: if cfg.Matrix.RegistrationSharedSecret == "" { return util.MessageResponse(400, "Shared secret registration is disabled") 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 82bdc3dca..90e267d96 100644 --- a/src/github.com/matrix-org/dendrite/common/config/config.go +++ b/src/github.com/matrix-org/dendrite/common/config/config.go @@ -25,8 +25,8 @@ import ( "strings" "time" - "github.com/sirupsen/logrus" "github.com/matrix-org/gomatrixserverlib" + "github.com/sirupsen/logrus" "golang.org/x/crypto/ed25519" "gopkg.in/yaml.v2" @@ -82,6 +82,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 + RegistrationRecaptcha 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"` } `yaml:"matrix"` // The configuration specific to the media repostitory.