Strong Customer Authentication for the Wise API in Python

This might be of interest if you are interfacing with the Wise (formerly TransferWise) API from a Python project. Note that you need to have regulatory approval to be able to access 2FA/SCA-protected endpoints - speak to your contact at Wise for more info. But once you get your private key enrolled, this code should work.

import typing
from base64 import b64encode
from datetime import datetime
from enum import StrEnum
from typing import List

import niquests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from niquests import PreparedRequest, Response

TRANSFERWISE_BASE_URL = "https://api.transferwise.com"


class StatementType(StrEnum):
    COMPACT = "COMPACT"  # a single statement line per transaction
    FLAT = "FLAT"  # accounting statements where transaction fees are on a separate line


class TransferWise2FASession(niquests.Session):
    """
    TransferWise 2FA session implementation.
    """

    def __init__(self, *args, tfa_private_key: typing.Optional[PrivateKeyTypes] = None, **kwargs):
        super().__init__(*args, **kwargs)

        self.tfa_private_key = tfa_private_key

    def send(self, request: PreparedRequest, **kwargs: typing.Any) -> Response:
        resp = super().send(request, **kwargs)

        needs_2fa = (
            resp.status_code == 403
            and "x-2fa-approval" in resp.headers
            and resp.headers.get("x-2fa-approval-result") == "REJECTED"
        )

        if needs_2fa:
            if not self.tfa_private_key:
                raise RuntimeError("2FA required by no private key provided.")

            tfa_flow_id = resp.headers["x-2fa-approval"]

            tfa_signature = self.tfa_private_key.sign(
                tfa_flow_id.encode("utf-8"),
                padding.PKCS1v15(),
                hashes.SHA256(),
            )

            request.headers.update(
                {
                    "x-2fa-approval": tfa_flow_id,
                    "X-Signature": b64encode(tfa_signature).decode("utf-8"),
                }
            )

            return super().send(request, **kwargs)

        return resp


class TransferwiseClient:
    def __init__(self, access_token: str, private_key: PrivateKeyTypes):
        self.session = TransferWise2FASession(tfa_private_key=private_key)
        self.session.headers.update({"Authorization": f"Bearer {access_token}"})

    def get_balance_by_profile_and_balance_id(self, profile_id: str, balance_id: str) -> dict:
        resp = self.session.get(
            TRANSFERWISE_BASE_URL + f"/v4/profiles/{profile_id}/balances/{balance_id}",
        )

        resp.raise_for_status()

        return resp.json()

    def get_statement_by_profile_and_balance_id(
        self,
        profile_id: str,
        balance_id: str,
        currency: str,
        from_: datetime,
        to: datetime,
        type: StatementType = StatementType.FLAT,
        locale: str = "en",
    ) -> List[dict]:
        resp = self.session.get(
            TRANSFERWISE_BASE_URL + f"/v1/profiles/{profile_id}/balance-statements/{balance_id}/statement.json",
            params={
                "currency": currency,
                "intervalStart": from_.isoformat(),
                "intervalEnd": to.isoformat(),
                "type": type,
                "statementLocale": locale,
            },
        )

        resp.raise_for_status()

        return resp.json().get("transactions", [])


class TransferWiseProfileClient(TransferwiseClient):
    def __init__(self, *args, profile_id: str, **kwargs):
        super().__init__(*args, **kwargs)

        self.profile_id = profile_id


class TransferWiseBalanceClient(TransferWiseProfileClient):
    def __init__(self, *args, balance_id: str, **kwargs):
        super().__init__(*args, **kwargs)

        self.balance_id = balance_id

    def get_balance(self):
        return self.get_balance_by_profile_and_balance_id(self.profile_id, self.balance_id)

    def get_statement(
        self,
        currency: str,
        from_: datetime,
        to: datetime,
        type: StatementType = StatementType.FLAT,
        locale: str = "en",
    ):
        return self.get_statement_by_profile_and_balance_id(
            profile_id=self.profile_id,
            balance_id=self.balance_id,
            currency=currency,
            from_=from_,
            to=to,
            type=type,
            locale=locale,
        )

Like what you see? I may be available for fintech & open-banking-related consulting or implementation work. Reach out on LinkedIn or via e-mail if you need assistance with technical matters!

 
1
Kudos
 
1
Kudos

Now read this

StrongSwan IKEv2 iOS road-warrior server config

Note: this is an old post from 2016 and hasn’t been updated nor reviewed since. Nowadays I would recommend using Wireguard which is harder to configure insecurely, or at least use something like OpenWrt which provides a nice GUI to... Continue →