Files
app/test/test.passport.node.ts
jkunz e9eb9b4172 add office-aware passport policies and alert lifecycle
Enforce geofenced location evidence for passport challenges and extend admin alerting so mobile devices can review, dismiss, and act on real org and security events.
2026-04-20 13:21:28 +00:00

453 lines
15 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/plugins.js';
import { PassportChallenge } from '../ts/reception/classes.passportchallenge.js';
import { PassportDevice } from '../ts/reception/classes.passportdevice.js';
import { PassportManager } from '../ts/reception/classes.passportmanager.js';
import { PassportNonce } from '../ts/reception/classes.passportnonce.js';
const getNestedValue = (targetArg: any, pathArg: string) => {
return pathArg.split('.').reduce((currentArg, keyArg) => currentArg?.[keyArg], targetArg);
};
const matchesQuery = (targetArg: any, queryArg: Record<string, any>) => {
return Object.entries(queryArg).every(([keyArg, valueArg]) => {
return getNestedValue(targetArg, keyArg) === valueArg;
});
};
const createTestPassportManager = () => {
const passportDevices = new Map<string, PassportDevice>();
const passportChallenges = new Map<string, PassportChallenge>();
const passportNonces = new Map<string, PassportNonce>();
const activityLogCalls: Array<{
userId: string;
action: string;
description: string;
}> = [];
const deliveredHintIds: string[] = [];
const manager = new PassportManager({
db: { smartdataDb: {} },
typedrouter: { addTypedRouter: () => undefined },
options: { baseUrl: 'https://idp.global' },
jwtManager: { verifyJWTAndGetData: async () => null },
activityLogManager: {
logActivity: async (userIdArg: string, actionArg: string, descriptionArg: string) => {
activityLogCalls.push({
userId: userIdArg,
action: actionArg,
description: descriptionArg,
});
},
},
passportPushManager: {
deliverChallengeHint: async (_passportDeviceArg: PassportDevice, passportChallengeArg: PassportChallenge) => {
deliveredHintIds.push(passportChallengeArg.data.notification!.hintId);
passportChallengeArg.data.notification = {
...passportChallengeArg.data.notification!,
status: 'sent',
attemptCount: passportChallengeArg.data.notification!.attemptCount + 1,
deliveredAt: Date.now(),
lastError: null,
};
await passportChallengeArg.save();
return true;
},
},
} as any);
const originalPassportDeviceSave = PassportDevice.prototype.save;
const originalPassportDeviceDelete = PassportDevice.prototype.delete;
const originalPassportChallengeSave = PassportChallenge.prototype.save;
const originalPassportChallengeDelete = PassportChallenge.prototype.delete;
const originalPassportNonceSave = PassportNonce.prototype.save;
const originalPassportNonceDelete = PassportNonce.prototype.delete;
(PassportDevice.prototype as PassportDevice & { save: () => Promise<void> }).save = async function () {
passportDevices.set(this.id, this);
};
(PassportDevice.prototype as PassportDevice & { delete: () => Promise<void> }).delete = async function () {
passportDevices.delete(this.id);
};
(PassportChallenge.prototype as PassportChallenge & { save: () => Promise<void> }).save = async function () {
passportChallenges.set(this.id, this);
};
(PassportChallenge.prototype as PassportChallenge & { delete: () => Promise<void> }).delete = async function () {
passportChallenges.delete(this.id);
};
(PassportNonce.prototype as PassportNonce & { save: () => Promise<void> }).save = async function () {
passportNonces.set(this.id, this);
};
(PassportNonce.prototype as PassportNonce & { delete: () => Promise<void> }).delete = async function () {
passportNonces.delete(this.id);
};
(manager as any).CPassportDevice = {
getInstance: async (queryArg: Record<string, any>) => {
return Array.from(passportDevices.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
},
getInstances: async (queryArg: Record<string, any>) => {
return Array.from(passportDevices.values()).filter((docArg) => matchesQuery(docArg, queryArg));
},
};
(manager as any).CPassportChallenge = {
getInstance: async (queryArg: Record<string, any>) => {
return (
Array.from(passportChallenges.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null
);
},
getInstances: async (queryArg: Record<string, any>) => {
return Array.from(passportChallenges.values()).filter((docArg) => matchesQuery(docArg, queryArg));
},
};
(manager as any).CPassportNonce = {
getInstance: async (queryArg: Record<string, any>) => {
return Array.from(passportNonces.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
},
getInstances: async (queryArg: Record<string, any>) => {
return Array.from(passportNonces.values()).filter((docArg) => matchesQuery(docArg, queryArg));
},
};
return {
manager,
passportDevices,
passportChallenges,
passportNonces,
activityLogCalls,
deliveredHintIds,
restore: () => {
PassportDevice.prototype.save = originalPassportDeviceSave;
PassportDevice.prototype.delete = originalPassportDeviceDelete;
PassportChallenge.prototype.save = originalPassportChallengeSave;
PassportChallenge.prototype.delete = originalPassportChallengeDelete;
PassportNonce.prototype.save = originalPassportNonceSave;
PassportNonce.prototype.delete = originalPassportNonceDelete;
},
};
};
const createRawPassportSigner = async () => {
const subtle = plugins.crypto.webcrypto.subtle;
const keyPair = await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
'sign',
'verify',
]);
const publicKeyRaw = Buffer.from(await subtle.exportKey('raw', keyPair.publicKey)).toString('base64');
return {
publicKeyX963Base64: publicKeyRaw,
sign: async (payloadArg: string) => {
const signature = await subtle.sign(
{ name: 'ECDSA', hash: 'SHA-256' },
keyPair.privateKey,
Buffer.from(payloadArg, 'utf8')
);
return Buffer.from(signature).toString('base64');
},
};
};
const createDerPassportSigner = () => {
const keyPair = plugins.crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const publicJwk = keyPair.publicKey.export({ format: 'jwk' }) as JsonWebKey;
const publicKeyX963Base64 = Buffer.concat([
Buffer.from([4]),
Buffer.from(publicJwk.x!, 'base64url'),
Buffer.from(publicJwk.y!, 'base64url'),
]).toString('base64');
return {
publicKeyX963Base64,
sign: (payloadArg: string) => {
return plugins.crypto.sign('sha256', Buffer.from(payloadArg, 'utf8'), keyPair.privateKey).toString('base64');
},
};
};
const createSignedDeviceRequest = async (
managerArg: PassportManager,
signerArg: { sign: (payloadArg: string) => Promise<string> | string },
requestArg: {
deviceId: string;
action: string;
signedFields?: string[];
}
) => {
const baseRequest = {
deviceId: requestArg.deviceId,
timestamp: Date.now(),
nonce: plugins.crypto.randomUUID(),
};
const payload = (managerArg as any).buildDeviceRequestSigningPayload(
baseRequest,
requestArg.action,
requestArg.signedFields || []
);
return {
...baseRequest,
signatureBase64: await signerArg.sign(payload),
signatureFormat: 'raw' as const,
};
};
tap.test('enrolls a passport device from a pairing challenge', async () => {
const { manager, passportDevices, passportChallenges, activityLogCalls, restore } =
createTestPassportManager();
try {
const enrollment = await manager.createEnrollmentChallengeForUser('user-1', {
deviceLabel: 'Phil iPhone',
platform: 'ios',
capabilities: {
gps: true,
nfc: true,
push: true,
},
});
const signer = await createRawPassportSigner();
const signatureBase64 = await signer.sign(enrollment.signingPayload);
const passportDevice = await manager.completeEnrollment({
pairingToken: enrollment.pairingToken,
deviceLabel: 'Phil iPhone',
platform: 'ios',
publicKeyX963Base64: signer.publicKeyX963Base64,
signatureBase64,
signatureFormat: 'raw',
capabilities: {
gps: true,
nfc: true,
push: true,
},
appVersion: '1.0.0',
});
expect(passportDevice.data.userId).toEqual('user-1');
expect(passportDevice.data.label).toEqual('Phil iPhone');
expect(passportDevices.size).toEqual(1);
expect(passportChallenges.size).toEqual(1);
expect(Array.from(passportChallenges.values())[0].data.status).toEqual('approved');
expect(activityLogCalls[0].action).toEqual('passport_device_enrolled');
} finally {
restore();
}
});
tap.test('creates and approves a passport challenge with DER signatures and evidence', async () => {
const { manager, activityLogCalls, deliveredHintIds, restore } = createTestPassportManager();
try {
const enrollment = await manager.createEnrollmentChallengeForUser('user-2', {
deviceLabel: 'Office iPhone',
platform: 'ios',
capabilities: {
gps: true,
nfc: true,
push: true,
},
});
const signer = createDerPassportSigner();
const passportDevice = await manager.completeEnrollment({
pairingToken: enrollment.pairingToken,
deviceLabel: 'Office iPhone',
platform: 'ios',
publicKeyX963Base64: signer.publicKeyX963Base64,
signatureBase64: signer.sign(enrollment.signingPayload),
signatureFormat: 'der',
capabilities: {
gps: true,
nfc: true,
push: true,
},
});
const challengeResult = await manager.createPassportChallengeForUser('user-2', {
type: 'physical_access',
preferredDeviceId: passportDevice.id,
audience: 'hq-door-a',
notificationTitle: 'Office entry request',
requireLocation: true,
requireNfc: true,
locationPolicy: {
mode: 'geofence',
label: 'HQ Berlin',
latitude: 53.0793,
longitude: 8.8017,
radiusMeters: 80,
maxAccuracyMeters: 25,
},
});
expect(deliveredHintIds).toHaveLength(1);
expect(challengeResult.challenge.data.notification?.status).toEqual('sent');
await expect(
manager.approvePassportChallenge({
challengeId: challengeResult.challenge.id,
deviceId: passportDevice.id,
signatureBase64: signer.sign(challengeResult.signingPayload),
signatureFormat: 'der',
location: {
latitude: 53.5,
longitude: 8.1,
accuracyMeters: 12,
capturedAt: Date.now(),
},
nfc: {
readerId: 'door-reader-a',
},
})
).rejects.toThrow();
const approvedChallenge = await manager.approvePassportChallenge({
challengeId: challengeResult.challenge.id,
deviceId: passportDevice.id,
signatureBase64: signer.sign(challengeResult.signingPayload),
signatureFormat: 'der',
location: {
latitude: 53.0793,
longitude: 8.8017,
accuracyMeters: 12,
capturedAt: Date.now(),
},
nfc: {
readerId: 'door-reader-a',
},
});
expect(approvedChallenge.data.status).toEqual('approved');
expect(approvedChallenge.data.evidence?.signatureFormat).toEqual('der');
expect(approvedChallenge.data.evidence?.location?.accuracyMeters).toEqual(12);
expect(approvedChallenge.data.evidence?.locationEvaluation?.matched).toBeTrue();
expect(approvedChallenge.data.evidence?.nfc?.readerId).toEqual('door-reader-a');
expect(activityLogCalls.at(-1)?.action).toEqual('passport_challenge_approved');
} finally {
restore();
}
});
tap.test('registers push tokens and loads pending challenges through signed device requests', async () => {
const { manager, passportNonces, restore } = createTestPassportManager();
try {
const enrollment = await manager.createEnrollmentChallengeForUser('user-3', {
deviceLabel: 'Work iPhone',
platform: 'ios',
capabilities: {
gps: true,
nfc: false,
push: true,
},
});
const signer = await createRawPassportSigner();
const passportDevice = await manager.completeEnrollment({
pairingToken: enrollment.pairingToken,
deviceLabel: 'Work iPhone',
platform: 'ios',
publicKeyX963Base64: signer.publicKeyX963Base64,
signatureBase64: await signer.sign(enrollment.signingPayload),
signatureFormat: 'raw',
capabilities: {
gps: true,
nfc: false,
push: true,
},
});
const pushRequest = await createSignedDeviceRequest(manager, signer, {
deviceId: passportDevice.id,
action: 'registerPassportPushToken',
signedFields: [
'provider=apns',
'token=device-token-1',
'topic=global.idp.swiftapp',
'environment=development',
],
});
const registeredPassportDevice = await (manager as any).authenticatePassportDeviceRequest(
{
...pushRequest,
},
{
action: 'registerPassportPushToken',
signedFields: [
'provider=apns',
'token=device-token-1',
'topic=global.idp.swiftapp',
'environment=development',
],
}
);
registeredPassportDevice.data.pushRegistration = {
provider: 'apns',
token: 'device-token-1',
topic: 'global.idp.swiftapp',
environment: 'development',
registeredAt: Date.now(),
};
await registeredPassportDevice.save();
const challengeResult = await manager.createPassportChallengeForUser('user-3', {
type: 'authentication',
preferredDeviceId: passportDevice.id,
audience: 'office-saas',
notificationTitle: 'Office sign-in verification',
});
const listRequest = await createSignedDeviceRequest(manager, signer, {
deviceId: passportDevice.id,
action: 'listPendingPassportChallenges',
});
const authenticatedDevice = await (manager as any).authenticatePassportDeviceRequest(listRequest, {
action: 'listPendingPassportChallenges',
});
const pendingChallenges = await manager.listPendingChallengesForDevice(authenticatedDevice.id);
expect(pendingChallenges).toHaveLength(1);
expect(pendingChallenges[0].id).toEqual(challengeResult.challenge.id);
const hintId = challengeResult.challenge.data.notification!.hintId;
const getRequest = await createSignedDeviceRequest(manager, signer, {
deviceId: passportDevice.id,
action: 'getPassportChallengeByHint',
signedFields: [`hint_id=${hintId}`],
});
const hintChallenge = await manager.getPassportChallengeByHint(
(
await (manager as any).authenticatePassportDeviceRequest(getRequest, {
action: 'getPassportChallengeByHint',
signedFields: [`hint_id=${hintId}`],
})
).id,
hintId
);
expect(hintChallenge?.id).toEqual(challengeResult.challenge.id);
const seenRequest = await createSignedDeviceRequest(manager, signer, {
deviceId: passportDevice.id,
action: 'markPassportChallengeSeen',
signedFields: [`hint_id=${hintId}`],
});
await (manager as any).authenticatePassportDeviceRequest(seenRequest, {
action: 'markPassportChallengeSeen',
signedFields: [`hint_id=${hintId}`],
});
const seenChallenge = await manager.markPassportChallengeSeen(passportDevice.id, hintId);
expect(seenChallenge.data.notification?.status).toEqual('seen');
expect(passportNonces.size).toEqual(4);
} finally {
restore();
}
});
export default tap.start();