Source code for fido2.payment

# Copyright (c) 2025 Yubico AB
# All rights reserved.
#
#   Redistribution and use in source and binary forms, with or
#   without modification, are permitted provided that the following
#   conditions are met:
#
#    1. Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#    2. Redistributions in binary form must reproduce the above
#       copyright notice, this list of conditions and the following
#       disclaimer in the documentation and/or other materials provided
#       with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from dataclasses import dataclass

from .client import DefaultClientDataCollector
from .ctap2.extensions import (
    AuthenticationExtensionsPaymentInputs,
    PaymentCredentialInstrument,
    PaymentCurrencyAmount,
)
from .utils import _JsonDataObject
from .webauthn import (
    AuthenticatorAttachment,
    CollectedClientData,
    PublicKeyCredentialCreationOptions,
    PublicKeyCredentialRequestOptions,
    ResidentKeyRequirement,
    UserVerificationRequirement,
)

"""
Implements client and server functionality for the WebAuthn "payment" extension.

https://www.w3.org/TR/secure-payment-confirmation/#sctn-payment-extension-registration
"""


[docs] @dataclass(eq=False, frozen=True, kw_only=True) class CollectedClientAdditionalPaymentData(_JsonDataObject): rp_id: str top_origin: str payee_name: str | None = None payee_origin: str | None = None total: PaymentCurrencyAmount instrument: PaymentCredentialInstrument
[docs] @dataclass(init=False, frozen=True, kw_only=True) class CollectedClientPaymentData(CollectedClientData): payment: CollectedClientAdditionalPaymentData def __init__(self, serialized: bytes): super().__init__(serialized) payment = CollectedClientAdditionalPaymentData.from_dict(self._data["payment"]) object.__setattr__(self, "payment", payment)
[docs] @classmethod def create( cls, type: str, challenge: bytes | str, origin: str, cross_origin: bool = False, **kwargs, ) -> CollectedClientData: return super().create( type=type, challenge=challenge, origin=origin, cross_origin=cross_origin, payment=dict(kwargs.pop("payment")), **kwargs, )
[docs] class PaymentClientDataCollector(DefaultClientDataCollector): """ClientDataCollector for the WebAuthn "payment" extension. This class can be used together with the CTAP2 "thirdPartyPayment" extension to enable third-party payment confirmation. It collects the necessary client data and validates the options provided by the client. """
[docs] def collect_client_data(self, options): # Get the effective RP ID from the request options, falling back to the origin rp_id = self.get_rp_id(options, self._origin) inputs = options.extensions or {} data = AuthenticationExtensionsPaymentInputs.from_dict(inputs.get("payment")) if data and data.is_payment: if isinstance(options, PublicKeyCredentialCreationOptions): sel = options.authenticator_selection if ( not sel or sel.authenticator_attachment not in ( AuthenticatorAttachment.PLATFORM, # This is against the spec, but we need cross-platform AuthenticatorAttachment.CROSS_PLATFORM, ) or sel.resident_key not in ( ResidentKeyRequirement.REQUIRED, ResidentKeyRequirement.PREFERRED, ) or sel.user_verification != UserVerificationRequirement.REQUIRED ): raise ValueError("Invalid options for payment extension") elif isinstance(options, PublicKeyCredentialRequestOptions): # NOTE: We skip RP ID validation, as per the spec return ( CollectedClientPaymentData.create( type="payment.get", origin=self._origin, challenge=options.challenge, payment=CollectedClientAdditionalPaymentData( rp_id=data.rp_id, top_origin=data.top_origin, payee_name=data.payee_name, payee_origin=data.payee_origin, total=data.total, instrument=data.instrument, ), ), rp_id, ) # Validate that the RP ID is valid for the given origin self.verify_rp_id(rp_id, self._origin) return super().collect_client_data(options)