Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix store invite #113

Merged
merged 9 commits into from
Jul 17, 2024
3 changes: 3 additions & 0 deletions packages/matrix-identity-server/src/3pid/bind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
})
return
}

// TODO : hook for any pending invite and call the onbind api : https://spec.matrix.org/v1.11/client-server-api/#room-aliases

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment

idServer.db
.get('keys', ['data'], { name: 'pepper' })
.then(async (pepperRow) => {
Expand Down
25 changes: 25 additions & 0 deletions packages/matrix-identity-server/src/db/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@ describe('Id Server DB', () => {
.catch(done)
})

it('should provide verification-token', (done) => {
idDb = new IdDb(baseConf, logger)
idDb.ready
.then(() => {
idDb
.createInvitationToken('randomMailorPhone', { a: 1 })
.then((token) => {
expect(token).toMatch(/^[a-zA-Z0-9]+$/)
idDb
.verifyInvitationToken(token)
.then((data) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
// @ts-ignore
expect(data.a).toEqual(1)
clearTimeout(idDb.cleanJob)
idDb.close()
done()
})
.catch((e) => done(e))
})
.catch((e) => done(e))
})
.catch((e) => done(e))
})

it('should provide match()', (done) => {
idDb = new IdDb(baseConf, logger)
idDb.ready
Expand Down
88 changes: 71 additions & 17 deletions packages/matrix-identity-server/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,51 @@ export type SupportedDatabases = 'sqlite' | 'pg'

export type Collections =
| 'accessTokens'
| 'oneTimeTokens'
| 'activeContacts'
| 'attempts'
| 'keys'
| 'oneTimeTokens'
| 'hashes'
| 'invitationTokens'
| 'keys'
| 'longTermKeypairs'
| 'mappings'
| 'privateNotes'
| 'roomTags'
| 'userHistory'
| 'userQuotas'
| 'mappings'
| 'longTermKeypairs'
| 'shortTermKeypairs'
| 'userHistory'
| 'userPolicies'
| 'activeContacts'
| 'userQuotas'

const cleanByExpires: Collections[] = ['oneTimeTokens', 'attempts']

const tables: Record<Collections, string> = {
accessTokens: 'id varchar(64) PRIMARY KEY, data text',
oneTimeTokens: 'id varchar(64) PRIMARY KEY, expires int, data text',
activeContacts: 'userId text PRIMARY KEY, contacts text',
attempts: 'email text PRIMARY KEY, expires int, attempt int',
keys: 'name varchar(32) PRIMARY KEY, data text',
oneTimeTokens: 'id varchar(64) PRIMARY KEY, expires int, data text',
hashes:
'hash varchar(48) PRIMARY KEY, pepper varchar(32), type varchar(8), value text, active integer',
invitationTokens: 'id varchar(64) PRIMARY KEY, address text, data text',
keys: 'name varchar(32) PRIMARY KEY, data text',
longTermKeypairs:
'name text PRIMARY KEY, keyID varchar(64), public text, private text',
mappings:
'client_secret varchar(255) PRIMARY KEY, session_id varchar(12), medium varchar(8), valid integer, address text, submit_time integer, send_attempt integer',
privateNotes:
'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, targetId varchar(64)',
roomTags:
'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, roomId varchar(64)',
userHistory: 'address text PRIMARY KEY, active integer, timestamp integer',
userQuotas: 'user_id varchar(64) PRIMARY KEY, size int',
mappings:
'client_secret varchar(255) PRIMARY KEY, session_id varchar(12), medium varchar(8), valid integer, address text, submit_time integer, send_attempt integer',
longTermKeypairs:
'name text PRIMARY KEY, keyID varchar(64), public text, private text',
shortTermKeypairs:
'keyID varchar(64) PRIMARY KEY, public text, private text, active integer',
userHistory: 'address text PRIMARY KEY, active integer, timestamp integer',
userPolicies: 'user_id text, policy_name text, accepted integer',
activeContacts: 'userId text PRIMARY KEY, contacts text'
userQuotas: 'user_id varchar(64) PRIMARY KEY, size int'
}

const indexes: Partial<Record<Collections, string[]>> = {
oneTimeTokens: ['expires'],
attempts: ['expires'],
invitationTokens: ['address'],
oneTimeTokens: ['expires'],
userHistory: ['timestamp']
}

Expand Down Expand Up @@ -563,6 +566,57 @@ class IdentityServerDb<T extends string = never>
return this.createOneTimeToken(data, expires)
}

// eslint-disable-next-line @typescript-eslint/promise-function-async
createInvitationToken(address: string, data: object): Promise<string> {
/* istanbul ignore if */
if (this.db == null) {
throw new Error('Wait for database to be ready')
}
const id = randomString(64)
return new Promise((resolve, reject) => {
this.db
.insert('invitationTokens', {
id,
address,
data: JSON.stringify(data)
})
.then(() => {
this.logger.info(`Invitation token created for ${address}`)
resolve(id)
})
.catch((err) => {
/* istanbul ignore next */
this.logger.error('Failed to insert token', err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing "reject": a promise must always call resolve or reject

reject(err)
})
})
}

// eslint-disable-next-line @typescript-eslint/promise-function-async
verifyInvitationToken(id: string): Promise<object> {
/* istanbul ignore if */
if (this.db == null) {
throw new Error('Wait for database to be ready')
}
return new Promise((resolve, reject) => {
this.db
.get('invitationTokens', ['data', 'address'], { id })
.then((rows) => {
/* istanbul ignore else */
if (rows.length > 0) {
resolve(JSON.parse(rows[0].data as string))
} else {
reject(new Error('Unknown token'))
}
})
.catch((e) => {
/* istanbul ignore next */
this.logger.error('Failed to get token', e)
reject(e)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add logger.error ?

})
})
}

// eslint-disable-next-line @typescript-eslint/promise-function-async
verifyToken(id: string): Promise<object> {
/* istanbul ignore if */
Expand Down
48 changes: 20 additions & 28 deletions packages/matrix-identity-server/src/ephemeral_signing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,37 +41,29 @@ const SignEd25519 = <T extends string = never>(
send(res, 400, errMsg('invalidParam', 'invalid Matrix user ID'))
} else {
idServer.db
.get('oneTimeTokens', ['data'], { id: token })
.then((rows) => {
if (rows.length === 0) {
send(res, 404, errMsg('invalidParam', 'token not found'))
} else {
const parsedData = JSON.parse(rows[0].data as string)
const sender = parsedData.sender
const newToken = randomString(64)
const identifier = nacl.randomBytes(8)
let identifierHex = naclUtil.encodeBase64(identifier)
identifierHex = toBase64Url(identifierHex)
send(
res,
200,
signJson(
{ mxid, sender, token: newToken },
privateKey,
idServer.conf.server_name,
`ed25519:${identifierHex}`
)
.verifyInvitationToken(token)
.then((data) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
// @ts-ignore
const sender = data.sender as string
const newToken = randomString(64)
const identifier = nacl.randomBytes(8)
let identifierHex = naclUtil.encodeBase64(identifier)
identifierHex = toBase64Url(identifierHex)
send(
res,
200,
signJson(
{ mxid, sender, token: newToken },
privateKey,
idServer.conf.server_name,
`ed25519:${identifierHex}`
)
}
)
})
.catch((err) => {
/* istanbul ignore next */
idServer.logger.error(
'Error while fetching one-time token',
err
)
/* istanbul ignore next */
send(res, 400, errMsg('unknown', err))
idServer.logger.error('Token denied', err)
guimard marked this conversation as resolved.
Show resolved Hide resolved
send(res, 404, errMsg('notFound', err))
})
}
})
Expand Down
Loading
Loading