DocumentaciónAPI ReferenceCertificando Solicitudes

Certificando las solicitudes

Todas las solicitudes enviadas al servidor deben estar firmadas criptográficamente.

En lo que respecta a las medidas de seguridad, muchas API se contentan con solo tener una clave de API o un nombre de usuario como parámetro cuando se llama a un endpoint. El peligro con ese esquema es la capacidad de alguien de robar el token de la API y luego hacer sus propias llamadas con él. Para un sistema como Falabella Seller Center, que administra grandes sumas de dinero, tuvimos que elegir un enfoque más seguro: la clave API nunca sale de su computadora y solo se usa para firmar la solicitud. Además, para asegurarse de que las llamadas a la API no se puedan grabar y reproducir, uno de los parámetros que se firman es una marca de tiempo (timestamp). Más específicamente, el parámetro de firma que requerimos en todas las llamadas es el HMAC de la cadena de solicitud y su clave API con el algoritmo de resumen SHA256. Entonces, veamos cómo se implementa la firma:

Clave de la API (API Key)

Primero necesitamos nuestras credenciales para poder operar, para aquello puedes ver el apartado de ¿Cómo Obtener Tus Credenciales? (ver: Opera con Nuestras APIs).

Calcular el parámetro Signature

La cadena para firmar es...
  • el resultado concatenado de todos los parámetros de la solicitud,
  • ordenados por nombre,
  • incluyendo parámetros opcionales,
  • y excluyendo el parámetro Signature.
Los nombres y valores deben estar codificados en la URL de acuerdo con el estándar RFC 3986, concatenados con el carácter '='. Cada conjunto de parámetros (nombre = valor) debe separarse con el carácter '&'. La siguiente es el PHP de la implementación de referencia:
<?php

// Pay no attention to this statement.
// It's only needed if timezone in php.ini is not set correctly.
date_default_timezone_set("UTC");

// The current time. Needed to create the Timestamp parameter below.
$now = new DateTime();

// The parameters for our GET request. These will get signed.
$parameters = array(
    // The user ID for which we are making the call.
    'UserID' => '[email protected]',

    // The API version. Currently must be 1.0
    'Version' => '1.0',

    // The API method to call.
    'Action' => 'FeedList',

    // The format of the result.
    'Format' => 'XML',

    // The current time formatted as ISO8601
    'Timestamp' => $now->format(DateTime::ISO8601)
);

// Sort parameters by name.
ksort($parameters);

// URL encode the parameters.
$encoded = array();
foreach ($parameters as $name => $value) {
    $encoded[] = rawurlencode($name) . '=' . rawurlencode($value);
}

// Concatenate the sorted and URL encoded parameters into a string.
$concatenated = implode('&', $encoded);

// The API key for the user as generated in the Seller Center GUI.
// Must be an API key associated with the UserID parameter.
$api_key = 'YOUR_API_KEY';

// Compute signature and add it to the parameters.
$parameters['Signature'] =
    rawurlencode(hash_hmac('sha256', $concatenated, $api_key, false));
Si desea verificar la referencia anterior, reemplace el timestamp con el siguiente valor...
'Timestamp' => '2015-07-01T11:11:11+00:00'
...y debería obtener las siguientes entradas en $parameters:
Action=FeedList
Format=XML
Timestamp=2015-07-01T11:11:11+00:00
UserID[email protected]
Version=1.0
Signature=3ceb8ed91049dfc718b0d2d176fb2ed0e5fd74f76c5971f34cdab48412476041
Para luego realizar la solicitud GET en PHP, debe escribir algo como esto:
<?php

// ...continued from above

// Replace with the URL of your API host.
$url = "https://sellercenter-api.falabella.com/";

// Build Query String
$queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);

// Open cURL connection
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url."?".$queryString);

// Save response to the variable $data
curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$data = curl_exec($ch);

// Close Curl connection
curl_close($ch);
Si está más familiarizado con otros lenguajes de programación, aquí hay implementaciones en:
  • Java
  • Python
  • Node.js
  • Visual Basic
  • Cold Fusion

Llamada a la API en Java

/*
  * Sample Interface for Seller Center API
  */
package com.rocket.sellercenter;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.text.DateFormat;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class SellercenterAPI {
  private static final String ScApiHost = "https://sellercenter-api.falabella.com/";
  private static final String HASH_ALGORITHM = "HmacSHA256";
  private static final String CHAR_UTF_8 = "UTF-8";
  private static final String CHAR_ASCII = "ASCII";

  public static void main(String[] args) {
    Map<String, String> params = new HashMap<String, String>();
    params.put("UserID", "[email protected]");
    params.put("Timestamp", getCurrentTimestamp());
    params.put("Version", "1.0");
    params.put("Action", "ProductUpdate");
    final String apiKey = "YOUR_API_KEY";
    final String XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><Request><Product><SellerSku>4105382173aaee4</SellerSku><Price>12</Price></Product></Request>";
    final String out = getSellercenterApiResponse(params, apiKey, XML);
    System.out.println(out);
  }

  public static String getSellercenterApiResponse(Map<String, String> params, String apiKey, String XML) {
    String queryString = "";
    String Output = "";
    HttpURLConnection connection = null;
    URL url = null;
    Map<String, String> sortedParams = new TreeMap<String, String>(params);
    queryString = toQueryString(sortedParams);
    final String signature = hmacDigest(queryString, apiKey, HASH_ALGORITHM);
    queryString = queryString.concat("&Signature=".concat(signature));
    final String request = ScApiHost.concat("?".concat(queryString));
    try {
      url = new URL(request);
      connection = (HttpURLConnection) url.openConnection();
      connection.setDoOutput(true);
      connection.setDoInput(true);
      connection.setInstanceFollowRedirects(false);
      connection.setRequestMethod("POST");
      connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
      connection.setRequestProperty("charset", CHAR_UTF_8);
      connection.setUseCaches(false);
      if (!XML.equals("")) {
        connection.setRequestProperty("Content-Length", "" + Integer.toString(XML.getBytes().length));
        DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
        wr.writeBytes(XML);
        wr.flush();
        wr.close();
      }
      String line;
      BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
      while ((line = reader.readLine()) != null) {
        Output += line + "\n";
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return Output;
  }

  private static String hmacDigest(String msg, String keyString, String algo) {
    String digest = null;
    try {
      SecretKeySpec key = new SecretKeySpec((keyString).getBytes(CHAR_UTF_8), algo);
      Mac mac = Mac.getInstance(algo);
      mac.init(key);
      final byte[] bytes = mac.doFinal(msg.getBytes(CHAR_ASCII));
      StringBuffer hash = new StringBuffer();
      for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
          hash.append('0');
        }
        hash.append(hex);
      }
      digest = hash.toString();
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    } catch (InvalidKeyException e) {
      e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
    return digest;
  }

  private static String toQueryString(Map<String, String> data) {
    String queryString = "";
    try {
      StringBuffer params = new StringBuffer();
      for (Map.Entry<String, String> pair : data.entrySet()) {
        params.append(URLEncoder.encode((String) pair.getKey(), CHAR_UTF_8) + "=");
        params.append(URLEncoder.encode((String) pair.getValue(), CHAR_UTF_8) + "&");
      }
      if (params.length() > 0) {
        params.deleteCharAt(params.length() - 1);
      }
      queryString = params.toString();
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    }
    return queryString;
  }

  private static String getCurrentTimestamp() {
    final TimeZone tz = TimeZone.getTimeZone("UTC");
    final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ");
    df.setTimeZone(tz);
    final String nowAsISO = df.format(new Date());
    return nowAsISO;
  }
}

Llamada a la API en Python

import urllib
from hashlib import sha256
from hmac import HMAC
from datetime import datetime

parameters = {
  'UserID': '[email protected]',
  'Version': '1.0',
  'Action': 'FeedList',
  'Format':'XML',
  'Timestamp': datetime.now().isoformat()
}
api_key = 'YOUR_API_KEY'
concatenated = urllib.urlencode(sorted(parameters.items()))
parameters['Signature'] = HMAC(api_key, concatenated, sha256).hexdigest()

Llamada a la API en Node.js

const crypto = require('crypto');

const parameters = {
  UserID: 'YOUR_USER_ID',
  Version: '1.0',
  Action: 'FeedList',
  Format: 'XML',
  Timestamp: new Date().toISOString()
};

const apiKey = '[YOUR_API_KEY]';

// Sort and stringify parameters
const concatenated = Object.keys(parameters)
  .sort()
  .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(parameters[key])}`)
  .join('&');

const signature = crypto
  .createHmac('sha256', apiKey)
  .update(concatenated)
  .digest('hex');

parameters.Signature = signature;

Llamada a la API en Visual Basic

Imports System
Public Module modmain
  Sub Main()
    ' add your data here:
    Dim userId As String = "" 'login name / your email
    Dim password As String = "" 'your API key/password
    Dim version As String = "1.0"
    Dim action As String = "ProductCreate"
    Dim url As String = ""
    'e.g.: "https://sellercenter-api.falabella.com/"
    Dim result As String

    ' this is where the magic happens:
    result = generateRequest(url, userId, password, version, action)
    Console.WriteLine(result)
  End Sub

  Function generateRequest(Url As String, user As String, key As String, version As String, action As String) As String
    Dim timeStamp As String = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss-0000")
    ' ATTENTION: parameters must be in alphabetical order
    Dim stringToHash As String = _
      "Action=" + URLEncode(action) + _
      "&Timestamp=" + URLEncode(timeStamp) + _
      "&UserID=" + URLEncode(user) + _
      "&Version=" + URLEncode(version)
    Dim hash As String = HashString(stringToHash, key)
    ' ATTENTION: parameters must be in alphabetical order
    Dim request As String = _
      "Action=" + URLEncode(action) + _
      "&Signature=" + URLEncode(hash) + _
      "&Timestamp=" + URLEncode(timeStamp) + _
      "&UserID=" + URLEncode(user) + _
      "&Version=" + URLEncode(version)
    Return url + "?" + request
  End Function

  ' use this function instead of HttpServerUtility.UrlEncode()
  ' because we need uppercase letters
  Function URLEncode(EncodeStr As String) As String
    Dim i As Integer
    Dim erg As String
    erg = EncodeStr
    erg = Replace(erg, "%", Chr(1))
    erg = Replace(erg, "+", Chr(2))
    For i = 0 To 255
      Select Case i
        ' *** Allowed 'regular' characters
        Case 37, 43, 45, 46, 48 To 57, 65 To 90, 95, 97 To 122, 126
        Case 1 ' *** Replace original %
          erg = Replace(erg, Chr(i), "%25")
        Case 2 ' *** Replace original +
          erg = Replace(erg, Chr(i), "%2B")
        Case 32
          erg = Replace(erg, Chr(i), "+")
        Case 3 To 15
          erg = Replace(erg, Chr(i), "%0" & Hex(i))
        Case Else
          erg = Replace(erg, Chr(i), "%" & Hex(i))
      End Select
    Next
    Return erg
  End Function

  Function HashString(ByVal StringToHash As String, ByVal HachKey As String) As String
    Dim myEncoder As New System.Text.UTF8Encoding
    Dim Key() As Byte = myEncoder.GetBytes(HachKey)
    Dim Text() As Byte = myEncoder.GetBytes(StringToHash)
    Dim myHMACSHA256 As New System.Security.Cryptography.HMACSHA256(Key)
    Dim HashCode As Byte() = myHMACSHA256.ComputeHash(Text)
    Dim hash As String = Replace(BitConverter.ToString(HashCode), "-", "")
    Return hash.ToLower
  End Function
End Module

Llamada a la API en Adobe ColdFusion

<!--- this is a sample Adobe ColdFusion script for sending API request to Seller Center --->
<!--- hashing function --->
<cffunction name="HMAC_SHA256" returntype="string" access="private" output="false">
  <cfargument name="Data" type="string" required="true" />
  <cfargument name="Key" type="string" required="true" />
  <cfargument name="Bits" type="numeric" required="false" default="256" />
  <cfset var i = 0 />
  <cfset var HexData = "" />
  <cfset var HexKey = "" />
  <cfset var KeyLen = 0 />
  <cfset var KeyI = "" />
  <cfset var KeyO = "" />
  <cfset HexData = BinaryEncode(CharsetDecode(Arguments.data, "iso-8859-1"), "hex") />
  <cfset HexKey = BinaryEncode(CharsetDecode(Arguments.key, "iso-8859-1"), "hex") />
  <cfset KeyLen = Len(HexKey)/2 />
  <cfif KeyLen gt 64>
    <cfset HexKey = Hash(CharsetEncode(BinaryDecode(HexKey, "hex"), "iso-8859-1"), "SHA-256", "iso-8859-1") />
    <cfset KeyLen = Len(HexKey)/2 />
  </cfif>
  <cfloop index="i" from="1" to="#KeyLen#">
    <cfset KeyI = KeyI & Right("0"&FormatBaseN(BitXor(InputBaseN(Mid(HexKey,2*i-1,2),16),InputBaseN("36",16)),16),2) />
    <cfset KeyO = KeyO & Right("0"&FormatBaseN(BitXor(InputBaseN(Mid(HexKey,2*i-1,2),16),InputBaseN("5c",16)),16),2) />
  </cfloop>
  <cfset KeyI = KeyI & RepeatString("36",64-KeyLen) />
  <cfset KeyO = KeyO & RepeatString("5c",64-KeyLen) />
  <cfset HexKey = Hash(CharsetEncode(BinaryDecode(KeyI&HexData, "hex"), "iso-8859-1"), "SHA-256", "iso-8859-1") />
  <cfset HexKey = Hash(CharsetEncode(BinaryDecode(KeyO&HexKey, "hex"), "iso-8859-1"), "SHA-256", "iso-8859-1") />
  <cfreturn Left(HexKey,arguments.Bits/4) />
</cffunction>
<!---/ hashing function --->
<!--- define --->
<cfset send_xml = false><!--- for APIs that need XML request body, like xxxxProduct APIs --->
<cfset secret_key="YOUR_API_KEY"/>
<cfset sc_api_host="http://sellercenter-api.local/"/>
<!---/ define --->
<!--- request params --->
<cfset params = structNew() />
<cfset params["[email protected]"] = "UserID"/>
<cfset params["GetShipmentProviders"] = "Action"/>
<cfset params["2014-07-24T20:06:33+02:00"] = "Timestamp"/>
<cfset params["1.0"] = "Version"/>
<cfsavecontent variable="strXML">
  <?xml version="1.0" encoding="UTF-8" ?>
  <Request>
    <Product>
      <SellerSku>4105382173aaee4</SellerSku>
      <Price>12</Price>
    </Product>
  </Request>
</cfsavecontent>
<!---/ request params --->
<!--- generate signature --->
<cfset strtohash = "" />
<cfloop list="#ArrayToList(StructSort(params, "text", "asc"))#" index="key" >
  <cfset strtohash = strtohash & #params[key]# & "=" & #URLEncodedFormat(key)# & "&" />
</cfloop>
<cfset strtohash = #RemoveChars(strtohash, len(strtohash), 1)#/>
<cfset strtohash = #Replace(strtohash, "%2D", "-", "All")#/>
<cfset strtohash = #Replace(strtohash, "%2E", ".", "All")#/>
<cfset signature="#LCase(HMAC_SHA256(strtohash, secret_key))#"/>
<!---/ generate signature --->
<!--- issue the request --->
<cfif send_xml>
  <cfset http_method = "post" />
<cfelse>
  <cfset http_method = "get" />
</cfif>
<cfhttp method="#http_method#" url="#sc_api_host#">
  <cfloop list="#ArrayToList( StructSort(params, "text", "asc") )#" index="key" >
    <cfhttpparam type="url" name="#params[key]#" value="#key#"></cfhttpparam>
  </cfloop>
  <cfhttpparam type="xml" value="#strXML.Trim()#"></cfhttpparam>
  <cfhttpparam type="url" name="Signature" value="#signature#"></cfhttpparam>
</cfhttp>
<!---/ issue the request --->
<!--- XML response --->
<cfoutput>#cfhttp.filecontent#</cfoutput>