Skip to content

Commit

Permalink
Merge pull request #126 from michaelkamphausen/feature/unique-userid-…
Browse files Browse the repository at this point in the history
…username

auth: ensure that userId and user name are unique among team members
  • Loading branch information
HerbCaudill authored Sep 24, 2024
2 parents 593f5c7 + eaf5956 commit acedb19
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 3 deletions.
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

0 comments on commit acedb19

Please sign in to comment.