diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go index d0f36a6fd..88ab57a10 100644 --- a/clientapi/routing/register.go +++ b/clientapi/routing/register.go @@ -19,20 +19,13 @@ import ( "context" "crypto/hmac" "crypto/sha1" - "encoding/json" "errors" "fmt" - "io/ioutil" "net/http" - "net/url" "regexp" - "sort" "strconv" "strings" "sync" - "time" - - "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" @@ -42,6 +35,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/userutil" "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" "github.com/prometheus/client_golang/prometheus" @@ -77,37 +71,12 @@ type sessionsDict struct { sessions map[string][]authtypes.LoginType } -// GetCompletedStages returns the completed stages for a session. -func (d *sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType { - d.Lock() - defer d.Unlock() - - if completedStages, ok := d.sessions[sessionID]; ok { - return completedStages - } - // Ensure that a empty slice is returned and not nil. See #399. - return make([]authtypes.LoginType, 0) -} - func newSessionsDict() *sessionsDict { return &sessionsDict{ sessions: make(map[string][]authtypes.LoginType), } } -// AddCompletedSessionStage records that a session has completed an auth stage. -func AddCompletedSessionStage(sessionID string, stage authtypes.LoginType) { - sessions.Lock() - defer sessions.Unlock() - - for _, completedStage := range sessions.sessions[sessionID] { - if completedStage == stage { - return - } - } - sessions.sessions[sessionID] = append(sessions.sessions[sessionID], stage) -} - var ( // TODO: Remove old sessions. Need to do so on a session-specific timeout. // sessions stores the completed flow stages for all sessions. Referenced using their sessionID. @@ -127,7 +96,7 @@ type registerRequest struct { Username string `json:"username"` Admin bool `json:"admin"` // user-interactive auth params - Auth authDict `json:"auth"` + Auth AuthDict `json:"auth"` // Both DeviceID and InitialDisplayName can be omitted, or empty strings ("") // Thus a pointer is needed to differentiate between the two @@ -138,28 +107,10 @@ type registerRequest struct { InhibitLogin common.WeakBoolean `json:"inhibit_login"` // Application Services place Type in the root of their registration - // request, whereas clients place it in the authDict struct. + // request, whereas clients place it in the AuthDict struct. Type authtypes.LoginType `json:"type"` } -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 -} - -// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api -type userInteractiveResponse struct { - Flows []authtypes.Flow `json:"flows"` - Completed []authtypes.LoginType `json:"completed"` - Params map[string]interface{} `json:"params"` - Session string `json:"session"` -} - // legacyRegisterRequest represents the submitted registration request for v1 API. type legacyRegisterRequest struct { Password string `json:"password"` @@ -169,18 +120,6 @@ type legacyRegisterRequest struct { Mac gomatrixserverlib.HexString `json:"mac"` } -// newUserInteractiveResponse will return a struct to be sent back to the client -// during registration. -func newUserInteractiveResponse( - sessionID string, - fs []authtypes.Flow, - params map[string]interface{}, -) userInteractiveResponse { - return userInteractiveResponse{ - fs, sessions.GetCompletedStages(sessionID), params, sessionID, - } -} - // http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register type registerResponse struct { UserID string `json:"user_id"` @@ -189,14 +128,6 @@ type registerResponse struct { DeviceID string `json:"device_id,omitempty"` } -// 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 @@ -252,72 +183,6 @@ 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: http.StatusConflict, - JSON: jsonerror.Unknown("Captcha registration is disabled"), - } - } - - if response == "" { - return &util.JSONResponse{ - Code: http.StatusBadRequest, - 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: http.StatusInternalServerError, - 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: http.StatusGatewayTimeout, - JSON: jsonerror.Unknown("Error in contacting captcha server" + err.Error()), - } - } - err = json.Unmarshal(body, &r) - if err != nil { - return &util.JSONResponse{ - Code: http.StatusInternalServerError, - 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: http.StatusUnauthorized, - JSON: jsonerror.BadJSON("Invalid captcha response. Please try again."), - } - } - return nil -} - // UserIDIsWithinApplicationServiceNamespace checks to see if a given userID // falls within any of the namespaces of a given Application Service. If no // Application Service is given, it will check to see if it matches any @@ -516,7 +381,6 @@ func handleRegistrationFlow( accountDB *accounts.Database, deviceDB *devices.Database, ) util.JSONResponse { - // TODO: Shared secret registration (create new user scripts) // TODO: Enable registration config flag // TODO: Guest account upgrading @@ -529,17 +393,9 @@ func handleRegistrationFlow( return util.MessageResponse(http.StatusForbidden, "Registration has been disabled") } + // Before doing the generic user-interactive auth, check some auth mechanisms which are specific + // to registration switch r.Auth.Type { - case authtypes.LoginTypeRecaptcha: - // Check given captcha response - resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr) - if resErr != nil { - return *resErr - } - - // Add Recaptcha to the list of completed registration stages - AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha) - case authtypes.LoginTypeSharedSecret: // Check shared secret against config valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Auth.Mac) @@ -553,21 +409,6 @@ func handleRegistrationFlow( // Add SharedSecret to the list of completed registration stages AddCompletedSessionStage(sessionID, authtypes.LoginTypeSharedSecret) - case "": - // Extract the access token from the request, if there's one to extract - // (which we can know by checking whether the error is nil or not). - accessToken, err := auth.ExtractAccessToken(req) - - // A missing auth type can mean either the registration is performed by - // an AS or the request is made as the first step of a registration - // using the User-Interactive Authentication API. This can be determined - // by whether the request contains an access token. - if err == nil { - return handleApplicationServiceRegistration( - accessToken, err, req, r, cfg, accountDB, deviceDB, - ) - } - case authtypes.LoginTypeApplicationService: // Extract the access token from the request. accessToken, err := auth.ExtractAccessToken(req) @@ -578,23 +419,34 @@ func handleRegistrationFlow( accessToken, err, req, r, cfg, accountDB, deviceDB, ) - case authtypes.LoginTypeDummy: - // there is nothing to do - // Add Dummy to the list of completed registration stages - AddCompletedSessionStage(sessionID, authtypes.LoginTypeDummy) + // A missing auth type can mean either the registration is performed by + // an AS or the request is made as the first step of a registration + // using the User-Interactive Authentication API. This can be determined + // by whether the request contains an access token. + case "": + // Extract the access token from the request, if there's one to extract + // (which we can know by checking whether the error is nil or not). + accessToken, err := auth.ExtractAccessToken(req) - default: - return util.JSONResponse{ - Code: http.StatusNotImplemented, - JSON: jsonerror.Unknown("unknown/unimplemented auth type"), + if err == nil { + return handleApplicationServiceRegistration( + accessToken, err, req, r, cfg, accountDB, deviceDB, + ) } + // The else case is handled in the generic HandleUserInteractiveFlow function. } - // Check if the user's registration flow has been completed successfully - // A response with current registration flow and remaining available methods - // will be returned if a flow has not been successfully completed yet - return checkAndCompleteFlow(sessions.GetCompletedStages(sessionID), - req, r, sessionID, cfg, accountDB, deviceDB) + // Invoke generic User Interactive auth Flow handler + jsonErr := HandleUserInteractiveFlow(req, r.Auth, sessionID, cfg, cfg.Derived.Registration) + if jsonErr != nil { + return *jsonErr + } + + // Invoke complete registration method if error is nil + return completeRegistration( + req.Context(), accountDB, deviceDB, r.Username, r.Password, "", + r.InhibitLogin, r.InitialDisplayName, r.DeviceID, + ) } // handleApplicationServiceRegistration handles the registration of an @@ -641,35 +493,6 @@ func handleApplicationServiceRegistration( ) } -// checkAndCompleteFlow checks if a given registration flow is completed given -// a set of allowed flows. If so, registration is completed, otherwise a -// response with -func checkAndCompleteFlow( - flow []authtypes.LoginType, - req *http.Request, - r registerRequest, - sessionID string, - cfg *config.Dendrite, - accountDB *accounts.Database, - deviceDB *devices.Database, -) util.JSONResponse { - if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) { - // This flow was completed, registration can continue - return completeRegistration( - req.Context(), accountDB, deviceDB, r.Username, r.Password, "", - r.InhibitLogin, r.InitialDisplayName, r.DeviceID, - ) - } - - // There are still more stages to complete. - // Return the flows and those that have been completed. - return util.JSONResponse{ - Code: http.StatusUnauthorized, - JSON: newUserInteractiveResponse(sessionID, - cfg.Derived.Registration.Flows, cfg.Derived.Registration.Params), - } -} - // LegacyRegister process register requests from the legacy v1 API func LegacyRegister( req *http.Request, @@ -875,62 +698,6 @@ func isValidMacLogin( return hmac.Equal(givenMac, expectedMAC), nil } -// checkFlows checks a single completed flow against another required one. If -// one contains at least all of the stages that the other does, checkFlows -// returns true. -func checkFlows( - completedStages []authtypes.LoginType, - requiredStages []authtypes.LoginType, -) bool { - // Create temporary slices so they originals will not be modified on sorting - completed := make([]authtypes.LoginType, len(completedStages)) - required := make([]authtypes.LoginType, len(requiredStages)) - copy(completed, completedStages) - copy(required, requiredStages) - - // Sort the slices for simple comparison - sort.Slice(completed, func(i, j int) bool { return completed[i] < completed[j] }) - sort.Slice(required, func(i, j int) bool { return required[i] < required[j] }) - - // Iterate through each slice, going to the next required slice only once - // we've found a match. - i, j := 0, 0 - for j < len(required) { - // Exit if we've reached the end of our input without being able to - // match all of the required stages. - if i >= len(completed) { - return false - } - - // If we've found a stage we want, move on to the next required stage. - if completed[i] == required[j] { - j++ - } - i++ - } - return true -} - -// checkFlowCompleted checks if a registration flow complies with any allowed flow -// dictated by the server. Order of stages does not matter. A user may complete -// extra stages as long as the required stages of at least one flow is met. -func checkFlowCompleted( - flow []authtypes.LoginType, - allowedFlows []authtypes.Flow, -) bool { - // Iterate through possible flows to check whether any have been fully completed. - for _, allowedFlow := range allowedFlows { - if checkFlows(flow, allowedFlow.Stages) { - return true - } - } - return false -} - -type availableResponse struct { - Available bool `json:"available"` -} - // RegisterAvailable checks if the username is already taken or invalid. func RegisterAvailable( req *http.Request, diff --git a/clientapi/routing/user_interactive_auth_handler.go b/clientapi/routing/user_interactive_auth_handler.go new file mode 100644 index 000000000..c7b6104f0 --- /dev/null +++ b/clientapi/routing/user_interactive_auth_handler.go @@ -0,0 +1,285 @@ +// Copyright 2019 Vector Creations Ltd +// Copyright 2019 New Vector 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 routing + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "sort" + "time" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" +) + +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 +} + +// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api +type userInteractiveResponse struct { + Flows []authtypes.Flow `json:"flows"` + Completed []authtypes.LoginType `json:"completed"` + Params map[string]interface{} `json:"params"` + Session string `json:"session"` +} + +// newUserInteractiveResponse will return a struct to be sent back to the client. +func newUserInteractiveResponse( + sessionID string, + fs []authtypes.Flow, + params map[string]interface{}, +) userInteractiveResponse { + return userInteractiveResponse{ + fs, sessions.GetCompletedStages(sessionID), params, sessionID, + } +} + +// getCompletedStages returns the completed stages for a session. +func (d *sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType { + d.Lock() + defer d.Unlock() + + if completedStages, ok := d.sessions[sessionID]; ok { + return completedStages + } + // Ensure that a empty slice is returned and not nil. See #399. + return make([]authtypes.LoginType, 0) +} + +// addCompletedSessionStage records that a session has completed an auth stage. +func AddCompletedSessionStage(sessionID string, stage authtypes.LoginType) { + sessions.Lock() + defer sessions.Unlock() + + for _, completedStage := range sessions.sessions[sessionID] { + if completedStage == stage { + return + } + } + sessions.sessions[sessionID] = append(sessions.sessions[sessionID], stage) +} + +// HandleUserInteractiveFlow will direct and complete auth flow stages +// that the client has requested. This function requires http request, session Id +// authentication data as AuthDict object, config object and a config.UserInteractiveAuthConfig +// object containing list of allowed auth flows and params for successful authentication. +// It returns nil if auth is successful else returns jsonResponse (can be sent directly to +// client as http response) containing userInteractiveResponse or relevant error (if encountered). +func HandleUserInteractiveFlow( + req *http.Request, + auth AuthDict, + sessionID string, + cfg *config.Dendrite, + authCfg config.UserInteractiveAuthConfig, +) *util.JSONResponse { + + switch auth.Type { + case authtypes.LoginTypeRecaptcha: + // Check given captcha response + resErr := validateRecaptcha(cfg, auth.Response, req.RemoteAddr) + if resErr != nil { + return resErr + } + + // Add Recaptcha to the list of completed authentication stages + AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha) + + // A missing auth type means the request is made as the first step of a authentication + // using the User-Interactive Authentication API. Skip the default case for this. + case "": + + case authtypes.LoginTypeDummy: + // there is nothing to do + // Add Dummy to the list of completed authentication stages + AddCompletedSessionStage(sessionID, authtypes.LoginTypeDummy) + + default: + return &util.JSONResponse{ + Code: http.StatusNotImplemented, + JSON: jsonerror.Unknown("unknown/unimplemented auth type"), + } + } + + // Check if the user's authentication flow has been completed successfully + // A response with current authentication flow and remaining available methods + // will be returned if a flow has not been successfully completed yet + return checkAndCompleteFlow(sessions.GetCompletedStages(sessionID), sessionID, authCfg) +} + +// checkAndCompleteFlow checks if authentication flow is completed given +// a set of allowed stages. If so, authentication is completed and +// function returns nil, otherwise a userInteractiveResponse +// with required stages is returned. +func checkAndCompleteFlow( + flow []authtypes.LoginType, + sessionID string, + authCfg config.UserInteractiveAuthConfig, +) *util.JSONResponse { + if checkFlowCompleted(flow, authCfg.Flows) { + // This flow was completed, authentication successful. + return nil + } + + // There are still more stages to complete. + // Return the flows and those that have been completed. + return &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: newUserInteractiveResponse(sessionID, + authCfg.Flows, authCfg.Params), + } +} + +// checkFlows checks a single completed flow against another required one. If +// one contains at least all of the stages that the other does, checkFlows +// returns true. +func checkFlows( + completedStages []authtypes.LoginType, + requiredStages []authtypes.LoginType, +) bool { + // Create temporary slices so they originals will not be modified on sorting + completed := make([]authtypes.LoginType, len(completedStages)) + required := make([]authtypes.LoginType, len(requiredStages)) + copy(completed, completedStages) + copy(required, requiredStages) + + // Sort the slices for simple comparison + sort.Slice(completed, func(i, j int) bool { return completed[i] < completed[j] }) + sort.Slice(required, func(i, j int) bool { return required[i] < required[j] }) + + // Iterate through each slice, going to the next required slice only once + // we've found a match. + i, j := 0, 0 + for j < len(required) { + // Exit if we've reached the end of our input without being able to + // match all of the required stages. + if i >= len(completed) { + return false + } + + // If we've found a stage we want, move on to the next required stage. + if completed[i] == required[j] { + j++ + } + i++ + } + return true +} + +// checkFlowCompleted checks if a authentication flow complies with any allowed flow +// dictated by the server. Order of stages does not matter. A user may complete +// extra stages as long as the required stages of at least one flow is met. +func checkFlowCompleted( + flow []authtypes.LoginType, + allowedFlows []authtypes.Flow, +) bool { + // Iterate through possible flows to check whether any have been fully completed. + for _, allowedFlow := range allowedFlows { + if checkFlows(flow, allowedFlow.Stages) { + return true + } + } + return false +} + +// 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"` +} + +// 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: http.StatusConflict, + JSON: jsonerror.Unknown("Captcha authentication is disabled"), + } + } + + if response == "" { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + 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: http.StatusInternalServerError, + 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: http.StatusGatewayTimeout, + JSON: jsonerror.Unknown("Error in contacting captcha server" + err.Error()), + } + } + err = json.Unmarshal(body, &r) + if err != nil { + return &util.JSONResponse{ + Code: http.StatusInternalServerError, + 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: http.StatusUnauthorized, + JSON: jsonerror.BadJSON("Invalid captcha response. Please try again."), + } + } + return nil +} + +type availableResponse struct { + Available bool `json:"available"` +} diff --git a/common/config/config.go b/common/config/config.go index 0332d0358..6ff2f0d79 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -240,17 +240,7 @@ type Dendrite struct { // Any information derived from the configuration options for later use. Derived struct { - Registration struct { - // Flows is a slice of flows, which represent one possible way that the client can authenticate a request. - // http://matrix.org/docs/spec/HEAD/client_server/r0.3.0.html#user-interactive-authentication-api - // As long as the generated flows only rely on config file options, - // we can generate them on startup and store them until needed - Flows []authtypes.Flow `json:"flows"` - - // Params that need to be returned to the client during - // registration in order to complete registration stages. - Params map[string]interface{} `json:"params"` - } + Registration UserInteractiveAuthConfig // Application services parsed from their config files // The paths of which were given above in the main config file @@ -270,6 +260,23 @@ type Dendrite struct { } `yaml:"-"` } +// UserInteractiveAuthConfig is a configuration for user interactive Flow API. +// It contains sets of auth flows, which represent the possible ways a client +// can authenticate via User Interactive Authentication as well as any +// parameters returned to the client during this process. +// http://matrix.org/docs/spec/HEAD/client_server/r0.3.0.html#user-interactive-authentication-api +type UserInteractiveAuthConfig struct { + // Flows is a slice of flows, which represent one possible way that the client can authenticate a request. + // http://matrix.org/docs/spec/HEAD/client_server/r0.3.0.html#user-interactive-authentication-api + // As long as the generated flows only rely on config file options, + // we can generate them on startup and store them until needed + Flows []authtypes.Flow `json:"flows"` + + // Params that need to be returned to the client + // in order to complete auth stages. + Params map[string]interface{} `json:"params"` +} + // A Path on the filesystem. type Path string