Handling credentials where users can enter them as a credential object or inline in the node's configuration pane, with an option to provide password or token

So, I’m working on a custom node with a lot of options for configuration. Not terribly complex, just busy. One of the things that I’ve been wracking my brain about is handling credentials – basically given four different options in two different sets.

The first choice users get is whether credentials are “hard coded” as a credential object (based on a .credentials.ts definition) or dynamically provided in the node’s properties (defined in the .node.ts file), for cases where a single pipeline might be used in a mutli-site setting (allowing the credentials to be passed dynamically from a secure source; the “from where” is beyond my scope of influence, so this seemed to be the most rational approach).

The second choice is whether they provide an API token directly or provide a password that is used to retrieve a token. If the user provides a token, it’s just encoded and plopped into an Auth header for whatever request – no problem. But if they provide a password, the node needs make a “special” request, first, with a custom Auth header containing a base64-encoded string built from the username and password, and the token is returned from that.

After getting part of the way through implementing this, I realized I had to handle a lot of this manually, so the .credentials.ts file just collects data (if the user is going that route) – just like the inputs for the node. I’ve got an apiRequest() function defined and exported from a separate file that handles making the API requests, themselves, and a getApiToken() function in that same file (not exported – it’s only used by apiRequest() (at the moment) that specifically handles the request for the token.

Users can choose either/or for either/or. Regardless, the inputs are the same: username, site URL, and password or API token. I had already had all this working without the option to use a password, but I took up that quest today, and I’ve gotten it working with the dynamic credentials option, but this seems to have broken any ability to pull the credentials from the .credentials.ts-based configuration.

I’ve narrowed the problem down to a block of code in the .node.ts file, which is just a series of conditional assignments.

First, it declares a variable – credentials – and tries to use getCredentials() to pull them from the .credentials.ts configuration. If they’re not present, there’s an error catch that just defaults credentials to undefined. Immediately afterwards, there’s a constant declared – dynamicConnection – that uses getNodeParamter() to get the inputs from the node’s properties input (each property is type-safe, specified as a string) – so if nothing’s in there, they’re just null. Next, a connection object is created where each of the four properties are assigned based on conditional checks of the credentials and dynamicConnection objects’ properties. For each one, we check to see if the credentials.<prop> object contains a string; if it does it gets assigned, and if not we do the same for dynamicConnection. If neither contains a string value, it defaults to undefined. Finally, an if() condition checks to make sure we have (a) a username, (b) an instance URL, and (c) either a password or a token; if any of those conditions are not met, it throws an error. That if() condition is read as “if NOT username -OR- NOT site URL -OR- ( NOT api token -AND- NOT password): throw an error.”

This works fine for the dynamic input of credentials, but if I try to do this with saved credentials, they kick back the error from that if() block, as if something was missing, even though I know all the fields are there. Critically – this worked fine before I added the password property to connection, so I genuinely don’t think it’s an assignment problem or I would expect it to have failed prior to implementing the password field.

I feel like I’m missing something absolutely stupid, but I’m hoping someone here has more insight to this than I do on my own. Or that you can at least confirm that I’m not crazy? Please and thank you!

@robby.emslie youre not crazy, heres my bet: adding the password option added a getNodeParameter call (or a displayOptions auth-selector that hides a field), and getNodeParameter throws “Could not get parameter” when it cant resolve the prop, which is the case on the saved-cred path. if that throw lands in the same try/catch youre using to null credentials, the whole saved-cred object gets wiped and your if() fires as missing. give each getNodeParameter a default (this.getNodeParameter(‘password’, i, ‘’)) so it cant throw, and keep the try scoped to getCredentials only.

also, for the password to token exchange youre hand-rolling getApiToken() for, n8n creds have a built-in preAuthentication hook that runs a pre-request, grabs the token and caches it, then authenticate injects it. MetabaseApi and Auth0ManagementApi creds do exactly username/password to token, worth copying that pattern for the saved-cred route.

1 Like

@robby.emslie this smells like a mismatch between how the fields get assigned vs how they get validated.

Fastest way to see the culprit — log the resolved objects right before the if(), on the saved-credential path:

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

Two things I’d bet on:

  1. Empty string vs undefined. Through getCredentials(), an unused field (like password on the token route) often comes back as “” rather than undefined. “” passes a “typeof === ‘string’” assignment check but is falsy in the if() — so the validator throws “missing” even though the field technically exists. Make the two agree, e.g.:
    const val = (typeof x === ‘string’ && x.trim() !== ‘’) ? x : undefined;

  2. The precedence/grouping of that final if(). !user || !url || !token && !password evaluates as !user || !url || (!token && !password) — which is what you want. But if the actual code is ... || !token || !password, it throws whenever password is empty even when a valid token is present. That single OR/AND swap matches your “it worked before I added the password property” symptom exactly — before, there was no password term in the condition at all.

If you paste the .node.ts block (the four assignments + the if), I can pinpoint it.

1 Like

The two replies above cover the likely bug. I’d add one guardrail after: make both saved creds and inline input resolve into the same normalized connection object, with empty strings stripped, auth mode recorded, and no raw secrets logged. Then validate only that object. It makes this easier to debug without leaking tokens.

3 Likes

Something got way goofy here – hah! I’m basically going to have to reubild this, this afternoon, I think. I goofed something up somewhere. But I’m curious about the built-in hook – I’ll take a look at Metabase and Auth0Management for a template. I actually tried using that, originally, and I think I almost had it working, but then when I rolled in the dynamic credentialing option, I broke something.

This is super-helpful, though. Thank you, so much. I think the issue is happening somewhere with getCredentials() but I feel like, at this point, it’s going to be more work to figure out what’s going wrong than to rebase my repo from my last know-working configuration.

Thanks, friend!

I wanted to circle back on this and thank you. The suggestion to look at Metabase and Auth0Management was a life saver.

I think that resolves the issue I had, but I’m going to actually chop out the password-based auth from the dynamicCredentials, I think. I feel like that’s a security flaw waiting to happen. It already works, but I’m going to comment it out and if someone on the receiving end wants to turn it on, it can be their responsibility. :slightly_smiling_face: lol

Anyway, thanks again, @achamm !

1 Like

@robby.emslie Happy I could help! Feel free to mark any of the replies as the solution, and good luck!