Merge pull request #1344 from Tria-plc/alpha

fix: ( fayda ) block one Fayda identity from verifying multiple passe…
This commit is contained in:
Abubeker Yasin
2026-08-19 09:26:34 +03:00
committed by GitHub
3 changed files with 58 additions and 7 deletions

View File

@@ -57,6 +57,14 @@ export class CompleteVerificationResultDto {
agentId?: string;
};
@ApiPropertyOptional({
description:
'eSignet subject identifier for the verified individual (VERIFY flow). A PSUT — ' +
'pairwise and stable per client_id, never the FIN. The booking flow compares it across ' +
'passengers so one Fayda identity cannot verify more than one passenger on a booking.',
})
faydaSub?: string;
@ApiPropertyOptional({ description: 'Verified full name from Fayda (VERIFY flow).' })
fullName?: string;

View File

@@ -70,11 +70,19 @@ export interface FaydaUserSummary {
/**
* Result of completing a verification. `verified` is always true on success.
* LOGIN additionally returns a JWT + user; VERIFY returns the verified identity
* attributes (name, email, phone, dob, gender) for the caller to consume.
* attributes (name, email, phone, dob, gender, faydaSub) for the caller to consume.
*/
export interface CompleteVerificationResult {
purpose: VerifaydaPurpose;
verified: boolean;
/**
* eSignet subject identifier for the verified individual. This is a PSUT —
* pairwise and stable per `client_id`, never the FIN — so it is safe to hand
* to the browser, and it is the same value `/passengers/me` already returns.
* The booking flow uses it to stop one Fayda identity from verifying more
* than one passenger on the same booking.
*/
faydaSub?: string;
token?: string;
refreshToken?: string;
requiresPassword?: boolean;
@@ -280,6 +288,7 @@ export class VerifaydaService {
result = {
purpose: 'VERIFY',
verified: true,
faydaSub: normalized.sub,
fullName: normalized.fullName,
email: normalized.email,
phoneNumber: normalized.phoneNumber,
@@ -357,6 +366,13 @@ export class VerifaydaService {
code_challenge_method: 'S256',
acr_values: this.faydaConfig.acrValues,
claims_locales: this.faydaConfig.claimsLocales,
// Force a fresh authentication instead of silently reusing the eSignet
// SSO session. A booking can carry several passengers, each of whom must
// verify with their OWN Fayda; without this, the second and third
// "Verify with Fayda" clicks round-trip in a couple of seconds and hand
// back the first passenger's identity, which the booking flow then has to
// reject with no way for the user to authenticate as the right person.
prompt: 'login',
});
// Every claim is marked essential so eSignet shows them locked/pre-checked

View File

@@ -761,6 +761,10 @@ function PassengersForm() {
passportExpiryDate: stored.passportExpiryDate || '',
passportIssuingAuthority: stored.passportIssuingAuthority || '',
faydaVerified: stored.faydaVerified || false,
// Restore the identity that verified this passenger, so returning here from a
// later step (e.g. Back from /booking/seats) doesn't silently reopen the slot to
// an already-used Fayda.
faydaSub: stored.faydaSub || undefined,
formExpanded: true,
};
}
@@ -873,13 +877,22 @@ function PassengersForm() {
if (d?.verified) {
const faydaSub: string | undefined = d.sub || d.faydaSub || d.fin;
// A single Fayda identity can't be reused across two different passengers.
const usedByOther = faydaSub && passengers.some(
(p, i) => i !== targetIndex && (p as any).faydaSub === faydaSub,
);
// A single Fayda identity can't be reused across two different passengers. Read the
// live form rather than the `passengers` captured when this effect was created — the
// snapshot restore repopulates the array as the form initializes.
const currentPassengers = watch('passengers') || [];
const conflictIndex = faydaSub
? currentPassengers.findIndex(
(p, i) => i !== targetIndex && (p as any)?.faydaSub === faydaSub,
)
: -1;
if (usedByOther) {
setFaydaErrors((prev) => ({ ...prev, [targetIndex]: 'This Fayda identity is already linked to another passenger on this booking.' }));
if (conflictIndex >= 0) {
const conflictName = currentPassengers[conflictIndex]?.name?.trim();
setFaydaErrors((prev) => ({
...prev,
[targetIndex]: `This Fayda ID has already been used to verify Passenger ${conflictIndex + 1}${conflictName ? ` (${conflictName})` : ''}. Each traveller must verify with their own Fayda.`,
}));
setVerificationStatus((prev) => ({ ...prev, [targetIndex]: 'error' }));
} else {
// Convert "1980/12/01" → "1980-12-01"
@@ -989,6 +1002,13 @@ function PassengersForm() {
const emailVal = pick(passengerData.email, user.email);
if (emailVal) setValue('passengers.0.email', emailVal);
// A logged-in, already-verified user occupies slot 0 without going through a fresh
// Fayda round trip, so the callback never records their sub on the form. Seed it from
// the account here, otherwise the duplicate-identity check has nothing to compare
// against and the account holder can re-use their own Fayda on passenger 2.
const accountFaydaSub = pick(passengerData.faydaSub, (user as any).faydaSub);
if (accountFaydaSub) setValue('passengers.0.faydaSub', accountFaydaSub);
if (mustVerifyFayda) {
// Force the Fayda gate: leave name/DOB/gender empty and keep the form collapsed so the
// "Verify with Fayda" screen is shown instead of an editable, pre-filled form.
@@ -1103,6 +1123,13 @@ function PassengersForm() {
gender: p.gender,
nationality: p.nationality,
nationalId: p.nationalId,
// Carry the verified Fayda identity into the booking store so the duplicate-identity
// check still has it if the user comes back to this page from /booking/seats. Without
// it the restore path below rebuilds each passenger without a sub, and one Fayda could
// then re-verify every passenger. Stripped server-side by the global ValidationPipe
// (whitelist: true), so sending it to /passengers/save-details is a no-op there.
faydaVerified: p.faydaVerified,
faydaSub: p.faydaSub,
passportNumber: p.passportNumber,
passportCountry: p.passportCountry,
passportIssueDate: p.passportIssueDate,