mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-12 09:23:09 -06:00
[WIP] Refactored registration to align with the spec.
* We now keep track of sessions and their completed registration stages.
* We only complete registration if the client has completed a full flow.
* New Derived section in config for data derived from config options.
* New config options for captcha.
TODO:
* Send params back to client for each auth type (recaptcha pub key).
* Refactor PR into two. One for refactor, other for adding
Recaptcha.
Signed-off-by: Andrew (anoa) <anoa@openmailbox.org>
This commit is contained in:
parent
d2154482dc
commit
98aa6e5421
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright 2017 Vector Creations Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package authtypes
|
||||||
|
|
||||||
|
// AuthFlow represents one possible way that the client can authenticate a request.
|
||||||
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
||||||
|
type Flow struct {
|
||||||
|
Stages []LoginType `json:"stages"`
|
||||||
|
}
|
||||||
|
|
@ -23,9 +23,12 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -46,9 +49,13 @@ const (
|
||||||
minPasswordLength = 8 // http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based
|
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
|
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
|
maxUsernameLength = 254 // http://matrix.org/speculator/spec/HEAD/intro.html#user-identifiers TODO account for domain
|
||||||
|
sessionIDLength = 24
|
||||||
)
|
)
|
||||||
|
|
||||||
var validUsernameRegex = regexp.MustCompile(`^[0-9a-zA-Z_\-./]+$`)
|
var (
|
||||||
|
sessions = make(map[string][]authtypes.LoginType) // Sessions and completed flow stages
|
||||||
|
validUsernameRegex = regexp.MustCompile(`^[0-9a-zA-Z_\-./]+$`)
|
||||||
|
)
|
||||||
|
|
||||||
// registerRequest represents the submitted registration request.
|
// registerRequest represents the submitted registration request.
|
||||||
// It can be broken down into 2 sections: the auth dictionary and registration parameters.
|
// It can be broken down into 2 sections: the auth dictionary and registration parameters.
|
||||||
|
|
@ -79,18 +86,12 @@ type authDict struct {
|
||||||
|
|
||||||
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
||||||
type userInteractiveResponse struct {
|
type userInteractiveResponse struct {
|
||||||
Flows []authFlow `json:"flows"`
|
Flows []authtypes.Flow `json:"flows"`
|
||||||
Completed []authtypes.LoginType `json:"completed"`
|
Completed []authtypes.LoginType `json:"completed"`
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]interface{} `json:"params"`
|
||||||
Session string `json:"session"`
|
Session string `json:"session"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// authFlow represents one possible way that the client can authenticate a request.
|
|
||||||
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
|
||||||
type authFlow struct {
|
|
||||||
Stages []authtypes.LoginType `json:"stages"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// legacyRegisterRequest represents the submitted registration request for v1 API.
|
// legacyRegisterRequest represents the submitted registration request for v1 API.
|
||||||
type legacyRegisterRequest struct {
|
type legacyRegisterRequest struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
|
@ -100,9 +101,9 @@ type legacyRegisterRequest struct {
|
||||||
Mac gomatrixserverlib.HexString `json:"mac"`
|
Mac gomatrixserverlib.HexString `json:"mac"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUserInteractiveResponse(sessionID string, fs []authFlow) userInteractiveResponse {
|
func newUserInteractiveResponse(sessionID string, fs []authtypes.Flow) userInteractiveResponse {
|
||||||
return userInteractiveResponse{
|
return userInteractiveResponse{
|
||||||
fs, []authtypes.LoginType{}, make(map[string]interface{}), sessionID,
|
fs, sessions[sessionID], make(map[string]interface{}), sessionID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,6 +115,7 @@ type registerResponse struct {
|
||||||
DeviceID string `json:"device_id"`
|
DeviceID string `json:"device_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recaptchaResponse represents the HTTP response from a Google ReCaptcha server
|
||||||
type recaptchaResponse struct {
|
type recaptchaResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
ChallengeTS time.Time `json:"challenge_ts"`
|
ChallengeTS time.Time `json:"challenge_ts"`
|
||||||
|
|
@ -190,16 +192,8 @@ func validateRecaptcha(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
// Close the request once we're finishing reading from it
|
||||||
err := resp.Body.Close()
|
defer resp.Body.Close() // noline: errcheck
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger := util.GetLogger(req.Context())
|
|
||||||
logger.WithFields(log.Fields{
|
|
||||||
"response": response,
|
|
||||||
}).Info("Failed to close recaptcha request response body")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Grab the body of the response from the captcha server
|
// Grab the body of the response from the captcha server
|
||||||
var r recaptchaResponse
|
var r recaptchaResponse
|
||||||
|
|
@ -228,6 +222,9 @@ func validateRecaptcha(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Create flows in config.go so that they're cached. Always show msisdn flows as long as flows only depend on config-file options.
|
||||||
|
// Store it just like the config does in a struct and keep it there.
|
||||||
|
|
||||||
// Register processes a /register request. http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
// Register processes a /register request. http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
||||||
func Register(
|
func Register(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
|
|
@ -235,23 +232,25 @@ func Register(
|
||||||
deviceDB *devices.Database,
|
deviceDB *devices.Database,
|
||||||
cfg *config.Dendrite,
|
cfg *config.Dendrite,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
|
|
||||||
var r registerRequest
|
var r registerRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// All registration requests must specify what auth they are using to perform this request
|
// Retrieve or generate the sessionID
|
||||||
|
sessionID := r.Auth.Session
|
||||||
|
if sessionID == "" {
|
||||||
|
// Generate a new, random session ID
|
||||||
|
sessionID = RandString(sessionIDLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no auth type is specified by the client, send back the list of available flows
|
||||||
if r.Auth.Type == "" {
|
if r.Auth.Type == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 401,
|
Code: 401,
|
||||||
// TODO: Hard-coded 'dummy' auth for now with a bogus session ID.
|
JSON: newUserInteractiveResponse(sessionID, cfg.Derived.Flows),
|
||||||
// Server admins should be able to change things around (eg enable captcha)
|
|
||||||
JSON: newUserInteractiveResponse(time.Now().String(), []authFlow{
|
|
||||||
{[]authtypes.LoginType{authtypes.LoginTypeDummy}},
|
|
||||||
{[]authtypes.LoginType{authtypes.LoginTypeRecaptcha}},
|
|
||||||
{[]authtypes.LoginType{authtypes.LoginTypeSharedSecret}},
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +279,7 @@ func Register(
|
||||||
// TODO: email / msisdn auth types.
|
// TODO: email / msisdn auth types.
|
||||||
switch r.Auth.Type {
|
switch r.Auth.Type {
|
||||||
case authtypes.LoginTypeRecaptcha:
|
case authtypes.LoginTypeRecaptcha:
|
||||||
if !cfg.Matrix.RegistrationRecaptcha {
|
if !cfg.Matrix.RecaptchaEnabled {
|
||||||
return util.MessageResponse(400, "Captcha registration is disabled")
|
return util.MessageResponse(400, "Captcha registration is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,8 +293,9 @@ func Register(
|
||||||
if resErr = validateRecaptcha(req, cfg, r.Auth.Response, req.RemoteAddr); resErr != nil {
|
if resErr = validateRecaptcha(req, cfg, r.Auth.Response, req.RemoteAddr); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
return completeRegistration(req.Context(), accountDB, deviceDB,
|
|
||||||
r.Username, r.Password, r.InitialDisplayName)
|
// Add Recaptcha to the list of completed registration stages
|
||||||
|
sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeRecaptcha)
|
||||||
|
|
||||||
case authtypes.LoginTypeSharedSecret:
|
case authtypes.LoginTypeSharedSecret:
|
||||||
if cfg.Matrix.RegistrationSharedSecret == "" {
|
if cfg.Matrix.RegistrationSharedSecret == "" {
|
||||||
|
|
@ -313,18 +313,35 @@ func Register(
|
||||||
return util.MessageResponse(403, "HMAC incorrect")
|
return util.MessageResponse(403, "HMAC incorrect")
|
||||||
}
|
}
|
||||||
|
|
||||||
return completeRegistration(req.Context(), accountDB, deviceDB,
|
// Add SharedSecret to the list of completed registration stages
|
||||||
r.Username, r.Password, r.InitialDisplayName)
|
sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeSharedSecret)
|
||||||
|
|
||||||
case authtypes.LoginTypeDummy:
|
case authtypes.LoginTypeDummy:
|
||||||
// there is nothing to do
|
// there is nothing to do
|
||||||
return completeRegistration(req.Context(), accountDB, deviceDB,
|
// Add Dummy to the list of completed registration stages
|
||||||
r.Username, r.Password, r.InitialDisplayName)
|
sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeDummy)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 501,
|
Code: 501,
|
||||||
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a registration flow has been completed successfully
|
||||||
|
for _, flow := range cfg.Derived.Flows {
|
||||||
|
if checkFlowsEqual(flow, authtypes.Flow{sessions[sessionID]}) {
|
||||||
|
return completeRegistration(req.Context(), accountDB, deviceDB,
|
||||||
|
r.Username, r.Password, r.InitialDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are still more stages to complete.
|
||||||
|
// Return the flows and those that have been completed.
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 401,
|
||||||
|
JSON: newUserInteractiveResponse(sessionID, cfg.Derived.Flows),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LegacyRegister process register requests from the legacy v1 API
|
// LegacyRegister process register requests from the legacy v1 API
|
||||||
|
|
@ -479,6 +496,62 @@ func isValidMacLogin(
|
||||||
return hmac.Equal(givenMac, expectedMAC), nil
|
return hmac.Equal(givenMac, expectedMAC), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
const (
|
||||||
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||||
|
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||||
|
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandString returns a random string of characters with a given length.
|
||||||
|
// Do note that it is not thread-safe in its current form.
|
||||||
|
// https://stackoverflow.com/a/31832326
|
||||||
|
var src = rand.NewSource(time.Now().UnixNano())
|
||||||
|
|
||||||
|
func RandString(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
|
||||||
|
// A src.Int63() generates 63 random bits
|
||||||
|
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||||
|
if remain == 0 {
|
||||||
|
cache, remain = src.Int63(), letterIdxMax
|
||||||
|
}
|
||||||
|
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||||
|
b[i] = letterBytes[idx]
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
cache >>= letterIdxBits
|
||||||
|
remain--
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFlowsEqual checks if two registration flows have the same stages
|
||||||
|
// within them. Order of stages does not matter.
|
||||||
|
func checkFlowsEqual(aFlow, bFlow authtypes.Flow) bool {
|
||||||
|
a := aFlow.Stages
|
||||||
|
b := bFlow.Stages
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
a_copy := make([]string, len(a))
|
||||||
|
b_copy := make([]string, len(b))
|
||||||
|
|
||||||
|
for loginType := range a {
|
||||||
|
a_copy = append(a_copy, string(loginType))
|
||||||
|
}
|
||||||
|
for loginType := range b {
|
||||||
|
b_copy = append(b_copy, string(loginType))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(a_copy)
|
||||||
|
sort.Strings(b_copy)
|
||||||
|
|
||||||
|
return reflect.DeepEqual(a_copy, b_copy)
|
||||||
|
}
|
||||||
|
|
||||||
type availableResponse struct {
|
type availableResponse struct {
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
|
|
@ -88,7 +89,7 @@ type Dendrite struct {
|
||||||
RecaptchaPrivateKey string `yaml:"recaptcha_private_key"`
|
RecaptchaPrivateKey string `yaml:"recaptcha_private_key"`
|
||||||
// Boolean stating whether catpcha registration is enabled
|
// Boolean stating whether catpcha registration is enabled
|
||||||
// and required
|
// and required
|
||||||
RegistrationRecaptcha bool `yaml:"enable_registration_captcha"`
|
RecaptchaEnabled bool `yaml:"enable_registration_captcha"`
|
||||||
// Secret used to bypass the captcha registration entirely
|
// Secret used to bypass the captcha registration entirely
|
||||||
RecaptchaBypassSecret string `yaml:"captcha_bypass_secret"`
|
RecaptchaBypassSecret string `yaml:"captcha_bypass_secret"`
|
||||||
// HTTP API endpoint used to verify whether the captcha response
|
// HTTP API endpoint used to verify whether the captcha response
|
||||||
|
|
@ -201,6 +202,13 @@ type Dendrite struct {
|
||||||
// The config for the jaeger opentracing reporter.
|
// The config for the jaeger opentracing reporter.
|
||||||
Jaeger jaegerconfig.Configuration `yaml:"jaeger"`
|
Jaeger jaegerconfig.Configuration `yaml:"jaeger"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any information derived from the configuration options for later use.
|
||||||
|
Derived struct {
|
||||||
|
// Flows for registration. As long as they given flows only relies on config file options,
|
||||||
|
// we can generate them on startup and store them until needed
|
||||||
|
Flows []authtypes.Flow `json:"flows"`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Path on the filesystem.
|
// A Path on the filesystem.
|
||||||
|
|
@ -317,9 +325,29 @@ func loadConfig(
|
||||||
|
|
||||||
config.Media.AbsBasePath = Path(absPath(basePath, config.Media.BasePath))
|
config.Media.AbsBasePath = Path(absPath(basePath, config.Media.BasePath))
|
||||||
|
|
||||||
|
// Generate data from config options
|
||||||
|
config.derive()
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// derive generates data that is derived from various values provided in
|
||||||
|
// the config file
|
||||||
|
func (config *Dendrite) derive() {
|
||||||
|
// Determine registrations flows based off config values
|
||||||
|
// TODO: Add email auth type
|
||||||
|
// TODO: Add MSISDN auth type
|
||||||
|
|
||||||
|
if config.Matrix.RecaptchaEnabled {
|
||||||
|
config.Derived.Flows = append(config.Derived.Flows,
|
||||||
|
authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeRecaptcha}})
|
||||||
|
} else {
|
||||||
|
config.Derived.Flows = append(config.Derived.Flows,
|
||||||
|
authtypes.Flow{[]authtypes.LoginType{authtypes.LoginTypeDummy}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDefaults sets default config values if they are not explicitly set
|
||||||
func (config *Dendrite) setDefaults() {
|
func (config *Dendrite) setDefaults() {
|
||||||
if config.Matrix.KeyValidityPeriod == 0 {
|
if config.Matrix.KeyValidityPeriod == 0 {
|
||||||
config.Matrix.KeyValidityPeriod = 24 * time.Hour
|
config.Matrix.KeyValidityPeriod = 24 * time.Hour
|
||||||
|
|
@ -339,6 +367,8 @@ func (config *Dendrite) setDefaults() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error returns a string detailing how many errors were contained within an
|
||||||
|
// Error type
|
||||||
func (e Error) Error() string {
|
func (e Error) Error() string {
|
||||||
if len(e.Problems) == 1 {
|
if len(e.Problems) == 1 {
|
||||||
return e.Problems[0]
|
return e.Problems[0]
|
||||||
|
|
@ -348,6 +378,8 @@ func (e Error) Error() string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check returns an error type containing all errors found within the config
|
||||||
|
// file
|
||||||
func (config *Dendrite) check(monolithic bool) error {
|
func (config *Dendrite) check(monolithic bool) error {
|
||||||
var problems []string
|
var problems []string
|
||||||
|
|
||||||
|
|
@ -432,6 +464,7 @@ func (config *Dendrite) check(monolithic bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// absPath returns the absolute path for a given relative or absolute path
|
||||||
func absPath(dir string, path Path) string {
|
func absPath(dir string, path Path) string {
|
||||||
if filepath.IsAbs(string(path)) {
|
if filepath.IsAbs(string(path)) {
|
||||||
// filepath.Join cleans the path so we should clean the absolute paths as well for consistency.
|
// filepath.Join cleans the path so we should clean the absolute paths as well for consistency.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue