diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/Flow.go b/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/Flow.go new file mode 100644 index 000000000..1fa681151 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/Flow.go @@ -0,0 +1,21 @@ +// Copyright 2017 Vector Creations 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 authtypes + +// AuthFlow represents one possible way that the client can authenticate a request. +// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api +type Flow struct { + Stages []LoginType `json:"stages"` +} 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 8ac7c9181..9234b946d 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/register.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/register.go @@ -23,9 +23,12 @@ import ( "errors" "fmt" "io/ioutil" + "math/rand" "net/http" "net/url" + "reflect" "regexp" + "sort" "strings" "time" @@ -46,9 +49,13 @@ 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 ) -var validUsernameRegex = regexp.MustCompile(`^[0-9a-zA-Z_\-./]+$`) +var ( + sessions = make(map[string][]authtypes.LoginType) // Sessions and completed flow stages + validUsernameRegex = regexp.MustCompile(`^[0-9a-zA-Z_\-./]+$`) +) // registerRequest represents the submitted registration request. // It can be broken down into 2 sections: the auth dictionary and registration parameters. @@ -79,18 +86,12 @@ type authDict struct { // http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api type userInteractiveResponse struct { - Flows []authFlow `json:"flows"` + Flows []authtypes.Flow `json:"flows"` Completed []authtypes.LoginType `json:"completed"` Params map[string]interface{} `json:"params"` Session string `json:"session"` } -// authFlow represents one possible way that the client can authenticate a request. -// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api -type authFlow struct { - Stages []authtypes.LoginType `json:"stages"` -} - // legacyRegisterRequest represents the submitted registration request for v1 API. type legacyRegisterRequest struct { Password string `json:"password"` @@ -100,9 +101,9 @@ type legacyRegisterRequest struct { Mac gomatrixserverlib.HexString `json:"mac"` } -func newUserInteractiveResponse(sessionID string, fs []authFlow) userInteractiveResponse { +func newUserInteractiveResponse(sessionID string, fs []authtypes.Flow) userInteractiveResponse { return userInteractiveResponse{ - fs, []authtypes.LoginType{}, make(map[string]interface{}), sessionID, + fs, sessions[sessionID], make(map[string]interface{}), sessionID, } } @@ -114,6 +115,7 @@ 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"` @@ -190,16 +192,8 @@ func validateRecaptcha( } } - defer func() { - err := resp.Body.Close() - - if err != nil { - logger := util.GetLogger(req.Context()) - logger.WithFields(log.Fields{ - "response": response, - }).Info("Failed to close recaptcha request response body") - } - }() + // Close the request once we're finishing reading from it + defer resp.Body.Close() // noline: errcheck // Grab the body of the response from the captcha server var r recaptchaResponse @@ -228,6 +222,9 @@ func validateRecaptcha( return nil } +// TODO: Create flows in config.go so that they're cached. Always show msisdn flows as long as flows only depend on config-file options. +// Store it just like the config does in a struct and keep it there. + // 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, @@ -235,23 +232,25 @@ func Register( deviceDB *devices.Database, cfg *config.Dendrite, ) util.JSONResponse { + var r registerRequest resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { return *resErr } - // All registration requests must specify what auth they are using to perform this request + // Retrieve or generate the sessionID + sessionID := r.Auth.Session + if sessionID == "" { + // Generate a new, random session ID + sessionID = RandString(sessionIDLength) + } + + // If no auth type is specified by the client, send back the list of available flows if r.Auth.Type == "" { return util.JSONResponse{ Code: 401, - // TODO: Hard-coded 'dummy' auth for now with a bogus session ID. - // 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}}, - }), + JSON: newUserInteractiveResponse(sessionID, cfg.Derived.Flows), } } @@ -280,7 +279,7 @@ func Register( // TODO: email / msisdn auth types. switch r.Auth.Type { case authtypes.LoginTypeRecaptcha: - if !cfg.Matrix.RegistrationRecaptcha { + if !cfg.Matrix.RecaptchaEnabled { return util.MessageResponse(400, "Captcha registration is disabled") } @@ -294,8 +293,9 @@ func Register( if resErr = validateRecaptcha(req, cfg, r.Auth.Response, req.RemoteAddr); resErr != nil { return *resErr } - return completeRegistration(req.Context(), accountDB, deviceDB, - r.Username, r.Password, r.InitialDisplayName) + + // Add Recaptcha to the list of completed registration stages + sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeRecaptcha) case authtypes.LoginTypeSharedSecret: if cfg.Matrix.RegistrationSharedSecret == "" { @@ -313,18 +313,35 @@ func Register( return util.MessageResponse(403, "HMAC incorrect") } - return completeRegistration(req.Context(), accountDB, deviceDB, - r.Username, r.Password, r.InitialDisplayName) + // Add SharedSecret to the list of completed registration stages + sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeSharedSecret) + case authtypes.LoginTypeDummy: // there is nothing to do - return completeRegistration(req.Context(), accountDB, deviceDB, - r.Username, r.Password, r.InitialDisplayName) + // Add Dummy to the list of completed registration stages + sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeDummy) + default: return util.JSONResponse{ Code: 501, JSON: jsonerror.Unknown("unknown/unimplemented auth type"), } } + + // Check if a registration flow has been completed successfully + for _, flow := range cfg.Derived.Flows { + if checkFlowsEqual(flow, authtypes.Flow{sessions[sessionID]}) { + return completeRegistration(req.Context(), accountDB, deviceDB, + r.Username, r.Password, r.InitialDisplayName) + } + } + + // There are still more stages to complete. + // Return the flows and those that have been completed. + return util.JSONResponse{ + Code: 401, + JSON: newUserInteractiveResponse(sessionID, cfg.Derived.Flows), + } } // LegacyRegister process register requests from the legacy v1 API @@ -479,6 +496,62 @@ func isValidMacLogin( return hmac.Equal(givenMac, expectedMAC), nil } +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) +} + +// checkFlowsEqual checks if two registration flows have the same stages +// within them. Order of stages does not matter. +func checkFlowsEqual(aFlow, bFlow authtypes.Flow) bool { + a := aFlow.Stages + b := bFlow.Stages + if len(a) != len(b) { + return false + } + + a_copy := make([]string, len(a)) + b_copy := make([]string, len(b)) + + for loginType := range a { + a_copy = append(a_copy, string(loginType)) + } + for loginType := range b { + b_copy = append(b_copy, string(loginType)) + } + + sort.Strings(a_copy) + sort.Strings(b_copy) + + return reflect.DeepEqual(a_copy, b_copy) +} + type availableResponse struct { Available bool `json:"available"` } 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 784c0f5ab..7f8faf5f6 100644 --- a/src/github.com/matrix-org/dendrite/common/config/config.go +++ b/src/github.com/matrix-org/dendrite/common/config/config.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/gomatrixserverlib" "github.com/sirupsen/logrus" "golang.org/x/crypto/ed25519" @@ -88,7 +89,7 @@ type Dendrite struct { RecaptchaPrivateKey string `yaml:"recaptcha_private_key"` // Boolean stating whether catpcha registration is enabled // and required - RegistrationRecaptcha bool `yaml:"enable_registration_captcha"` + 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 @@ -201,6 +202,13 @@ type Dendrite struct { // The config for the jaeger opentracing reporter. Jaeger jaegerconfig.Configuration `yaml:"jaeger"` } + + // Any information derived from the configuration options for later use. + Derived struct { + // Flows for registration. As long as they given flows only relies on config file options, + // we can generate them on startup and store them until needed + Flows []authtypes.Flow `json:"flows"` + } } // A Path on the filesystem. @@ -317,9 +325,29 @@ func loadConfig( config.Media.AbsBasePath = Path(absPath(basePath, config.Media.BasePath)) + // Generate data from config options + config.derive() + return &config, nil } +// derive generates data that is derived from various values provided in +// the config file +func (config *Dendrite) derive() { + // Determine registrations flows based off config values + // TODO: Add email auth type + // TODO: Add MSISDN auth type + + if config.Matrix.RecaptchaEnabled { + config.Derived.Flows = append(config.Derived.Flows, + authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeRecaptcha}}) + } else { + config.Derived.Flows = append(config.Derived.Flows, + authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeDummy}}) + } +} + +// setDefaults sets default config values if they are not explicitly set func (config *Dendrite) setDefaults() { if config.Matrix.KeyValidityPeriod == 0 { config.Matrix.KeyValidityPeriod = 24 * time.Hour @@ -339,6 +367,8 @@ func (config *Dendrite) setDefaults() { } } +// Error returns a string detailing how many errors were contained within an +// Error type func (e Error) Error() string { if len(e.Problems) == 1 { return e.Problems[0] @@ -348,6 +378,8 @@ func (e Error) Error() string { ) } +// check returns an error type containing all errors found within the config +// file func (config *Dendrite) check(monolithic bool) error { var problems []string @@ -432,6 +464,7 @@ func (config *Dendrite) check(monolithic bool) error { return nil } +// absPath returns the absolute path for a given relative or absolute path func absPath(dir string, path Path) string { if filepath.IsAbs(string(path)) { // filepath.Join cleans the path so we should clean the absolute paths as well for consistency.