Add ability for App Services to register users

AS Tokens are pulled from their respective configs, which are then
checked against when an AS tries to register using
m.login.application_service. If the token exists and the new username is
within their specified namespace, then the user is created as a
password-less user.

Signed-off-by: Andrew Morgan (https://amorgan.xyz) <andrew@amorgan.xyz>
This commit is contained in:
Andrew Morgan (https://amorgan.xyz) 2018-01-19 20:26:30 -08:00
parent 1bcb673e3c
commit 4f78a32c82
No known key found for this signature in database
GPG key ID: 174BEAB009FD176D
8 changed files with 168 additions and 29 deletions

View file

@ -20,10 +20,11 @@ import (
// Account represents a Matrix account on this home server.
type Account struct {
UserID string
Localpart string
ServerName gomatrixserverlib.ServerName
Profile *Profile
UserID string
Localpart string
ServerName gomatrixserverlib.ServerName
Profile *Profile
AppServiceID string
// TODO: Other flags like IsAdmin, IsGuest
// TODO: Devices
// TODO: Associations (e.g. with application services)

View file

@ -5,7 +5,8 @@ type LoginType string
// The relevant login types implemented in Dendrite
const (
LoginTypeDummy = "m.login.dummy"
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
LoginTypeRecaptcha = "m.login.recaptcha"
LoginTypeDummy = "m.login.dummy"
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
LoginTypeRecaptcha = "m.login.recaptcha"
LoginTypeApplicationService = "m.login.application_service"
)

View file

@ -32,14 +32,16 @@ CREATE TABLE IF NOT EXISTS account_accounts (
-- When this account was first created, as a unix timestamp (ms resolution).
created_ts BIGINT NOT NULL,
-- The password hash for this account. Can be NULL if this is a passwordless account.
password_hash TEXT
password_hash TEXT,
-- Identifies which Application Service this account belongs to.
appservice_id TEXT,
-- TODO:
-- is_guest, is_admin, appservice_id, upgraded_ts, devices, any email reset stuff?
-- is_guest, is_admin, upgraded_ts, devices, any email reset stuff?
);
`
const insertAccountSQL = "" +
"INSERT INTO account_accounts(localpart, created_ts, password_hash) VALUES ($1, $2, $3)"
"INSERT INTO account_accounts(localpart, created_ts, password_hash, appservice_id) VALUES ($1, $2, $3, $4)"
const selectAccountByLocalpartSQL = "" +
"SELECT localpart FROM account_accounts WHERE localpart = $1"
@ -78,7 +80,7 @@ func (s *accountsStatements) prepare(db *sql.DB, server gomatrixserverlib.Server
// this account will be passwordless. Returns an error if this account already exists. Returns the account
// on success.
func (s *accountsStatements) insertAccount(
ctx context.Context, localpart, hash string,
ctx context.Context, localpart, hash, appserviceID string,
) (*authtypes.Account, error) {
createdTimeMS := time.Now().UnixNano() / 1000000
stmt := s.insertAccountStmt
@ -86,9 +88,10 @@ func (s *accountsStatements) insertAccount(
return nil, err
}
return &authtypes.Account{
Localpart: localpart,
UserID: makeUserID(localpart, s.serverName),
ServerName: s.serverName,
Localpart: localpart,
UserID: makeUserID(localpart, s.serverName),
ServerName: s.serverName,
AppServiceID: appserviceID,
}, nil
}

View file

@ -121,11 +121,17 @@ func (d *Database) SetDisplayName(
// for this account. If no password is supplied, the account will be a passwordless account. If the
// account already exists, it will return nil, nil.
func (d *Database) CreateAccount(
ctx context.Context, localpart, plaintextPassword string,
ctx context.Context, localpart, plaintextPassword, appserviceID string,
) (*authtypes.Account, error) {
hash, err := hashPassword(plaintextPassword)
if err != nil {
return nil, err
var err error
// Generate a password hash if this is not a password-less user
hash := ""
if appserviceID == "" && plaintextPassword != "" {
hash, err = hashPassword(plaintextPassword)
if err != nil {
return nil, err
}
}
if err := d.profiles.insertProfile(ctx, localpart); err != nil {
if common.IsUniqueConstraintViolationErr(err) {
@ -133,7 +139,7 @@ func (d *Database) CreateAccount(
}
return nil, err
}
return d.accounts.insertAccount(ctx, localpart, hash)
return d.accounts.insertAccount(ctx, localpart, hash, appserviceID)
}
// SaveMembership saves the user matching a given localpart as a member of a given

View file

@ -109,6 +109,12 @@ func UserInUse(msg string) *MatrixError {
return &MatrixError{"M_USER_IN_USE", msg}
}
// Exclusive is an error returned when an application service tries to register
// an username that is outside of its registered namespace
func Exclusive(msg string) *MatrixError {
return &MatrixError{"M_EXCLUSIVE", msg}
}
// GuestAccessForbidden is an error which is returned when the client is
// forbidden from accessing a resource as a guest.
func GuestAccessForbidden(msg string) *MatrixError {

View file

@ -63,7 +63,7 @@ var (
// 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.
// registration parameters
Password string `json:"password"`
Username string `json:"username"`
Admin bool `json:"admin"`
@ -71,6 +71,10 @@ type registerRequest struct {
Auth authDict `json:"auth"`
InitialDisplayName *string `json:"initial_device_display_name"`
// 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 {
@ -233,6 +237,95 @@ func validateRecaptcha(
return nil
}
// 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, *util.JSONResponse) {
if appservice != nil {
// Loop through given Application Service's namespaces and see if any match
for _, namespace := range appservice.Namespaces["users"] {
match, err := regexp.MatchString(namespace.Regex, username)
if err != nil {
return false, &util.JSONResponse{
Code: 401,
JSON: jsonerror.BadJSON("Supplied Application Service namespace" + namespace.Regex + "is invalid."),
}
}
if match {
return true, nil
}
}
return false, nil
}
// Loop through all known Application Service's namespaces and see if any match
for _, knownAppservice := range cfg.Derived.ApplicationServices {
for _, namespace := range knownAppservice.Namespaces["users"] {
match, err := regexp.MatchString(namespace.Regex, username)
if err != nil {
return false, &util.JSONResponse{
Code: 401,
JSON: jsonerror.BadJSON("Supplied Application Service namespace" + namespace.Regex + "is invalid."),
}
}
if match {
return true, nil
}
}
}
return false, nil
}
// 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.BadJSON("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.
matched, err := UsernameIsWithinApplicationServiceNamespace(cfg, username, matchedApplicationService)
if err != nil {
return "", err
}
// If we didn't find any matches, return M_EXCLUSIVE
if !matched {
return "", &util.JSONResponse{
Code: 401,
JSON: jsonerror.Exclusive("Supplied username " + username +
" did not match any namespaces for application service ID: " + matchedApplicationService.ID),
}
}
// 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
func Register(
req *http.Request,
@ -294,7 +387,6 @@ func handleRegistrationFlow(
deviceDB *devices.Database,
) util.JSONResponse {
// TODO: Shared secret registration (create new user scripts)
// TODO: AS API registration
// TODO: Enable registration config flag
// TODO: Guest account upgrading
@ -331,6 +423,21 @@ func handleRegistrationFlow(
// Add SharedSecret to the list of completed registration stages
sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeSharedSecret)
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)
case authtypes.LoginTypeDummy:
// there is nothing to do
// Add Dummy to the list of completed registration stages
@ -355,7 +462,7 @@ func handleRegistrationFlow(
}
return completeRegistration(req.Context(), accountDB, deviceDB,
r.Username, r.Password, r.InitialDisplayName)
r.Username, r.Password, "", r.InitialDisplayName)
}
// LegacyRegister process register requests from the legacy v1 API
@ -396,10 +503,10 @@ func LegacyRegister(
return util.MessageResponse(403, "HMAC incorrect")
}
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, nil)
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", nil)
case authtypes.LoginTypeDummy:
// there is nothing to do
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, nil)
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", nil)
default:
return util.JSONResponse{
Code: 501,
@ -441,7 +548,7 @@ func completeRegistration(
ctx context.Context,
accountDB *accounts.Database,
deviceDB *devices.Database,
username, password string,
username, password, appserviceID string,
displayName *string,
) util.JSONResponse {
if username == "" {
@ -450,14 +557,15 @@ func completeRegistration(
JSON: jsonerror.BadJSON("missing username"),
}
}
if password == "" {
// Blank passwords are only allowed by registered application services
if password == "" && appserviceID == "" {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.BadJSON("missing password"),
}
}
acc, err := accountDB.CreateAccount(ctx, username, password)
acc, err := accountDB.CreateAccount(ctx, username, password, appserviceID)
if err != nil {
return util.JSONResponse{
Code: 500,

View file

@ -69,7 +69,7 @@ func main() {
os.Exit(1)
}
account, err := accountDB.CreateAccount(context.Background(), *username, *password)
account, err := accountDB.CreateAccount(context.Background(), *username, *password, "")
if err != nil {
fmt.Println(err.Error())
os.Exit(1)

View file

@ -47,8 +47,9 @@ type ApplicationService struct {
Namespaces map[string][]ApplicationServiceNamespace `yaml:"namespaces"`
}
// loadAppservices iterates through all application service config files
// and loads their data into the config object for later access.
func loadAppservices(config *Dendrite) error {
// Iterate through and return all the Application Services
for _, configPath := range config.ApplicationServices.ConfigFiles {
// Create a new application service
var appservice ApplicationService
@ -75,5 +76,18 @@ func loadAppservices(config *Dendrite) error {
config.Derived.ApplicationServices, appservice)
}
// Check for any errors in the loaded application services
return checkErrors(config)
}
// checkErrors checks for any configuration errors amongst the loaded
// application services according to the application service spec.
func checkErrors(config *Dendrite) error {
// TODO: Check that no two app services have the same as_token or id
// TODO: Check that namespace(s) are valid regex
// TODO: Check that exclusive namespaces are actually exclusive
return nil
}