mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
WebAuthn platform passkeys: opt-in enrollment after a password login, a password-less sign-in that mints the same 14-day campaign-token, and revocation from the profile. The session is minted through Payload's public pieces (checkLoginPermission, addSessionToUser, getFieldsToSign, jwtSign) inside a transaction with an advisory lock, so a passkey login neither bypasses a lockout nor evicts the account's other sessions. The rpID is derived from NEXT_PUBLIC_SITE_URL and resolves to null when the request host disagrees, so previews never offer a ceremony the browser would refuse.
165 lines
4.9 KiB
TypeScript
165 lines
4.9 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { getPayload, type Payload } from 'payload'
|
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
import config from '@/payload.config'
|
|
|
|
import { installCampaignFixtures } from '../helpers/campaignFixtures'
|
|
|
|
let payload: Payload
|
|
const campaignFixtures = installCampaignFixtures({
|
|
getPayload: () => payload,
|
|
setPayload: (nextPayload) => {
|
|
payload = nextPayload
|
|
},
|
|
})
|
|
|
|
const enrollPasskey = (userID: number, credentialId: string) =>
|
|
payload.create({
|
|
collection: 'campaignWebAuthnCredential',
|
|
data: {
|
|
user: userID,
|
|
credentialId,
|
|
publicKey: 'cHVibGljLWtleQ',
|
|
counter: 0,
|
|
deviceLabel: 'iPhone de teste',
|
|
},
|
|
overrideAccess: true,
|
|
})
|
|
|
|
/**
|
|
* A passkey is a login factor, so its row is the owner's business only — not
|
|
* the coordinator's, not another advisor's. Payload's default access is "any
|
|
* authenticated user", and a `campaign-token` JWT reaches
|
|
* `/api/campaignWebAuthnCredential` directly, so every deny here has to be
|
|
* explicit (roadmap B40).
|
|
*/
|
|
describe('campaignWebAuthnCredential access', () => {
|
|
beforeAll(async () => {
|
|
payload = await getPayload({ config: await config })
|
|
})
|
|
|
|
it('denies anonymous reads', async () => {
|
|
await expect(
|
|
payload.find({ collection: 'campaignWebAuthnCredential', overrideAccess: false }),
|
|
).rejects.toThrow(/permissão|not allowed/i)
|
|
})
|
|
|
|
it('shows an owner only their own passkeys', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const [owner, other] = await Promise.all([
|
|
fixtures.createCampaignUser('leader'),
|
|
fixtures.createCampaignUser('advisor'),
|
|
])
|
|
const ownCredential = await enrollPasskey(owner.id, `own-${owner.id}`)
|
|
await enrollPasskey(other.id, `other-${other.id}`)
|
|
|
|
const visible = await payload.find({
|
|
collection: 'campaignWebAuthnCredential',
|
|
depth: 0,
|
|
user: owner,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
expect(visible.docs.map((doc) => doc.id)).toEqual([ownCredential.id])
|
|
})
|
|
|
|
it('denies the coordinator reading or deleting another account passkey', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const [owner, coordinator] = await Promise.all([
|
|
fixtures.createCampaignUser('advisor'),
|
|
fixtures.createCampaignUser('coordinator'),
|
|
])
|
|
const credential = await enrollPasskey(owner.id, `owner-${owner.id}`)
|
|
|
|
const visible = await payload.find({
|
|
collection: 'campaignWebAuthnCredential',
|
|
depth: 0,
|
|
user: coordinator,
|
|
overrideAccess: false,
|
|
})
|
|
expect(visible.docs).toHaveLength(0)
|
|
|
|
await expect(
|
|
payload.delete({
|
|
collection: 'campaignWebAuthnCredential',
|
|
id: credential.id,
|
|
user: coordinator,
|
|
overrideAccess: false,
|
|
}),
|
|
).rejects.toThrow(/permissão|not allowed|not found/i)
|
|
|
|
await expect(
|
|
payload.findByID({
|
|
collection: 'campaignWebAuthnCredential',
|
|
id: credential.id,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
}),
|
|
).resolves.toMatchObject({ id: credential.id })
|
|
})
|
|
|
|
it('lets the owner delete their own passkey', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const owner = await fixtures.createCampaignUser('leader')
|
|
const credential = await enrollPasskey(owner.id, `revoke-${owner.id}`)
|
|
|
|
await payload.delete({
|
|
collection: 'campaignWebAuthnCredential',
|
|
id: credential.id,
|
|
user: owner,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
await expect(
|
|
payload.count({
|
|
collection: 'campaignWebAuthnCredential',
|
|
where: { user: { equals: owner.id } },
|
|
overrideAccess: true,
|
|
}),
|
|
).resolves.toMatchObject({ totalDocs: 0 })
|
|
})
|
|
|
|
it('refuses a client-shaped enrollment even by the owner', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const owner = await fixtures.createCampaignUser('leader')
|
|
|
|
await expect(
|
|
payload.create({
|
|
collection: 'campaignWebAuthnCredential',
|
|
data: {
|
|
user: owner.id,
|
|
credentialId: `forged-${owner.id}`,
|
|
publicKey: 'YXR0YWNrZXI',
|
|
counter: 0,
|
|
deviceLabel: 'Aparelho do atacante',
|
|
},
|
|
user: owner,
|
|
overrideAccess: false,
|
|
}),
|
|
).rejects.toThrow(/permissão|not allowed/i)
|
|
})
|
|
|
|
it('revokes the passkeys of a deleted account', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const doomed = await fixtures.createCampaignUser('advisor')
|
|
await enrollPasskey(doomed.id, `doomed-${doomed.id}`)
|
|
|
|
await payload.delete({
|
|
collection: 'campaignUser',
|
|
id: doomed.id,
|
|
user: coordinator,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
await expect(
|
|
payload.count({
|
|
collection: 'campaignWebAuthnCredential',
|
|
where: { user: { equals: doomed.id } },
|
|
overrideAccess: true,
|
|
}),
|
|
).resolves.toMatchObject({ totalDocs: 0 })
|
|
})
|
|
})
|