dendrite/clientapi/routing/register.go
Till d357615452
Don't send device list updates upon registration (#3307)
Fixes https://github.com/matrix-org/dendrite/issues/3273

As we otherwise send down device list updates which are merely useful
for the user and causes tests to be flakey:

```
 TestPushSync/Adding_a_push_rule_wakes_up_an_incremental_/sync (10ms)
      push_test.go:57: no pushrules found in sync response: {"next_batch":"s0_0_0_0_0_1_1_0_1","device_lists":{"changed":["@user-1:hs1"]}}
```

What this does: If a `PerformDeviceCreation` request is coming from
registering an account, it does **not** send device list updates, as
they are merely useful (no joined rooms, no one to inform) . In all
other cases, the behavior is unchanged and device list updates are sent
as usual.
2024-01-20 21:20:37 +01:00

1107 lines
35 KiB
Go

// 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
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/matrix-org/dendrite/internal"
"github.com/tidwall/gjson"
"github.com/matrix-org/dendrite/internal/eventutil"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/gomatrixserverlib/tokens"
"github.com/matrix-org/util"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/clientapi/auth"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/userutil"
userapi "github.com/matrix-org/dendrite/userapi/api"
)
var (
// Prometheus metrics
amtRegUsers = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "dendrite_clientapi_reg_users_total",
Help: "Total number of registered users",
},
)
)
const sessionIDLength = 24
// sessionsDict keeps track of completed auth stages for each session.
// It shouldn't be passed by value because it contains a mutex.
type sessionsDict struct {
sync.RWMutex
sessions map[string][]authtypes.LoginType
sessionCompletedResult map[string]registerResponse
params map[string]registerRequest
timer map[string]*time.Timer
// deleteSessionToDeviceID protects requests to DELETE /devices/{deviceID} from being abused.
// If a UIA session is started by trying to delete device1, and then UIA is completed by deleting device2,
// the delete request will fail for device2 since the UIA was initiated by trying to delete device1.
deleteSessionToDeviceID map[string]string
}
// defaultTimeout is the timeout used to clean up sessions
const defaultTimeOut = time.Minute * 5
// getCompletedStages returns the completed stages for a session.
func (d *sessionsDict) getCompletedStages(sessionID string) []authtypes.LoginType {
d.RLock()
defer d.RUnlock()
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)
}
// addParams adds a registerRequest to a sessionID and starts a timer to delete that registerRequest
func (d *sessionsDict) addParams(sessionID string, r registerRequest) {
d.startTimer(defaultTimeOut, sessionID)
d.Lock()
defer d.Unlock()
d.params[sessionID] = r
}
func (d *sessionsDict) getParams(sessionID string) (registerRequest, bool) {
d.RLock()
defer d.RUnlock()
r, ok := d.params[sessionID]
return r, ok
}
// deleteSession cleans up a given session, either because the registration completed
// successfully, or because a given timeout (default: 5min) was reached.
func (d *sessionsDict) deleteSession(sessionID string) {
d.Lock()
defer d.Unlock()
delete(d.params, sessionID)
delete(d.sessions, sessionID)
delete(d.deleteSessionToDeviceID, sessionID)
delete(d.sessionCompletedResult, sessionID)
// stop the timer, e.g. because the registration was completed
if t, ok := d.timer[sessionID]; ok {
if !t.Stop() {
select {
case <-t.C:
default:
}
}
delete(d.timer, sessionID)
}
}
func newSessionsDict() *sessionsDict {
return &sessionsDict{
sessions: make(map[string][]authtypes.LoginType),
sessionCompletedResult: make(map[string]registerResponse),
params: make(map[string]registerRequest),
timer: make(map[string]*time.Timer),
deleteSessionToDeviceID: make(map[string]string),
}
}
func (d *sessionsDict) startTimer(duration time.Duration, sessionID string) {
d.Lock()
defer d.Unlock()
t, ok := d.timer[sessionID]
if ok {
if !t.Stop() {
<-t.C
}
t.Reset(duration)
return
}
d.timer[sessionID] = time.AfterFunc(duration, func() {
d.deleteSession(sessionID)
})
}
// addCompletedSessionStage records that a session has completed an auth stage
// also starts a timer to delete the session once done.
func (d *sessionsDict) addCompletedSessionStage(sessionID string, stage authtypes.LoginType) {
d.startTimer(defaultTimeOut, sessionID)
d.Lock()
defer d.Unlock()
for _, completedStage := range d.sessions[sessionID] {
if completedStage == stage {
return
}
}
d.sessions[sessionID] = append(d.sessions[sessionID], stage)
}
func (d *sessionsDict) addDeviceToDelete(sessionID, deviceID string) {
d.startTimer(defaultTimeOut, sessionID)
d.Lock()
defer d.Unlock()
d.deleteSessionToDeviceID[sessionID] = deviceID
}
func (d *sessionsDict) addCompletedRegistration(sessionID string, response registerResponse) {
d.Lock()
defer d.Unlock()
d.sessionCompletedResult[sessionID] = response
}
func (d *sessionsDict) getCompletedRegistration(sessionID string) (registerResponse, bool) {
d.RLock()
defer d.RUnlock()
result, ok := d.sessionCompletedResult[sessionID]
return result, ok
}
func (d *sessionsDict) getDeviceToDelete(sessionID string) (string, bool) {
d.RLock()
defer d.RUnlock()
deviceID, ok := d.deleteSessionToDeviceID[sessionID]
return deviceID, ok
}
var (
sessions = newSessionsDict()
)
// 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
// previous parameters with the ones supplied. This mean you cannot "build up" request params.
type registerRequest struct {
// registration parameters
Password string `json:"password"`
Username string `json:"username"`
ServerName spec.ServerName `json:"-"`
Admin bool `json:"admin"`
// user-interactive auth params
Auth authDict `json:"auth"`
// Both DeviceID and InitialDisplayName can be omitted, or empty strings ("")
// Thus a pointer is needed to differentiate between the two
InitialDisplayName *string `json:"initial_device_display_name"`
DeviceID *string `json:"device_id"`
// Prevent this user from logging in
InhibitLogin eventutil.WeakBoolean `json:"inhibit_login"`
// Application Services place Type in the root of their registration
// 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
}
// https://spec.matrix.org/v1.7/client-server-api/#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
// during registration.
func newUserInteractiveResponse(
sessionID string,
fs []authtypes.Flow,
params map[string]interface{},
) userInteractiveResponse {
return userInteractiveResponse{
fs, sessions.getCompletedStages(sessionID), params, sessionID,
}
}
// https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
type registerResponse struct {
UserID string `json:"user_id"`
AccessToken string `json:"access_token,omitempty"`
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"`
}
var (
ErrInvalidCaptcha = errors.New("invalid captcha response")
ErrMissingResponse = errors.New("captcha response is required")
ErrCaptchaDisabled = errors.New("captcha registration is disabled")
)
// validateRecaptcha returns an error response if the captcha response is invalid
func validateRecaptcha(
cfg *config.ClientAPI,
response string,
clientip string,
) error {
ip, _, _ := net.SplitHostPort(clientip)
if !cfg.RecaptchaEnabled {
return ErrCaptchaDisabled
}
if response == "" {
return ErrMissingResponse
}
// Make a POST request to the captcha provider API to check the captcha response
resp, err := http.PostForm(cfg.RecaptchaSiteVerifyAPI,
url.Values{
"secret": {cfg.RecaptchaPrivateKey},
"response": {response},
"remoteip": {ip},
},
)
if err != nil {
return err
}
// 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 := io.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, &r)
if err != nil {
return err
}
// Check that we received a "success"
if !r.Success {
return ErrInvalidCaptcha
}
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
// Application Service's namespace.
func UserIDIsWithinApplicationServiceNamespace(
cfg *config.ClientAPI,
userID string,
appservice *config.ApplicationService,
) bool {
var local, domain, err = gomatrixserverlib.SplitID('@', userID)
if err != nil {
// Not a valid userID
return false
}
if !cfg.Matrix.IsLocalServerName(domain) {
return false
}
if appservice != nil {
if appservice.SenderLocalpart == local {
return true
}
// 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(userID) {
return true
}
}
return false
}
// Loop through all known application service's namespaces and see if any match
for _, knownAppService := range cfg.Derived.ApplicationServices {
if knownAppService.SenderLocalpart == local {
return true
}
for _, namespace := range knownAppService.NamespaceMap["users"] {
// AS namespaces are checked for validity in config
if namespace.RegexpObject.MatchString(userID) {
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.ClientAPI,
username string,
) bool {
userID := userutil.MakeUserID(username, cfg.Matrix.ServerName)
// Check namespaces and see if more than one match
matchCount := 0
for _, appservice := range cfg.Derived.ApplicationServices {
if appservice.OwnsNamespaceCoveringUserId(userID) {
if matchCount++; matchCount > 1 {
return true
}
}
}
return false
}
// UsernameMatchesExclusiveNamespaces will check if a given username matches any
// application service's exclusive users namespace
func UsernameMatchesExclusiveNamespaces(
cfg *config.ClientAPI,
username string,
) bool {
userID := userutil.MakeUserID(username, cfg.Matrix.ServerName)
return cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(userID)
}
// 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.ClientAPI,
username string,
accessToken string,
) (string, *util.JSONResponse) {
// Check if the token if the application service is valid with one we have
// registered in the config.
var matchedApplicationService *config.ApplicationService
for _, appservice := range cfg.Derived.ApplicationServices {
if appservice.ASToken == accessToken {
matchedApplicationService = &appservice
break
}
}
if matchedApplicationService == nil {
return "", &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: spec.UnknownToken("Supplied access_token does not match any known application service"),
}
}
userID := userutil.MakeUserID(username, cfg.Matrix.ServerName)
// Ensure the desired username is within at least one of the application service's namespaces.
if !UserIDIsWithinApplicationServiceNamespace(cfg, userID, matchedApplicationService) {
// If we didn't find any matches, return M_EXCLUSIVE
return "", &util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.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, userID) {
return "", &util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.ASExclusive(fmt.Sprintf(
"Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)),
}
}
// Check username application service is trying to register is valid
if err := internal.ValidateApplicationServiceUsername(username, cfg.Matrix.ServerName); err != nil {
return "", internal.UsernameResponse(err)
}
// No errors, registration valid
return matchedApplicationService.ID, nil
}
// Register processes a /register request.
// https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
func Register(
req *http.Request,
userAPI userapi.ClientUserAPI,
cfg *config.ClientAPI,
) util.JSONResponse {
defer req.Body.Close() // nolint: errcheck
reqBody, err := io.ReadAll(req.Body)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.NotJSON("Unable to read request body"),
}
}
var r registerRequest
host := spec.ServerName(req.Host)
if v := cfg.Matrix.VirtualHostForHTTPHost(host); v != nil {
r.ServerName = v.ServerName
} else {
r.ServerName = cfg.Matrix.ServerName
}
sessionID := gjson.GetBytes(reqBody, "auth.session").String()
if sessionID == "" {
// Generate a new, random session ID
sessionID = util.RandomString(sessionIDLength)
} else if data, ok := sessions.getParams(sessionID); ok {
// Use the parameters from the session as our defaults.
// Some of these might end up being overwritten if the
// values are specified again in the request body.
r.Username = data.Username
r.ServerName = data.ServerName
r.Password = data.Password
r.DeviceID = data.DeviceID
r.InitialDisplayName = data.InitialDisplayName
r.InhibitLogin = data.InhibitLogin
// Check if the user already registered using this session, if so, return that result
if response, ok := sessions.getCompletedRegistration(sessionID); ok {
return util.JSONResponse{
Code: http.StatusOK,
JSON: response,
}
}
}
if resErr := httputil.UnmarshalJSON(reqBody, &r); resErr != nil {
return *resErr
}
if req.URL.Query().Get("kind") == "guest" {
return handleGuestRegistration(req, r, cfg, userAPI)
}
// Don't allow numeric usernames less than MAX_INT64.
if _, err = strconv.ParseInt(r.Username, 10, 64); err == nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.InvalidUsername("Numeric user IDs are reserved"),
}
}
// Auto generate a numeric username if r.Username is empty
if r.Username == "" {
nreq := &userapi.QueryNumericLocalpartRequest{
ServerName: r.ServerName,
}
nres := &userapi.QueryNumericLocalpartResponse{}
if err = userAPI.QueryNumericLocalpart(req.Context(), nreq, nres); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryNumericLocalpart failed")
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.InternalServerError{},
}
}
r.Username = strconv.FormatInt(nres.ID, 10)
}
// Is this an appservice registration? It will be if the access
// token is supplied
accessToken, accessTokenErr := auth.ExtractAccessToken(req)
// Squash username to all lowercase letters
r.Username = strings.ToLower(r.Username)
switch {
case r.Type == authtypes.LoginTypeApplicationService && accessTokenErr == nil:
// Spec-compliant case (the access_token is specified and the login type
// is correctly set, so it's an appservice registration)
if err = internal.ValidateApplicationServiceUsername(r.Username, r.ServerName); err != nil {
return *internal.UsernameResponse(err)
}
case accessTokenErr == nil:
// Non-spec-compliant case (the access_token is specified but the login
// type is not known or specified)
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.MissingParam("A known registration type (e.g. m.login.application_service) must be specified if an access_token is provided"),
}
default:
// Spec-compliant case (neither the access_token nor the login type are
// specified, so it's a normal user registration)
if err = internal.ValidateUsername(r.Username, r.ServerName); err != nil {
return *internal.UsernameResponse(err)
}
}
if err = internal.ValidatePassword(r.Password); err != nil {
return *internal.PasswordResponse(err)
}
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")
return handleRegistrationFlow(req, r, sessionID, cfg, userAPI, accessToken, accessTokenErr)
}
func handleGuestRegistration(
req *http.Request,
r registerRequest,
cfg *config.ClientAPI,
userAPI userapi.ClientUserAPI,
) util.JSONResponse {
registrationEnabled := !cfg.RegistrationDisabled
guestsEnabled := !cfg.GuestsDisabled
if v := cfg.Matrix.VirtualHost(r.ServerName); v != nil {
registrationEnabled, guestsEnabled = v.RegistrationAllowed()
}
if !registrationEnabled || !guestsEnabled {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: spec.Forbidden(
fmt.Sprintf("Guest registration is disabled on %q", r.ServerName),
),
}
}
var res userapi.PerformAccountCreationResponse
err := userAPI.PerformAccountCreation(req.Context(), &userapi.PerformAccountCreationRequest{
AccountType: userapi.AccountTypeGuest,
ServerName: r.ServerName,
}, &res)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.Unknown("failed to create account: " + err.Error()),
}
}
token, err := tokens.GenerateLoginToken(tokens.TokenOptions{
ServerPrivateKey: cfg.Matrix.PrivateKey.Seed(),
ServerName: string(res.Account.ServerName),
UserID: res.Account.UserID,
})
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.Unknown("Failed to generate access token"),
}
}
//we don't allow guests to specify their own device_id
var devRes userapi.PerformDeviceCreationResponse
err = userAPI.PerformDeviceCreation(req.Context(), &userapi.PerformDeviceCreationRequest{
Localpart: res.Account.Localpart,
ServerName: res.Account.ServerName,
DeviceDisplayName: r.InitialDisplayName,
AccessToken: token,
IPAddr: req.RemoteAddr,
UserAgent: req.UserAgent(),
FromRegistration: true,
}, &devRes)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.Unknown("failed to create device: " + err.Error()),
}
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: registerResponse{
UserID: devRes.Device.UserID,
AccessToken: devRes.Device.AccessToken,
DeviceID: devRes.Device.ID,
},
}
}
// localpartMatchesExclusiveNamespaces will check if a given username matches any
// application service's exclusive users namespace
func localpartMatchesExclusiveNamespaces(
cfg *config.ClientAPI,
localpart string,
) bool {
userID := userutil.MakeUserID(localpart, cfg.Matrix.ServerName)
return cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(userID)
}
// handleRegistrationFlow will direct and complete registration flow stages
// that the client has requested.
// nolint: gocyclo
func handleRegistrationFlow(
req *http.Request,
r registerRequest,
sessionID string,
cfg *config.ClientAPI,
userAPI userapi.ClientUserAPI,
accessToken string,
accessTokenErr error,
) util.JSONResponse {
// 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
// TODO: email / msisdn auth types.
// Appservices are special and are not affected by disabled
// registration or user exclusivity. We'll go onto the appservice
// registration flow if a valid access token was provided or if
// the login type specifically requests it.
if r.Type == authtypes.LoginTypeApplicationService && accessTokenErr == nil {
return handleApplicationServiceRegistration(
accessToken, accessTokenErr, req, r, cfg, userAPI,
)
}
registrationEnabled := !cfg.RegistrationDisabled
if v := cfg.Matrix.VirtualHost(r.ServerName); v != nil {
registrationEnabled, _ = v.RegistrationAllowed()
}
if !registrationEnabled && r.Auth.Type != authtypes.LoginTypeSharedSecret {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: spec.Forbidden(
fmt.Sprintf("Registration is disabled on %q", r.ServerName),
),
}
}
// Make sure normal user isn't registering under an exclusive application
// service namespace. Skip this check if no app services are registered.
// If an access token is provided, ignore this check this is an appservice
// request and we will validate in validateApplicationService
if len(cfg.Derived.ApplicationServices) != 0 &&
localpartMatchesExclusiveNamespaces(cfg, r.Username) {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.ASExclusive("This username is reserved by an application service."),
}
}
switch r.Auth.Type {
case authtypes.LoginTypeRecaptcha:
// Check given captcha response
err := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
switch err {
case ErrCaptchaDisabled:
return util.JSONResponse{Code: http.StatusForbidden, JSON: spec.Unknown(err.Error())}
case ErrMissingResponse:
return util.JSONResponse{Code: http.StatusBadRequest, JSON: spec.BadJSON(err.Error())}
case ErrInvalidCaptcha:
return util.JSONResponse{Code: http.StatusUnauthorized, JSON: spec.BadJSON(err.Error())}
case nil:
default:
util.GetLogger(req.Context()).WithError(err).Error("failed to validate recaptcha")
return util.JSONResponse{Code: http.StatusInternalServerError, JSON: spec.InternalServerError{}}
}
// Add Recaptcha to the list of completed registration stages
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
case authtypes.LoginTypeDummy:
// there is nothing to do
// Add Dummy to the list of completed registration stages
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeDummy)
case "":
// An empty auth type means that we want to fetch the available
// flows. It can also mean that we want to register as an appservice
// but that is handed above.
default:
return util.JSONResponse{
Code: http.StatusNotImplemented,
JSON: spec.Unknown("unknown/unimplemented auth type"),
}
}
// 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, userAPI)
}
// handleApplicationServiceRegistration handles the registration of an
// application service's user by validating the AS from its access token and
// registering the user. Its two first parameters must be the two return values
// of the auth.ExtractAccessToken function.
// Returns an error if the access token couldn't be extracted from the request
// at an earlier step of the registration workflow, or if the provided access
// token doesn't belong to a valid AS, or if there was an issue completing the
// registration process.
func handleApplicationServiceRegistration(
accessToken string,
tokenErr error,
req *http.Request,
r registerRequest,
cfg *config.ClientAPI,
userAPI userapi.ClientUserAPI,
) util.JSONResponse {
// Check if we previously had issues extracting the access token from the
// request.
if tokenErr != nil {
return util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: spec.MissingToken(tokenErr.Error()),
}
}
// Check application service register user request is valid.
// The application service's ID is returned if so.
appserviceID, err := internal.ValidateApplicationServiceRequest(
cfg, r.Username, accessToken,
)
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(), userAPI, r.Username, r.ServerName, "", "", appserviceID, req.RemoteAddr,
req.UserAgent(), r.Auth.Session, r.InhibitLogin, r.InitialDisplayName, r.DeviceID,
userapi.AccountTypeAppService,
)
}
// 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.ClientAPI,
userAPI userapi.ClientUserAPI,
) util.JSONResponse {
if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) {
// This flow was completed, registration can continue
return completeRegistration(
req.Context(), userAPI, r.Username, r.ServerName, "", r.Password, "", req.RemoteAddr,
req.UserAgent(), sessionID, r.InhibitLogin, r.InitialDisplayName, r.DeviceID,
userapi.AccountTypeUser,
)
}
sessions.addParams(sessionID, r)
// 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),
}
}
// completeRegistration runs some rudimentary checks against the submitted
// input, then if successful creates an account and a newly associated device
// We pass in each individual part of the request here instead of just passing a
// registerRequest, as this function serves requests encoded as both
// registerRequests and legacyRegisterRequests, which share some attributes but
// not all
func completeRegistration(
ctx context.Context,
userAPI userapi.ClientUserAPI,
username string, serverName spec.ServerName, displayName string,
password, appserviceID, ipAddr, userAgent, sessionID string,
inhibitLogin eventutil.WeakBoolean,
deviceDisplayName, deviceID *string,
accType userapi.AccountType,
) util.JSONResponse {
if username == "" {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.MissingParam("Missing username"),
}
}
// Blank passwords are only allowed by registered application services
if password == "" && appserviceID == "" {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.MissingParam("Missing password"),
}
}
var accRes userapi.PerformAccountCreationResponse
err := userAPI.PerformAccountCreation(ctx, &userapi.PerformAccountCreationRequest{
AppServiceID: appserviceID,
Localpart: username,
ServerName: serverName,
Password: password,
AccountType: accType,
OnConflict: userapi.ConflictAbort,
}, &accRes)
if err != nil {
if _, ok := err.(*userapi.ErrorConflict); ok { // user already exists
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.UserInUse("Desired user ID is already taken."),
}
}
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.Unknown("failed to create account: " + err.Error()),
}
}
// Increment prometheus counter for created users
amtRegUsers.Inc()
// Check whether inhibit_login option is set. If so, don't create an access
// token or a device for this user
if inhibitLogin {
return util.JSONResponse{
Code: http.StatusOK,
JSON: registerResponse{
UserID: userutil.MakeUserID(username, accRes.Account.ServerName),
},
}
}
token, err := auth.GenerateAccessToken()
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.Unknown("Failed to generate access token"),
}
}
if displayName != "" {
_, _, err = userAPI.SetDisplayName(ctx, username, serverName, displayName)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.Unknown("failed to set display name: " + err.Error()),
}
}
}
var devRes userapi.PerformDeviceCreationResponse
err = userAPI.PerformDeviceCreation(ctx, &userapi.PerformDeviceCreationRequest{
Localpart: username,
ServerName: serverName,
AccessToken: token,
DeviceDisplayName: deviceDisplayName,
DeviceID: deviceID,
IPAddr: ipAddr,
UserAgent: userAgent,
FromRegistration: true,
}, &devRes)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.Unknown("failed to create device: " + err.Error()),
}
}
result := registerResponse{
UserID: devRes.Device.UserID,
AccessToken: devRes.Device.AccessToken,
DeviceID: devRes.Device.ID,
}
sessions.addCompletedRegistration(sessionID, result)
return util.JSONResponse{
Code: http.StatusOK,
JSON: result,
}
}
// 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,
cfg *config.ClientAPI,
registerAPI userapi.ClientUserAPI,
) util.JSONResponse {
username := req.URL.Query().Get("username")
// Squash username to all lowercase letters
username = strings.ToLower(username)
domain := cfg.Matrix.ServerName
host := spec.ServerName(req.Host)
if v := cfg.Matrix.VirtualHostForHTTPHost(host); v != nil {
domain = v.ServerName
}
if u, l, err := cfg.Matrix.SplitLocalID('@', username); err == nil {
username, domain = u, l
}
for _, v := range cfg.Matrix.VirtualHosts {
if v.ServerName == domain && !v.AllowRegistration {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: spec.Forbidden(
fmt.Sprintf("Registration is not allowed on %q", string(v.ServerName)),
),
}
}
}
if err := internal.ValidateUsername(username, domain); err != nil {
return *internal.UsernameResponse(err)
}
// Check if this username is reserved by an application service
userID := userutil.MakeUserID(username, domain)
for _, appservice := range cfg.Derived.ApplicationServices {
if appservice.OwnsNamespaceCoveringUserId(userID) {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.UserInUse("Desired user ID is reserved by an application service."),
}
}
}
res := &userapi.QueryAccountAvailabilityResponse{}
err := registerAPI.QueryAccountAvailability(req.Context(), &userapi.QueryAccountAvailabilityRequest{
Localpart: username,
ServerName: domain,
}, res)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.Unknown("failed to check availability:" + err.Error()),
}
}
if !res.Available {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.UserInUse("Desired User ID is already taken."),
}
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: availableResponse{
Available: true,
},
}
}
func handleSharedSecretRegistration(cfg *config.ClientAPI, userAPI userapi.ClientUserAPI, sr *SharedSecretRegistration, req *http.Request) util.JSONResponse {
ssrr, err := NewSharedSecretRegistrationRequest(req.Body)
if err != nil {
return util.JSONResponse{
Code: 400,
JSON: spec.BadJSON(fmt.Sprintf("malformed json: %s", err)),
}
}
valid, err := sr.IsValidMacLogin(ssrr.Nonce, ssrr.User, ssrr.Password, ssrr.Admin, ssrr.MacBytes)
if err != nil {
return util.ErrorResponse(err)
}
if !valid {
return util.JSONResponse{
Code: 403,
JSON: spec.Forbidden("bad mac"),
}
}
// downcase capitals
ssrr.User = strings.ToLower(ssrr.User)
if err = internal.ValidateUsername(ssrr.User, cfg.Matrix.ServerName); err != nil {
return *internal.UsernameResponse(err)
}
if err = internal.ValidatePassword(ssrr.Password); err != nil {
return *internal.PasswordResponse(err)
}
deviceID := "shared_secret_registration"
accType := userapi.AccountTypeUser
if ssrr.Admin {
accType = userapi.AccountTypeAdmin
}
return completeRegistration(req.Context(), userAPI, ssrr.User, cfg.Matrix.ServerName, ssrr.DisplayName, ssrr.Password, "", req.RemoteAddr, req.UserAgent(), "", false, &ssrr.User, &deviceID, accType)
}