Autenticacion y Firma HMAC
Public API - Autenticacion y Firma HMAC#
Este documento explica a detalle como autenticar requests hacia la Public API de Payday, como construir la firma HMAC correctamente, que se firma exactamente, y como implementar helpers reutilizables en distintos lenguajes.Su objetivo es resolver las dudas mas comunes de integracion:1.
Que headers son obligatorios.
2.
Que valor usa cada header.
3.
Como construir el canonical string.
4.
Que body se debe hashear.
5.
Como firmar requests en Node.js, Python, PowerShell, C#, Bash y Postman/Apidog.
6.
Como diagnosticar errores como INVALID_SIGNATURE o REPLAY_DETECTED.
1. Alcance de esta autenticacion#
usan una cadena de seguridad en este orden:2.
Validacion de IP whitelist, si la key tiene whitelist configurada.
3.
Validacion HMAC con X-Timestamp, X-Nonce y X-Signature.
4.
Proteccion anti replay por nonce.
2. Credenciales que usa la firma#
Cuando se crea o rota una API key, el backend devuelve credenciales que se muestran una sola vez.Para firmar requests necesitas estas dos:1.
CLIENT_ID: se envia en X-Api-Key.
2.
HMAC_SECRET: se usa para calcular X-Signature.
1.
clientSecret no participa en la firma HMAC.
2.
HMAC_SECRET debe almacenarse en un backend o entorno confiable.
3.
No debes exponer HMAC_SECRET en frontend publico, apps web abiertas o codigo cliente distribuido.
Toda request a /public-api/v1/... debe incluir:Descripcion de cada header:| Header | Descripcion |
|---|
X-Api-Key | Identificador publico de la API key. |
X-Timestamp | Timestamp Unix en milisegundos. |
X-Nonce | Valor unico por request, normalmente UUID v4. |
X-Signature | Firma HMAC SHA-256 en hexadecimal. |
Content-Type | Recomendado application/json para requests con body JSON. |
4. Reglas exactas de la firma#
4.1 Canonical string#
La firma se calcula sobre este canonical string:METHOD
PATH_Y_QUERY
TIMESTAMP
NONCE
BODY_HASH
La funcion real del backend construye:${method.toUpperCase()}\n${path}\n${timestamp}\n${nonce}\n${bodyHash}
4.2 Que valor va en PATH_Y_QUERY#
2.
El querystring exacto, si existe.
/public-api/v1/sales-process/cotizaciones
/public-api/v1/sales-process/cotizaciones/69fa7b48e65c5ec021a8aeb0
/public-api/v1/sales-process/validaciones/imei/356789012345678?cotizacionId=69fa7b48e65c5ec021a8aeb0
https://api.tu-dominio.com/public-api/v1/sales-process/cotizaciones
http://localhost:4000/public-api/v1/sales-process/cotizaciones
La firma no usa protocolo ni host. Solo usa pathname + search.4.3 Que valor va en BODY_HASH#
BODY_HASH es el SHA-256 hexadecimal del body crudo exacto.1.
Se firma el string exacto enviado por HTTP.
2.
Espacios, tabs, saltos de linea y orden de llaves cambian el hash.
3.
Si tu cliente firma un body y luego envia otro body serializado distinto, la firma falla.
Para requests sin body, el backend hashea string vacio.e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
4.4 Tolerancias y protecciones del servidor#
Por default, el backend aplica estas validaciones adicionales:1.
X-Timestamp debe caer dentro de una ventana de 5 minutos.
2.
El nonce no puede reutilizarse.
3.
El TTL por default del nonce es 600 segundos.
Si reusas un nonce, el backend responde REPLAY_DETECTED.5. Flujo general para firmar una request#
2.
Extrae pathname + search.
3.
Genera timestamp en milisegundos.
5.
Obtiene el body crudo exacto que vas a enviar.
6.
Calcula bodyHash = SHA256_HEX(rawBody).
7.
Construye canonical con saltos de linea.
8.
Calcula signature = HMAC_SHA256_HEX(HMAC_SECRET, canonical).
9.
Envia la request con X-Api-Key, X-Timestamp, X-Nonce y X-Signature.
6. Reglas practicas para el body#
6.1 GET y HEAD#
3.
El bodyHash debe ser el hash SHA-256 de "".
6.2 POST, PATCH, PUT y DELETE con body#
2.
Firma exactamente ese mismo string.
3.
La opcion mas estable es JSON minificado.
{
"terminos_buro": true
}
el hash cambia y la firma deja de ser valida.6.3 POST sin body#
El servidor soporta firmar string vacio cuando no hay body.Sin embargo, algunos helpers de testing prefieren fallar si detectan POST sin body para evitar errores accidentales. Si usas un helper estricto y necesitas firmar un POST vacio, debes ajustar esa validacion en el helper cliente.7. Ejemplo reproducible completo#
El siguiente ejemplo usa valores fijos y un secreto dummy solo para documentacion.METHOD = POST
PATH = /public-api/v1/sales-process/cotizaciones
TIMESTAMP = 1778023239418
NONCE = 1e32736b-9bb0-4cf2-ab8d-12cdd6ef7631
BODY = {"terminos_buro":true}
SECRET = demo_hmac_secret_1234567890
7.2 Body hash#
9d090fbc4969d8ac1c7f2bc87a1add353990b08dbfd55710f64bb2a61d3098e3
7.3 Canonical string#
POST
/public-api/v1/sales-process/cotizaciones
1778023239418
1e32736b-9bb0-4cf2-ab8d-12cdd6ef7631
9d090fbc4969d8ac1c7f2bc87a1add353990b08dbfd55710f64bb2a61d3098e3
7.4 Signature resultante#
0fb6ebec2f82d25d3ccb6d31f07d91ef01592cfcc9d473e165c79eae14cd986b
Este valor solo es util como caso de prueba. No uses ese secreto fuera de documentacion.8. Contrato de una funcion auxiliar reusable#
La forma mas estable de integrarte es encapsular la firma en una funcion auxiliar con este contrato:type SignInput = {
method: string;
urlOrPath: string;
body?: string | object | null;
clientId: string;
hmacSecret: string;
timestamp?: string;
nonce?: string;
};
type SignOutput = {
path: string;
rawBody: string;
bodyHash: string;
canonical: string;
signature: string;
headers: {
"X-Api-Key": string;
"X-Timestamp": string;
"X-Nonce": string;
"X-Signature": string;
};
};
1.
Resolver urlOrPath a pathname + search.
2.
Convertir el body a string crudo una sola vez.
3.
Reusar ese mismo rawBody al enviar la request.
4.
Regresar tambien canonical y bodyHash para debug.
9. JavaScript / Node.js#
Este helper esta pensado para Node.js y servicios backend.Ejemplo con fetch#
10. Python#
Ejemplo con requests#
11. PowerShell#
function Get-Sha256Hex {
param([string]$Value = '')
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
$sha = [System.Security.Cryptography.SHA256]::Create()
try {
$hashBytes = $sha.ComputeHash($bytes)
}
finally {
$sha.Dispose()
}
-join ($hashBytes | ForEach-Object { $_.ToString('x2') })
}
function Get-HmacSha256Hex {
param(
[string]$Secret,
[string]$Value
)
$keyBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
$valueBytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
$hmac = [System.Security.Cryptography.HMACSHA256]::new($keyBytes)
try {
$hashBytes = $hmac.ComputeHash($valueBytes)
}
finally {
$hmac.Dispose()
}
-join ($hashBytes | ForEach-Object { $_.ToString('x2') })
}
function Convert-ToPathWithQuery {
param([string]$UrlOrPath)
if ($UrlOrPath -match '^https?://') {
$uri = [System.Uri]$UrlOrPath
return "$($uri.AbsolutePath)$($uri.Query)"
}
return $UrlOrPath
}
function New-PublicApiSignature {
param(
[string]$Method,
[string]$UrlOrPath,
[string]$ClientId,
[string]$HmacSecret,
[string]$Body = ''
)
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds().ToString()
$nonce = [guid]::NewGuid().ToString()
$path = Convert-ToPathWithQuery -UrlOrPath $UrlOrPath
$bodyHash = Get-Sha256Hex -Value $Body
$canonical = @($Method.ToUpper(), $path, $timestamp, $nonce, $bodyHash) -join "`n"
$signature = Get-HmacSha256Hex -Secret $HmacSecret -Value $canonical
return [pscustomobject]@{
Path = $path
Body = $Body
BodyHash = $bodyHash
Canonical = $canonical
Signature = $signature
Headers = @{
'X-Api-Key' = $ClientId
'X-Timestamp' = $timestamp
'X-Nonce' = $nonce
'X-Signature' = $signature
}
}
}
Ejemplo de uso#
$BASE_URL = 'http://localhost:4000'
$CLIENT_ID = 'pk_live_xxx'
$HMAC_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
$body = '{"terminos_buro":true}'
$signed = New-PublicApiSignature `
-Method 'POST' `
-UrlOrPath '/public-api/v1/sales-process/cotizaciones' `
-ClientId $CLIENT_ID `
-HmacSecret $HMAC_SECRET `
-Body $body
Invoke-RestMethod `
-Method POST `
-Uri "$BASE_URL$($signed.Path)" `
-Headers $signed.Headers `
-ContentType 'application/json' `
-Body $signed.Body
12. C##
13. Bash + OpenSSL#
Este ejemplo es util para Linux o CI simples.14. Postman / Apidog - script auxiliar#
Este script esta pensado para pre-request script y toma CLIENT_ID y HMAC_SECRET desde variables globales.Script recomendado#
Que hace este script#
1.
Lee HMAC_SECRET y CLIENT_ID desde variables globales.
2.
Obtiene el metodo HTTP real.
3.
Resuelve variables de entorno dentro de la URL.
4.
Extrae pathname + search.
5.
Para metodos con body, exige body raw.
6.
Obtiene el body exacto despues de resolver variables.
7.
Calcula bodyHash con SHA-256.
8.
Construye el canonical string.
9.
Calcula la firma HMAC SHA-256.
10.
Inserta los headers requeridos.
11.
Imprime valores utiles en consola para debug.
Nota importante sobre este helper#
Este helper es intencionalmente estricto: si detecta un metodo con body pero el body esta vacio, lanza error.Eso es util para evitar requests mal firmadas en testing, pero significa que:1.
Funciona muy bien para endpoints POST o PATCH que siempre llevan body.
2.
Si necesitas firmar un metodo sin body distinto de GET o HEAD, debes quitar la validacion que rechaza BODY vacio.
Version flexible de esa parte:15. Errores comunes y como resolverlos#
15.1 INVALID_SIGNATURE#
Ocurre cuando falla alguno de estos puntos:1.
Falta X-Timestamp, X-Nonce o X-Signature.
2.
X-Timestamp esta fuera de la tolerancia.
3.
El PATH firmado no coincide con la request real.
4.
El bodyHash no coincide con el body enviado.
5.
Se uso un HMAC_SECRET incorrecto.
1.
Verifica que firmaste pathname + search y no la URL completa.
2.
Verifica que el body firmado sea el mismo string que se envio.
3.
Verifica que no hubo reordenamiento de JSON por tu cliente.
4.
Verifica que el reloj del cliente este sincronizado.
5.
Verifica que X-Api-Key corresponda al mismo HMAC_SECRET.
15.2 REPLAY_DETECTED#
Ocurre cuando el nonce ya fue usado antes por la misma API key.1.
Genera un UUID nuevo por request.
2.
No reutilices requests firmadas en reintentos.
3.
Si reintentas, genera nuevo timestamp, nuevo nonce y nueva signature.
15.3 UNAUTHORIZED#
15.4 KEY_EXPIRED#
La API key ya expiro. Debe renovarse o rotarse segun el flujo administrativo.15.5 KEY_SUSPENDED#
La API key existe pero esta suspendida. Debe reactivarse del lado administrativo.15.6 IP_NOT_ALLOWED#
La IP origen no coincide con la whitelist configurada para esa key.15.7 RATE_LIMIT_EXCEEDED#
La key supero su limite configurado de requests.16. Debug util en desarrollo#
Cuando el servidor corre en development, una firma invalida puede devolver un bloque debug con informacion muy util, por ejemplo:Esto ayuda a detectar exactamente donde difiere la firma cliente contra la del servidor.En ambientes no development, la API responde el error resumido sin exponer ese detalle.17. Recomendaciones de implementacion#
1.
Implementa la firma en una sola funcion auxiliar reutilizable.
2.
Haz que esa funcion devuelva tambien rawBody, bodyHash y canonical.
3.
Envia siempre el mismo rawBody que se uso para firmar.
4.
Usa JSON minificado en integraciones automatizadas.
5.
Genera un nonce nuevo por request y no lo reutilices en reintentos.
6.
Manten HMAC_SECRET solo del lado servidor.
7.
Loggea path, timestamp, nonce, bodyHash y canonical en tu cliente cuando estes integrando.
8.
No incluyas HMAC_SECRET en logs.
18. Checklist rapido#
1.
Tienes CLIENT_ID y HMAC_SECRET correctos.
2.
En X-Api-Key envias CLIENT_ID.
3.
Firmas pathname + search, no la URL completa.
4.
El timestamp va en milisegundos.
6.
El body firmado es exactamente el body enviado.
7.
X-Signature esta en hexadecimal SHA-256 HMAC.
8.
Si reintentas, vuelves a firmar todo.
Modificado en 2026-05-07 00:17:28