From 96c3ad5a90b5be2d837370fab9bbef6ccd4f24e9 Mon Sep 17 00:00:00 2001 From: Michael Kamphausen Date: Fri, 30 Aug 2024 15:09:07 +0200 Subject: [PATCH 1/4] accept proof of invitation from a new user only if the username is unique among team members --- packages/auth/src/team/Team.ts | 19 +++++++++++++-- .../auth/src/team/test/invitations.test.ts | 23 ++++++++++++++++++- packages/auth/src/team/validate.ts | 16 +++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/auth/src/team/Team.ts b/packages/auth/src/team/Team.ts index c2ca881b..0fbc0beb 100644 --- a/packages/auth/src/team/Team.ts +++ b/packages/auth/src/team/Team.ts @@ -554,14 +554,29 @@ export class Team extends EventEmitter { return invitations.validate(proof, invitation) } + /** Check if username is not used by any other person within the team. */ + public validateUserName = (userName: string) => { + const memberWithSameUserName = this.members().find(member => + member.userName.toLowerCase() == userName.toLowerCase() + ) + if (memberWithSameUserName != undefined) { + return invitations.fail('Username is not unique within the team.') + } + + return VALID; + } + /** An existing team member calls this to admit a new member & their device to the team based on proof of invitation */ public admitMember = ( proof: ProofOfInvitation, memberKeys: Keyset | KeysetWithSecrets, // We accept KeysetWithSecrets here to simplify testing - in practice we'll only receive Keyset userName: string // The new member's desired user-facing name ) => { - const validation = this.validateInvitation(proof) - if (!validation.isValid) throw validation.error + const invitationValidation = this.validateInvitation(proof) + if (!invitationValidation.isValid) throw invitationValidation.error + + const userNameValidation = this.validateUserName(userName) + if (!userNameValidation.isValid) throw userNameValidation.error const { id } = proof diff --git a/packages/auth/src/team/test/invitations.test.ts b/packages/auth/src/team/test/invitations.test.ts index a050eed2..ec40202c 100644 --- a/packages/auth/src/team/test/invitations.test.ts +++ b/packages/auth/src/team/test/invitations.test.ts @@ -80,7 +80,7 @@ describe('Team', () => { expect(bobsTeam.memberIsAdmin(bob.userId)).toBe(false) // ๐Ÿ‘ณ๐Ÿฝโ€โ™‚๏ธ Charlie shows ๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฒ Bob his proof of invitation - bobsTeam.admitMember(proofOfInvitation, charlie.user.keys, bob.user.userName) + bobsTeam.admitMember(proofOfInvitation, charlie.user.keys, charlie.user.userName) // ๐Ÿ‘๐Ÿ‘ณ๐Ÿฝโ€โ™‚๏ธ Charlie is now on the team expect(bobsTeam.has(charlie.userId)).toBe(true) @@ -260,6 +260,27 @@ describe('Team', () => { expect(submitBadProof).toThrow('Signature provided is not valid') }) + it("won't accept proof of invitation with a username that is not unique", () => { + const { alice, bob } = setup('alice', { user: 'bob', member: false }) + + // ๐Ÿ‘ฉ๐Ÿพ Alice invites ๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฒ Bob by sending him a random secret key + const { seed } = alice.team.inviteMember() + + // ๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฒ Bob accepts the invitation + const proofOfInvitation = generateProof(seed) + + // ๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฒ Bob shows ๐Ÿ‘ฉ๐Ÿพ Alice his proof of invitation, but uses Alice's username + const tryToAdmitBob = () => { + alice.team.admitMember(proofOfInvitation, bob.user.keys, alice.user.userName) + } + + // ๐Ÿ‘Ž But the invitation is rejected because it the username is not unique + expect(tryToAdmitBob).toThrowError('Username is not unique within the team.') + + // โŒ ๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฒ Bob is not on the team + expect(alice.team.has(bob.userId)).toBe(false) + }) + describe('devices', () => { it('creates and accepts an invitation for a device', () => { const { alice: aliceLaptop } = setup('alice') diff --git a/packages/auth/src/team/validate.ts b/packages/auth/src/team/validate.ts index 33ebc884..6e1a8813 100644 --- a/packages/auth/src/team/validate.ts +++ b/packages/auth/src/team/validate.ts @@ -112,6 +112,22 @@ const validators: TeamStateValidatorSet = { } return VALID }, + + /** Check if the username is not used by any other person within the team */ + mustBeUniqueUsername(...args) { + const [previousState, link] = args + if (link.body.type === 'ADMIT_MEMBER') { + const { userName } = link.body.payload + const memberWithSameUserName = previousState.members.find(member => + member.userName.toLowerCase() == userName.toLowerCase() + ) + + if (memberWithSameUserName != undefined) { + return fail('Username is not unique within the team.', ...args) + } + } + return VALID + }, } const fail = (message: string, previousState: TeamState, link: TeamLink) => { From 299a94177d18b02db56e417d53d3ab4f3468083c Mon Sep 17 00:00:00 2001 From: Michael Kamphausen Date: Fri, 30 Aug 2024 15:09:45 +0200 Subject: [PATCH 2/4] accept proof of invitation from a new user only if the userId is unique among team members --- packages/auth/src/team/Team.ts | 13 +++++--- .../auth/src/team/test/invitations.test.ts | 31 ++++++++++++++++++- packages/auth/src/team/validate.ts | 14 +++++++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/auth/src/team/Team.ts b/packages/auth/src/team/Team.ts index 0fbc0beb..3229bd63 100644 --- a/packages/auth/src/team/Team.ts +++ b/packages/auth/src/team/Team.ts @@ -554,8 +554,13 @@ export class Team extends EventEmitter { return invitations.validate(proof, invitation) } - /** Check if username is not used by any other person within the team. */ - public validateUserName = (userName: string) => { + /** Check if userId and userName are not used by any other member within the team. */ + public validateUser = (userId: string, userName: string) => { + const memberWithSameUserId = this.members().find(member => member.userId == userId) + if (memberWithSameUserId != undefined) { + return invitations.fail('userId is not unique within the team.') + } + const memberWithSameUserName = this.members().find(member => member.userName.toLowerCase() == userName.toLowerCase() ) @@ -575,8 +580,8 @@ export class Team extends EventEmitter { const invitationValidation = this.validateInvitation(proof) if (!invitationValidation.isValid) throw invitationValidation.error - const userNameValidation = this.validateUserName(userName) - if (!userNameValidation.isValid) throw userNameValidation.error + const userValidation = this.validateUser(memberKeys.name, userName) + if (!userValidation.isValid) throw userValidation.error const { id } = proof diff --git a/packages/auth/src/team/test/invitations.test.ts b/packages/auth/src/team/test/invitations.test.ts index ec40202c..f63723a5 100644 --- a/packages/auth/src/team/test/invitations.test.ts +++ b/packages/auth/src/team/test/invitations.test.ts @@ -274,13 +274,42 @@ describe('Team', () => { alice.team.admitMember(proofOfInvitation, bob.user.keys, alice.user.userName) } - // ๐Ÿ‘Ž But the invitation is rejected because it the username is not unique + // ๐Ÿ‘Ž But the invitation is rejected because the username is not unique expect(tryToAdmitBob).toThrowError('Username is not unique within the team.') // โŒ ๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฒ Bob is not on the team expect(alice.team.has(bob.userId)).toBe(false) }) + it("won't accept proof of invitation with a userId that is not unique", () => { + const { alice, eve } = setup('alice', { user: 'eve', member: false }) + + // ๐Ÿ‘ฉ๐Ÿพ Alice invites ๐Ÿฆนโ€โ™€๏ธ Eve by sending her a random secret key + const { seed } = alice.team.inviteMember() + + // ๐Ÿฆนโ€โ™€๏ธ Eve accepts the invitation + const proofOfInvitation = generateProof(seed) + + // ๐Ÿฆนโ€โ™€๏ธ Eve prepares keys using Alice's userId + const keysWithAliceUserId = { + ...eve.user.keys, + name: alice.userId + } + + // ๐Ÿฆนโ€โ™€๏ธ Eve shows ๐Ÿ‘ฉ๐Ÿพ Alice her proof of invitation, but uses Alice's userId + const tryToAdmitEve = () => { + alice.team.admitMember(proofOfInvitation, keysWithAliceUserId, eve.user.userName) + } + + // ๐Ÿ‘Ž But the invitation is rejected because the userId is not unique + expect(tryToAdmitEve).toThrowError('userId is not unique within the team.') + + // โŒ ๐Ÿฆนโ€โ™€๏ธ Eve is not on the team + expect(alice.team.has(eve.userId)).toBe(false) + expect(alice.team.state.members.filter(({ userId }) => alice.userId)).toHaveLength(1) + expect(alice.team.members(alice.userId).userName == alice.userName).toBe(true) + }) + describe('devices', () => { it('creates and accepts an invitation for a device', () => { const { alice: aliceLaptop } = setup('alice') diff --git a/packages/auth/src/team/validate.ts b/packages/auth/src/team/validate.ts index 6e1a8813..5922231e 100644 --- a/packages/auth/src/team/validate.ts +++ b/packages/auth/src/team/validate.ts @@ -113,11 +113,19 @@ const validators: TeamStateValidatorSet = { return VALID }, - /** Check if the username is not used by any other person within the team */ - mustBeUniqueUsername(...args) { + /** Check if userId and userName are not used by any other member within the team */ + mustBeUniqueUserNameAndUserId(...args) { const [previousState, link] = args if (link.body.type === 'ADMIT_MEMBER') { - const { userName } = link.body.payload + const { userName, memberKeys } = link.body.payload + + const memberWithSameUserId = previousState.members.find(member => + member.userId == memberKeys.name + ) + if (memberWithSameUserId != undefined) { + return fail('userId is not unique within the team.', ...args) + } + const memberWithSameUserName = previousState.members.find(member => member.userName.toLowerCase() == userName.toLowerCase() ) From ba80a04ec396a9627581bf18407b3b910d719ccb Mon Sep 17 00:00:00 2001 From: Herb Caudill Date: Thu, 19 Sep 2024 15:24:10 +0200 Subject: [PATCH 3/4] make linter happy --- packages/auth/src/team/Team.ts | 12 ++++++------ packages/auth/src/team/test/invitations.test.ts | 8 +++++--- packages/auth/src/team/validate.ts | 12 ++++++------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/auth/src/team/Team.ts b/packages/auth/src/team/Team.ts index 3229bd63..66bc1eae 100644 --- a/packages/auth/src/team/Team.ts +++ b/packages/auth/src/team/Team.ts @@ -556,19 +556,19 @@ export class Team extends EventEmitter { /** Check if userId and userName are not used by any other member within the team. */ public validateUser = (userId: string, userName: string) => { - const memberWithSameUserId = this.members().find(member => member.userId == userId) - if (memberWithSameUserId != undefined) { + const memberWithSameUserId = this.members().find(member => member.userId === userId) + if (memberWithSameUserId !== undefined) { return invitations.fail('userId is not unique within the team.') } - const memberWithSameUserName = this.members().find(member => - member.userName.toLowerCase() == userName.toLowerCase() + const memberWithSameUserName = this.members().find( + member => member.userName.toLowerCase() === userName.toLowerCase() ) - if (memberWithSameUserName != undefined) { + if (memberWithSameUserName !== undefined) { return invitations.fail('Username is not unique within the team.') } - return VALID; + return VALID } /** An existing team member calls this to admit a new member & their device to the team based on proof of invitation */ diff --git a/packages/auth/src/team/test/invitations.test.ts b/packages/auth/src/team/test/invitations.test.ts index f63723a5..c4acfdf5 100644 --- a/packages/auth/src/team/test/invitations.test.ts +++ b/packages/auth/src/team/test/invitations.test.ts @@ -293,7 +293,7 @@ describe('Team', () => { // ๐Ÿฆนโ€โ™€๏ธ Eve prepares keys using Alice's userId const keysWithAliceUserId = { ...eve.user.keys, - name: alice.userId + name: alice.userId, } // ๐Ÿฆนโ€โ™€๏ธ Eve shows ๐Ÿ‘ฉ๐Ÿพ Alice her proof of invitation, but uses Alice's userId @@ -306,8 +306,10 @@ describe('Team', () => { // โŒ ๐Ÿฆนโ€โ™€๏ธ Eve is not on the team expect(alice.team.has(eve.userId)).toBe(false) - expect(alice.team.state.members.filter(({ userId }) => alice.userId)).toHaveLength(1) - expect(alice.team.members(alice.userId).userName == alice.userName).toBe(true) + expect( + alice.team.state.members.filter(({ userId }) => userId === alice.userId) + ).toHaveLength(1) + expect(alice.team.members(alice.userId).userName === alice.userName).toBe(true) }) describe('devices', () => { diff --git a/packages/auth/src/team/validate.ts b/packages/auth/src/team/validate.ts index 5922231e..89a35650 100644 --- a/packages/auth/src/team/validate.ts +++ b/packages/auth/src/team/validate.ts @@ -119,18 +119,18 @@ const validators: TeamStateValidatorSet = { if (link.body.type === 'ADMIT_MEMBER') { const { userName, memberKeys } = link.body.payload - const memberWithSameUserId = previousState.members.find(member => - member.userId == memberKeys.name + const memberWithSameUserId = previousState.members.find( + member => member.userId === memberKeys.name ) - if (memberWithSameUserId != undefined) { + if (memberWithSameUserId !== undefined) { return fail('userId is not unique within the team.', ...args) } - const memberWithSameUserName = previousState.members.find(member => - member.userName.toLowerCase() == userName.toLowerCase() + const memberWithSameUserName = previousState.members.find( + member => member.userName.toLowerCase() === userName.toLowerCase() ) - if (memberWithSameUserName != undefined) { + if (memberWithSameUserName !== undefined) { return fail('Username is not unique within the team.', ...args) } } From 2c155d3617834e3e1f8a707f1f04f9602680d943 Mon Sep 17 00:00:00 2001 From: Herb Caudill Date: Thu, 19 Sep 2024 15:30:51 +0200 Subject: [PATCH 4/4] rename validator --- packages/auth/src/team/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auth/src/team/validate.ts b/packages/auth/src/team/validate.ts index 89a35650..b65232d5 100644 --- a/packages/auth/src/team/validate.ts +++ b/packages/auth/src/team/validate.ts @@ -114,7 +114,7 @@ const validators: TeamStateValidatorSet = { }, /** Check if userId and userName are not used by any other member within the team */ - mustBeUniqueUserNameAndUserId(...args) { + uniqueUserNameAndId(...args) { const [previousState, link] = args if (link.body.type === 'ADMIT_MEMBER') { const { userName, memberKeys } = link.body.payload