Merchants can use Secure Payment Confirmation (SPC) as part of a strong customer authentication (SCA) process for a given credit card or bank account. WebAuthn performs the authentication (frequently through biometrics). WebAuthn must be registered in advance, which you can learn about in Register a Secure Payment Confirmation.
How a typical implementation works
The most common use for SPC is when a customer makes a purchase on a merchant's site, and the credit card issuer or bank requires payer authentication.
Let's walk through the authentication flow:
- A customer provides their payment credentials (such as credit card information) to the merchant.
- The merchant asks the payment credential's corresponding issuer or bank
(relying party or RP) if the payer needs a separate authentication. This
exchange might happen, for example, with
EMV® 3-D Secure.
- If the RP wishes the merchant to use SPC, and if the user has previously registered, the RP responds with a list of credential IDs registered by the payer and a challenge.
- If an authentication is not needed, the merchant can continue to complete the transaction.
- If an authentication is needed, the merchant determines whether the browser supports SPC.
- If the browser does not support SPC, proceed with the existing authentication flow.
- The merchant invokes SPC. The browser displays a confirmation dialog.
- If there are no credential IDs passed from the RP, fall back to the existing authentication flow. After a successful authentication, consider using SPC registration to streamline future authentications.
- The user confirms and authenticates the amount and the destination of the payment by unlocking the device.
- The merchant receives a credential from the authentication.
- The RP receives the credential from the merchant and verifies its authenticity.
- The RP sends the verification results to the merchant.
- The merchant shows the user a message to indicate if the payment was successful or unsuccessful.
Feature detection
To detect whether SPC is supported on the browser, you can send a fake call to
canMakePayment()
.
Copy and paste the following code to feature detect SPC on a merchant's website.
const isSecurePaymentConfirmationSupported = async () => {
if (!'PaymentRequest' in window) {
return [false, 'Payment Request API is not supported'];
}
try {
// The data below is the minimum required to create the request and
// check if a payment can be made.
const supportedInstruments = [
{
supportedMethods: "secure-payment-confirmation",
data: {
// RP's hostname as its ID
rpId: 'rp.example',
// A dummy credential ID
credentialIds: [new Uint8Array(1)],
// A dummy challenge
challenge: new Uint8Array(1),
instrument: {
// Non-empty display name string
displayName: ' ',
// Transparent-black pixel.
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==',
},
// A dummy merchant origin
payeeOrigin: 'https://non-existent.example',
}
}
];
const details = {
// Dummy shopping details
total: {label: 'Total', amount: {currency: 'USD', value: '0'}},
};
const request = new PaymentRequest(supportedInstruments, details);
const canMakePayment = await request.canMakePayment();
return [canMakePayment, canMakePayment ? '' : 'SPC is not available'];
} catch (error) {
console.error(error);
return [false, error.message];
}
};
isSecurePaymentConfirmationSupported().then(result => {
const [isSecurePaymentConfirmationSupported, reason] = result;
if (isSecurePaymentConfirmationSupported) {
// Display the payment button that invokes SPC.
} else {
// Fallback to the legacy authentication method.
}
});
Authenticate the user
To authenticate the user, invoke the PaymentRequest.show()
method with
secure-payment-confirmation
and WebAuthn parameters:
PublicKeyCredentialRequestOptions
- Other payment specific parameters on the merchant's platform.
Here's the parameters you should provide to the payment method's data
property, SecurePaymentConfirmationRequest
.
Check out this example code:
// After confirming SPC is available on this browser via a feature detection,
// fetch the request options cross-origin from the RP server.
const options = fetchFromServer('https://rp.example/spc-auth-request');
const { credentialIds, challenge } = options;
const request = new PaymentRequest([{
// Specify `secure-payment-confirmation` as payment method.
supportedMethods: "secure-payment-confirmation",
data: {
// The RP ID
rpId: 'rp.example',
// List of credential IDs obtained from the RP server.
credentialIds,
// The challenge is also obtained from the RP server.
challenge,
// A display name and an icon that represent the payment instrument.
instrument: {
displayName: "Fancy Card ****1234",
icon: "https://rp.example/card-art.png",
iconMustBeShown: false
},
// The origin of the payee (merchant)
payeeOrigin: "https://merchant.example",
// The number of milliseconds to timeout.
timeout: 360000, // 6 minutes
}
}], {
// Payment details.
total: {
label: "Total",
amount: {
currency: "USD",
value: "5.00",
},
},
});
try {
const response = await request.show();
// response.details is a PublicKeyCredential, with a clientDataJSON that
// contains the transaction data for verification by the issuing bank.
// Make sure to serialize the binary part of the credential before
// transferring to the server.
const result = fetchFromServer('https://rp.example/spc-auth-response', response.details);
if (result.success) {
await response.complete('success');
} else {
await response.complete('fail');
}
} catch (err) {
// SPC cannot be used; merchant should fallback to traditional flows
console.error(err);
}
The .show()
function results in a
PaymentResponse
object except the details
contains a public key credential with a
clientDataJSON
that contains the transaction data
(payment
)
for verification by the RP.
The resulting credential must be transferred cross-origin to the RP and verified.
How the RP verifies the transaction
Verifying the transaction data at the RP server is the most important step in the payment process.
To verify the transaction data, the RP can follow WebAuthn's authentication assertion verification process.
In addition, they need to
verify the payment
.
An example payload of the clientDataJSON
:
{
"type":"payment.get",
"challenge":"SAxYy64IvwWpoqpr8JV1CVLHDNLKXlxbtPv4Xg3cnoc",
"origin":"https://spc-merchant.glitch.me",
"crossOrigin":false,
"payment":{
"rp":"spc-rp.glitch.me",
"topOrigin":"https://spc-merchant.glitch.me",
"payeeOrigin":"https://spc-merchant.glitch.me",
"total":{
"value":"15.00",
"currency":"USD"
},
"instrument":{
"icon":"https://cdn.glitch.me/94838ffe-241b-4a67-a9e0-290bfe34c351%2Fbank.png?v=1639111444422",
"displayName":"Fancy Card 825809751248"
}
}
}
- The
rp
matches the RP's origin. - The
topOrigin
matches the top-level origin that the RP expects (the merchant's origin in the example above). - The
payeeOrigin
matches the origin of the payee that should have been displayed to the user. - The
total
matches the transaction amount that should have been displayed to the user. - The
instrument
matches the payment instrument details that should have been displayed to the user.
const clientData = base64url.decode(response.clientDataJSON);
const clientDataJSON = JSON.parse(clientData);
if (!clientDataJSON.payment) {
throw 'The credential does not contain payment payload.';
}
const payment = clientDataJSON.payment;
if (payment.rp !== expectedRPID ||
payment.topOrigin !== expectedOrigin ||
payment.payeeOrigin !== expectedOrigin ||
payment.total.value !== '15.00' ||
payment.total.currency !== 'USD') {
throw 'Malformed payment information.';
}
After all the verification criteria have been passed, the RP can tell the merchant that the transaction is successful.
Next steps
- Read the overview of Secure Payment Confirmation
- Learn about registration with Secure Payment Confirmation