Skip to content

fix(ad-hoc): reuse Checkout 3DS transaction for challenge - #547

Merged
andrii-vysotskyi-cko merged 6 commits into
masterfrom
fix/checkout-3ds-reuse-transaction
Nov 5, 2026
Merged

andrii-vysotskyi-cko merged 6 commits into
masterfrom
fix/checkout-3ds-reuse-transaction

Conversation

@andrii-vysotskyi-cko

@andrii-vysotskyi-cko andrii-vysotskyi-cko commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

POCheckout3DSService.performChallenge called createTransaction() a second time instead of reusing the transaction that produced the authentication request. Checkout's createTransaction() closes the current transaction and creates a new one with a fresh SDK transaction ID and ephemeral key pair. The challenge therefore ran on a transaction that didn't match the AReq, so the ACS shouldn't be able to decrypt it. The regression came from the async rework in #375. Before that, one transaction was kept for both steps.

  • The service now keeps the transaction created during authentication in a TransactionContext and runs the challenge on it.
  • Its state handling now follows PONetcetera3DS2Service: idle, authenticating, ready, challenging. There's no cleaning state because Checkout's teardown is synchronous, so nothing could observe it. Overlapping calls and re-authentication without clean() are rejected. A challenge whose threeDSServerTransID doesn't match the authentication is rejected. clean() during an in-flight operation is ignored with a warning.
  • Teardown closes the transaction and then calls cleanUp(), as Netcetera does and as this service did before feat(POM-434): update Checkout3DS SDK #375. A failed authentication now tears down the SDK straight away.
  • The public API and delegate callbacks are unchanged.

Tests

  • Ported the PONetcetera3DS2Service state tests to Checkout. They run against the real Checkout SDK and use only the public API. Request fixtures are decoded from JSON because the request types have no public initializers.
  • Re-added the ProcessOutCheckout3DS scheme to Scripts/Tests/Run.sh, so these tests run in CI. It had been removed in feat(POM-533): card scanner denied permission alert #502.
  • The transaction reuse itself isn't covered by an automated test. The public API doesn't show which transaction a challenge runs on, so it needs a real 3DS challenge to verify.
  • Disabled type_body_length for PONetcetera3DS2Service, which is now one line over the limit.

Checkout's createTransaction() closes the current transaction and creates
a new one with fresh SDK transaction ID and ephemeral keys, so performing
the challenge on a second transaction did not match the authentication
request.

* Retain transaction created during authentication and reuse it for challenge.
* Track service state explicitly to match PONetcetera3DS2Service.
* Release SDK resources when authentication fails.
* Close transaction on clean().
* Add internal initializer to inject Checkout 3DS service provider.
* Port Netcetera 3DS service tests to Checkout 3DS service.
* Verify that challenge reuses authentication transaction.
* Rely on Checkout cleanUp() to close current transaction.
* Disable type_body_length for PONetcetera3DS2Service.
* Remove internal initializer and service provider seam.
* Remove mocked transaction reuse tests.
* Decode request fixtures instead of using internal initializers.
* Close transaction before cleaning up service instead of relying on cleanUp().
* Remove unreachable cleaning state since teardown is synchronous.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The tests leave in-flight authentication rejection untested and allow unrelated validation errors to satisfy the transaction-ID mismatch case.

2 open issues
What changed in this PR

Fixes the Checkout 3DS regression introduced by the async rework by reusing the authentication transaction for the challenge, preserving its SDK transaction ID and keys.

Changes:

  • Add explicit lifecycle states, reject overlapping operations and mismatched transaction IDs, and tear down resources after failures or cleanup.
  • Replace the placeholder test with public-API integration tests and restore Checkout 3DS testing in CI.
  • Split Netcetera’s logger declaration and add a type-length lint suppression without changing runtime behavior.
File Description
Tests/​ProcessOutCheckout3DSTests/​Sources/​Integration/​Service/​Checkout3DSServiceTests.swift Add authentication, state, cleanup, and challenge-rejection tests.
Tests/​ProcessOutCheckout3DSTests/​Sources/​Checkout3DSServiceTests.swift Remove the placeholder test suite.
Sources/​ProcessOutNetcetera3DS/​Sources/​Service/​PONetcetera3DS2Service.swift Split property declarations and suppress the type-length lint rule.
Sources/​ProcessOutCheckout3DS/​Sources/​Service/​POCheckout3DSService.swift Retain and reuse the transaction, enforce lifecycle checks, and centralize teardown.
Scripts/​Tests/​Run.sh Restore the Checkout 3DS scheme to CI test runs.

Review effort: Max

* Test concurrent authentication while the first call is in flight.
* Rename not-cleaned transaction test to match the state it covers.
* Match state rejection failures by message instead of accepting any error.
@andrii-vysotskyi-cko
andrii-vysotskyi-cko merged commit ddb24a5 into master Nov 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants