I have a problem that's quite difficult to find resources for on the internet. I've been trying to send a POST request to E-Bay's refund API. This API has recently switched to using a Digital Signature header for validation and is causing some issues
The following is the python script (found here: https://qubitsandbytes.co.uk/ebay-developer-api/using-ebay-digital-signatures-in-python) I'm using to make the call:
from base64 import b64encode
from urllib.parse import urlparse
import json
import sys
import time
import requests
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from Crypto.Hash import SHA256
from requests.exceptions import HTTPError
class EBayRefund:
__config: dict = {}
def __init__(self):
with open('config.json') as user_file:
json_file = user_file.read()
self.__config = json.loads(json_file)
def __get_access_token(self, ebay_refresh_token: str, oauth_token: str, scope: str):
"""Returns an eBay API access token, required to make further API calls."""
headers = {
"Authorization": "Basic " + oauth_token,
"Content-Type": "application/x-www-form-urlencoded",
}
data = {
"grant_type": "refresh_token",
"refresh_token": ebay_refresh_token,
"scope": scope,
}
try:
response = requests.post(
"https://api.ebay.com/identity/v1/oauth2/token",
headers=headers,
data=data,
timeout=10,
)
response.raise_for_status()
result = response.json()
return result["access_token"]
except (HTTPError, KeyError) as error:
sys.exit(f"Unable to fetch access token: {error}")
def __get_content_digest(self, content: str) -> str:
"""
Generate a digest of the provided content.
A content digest is needed when using one of the few POST requests requiring a digital
signature.
"""
hasher = SHA256.new()
hasher.update(bytes(content, encoding="utf-8"))
digest = b64encode(hasher.digest()).decode()
return digest
def __get_digital_signature(self, ebay_private_key: str, ebay_public_key_jwe: str, request_url: str, signature_params: str, digest: str = "") -> str:
"""
Generate the digital signature using the details provided. The signature is created
using ED25519.
To add support for POST requests, pass in a content digest and add a "Content-Digest"
entry to params, with a value of sha-256:digest:
"""
url = urlparse(request_url)
params = (
f'"x-ebay-signature-key": {ebay_public_key_jwe}\n'
f'"@method": POST\n'
f'"@path": {url.path}\n'
f'"@authority": {url.netloc}\n'
f'"@signature-params": {signature_params}\n'
f'"content-digest": sha-256=:{digest}:'
).encode()
print(params)
try:
private_key = ECC.import_key(f"""-----BEGIN PRIVATE KEY-----\n{ebay_private_key}\n-----END PRIVATE KEY-----""")
signer = eddsa.new(private_key, mode="rfc8032")
signature = signer.sign(params)
return b64encode(signature).decode()
except ValueError as error:
sys.exit(f"Error creating digital signature: {error}")
def __send_signed_api_request(self, ebay_private_key: str, ebay_public_key_jwe: str, access_token: str) -> None:
"""
Sends a request to the eBay API with a digital signature attached.
The API response text is printed before exiting.
"""
order_id = "XX-XXXXX-XXXXX"
request_url: str = f"https://api.ebay.com/sell/fulfillment/v1/order/{order_id}/issue_refund"
signature_input = f'("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created={int(time.time())}'
content = {
"orderLevelRefundAmount":
{
"value": "17.52",
"currency": "GBP"
},
"reasonForRefund": "BUYER_RETURN"
}
content = json.dumps(content)
content_digest = self.__get_content_digest(content = content)
signature = self.__get_digital_signature(
ebay_private_key=ebay_private_key,
ebay_public_key_jwe=ebay_public_key_jwe,
request_url=request_url,
signature_params=signature_input,
digest=content_digest
)
headers = {
"Authorization": "Bearer " + access_token,
"Signature-Input": f'sig1={signature_input}',
"Signature": f"sig1=:{signature}:",
"x-ebay-signature-key": ebay_public_key_jwe,
"x-ebay-enforce-signature": "true",
"content-digest": f"sha-256=:{content_digest}:"
}
print(json.dumps(headers, indent=4))
try:
response = requests.post(request_url, headers = headers, data = content, timeout = 10)
result = response.json()
print(json.dumps(result, indent = 4))
sys.exit()
except HTTPError as error:
sys.exit(f"Unable to send request: {error}")
def start(self):
"""Load credentials and read runtime arguments."""
scope = "https://api.ebay.com/oauth/api_scope/sell.fulfillment"
access_token = self.__get_access_token(
ebay_refresh_token = self.__config["refreshToken"],
oauth_token = self.__config["credentials"],
scope = scope,
)
self.__send_signed_api_request(
ebay_private_key = self.__config["privateKey"],
ebay_public_key_jwe = self.__config["jweKey"],
access_token = access_token,
)
if __name__ == "__main__":
EBR = EBayRefund()
EBR.start()
but I'm getting the following error:
{
"errors": [
{
"errorId": 215122,
"domain": "ACCESS",
"category": "REQUEST",
"message": "Signature validation failed",
"longMessage": "Signature validation failed to fulfill the request."
}
]
}
The original script uses GET as an example and it works fine when I test it with https://apiz.ebay.com/sell/finances/v1/transaction?limit=20&offset=0 but I just can't get it to work with POST
I've tried shuffling things around, playing with the signature inputs etc. but it just won't work
OK, I'll be answering my own question and hope it helps others
After asking for help here: https://github.com/eBay/digital-signature-verification-ebay-api/issues/20 I finally managed to get it working. Here's how:
my new signature base looks like this now:
params = (
f'"content-digest": sha-256=:{digest}:\n'
f'"x-ebay-signature-key": {ebay_public_key_jwe}\n'
f'"@method": POST\n'
f'"@path": {url.path}\n'
f'"@authority": {url.netloc}\n'
f'"@signature-params": {signature_params}'
).encode()
note the order in which the properties are arranged
and the headers:
headers = {
"Authorization": "TOKEN " + access_token,
"Signature-Input": f'sig1={signature_input}',
"Signature": f"sig1=:{signature}:",
"Accept": "application/json",
"Content-Type": "application/json",
"x-ebay-signature-key": ebay_public_key_jwe,
"content-digest": f"sha-256=:{content_digest}:"
}
Note: Because I'm calling the /post-order/v2/return/{return_id}/issue_refund
API endpoint I had to replace Bearer
with TOKEN
inside the Authorization
header.
Big thanks to Ulrich Hergerb from GitHub