Implemented ReCaptcha registration method (#343)
Signed-off-by: Andrew (anoa) <anoa@openmailbox.org>main
parent
8da05cc413
commit
bc3dd821f9
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
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,
|
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows,
|
||||||
authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeDummy}})
|
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.
|
||||||
|
|
Loading…
Reference in New Issue