Skip to content

Commit

Permalink
Merge pull request #126 from linagora/client-server-refresh
Browse files Browse the repository at this point in the history
Client server refresh
  • Loading branch information
guimard authored Jul 22, 2024
2 parents c260d5e + 9709354 commit bffa57f
Show file tree
Hide file tree
Showing 23 changed files with 758 additions and 158 deletions.
12 changes: 7 additions & 5 deletions packages/matrix-client-server/src/__testData__/buildUserDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,23 @@ const matrixDbQueries = [
'CREATE TABLE IF NOT EXISTS users( name TEXT, password_hash TEXT, creation_ts BIGINT, admin SMALLINT DEFAULT 0 NOT NULL, upgrade_ts BIGINT, is_guest SMALLINT DEFAULT 0 NOT NULL, appservice_id TEXT, consent_version TEXT, consent_server_notice_sent TEXT, user_type TEXT DEFAULT NULL, deactivated SMALLINT DEFAULT 0 NOT NULL, shadow_banned INT DEFAULT 0, consent_ts bigint, UNIQUE(name) )',
'CREATE TABLE IF NOT EXISTS user_ips ( user_id TEXT NOT NULL, access_token TEXT NOT NULL, device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, last_seen BIGINT NOT NULL)',
'CREATE TABLE IF NOT EXISTS registration_tokens (token TEXT NOT NULL, uses_allowed INT, pending INT NOT NULL, completed INT NOT NULL, expiry_time BIGINT,UNIQUE (token))',
'CREATE TABLE IF NOT EXISTS events( stream_ordering INTEGER PRIMARY KEY, topological_ordering BIGINT NOT NULL, event_id TEXT NOT NULL, type TEXT NOT NULL, room_id TEXT NOT NULL, content TEXT, unrecognized_keys TEXT, processed BOOL NOT NULL, outlier BOOL NOT NULL, depth BIGINT DEFAULT 0 NOT NULL, origin_server_ts BIGINT, received_ts BIGINT, sender TEXT, contains_url BOOLEAN, instance_name TEXT, state_key TEXT DEFAULT NULL, rejection_reason TEXT DEFAULT NULL, UNIQUE (event_id) )',
'CREATE TABLE IF NOT EXISTS events( stream_ordering INTEGER PRIMARY KEY, topological_ordering BIGINT NOT NULL, event_id TEXT NOT NULL, type TEXT NOT NULL, room_id TEXT NOT NULL, content TEXT, unrecognized_keys TEXT, processed BOOL NOT NULL, outlier BOOL NOT NULL, depth BIGINT DEFAULT 0 NOT NULL, origin_server_ts BIGINT, received_ts BIGINT, sender TEXT, contains_url INT, instance_name TEXT, state_key TEXT DEFAULT NULL, rejection_reason TEXT DEFAULT NULL, UNIQUE (event_id) )',
'CREATE TABLE IF NOT EXISTS room_memberships( event_id TEXT NOT NULL, user_id TEXT NOT NULL, sender TEXT NOT NULL, room_id TEXT NOT NULL, membership TEXT NOT NULL, forgotten INTEGER DEFAULT 0, display_name TEXT, avatar_url TEXT, UNIQUE (event_id) )',
'CREATE TABLE IF NOT EXISTS devices (user_id TEXT NOT NULL, device_id TEXT NOT NULL, display_name TEXT, last_seen BIGINT, ip TEXT, user_agent TEXT, hidden BOOLEAN DEFAULT 0,CONSTRAINT device_uniqueness UNIQUE (user_id, device_id))',
'CREATE TABLE IF NOT EXISTS devices (user_id TEXT NOT NULL, device_id TEXT NOT NULL, display_name TEXT, last_seen BIGINT, ip TEXT, user_agent TEXT, hidden INT DEFAULT 0,CONSTRAINT device_uniqueness UNIQUE (user_id, device_id))',
'CREATE TABLE IF NOT EXISTS account_data( user_id TEXT NOT NULL, account_data_type TEXT NOT NULL, stream_id BIGINT NOT NULL, content TEXT NOT NULL, instance_name TEXT, CONSTRAINT account_data_uniqueness UNIQUE (user_id, account_data_type))',
'CREATE TABLE IF NOT EXISTS room_account_data( user_id TEXT NOT NULL, room_id TEXT NOT NULL, account_data_type TEXT NOT NULL, stream_id BIGINT NOT NULL, content TEXT NOT NULL, instance_name TEXT, CONSTRAINT room_account_data_uniqueness UNIQUE (user_id, room_id, account_data_type) )',
'CREATE TABLE IF NOT EXISTS profiles( user_id TEXT NOT NULL, displayname TEXT, avatar_url TEXT, UNIQUE(user_id) )',
'CREATE TABLE IF NOT EXISTS local_current_membership (room_id TEXT NOT NULL, user_id TEXT NOT NULL, event_id TEXT NOT NULL, membership TEXT NOT NULL)',
'CREATE TABLE IF NOT EXISTS room_stats_state (room_id TEXT NOT NULL,name TEXT,canonical_alias TEXT,join_rules TEXT,history_visibility TEXT,encryption TEXT,avatar TEXT,guest_access TEXT,is_federatable BOOLEAN,topic TEXT, room_type TEXT)',
'CREATE TABLE IF NOT EXISTS room_stats_state (room_id TEXT NOT NULL,name TEXT,canonical_alias TEXT,join_rules TEXT,history_visibility TEXT,encryption TEXT,avatar TEXT,guest_access TEXT,is_federatable INT,topic TEXT, room_type TEXT)',
'CREATE TABLE IF NOT EXISTS room_aliases( room_alias TEXT NOT NULL, room_id TEXT NOT NULL, creator TEXT, UNIQUE (room_alias) )',
'CREATE TABLE IF NOT EXISTS rooms( room_id TEXT PRIMARY KEY NOT NULL, is_public BOOL, creator TEXT , room_version TEXT, has_auth_chain_index BOOLEAN)',
'CREATE TABLE IF NOT EXISTS rooms( room_id TEXT PRIMARY KEY NOT NULL, is_public BOOL, creator TEXT , room_version TEXT, has_auth_chain_index INT)',
'CREATE TABLE IF NOT EXISTS room_tags( user_id TEXT NOT NULL, room_id TEXT NOT NULL, tag TEXT NOT NULL, content TEXT NOT NULL, CONSTRAINT room_tag_uniqueness UNIQUE (user_id, room_id, tag) )',
'CREATE TABLE IF NOT EXISTS "user_threepids" ( user_id TEXT NOT NULL, medium TEXT NOT NULL, address TEXT NOT NULL, validated_at BIGINT NOT NULL, added_at BIGINT NOT NULL, CONSTRAINT medium_address UNIQUE (medium, address) )',
'CREATE TABLE IF NOT EXISTS threepid_validation_session (session_id TEXT PRIMARY KEY,medium TEXT NOT NULL,address TEXT NOT NULL,client_secret TEXT NOT NULL,last_send_attempt BIGINT NOT NULL,validated_at BIGINT)',
'CREATE TABLE IF NOT EXISTS threepid_validation_token (token TEXT PRIMARY KEY,session_id TEXT NOT NULL,next_link TEXT,expires BIGINT NOT NULL)',
'CREATE TABLE IF NOT EXISTS presence (user_id TEXT NOT NULL, state VARCHAR(20), status_msg TEXT, mtime BIGINT, UNIQUE (user_id))'
'CREATE TABLE IF NOT EXISTS presence (user_id TEXT NOT NULL, state VARCHAR(20), status_msg TEXT, mtime BIGINT, UNIQUE (user_id))',
'CREATE TABLE IF NOT EXISTS refresh_tokens (id BIGINT PRIMARY KEY,user_id TEXT NOT NULL,device_id TEXT NOT NULL,token TEXT NOT NULL,next_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE, expiry_ts BIGINT DEFAULT NULL, ultimate_session_expiry_ts BIGINT DEFAULT NULL,UNIQUE(token))',
'CREATE TABLE IF NOT EXISTS "access_tokens" (id BIGINT PRIMARY KEY, user_id TEXT NOT NULL, device_id TEXT, token TEXT NOT NULL,valid_until_ms BIGINT,puppets_user_id TEXT,last_validated BIGINT, refresh_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE, used INT, UNIQUE(token))'
]

// eslint-disable-next-line @typescript-eslint/promise-function-async
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import type MatrixClientServer from '../../../index'
import Mailer from '../../../utils/mailer'
import {
fillTable,
fillTableAndSend,
getSubmitUrl,
preConfigureTemplate
} from '../../../register/email/requestToken'
Expand All @@ -36,6 +36,7 @@ const schema = {

const clientSecretRe = /^[0-9a-zA-Z.=_-]{6,255}$/
const validEmailRe = /^\w[+.-\w]*\w@\w[.-\w]*\w\.\w{2,6}$/
const maxAttemps = 1000000000

const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
const transport = new Mailer(clientServer.conf)
Expand Down Expand Up @@ -70,6 +71,16 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
errMsg('invalidParam', 'invalid next_link'),
clientServer.logger
)
} else if (
typeof sendAttempt !== 'number' ||
sendAttempt > maxAttemps
) {
send(
res,
400,
errMsg('invalidParam', 'Invalid send attempt'),
clientServer.logger
)
} else {
clientServer.matrixDb
.get('user_threepids', ['user_id'], { address: dst })
Expand Down Expand Up @@ -113,7 +124,7 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
}
])
.then(() => {
fillTable(
fillTableAndSend(
// The calls to send are made in this function
clientServer,
dst,
Expand All @@ -128,7 +139,7 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
})
.catch((err) => {
// istanbul ignore next
clientServer.logger.error('Deletion error')
clientServer.logger.error('Deletion error', err)
// istanbul ignore next
send(
res,
Expand All @@ -139,7 +150,7 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
})
}
} else {
fillTable(
fillTableAndSend(
// The calls to send are made in this function
clientServer,
dst,
Expand All @@ -155,15 +166,15 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
})
.catch((err) => {
/* istanbul ignore next */
clientServer.logger.error('Send_attempt error')
clientServer.logger.error('Send_attempt error', err)
/* istanbul ignore next */
send(res, 500, errMsg('unknown', err), clientServer.logger)
})
}
})
.catch((err) => {
/* istanbul ignore next */
clientServer.logger.error('Error getting userID')
clientServer.logger.error('Error getting userID :', err)
/* istanbul ignore next */
send(res, 500, errMsg('unknown', err), clientServer.logger)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type MatrixClientServer from '../../../index'
import SmsSender from '../../../utils/smsSender'
import { getSubmitUrl } from '../../../register/email/requestToken'
import {
fillTable,
fillTableAndSend,
formatPhoneNumber,
preConfigureTemplate
} from '../../../register/msisdn/requestToken'
Expand Down Expand Up @@ -40,6 +40,7 @@ const schema = {
const clientSecretRegex = /^[0-9a-zA-Z.=_-]{6,255}$/
const validCountryRegex = /^[A-Z]{2}$/ // ISO 3166-1 alpha-2 as per the spec : https://spec.matrix.org/v1.11/client-server-api/#post_matrixclientv3registermsisdnrequesttoken
const validPhoneNumberRegex = /^[1-9]\d{1,14}$/
const maxAttemps = 1000000000

const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
const transport = new SmsSender(clientServer.conf)
Expand Down Expand Up @@ -88,6 +89,16 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
errMsg('invalidParam', 'Invalid phone number'),
clientServer.logger
)
} else if (
typeof sendAttempt !== 'number' ||
sendAttempt > maxAttemps
) {
send(
res,
400,
errMsg('invalidParam', 'Invalid send attempt'),
clientServer.logger
)
} else {
clientServer.matrixDb
.get('user_threepids', ['user_id'], { address: dst })
Expand Down Expand Up @@ -131,7 +142,7 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
}
])
.then(() => {
fillTable(
fillTableAndSend(
// The calls to send are made in this function
clientServer,
dst,
Expand All @@ -146,7 +157,7 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
})
.catch((err) => {
// istanbul ignore next
clientServer.logger.error('Deletion error')
clientServer.logger.error('Deletion error:', err)
// istanbul ignore next
send(
res,
Expand All @@ -157,7 +168,7 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
})
}
} else {
fillTable(
fillTableAndSend(
// The calls to send are made in this function
clientServer,
dst,
Expand All @@ -173,15 +184,15 @@ const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => {
})
.catch((err) => {
/* istanbul ignore next */
clientServer.logger.error('Send_attempt error')
clientServer.logger.error('Send_attempt error:', err)
/* istanbul ignore next */
send(res, 500, errMsg('unknown', err), clientServer.logger)
})
}
})
.catch((err) => {
/* istanbul ignore next */
clientServer.logger.error('Error getting userID')
clientServer.logger.error('Error getting userID :', err)
/* istanbul ignore next */
send(res, 500, errMsg('unknown', err), clientServer.logger)
})
Expand Down
119 changes: 84 additions & 35 deletions packages/matrix-client-server/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import { type Config } from './types'
import defaultConfig from './__testData__/registerConf.json'
import { getLogger, type TwakeLogger } from '@twake/logger'
import { Hash, randomString } from '@twake/crypto'
import {
setupTokens,
validToken,
validToken2,
validRefreshToken1,
validRefreshToken2
} from './utils/setupTokens'

process.env.TWAKE_CLIENT_SERVER_CONF = './src/__testData__/registerConf.json'
jest.mock('node-fetch', () => jest.fn())
Expand Down Expand Up @@ -268,44 +275,56 @@ describe('Use configuration file', () => {
})
})

let validToken: string
let validToken2: string
let validToken3: string
describe('Endpoints with authentication', () => {
beforeAll(async () => {
validToken = randomString(64)
validToken2 = randomString(64)
validToken3 = randomString(64)
try {
await clientServer.matrixDb.insert('user_ips', {
user_id: '@testuser:example.com',
device_id: 'testdevice',
access_token: validToken,
ip: '127.0.0.1',
user_agent: 'curl/7.31.0-DEV',
last_seen: 1411996332123
})

await clientServer.matrixDb.insert('user_ips', {
user_id: '@testuser2:example.com',
device_id: 'testdevice2',
access_token: validToken2,
ip: '137.0.0.1',
user_agent: 'curl/7.31.0-DEV',
last_seen: 1411996332123
})

await clientServer.matrixDb.insert('user_ips', {
user_id: '@testuser3:example.com',
device_id: 'testdevice3',
access_token: validToken3,
ip: '147.0.0.1',
user_agent: 'curl/7.31.0-DEV',
last_seen: 1411996332123
await setupTokens(clientServer, logger)
})
describe('/_matrix/client/v3/refresh', () => {
it('should refuse a request without refresh token', async () => {
const response = await request(app)
.post('/_matrix/client/v3/refresh')
.send({})
expect(response.statusCode).toBe(400)
expect(response.body.errcode).toBe('M_MISSING_PARAMS')
})
it('should refuse a request with an unknown refresh token', async () => {
const response = await request(app)
.post('/_matrix/client/v3/refresh')
.send({ refresh_token: 'unknownToken' })
expect(response.statusCode).toBe(400)
expect(response.body.errcode).toBe('M_UNKNOWN_TOKEN')
})
it('should refuse a request with an expired refresh token', async () => {
await clientServer.matrixDb.insert('refresh_tokens', {
id: 0,
user_id: 'expiredUser',
device_id: 'expiredDevice',
token: 'expiredToken',
expiry_ts: 0
})
} catch (e) {
logger.error('Error creating tokens for authentification', e)
}
const response = await request(app)
.post('/_matrix/client/v3/refresh')
.send({ refresh_token: 'expiredToken' })
expect(response.statusCode).toBe(401)
expect(response.body.errcode).toBe('INVALID_TOKEN')
})
it('should send the next request token if the token sent in the request has such a field in the DB', async () => {
const response = await request(app)
.post('/_matrix/client/v3/refresh')
.send({ refresh_token: validRefreshToken1 })
expect(response.statusCode).toBe(200)
expect(response.body).toHaveProperty('access_token')
expect(response.body).toHaveProperty('refresh_token')
expect(response.body.refresh_token).toBe(validRefreshToken2)
})
it('should generate a new refresh token and access token if there was no next token in the DB', async () => {
const response = await request(app)
.post('/_matrix/client/v3/refresh')
.send({ refresh_token: validRefreshToken2 })
expect(response.statusCode).toBe(200)
expect(response.body).toHaveProperty('access_token')
expect(response.body).toHaveProperty('refresh_token')
})
})
describe('/_matrix/client/v3/account/whoami', () => {
let asToken: string
Expand Down Expand Up @@ -927,6 +946,21 @@ describe('Use configuration file', () => {
expect(response.statusCode).toBe(403)
expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN')
})
it('should refuse content that is too long', async () => {
let content = ''
for (let i = 0; i < 10000; i++) {
content += 'a'
}
const response = await request(app)
.put(
'/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message'
)
.set('Authorization', `Bearer ${validToken}`)
.set('Accept', 'application/json')
.send({ content })
expect(response.statusCode).toBe(400)
expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM')
})
it('should update account data', async () => {
const response = await request(app)
.put(
Expand Down Expand Up @@ -1069,6 +1103,21 @@ describe('Use configuration file', () => {
expect(response.statusCode).toBe(403)
expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN')
})
it('should refuse content that is too long', async () => {
let content = ''
for (let i = 0; i < 10000; i++) {
content += 'a'
}
const response = await request(app)
.put(
'/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message'
)
.set('Authorization', `Bearer ${validToken}`)
.set('Accept', 'application/json')
.send({ content })
expect(response.statusCode).toBe(400)
expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM')
})
it('should update account data', async () => {
const response = await request(app)
.put(
Expand Down
Loading

0 comments on commit bffa57f

Please sign in to comment.