Implemented ReCaptcha registration method (#343)

Signed-off-by: Andrew (anoa) <anoa@openmailbox.org>
This commit is contained in:
Andrew Morgan 2017-12-05 16:16:14 +00:00 committed by Erik Johnston
parent 8da05cc413
commit bc3dd821f9
3 changed files with 123 additions and 14 deletions

View File

@ -7,4 +7,5 @@ type LoginType string
const ( const (
LoginTypeDummy = "m.login.dummy" LoginTypeDummy = "m.login.dummy"
LoginTypeSharedSecret = "org.matrix.login.shared_secret" LoginTypeSharedSecret = "org.matrix.login.shared_secret"
LoginTypeRecaptcha = "m.login.recaptcha"
) )

View File

@ -19,12 +19,16 @@ import (
"context" "context"
"crypto/hmac" "crypto/hmac"
"crypto/sha1" "crypto/sha1"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"time"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
@ -74,6 +78,8 @@ type authDict struct {
Session string `json:"session"` Session string `json:"session"`
Mac gomatrixserverlib.HexString `json:"mac"` Mac gomatrixserverlib.HexString `json:"mac"`
// Recaptcha
Response string `json:"response"`
// TODO: Lots of custom keys depending on the type // TODO: Lots of custom keys depending on the type
} }
@ -114,6 +120,14 @@ type registerResponse struct {
DeviceID string `json:"device_id"` 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 // validateUserName returns an error response if the username is invalid
func validateUserName(username string) *util.JSONResponse { func validateUserName(username string) *util.JSONResponse {
// https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161 // 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 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 // Register processes a /register request. http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
func Register( func Register(
req *http.Request, req *http.Request,
@ -221,26 +301,30 @@ func handleRegistrationFlow(
// TODO: Handle loading of previous session parameters from database. // TODO: Handle loading of previous session parameters from database.
// TODO: Handle mapping registrationRequest parameters into session parameters // 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 { if cfg.Matrix.RegistrationDisabled && r.Auth.Type != authtypes.LoginTypeSharedSecret {
return util.MessageResponse(403, "Registration has been disabled") return util.MessageResponse(403, "Registration has been disabled")
} }
switch r.Auth.Type { switch r.Auth.Type {
case authtypes.LoginTypeSharedSecret: case authtypes.LoginTypeRecaptcha:
if cfg.Matrix.RegistrationSharedSecret == "" { // Check given captcha response
return util.MessageResponse(400, "Shared secret registration is disabled") resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
if resErr != nil {
return *resErr
} }
valid, err := isValidMacLogin(r.Username, r.Password, r.Admin, // Add Recaptcha to the list of completed registration stages
r.Auth.Mac, cfg.Matrix.RegistrationSharedSecret) 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 { if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} } else if !valid {
if !valid {
return util.MessageResponse(403, "HMAC incorrect") return util.MessageResponse(403, "HMAC incorrect")
} }
@ -303,7 +387,7 @@ func LegacyRegister(
return util.MessageResponse(400, "Shared secret registration is disabled") 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 { if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
@ -412,11 +496,18 @@ func completeRegistration(
// Used for shared secret registration. // Used for shared secret registration.
// Checks if the username, password and isAdmin flag matches the given mac. // Checks if the username, password and isAdmin flag matches the given mac.
func isValidMacLogin( func isValidMacLogin(
cfg *config.Dendrite,
username, password string, username, password string,
isAdmin bool, isAdmin bool,
givenMac []byte, givenMac []byte,
sharedSecret string,
) (bool, error) { ) (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 // Double check that username/password don't contain the HMAC delimiters. We should have
// already checked this. // already checked this.
if strings.Contains(username, "\x00") { if strings.Contains(username, "\x00") {

View File

@ -83,6 +83,18 @@ type Dendrite struct {
// If set, allows registration by anyone who also has the shared // If set, allows registration by anyone who also has the shared
// secret, even if registration is otherwise disabled. // secret, even if registration is otherwise disabled.
RegistrationSharedSecret string `yaml:"registration_shared_secret"` 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 // If set disables new users from registering (except via shared
// secrets) // secrets)
RegistrationDisabled bool `yaml:"registration_disabled"` RegistrationDisabled bool `yaml:"registration_disabled"`
@ -339,10 +351,15 @@ func (config *Dendrite) derive() {
// TODO: Add email auth type // TODO: Add email auth type
// TODO: Add MSISDN auth type // TODO: Add MSISDN auth type
// TODO: Add Recaptcha auth type
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows, if config.Matrix.RecaptchaEnabled {
authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeDummy}}) 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. // setDefaults sets default config values if they are not explicitly set.