Skip to content

Threat Model — Account Merge

Threat matrix

#ThreatImpactMitigation
T1Confused Deputy (T3) — absorb another account with a stolen sessionX's data mixed into A's data viewThe caller is fixed as the survivor and the input email fixes the absorption direction. dual PoP (session + verified email OTP)
T2Replay — resend a past request/webhookduplicate mergeno external surface for T1/T2. OTP 5 min + single-use. Webhook X-Logi-Timestamp ±5 min + event_id dedupe. idempotency_key
T3Race / concurrent mergeambiguous canonical[survivor, merged] ORDER BY id ASC FOR UPDATE. identity_links_no_cycle_trg. 2nd tx → :already_processed
T4Stale device_secret takeover (T1)impersonated absorptionT1 requires verifying each SSO's fresh provider id_token. A device_secret alone cannot pass SSO
T5Cross-provider email squatting (T2)absorption link on the victim's signupT2 requires a verified email on both providers. Impossible unless the attacker controls the mailbox
T6Token takeover after mergereuse of an absorbed tokenwithin the merge tx, immediately revoke 9 categories of credentials + jti blocklist. RP sessions are cleaned up via the user.merged webhook
T7Cycle / chain corruption (A→B+B→C)ambiguous canonicalDB BEFORE INSERT trigger rejects the cycle + app MergeService.guard_cycle! + resolver MAX_DEPTH=10
T8OAuth Code Injection / Mix-Up (RFC 6819 §4.4.1.1, RFC 8252 §8.1)code interceptionPKCE enforced + downgrade defense (reject public client secret) + redirect URI exact-match + issuer validation + state echo
T9Refresh Token Reuse / Thefttoken theftRT rotation + reuse detection → revoke_chain! revokes the entire subsequent chain. HMAC-SHA256 digest stored. User-row lock
T10RP-to-RP user correlationcross-RP profilingPairwise sub (OIDC Core §8.1): SHA256(sector_id ‖ user_id ‖ app.pairwise_salt)[0..32]. subject_types_supported:["pairwise"]
T11Audit Log Tamperingforged footprintsAuthenticationAuditLog SHA256 hash chain (previous_hash→current_hash) + verify_chain! + GENESIS_HASH. Limit: a DB superuser could recompute the whole chain → exporting to external immutable storage is recommended

STRIDE mapping

STRIDEThreats
SpoofingT1, T4, T5, T8
TamperingT7, T11
RepudiationT11
Information DisclosureT10
Denial of ServiceT3
Elevation of PrivilegeT1, T6, T9

Security controls

ControlThreats addressedImplementation
PKCE enforced (reject public client downgrade)T8Oauth::AuthorizationsController, Oauth::ClientAuthentication L30-44
Redirect URI exact-matchT8Oauth::RedirectUriValidator
Issuer validationT8discovery issuerOIDC_ISSUER env
RT rotation + reuse chain revokeT9Oauth::TokensController#rotate_refresh_token L98-101
RT digest (HMAC-SHA256)T9OauthAccessToken#refresh_token_digest
Pairwise subT10Oauth::SubjectIdentifier
Hash chain audit logT11Authentication::AuditLogger
Merge canonical lock + cycle triggerT3, T7MergeService, identity_links_no_cycle_trg, guard_cycle!
9-category credential revoke on mergeT6merge transaction
Webhook HMAC + timestamp + event_idT2RP webhook spec
OTP 5 min + single-use (consumed_at)T2T3 merge OTP
Fresh provider id_token verificationT1, T4SSO callback
Verified email on both providersT5T2 trigger condition

Metrics

  • logi_merge_attempts_total{trigger=}
  • logi_merge_failures_total{reason=conflict|cycle|contention|user_in_purge|both_pop_required}
  • logi_t3_merge_otp_dispatches_total{outcome=sent|invalid_target|rate_limited|unverified_email}
  • logi_webhook_delivery_failures_total{rp=, retry_count=}

A spike in cycle / both_pop_required → escalate to an incident.

Identity가 제품의 신뢰를 만듭니다.