diff --git a/packages/matrix-client-server/src/__testData__/buildUserDB.ts b/packages/matrix-client-server/src/__testData__/buildUserDB.ts index e573ae14..b11cebd1 100644 --- a/packages/matrix-client-server/src/__testData__/buildUserDB.ts +++ b/packages/matrix-client-server/src/__testData__/buildUserDB.ts @@ -73,7 +73,8 @@ const matrixDbQueries = [ 'CREATE TABLE IF NOT EXISTS e2e_device_keys_json ( user_id TEXT NOT NULL, device_id TEXT NOT NULL, ts_added_ms BIGINT NOT NULL, key_json TEXT NOT NULL, CONSTRAINT e2e_device_keys_json_uniqueness UNIQUE (user_id, device_id) )', 'CREATE TABLE IF NOT EXISTS e2e_one_time_keys_json ( user_id TEXT NOT NULL, device_id TEXT NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, ts_added_ms BIGINT NOT NULL, key_json TEXT NOT NULL, CONSTRAINT e2e_one_time_keys_json_uniqueness UNIQUE (user_id, device_id, algorithm, key_id) )', 'CREATE TABLE IF NOT EXISTS e2e_fallback_keys_json (user_id TEXT NOT NULL, device_id TEXT NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_json TEXT NOT NULL, used BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT e2e_fallback_keys_json_uniqueness UNIQUE (user_id, device_id, algorithm))', - 'CREATE TABLE dehydrated_devices(user_id TEXT NOT NULL PRIMARY KEY,device_id TEXT NOT NULL,device_data TEXT NOT NULL)' + 'CREATE TABLE dehydrated_devices(user_id TEXT NOT NULL PRIMARY KEY,device_id TEXT NOT NULL,device_data TEXT NOT NULL)', + 'CREATE TABLE device_inbox ( user_id TEXT NOT NULL, device_id TEXT NOT NULL, stream_id BIGINT NOT NULL, message_json TEXT NOT NULL , instance_name TEXT)' ] // eslint-disable-next-line @typescript-eslint/promise-function-async diff --git a/packages/matrix-client-server/src/__testData__/setupTokens.ts b/packages/matrix-client-server/src/__testData__/setupTokens.ts index 0c45c6bb..18139a38 100644 --- a/packages/matrix-client-server/src/__testData__/setupTokens.ts +++ b/packages/matrix-client-server/src/__testData__/setupTokens.ts @@ -7,6 +7,7 @@ export let validToken: string export let validToken1: string export let validToken2: string export let validToken3: string +export let validToken4 : string export let validRefreshToken1: string export let validRefreshToken2: string export let validRefreshToken3: string @@ -18,6 +19,7 @@ export async function setupTokens( validToken1 = randomString(64) validToken2 = randomString(64) validToken3 = randomString(64) + validToken4 = randomString(64) const validRefreshTokenId1 = randomString(64) const validRefreshTokenId2 = randomString(64) const validRefreshTokenId3 = randomString(64) @@ -141,7 +143,12 @@ export async function setupTokens( validated_at: epoch(), added_at: epoch() }) - + await clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), + user_id: '@validated:example.com', + device_id: 'thirddevice', + token: validToken4 + }) await clientServer.matrixDb.insert('access_tokens', { id: randomString(64), user_id: '@thirduser:example.com', diff --git a/packages/matrix-client-server/src/account/3pid/3pid.test.ts b/packages/matrix-client-server/src/account/3pid/3pid.test.ts index 7c79f70b..4e4be4fc 100644 --- a/packages/matrix-client-server/src/account/3pid/3pid.test.ts +++ b/packages/matrix-client-server/src/account/3pid/3pid.test.ts @@ -127,7 +127,7 @@ describe('Use configuration file', () => { .set('Authorization', `Bearer wrongUserAccessToken`) .send({ sid: 'sid', - client_secret: 'cs' + client_secret: 'clientsecret' }) expect(response.statusCode).toBe(400) expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') @@ -140,7 +140,7 @@ describe('Use configuration file', () => { .set('Authorization', `Bearer ${validToken2}`) .send({ sid: 'sid', - client_secret: 'cs' + client_secret: 'clientsecret' }) expect(response.statusCode).toBe(401) session = response.body.session @@ -150,7 +150,7 @@ describe('Use configuration file', () => { .set('Authorization', `Bearer ${validToken2}`) .send({ sid: 'sid', - client_secret: 'cs', + client_secret: 'clientsecret', auth: { type: 'm.login.password', session, @@ -165,23 +165,13 @@ describe('Use configuration file', () => { expect(response1.body).toHaveProperty('errcode', 'M_FORBIDDEN') expect(response1.body).toHaveProperty( 'error', - 'The user does not have a password registered' + 'The user does not have a password registered or the provided password is wrong.' ) }) }) let sid: string let token: string it('should refuse an invalid secret', async () => { - const response1 = await request(app) - .post('/_matrix/client/v3/account/3pid/add') - .set('Accept', 'application/json') - .set('Authorization', `Bearer ${validToken}`) - .send({ - sid: 'sid', - client_secret: 'my' - }) - expect(response1.statusCode).toBe(401) - session = response1.body.session const response = await request(app) .post('/_matrix/client/v3/account/3pid/add') .set('Accept', 'application/json') @@ -191,7 +181,7 @@ describe('Use configuration file', () => { client_secret: 'my', auth: { type: 'm.login.password', - session, + session: 'session', password: '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', identifier: { type: 'm.id.user', user: '@testuser:example.com' } @@ -202,16 +192,6 @@ describe('Use configuration file', () => { expect(response.body).toHaveProperty('error', 'Invalid client_secret') }) it('should refuse an invalid session ID', async () => { - const response1 = await request(app) - .post('/_matrix/client/v3/account/3pid/add') - .set('Accept', 'application/json') - .set('Authorization', `Bearer ${validToken}`) - .send({ - sid: 'sid', - client_secret: 'my' - }) - expect(response1.statusCode).toBe(401) - session = response1.body.session const response = await request(app) .post('/_matrix/client/v3/account/3pid/add') .set('Accept', 'application/json') @@ -221,7 +201,7 @@ describe('Use configuration file', () => { client_secret: 'mysecret', auth: { type: 'm.login.password', - session, + session: 'session', password: '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', identifier: { type: 'm.id.user', user: '@testuser:example.com' } @@ -231,16 +211,33 @@ describe('Use configuration file', () => { expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') expect(response.body).toHaveProperty('error', 'Invalid session ID') }) - it('should return 400 for a wrong combination of client secret and session ID', async () => { - const response1 = await request(app) + it('should refuse an invalid auth', async () => { + const response = await request(app) .post('/_matrix/client/v3/account/3pid/add') .set('Accept', 'application/json') .set('Authorization', `Bearer ${validToken}`) .send({ sid: 'sid', - client_secret: 'my' + client_secret: 'mysecret', + auth: { + type: 'invalidtype' + } }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty( + 'error', + 'Invalid authentication data' + ) + }) + it('should return 400 for a wrong combination of client secret and session ID', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({}) expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') session = response1.body.session const response = await request(app) .post('/_matrix/client/v3/account/3pid/add') @@ -283,7 +280,7 @@ describe('Use configuration file', () => { .set('Authorization', `Bearer ${validToken}`) .send({ sid: 'sid', - client_secret: 'my' + client_secret: 'mysecret' }) expect(response1.statusCode).toBe(401) session = response1.body.session @@ -324,7 +321,7 @@ describe('Use configuration file', () => { .set('Authorization', `Bearer ${validToken}`) .send({ sid: 'sid', - client_secret: 'my' + client_secret: 'mysecret' }) expect(response1.statusCode).toBe(401) session = response1.body.session @@ -352,7 +349,7 @@ describe('Use configuration file', () => { .set('Authorization', `Bearer ${validToken}`) .send({ sid: 'sid', - client_secret: 'my' + client_secret: 'mysecret' }) expect(response1.statusCode).toBe(401) session = response1.body.session @@ -382,7 +379,7 @@ describe('Use configuration file', () => { .set('Authorization', `Bearer ${validToken}`) .send({ sid: 'sid', - client_secret: 'my' + client_secret: 'mysecret' }) expect(response1.statusCode).toBe(401) session = response1.body.session @@ -431,6 +428,25 @@ describe('Use configuration file', () => { // }) }) describe('/_matrix/client/v3/account/3pid/delete', () => { + it('should return 403 if the user is not an admin and the server does not allow it', async () => { + clientServer.conf.capabilities.enable_3pid_changes = false + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + medium: 'email', + address: 'testuser@example.com' + }) + + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response.body).toHaveProperty( + 'error', + 'Cannot add 3pid as it is not allowed by server' + ) + delete clientServer.conf.capabilities.enable_3pid_changes + }) it('should refuse an invalid medium', async () => { const response = await request(app) .post('/_matrix/client/v3/account/3pid/delete') diff --git a/packages/matrix-client-server/src/account/3pid/add.ts b/packages/matrix-client-server/src/account/3pid/add.ts index 25bad031..4ddb5621 100644 --- a/packages/matrix-client-server/src/account/3pid/add.ts +++ b/packages/matrix-client-server/src/account/3pid/add.ts @@ -3,6 +3,7 @@ import { errMsg, isClientSecretValid, isSidValid, + jsonContent, send, validateParameters, type expressAppHandler @@ -11,17 +12,14 @@ import { type AuthenticationData } from '../../types' import type MatrixClientServer from '../..' import { validateUserWithUIAuthentication } from '../../utils/userInteractiveAuthentication' import { isAdmin } from '../../utils/utils' +import { verifyAuthenticationData } from '../../typecheckers' interface RequestBody { - auth: AuthenticationData + auth?: AuthenticationData client_secret: string sid: string } -const requestBodyReference = { - client_secret: 'string', - sid: 'string' -} const schema = { auth: false, client_secret: true, @@ -31,157 +29,174 @@ const schema = { const add = (clientServer: MatrixClientServer): expressAppHandler => { return (req, res) => { clientServer.authenticate(req, res, (data, token) => { - validateUserWithUIAuthentication( - clientServer, - req, - res, - requestBodyReference, - data.sub, - 'add a 3pid to a user account', - (obj, userId) => { - validateParameters( + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + if ( + body.auth != null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send( res, - schema, - obj, - clientServer.logger, - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (obj) => { - if (!isClientSecretValid((obj as RequestBody).client_secret)) { - send( - res, - 400, - errMsg('invalidParam', 'Invalid client_secret'), - clientServer.logger - ) - return - } - if (!isSidValid((obj as RequestBody).sid)) { - send( - res, - 400, - errMsg('invalidParam', 'Invalid session ID'), - clientServer.logger - ) - return - } - const body = obj as RequestBody - const byAdmin = await isAdmin(clientServer, userId as string) - const allowed = - clientServer.conf.capabilities.enable_3pid_changes ?? true - if (!byAdmin && !allowed) { - send( - res, - 403, - errMsg( - 'forbidden', - 'Cannot add 3pid as it is not allowed by server' - ), - clientServer.logger - ) - return - } - clientServer.matrixDb - .get( - 'threepid_validation_session', - ['address', 'medium', 'validated_at'], - { - // Get the address from the validation session. This API has to be called after /requestToken, else it will send error 400 - client_secret: body.client_secret, - session_id: body.sid - } - ) - .then((sessionRows) => { - if (sessionRows.length === 0) { - send( - res, - 400, - errMsg('noValidSession'), - clientServer.logger - ) - return - } - if ( - sessionRows[0].validated_at === null || - sessionRows[0].validated_at === undefined - ) { - send( - res, - 400, - errMsg('sessionNotValidated'), - clientServer.logger - ) - return - } - clientServer.matrixDb - .get('user_threepids', ['user_id'], { - address: sessionRows[0].address - }) - .then((rows) => { - if (rows.length > 0) { - send( - res, - 400, - errMsg('threepidInUse'), - clientServer.logger - ) - } else { - clientServer.matrixDb - .insert('user_threepids', { - user_id: userId as string, - address: sessionRows[0].address as string, - medium: sessionRows[0].medium as string, - validated_at: sessionRows[0].validated_at as number, - added_at: epoch() - }) - .then(() => { - send(res, 200, {}, clientServer.logger) - }) - .catch((e) => { - // istanbul ignore next - clientServer.logger.error( - 'Error while inserting user_threepids' - ) - // istanbul ignore next - send( - res, - 400, - errMsg('unknown', e.toString()), - clientServer.logger - ) - }) - } - }) - .catch((e) => { - // istanbul ignore next - clientServer.logger.error( - 'Error while getting user_threepids' + 400, + errMsg('invalidParam', 'Invalid authentication data'), + clientServer.logger + ) + return + } + if (!isClientSecretValid(body.client_secret)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid client_secret'), + clientServer.logger + ) + return + } + if (!isSidValid(body.sid)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid session ID'), + clientServer.logger + ) + return + } + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'add a 3pid to a user account', + obj, + (obj, userId) => { + validateParameters( + res, + schema, + obj, + clientServer.logger, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (obj) => { + const body = obj as RequestBody + const byAdmin = await isAdmin(clientServer, userId as string) + const allowed = + clientServer.conf.capabilities.enable_3pid_changes ?? true + if (!byAdmin && !allowed) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot add 3pid as it is not allowed by server' + ), + clientServer.logger + ) + return + } + clientServer.matrixDb + .get( + 'threepid_validation_session', + ['address', 'medium', 'validated_at'], + { + // Get the address from the validation session. This API has to be called after /requestToken, else it will send error 400 + client_secret: body.client_secret, + session_id: body.sid + } + ) + .then((sessionRows) => { + if (sessionRows.length === 0) { + send( + res, + 400, + errMsg('noValidSession'), + clientServer.logger ) - // istanbul ignore next + return + } + if ( + sessionRows[0].validated_at === null || + sessionRows[0].validated_at === undefined + ) { send( res, - 500, - errMsg('unknown', e.toString()), + 400, + errMsg('sessionNotValidated'), clientServer.logger ) - }) - }) - .catch((e) => { - // istanbul ignore next - clientServer.logger.error( - 'Error while getting threepid_validation_session' - ) - // istanbul ignore next - send( - res, - 500, - errMsg('unknown', e.toString()), - clientServer.logger - ) - }) - } - ) - } - ) + return + } + clientServer.matrixDb + .get('user_threepids', ['user_id'], { + address: sessionRows[0].address + }) + .then((rows) => { + if (rows.length > 0) { + send( + res, + 400, + errMsg('threepidInUse'), + clientServer.logger + ) + } else { + clientServer.matrixDb + .insert('user_threepids', { + user_id: userId as string, + address: sessionRows[0].address as string, + medium: sessionRows[0].medium as string, + validated_at: sessionRows[0] + .validated_at as number, + added_at: epoch() + }) + .then(() => { + send(res, 200, {}, clientServer.logger) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while inserting user_threepids' + ) + // istanbul ignore next + send( + res, + 400, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while getting user_threepids' + ) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while getting threepid_validation_session' + ) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + } + ) + }) }) } } diff --git a/packages/matrix-client-server/src/account/3pid/delete.ts b/packages/matrix-client-server/src/account/3pid/delete.ts index 804abcd5..b57218c8 100644 --- a/packages/matrix-client-server/src/account/3pid/delete.ts +++ b/packages/matrix-client-server/src/account/3pid/delete.ts @@ -39,79 +39,94 @@ export const delete3pid = async ( address: string, medium: string, clientServer: MatrixClientServer, - idServer: string, - userId: string + userId: string, + potentialIdServer?: string // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ): Promise => { - try { - const openIDRows = await insertOpenIdToken( - clientServer, - userId, - randomString(64) - ) - const matrixResolve = new MatrixResolve({ - cache: 'toad-cache' - }) - const baseUrl: string | string[] = await matrixResolve.resolve(idServer) - const registerResponse = await fetch( - `https://${baseUrl as string}/_matrix/identity/v2/account/register`, - { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - access_token: openIDRows[0].token, - expires_in: clientServer.conf.open_id_token_lifetime, - matrix_server_name: clientServer.conf.server_name, - token_type: 'Bearer' - }) - } - ) - const validToken = ((await registerResponse.json()) as RegisterResponseBody) - .token - const UnbindResponse = await fetch( - `https://${baseUrl as string}/_matrix/identity/v2/3pid/unbind`, + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + let idServer: string + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (potentialIdServer) { + idServer = potentialIdServer + } else { + const rows = await clientServer.matrixDb.get( + 'user_threepid_id_server', + ['id_server'], { - method: 'POST', - headers: { - Authorization: `Bearer ${validToken}`, - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - address, - medium - }) + user_id: userId, + medium, + address } ) - if (UnbindResponse.ok) { - const deleteAdd = clientServer.matrixDb.deleteWhere('user_threepids', [ - { field: 'address', value: address, operator: '=' }, - { field: 'medium', value: medium, operator: '=' }, - { field: 'user_id', value: userId, operator: '=' } - ]) - const deleteBind = clientServer.matrixDb.deleteWhere( - 'user_threepid_id_server', - [ - { field: 'address', value: address, operator: '=' }, - { field: 'medium', value: medium, operator: '=' }, - { field: 'user_id', value: userId, operator: '=' }, - { field: 'id_server', value: idServer, operator: '=' } - ] - ) - await Promise.all([deleteAdd, deleteBind]) - return { success: true } + if (rows.length === 0) { + return { success: false, status: 400 } } else { - // istanbul ignore next - return { success: false, status: UnbindResponse.status } + idServer = rows[0].id_server as string } - } catch (error) { - // istanbul ignore next - clientServer.logger.error('Error while deleting 3pid', error) + } + + const openIDRows = await insertOpenIdToken( + clientServer, + userId, + randomString(64) + ) + const matrixResolve = new MatrixResolve({ + cache: 'toad-cache' + }) + const baseUrl: string | string[] = await matrixResolve.resolve(idServer) + const registerResponse = await fetch( + `https://${baseUrl as string}/_matrix/identity/v2/account/register`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + access_token: openIDRows[0].token, + expires_in: clientServer.conf.open_id_token_lifetime, + matrix_server_name: clientServer.conf.server_name, + token_type: 'Bearer' + }) + } + ) + const validToken = ((await registerResponse.json()) as RegisterResponseBody) + .token + const UnbindResponse = await fetch( + `https://${baseUrl as string}/_matrix/identity/v2/3pid/unbind`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${validToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + address, + medium + }) + } + ) + if (UnbindResponse.ok) { + const deleteAdd = clientServer.matrixDb.deleteWhere('user_threepids', [ + { field: 'address', value: address, operator: '=' }, + { field: 'medium', value: medium, operator: '=' }, + { field: 'user_id', value: userId, operator: '=' } + ]) + const deleteBind = clientServer.matrixDb.deleteWhere( + 'user_threepid_id_server', + [ + { field: 'address', value: address, operator: '=' }, + { field: 'medium', value: medium, operator: '=' }, + { field: 'user_id', value: userId, operator: '=' }, + { field: 'id_server', value: idServer, operator: '=' } + ] + ) + await Promise.all([deleteAdd, deleteBind]) + return { success: true } + } else { // istanbul ignore next - throw error + return { success: false, status: UnbindResponse.status } } } @@ -174,103 +189,36 @@ const delete3pidHandler = ( ) return } - let idServer: string - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (typeof body.id_server === 'string' && body.id_server) { - idServer = body.id_server - delete3pid( - body.address, - body.medium, - clientServer, - idServer, - data.sub - ) - .then((response) => { - if (response.success) { - send(res, 200, { id_server_unbind_result: 'success' }) - } else { - send(res, response.status as number, { - id_server_unbind_result: 'no-support' - }) - } - }) - .catch((e) => { - // istanbul ignore next - clientServer.logger.error( - 'Error while deleting user_threepids', - e - ) - // istanbul ignore next - send( - res, - 500, - errMsg('unknown', e.toString()), - clientServer.logger - ) - }) - } else { - clientServer.matrixDb - .get('user_threepid_id_server', ['id_server'], { - user_id: data.sub, - medium: body.medium, - address: body.address - }) - .then((rows) => { - if (rows.length === 0) { - clientServer.logger.error( - `No id_server found corresponding to user ${data.sub}` - ) - send(res, 400, { - id_server_unbind_result: 'no-support' - }) - return - } - delete3pid( - body.address, - body.medium, - clientServer, - rows[0].id_server as string, - data.sub - ) - .then((response) => { - if (response.success) { - send(res, 200, { id_server_unbind_result: 'success' }) - } else { - send(res, response.status as number, { - id_server_unbind_result: 'no-support' - }) - } - }) - .catch((e) => { - // istanbul ignore next - clientServer.logger.error( - 'Error while deleting user_threepids', - e - ) - // istanbul ignore next - send( - res, - 500, - errMsg('unknown', e.toString()), - clientServer.logger - ) - }) - }) - .catch((e) => { - // istanbul ignore next - clientServer.logger.error( - 'Error while getting id_server from the database', - e - ) - // istanbul ignore next - send( - res, - 500, - errMsg('unknown', e.toString()), - clientServer.logger - ) - }) - } + delete3pid( + body.address, + body.medium, + clientServer, + data.sub, + body.id_server + ) + .then((response) => { + if (response.success) { + send(res, 200, { id_server_unbind_result: 'success' }) + } else { + send(res, response.status as number, { + id_server_unbind_result: 'no-support' + }) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while deleting user_threepids', + e + ) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) } ) }) diff --git a/packages/matrix-client-server/src/account/account.test.ts b/packages/matrix-client-server/src/account/account.test.ts index 6705ed4d..fb988d5c 100644 --- a/packages/matrix-client-server/src/account/account.test.ts +++ b/packages/matrix-client-server/src/account/account.test.ts @@ -254,6 +254,47 @@ describe('Use configuration file', () => { ) clientServer.conf.capabilities.enable_set_avatar_url = true }) + it('should refuse an invalid auth', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.password', + session: 'session', + password: 'wrongpassword', + identifier: { type: 'wrongtype', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid auth') + }) + it('should refuse an invalid id_server', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + id_server: 42 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid id_server') + }) + it('should refuse an invalid erase', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + erase: 'true' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid erase') + }) it('should deactivate a user account who authenticated with a token', async () => { const response1 = await request(app) .post('/_matrix/client/v3/account/deactivate') @@ -504,7 +545,7 @@ describe('Use configuration file', () => { expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') expect(response.body).toHaveProperty( 'error', - 'The user does not have a password registered' + 'The user does not have a password registered or the provided password is wrong.' ) // Error from UI Authentication since the password was deleted upon deactivation of the account }) it('should send a no-support response if the identity server did not unbind the 3pid association', async () => { diff --git a/packages/matrix-client-server/src/account/deactivate.ts b/packages/matrix-client-server/src/account/deactivate.ts index 37d3c5e7..5d1a42d4 100644 --- a/packages/matrix-client-server/src/account/deactivate.ts +++ b/packages/matrix-client-server/src/account/deactivate.ts @@ -3,6 +3,7 @@ import { errMsg, type expressAppHandler, getAccessToken, + jsonContent, send } from '@twake/utils' import type MatrixClientServer from '..' @@ -25,19 +26,19 @@ import { SafeClientEvent } from '../utils/event' import { delete3pid, type DeleteResponse } from './3pid/delete' import { randomString } from '@twake/crypto' import pLimit from 'p-limit' +import { + verifyAuthenticationData, + verifyBoolean, + verifyString +} from '../typecheckers' const maxPromisesToExecuteConcurrently = 10 const limit = pLimit(maxPromisesToExecuteConcurrently) interface RequestBody { - auth: AuthenticationData - erase: boolean - id_server: string -} - -const requestBodyReference = { - erase: 'boolean', - id_server: 'string' + auth?: AuthenticationData + erase?: boolean + id_server?: string } const allowedFlows: AuthenticationFlowContent = { @@ -345,7 +346,7 @@ const realMethod = async ( return } allowed = clientServer.conf.capabilities.enable_set_avatar_url ?? true - if (body.erase && !byAdmin && !allowed) { + if ((body.erase ?? false) && !byAdmin && !allowed) { send( res, 403, @@ -371,18 +372,23 @@ const realMethod = async ( row.address as string, row.medium as string, clientServer, - body.id_server, - userId + userId, + body.id_server ) ) ) }) const deleteDevicesPromises = deleteDevices(clientServer, userId) - const deleteTokenPromise = limit(() => + const deleteAccessTokensPromise = limit(() => clientServer.matrixDb.deleteWhere('access_tokens', [ { field: 'user_id', value: userId, operator: '=' } ]) ) + const deleteRefreshTokensPromise = limit(() => + clientServer.matrixDb.deleteWhere('refresh_tokens', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) const removePasswordPromise = limit(() => clientServer.matrixDb.updateWithConditions( 'users', @@ -420,7 +426,8 @@ const realMethod = async ( const promisesToExecute = [ ...threepidDeletePromises, // We put the threepid delete promises first so that we can check if all threepids were successfully unbound from the associated id-servers ...deleteDevicesPromises, - deleteTokenPromise, + deleteAccessTokensPromise, + deleteRefreshTokensPromise, removePasswordPromise, rejectPendingInvitesAndKnocksPromise, deleteRoomKeysPromise, @@ -430,7 +437,7 @@ const realMethod = async ( ...deleteUserDirectoryPromises, ...purgeAccountDataPromises ] - if (body.erase) { + if (body.erase ?? false) { promisesToExecute.push( limit(() => clientServer.matrixDb.updateWithConditions( @@ -486,16 +493,82 @@ const realMethod = async ( // There should be a method to reactivate the account to match this one but it isn't implemented yet const deactivate = (clientServer: MatrixClientServer): expressAppHandler => { return (req, res) => { - const token = getAccessToken(req) - if (token != null) { - clientServer.authenticate(req, res, (data: TokenContent) => { - validateUserWithUIAuthentication( - clientServer, + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + if ( + body.auth !== null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid auth'), + clientServer.logger + ) + return + } else if ( + body.id_server !== null && + body.id_server !== undefined && + !verifyString(body.id_server) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid id_server'), + clientServer.logger + ) + return + } else if ( + body.erase !== null && + body.erase !== undefined && + !verifyBoolean(body.erase) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid erase'), + clientServer.logger + ) + return + } + const token = getAccessToken(req) + if (token != null) { + clientServer.authenticate(req, res, (data: TokenContent) => { + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'deactivate your account', + obj, + (obj, userId) => { + realMethod( + res, + clientServer, + obj as RequestBody, + userId as string + ).catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while deactivating account') + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + }) + } else { + clientServer.uiauthenticate( req, res, - requestBodyReference, - data.sub, + allowedFlows, 'deactivate your account', + obj, (obj, userId) => { realMethod( res, @@ -504,7 +577,7 @@ const deactivate = (clientServer: MatrixClientServer): expressAppHandler => { userId as string ).catch((e) => { // istanbul ignore next - clientServer.logger.error('Error while deactivating account') + clientServer.logger.error('Error while changing password') // istanbul ignore next send( res, @@ -515,29 +588,8 @@ const deactivate = (clientServer: MatrixClientServer): expressAppHandler => { }) } ) - }) - } else { - clientServer.uiauthenticate( - req, - res, - requestBodyReference, - allowedFlows, - 'deactivate your account', - (obj, userId) => { - realMethod( - res, - clientServer, - obj as RequestBody, - userId as string - ).catch((e) => { - // istanbul ignore next - clientServer.logger.error('Error while changing password') - // istanbul ignore next - send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) - }) - } - ) - } + } + }) } } export default deactivate diff --git a/packages/matrix-client-server/src/account/password/index.ts b/packages/matrix-client-server/src/account/password/index.ts index 1a42d96a..90529f29 100644 --- a/packages/matrix-client-server/src/account/password/index.ts +++ b/packages/matrix-client-server/src/account/password/index.ts @@ -2,6 +2,7 @@ import { errMsg, type expressAppHandler, getAccessToken, + jsonContent, send, validateParameters } from '@twake/utils' @@ -19,18 +20,18 @@ import type e from 'express' import { Hash } from '@twake/crypto' import { type TokenContent } from '../../utils/authenticate' import { isAdmin } from '../../utils/utils' +import { + verifyAuthenticationData, + verifyBoolean, + verifyString +} from '../../typecheckers' interface RequestBody { - auth: AuthenticationData - logout_devices: boolean + auth?: AuthenticationData + logout_devices?: boolean new_password: string } -const requestBodyReference = { - logout_devices: 'boolean', - new_password: 'string' -} - const schema = { auth: false, logout_devices: false, @@ -149,25 +150,96 @@ const realMethod = async ( } const passwordReset = (clientServer: MatrixClientServer): expressAppHandler => { return (req, res) => { - const token = getAccessToken(req) - if (token != null) { - clientServer.authenticate(req, res, (data: TokenContent) => { - validateUserWithUIAuthentication( - clientServer, + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + if ( + body.auth != null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid auth'), + clientServer.logger + ) + return + } else if ( + body.logout_devices != null && + body.logout_devices !== undefined && + !verifyBoolean(body.logout_devices) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid logout_devices'), + clientServer.logger + ) + return + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + } else if (body.new_password && !verifyString(body.new_password)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid new_password'), + clientServer.logger + ) + return + } + const token = getAccessToken(req) + if (token != null) { + clientServer.authenticate(req, res, (data: TokenContent) => { + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'modify your account password', + obj, + (obj, userId) => { + validateParameters( + res, + schema, + obj, + clientServer.logger, + (obj) => { + realMethod( + res, + clientServer, + obj as RequestBody, + userId as string, + data.device_id, + token + ).catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while changing password') + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + } + ) + }) + } else { + clientServer.uiauthenticate( req, res, - requestBodyReference, - data.sub, + allowedFlows, 'modify your account password', + obj, (obj, userId) => { validateParameters(res, schema, obj, clientServer.logger, (obj) => { realMethod( res, clientServer, obj as RequestBody, - userId as string, - data.device_id, - token + userId as string ).catch((e) => { // istanbul ignore next clientServer.logger.error('Error while changing password') @@ -182,36 +254,8 @@ const passwordReset = (clientServer: MatrixClientServer): expressAppHandler => { }) } ) - }) - } else { - clientServer.uiauthenticate( - req, - res, - requestBodyReference, - allowedFlows, - 'modify your account password', - (obj, userId) => { - validateParameters(res, schema, obj, clientServer.logger, (obj) => { - realMethod( - res, - clientServer, - obj as RequestBody, - userId as string - ).catch((e) => { - // istanbul ignore next - clientServer.logger.error('Error while changing password') - // istanbul ignore next - send( - res, - 500, - errMsg('unknown', e.toString()), - clientServer.logger - ) - }) - }) - } - ) - } + } + }) } } diff --git a/packages/matrix-client-server/src/account/password/password.test.ts b/packages/matrix-client-server/src/account/password/password.test.ts index d12e95ae..ed0e4e49 100644 --- a/packages/matrix-client-server/src/account/password/password.test.ts +++ b/packages/matrix-client-server/src/account/password/password.test.ts @@ -101,6 +101,42 @@ describe('Use configuration file', () => { }) describe('/_matrix/client/v3/account/password', () => { let session: string + it('should refuse an invalid logout_devices', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + logout_devices: 'true' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid logout_devices') + }) + it('should refuse an invalid new_password', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + new_password: 55 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid new_password') + }) + it('should refuse an invalid auth', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + auth: { type: 'wrongtype' } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid auth') + }) it('should return 403 if the user is not an admin and the server does not allow it', async () => { clientServer.conf.capabilities.enable_change_password = false const response1 = await request(app) diff --git a/packages/matrix-client-server/src/delete_devices.ts b/packages/matrix-client-server/src/delete_devices.ts index 3c0d5fe2..f38a3227 100644 --- a/packages/matrix-client-server/src/delete_devices.ts +++ b/packages/matrix-client-server/src/delete_devices.ts @@ -1,28 +1,279 @@ -import { type expressAppHandler } from '@twake/utils' +/* eslint-disable @typescript-eslint/promise-function-async */ +import { errMsg, jsonContent, send, type expressAppHandler } from '@twake/utils' import type MatrixClientServer from '.' import { validateUserWithUIAuthentication } from './utils/userInteractiveAuthentication' import { type AuthenticationData } from './types' +import { randomString } from '@twake/crypto' +import pLimit from 'p-limit' +import { verifyArray, verifyAuthenticationData } from './typecheckers' + +const MESSAGES_TO_DELETE_BATCH_SIZE = 10 interface RequestBody { auth?: AuthenticationData devices: string[] } +const maxPromisesToExecuteConcurrently = 10 +const limit = pLimit(maxPromisesToExecuteConcurrently) -const reference = {} -const deleteDevices = (clientServer: MatrixClientServer): expressAppHandler => { +const deleteDevices = ( + clientServer: MatrixClientServer, + devices: string[] +): Array> => { + const devicePromises: Array> = [] + for (const deviceId of devices) { + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('devices', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('device_auth_providers', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('e2e_device_keys_json', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('e2e_one_time_keys_json', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('dehydrated_devices', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('e2e_fallback_keys_json', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + } + return devicePromises +} + +const deletePushers = async ( + clientServer: MatrixClientServer, + devices: string[], + userId: string +): Promise>> => { + let insertDeletedPushersPromises: Array> = [] + for (const deviceId of devices) { + const deviceDisplayNameRow = await clientServer.matrixDb.get( + 'devices', + ['display_name'], + { device_id: deviceId } + ) + // istanbul ignore if + if (deviceDisplayNameRow.length === 0) { + // Since device_display_name has the NOT NULL constraint, we assume that if the device has no display name it has no associated pushers + // Ideally there should be a device_id field in the pushers table to delete by device_id + continue + } + const pushers = await clientServer.matrixDb.get( + 'pushers', + ['app_id', 'pushkey'], + { + user_name: userId, + device_display_name: deviceDisplayNameRow[0].display_name + } + ) + await clientServer.matrixDb.deleteWhere( + // We'd like to delete by device_id but there is no device_id field in the pushers table + 'pushers', + [ + { + field: 'device_display_name', + value: deviceDisplayNameRow[0].display_name as string, + operator: '=' + }, + { field: 'user_name', value: userId, operator: '=' } + ] + ) + insertDeletedPushersPromises = pushers.map(async (pusher) => { + await limit(() => + clientServer.matrixDb.insert('deleted_pushers', { + stream_id: randomString(64), // TODO: Update when stream ordering is implemented since the stream_id has to keep track of the order of operations + app_id: pusher.app_id as string, + pushkey: pusher.pushkey as string, + user_id: userId + }) + ) + }) + } + return insertDeletedPushersPromises +} + +const deleteTokens = ( + clientServer: MatrixClientServer, + devices: string[], + userId: string +): Array> => { + const deleteTokensPromises: Array> = [] + for (const deviceId of devices) { + deleteTokensPromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('access_tokens', [ + { field: 'user_id', value: userId, operator: '=' }, + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + deleteTokensPromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('refresh_tokens', [ + { field: 'user_id', value: userId, operator: '=' }, + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + } + return deleteTokensPromises +} + +export const deleteMessagesBetweenStreamIds = async ( + clientServer: MatrixClientServer, + userId: string, + deviceId: string, + fromStreamId: number, + upToStreamId: number, + limit: number +): Promise => { + const maxStreamId = await clientServer.matrixDb.getMaxStreamId( + userId, + deviceId, + fromStreamId, + upToStreamId, + limit + ) + if (maxStreamId === null) { + return 0 + } + await clientServer.matrixDb.deleteWhere('device_inbox', [ + { field: 'user_id', value: userId, operator: '=' }, + { field: 'device_id', value: deviceId, operator: '=' }, + { field: 'stream_id', value: maxStreamId, operator: '<=' }, + { field: 'stream_id', value: fromStreamId, operator: '>' } + ]) + return maxStreamId +} +const deleteDeviceInbox = async ( + clientServer: MatrixClientServer, + userId: string, + deviceId: string, + upToStreamId: number +): Promise => { + let fromStreamId = 0 + while (true) { + // Maybe add a counter to prevent infinite loops if the deletion process is broken + const maxStreamId = await deleteMessagesBetweenStreamIds( + clientServer, + userId, + deviceId, + fromStreamId, + upToStreamId, + MESSAGES_TO_DELETE_BATCH_SIZE + ) + if (maxStreamId === 0) { + break + } + fromStreamId = maxStreamId + } +} + +export const deleteDevicesData = async ( + clientServer: MatrixClientServer, + devices: string[], + userId: string + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type +): Promise => { + // In Synapse's implementation, they also delete account data relative to local notification settings according to this MR : https://github.com/matrix-org/matrix-spec-proposals/pull/3890 + // I did not include it since it is not in the spec + const deleteTokensPromises = deleteTokens(clientServer, devices, userId) + const deleteDevicesPromises = deleteDevices(clientServer, devices) + const deletePushersPromises = await deletePushers( + clientServer, + devices, + userId + ) + const deleteDeviceInboxPromises = devices.map((deviceId) => { + return limit(() => deleteDeviceInbox(clientServer, userId, deviceId, 1000)) // TODO : Fix the upToStreamId when stream ordering is implemented. It should be set to avoid deleting non delivered messages + }) + return await Promise.all([ + ...deleteTokensPromises, + ...deleteDevicesPromises, + ...deletePushersPromises, + ...deleteDeviceInboxPromises + ]) +} + +const deleteDevicesHandler = ( + clientServer: MatrixClientServer +): expressAppHandler => { return (req, res) => { clientServer.authenticate(req, res, (data) => { - validateUserWithUIAuthentication( - clientServer, - req, - res, - reference, - data.sub, - 'remove device(s) from your account', - (obj, userId) => {} - ) + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + if ( + body.auth != null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid auth')) + return + } else if (!verifyArray(body.devices, 'string')) { + send(res, 400, errMsg('invalidParam', 'Invalid devices')) + return + } + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'remove device(s) from your account', + obj, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + (obj, userId) => { + deleteDevicesData( + clientServer, + (obj as RequestBody).devices, + userId as string + ) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error(`Unable to delete devices`, e) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + }) }) } } -export default deleteDevices +export default deleteDevicesHandler diff --git a/packages/matrix-client-server/src/devices/deleteDevice.ts b/packages/matrix-client-server/src/devices/deleteDevice.ts new file mode 100644 index 00000000..fd612afe --- /dev/null +++ b/packages/matrix-client-server/src/devices/deleteDevice.ts @@ -0,0 +1,63 @@ +import { errMsg, type expressAppHandler, jsonContent, send } from '@twake/utils' +import type MatrixClientServer from '..' +import { type AuthenticationData } from '../types' +import { validateUserWithUIAuthentication } from '../utils/userInteractiveAuthentication' +import { verifyAuthenticationData, verifyString } from '../typecheckers' +import { deleteDevicesData } from '../delete_devices' + +interface RequestBody { + auth?: AuthenticationData +} + +interface Parameters { + deviceId: string +} +const deleteDevice = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data) => { + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + // @ts-expect-error : request has parameters + const deviceId = (req.params as Parameters).deviceId + if ( + body.auth != null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid auth')) + return + } else if (!verifyString(deviceId)) { + send(res, 400, errMsg('invalidParam', 'Invalid device ID')) + return + } + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'delete device', + obj, + (obj, userId) => { + deleteDevicesData(clientServer, [deviceId], userId as string) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error(`Error while deleting device`, e) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + }) + }) + } +} + +export default deleteDevice diff --git a/packages/matrix-client-server/src/devices/devices.test.ts b/packages/matrix-client-server/src/devices/devices.test.ts new file mode 100644 index 00000000..3c374bdb --- /dev/null +++ b/packages/matrix-client-server/src/devices/devices.test.ts @@ -0,0 +1,387 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../index' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import { type Config } from '../types' +import defaultConfig from '../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { setupTokens, validToken } from '../__testData__/setupTokens' +import { randomString } from '@twake/crypto' +jest.mock('node-fetch', () => jest.fn()) +const sendMailMock = jest.fn() +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: sendMailMock + })) +})) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + base_url: 'http://example.com/', + matrix_database_host: 'src/__testData__/devicesTestMatrix.db', + userdb_host: 'src/__testData__/devicesTest.db', + database_host: 'src/__testData__/devicesTest.db', + registration_required_3pid: ['email', 'msisdn'] + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/devicesTest.db') + fs.unlinkSync('src/__testData__/devicesTestMatrix.db') +}) + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + + describe('/_matrix/client/v3/devices', () => { + const testUserId = '@testuser:example.com' + + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('devices', { + user_id: testUserId, + device_id: 'testdevice1', + display_name: 'Test Device 1', + last_seen: 1411996332123, + ip: '127.0.0.1', + user_agent: 'curl/7.31.0-DEV' + }) + logger.info('Test device 1 created') + + await clientServer.matrixDb.insert('devices', { + user_id: testUserId, + device_id: 'testdevice2', + display_name: 'Test Device 2', + last_seen: 14119963321254, + ip: '127.0.0.2', + user_agent: 'curl/7.31.0-DEV' + }) + logger.info('Test device 2 created') + } catch (e) { + logger.error('Error creating devices:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'devices', + 'device_id', + 'testdevice1' + ) + logger.info('Test device 1 deleted') + + await clientServer.matrixDb.deleteEqual( + 'devices', + 'device_id', + 'testdevice2' + ) + logger.info('Test device 2 deleted') + } catch (e) { + logger.error('Error deleting devices:', e) + } + }) + + it('should return 401 if the user is not authenticated', async () => { + const response = await request(app) + .get('/_matrix/client/v3/devices') + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + + it('should return all devices for the current user', async () => { + const response = await request(app) + .get('/_matrix/client/v3/devices') + .set('Authorization', `Bearer ${validToken}`) + + expect(response.statusCode).toBe(200) + + expect(response.body).toHaveProperty('devices') + expect(response.body.devices).toHaveLength(2) + expect(response.body.devices[0]).toHaveProperty('device_id') + expect(response.body.devices[0]).toHaveProperty('display_name') + expect(response.body.devices[0]).toHaveProperty('last_seen_ts') + expect(response.body.devices[0]).toHaveProperty('last_seen_ip') + }) + describe('/_matrix/client/v3/devices/:deviceId', () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + let _device_id: string + beforeAll(async () => { + try { + _device_id = 'testdevice2_id' + await clientServer.matrixDb.insert('devices', { + user_id: '@testuser:example.com', + device_id: _device_id, + display_name: 'testdevice2_name', + last_seen: 12345678, + ip: '127.0.0.1', + user_agent: 'curl/7.31.0-DEV', + hidden: 0 + }) + + await clientServer.matrixDb.insert('devices', { + user_id: '@testuser2:example.com', + device_id: 'another_device_id', + display_name: 'another_name', + last_seen: 12345678, + ip: '127.0.0.1', + user_agent: 'curl/7.31.0-DEV', + hidden: 0 + }) + logger.info('Devices inserted in db') + } catch (e) { + logger.error('Error when inserting devices', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'devices', + 'device_id', + _device_id + ) + await clientServer.matrixDb.deleteEqual( + 'devices', + 'device_id', + 'another_device_id' + ) + logger.info('Devices deleted from db') + } catch (e) { + logger.error('Error when deleting devices', e) + } + }) + + describe('GET', () => { + it('should return the device information for the given device ID', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + + expect(response.statusCode).toBe(200) + + expect(response.body).toHaveProperty('device_id') + expect(response.body.device_id).toEqual(_device_id) + expect(response.body).toHaveProperty('display_name') + expect(response.body.display_name).toEqual('testdevice2_name') + expect(response.body).toHaveProperty('last_seen_ip') + expect(response.body.last_seen_ip).toEqual('127.0.0.1') + expect(response.body).toHaveProperty('last_seen_ts') + expect(response.body.last_seen_ts).toEqual(12345678) + }) + + it('should return 404 if the device ID does not exist', async () => { + const deviceId = 'NON_EXISTENT_DEVICE_ID' + const response = await request(app) + .get(`/_matrix/client/v3/devices/${deviceId}`) + .set('Authorization', `Bearer ${validToken}`) + + expect(response.statusCode).toBe(404) + }) + + it('should return 404 if the user has no device with the given device Id', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/devices/another_device_id`) + .set('Authorization', `Bearer ${validToken}`) + + expect(response.statusCode).toBe(404) + }) + + it('should return 401 if the user is not authenticated', async () => { + const response = await request(app).get( + `/_matrix/client/v3/devices/${_device_id}` + ) + + expect(response.statusCode).toBe(401) + }) + }) + + describe('PUT', () => { + const updateData = { + display_name: 'updated_device_name' + } + + it('should update the device information for the given device ID', async () => { + // Update the device + const response = await request(app) + .put(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send(updateData) + expect(response.statusCode).toBe(200) + + // Verify the update in the database + const updatedDevice = await clientServer.matrixDb.get( + 'devices', + ['device_id', 'display_name'], + { device_id: _device_id } + ) + + expect(updatedDevice[0]).toHaveProperty('device_id', _device_id) + expect(updatedDevice[0]).toHaveProperty( + 'display_name', + updateData.display_name + ) + }) + + it('should return 400 if the display_name is too long', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ display_name: randomString(257) }) + + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + + it('should return 404 if the device ID does not exist', async () => { + const response = await request(app) + .put('/_matrix/client/v3/devices/NON_EXISTENT_DEVICE_ID') + .set('Authorization', `Bearer ${validToken}`) + .send(updateData) + + expect(response.statusCode).toBe(404) + }) + + it('should return 404 if the user has no device with the given device ID', async () => { + const deviceId = 'another_device_id' + const response = await request(app) + .put(`/_matrix/client/v3/devices/${deviceId}`) + .set('Authorization', `Bearer ${validToken}`) + .send(updateData) + + expect(response.statusCode).toBe(404) + }) + + it('should return 401 if the user is not authenticated', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/devices/${_device_id}`) + .send(updateData) + + expect(response.statusCode).toBe(401) + }) + }) + + describe('DELETE', () => { + it('should refuse an invalid auth token', async () => { + const response = await request(app) + .delete(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device1', 'device2'], + auth: { invalid: 'auth' } + }) + expect(response.status).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should refuse an invalid deviceId', async () => { + const response = await request(app) + .delete(`/_matrix/client/v3/devices/${randomString(1000)}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device1', 'device2'] + }) + expect(response.status).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should delete a device', async () => { + const response1 = await request(app) + .delete(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send({}) + expect(response1.status).toBe(401) + expect(response1.body).toHaveProperty('session') + const session = response1.body.session + const response = await request(app) + .delete(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: [_device_id], + auth: { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: testUserId }, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + session + } + }) + expect(response.status).toBe(200) + const devices = await clientServer.matrixDb.get( + 'devices', + ['device_id'], + { device_id: _device_id } + ) + expect(devices).toHaveLength(0) + }) + }) + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/index.test.ts b/packages/matrix-client-server/src/index.test.ts index fa95ab70..356b59c2 100644 --- a/packages/matrix-client-server/src/index.test.ts +++ b/packages/matrix-client-server/src/index.test.ts @@ -578,238 +578,6 @@ describe('Use configuration file', () => { }) }) - describe('/_matrix/client/v3/devices', () => { - const testUserId = '@testuser:example.com' - - beforeAll(async () => { - try { - await clientServer.matrixDb.insert('devices', { - user_id: testUserId, - device_id: 'testdevice1', - display_name: 'Test Device 1', - last_seen: 1411996332123, - ip: '127.0.0.1', - user_agent: 'curl/7.31.0-DEV' - }) - logger.info('Test device 1 created') - - await clientServer.matrixDb.insert('devices', { - user_id: testUserId, - device_id: 'testdevice2', - display_name: 'Test Device 2', - last_seen: 14119963321254, - ip: '127.0.0.2', - user_agent: 'curl/7.31.0-DEV' - }) - logger.info('Test device 2 created') - } catch (e) { - logger.error('Error creating devices:', e) - } - }) - - afterAll(async () => { - try { - await clientServer.matrixDb.deleteEqual( - 'devices', - 'device_id', - 'testdevice1' - ) - logger.info('Test device 1 deleted') - - await clientServer.matrixDb.deleteEqual( - 'devices', - 'device_id', - 'testdevice2' - ) - logger.info('Test device 2 deleted') - } catch (e) { - logger.error('Error deleting devices:', e) - } - }) - - it('should return 401 if the user is not authenticated', async () => { - const response = await request(app) - .get('/_matrix/client/v3/devices') - .set('Authorization', 'Bearer invalidToken') - .set('Accept', 'application/json') - expect(response.statusCode).toBe(401) - }) - - it('should return all devices for the current user', async () => { - const response = await request(app) - .get('/_matrix/client/v3/devices') - .set('Authorization', `Bearer ${validToken}`) - - expect(response.statusCode).toBe(200) - - expect(response.body).toHaveProperty('devices') - expect(response.body.devices).toHaveLength(2) - expect(response.body.devices[0]).toHaveProperty('device_id') - expect(response.body.devices[0]).toHaveProperty('display_name') - expect(response.body.devices[0]).toHaveProperty('last_seen_ts') - expect(response.body.devices[0]).toHaveProperty('last_seen_ip') - }) - }) - - describe('/_matrix/client/v3/devices/:deviceId', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - let _device_id: string - beforeAll(async () => { - try { - _device_id = 'testdevice2_id' - await clientServer.matrixDb.insert('devices', { - user_id: '@testuser:example.com', - device_id: _device_id, - display_name: 'testdevice2_name', - last_seen: 12345678, - ip: '127.0.0.1', - user_agent: 'curl/7.31.0-DEV', - hidden: 0 - }) - - await clientServer.matrixDb.insert('devices', { - user_id: '@testuser2:example.com', - device_id: 'another_device_id', - display_name: 'another_name', - last_seen: 12345678, - ip: '127.0.0.1', - user_agent: 'curl/7.31.0-DEV', - hidden: 0 - }) - logger.info('Devices inserted in db') - } catch (e) { - logger.error('Error when inserting devices', e) - } - }) - - afterAll(async () => { - try { - await clientServer.matrixDb.deleteEqual( - 'devices', - 'device_id', - _device_id - ) - await clientServer.matrixDb.deleteEqual( - 'devices', - 'device_id', - 'another_device_id' - ) - logger.info('Devices deleted from db') - } catch (e) { - logger.error('Error when deleting devices', e) - } - }) - - describe('GET', () => { - it('should return the device information for the given device ID', async () => { - const response = await request(app) - .get(`/_matrix/client/v3/devices/${_device_id}`) - .set('Authorization', `Bearer ${validToken}`) - - expect(response.statusCode).toBe(200) - - expect(response.body).toHaveProperty('device_id') - expect(response.body.device_id).toEqual(_device_id) - expect(response.body).toHaveProperty('display_name') - expect(response.body.display_name).toEqual('testdevice2_name') - expect(response.body).toHaveProperty('last_seen_ip') - expect(response.body.last_seen_ip).toEqual('127.0.0.1') - expect(response.body).toHaveProperty('last_seen_ts') - expect(response.body.last_seen_ts).toEqual(12345678) - }) - - it('should return 404 if the device ID does not exist', async () => { - const deviceId = 'NON_EXISTENT_DEVICE_ID' - const response = await request(app) - .get(`/_matrix/client/v3/devices/${deviceId}`) - .set('Authorization', `Bearer ${validToken}`) - - expect(response.statusCode).toBe(404) - }) - - it('should return 404 if the user has no device with the given device Id', async () => { - const response = await request(app) - .get(`/_matrix/client/v3/devices/another_device_id`) - .set('Authorization', `Bearer ${validToken}`) - - expect(response.statusCode).toBe(404) - }) - - it('should return 401 if the user is not authenticated', async () => { - const response = await request(app).get( - `/_matrix/client/v3/devices/${_device_id}` - ) - - expect(response.statusCode).toBe(401) - }) - }) - - describe('PUT', () => { - const updateData = { - display_name: 'updated_device_name' - } - - it('should update the device information for the given device ID', async () => { - // Update the device - const response = await request(app) - .put(`/_matrix/client/v3/devices/${_device_id}`) - .set('Authorization', `Bearer ${validToken}`) - .send(updateData) - expect(response.statusCode).toBe(200) - - // Verify the update in the database - const updatedDevice = await clientServer.matrixDb.get( - 'devices', - ['device_id', 'display_name'], - { device_id: _device_id } - ) - - expect(updatedDevice[0]).toHaveProperty('device_id', _device_id) - expect(updatedDevice[0]).toHaveProperty( - 'display_name', - updateData.display_name - ) - }) - - it('should return 400 if the display_name is too long', async () => { - const response = await request(app) - .put(`/_matrix/client/v3/devices/${_device_id}`) - .set('Authorization', `Bearer ${validToken}`) - .send({ display_name: randomString(257) }) - - expect(response.statusCode).toBe(400) - expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') - }) - - it('should return 404 if the device ID does not exist', async () => { - const response = await request(app) - .put('/_matrix/client/v3/devices/NON_EXISTENT_DEVICE_ID') - .set('Authorization', `Bearer ${validToken}`) - .send(updateData) - - expect(response.statusCode).toBe(404) - }) - - it('should return 404 if the user has no device with the given device ID', async () => { - const deviceId = 'another_device_id' - const response = await request(app) - .put(`/_matrix/client/v3/devices/${deviceId}`) - .set('Authorization', `Bearer ${validToken}`) - .send(updateData) - - expect(response.statusCode).toBe(404) - }) - - it('should return 401 if the user is not authenticated', async () => { - const response = await request(app) - .put(`/_matrix/client/v3/devices/${_device_id}`) - .send(updateData) - - expect(response.statusCode).toBe(401) - }) - }) - }) - describe('/_matrix/client/v3/directory/list/room/:roomId', () => { describe('GET', () => { const publicRoomId = '!testroomid:example.com' @@ -1100,5 +868,159 @@ describe('Use configuration file', () => { expect(numKeyValuePairs).toBe(2) }) }) + describe('/_matrix/client/v3/delete_devices', () => { + let session: string + const userId = '@testuser:example.com' + it('should return 400 if devices is not an array of strings', async () => { + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ devices: 'not an array' }) + expect(response.status).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + + it('should return 400 if auth is provided but invalid', async () => { + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device1', 'device2'], + auth: { invalid: 'auth' } + }) + expect(response.status).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + + it('should successfully delete devices', async () => { + await clientServer.matrixDb.insert('devices', { + device_id: 'device_id', + user_id: userId, + display_name: 'Device to delete' + }) + const response1 = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ devices: ['device_id'] }) + expect(response1.status).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device_id'], + auth: { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: userId }, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + session + } + }) + expect(response.status).toBe(200) + const devices = await clientServer.matrixDb.get( + 'devices', + ['device_id'], + { user_id: userId } + ) + expect(devices).toHaveLength(0) + }) + it('should delete associated pushers', async () => { + await clientServer.matrixDb.insert('devices', { + device_id: 'device1', + user_id: userId, + display_name: 'Test Device' + }) + await clientServer.matrixDb.insert('pushers', { + user_name: userId, + device_display_name: 'Test Device', + app_id: 'test_app', + pushkey: 'test_pushkey', + profile_tag: 'test_profile_tag', + kind: 'test_kind', + app_display_name: 'test_app_display_name', + ts: 0 + }) + const response1 = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ devices: ['device1'] }) + expect(response1.status).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device1'], + auth: { + type: 'm.login.password', + session, + identifier: { type: 'm.id.user', user: userId }, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK' + } + }) + expect(response.status).toBe(200) + + const pushers = await clientServer.matrixDb.get('pushers', ['app_id'], { + user_name: userId + }) + expect(pushers).toHaveLength(0) + + const deletedPushers = await clientServer.matrixDb.get( + 'deleted_pushers', + ['app_id'], + { user_id: userId } + ) + expect(deletedPushers).toHaveLength(1) + expect(deletedPushers[0].app_id).toBe('test_app') + }) + it('should delete messages in batches', async () => { + const deviceId = 'device1' + + await clientServer.matrixDb.insert('devices', { + device_id: deviceId, + user_id: userId + }) + for (let i = 1; i <= 25; i++) { + await clientServer.matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: i, + message_json: JSON.stringify({ content: `Message ${i}` }) + }) + } + + const response1 = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ devices: ['device1'] }) + expect(response1.status).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: [deviceId], + auth: { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: userId }, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + session + } + }) + expect(response.status).toBe(200) + const remainingMessages = await clientServer.matrixDb.get( + 'device_inbox', + ['stream_id'], + { user_id: userId, device_id: deviceId } + ) + expect(remainingMessages).toHaveLength(0) + }) + }) }) }) diff --git a/packages/matrix-client-server/src/index.ts b/packages/matrix-client-server/src/index.ts index 2651269f..5e58459c 100644 --- a/packages/matrix-client-server/src/index.ts +++ b/packages/matrix-client-server/src/index.ts @@ -76,6 +76,8 @@ import passwordReset from './account/password' import delete3pidHandler from './account/3pid/delete' import userSearch from './user_data/user_directory/search' import deactivate from './account/deactivate' +import deleteDevicesHandler from './delete_devices' +import deleteDevice from './devices/deleteDevice' // const tables = {} // Add tables declaration here to add new tables to this.db @@ -93,16 +95,9 @@ export default class MatrixClientServer extends MatrixIdentityServer { + this._uiauthenticate = (req, res, allowedFlows, description, obj, cb) => { this.rateLimiter(req as Request, res as Response, () => { - uiauthenticate(req, res, reference, allowedFlows, description, cb) + uiauthenticate(req, res, allowedFlows, description, obj, cb) }) } } @@ -192,7 +187,8 @@ export default class MatrixClientServer extends MatrixIdentityServer { }) .catch(done) }) + describe('getMaxStreamId', () => { + it('should return the maximum stream ID within the given range', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = 'user1' + const deviceId = 'device1' + + const insertsPromises: Array> = [] + for (let streamId = 1; streamId <= 25; streamId++) { + insertsPromises.push( + matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: streamId, + message_json: JSON.stringify({ content: `Message ${streamId}` }) + }) + ) + } + + return Promise.all(insertsPromises) + }) + .then(() => { + return matrixDb.getMaxStreamId('user1', 'device1', 10, 20, 10) + }) + .then((maxStreamId) => { + expect(maxStreamId).toBe(20) + matrixDb.close() + }) + .then(() => done()) + .catch(done) + }) + + it('should return an empty array if no stream IDs are found', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = 'user2' + const deviceId = 'device2' + + return matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: 1, + message_json: JSON.stringify({ content: 'Message 1' }) + }) + }) + .then(() => { + return matrixDb.getMaxStreamId('user2', 'device2', 50, 100, 10) + }) + .then((maxStreamId) => { + expect(maxStreamId).toBe(null) + matrixDb.close() + }) + .then(() => done()) + .catch(done) + }) + + it('should handle cases where limit is 1', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = 'user3' + const deviceId = 'device3' + + const insertsPromises: Array> = [] + for (let streamId = 1; streamId <= 15; streamId++) { + insertsPromises.push( + matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: streamId, + message_json: JSON.stringify({ content: `Message ${streamId}` }) + }) + ) + } + + return Promise.all(insertsPromises) + }) + .then(() => { + return matrixDb.getMaxStreamId('user3', 'device3', 5, 15, 1) + }) + .then((maxStreamId) => { + expect(maxStreamId).toBe(6) + matrixDb.close() + }) + .then(() => done()) + .catch(done) + }) + + it('should handle cases with special characters in user_id or device_id', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = 'user@domain.com' + const deviceId = 'device#1' + + const insertsPromises: Array> = [] + for (let streamId = 1; streamId <= 10; streamId++) { + insertsPromises.push( + matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: streamId, + message_json: JSON.stringify({ content: `Message ${streamId}` }) + }) + ) + } + + return Promise.all(insertsPromises) + }) + .then(() => { + return matrixDb.getMaxStreamId( + 'user@domain.com', + 'device#1', + 1, + 10, + 10 + ) + }) + .then((maxStreamId) => { + expect(maxStreamId).toBe(10) + matrixDb.close() + }) + .then(() => done()) + .catch(done) + }) + }) }) diff --git a/packages/matrix-client-server/src/matrixDb/index.ts b/packages/matrix-client-server/src/matrixDb/index.ts index 97e7d761..a4de2873 100644 --- a/packages/matrix-client-server/src/matrixDb/index.ts +++ b/packages/matrix-client-server/src/matrixDb/index.ts @@ -63,6 +63,7 @@ export type Collections = | 'event_json' | 'device_auth_providers' | 'dehydrated_devices' + | 'device_inbox' type sqlComparaisonOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | '<>' interface ISQLCondition { @@ -142,6 +143,13 @@ type SearchUserDirectory = ( limit: number, searchAllUsers: boolean ) => Promise +type GetMaxStreamId = ( + userId: string, + deviceId: string, + lowerBoundStreamId: number, + upperBoundStreamId: number, + limit: number +) => Promise export interface MatrixDBmodifiedBackend { ready: Promise @@ -158,6 +166,7 @@ export interface MatrixDBmodifiedBackend { deleteEqual: DeleteEqual deleteWhere: DeleteWhere updateWithConditions: updateWithConditions + getMaxStreamId: GetMaxStreamId // This function is only used in the delete_devices function // The following functions are specific to the user_directory module searchUserDirectory: SearchUserDirectory close: () => void @@ -479,6 +488,23 @@ class MatrixDBmodified implements MatrixDBmodifiedBackend { this.db.close() } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxStreamId( + userId: string, + deviceId: string, + lowerBoundStreamId: number, + upperBoundStreamId: number, + limit: number + ) { + return this.db.getMaxStreamId( + userId, + deviceId, + lowerBoundStreamId, + upperBoundStreamId, + limit + ) + } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async searchUserDirectory( userId: string, diff --git a/packages/matrix-client-server/src/matrixDb/sql/pg.ts b/packages/matrix-client-server/src/matrixDb/sql/pg.ts index 4ad1400f..9296e5c4 100644 --- a/packages/matrix-client-server/src/matrixDb/sql/pg.ts +++ b/packages/matrix-client-server/src/matrixDb/sql/pg.ts @@ -104,6 +104,50 @@ class MatrixDBPg extends Pg implements MatrixDBmodifiedBackend { }) } + // eslint-disable-next-line @typescript-eslint/promise-function-async + getMaxStreamId( + userId: string, + deviceId: string, + fromStreamId: number, + toStreamId: number, + limit: number + ): Promise { + return new Promise((resolve, reject) => { + if (this.db == null) { + reject(new Error('Wait for database to be ready')) + return + } + + const args = [userId, deviceId, fromStreamId, toStreamId, limit] + + const sql = ` + SELECT MAX(stream_id) AS max_stream_id FROM ( + SELECT stream_id FROM device_inbox + WHERE user_id = $1 AND device_id = $2 + AND $3 < stream_id AND stream_id <= $4 + ORDER BY stream_id + LIMIT $5 + ) AS d + ` + + this.db.query( + sql, + args, + ( + err: Error, + result: { rows: Array<{ max_stream_id: number | null }> } + ) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } else { + resolve(result.rows[0].max_stream_id) + } + } + ) + }) + } + // eslint-disable-next-line @typescript-eslint/promise-function-async searchUserDirectory( userId: string, diff --git a/packages/matrix-client-server/src/matrixDb/sql/sqlite.ts b/packages/matrix-client-server/src/matrixDb/sql/sqlite.ts index 7cefc31d..df7883c6 100644 --- a/packages/matrix-client-server/src/matrixDb/sql/sqlite.ts +++ b/packages/matrix-client-server/src/matrixDb/sql/sqlite.ts @@ -92,6 +92,51 @@ class MatrixDBSQLite }) } + // eslint-disable-next-line @typescript-eslint/promise-function-async + getMaxStreamId( + userId: string, + deviceId: string, + fromStreamId: number, + toStreamId: number, + limit: number + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + + const stmt = this.db.prepare(` + SELECT MAX(stream_id) AS max_stream_id FROM ( + SELECT stream_id FROM device_inbox + WHERE user_id = ? AND device_id = ? + AND ? < stream_id AND stream_id <= ? + ORDER BY stream_id + LIMIT ? + ) AS d + `) + + stmt.get( + [userId, deviceId, fromStreamId, toStreamId, limit], + (err: Error | null, row: { max_stream_id: number | null }) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + resolve(row.max_stream_id) + } + } + ) + + stmt.finalize((err: Error | null) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } + }) + }) + } + // eslint-disable-next-line @typescript-eslint/promise-function-async searchUserDirectory( userId: string, diff --git a/packages/matrix-client-server/src/register/index.ts b/packages/matrix-client-server/src/register/index.ts index 5a79dde2..1d686a61 100644 --- a/packages/matrix-client-server/src/register/index.ts +++ b/packages/matrix-client-server/src/register/index.ts @@ -7,7 +7,8 @@ import { type expressAppHandler, send, epoch, - toMatrixId + toMatrixId, + isSenderLocalpartValid } from '@twake/utils' import { type AuthenticationData } from '../types' import { Hash, randomString } from '@twake/crypto' @@ -20,6 +21,11 @@ import { import type { ServerResponse } from 'http' import type e from 'express' import { getRegisterAllowedFlows } from '../utils/userInteractiveAuthentication' +import { + verifyAuthenticationData, + verifyBoolean, + verifyString +} from '../typecheckers' interface Parameters { kind: 'guest' | 'user' @@ -36,16 +42,6 @@ interface RegisterRequestBody { username?: string } -// Reference types for clientDict verification in UiAuthentication -const registerRequestBodyReference = { - device_id: 'string', - inhibit_login: 'boolean', - initial_device_display_name: 'string', - password: 'string', - refresh_token: 'boolean', - username: 'string' -} - interface InsertedData { name: string creation_ts: number @@ -133,17 +129,13 @@ const registerAccount = ( commonUserData.user_type = 'guest' // User type is NULL for normal users } if (password) { - if (typeof password !== 'string' || password.length > 512) { - send(res, 400, errMsg('invalidParam', 'Invalid password')) - } else { - const hash = new Hash() - return hash.ready.then(() => { - return clientServer.matrixDb.insert('users', { - ...commonUserData, - password_hash: hash.sha256(password) // TODO: Handle other hashing algorithms - }) + const hash = new Hash() + return hash.ready.then(() => { + return clientServer.matrixDb.insert('users', { + ...commonUserData, + password_hash: hash.sha256(password) // TODO: Handle other hashing algorithms }) - } + }) } else { return clientServer.matrixDb.insert('users', { ...commonUserData }) } @@ -341,73 +333,114 @@ const register = (clientServer: MatrixClientServer): expressAppHandler => { return } const userAgent = req.headers['user-agent'] ?? 'undefined' - if (parameters.kind === 'user') { - clientServer.uiauthenticate( - req, - res, - registerRequestBodyReference, - getRegisterAllowedFlows(clientServer.conf), - 'register a new account', - (obj) => { - const body = obj as unknown as RegisterRequestBody - const deviceId = body.device_id ?? randomString(20) // Length chosen arbitrarily - const username = body.username ?? randomString(9) // Length chosen to match the localpart restrictions for a Matrix userId - const userId = toMatrixId(username, clientServer.conf.server_name) // Checks for username validity are done in this function - clientServer.matrixDb - .get('users', ['name'], { - name: userId - }) - .then((rows) => { - if (rows.length > 0) { - send(res, 400, errMsg('userInUse')) - } else { - clientServer.matrixDb - .get('devices', ['display_name', 'user_id'], { - device_id: deviceId - }) - .then((deviceRows) => { - let initial_device_display_name - if (deviceRows.length > 0) { - // TODO : Refresh access tokens using refresh tokens and invalidate the previous access_token associated with the device after implementing the /refresh endpoint - } else { - initial_device_display_name = - body.initial_device_display_name ?? randomString(20) // Length chosen arbitrarily - registerAccount( - initial_device_display_name, - clientServer, - userId, - deviceId, - ip, - userAgent, - body, - res, - 'user', - body.password - ) - } - }) - .catch((e) => { - // istanbul ignore next - clientServer.logger.error( - 'Error while checking if a device_id is already in use', - e - ) - // istanbul ignore next - send(res, 500, e) - }) - } - }) - .catch((e) => { - // istanbul ignore next - clientServer.logger.error( - 'Error while checking if a username is already in use', - e - ) - // istanbul ignore next - send(res, 500, e) - }) + if (!parameters.kind || parameters.kind === 'user') { + // kind defaults to user + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RegisterRequestBody + if (body.username && !isSenderLocalpartValid(body.username)) { + send(res, 400, errMsg('invalidUsername', 'Invalid username')) + return + } else if (body.device_id && !verifyString(body.device_id)) { + send(res, 400, errMsg('invalidParam', 'Invalid device_id')) + return + } else if ( + body.initial_device_display_name && + !verifyString(body.initial_device_display_name) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid initial_device_display_name') + ) + return + } else if (body.password && !verifyString(body.password)) { + send(res, 400, errMsg('invalidParam', 'Invalid password')) + return + } else if (body.refresh_token && !verifyBoolean(body.refresh_token)) { + send(res, 400, errMsg('invalidParam', 'Invalid refresh_token')) + return + } else if ( + body.inhibit_login !== null && + body.inhibit_login !== undefined && + !verifyBoolean(body.inhibit_login) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid inhibit_login')) + return + } else if ( + body.auth !== null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid auth')) + return } - ) + clientServer.uiauthenticate( + req, + res, + getRegisterAllowedFlows(clientServer.conf), + 'register a new account', + obj, + (obj) => { + const body = obj as unknown as RegisterRequestBody + const deviceId = body.device_id ?? randomString(20) // Length chosen arbitrarily + const username = body.username ?? randomString(9) // Length chosen to match the localpart restrictions for a Matrix userId + const userId = toMatrixId(username, clientServer.conf.server_name) // Checks for username validity are done in this function + clientServer.matrixDb + .get('users', ['name'], { + name: userId + }) + .then((rows) => { + if (rows.length > 0) { + send(res, 400, errMsg('userInUse')) + } else { + clientServer.matrixDb + .get('devices', ['display_name', 'user_id'], { + device_id: deviceId + }) + .then((deviceRows) => { + let initial_device_display_name + if (deviceRows.length > 0) { + // TODO : Refresh access tokens using refresh tokens and invalidate the previous access_token associated with the device after implementing the /refresh endpoint + } else { + initial_device_display_name = + body.initial_device_display_name ?? randomString(20) // Length chosen arbitrarily + registerAccount( + initial_device_display_name, + clientServer, + userId, + deviceId, + ip, + userAgent, + body, + res, + 'user', + body.password + ) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while checking if a device_id is already in use', + e + ) + // istanbul ignore next + send(res, 500, e) + }) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while checking if a username is already in use', + e + ) + // istanbul ignore next + send(res, 500, e) + }) + } + ) + }) } else { // We don't handle the threepid_guest_access_tokens table and give the guest an access token like any user. // This might be problematic to restrict the endpoints guests have access to as specified in the spec diff --git a/packages/matrix-client-server/src/register/register.test.ts b/packages/matrix-client-server/src/register/register.test.ts index ae2eb93e..39b445d4 100644 --- a/packages/matrix-client-server/src/register/register.test.ts +++ b/packages/matrix-client-server/src/register/register.test.ts @@ -454,10 +454,10 @@ describe('Use configuration file', () => { .set('Authorization', `Bearer ${validToken}`) .send({ sid: 'sid', - client_secret: 'cs', + client_secret: 'clientsecret', auth: { type: 'm.login.email.identity', - threepid_creds: { sid: 'sid', client_secret: 'cs' }, + threepid_creds: { sid: 'sid', client_secret: 'clientsecret' }, session } }) @@ -477,6 +477,103 @@ describe('Use configuration file', () => { expect(response.body).toHaveProperty('session') session = response.body.session }) + it('should refuse an invalid password', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ password: 400 }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid password') + }) + it('should refuse an invalid initial_device_display_name', async () => { + let initialDeviceDisplayName = '' + for (let i = 0; i < 1000; i++) { + initialDeviceDisplayName += 'a' + } + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .query({ kind: 'user' }) + .send({ initial_device_display_name: initialDeviceDisplayName }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty( + 'error', + 'Invalid initial_device_display_name' + ) + }) + it('should refuse an invalid username', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.dummy', + session: 'session' + }, + username: '@localhost:example.com' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_INVALID_USERNAME') + }) + it('should refuse an invalid deviceId', async () => { + let deviceId = '' + for (let i = 0; i < 1000; i++) { + deviceId += 'a' + } + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ device_id: deviceId }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid device_id') + }) + it('should refuse an invalid inhibit_login', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ inhibit_login: 'true' }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid inhibit_login') + }) + it('should refuse an invalid auth', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { type: 'wrongtype' } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid auth') + }) + it('should refuse an invalid refresh_token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + refresh_token: 'notaboolean' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid refresh_token') + }) it('should run the register endpoint after authentication was completed', async () => { const response = await request(app) .post('/_matrix/client/v3/register') @@ -521,10 +618,7 @@ describe('Use configuration file', () => { }) expect(response.statusCode).toBe(400) expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') - expect(response.body).toHaveProperty( - 'error', - 'Invalid refresh_token: expected boolean, got string' - ) + expect(response.body).toHaveProperty('error', 'Invalid refresh_token') }) it('should refuse an invalid password', async () => { const response = await request(app) @@ -535,10 +629,7 @@ describe('Use configuration file', () => { .send({ password: 400 }) expect(response.statusCode).toBe(400) expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') - expect(response.body).toHaveProperty( - 'error', - 'Invalid password: expected string, got number' - ) + expect(response.body).toHaveProperty('error', 'Invalid password') }) it('should refuse an invalid initial_device_display_name', async () => { let initialDeviceDisplayName = '' @@ -557,7 +648,7 @@ describe('Use configuration file', () => { expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') expect(response.body).toHaveProperty( 'error', - 'initial_device_display_name exceeds 512 characters' + 'Invalid initial_device_display_name' ) }) it('should refuse an invalid deviceId', async () => { @@ -575,10 +666,7 @@ describe('Use configuration file', () => { }) expect(response.statusCode).toBe(400) expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') - expect(response.body).toHaveProperty( - 'error', - 'device_id exceeds 512 characters' - ) + expect(response.body).toHaveProperty('error', 'Invalid device_id') }) it('should only return the userId when inhibit login is set to true', async () => { const response1 = await request(app) @@ -612,36 +700,6 @@ describe('Use configuration file', () => { expect(response.body).not.toHaveProperty('access_token') expect(response.body).not.toHaveProperty('device_id') }) - it('should refuse an incorrect username', async () => { - const response1 = await request(app) - .post('/_matrix/client/v3/register') - .set('User-Agent', 'curl/7.31.0-DEV') - .set('X-Forwarded-For', '203.0.113.195') - .query({ kind: 'user' }) - .send({ - username: 'new_user', - device_id: 'device_Id', - inhibit_login: true, - initial_device_display_name: 'testdevice' - }) - expect(response1.statusCode).toBe(401) - session = response1.body.session - const response = await request(app) - .post('/_matrix/client/v3/register') - .set('User-Agent', 'curl/7.31.0-DEV') - .set('X-Forwarded-For', '203.0.113.195') - .query({ kind: 'user' }) - .send({ - auth: { - type: 'm.login.dummy', - session - }, - username: '@localhost:example.com' - }) - expect(response.statusCode).toBe(400) - expect(response.body).toHaveProperty('error') - expect(response.body).toHaveProperty('errcode', 'M_INVALID_USERNAME') - }) it('should accept guest registration', async () => { const response = await request(app) .post('/_matrix/client/v3/register') diff --git a/packages/matrix-client-server/src/typecheckers.test.ts b/packages/matrix-client-server/src/typecheckers.test.ts new file mode 100644 index 00000000..ddbe747d --- /dev/null +++ b/packages/matrix-client-server/src/typecheckers.test.ts @@ -0,0 +1,312 @@ +import { + verifyString, + verifyArray, + verifyObject, + verifyNumber, + verifyBoolean, + verifyUserIdentifier, + verifyThreepidCreds, + verifyAuthenticationData +} from './typecheckers' +import { type AuthenticationData, type UserIdentifier } from './types' + +describe('Typecheck Functions', () => { + describe('verifyString', () => { + it('should return true for valid strings', () => { + expect(verifyString('hello')).toBe(true) + expect(verifyString('a'.repeat(511))).toBe(true) + }) + + it('should return false for invalid strings', () => { + expect(verifyString('')).toBe(false) + expect(verifyString('a'.repeat(513))).toBe(false) + expect(verifyString(123)).toBe(false) + expect(verifyString(null)).toBe(false) + expect(verifyString(undefined)).toBe(false) + }) + }) + + describe('verifyArray', () => { + it('should return true for valid arrays', () => { + expect(verifyArray(['a', 'b', 'c'], 'string')).toBe(true) + expect(verifyArray([1, 2, 3], 'number')).toBe(true) + }) + + it('should return false for invalid arrays', () => { + expect(verifyArray([], 'string')).toBe(false) + expect(verifyArray([1, 'b', 3], 'string')).toBe(false) + expect(verifyArray('not an array', 'string')).toBe(false) + }) + }) + + describe('verifyObject', () => { + it('should return true for valid objects', () => { + expect(verifyObject({ key: 'value' })).toBe(true) + expect(verifyObject({})).toBe(true) + }) + + it('should return false for invalid objects', () => { + expect(verifyObject(null)).toBe(false) + expect(verifyObject([])).toBe(false) + expect(verifyObject('not an object')).toBe(false) + }) + }) + + describe('verifyNumber', () => { + it('should return true for valid numbers', () => { + expect(verifyNumber(123)).toBe(true) + expect(verifyNumber(-456)).toBe(true) + }) + + it('should return false for invalid numbers', () => { + expect(verifyNumber('not a number')).toBe(false) + expect(verifyNumber(NaN)).toBe(false) + }) + }) + + describe('verifyBoolean', () => { + it('should return true for valid booleans', () => { + expect(verifyBoolean(true)).toBe(true) + expect(verifyBoolean(false)).toBe(true) + }) + + it('should return false for invalid booleans', () => { + expect(verifyBoolean('true')).toBe(false) + expect(verifyBoolean(1)).toBe(false) + }) + }) + + describe('verifyUserIdentifier', () => { + it('should return true for valid MatrixIdentifier', () => { + const identifier = { type: 'm.id.user', user: '@user:matrix.org' } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(true) + }) + + it('should return false for invalid MatrixIdentifier', () => { + const identifier = { type: 'm.id.user', user: 'invalidUser' } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(false) + }) + + it('should return true for valid ThirdPartyIdentifier', () => { + const identifier = { + type: 'm.id.thirdparty', + medium: 'email', + address: 'user@example.com' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(true) + }) + + it('should return false for invalid ThirdPartyIdentifier', () => { + const identifier = { + type: 'm.id.thirdparty', + medium: 'email', + address: 'invalidEmail' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(false) + }) + + it('should return true for valid PhoneIdentifier', () => { + const identifier = { + type: 'm.id.phone', + country: 'US', + phone: '1234567890' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(true) + }) + + it('should return false for invalid PhoneIdentifier', () => { + const identifier = { + type: 'm.id.phone', + country: 'US', + phone: 'invalidPhone' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(false) + }) + it('should return false for invalid UserIdentifier', () => { + const identifier = { + type: 'm.id.invalid' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(false) + }) + }) + + describe('verifyThreepidCreds', () => { + it('should return true for valid ThreepidCreds', () => { + const creds = { sid: 'sid123', client_secret: 'secret' } + expect(verifyThreepidCreds(creds)).toBe(true) + }) + + it('should return false for invalid ThreepidCreds', () => { + const creds = { sid: 'sid123', client_secret: '' } // Invalid client_secret + expect(verifyThreepidCreds(creds)).toBe(false) + }) + }) + + describe('verifyAuthenticationData', () => { + it('should return true for valid PasswordAuth', () => { + const authData = { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: '@user:matrix.org' }, + password: 'password123', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid PasswordAuth', () => { + const authData = { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: 'invalidUser' }, // Invalid user ID + password: 'password123', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid EmailAuth', () => { + const authData = { + type: 'm.login.email.identity', + threepid_creds: { sid: 'sid123', client_secret: 'secret' }, + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid EmailAuth', () => { + const authData = { + type: 'm.login.email.identity', + threepid_creds: { sid: '', client_secret: 'secret' }, // Invalid sid + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + it('should return true for valid RecaptchaAuth', () => { + const authData = { + type: 'm.login.recaptcha', + response: 'recaptchaResponse', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid RecaptchaAuth (missing session)', () => { + const authData = { + type: 'm.login.recaptcha', + response: 'recaptchaResponse' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return false for invalid RecaptchaAuth (empty response)', () => { + const authData = { + type: 'm.login.recaptcha', + response: '', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid SsoAuth', () => { + const authData = { + type: 'm.login.sso', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid SsoAuth (missing session)', () => { + const authData = { + type: 'm.login.sso' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid DummyAuth', () => { + const authData = { + type: 'm.login.dummy', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid DummyAuth (empty session)', () => { + const authData = { + type: 'm.login.dummy', + session: '' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid TokenAuth', () => { + const authData = { + type: 'm.login.registration_token', + token: 'registrationToken', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid TokenAuth (missing token)', () => { + const authData = { + type: 'm.login.registration_token', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid ApplicationServiceAuth', () => { + const authData = { + type: 'm.login.application_service', + username: 'user123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid ApplicationServiceAuth (missing username)', () => { + const authData = { + type: 'm.login.application_service' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return false for invalid AuthenticationData (unknown type)', () => { + const authData = { + type: 'm.login.unknown', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + }) +}) diff --git a/packages/matrix-client-server/src/typecheckers.ts b/packages/matrix-client-server/src/typecheckers.ts new file mode 100644 index 00000000..9e1d7f09 --- /dev/null +++ b/packages/matrix-client-server/src/typecheckers.ts @@ -0,0 +1,121 @@ +import { + isClientSecretValid, + isCountryValid, + isEmailValid, + isMatrixIdValid, + isPhoneNumberValid, + isSidValid +} from '@twake/utils' +import { + type AuthenticationData, + type ThreepidCreds, + type UserIdentifier +} from './types' + +const MAX_STRINGS_LENGTH = 512 // Arbitrary value, could be changed + +export const verifyString = (value: any): boolean => { + return ( + typeof value === 'string' && + value.length > 0 && + value.length < MAX_STRINGS_LENGTH + ) +} + +export const verifyArray = (value: any, expectedType: string): boolean => { + if (!Array.isArray(value) || value.length === 0) { + return false + } + // eslint-disable-next-line valid-typeof + return value.every((element) => typeof element === expectedType) +} +export const verifyObject = (value: any): boolean => { + return typeof value === 'object' && value !== null && !Array.isArray(value) // Since typeof returns 'object' for arrays, we need to check that it's not an array +} + +export const verifyNumber = (value: any): boolean => { + return ( + typeof value === 'number' && + !Number.isNaN(value) && + value.toString().length < MAX_STRINGS_LENGTH // Again arbitrary check so that the numbers aren't absurdly large + ) +} + +export const verifyBoolean = (value: any): boolean => { + return typeof value === 'boolean' +} + +// Function to validate UserIdentifier +export const verifyUserIdentifier = (identifier: UserIdentifier): boolean => { + if (!verifyObject(identifier)) return false + + switch (identifier.type) { + case 'm.id.user': + return isMatrixIdValid(identifier.user) + + case 'm.id.thirdparty': + return ( + (identifier.medium === 'msisdn' && + isPhoneNumberValid(identifier.address)) || + (identifier.medium === 'email' && isEmailValid(identifier.address)) + ) + + case 'm.id.phone': + return ( + isCountryValid(identifier.country) && + isPhoneNumberValid(identifier.phone) + ) + + default: + return false + } +} + +// Function to validate ThreepidCreds +export const verifyThreepidCreds = (creds: ThreepidCreds): boolean => { + return ( + isSidValid(creds.sid) && + isClientSecretValid(creds.client_secret) && + (creds.id_server === undefined || verifyString(creds.id_server)) && + (creds.id_access_token === undefined || verifyString(creds.id_access_token)) + ) +} + +// Main function to validate AuthenticationData +export const verifyAuthenticationData = ( + authData: AuthenticationData +): boolean => { + if (!verifyObject(authData)) return false + + switch (authData.type) { + case 'm.login.password': + return ( + verifyUserIdentifier(authData.identifier) && + verifyString(authData.password) && + verifyString(authData.session) + ) + + case 'm.login.email.identity': + case 'm.login.msisdn': + return ( + verifyThreepidCreds(authData.threepid_creds) && + verifyString(authData.session) + ) + + case 'm.login.recaptcha': + return verifyString(authData.response) && verifyString(authData.session) + + case 'm.login.sso': + case 'm.login.dummy': + case 'm.login.terms': + return verifyString(authData.session) + + case 'm.login.registration_token': + return verifyString(authData.token) && verifyString(authData.session) + + case 'm.login.application_service': + return verifyString(authData.username) // Could be userId or localpart according to spec so we only check if it's a string : https://spec.matrix.org/v1.11/client-server-api/#appservice-login + default: + return false + } +} diff --git a/packages/matrix-client-server/src/types.ts b/packages/matrix-client-server/src/types.ts index add83f58..1d8cc6dc 100644 --- a/packages/matrix-client-server/src/types.ts +++ b/packages/matrix-client-server/src/types.ts @@ -266,7 +266,7 @@ export interface UserQuota { size: number } -export type clientDbCollections = 'ui_auth_sessions' +export type clientDbCollections = '' export type ClientServerDb = IdentityServerDb diff --git a/packages/matrix-client-server/src/utils/UIAuth.md b/packages/matrix-client-server/src/utils/UIAuth.md index 80b09703..177e4f9e 100644 --- a/packages/matrix-client-server/src/utils/UIAuth.md +++ b/packages/matrix-client-server/src/utils/UIAuth.md @@ -8,7 +8,7 @@ To use this method in functions that require user interactive authentication, fo 1. Use the `uiauthenticate` method similarly to the `authenticate` method for `/register` 2. For other endpoints that use UI-Authentication and that are authenticated (such as `/add` for example), you first need to call the `clientServer.authenticate` method, followed by `validateUserWithUiAuthentication`. The second method checks that the user associated to the given access token is indeed who he claims to be, it serves as additional security. -3. Since we insert the request body in the `clientdict` column of the `ui_auth_sessions` table, we need to verify its content. For that we check type validity and that the strings are not too long (don't exceed 512 characters) with the `verifyClientDict` method. For this to work, you need to pass in an object that imposes the reference types as the `reference` argument as it is done in account/3pid/add.ts or register/index.ts . +3. Since we insert the request body in the `clientdict` column of the `ui_auth_sessions` table, we need to verify its content. For that we check type validity and that the strings are not too long (don't exceed 512 characters) before calling the `uiauthenticate` or the `validateUserWithUIAuth` methods. ## Allowed Flows diff --git a/packages/matrix-client-server/src/utils/userInteractiveAuthentication.ts b/packages/matrix-client-server/src/utils/userInteractiveAuthentication.ts index 3a97f4e7..4c3a06c9 100644 --- a/packages/matrix-client-server/src/utils/userInteractiveAuthentication.ts +++ b/packages/matrix-client-server/src/utils/userInteractiveAuthentication.ts @@ -14,21 +14,14 @@ import { } from '../types' import { Hash, randomString } from '@twake/crypto' import type MatrixDBmodified from '../matrixDb' -import { - epoch, - errMsg, - jsonContent, - send, - toMatrixId, - isMatrixIdValid -} from '@twake/utils' +import { epoch, errMsg, send, toMatrixId, isMatrixIdValid } from '@twake/utils' import type MatrixClientServer from '..' export type UiAuthFunction = ( req: Request | http.IncomingMessage, res: Response | http.ServerResponse, - reference: Record, allowedFlows: AuthenticationFlowContent, description: string, + obj: any, callback: (data: any, userId: string | null) => void ) => void @@ -68,9 +61,9 @@ export const validateUserWithUIAuthentication = ( clientServer: MatrixClientServer, req: Request | http.IncomingMessage, res: Response | http.ServerResponse, - reference: Record, userId: string, description: string, + obj: any, callback: (data: any, userId: string | null) => void ): void => { if (userId != null && !isMatrixIdValid(userId)) { @@ -80,6 +73,7 @@ export const validateUserWithUIAuthentication = ( errMsg('invalidParam', 'Invalid user ID'), clientServer.logger ) + return } // Authentication flows to verify that the user who has an access token is indeed who he claims to be, and has not just stolen another user's access token getAvailableValidateUIAuthFlows(clientServer, userId) @@ -87,9 +81,9 @@ export const validateUserWithUIAuthentication = ( clientServer.uiauthenticate( req, res, - reference, verificationFlows, description, + obj, callback ) }) @@ -220,9 +214,7 @@ export const getRegisterAllowedFlows = ( // eslint-disable-next-line @typescript-eslint/promise-function-async const checkAuthentication = ( auth: AuthenticationData, - matrixDb: MatrixDBmodified, - conf: Config, - req: Request | http.IncomingMessage + matrixDb: MatrixDBmodified ): Promise => { // It returns a Promise so that it can return the userId of the authenticated user for endpoints other than /register. For register and dummy auth we return ''. switch (auth.type) { @@ -254,7 +246,7 @@ const checkAuthentication = ( reject( errMsg( 'forbidden', - 'The user does not have a password registered' + 'The user does not have a password registered or the provided password is wrong.' ) ) }) @@ -457,260 +449,213 @@ const doAppServiceAuthentication = ( }) } -const verifyClientDict = ( - res: e.Response | http.ServerResponse, - content: T, - reference: Record, - logger: TwakeLogger, - callback: (obj: T) => void -): void => { - for (const key in reference) { - const expectedType = reference[key] - const value = (content as any)[key] - - if (value !== null && value !== undefined) { - // eslint-disable-next-line valid-typeof - if (typeof value !== expectedType) { - send( - res, - 400, - errMsg( - 'invalidParam', - `Invalid ${key}: expected ${expectedType}, got ${typeof value}` - ), - logger - ) - return - } - - if (expectedType === 'string' && (value as string).length > 512) { - send( - res, - 400, - errMsg('invalidParam', `${key} exceeds 512 characters`), - logger - ) - return - } - } - } - callback(content) -} - const UiAuthenticate = ( // db: ClientServerDb, matrixDb: MatrixDBmodified, conf: Config, logger: TwakeLogger ): UiAuthFunction => { - return (req, res, reference, allowedFlows, description, callback) => { - jsonContent(req, res, logger, (obj) => { + return (req, res, allowedFlows, description, obj, callback) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!(obj as requestBody).auth) { + // If there is no auth key in the request body, we create a new authentication session + const sessionId = randomString(24) // Chose 24 according to synapse implementation but seems arbitrary + const ip = (req as e.Request).ip + // istanbul ignore if + if (ip === undefined) { + // istanbul ignore next + send(res, 500, errMsg('unknown', 'IP address is missing')) + return + } + const userAgent = req.headers['user-agent'] ?? 'undefined' + const addUserIps = matrixDb.insert('ui_auth_sessions_ips', { + session_id: sessionId, + ip, + user_agent: userAgent + }) // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!(obj as requestBody).auth) { - verifyClientDict(res, obj, reference, logger, (obj) => { - // If there is no auth key in the request body, we create a new authentication session - const sessionId = randomString(24) // Chose 24 according to synapse implementation but seems arbitrary - const ip = (req as e.Request).ip - // istanbul ignore if - if (ip === undefined) { - // istanbul ignore next - send(res, 500, errMsg('unknown', 'IP address is missing')) - return - } - const userAgent = req.headers['user-agent'] ?? 'undefined' - const addUserIps = matrixDb.insert('ui_auth_sessions_ips', { - session_id: sessionId, - ip, - user_agent: userAgent - }) - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (obj.password) { - // Since we store the clientdict in the database, we don't want to store the unhashed password in it - delete obj.password - } - const createAuthSession = matrixDb.insert('ui_auth_sessions', { - session_id: sessionId, - creation_time: epoch(), - clientdict: JSON.stringify(obj), - serverdict: JSON.stringify({}), - uri: req.url as string, // TODO : Ensure this is the right way to get the URI - method: req.method as string, - description - }) - Promise.all([addUserIps, createAuthSession]) - .then(() => { - send( - // We send back the session_id to the client so that he can use it in future requests - res, - 401, - { - ...allowedFlows, - session: sessionId - }, - logger - ) - }) - .catch((e) => { - /* istanbul ignore next */ - logger.error( - 'Error while creating a new session during User-Interactive Authentication', - e - ) - /* istanbul ignore next */ - send(res, 500, e, logger) - }) - }) - } else { - const auth = (obj as requestBody).auth as AuthenticationData - if (auth.type === 'm.login.application_service') { - doAppServiceAuthentication( - req, + if (obj.password) { + // Since we store the clientdict in the database, we don't want to store the unhashed password in it + delete obj.password + } + const createAuthSession = matrixDb.insert('ui_auth_sessions', { + session_id: sessionId, + creation_time: epoch(), + clientdict: JSON.stringify(obj), + serverdict: JSON.stringify({}), + uri: req.url as string, // TODO : Ensure this is the right way to get the URI + method: req.method as string, + description + }) + Promise.all([addUserIps, createAuthSession]) + .then(() => { + send( + // We send back the session_id to the client so that he can use it in future requests res, - allowedFlows, - auth, - conf, - logger, - obj, - callback + 401, + { + ...allowedFlows, + session: sessionId + }, + logger ) - return - } - matrixDb - .get('ui_auth_sessions', ['*'], { session_id: auth.session }) - .then((rows) => { - if (rows.length === 0) { - logger.error(`Unknown session ID : ${auth.session}`) - send(res, 400, errMsg('noValidSession'), logger) - } else if ( - rows[0].uri !== req.url || - rows[0].method !== req.method - ) { - send( - res, - 403, - errMsg( - 'forbidden', - 'Requested operation has changed during the UI authentication session.' - ), - logger - ) - } else { - checkAuthentication(auth, matrixDb, conf, req) - .then((userId) => { - matrixDb - .insert('ui_auth_sessions_credentials', { - session_id: auth.session, - stage_type: auth.type, - result: userId - }) - .then((rows) => { - const getCompletedStages = matrixDb.get( - 'ui_auth_sessions_credentials', - ['stage_type'], - { - session_id: auth.session - } - ) - const updateClientDict = matrixDb.updateWithConditions( - 'ui_auth_sessions', - { clientdict: JSON.stringify(obj) }, - [{ field: 'session_id', value: auth.session }] - ) - Promise.all([getCompletedStages, updateClientDict]) - .then((rows) => { - const completed: string[] = rows[0].map( - (row) => row.stage_type as string - ) - const authOver = allowedFlows.flows.some((flow) => { - return ( - flow.stages.length === completed.length && - flow.stages.every((stage) => - completed.includes(stage) - ) - ) - }) - - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (authOver) { - callback(obj, userId) // Arguments of callback are subject to change - } else { - send( - res, - 401, - { - ...allowedFlows, - session: auth.session, - completed - }, - logger + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while creating a new session during User-Interactive Authentication', + e + ) + /* istanbul ignore next */ + send(res, 500, e, logger) + }) + } else { + const auth = (obj as requestBody).auth as AuthenticationData + if (auth.type === 'm.login.application_service') { + doAppServiceAuthentication( + req, + res, + allowedFlows, + auth, + conf, + logger, + obj, + callback + ) + return + } + matrixDb + .get('ui_auth_sessions', ['*'], { session_id: auth.session }) + .then((rows) => { + if (rows.length === 0) { + logger.error(`Unknown session ID : ${auth.session}`) + send(res, 400, errMsg('noValidSession'), logger) + } else if (rows[0].uri !== req.url || rows[0].method !== req.method) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Requested operation has changed during the UI authentication session.' + ), + logger + ) + } else { + checkAuthentication(auth, matrixDb) + .then((userId) => { + matrixDb + .insert('ui_auth_sessions_credentials', { + session_id: auth.session, + stage_type: auth.type, + result: userId + }) + .then((rows) => { + const getCompletedStages = matrixDb.get( + 'ui_auth_sessions_credentials', + ['stage_type'], + { + session_id: auth.session + } + ) + const updateClientDict = matrixDb.updateWithConditions( + 'ui_auth_sessions', + { clientdict: JSON.stringify(obj) }, + [{ field: 'session_id', value: auth.session }] + ) + Promise.all([getCompletedStages, updateClientDict]) + .then((rows) => { + const completed: string[] = rows[0].map( + (row) => row.stage_type as string + ) + const authOver = allowedFlows.flows.some((flow) => { + return ( + flow.stages.length === completed.length && + flow.stages.every((stage) => + completed.includes(stage) ) - } - }) - .catch((e) => { - /* istanbul ignore next */ - logger.error( - 'Error while retrieving session credentials from the database during User-Interactive Authentication', - e ) - /* istanbul ignore next */ - send(res, 400, e, logger) }) - }) - .catch((e) => { - /* istanbul ignore next */ - logger.error( - 'Error while inserting session credentials into the database during User-Interactive Authentication', - e - ) - /* istanbul ignore next */ - send(res, 400, e, logger) - }) - }) - .catch((e) => { - matrixDb - .get('ui_auth_sessions_credentials', ['stage_type'], { - session_id: auth.session - }) - .then((rows) => { - const completed: string[] = rows.map( - // istanbul ignore next - (row) => row.stage_type as string - ) - send( - res, - 401, - { - errcode: e.errcode, - error: e.error, - completed, - ...allowedFlows, - session: auth.session - }, - logger - ) - }) - .catch((e) => { - /* istanbul ignore next */ - logger.error( - 'Error while retrieving session credentials from the database during User-Interactive Authentication', - e - ) - /* istanbul ignore next */ - send(res, 400, e, logger) - }) - }) - } - }) - .catch((e) => { - // istanbul ignore next - logger.error( - 'Error retrieving UI Authentication session from the database' - ) - // istanbul ignore next - send(res, 500, e, logger) - }) - } - }) + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (authOver) { + callback(obj, userId) // Arguments of callback are subject to change + } else { + send( + res, + 401, + { + ...allowedFlows, + session: auth.session, + completed + }, + logger + ) + } + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while retrieving session credentials from the database during User-Interactive Authentication', + e + ) + /* istanbul ignore next */ + send(res, 500, e, logger) + }) + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while inserting session credentials into the database during User-Interactive Authentication', + e + ) + /* istanbul ignore next */ + send(res, 500, e, logger) + }) + }) + .catch((e) => { + matrixDb + .get('ui_auth_sessions_credentials', ['stage_type'], { + session_id: auth.session + }) + .then((rows) => { + const completed: string[] = rows.map( + // istanbul ignore next + (row) => row.stage_type as string + ) + send( + res, + 401, + { + errcode: e.errcode, + error: e.error, + completed, + ...allowedFlows, + session: auth.session + }, + logger + ) + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while retrieving session credentials from the database during User-Interactive Authentication', + e + ) + /* istanbul ignore next */ + send(res, 500, e, logger) + }) + }) + } + }) + .catch((e) => { + // istanbul ignore next + logger.error( + 'Error retrieving UI Authentication session from the database' + ) + // istanbul ignore next + send(res, 500, e, logger) + }) + } } } diff --git a/packages/utils/src/regex.ts b/packages/utils/src/regex.ts index c7fccac3..9875a023 100644 --- a/packages/utils/src/regex.ts +++ b/packages/utils/src/regex.ts @@ -3,6 +3,7 @@ const clientSecretRegex: RegExp = /^[0-9a-zA-Z.=_-]{6,255}$/ const eventTypeRegex: RegExp = /^(?:[a-z]+(?:\.[a-z][a-z0-9_]*)*)$/ // Following Java's package naming convention as per : https://spec.matrix.org/v1.11/#events const matrixIdRegex: RegExp = /^@[0-9a-zA-Z._=-]+:[0-9a-zA-Z.-]+$/ +const senderLocalpartRegex: RegExp = /^[a-z0-9_\-./=+]+$/ const roomIdRegex: RegExp = /^![0-9a-zA-Z._=/+-]+:[0-9a-zA-Z.-]+$/ // From : https://spec.matrix.org/v1.11/#room-structure const sidRegex: RegExp = /^[0-9a-zA-Z.=_-]{1,255}$/ const countryRegex: RegExp = /^[A-Z]{2}$/ // ISO 3166-1 alpha-2 as per the spec : https://spec.matrix.org/v1.11/client-server-api/#post_matrixclientv3registermsisdnrequesttoken @@ -21,6 +22,10 @@ export const isEventTypeValid = (eventType: string): boolean => export const isMatrixIdValid = (matrixId: string): boolean => matrixIdRegex.test(matrixId) && Buffer.byteLength(matrixId) < 256 +export const isSenderLocalpartValid = (senderLocalpart: string): boolean => + senderLocalpartRegex.test(senderLocalpart) && + Buffer.byteLength(senderLocalpart) < 256 + export const isRoomIdValid = (roomId: string): boolean => roomIdRegex.test(roomId) && Buffer.byteLength(roomId) < 256