Gestión de credenciales donde los usuarios pueden ingresarlas como un objeto de credencial o directamente en el panel de configuración del nodo, con la opción de proporcionar una contraseña o token

Entonces, estoy trabajando en un nodo personalizado con un montón de opciones de configuración. No es terriblemente complejo, solo está bastante ocupado. Una de las cosas en las que he estado devanándome los sesos es cómo manejar las credenciales – básicamente tengo cuatro opciones diferentes en dos conjuntos distintos.

La primera opción que los usuarios tienen es si las credenciales están “hardcodeadas” como un objeto credential (basado en una definición .credentials.ts) o se proporcionan dinámicamente en las propiedades del nodo (definidas en el archivo .node.ts), para casos donde un único pipeline podría usarse en un entorno de múltiples sitios (permitiendo que las credenciales se pasen dinámicamente desde una fuente segura; el “desde dónde” está fuera de mi alcance de influencia, así que este parecía ser el enfoque más racional).

La segunda opción es si proporcionan un token API directamente o proporcionan una contraseña que se usa para recuperar un token. Si el usuario proporciona un token, simplemente se codifica y se coloca en un encabezado Auth para cualquier solicitud – sin problema. Pero si proporcionan una contraseña, el nodo necesita hacer una solicitud “especial” primero, con un encabezado Auth personalizado que contiene una cadena codificada en base64 construida a partir del nombre de usuario y la contraseña, y el token se devuelve desde eso.

Después de avanzar parcialmente en la implementación de esto, me di cuenta de que tenía que manejar mucho de esto manualmente, así que el archivo .credentials.ts simplemente recopila datos (si el usuario va por esa ruta) – igual que las entradas del nodo. Tengo una función apiRequest() definida y exportada desde un archivo separado que maneja las solicitudes API en sí, y una función getApiToken() en ese mismo archivo (no exportada – solo se usa en apiRequest() en este momento) que maneja específicamente la solicitud del token.

Los usuarios pueden elegir una u otra para cualquiera de las dos. De todas formas, las entradas son las mismas: nombre de usuario, URL del sitio, y contraseña o token API. Ya había tenido todo esto funcionando sin la opción de usar una contraseña, pero me embarqué en esa búsqueda hoy, y lo tengo funcionando con la opción de credenciales dinámicas, pero esto parece haber roto la capacidad de extraer las credenciales de la configuración basada en .credentials.ts.

He reducido el problema a un bloque de código en el archivo .node.ts, que es simplemente una serie de asignaciones condicionales.

Primero, declara una variable – credentials – e intenta usar getCredentials() para extraerlas de la configuración .credentials.ts. Si no están presentes, hay una captura de error que simplemente establece credentials en undefined. Inmediatamente después, se declara una constante – dynamicConnection – que usa getNodeParameter() para obtener las entradas de la entrada de propiedades del nodo (cada propiedad es type-safe, especificada como un string) – así que si no hay nada ahí, simplemente son null. Luego, se crea un objeto connection donde cada una de las cuatro propiedades se asigna basándose en verificaciones condicionales de las propiedades de los objetos credentials y dynamicConnection. Para cada una, verificamos si el objeto credentials.<prop> contiene una cadena; si es así se asigna, y si no hacemos lo mismo para dynamicConnection. Si ninguno contiene un valor string, por defecto es undefined. Finalmente, una condición if() verifica que tengamos (a) un nombre de usuario, (b) una URL de instancia, y (c) una contraseña o un token; si alguna de esas condiciones no se cumple, lanza un error. Esa condición if() se lee como “si NO nombre de usuario -O- NO URL del sitio -O- ( NO token api -Y- NO contraseña): lanza un error.”

Esto funciona bien para la entrada dinámica de credenciales, pero si intento hacer esto con credenciales guardadas, me devuelven el error de ese bloque if(), como si faltara algo, aunque sé que todos los campos están ahí. Críticamente – esto funcionaba bien antes de que agregara la propiedad password a connection, así que genuinamente no creo que sea un problema de asignación o esperaría que hubiera fallado antes de implementar el campo de contraseña.

Siento que me estoy perdiendo algo absolutamente estúpido, pero espero que alguien aquí tenga más perspectiva sobre esto que yo mismo. ¿O al menos que puedas confirmar que no estoy loco? ¡Por favor y gracias!

@robby.emslie no estás loco, aquí va mi apuesta: agregar la opción de contraseña añadió una llamada a getNodeParameter (o un displayOptions auth-selector que oculta un campo), y getNodeParameter lanza “Could not get parameter” cuando no puede resolver la prop, que es el caso en la ruta saved-cred. si ese lanzamiento cae en el mismo try/catch que estás usando para anular las credenciales, todo el objeto saved-cred se borra y tu if() se dispara como si faltara. dale a cada getNodeParameter un default (this.getNodeParameter(‘password’, i, ‘’)) para que no pueda lanzar, y mantén el try limitado solo a getCredentials.

también, para el intercambio de contraseña a token que estás escribiendo manualmente con getApiToken(), las credenciales de n8n tienen un hook preAuthentication integrado que ejecuta una pre-request, captura el token y lo cachea, luego authenticate lo inyecta. Las credenciales MetabaseApi y Auth0ManagementApi hacen exactamente username/password a token, vale la pena copiar ese patrón para la ruta saved-cred.

1 me gusta

@robby.emslie esto parece ser una discrepancia entre cómo se asignan los campos vs cómo se validan.

La forma más rápida de encontrar el culpable — registra los objetos resueltos justo antes del if(), en la ruta de credenciales guardadas:

console.log(JSON.stringify({ credentials, dynamicConnection, connection }, null, 2));

Hay dos cosas en las que apostaría:

  1. Cadena vacía vs undefined. A través de getCredentials(), un campo sin usar (como password en la ruta de token) frecuentemente regresa como “” en lugar de undefined. “” pasa la comprobación de asignación “typeof === ‘string’” pero es falsy en el if() — así que el validador lanza “missing” aunque el campo técnicamente exista. Haz que ambos coincidan, p. ej.:
    const val = (typeof x === ‘string’ && x.trim() !== ‘’) ? x : undefined;

  2. La precedencia/agrupación de ese if() final. !user || !url || !token && !password se evalúa como !user || !url || (!token && !password) — que es lo que quieres. Pero si el código actual es ... || !token || !password, lanza excepción cada vez que password está vacío incluso cuando hay un token válido presente. Ese simple cambio de OR/AND coincide exactamente con tu síntoma “funcionaba antes de que agregara la propiedad password” — antes, no había ningún término password en la condición.

Si pegas el bloque .node.ts (las cuatro asignaciones + el if), puedo identificarlo.

1 me gusta

Las dos respuestas anteriores cubren el probable error. Yo añadiría una barrera protectora después: asegúrate de que tanto las credenciales guardadas como la entrada en línea se resuelvan en el mismo objeto de conexión normalizado, con cadenas vacías eliminadas, el modo de autenticación registrado y sin secrets sin procesar registrados. Luego valida solo ese objeto. Esto facilita la depuración sin filtrar tokens.

3 Me gusta

¡Algo se puso bastante raro aquí – ¡ja! Básicamente voy a tener que reconstruir esto esta tarde, creo. Metí la pata en algo. Pero tengo curiosidad sobre el hook integrado – voy a echarle un vistazo a Metabase y Auth0Management para ver un template. En realidad intenté usar eso originalmente, y creo que casi lo tenía funcionando, pero luego cuando agregué la opción de credenciales dinámicas, rompí algo.

Pero esto es súper útil. Muchas gracias. Creo que el problema está sucediendo en algún lugar con getCredentials() pero siento que, a estas alturas, va a ser más trabajo averiguar qué está mal que hacer un rebase de mi repo desde mi última configuración que funcionaba.

¡Gracias, amigo!

Quería volver a esto y darte las gracias. La sugerencia de revisar Metabase y Auth0Management fue una salvación.

Creo que eso resuelve el problema que tenía, pero voy a quitar la autenticación basada en contraseña de dynamicCredentials, creo. Me parece que es una vulnerabilidad de seguridad esperando a suceder. Ya funciona, pero voy a comentarla y si alguien del otro lado quiere activarla, que sea su responsabilidad. :slightly_smiling_face: jajaja

De todas formas, gracias de nuevo, @achamm !

1 me gusta

@robby.emslie ¡Me alegra haber podido ayudarte! Siéntete libre de marcar cualquiera de las respuestas como solución, ¡y buena suerte!