Skip to content

Commit d1dcbd9

Browse files
committed
feat(sso): add idp_link_decision diagnostic for link ceremonies
Emit a PII-safe decision-trace on the explicit link path (link=true) so staging/gcloud logs explain an access-denied without guesswork. Fields are booleans + idpId + reason only (no email/loginName): - intentMapped / intentMatchesSession → Shape 1 (identity already linked elsewhere) - allowLinkAnyEmail / emailVerified / emailOwnerMatchesSession → Shape 2 B2 (email checks) Registered idp_link_decision in the audit-coverage event inventory.
1 parent 44feca6 commit d1dcbd9

2 files changed

Lines changed: 27 additions & 0 deletions

File tree

app/resources/sso/sso-callback.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,30 @@ export async function processIdpCallback(
182182
idpId: intent.information?.idpId,
183183
});
184184
}
185+
186+
// Diagnostic (PII-safe): trace WHY an explicit link ceremony resolved as it did, so staging
187+
// logs explain an access-denied without guesswork. Only on the link path (link=true), which is
188+
// low volume. Surfaces the flag + the shape discriminators — read it as:
189+
// intentMapped=true, intentMatchesSession=false → Shape 1: identity already linked elsewhere
190+
// intentMapped=false, allowLinkAnyEmail=false → Shape 2 B2: emailVerified / owner checks
191+
// allowLinkAnyEmail=true → fresh identity should link (never denied)
192+
// NO email/loginName VALUES are logged (audit PII guard) — only booleans + idpId + reason.
193+
if (link === 'true') {
194+
logAuthEvent('idp_link_decision', decision.kind === 'error' ? 'failure' : 'success', {
195+
requestId,
196+
idpId: intent.information?.idpId,
197+
decision: decision.kind,
198+
reason: decision.kind === 'error' ? decision.reason : undefined,
199+
allowLinkAnyEmail,
200+
intentMapped: intent.userId != null, // false ⇒ fresh identity (Shape 2)
201+
intentMatchesSession: intent.userId != null && intent.userId === sessionUserId,
202+
sessionPresent: sessionUserId != null,
203+
emailVerified: intent.draft?.emailVerified ?? false,
204+
emailOwnerResolved: linkEmailOwnerUserId != null,
205+
emailOwnerMatchesSession:
206+
linkEmailOwnerUserId != null && linkEmailOwnerUserId === sessionUserId,
207+
});
208+
}
185209
} catch (err) {
186210
if (err instanceof ProviderError) {
187211
deps.onAuthEvent?.('idp.signin', 'failure');

cypress/support/audit-coverage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ export const REQUIRED_EVENTS = [
329329
'idp.link.denied',
330330
'idp.link.start',
331331
'idp.unlink',
332+
// Diagnostic decision-trace for the explicit link ceremony (PII-safe booleans; snake_case
333+
// per the P5+ convention). Emitted in sso-callback.ts to explain link access-denied outcomes.
334+
'idp_link_decision',
332335
// --- MFA methods ---
333336
'mfa_method_chosen',
334337
'mfa_totp',

0 commit comments

Comments
 (0)