Hace unos días me topé con la necesidad de integrar uno de nuestros sistemas en Harbour con la API de Google Sheets. El reto no es menor: Google exige autenticación mediante OAuth 2.0 para Cuentas de Servicio, lo que implica generar un JWT (JSON Web Token) firmado con el algoritmo RS256 (RSA con SHA-256).

Revisando mis notas y el histórico en el grupo de usuarios de Harbour, me di cuenta de que ya pasaron poco más de dos años desde mi primer intento. En aquel entonces desistí; no tenía un requerimiento de negocio urgente, eran simples pruebas de concepto y, honestamente, la comunidad no aportó mucha claridad sobre cómo manejar los punteros de hbssl para firmas asimétricas. Hoy, como ya todos sabemos, con la ayuda de la IA como par de programación, lo tenemos sin tanto complejo.

El Escenario Técnico

Para que Google nos entregue un access_token, debemos enviarle un JWT que incluya los claims de la cuenta de servicio y, lo más importante, una firma generada con nuestra private_key.

1. La importancia de hb_jsonDecode

A diferencia de mis intentos anteriores donde trataba de “parsear” el archivo JSON de Google con funciones de cadena como At() y SubStr(), la forma correcta y profesional es usar Hashes.

   LOCAL hJSON := {=>}
   cJSON := hb_MemoRead( "service_account.json" )
   
   // Decodificación robusta por referencia
   hb_jsonDecode( cJSON, @hJSON )

   IF hb_HHasKey( hJSON, "private_key" )
      cKey := hJSON[ "private_key" ]
      // Google envía \n literales, hay que convertirlos a saltos reales
      cKey := StrTran( cKey, '\n', hb_bChar( 10 ) )
   ENDIF

El uso de @hJSON permite que la función pueble nuestra variable local de forma eficiente, dándonos acceso inmediato a la clave privada sin importar el formato del archivo.

2. La Firma RS256 con hbssl

El corazón de la solución es la librería hbssl. Para que funcione en entornos modernos (OpenSSL 1.1 o 3.0) y en 64 bits, la inicialización del contexto debe ser precisa.

Ejemplo Completo de Implementación

A continuación, les comparto el código fuente íntegro para generar el token. Solo necesitan sustituir las constantes de email y el nombre del archivo JSON por los suyos. En la documentación oficial de Google Cloud encontrarán abundantes guías sobre cómo crear una “Service Account” y descargar su archivo de llaves privadas en formato JSON.

Este ejemplo utiliza el flujo completo: desde la lectura del archivo físico hasta la generación del JWT final listo para ser usado en un POST hacia los servidores de Google.

#require "hbssl"

#define CLIENT_EMAIL  "tu-cuenta-servicio@tu-proyecto.iam.gserviceaccount.com"
#define SCOPE         "https://www.googleapis.com/auth/spreadsheets"

REQUEST __HBEXTERN__HBSSL__

FUNCTION Main()
   LOCAL cJWT
   LOCAL cJSON
   LOCAL cKey
   LOCAL hJSON := {=>}

   // Cargamos el archivo JSON descargado de Google Cloud Console
   cJSON := hb_MemoRead( "tu-archivo-credenciales.json" )

   IF Empty( cJSON )
      ? "ERROR: No se encontró el archivo JSON"
      RETURN NIL
   ENDIF

   // Convertimos el JSON a un Hash de Harbour de forma segura por referencia
   hb_jsonDecode( cJSON, @hJSON )

   // Verificamos si la clave existe en el Hash
   IF ! hb_HHasKey( hJSON, "private_key" )
      ? "ERROR: El JSON no contiene la entrada 'private_key'"
      RETURN NIL
   ENDIF

   // Extraemos la clave directamente por su nombre
   cKey := hJSON[ "private_key" ]
   
   // Google entrega la clave con '\n' literales, los convertimos a saltos de línea reales
   cKey := StrTran( cKey, '\n', hb_bChar( 10 ) )

   ? "Generando JWT RS256 con hbssl nativo..."
   cJWT := GenerarJWT_Nativo( CLIENT_EMAIL, cKey, SCOPE )

   IF Empty( cJWT )
      ? "ERROR: No se pudo generar el JWT"
      RETURN NIL
   ENDIF

   ? "JWT generado OK, longitud:", Len( cJWT )
   ? "Copia este token para probarlo en Postman:"
   ? cJWT

RETURN NIL

FUNCTION GenerarJWT_Nativo( cEmail, cPemKey, cScope )
   LOCAL nNow, nExp
   LOCAL cHeader, cPayload
   LOCAL cB64Header, cB64Payload, cSigningInput
   LOCAL cSignature, cJWT
   LOCAL bio, pKey, ctx
   LOCAL nRet

   nNow := GETUNIXTIME()
   nExp := nNow + 3600 // Token válido por 1 hora

   // 1. Definir Header y Payload
   cHeader  := '{"alg":"RS256","typ":"JWT"}'
   cPayload := '{"iss":"' + cEmail + '",' + ;
                '"scope":"' + cScope + '",' + ;
                '"aud":"https://oauth2.googleapis.com/token",' + ;
                '"iat":' + LTrim(Str(nNow)) + ',' + ;
                '"exp":' + LTrim(Str(nExp)) + '}'

   // 2. Codificar en Base64Url
   cB64Header    := Base64UrlEncode( cHeader )
   cB64Payload   := Base64UrlEncode( cPayload )
   cSigningInput := cB64Header + "." + cB64Payload

   // 3. Preparar OpenSSL para la firma
   SSL_INIT()
   OpenSSL_add_all_algorithms()
   ERR_load_PEM_strings()

   bio  := BIO_new_mem_buf( cPemKey )
   pKey := PEM_READ_PRIVATEKEY( bio, "" )

   IF Empty( pKey )
      ? "ERROR: No se pudo parsear la clave privada PEM."
      RETURN ""
   ENDIF

   // 4. Firmar usando EVP API (SHA256)
   ctx  := EVP_MD_CTX_new()
   nRet := EVP_SignInit_ex( ctx, "SHA256" )

   IF nRet != 1
      ? "ERROR: EVP_SignInit_ex falló"
      RETURN ""
   ENDIF

   nRet := EVP_SignUpdate( ctx, cSigningInput )

   IF nRet != 1
      ? "ERROR: EVP_SignUpdate falló"
      RETURN ""
   ENDIF

   cSignature := ""
   // Pasamos cSignature por referencia para que OpenSSL la pueble
   nRet := EVP_SignFinal( ctx, @cSignature, pKey )

   IF nRet != 1 .OR. Empty( cSignature )
      ? "ERROR: EVP_SignFinal falló"
      RETURN ""
   ENDIF

   // 5. Construir el JWT Final
   cJWT := cSigningInput + "." + Base64UrlEncode( cSignature )

RETURN cJWT

FUNCTION Base64UrlEncode( cData )
   LOCAL cB64 := hb_base64Encode( cData )
   // Conversión estándar de Base64 a Base64Url
   cB64 := StrTran( cB64, "+", "-" )
   cB64 := StrTran( cB64, "/", "_" )
   cB64 := StrTran( cB64, "=", "" )
   cB64 := StrTran( cB64, hb_bChar( 13 ), "" )
   cB64 := StrTran( cB64, hb_bChar( 10 ), "" )
RETURN cB64

#pragma BEGINDUMP
#include <time.h>
#include <hbapi.h>

HB_FUNC( GETUNIXTIME )
{
   hb_retnl( (long) time( NULL ) );
}
#pragma ENDDUMP

Configuración del Proyecto (.hbp)

Para compilar este ejemplo, es fundamental enlazar correctamente las librerías de OpenSSL. Dependiendo de tu entorno (en mi caso MSVC64), deberás asegurarte de tener las DLLs correspondientes en tu PATH o junto al ejecutable.

Aquí les comparto mi archivo de proyecto .hbp

# jwt_google_test.hbp
-ojwt_google_test

# Archivo fuente
jwt_google_test.prg

# Dependencias de Harbour
hbwin.hbc
hbssl.hbc

# Enlace directo con librerías de OpenSSL
-lhbssl
-lhbssls

-llibcrypto-1_1-x64
-llibssl-1_1-x64

3. Obtención del Timestamp (Unix Epoch)

Google requiere el tiempo en formato Unix (segundos desde 1970). Aunque se puede calcular en Harbour nativo, prefiero la precisión de una pequeña función en C vía #pragma BEGINDUMP para asegurar compatibilidad total con UTC:

#pragma BEGINDUMP
#include <time.h>
#include <hbapi.h>

HB_FUNC( GETUNIXTIME )
{
   hb_retnl( (long) time( NULL ) );
}
#pragma ENDDUMP

Conclusión

El resultado de este proceso es un JWT que, al ser enviado vía POST a Google, nos devuelve un access_token listo para usarse en cabeceras Authorization: Bearer.

Lo que hace dos años parecía un muro infranqueable por falta de documentación clara en la comunidad, hoy se resuelve con unas pocas líneas de código bien estructuradas. La clave está en no pelearse con los tipos de datos: usar Hashes para el JSON y confiar en el EVP API de OpenSSL para la criptografía.

Espero que esta aportación le ayude a alguien más que esté intentando modernizar sus procesos en Harbour.


<
Previous Post
SDK CONTPAQi desde Harbour: worker process de 32 bits con cola persistente en SQL Server
>
Blog Archive
Archive of all previous blog posts