2017-10-11 12:16:53 -05:00
|
|
|
// Copyright 2017 Vector Creations Ltd
|
|
|
|
// Copyright 2017 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
|
2017-05-22 09:55:39 -05:00
|
|
|
|
|
|
|
import (
|
2017-09-18 08:15:27 -05:00
|
|
|
"context"
|
2017-09-22 10:13:19 -05:00
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha1"
|
2017-12-05 10:16:14 -06:00
|
|
|
"encoding/json"
|
2017-09-22 10:13:19 -05:00
|
|
|
"errors"
|
2017-05-22 09:55:39 -05:00
|
|
|
"fmt"
|
2017-12-05 10:16:14 -06:00
|
|
|
"io/ioutil"
|
2017-05-22 09:55:39 -05:00
|
|
|
"net/http"
|
2017-12-05 10:16:14 -06:00
|
|
|
"net/url"
|
2017-09-22 10:13:19 -05:00
|
|
|
"regexp"
|
2017-11-29 03:43:03 -06:00
|
|
|
"sort"
|
2017-09-22 10:13:19 -05:00
|
|
|
"strings"
|
2017-12-05 10:16:14 -06:00
|
|
|
"time"
|
2017-05-22 09:55:39 -05:00
|
|
|
|
2017-09-22 10:13:19 -05:00
|
|
|
"github.com/matrix-org/dendrite/common/config"
|
|
|
|
|
2017-05-30 11:51:40 -05:00
|
|
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
2017-05-23 11:43:05 -05:00
|
|
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
2017-05-22 13:28:26 -05:00
|
|
|
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
|
2017-05-30 11:51:40 -05:00
|
|
|
"github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
|
2017-05-22 09:55:39 -05:00
|
|
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
|
|
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
|
|
"github.com/matrix-org/gomatrixserverlib"
|
|
|
|
"github.com/matrix-org/util"
|
2017-11-14 03:56:23 -06:00
|
|
|
log "github.com/sirupsen/logrus"
|
2017-05-22 09:55:39 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
2017-11-29 03:43:03 -06:00
|
|
|
sessionIDLength = 24
|
2017-05-22 09:55:39 -05:00
|
|
|
)
|
|
|
|
|
2017-11-29 03:43:03 -06:00
|
|
|
var (
|
|
|
|
// TODO: Remove old sessions. Need to do so on a session-specific timeout.
|
|
|
|
sessions = make(map[string][]authtypes.LoginType) // Sessions and completed flow stages
|
2017-12-04 03:40:36 -06:00
|
|
|
validUsernameRegex = regexp.MustCompile(`^[0-9a-z_\-./]+$`)
|
2017-11-29 03:43:03 -06:00
|
|
|
)
|
2017-09-22 10:13:19 -05:00
|
|
|
|
2017-05-22 09:55:39 -05:00
|
|
|
// registerRequest represents the submitted registration request.
|
|
|
|
// It can be broken down into 2 sections: the auth dictionary and registration parameters.
|
|
|
|
// Registration parameters vary depending on the request, and will need to remembered across
|
|
|
|
// sessions. If no parameters are supplied, the server should use the parameters previously
|
|
|
|
// remembered. If ANY parameters are supplied, the server should REPLACE all knowledge of
|
2017-06-12 12:30:47 -05:00
|
|
|
// previous parameters with the ones supplied. This mean you cannot "build up" request params.
|
2017-05-22 09:55:39 -05:00
|
|
|
type registerRequest struct {
|
2018-02-08 05:02:48 -06:00
|
|
|
// registration parameters
|
2017-05-22 09:55:39 -05:00
|
|
|
Password string `json:"password"`
|
|
|
|
Username string `json:"username"`
|
2017-09-22 10:13:19 -05:00
|
|
|
Admin bool `json:"admin"`
|
2017-05-22 09:55:39 -05:00
|
|
|
// user-interactive auth params
|
|
|
|
Auth authDict `json:"auth"`
|
2017-11-14 03:59:02 -06:00
|
|
|
|
|
|
|
InitialDisplayName *string `json:"initial_device_display_name"`
|
2018-02-08 05:02:48 -06:00
|
|
|
|
|
|
|
// Application Services place Type in the root of their registration
|
|
|
|
// request, whereas clients place it in the authDict struct.
|
|
|
|
Type authtypes.LoginType `json:"type"`
|
2017-05-22 09:55:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type authDict struct {
|
2017-09-22 10:13:19 -05:00
|
|
|
Type authtypes.LoginType `json:"type"`
|
|
|
|
Session string `json:"session"`
|
|
|
|
Mac gomatrixserverlib.HexString `json:"mac"`
|
2017-11-29 03:43:03 -06:00
|
|
|
|
2017-12-05 10:16:14 -06:00
|
|
|
// Recaptcha
|
|
|
|
Response string `json:"response"`
|
2017-05-22 09:55:39 -05:00
|
|
|
// 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 {
|
2017-11-29 03:43:03 -06:00
|
|
|
Flows []authtypes.Flow `json:"flows"`
|
2017-05-23 11:43:05 -05:00
|
|
|
Completed []authtypes.LoginType `json:"completed"`
|
2017-05-22 09:55:39 -05:00
|
|
|
Params map[string]interface{} `json:"params"`
|
|
|
|
Session string `json:"session"`
|
|
|
|
}
|
|
|
|
|
2017-09-22 10:13:19 -05:00
|
|
|
// legacyRegisterRequest represents the submitted registration request for v1 API.
|
|
|
|
type legacyRegisterRequest struct {
|
|
|
|
Password string `json:"password"`
|
|
|
|
Username string `json:"user"`
|
|
|
|
Admin bool `json:"admin"`
|
|
|
|
Type authtypes.LoginType `json:"type"`
|
|
|
|
Mac gomatrixserverlib.HexString `json:"mac"`
|
|
|
|
}
|
|
|
|
|
2017-11-29 03:43:03 -06:00
|
|
|
// 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 {
|
2017-05-22 09:55:39 -05:00
|
|
|
return userInteractiveResponse{
|
2017-11-29 03:43:03 -06:00
|
|
|
fs, sessions[sessionID], params, sessionID,
|
2017-05-22 09:55:39 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
|
|
|
type registerResponse struct {
|
|
|
|
UserID string `json:"user_id"`
|
|
|
|
AccessToken string `json:"access_token"`
|
|
|
|
HomeServer gomatrixserverlib.ServerName `json:"home_server"`
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
}
|
|
|
|
|
2017-12-05 10:16:14 -06:00
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
2017-10-09 09:24:38 -05:00
|
|
|
// validateUserName returns an error response if the username is invalid
|
|
|
|
func validateUserName(username string) *util.JSONResponse {
|
2017-05-22 09:55:39 -05:00
|
|
|
// https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
|
2017-10-09 09:24:38 -05:00
|
|
|
if len(username) > maxUsernameLength {
|
2017-05-22 09:55:39 -05:00
|
|
|
return &util.JSONResponse{
|
|
|
|
Code: 400,
|
2017-10-09 09:24:38 -05:00
|
|
|
JSON: jsonerror.BadJSON(fmt.Sprintf("'username' >%d characters", maxUsernameLength)),
|
2017-05-22 09:55:39 -05:00
|
|
|
}
|
2017-10-09 09:24:38 -05:00
|
|
|
} else if !validUsernameRegex.MatchString(username) {
|
2017-05-22 09:55:39 -05:00
|
|
|
return &util.JSONResponse{
|
|
|
|
Code: 400,
|
2017-10-09 09:24:38 -05:00
|
|
|
JSON: jsonerror.InvalidUsername("User ID can only contain characters a-z, 0-9, or '_-./'"),
|
2017-05-22 09:55:39 -05:00
|
|
|
}
|
2017-10-09 09:24:38 -05:00
|
|
|
} else if username[0] == '_' { // Regex checks its not a zero length string
|
2017-05-22 09:55:39 -05:00
|
|
|
return &util.JSONResponse{
|
|
|
|
Code: 400,
|
2017-10-09 09:24:38 -05:00
|
|
|
JSON: jsonerror.InvalidUsername("User ID can't start with a '_'"),
|
2017-05-22 09:55:39 -05:00
|
|
|
}
|
2017-10-09 09:24:38 -05:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// validatePassword returns an error response if the password is invalid
|
|
|
|
func validatePassword(password string) *util.JSONResponse {
|
|
|
|
// https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
|
|
|
|
if len(password) > maxPasswordLength {
|
2017-09-22 10:13:19 -05:00
|
|
|
return &util.JSONResponse{
|
|
|
|
Code: 400,
|
2017-10-09 09:24:38 -05:00
|
|
|
JSON: jsonerror.BadJSON(fmt.Sprintf("'password' >%d characters", maxPasswordLength)),
|
2017-09-22 10:13:19 -05:00
|
|
|
}
|
2017-10-09 09:24:38 -05:00
|
|
|
} else if len(password) > 0 && len(password) < minPasswordLength {
|
2017-09-22 10:13:19 -05:00
|
|
|
return &util.JSONResponse{
|
|
|
|
Code: 400,
|
2017-10-09 09:24:38 -05:00
|
|
|
JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", minPasswordLength)),
|
2017-09-22 10:13:19 -05:00
|
|
|
}
|
2017-05-22 09:55:39 -05:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-12-05 10:16:14 -06:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2018-02-08 05:02:48 -06:00
|
|
|
// UsernameIsWithinApplicationServiceNamespace checks to see if a username 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
|
|
|
|
// Application Service's namespace.
|
|
|
|
func UsernameIsWithinApplicationServiceNamespace(
|
|
|
|
cfg *config.Dendrite,
|
|
|
|
username string,
|
|
|
|
appservice *config.ApplicationService,
|
|
|
|
) bool {
|
|
|
|
if appservice != nil {
|
|
|
|
// Loop through given Application Service's namespaces and see if any match
|
|
|
|
for _, namespace := range appservice.NamespaceMap["users"] {
|
|
|
|
// AS namespaces are checked for validity in config
|
|
|
|
if namespace.RegexpObject.MatchString(username) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop through all known Application Service's namespaces and see if any match
|
|
|
|
for _, knownAppservice := range cfg.Derived.ApplicationServices {
|
|
|
|
for _, namespace := range knownAppservice.NamespaceMap["users"] {
|
|
|
|
// AS namespaces are checked for validity in config
|
|
|
|
if namespace.RegexpObject.MatchString(username) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// UsernameMatchesMultipleExclusiveNamespaces will check if a given username matches
|
|
|
|
// more than one exclusive namespace. More than one is not allowed
|
|
|
|
func UsernameMatchesMultipleExclusiveNamespaces(
|
|
|
|
cfg *config.Dendrite,
|
|
|
|
username string,
|
|
|
|
) bool {
|
|
|
|
// Check namespaces and see if more than one match
|
|
|
|
matchCount := 0
|
|
|
|
for _, appservice := range cfg.Derived.ApplicationServices {
|
|
|
|
for _, namespaceSlice := range appservice.NamespaceMap {
|
|
|
|
for _, namespace := range namespaceSlice {
|
|
|
|
// Check if we have a match on this username
|
|
|
|
if namespace.RegexpObject.MatchString(username) {
|
|
|
|
matchCount++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return matchCount > 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// validateApplicationService checks if a provided application service token
|
|
|
|
// corresponds to one that is registered. If so, then it checks if the desired
|
|
|
|
// username is within that application service's namespace. As long as these
|
|
|
|
// two requirements are met, no error will be returned.
|
|
|
|
func validateApplicationService(
|
|
|
|
cfg *config.Dendrite,
|
|
|
|
req *http.Request,
|
|
|
|
username string,
|
|
|
|
) (string, *util.JSONResponse) {
|
|
|
|
// Check if the token if the application service is valid with one we have
|
|
|
|
// registered in the config.
|
|
|
|
accessToken := req.URL.Query().Get("access_token")
|
|
|
|
var matchedApplicationService *config.ApplicationService
|
|
|
|
for _, appservice := range cfg.Derived.ApplicationServices {
|
|
|
|
if appservice.ASToken == accessToken {
|
|
|
|
matchedApplicationService = &appservice
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if matchedApplicationService != nil {
|
|
|
|
return "", &util.JSONResponse{
|
|
|
|
Code: 401,
|
|
|
|
JSON: jsonerror.UnknownToken("Supplied access_token does not match any known application service"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the desired username is within at least one of the application service's namespaces.
|
|
|
|
if !UsernameIsWithinApplicationServiceNamespace(cfg, username, matchedApplicationService) {
|
|
|
|
// If we didn't find any matches, return M_EXCLUSIVE
|
|
|
|
return "", &util.JSONResponse{
|
|
|
|
Code: 401,
|
|
|
|
JSON: jsonerror.ASExclusive(fmt.Sprintf(
|
|
|
|
"Supplied username %s did not match any namespaces for application service ID: %s", username, matchedApplicationService.ID)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check this user does not fit multiple application service namespaces
|
|
|
|
if UsernameMatchesMultipleExclusiveNamespaces(cfg, username) {
|
|
|
|
return "", &util.JSONResponse{
|
|
|
|
Code: 401,
|
|
|
|
JSON: jsonerror.ASExclusive(fmt.Sprintf(
|
|
|
|
"Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No errors, registration valid
|
|
|
|
return matchedApplicationService.ID, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register processes a /register request.
|
|
|
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
2017-09-22 10:13:19 -05:00
|
|
|
func Register(
|
|
|
|
req *http.Request,
|
|
|
|
accountDB *accounts.Database,
|
|
|
|
deviceDB *devices.Database,
|
|
|
|
cfg *config.Dendrite,
|
|
|
|
) util.JSONResponse {
|
2017-11-29 03:43:03 -06:00
|
|
|
|
2017-05-22 09:55:39 -05:00
|
|
|
var r registerRequest
|
|
|
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
|
|
|
if resErr != nil {
|
|
|
|
return *resErr
|
|
|
|
}
|
2017-09-22 10:38:22 -05:00
|
|
|
|
2017-11-29 03:43:03 -06:00
|
|
|
// Retrieve or generate the sessionID
|
|
|
|
sessionID := r.Auth.Session
|
|
|
|
if sessionID == "" {
|
|
|
|
// Generate a new, random session ID
|
|
|
|
sessionID = util.RandomString(sessionIDLength)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no auth type is specified by the client, send back the list of available flows
|
2017-09-22 10:38:22 -05:00
|
|
|
if r.Auth.Type == "" {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 401,
|
2017-11-29 03:43:03 -06:00
|
|
|
JSON: newUserInteractiveResponse(sessionID,
|
|
|
|
cfg.Derived.Registration.Flows, cfg.Derived.Registration.Params),
|
2017-09-22 10:38:22 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-04 03:40:36 -06:00
|
|
|
// Squash username to all lowercase letters
|
|
|
|
r.Username = strings.ToLower(r.Username)
|
|
|
|
|
2017-10-09 09:24:38 -05:00
|
|
|
if resErr = validateUserName(r.Username); resErr != nil {
|
|
|
|
return *resErr
|
|
|
|
}
|
|
|
|
if resErr = validatePassword(r.Password); resErr != nil {
|
2017-05-22 09:55:39 -05:00
|
|
|
return *resErr
|
|
|
|
}
|
|
|
|
|
2018-02-08 05:02:48 -06:00
|
|
|
// Make sure normal user isn't registering under an exclusive application
|
|
|
|
// service namespace
|
|
|
|
if r.Auth.Type != "m.login.application_service" &&
|
|
|
|
cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(r.Username) {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 400,
|
|
|
|
JSON: jsonerror.ASExclusive("This username is reserved by an application service."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-22 09:55:39 -05:00
|
|
|
logger := util.GetLogger(req.Context())
|
|
|
|
logger.WithFields(log.Fields{
|
|
|
|
"username": r.Username,
|
|
|
|
"auth.type": r.Auth.Type,
|
|
|
|
"session_id": r.Auth.Session,
|
|
|
|
}).Info("Processing registration request")
|
|
|
|
|
2017-11-29 03:43:03 -06:00
|
|
|
return handleRegistrationFlow(req, r, sessionID, cfg, accountDB, deviceDB)
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleRegistrationFlow will direct and complete registration flow stages
|
|
|
|
// that the client has requested.
|
|
|
|
func handleRegistrationFlow(
|
|
|
|
req *http.Request,
|
|
|
|
r registerRequest,
|
|
|
|
sessionID string,
|
|
|
|
cfg *config.Dendrite,
|
|
|
|
accountDB *accounts.Database,
|
|
|
|
deviceDB *devices.Database,
|
|
|
|
) util.JSONResponse {
|
2017-05-22 09:55:39 -05:00
|
|
|
// TODO: Shared secret registration (create new user scripts)
|
|
|
|
// TODO: Enable registration config flag
|
|
|
|
// TODO: Guest account upgrading
|
|
|
|
|
|
|
|
// TODO: Handle loading of previous session parameters from database.
|
|
|
|
// TODO: Handle mapping registrationRequest parameters into session parameters
|
|
|
|
|
2017-12-05 10:16:14 -06:00
|
|
|
// TODO: email / msisdn auth types.
|
2017-12-04 11:07:45 -06:00
|
|
|
|
|
|
|
if cfg.Matrix.RegistrationDisabled && r.Auth.Type != authtypes.LoginTypeSharedSecret {
|
|
|
|
return util.MessageResponse(403, "Registration has been disabled")
|
|
|
|
}
|
|
|
|
|
2017-05-22 09:55:39 -05:00
|
|
|
switch r.Auth.Type {
|
2017-12-05 10:16:14 -06:00
|
|
|
case authtypes.LoginTypeRecaptcha:
|
|
|
|
// Check given captcha response
|
|
|
|
resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
|
|
|
|
if resErr != nil {
|
|
|
|
return *resErr
|
2017-09-22 10:13:19 -05:00
|
|
|
}
|
|
|
|
|
2017-12-05 10:16:14 -06:00
|
|
|
// Add Recaptcha to the list of completed registration stages
|
|
|
|
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)
|
2017-09-22 10:13:19 -05:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return httputil.LogThenError(req, err)
|
2017-12-05 10:16:14 -06:00
|
|
|
} else if !valid {
|
2017-09-22 10:13:19 -05:00
|
|
|
return util.MessageResponse(403, "HMAC incorrect")
|
|
|
|
}
|
|
|
|
|
2017-11-29 03:43:03 -06:00
|
|
|
// Add SharedSecret to the list of completed registration stages
|
|
|
|
sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeSharedSecret)
|
|
|
|
|
2018-02-08 05:02:48 -06:00
|
|
|
case authtypes.LoginTypeApplicationService:
|
|
|
|
// Check Application Service register user request is valid.
|
|
|
|
// The application service's ID is returned if so.
|
|
|
|
appserviceID, err := validateApplicationService(cfg, req, r.Username)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return *err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no error, application service was successfully validated.
|
|
|
|
// Don't need to worry about appending to registration stages as
|
|
|
|
// application service registration is entirely separate.
|
|
|
|
return completeRegistration(req.Context(), accountDB, deviceDB,
|
|
|
|
r.Username, "", appserviceID, r.InitialDisplayName)
|
|
|
|
|
2017-09-22 10:13:19 -05:00
|
|
|
case authtypes.LoginTypeDummy:
|
|
|
|
// there is nothing to do
|
2017-11-29 03:43:03 -06:00
|
|
|
// Add Dummy to the list of completed registration stages
|
|
|
|
sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeDummy)
|
|
|
|
|
2017-09-22 10:13:19 -05:00
|
|
|
default:
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 501,
|
|
|
|
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
|
|
|
}
|
|
|
|
}
|
2017-11-29 03:43:03 -06:00
|
|
|
|
|
|
|
// Check if the user's registration flow has been completed successfully
|
2018-02-08 05:02:48 -06:00
|
|
|
// 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[sessionID], req, r, sessionID, cfg, accountDB, deviceDB)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.InitialDisplayName)
|
2017-11-29 03:43:03 -06:00
|
|
|
}
|
|
|
|
|
2018-02-08 05:02:48 -06:00
|
|
|
// 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.Registration.Flows, cfg.Derived.Registration.Params),
|
|
|
|
}
|
2017-09-22 10:13:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// LegacyRegister process register requests from the legacy v1 API
|
|
|
|
func LegacyRegister(
|
|
|
|
req *http.Request,
|
|
|
|
accountDB *accounts.Database,
|
|
|
|
deviceDB *devices.Database,
|
|
|
|
cfg *config.Dendrite,
|
|
|
|
) util.JSONResponse {
|
|
|
|
var r legacyRegisterRequest
|
2017-12-04 11:07:45 -06:00
|
|
|
resErr := parseAndValidateLegacyLogin(req, &r)
|
2017-09-22 10:13:19 -05:00
|
|
|
if resErr != nil {
|
|
|
|
return *resErr
|
|
|
|
}
|
2017-12-04 03:40:36 -06:00
|
|
|
|
2017-09-22 10:13:19 -05:00
|
|
|
logger := util.GetLogger(req.Context())
|
|
|
|
logger.WithFields(log.Fields{
|
|
|
|
"username": r.Username,
|
|
|
|
"auth.type": r.Type,
|
|
|
|
}).Info("Processing registration request")
|
|
|
|
|
2017-12-04 11:07:45 -06:00
|
|
|
if cfg.Matrix.RegistrationDisabled && r.Type != authtypes.LoginTypeSharedSecret {
|
|
|
|
return util.MessageResponse(403, "Registration has been disabled")
|
2017-09-22 10:13:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
switch r.Type {
|
|
|
|
case authtypes.LoginTypeSharedSecret:
|
|
|
|
if cfg.Matrix.RegistrationSharedSecret == "" {
|
|
|
|
return util.MessageResponse(400, "Shared secret registration is disabled")
|
|
|
|
}
|
|
|
|
|
2017-12-05 10:16:14 -06:00
|
|
|
valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Mac)
|
2017-09-22 10:13:19 -05:00
|
|
|
if err != nil {
|
|
|
|
return httputil.LogThenError(req, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !valid {
|
|
|
|
return util.MessageResponse(403, "HMAC incorrect")
|
|
|
|
}
|
|
|
|
|
2018-02-08 05:02:48 -06:00
|
|
|
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", nil)
|
2017-05-23 11:43:05 -05:00
|
|
|
case authtypes.LoginTypeDummy:
|
2017-05-22 09:55:39 -05:00
|
|
|
// there is nothing to do
|
2018-02-08 05:02:48 -06:00
|
|
|
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", nil)
|
2017-05-22 09:55:39 -05:00
|
|
|
default:
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 501,
|
|
|
|
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-04 11:07:45 -06:00
|
|
|
// parseAndValidateLegacyLogin parses the request into r and checks that the
|
|
|
|
// request is valid (e.g. valid user names, etc)
|
|
|
|
func parseAndValidateLegacyLogin(req *http.Request, r *legacyRegisterRequest) *util.JSONResponse {
|
|
|
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
|
|
|
if resErr != nil {
|
|
|
|
return resErr
|
|
|
|
}
|
|
|
|
|
|
|
|
// Squash username to all lowercase letters
|
|
|
|
r.Username = strings.ToLower(r.Username)
|
|
|
|
|
|
|
|
if resErr = validateUserName(r.Username); resErr != nil {
|
|
|
|
return resErr
|
|
|
|
}
|
|
|
|
if resErr = validatePassword(r.Password); resErr != nil {
|
|
|
|
return resErr
|
|
|
|
}
|
|
|
|
|
|
|
|
// All registration requests must specify what auth they are using to perform this request
|
|
|
|
if r.Type == "" {
|
|
|
|
return &util.JSONResponse{
|
|
|
|
Code: 400,
|
|
|
|
JSON: jsonerror.BadJSON("invalid type"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-09-18 08:15:27 -05:00
|
|
|
func completeRegistration(
|
|
|
|
ctx context.Context,
|
|
|
|
accountDB *accounts.Database,
|
|
|
|
deviceDB *devices.Database,
|
2018-02-08 05:02:48 -06:00
|
|
|
username, password, appserviceID string,
|
2017-11-14 03:59:02 -06:00
|
|
|
displayName *string,
|
2017-09-18 08:15:27 -05:00
|
|
|
) util.JSONResponse {
|
2017-05-30 11:51:40 -05:00
|
|
|
if username == "" {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 400,
|
|
|
|
JSON: jsonerror.BadJSON("missing username"),
|
|
|
|
}
|
|
|
|
}
|
2018-02-08 05:02:48 -06:00
|
|
|
// Blank passwords are only allowed by registered application services
|
|
|
|
if password == "" && appserviceID == "" {
|
2017-05-30 11:51:40 -05:00
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 400,
|
|
|
|
JSON: jsonerror.BadJSON("missing password"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-08 05:02:48 -06:00
|
|
|
acc, err := accountDB.CreateAccount(ctx, username, password, appserviceID)
|
2017-05-22 09:55:39 -05:00
|
|
|
if err != nil {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 500,
|
|
|
|
JSON: jsonerror.Unknown("failed to create account: " + err.Error()),
|
|
|
|
}
|
2017-12-19 03:49:42 -06:00
|
|
|
} else if acc == nil {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 400,
|
|
|
|
JSON: jsonerror.UserInUse("Desired user ID is already taken."),
|
|
|
|
}
|
2017-05-22 09:55:39 -05:00
|
|
|
}
|
2017-05-30 11:51:40 -05:00
|
|
|
|
|
|
|
token, err := auth.GenerateAccessToken()
|
|
|
|
if err != nil {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 500,
|
|
|
|
JSON: jsonerror.Unknown("Failed to generate access token"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// // TODO: Use the device ID in the request.
|
2017-11-14 03:59:02 -06:00
|
|
|
dev, err := deviceDB.CreateDevice(ctx, username, nil, token, displayName)
|
2017-05-30 11:51:40 -05:00
|
|
|
if err != nil {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 500,
|
|
|
|
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-22 09:55:39 -05:00
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 200,
|
|
|
|
JSON: registerResponse{
|
2017-05-30 11:51:40 -05:00
|
|
|
UserID: dev.UserID,
|
|
|
|
AccessToken: dev.AccessToken,
|
2017-05-22 09:55:39 -05:00
|
|
|
HomeServer: acc.ServerName,
|
2017-05-30 11:51:40 -05:00
|
|
|
DeviceID: dev.ID,
|
2017-05-22 09:55:39 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2017-09-22 10:13:19 -05:00
|
|
|
|
|
|
|
// Used for shared secret registration.
|
|
|
|
// Checks if the username, password and isAdmin flag matches the given mac.
|
|
|
|
func isValidMacLogin(
|
2017-12-05 10:16:14 -06:00
|
|
|
cfg *config.Dendrite,
|
2017-09-22 10:13:19 -05:00
|
|
|
username, password string,
|
|
|
|
isAdmin bool,
|
|
|
|
givenMac []byte,
|
|
|
|
) (bool, error) {
|
2017-12-05 10:16:14 -06:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2017-11-29 03:43:03 -06:00
|
|
|
// Double check that username/password don't contain the HMAC delimiters. We should have
|
2017-09-22 10:13:19 -05:00
|
|
|
// already checked this.
|
|
|
|
if strings.Contains(username, "\x00") {
|
|
|
|
return false, errors.New("Username contains invalid character")
|
|
|
|
}
|
|
|
|
if strings.Contains(password, "\x00") {
|
|
|
|
return false, errors.New("Password contains invalid character")
|
|
|
|
}
|
|
|
|
if sharedSecret == "" {
|
|
|
|
return false, errors.New("Shared secret registration is disabled")
|
|
|
|
}
|
|
|
|
|
|
|
|
adminString := "notadmin"
|
|
|
|
if isAdmin {
|
|
|
|
adminString = "admin"
|
|
|
|
}
|
|
|
|
joined := strings.Join([]string{username, password, adminString}, "\x00")
|
|
|
|
|
|
|
|
mac := hmac.New(sha1.New, []byte(sharedSecret))
|
|
|
|
_, err := mac.Write([]byte(joined))
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
expectedMAC := mac.Sum(nil)
|
|
|
|
|
|
|
|
return hmac.Equal(givenMac, expectedMAC), nil
|
|
|
|
}
|
2017-10-09 09:24:38 -05:00
|
|
|
|
2017-11-29 03:43:03 -06:00
|
|
|
// 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.
|
2018-02-08 05:02:48 -06:00
|
|
|
func checkFlowCompleted(
|
|
|
|
flow []authtypes.LoginType,
|
|
|
|
allowedFlows []authtypes.Flow,
|
|
|
|
) bool {
|
2017-11-29 03:43:03 -06:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2017-10-09 09:24:38 -05:00
|
|
|
type availableResponse struct {
|
|
|
|
Available bool `json:"available"`
|
|
|
|
}
|
|
|
|
|
2017-11-29 03:43:03 -06:00
|
|
|
// RegisterAvailable checks if the username is already taken or invalid.
|
2017-10-09 09:24:38 -05:00
|
|
|
func RegisterAvailable(
|
|
|
|
req *http.Request,
|
|
|
|
accountDB *accounts.Database,
|
|
|
|
) util.JSONResponse {
|
|
|
|
username := req.URL.Query().Get("username")
|
|
|
|
|
2017-12-04 03:40:36 -06:00
|
|
|
// Squash username to all lowercase letters
|
|
|
|
username = strings.ToLower(username)
|
|
|
|
|
2017-10-09 09:24:38 -05:00
|
|
|
if err := validateUserName(username); err != nil {
|
|
|
|
return *err
|
|
|
|
}
|
|
|
|
|
|
|
|
availability, availabilityErr := accountDB.CheckAccountAvailability(req.Context(), username)
|
|
|
|
if availabilityErr != nil {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 500,
|
|
|
|
JSON: jsonerror.Unknown("failed to check availability: " + availabilityErr.Error()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !availability {
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 400,
|
|
|
|
JSON: jsonerror.InvalidUsername("A different user ID has already been registered for this session"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return util.JSONResponse{
|
|
|
|
Code: 200,
|
|
|
|
JSON: availableResponse{
|
|
|
|
Available: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|