forked from n8n-io/n8n
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Create TOTP node (n8n-io#5901)
* ✨ Create TOTP node * ♻️ Apply feedback * ♻️ Recreate `pnpm-lock.yaml` * ♻️ Apply Giulio's feedback * 🚧 WIP node tests * ✅ Finish node test setup * ⏪ Restore test command * ⚡ linter fixes, tweaks * ♻️ Address Michael's feedback --------- Co-authored-by: Michael Kret <[email protected]>
- Loading branch information
Showing
10 changed files
with
441 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { ICredentialType, INodeProperties } from 'n8n-workflow'; | ||
|
||
export class TotpApi implements ICredentialType { | ||
name = 'totpApi'; | ||
|
||
displayName = 'TOTP API'; | ||
|
||
documentationUrl = 'totp'; | ||
|
||
properties: INodeProperties[] = [ | ||
{ | ||
displayName: 'Secret', | ||
name: 'secret', | ||
type: 'string', | ||
typeOptions: { password: true }, | ||
default: '', | ||
placeholder: 'e.g. BVDRSBXQB2ZEL5HE', | ||
required: true, | ||
description: | ||
'Secret key encoded in the QR code during setup. <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format#secret">Learn more</a>.', | ||
}, | ||
{ | ||
displayName: 'Label', | ||
name: 'label', | ||
type: 'string', | ||
default: '', | ||
required: true, | ||
placeholder: 'e.g. GitHub:john-doe', | ||
description: | ||
'Identifier for the TOTP account, in the <code>issuer:username</code> format. <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label">Learn more</a>.', | ||
}, | ||
]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"node": "n8n-nodes-base.totp", | ||
"nodeVersion": "1.0", | ||
"codexVersion": "1.0", | ||
"categories": ["Core Nodes"], | ||
"subcategories": ["Helpers"], | ||
"details": "Generate a time-based one-time password", | ||
"alias": ["2FA", "MFA", "authentication", "Security", "OTP", "password", "multi", "factor"], | ||
"resources": { | ||
"primaryDocumentation": [ | ||
{ | ||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.totp/" | ||
} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import type { | ||
IExecuteFunctions, | ||
INodeExecutionData, | ||
INodeType, | ||
INodeTypeDescription, | ||
} from 'n8n-workflow'; | ||
|
||
import { NodeOperationError } from 'n8n-workflow'; | ||
|
||
import OTPAuth from 'otpauth'; | ||
|
||
export class Totp implements INodeType { | ||
description: INodeTypeDescription = { | ||
displayName: 'TOTP', | ||
name: 'totp', | ||
icon: 'fa:fingerprint', | ||
group: ['transform'], | ||
version: 1, | ||
subtitle: '={{ $parameter["operation"] }}', | ||
description: 'Generate a time-based one-time password', | ||
defaults: { | ||
name: 'TOTP', | ||
}, | ||
inputs: ['main'], | ||
outputs: ['main'], | ||
credentials: [ | ||
{ | ||
name: 'totpApi', | ||
required: true, | ||
}, | ||
], | ||
properties: [ | ||
{ | ||
displayName: 'Operation', | ||
name: 'operation', | ||
type: 'options', | ||
noDataExpression: true, | ||
options: [ | ||
{ | ||
name: 'Generate Secret', | ||
value: 'generateSecret', | ||
action: 'Generate secret', | ||
}, | ||
], | ||
default: 'generateSecret', | ||
}, | ||
{ | ||
displayName: 'Options', | ||
name: 'options', | ||
type: 'collection', | ||
displayOptions: { | ||
show: { | ||
operation: ['generateSecret'], | ||
}, | ||
}, | ||
default: {}, | ||
placeholder: 'Add Option', | ||
options: [ | ||
{ | ||
displayName: 'Algorithm', | ||
name: 'algorithm', | ||
type: 'options', | ||
default: 'SHA1', | ||
description: 'HMAC hashing algorithm. Defaults to SHA1.', | ||
options: [ | ||
{ | ||
name: 'SHA1', | ||
value: 'SHA1', | ||
}, | ||
{ | ||
name: 'SHA224', | ||
value: 'SHA224', | ||
}, | ||
{ | ||
name: 'SHA256', | ||
value: 'SHA256', | ||
}, | ||
{ | ||
name: 'SHA3-224', | ||
value: 'SHA3-224', | ||
}, | ||
{ | ||
name: 'SHA3-256', | ||
value: 'SHA3-256', | ||
}, | ||
{ | ||
name: 'SHA3-384', | ||
value: 'SHA3-384', | ||
}, | ||
{ | ||
name: 'SHA3-512', | ||
value: 'SHA3-512', | ||
}, | ||
{ | ||
name: 'SHA384', | ||
value: 'SHA384', | ||
}, | ||
{ | ||
name: 'SHA512', | ||
value: 'SHA512', | ||
}, | ||
], | ||
}, | ||
{ | ||
displayName: 'Digits', | ||
name: 'digits', | ||
type: 'number', | ||
default: 6, | ||
description: 'Number of digits in the generated TOTP code. Defaults to 6 digits.', | ||
}, | ||
{ | ||
displayName: 'Period', | ||
name: 'period', | ||
type: 'number', | ||
default: 30, | ||
description: | ||
'How many seconds the generated TOTP code is valid for. Defaults to 30 seconds.', | ||
}, | ||
], | ||
}, | ||
], | ||
}; | ||
|
||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { | ||
const items = this.getInputData(); | ||
const returnData: INodeExecutionData[] = []; | ||
|
||
const operation = this.getNodeParameter('operation', 0); | ||
const credentials = (await this.getCredentials('totpApi')) as { label: string; secret: string }; | ||
|
||
if (!credentials.label.includes(':')) { | ||
throw new NodeOperationError(this.getNode(), 'Malformed label - expected `issuer:username`'); | ||
} | ||
|
||
const options = this.getNodeParameter('options', 0) as { | ||
algorithm?: string; | ||
digits?: number; | ||
period?: number; | ||
}; | ||
|
||
if (!options.algorithm) options.algorithm = 'SHA1'; | ||
if (!options.digits) options.digits = 6; | ||
if (!options.period) options.period = 30; | ||
|
||
const [issuer] = credentials.label.split(':'); | ||
|
||
const totp = new OTPAuth.TOTP({ | ||
issuer, | ||
label: credentials.label, | ||
secret: credentials.secret, | ||
algorithm: options.algorithm, | ||
digits: options.digits, | ||
period: options.period, | ||
}); | ||
|
||
const token = totp.generate(); | ||
|
||
const secondsRemaining = | ||
(options.period * (1 - ((Date.now() / 1000 / options.period) % 1))) | 0; | ||
|
||
if (operation === 'generateSecret') { | ||
for (let i = 0; i < items.length; i++) { | ||
const executionData = this.helpers.constructExecutionMetaData( | ||
this.helpers.returnJsonArray({ token, secondsRemaining }), | ||
{ itemData: { item: i } }, | ||
); | ||
|
||
returnData.push(...executionData); | ||
} | ||
} | ||
|
||
return this.prepareOutputData(returnData); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import * as Helpers from '../../../test/nodes/Helpers'; | ||
import { executeWorkflow } from '../../../test/nodes/ExecuteWorkflow'; | ||
import type { WorkflowTestData } from '../../../test/nodes/types'; | ||
|
||
jest.mock('otpauth', () => { | ||
return { | ||
TOTP: jest.fn().mockImplementation(() => { | ||
return { | ||
generate: jest.fn().mockReturnValue('123456'), | ||
}; | ||
}), | ||
}; | ||
}); | ||
|
||
describe('Execute TOTP node', () => { | ||
const tests: WorkflowTestData[] = [ | ||
{ | ||
description: 'Generate TOTP Token', | ||
input: { | ||
workflowData: Helpers.readJsonFileSync('nodes/Totp/test/Totp.workflow.test.json'), | ||
}, | ||
output: { | ||
nodeData: { | ||
TOTP: [[{ json: { token: '123456' } }]], // ignore secondsRemaining to prevent flakiness | ||
}, | ||
}, | ||
}, | ||
]; | ||
|
||
const nodeTypes = Helpers.setup(tests); | ||
|
||
for (const testData of tests) { | ||
// eslint-disable-next-line @typescript-eslint/no-loop-func | ||
test(testData.description, async () => { | ||
const { result } = await executeWorkflow(testData, nodeTypes); | ||
|
||
Helpers.getResultNodeData(result, testData).forEach(({ nodeName, resultData }) => { | ||
const expected = testData.output.nodeData[nodeName][0][0].json; | ||
const actual = resultData[0]?.[0].json; | ||
|
||
expect(actual?.token).toEqual(expected.token); | ||
}); | ||
|
||
expect(result.finished).toEqual(true); | ||
}); | ||
} | ||
}); |
41 changes: 41 additions & 0 deletions
41
packages/nodes-base/nodes/Totp/test/Totp.workflow.test.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"nodes": [ | ||
{ | ||
"parameters": {}, | ||
"id": "f2e03169-0e94-4a42-821b-3e8f67f449d7", | ||
"name": "When clicking \"Execute Workflow\"", | ||
"type": "n8n-nodes-base.manualTrigger", | ||
"typeVersion": 1, | ||
"position": [580, 320] | ||
}, | ||
{ | ||
"parameters": { | ||
"additionalOptions": {} | ||
}, | ||
"id": "831f657d-2724-4a25-bb94-cf37355654bb", | ||
"name": "TOTP", | ||
"type": "n8n-nodes-base.totp", | ||
"typeVersion": 1, | ||
"position": [800, 320], | ||
"credentials": { | ||
"totpApi": { | ||
"id": "1", | ||
"name": "TOTP account" | ||
} | ||
} | ||
} | ||
], | ||
"connections": { | ||
"When clicking \"Execute Workflow\"": { | ||
"main": [ | ||
[ | ||
{ | ||
"node": "TOTP", | ||
"type": "main", | ||
"index": 0 | ||
} | ||
] | ||
] | ||
} | ||
} | ||
} |
Oops, something went wrong.