n8n Node Credentials: API Key, Credential Test and OAuth2
In this video
This is the written version of Part 4 of Build your own n8n node. You check the key and it's right. You paste it again and it's still right. And the API still says 401. If you've built anything on an API, you've had that evening.
In this part the 401 becomes a contact, created in a real account by a node you wrote. We also go one step further than most tutorials, so a wrong key gets caught in the form before it breaks someone's workflow twenty minutes in. OAuth2 is here too, and it's less code than you'd guess.
A credential is a file
You know the credential window in n8n: paste a key, save, see the green tick. Now you're the one building it, and it's one small file in credentials/.
Here's the whole thing for an API that wants Authorization: Bearer <key>:
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class AcmeCrmApi implements ICredentialType {
name = 'acmeCrmApi';
displayName = 'Acme CRM API';
documentationUrl = 'https://docs.acme.example/api/authentication';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: { password: true },
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: { Authorization: '=Bearer {{$credentials.apiKey}}' },
},
};
test: ICredentialTestRequest = {
request: { baseURL: 'https://api.acme.example/v1', url: '/me' },
};
}
The rest of this part goes through it piece by piece.
The API key field
properties is the form the user fills in. For us that's one field, the API key.
The line that matters is typeOptions: { password: true }. All it does is show the key as dots. Leave it out and the key sits in plain text, so the next time your user records their screen for a tutorial or shares it in a meeting, their secret is on display because of your node. n8n's UX guidelines require password fields for keys, and reviewers check it.
authenticate: say it once
Most APIs want the key in a header, so authenticate tells n8n to add one: "on every request, add Authorization: Bearer and the key."
The useful part is "every request." You write this once, in this one file, not inside each operation. Every request the node ever makes passes through it and gets the key on the way out.
If your API wants the key somewhere else, the same block takes qs for a query-string parameter, body for the request body, or auth for basic auth with username and password.
Connect the node to the credential
The node has to ask for this credential, by name, in its description:
credentials: [{ name: 'acmeCrmApi', required: true }],
And package.json has to list the built file in the n8n block:
"credentials": ["dist/credentials/AcmeCrmApi.credentials.js"]
The silent mistake: the name
The name in the credential class and the name in the node's credentials list must match exactly, capital letters and all. acmeCrmApi and acmeCRMApi look the same at a glance.
When they don't match, there's often no error at first. The credential doesn't appear in the node, and you sit there wondering what you did wrong. When n8n does complain, the message is Credentials of type "..." aren't known.
If your login isn't showing up, check the name. It's almost always the name.
It works
Restart npm run dev (credential and description changes need a restart), create the credential in n8n, paste the key, and press the same Execute step as last time.
It works. A real contact, in a real account, created by a node you wrote. One row, and it's the moment the project stops being a tutorial and starts doing something.
The test request
Now watch what happens with a wrong key and no test block. The user pastes it, saves, and n8n says fine. They find out twenty minutes later, when a workflow fails somewhere in the middle and they have no idea why.
The test block fixes that. It gives n8n one request to try when the credential is saved: something cheap that only succeeds with a good key, like GET /me or a list endpoint with a limit of one. A wrong key now fails in the form, straight away. A right one gets the green tick, and the user trusts it.
It's a few lines that nobody notices. They'd only notice if it were missing.
OAuth2
Then the "Connect my account" button, with its redirects, tokens and refreshing. You don't write any of that. n8n already knows how OAuth2 works, and your credential extends its built-in one:
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class AcmeCrmOAuth2Api implements ICredentialType {
name = 'acmeCrmOAuth2Api';
extends = ['oAuth2Api'];
displayName = 'Acme CRM OAuth2 API';
documentationUrl = 'https://docs.acme.example/api/oauth';
properties: INodeProperties[] = [
{ displayName: 'Grant Type', name: 'grantType', type: 'hidden', default: 'authorizationCode' },
{ displayName: 'Authorization URL', name: 'authUrl', type: 'hidden', default: 'https://app.acme.example/oauth/authorize' },
{ displayName: 'Access Token URL', name: 'accessTokenUrl', type: 'hidden', default: 'https://app.acme.example/oauth/token' },
{ displayName: 'Scope', name: 'scope', type: 'hidden', default: 'contacts:read contacts:write deals:write' },
{ displayName: 'Auth URI Query Parameters', name: 'authQueryParameters', type: 'hidden', default: '' },
{ displayName: 'Authentication', name: 'authentication', type: 'hidden', default: 'header' },
];
}
extends = ['oAuth2Api'] says "I'm an OAuth2 credential, do the usual thing." The rest is three facts about your API: where users log in, where the token comes from, and which permissions to ask for. The fields are hidden because your API decides them, not the user.
n8n's UX guidelines say to include the OAuth2 credential whenever your API offers it. If your node supports both, add an Authentication dropdown and show each credential only for its option, the way n8n's built-in nodes do.
Scopes, the hard part
Scopes are where OAuth2 goes wrong.
- Ask for too few, and some operations fail for reasons the user can't see.
- Ask for too many, and the user gets a long consent screen asking for access to everything, and closes the tab.
You want exactly what your operations need. That means going through the operations one by one and checking which permission each one uses.
What you have now
- a credential form with the key hidden
- the key sent on every request, from one place
- a bad key caught in the form, before it costs anyone an evening
- OAuth2 without writing any of the flow yourself
Now ask the node for every contact. Fifty come back. There are 1,240 in the account, and nothing failed. In Part 5, all 1,240 come back, and getting there means taking apart something we already built.
One more thing. If your OpenAPI spec already describes your authentication and scopes, NativeShip, the tool we build, reads them from there, so there's nothing to copy across by hand.
Frequently asked questions
Create a class in credentials/ that implements ICredentialType. Give it a password field for the key, an authenticate block that adds the key to every request, and a test request. Then list it in the node's credentials array and in package.json.
The name in your credential class and the name in the node's credentials array don't match exactly. Capital letters count.
Add a test request to the credential class. It should be a cheap call that only succeeds with a valid key, such as GET /me. n8n runs it when the user saves the credential and shows the result in the form.
Create a credential class with extends = ['oAuth2Api'] and fill in the authorization URL, token URL and scopes. n8n handles the redirects, tokens and refreshing.