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

auth: ensure that userId and user name are unique among team members #126

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions packages/auth/src/team/Team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,14 +555,34 @@ export class Team extends EventEmitter<TeamEvents> {
return invitations.validate(proof, invitation)
}

/** 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()
)
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 userValidation = this.validateUser(memberKeys.name, userName)
if (!userValidation.isValid) throw userValidation.error

const { id } = proof

Expand Down
54 changes: 53 additions & 1 deletion packages/auth/src/team/test/invitations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -336,6 +336,58 @@ describe('Team', () => {
).not.toThrow()
})

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 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 }) => 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')
Expand Down
24 changes: 24 additions & 0 deletions packages/auth/src/team/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,30 @@ const validators: TeamStateValidatorSet = {
}
return VALID
},

/** Check if userId and userName are not used by any other member within the team */
uniqueUserNameAndId(...args) {
const [previousState, link] = args
if (link.body.type === 'ADMIT_MEMBER') {
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()
)

if (memberWithSameUserName !== undefined) {
return fail('Username is not unique within the team.', ...args)
}
}
return VALID
},
}

const fail = (message: string, previousState: TeamState, link: TeamLink) => {
Expand Down
Loading