mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-12 17:33:09 -06:00
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:
parent
1bcb673e3c
commit
4f78a32c82
|
|
@ -20,10 +20,11 @@ import (
|
||||||
|
|
||||||
// Account represents a Matrix account on this home server.
|
// Account represents a Matrix account on this home server.
|
||||||
type Account struct {
|
type Account struct {
|
||||||
UserID string
|
UserID string
|
||||||
Localpart string
|
Localpart string
|
||||||
ServerName gomatrixserverlib.ServerName
|
ServerName gomatrixserverlib.ServerName
|
||||||
Profile *Profile
|
Profile *Profile
|
||||||
|
AppServiceID string
|
||||||
// TODO: Other flags like IsAdmin, IsGuest
|
// TODO: Other flags like IsAdmin, IsGuest
|
||||||
// TODO: Devices
|
// TODO: Devices
|
||||||
// TODO: Associations (e.g. with application services)
|
// TODO: Associations (e.g. with application services)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ type LoginType string
|
||||||
|
|
||||||
// The relevant login types implemented in Dendrite
|
// The relevant login types implemented in Dendrite
|
||||||
const (
|
const (
|
||||||
LoginTypeDummy = "m.login.dummy"
|
LoginTypeDummy = "m.login.dummy"
|
||||||
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
|
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
|
||||||
LoginTypeRecaptcha = "m.login.recaptcha"
|
LoginTypeRecaptcha = "m.login.recaptcha"
|
||||||
|
LoginTypeApplicationService = "m.login.application_service"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,16 @@ CREATE TABLE IF NOT EXISTS account_accounts (
|
||||||
-- When this account was first created, as a unix timestamp (ms resolution).
|
-- When this account was first created, as a unix timestamp (ms resolution).
|
||||||
created_ts BIGINT NOT NULL,
|
created_ts BIGINT NOT NULL,
|
||||||
-- The password hash for this account. Can be NULL if this is a passwordless account.
|
-- 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:
|
-- 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 = "" +
|
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 = "" +
|
const selectAccountByLocalpartSQL = "" +
|
||||||
"SELECT localpart FROM account_accounts WHERE localpart = $1"
|
"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
|
// this account will be passwordless. Returns an error if this account already exists. Returns the account
|
||||||
// on success.
|
// on success.
|
||||||
func (s *accountsStatements) insertAccount(
|
func (s *accountsStatements) insertAccount(
|
||||||
ctx context.Context, localpart, hash string,
|
ctx context.Context, localpart, hash, appserviceID string,
|
||||||
) (*authtypes.Account, error) {
|
) (*authtypes.Account, error) {
|
||||||
createdTimeMS := time.Now().UnixNano() / 1000000
|
createdTimeMS := time.Now().UnixNano() / 1000000
|
||||||
stmt := s.insertAccountStmt
|
stmt := s.insertAccountStmt
|
||||||
|
|
@ -86,9 +88,10 @@ func (s *accountsStatements) insertAccount(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &authtypes.Account{
|
return &authtypes.Account{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
UserID: makeUserID(localpart, s.serverName),
|
UserID: makeUserID(localpart, s.serverName),
|
||||||
ServerName: s.serverName,
|
ServerName: s.serverName,
|
||||||
|
AppServiceID: appserviceID,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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.
|
// account already exists, it will return nil, nil.
|
||||||
func (d *Database) CreateAccount(
|
func (d *Database) CreateAccount(
|
||||||
ctx context.Context, localpart, plaintextPassword string,
|
ctx context.Context, localpart, plaintextPassword, appserviceID string,
|
||||||
) (*authtypes.Account, error) {
|
) (*authtypes.Account, error) {
|
||||||
hash, err := hashPassword(plaintextPassword)
|
var err error
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
// 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 err := d.profiles.insertProfile(ctx, localpart); err != nil {
|
||||||
if common.IsUniqueConstraintViolationErr(err) {
|
if common.IsUniqueConstraintViolationErr(err) {
|
||||||
|
|
@ -133,7 +139,7 @@ func (d *Database) CreateAccount(
|
||||||
}
|
}
|
||||||
return nil, err
|
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
|
// SaveMembership saves the user matching a given localpart as a member of a given
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,12 @@ func UserInUse(msg string) *MatrixError {
|
||||||
return &MatrixError{"M_USER_IN_USE", msg}
|
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
|
// GuestAccessForbidden is an error which is returned when the client is
|
||||||
// forbidden from accessing a resource as a guest.
|
// forbidden from accessing a resource as a guest.
|
||||||
func GuestAccessForbidden(msg string) *MatrixError {
|
func GuestAccessForbidden(msg string) *MatrixError {
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ var (
|
||||||
// remembered. If ANY parameters are supplied, the server should REPLACE all knowledge of
|
// 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.
|
// previous parameters with the ones supplied. This mean you cannot "build up" request params.
|
||||||
type registerRequest struct {
|
type registerRequest struct {
|
||||||
// registration parameters.
|
// registration parameters
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
|
|
@ -71,6 +71,10 @@ type registerRequest struct {
|
||||||
Auth authDict `json:"auth"`
|
Auth authDict `json:"auth"`
|
||||||
|
|
||||||
InitialDisplayName *string `json:"initial_device_display_name"`
|
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 {
|
type authDict struct {
|
||||||
|
|
@ -233,6 +237,95 @@ func validateRecaptcha(
|
||||||
return nil
|
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
|
// 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,
|
||||||
|
|
@ -294,7 +387,6 @@ func handleRegistrationFlow(
|
||||||
deviceDB *devices.Database,
|
deviceDB *devices.Database,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
// TODO: Shared secret registration (create new user scripts)
|
// TODO: Shared secret registration (create new user scripts)
|
||||||
// TODO: AS API registration
|
|
||||||
// TODO: Enable registration config flag
|
// TODO: Enable registration config flag
|
||||||
// TODO: Guest account upgrading
|
// TODO: Guest account upgrading
|
||||||
|
|
||||||
|
|
@ -331,6 +423,21 @@ func handleRegistrationFlow(
|
||||||
// Add SharedSecret to the list of completed registration stages
|
// Add SharedSecret to the list of completed registration stages
|
||||||
sessions[sessionID] = append(sessions[sessionID], authtypes.LoginTypeSharedSecret)
|
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:
|
case authtypes.LoginTypeDummy:
|
||||||
// there is nothing to do
|
// there is nothing to do
|
||||||
// Add Dummy to the list of completed registration stages
|
// Add Dummy to the list of completed registration stages
|
||||||
|
|
@ -355,7 +462,7 @@ func handleRegistrationFlow(
|
||||||
}
|
}
|
||||||
|
|
||||||
return completeRegistration(req.Context(), accountDB, deviceDB,
|
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
|
// LegacyRegister process register requests from the legacy v1 API
|
||||||
|
|
@ -396,10 +503,10 @@ func LegacyRegister(
|
||||||
return util.MessageResponse(403, "HMAC incorrect")
|
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:
|
case authtypes.LoginTypeDummy:
|
||||||
// there is nothing to do
|
// 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:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 501,
|
Code: 501,
|
||||||
|
|
@ -441,7 +548,7 @@ func completeRegistration(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
accountDB *accounts.Database,
|
accountDB *accounts.Database,
|
||||||
deviceDB *devices.Database,
|
deviceDB *devices.Database,
|
||||||
username, password string,
|
username, password, appserviceID string,
|
||||||
displayName *string,
|
displayName *string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
|
|
@ -450,14 +557,15 @@ func completeRegistration(
|
||||||
JSON: jsonerror.BadJSON("missing username"),
|
JSON: jsonerror.BadJSON("missing username"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if password == "" {
|
// Blank passwords are only allowed by registered application services
|
||||||
|
if password == "" && appserviceID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.BadJSON("missing password"),
|
JSON: jsonerror.BadJSON("missing password"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
acc, err := accountDB.CreateAccount(ctx, username, password)
|
acc, err := accountDB.CreateAccount(ctx, username, password, appserviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 500,
|
Code: 500,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := accountDB.CreateAccount(context.Background(), *username, *password)
|
account, err := accountDB.CreateAccount(context.Background(), *username, *password, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ type ApplicationService struct {
|
||||||
Namespaces map[string][]ApplicationServiceNamespace `yaml:"namespaces"`
|
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 {
|
func loadAppservices(config *Dendrite) error {
|
||||||
// Iterate through and return all the Application Services
|
|
||||||
for _, configPath := range config.ApplicationServices.ConfigFiles {
|
for _, configPath := range config.ApplicationServices.ConfigFiles {
|
||||||
// Create a new application service
|
// Create a new application service
|
||||||
var appservice ApplicationService
|
var appservice ApplicationService
|
||||||
|
|
@ -75,5 +76,18 @@ func loadAppservices(config *Dendrite) error {
|
||||||
config.Derived.ApplicationServices, appservice)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue