n8n-nodes-authentication 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/nodes/SomepharmAuthentication/SomepharmAuthentication.node.js +72 -0
- package/dist/nodes/SomepharmAuthentication/authentication.js +48 -0
- package/dist/nodes/SomepharmAuthentication/authentication.svg +1 -0
- package/dist/nodes/SomepharmAuthentication/connection.js +21 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Somepharm Authentication
|
|
2
|
+
|
|
3
|
+
Private self-hosted n8n package: `n8n-nodes-authentication`, version 1.0.0.
|
|
4
|
+
Replaces the supplied SQL Authentication check and If Authentication nodes.
|
|
5
|
+
The default canvas name is `Authentication`, so an authorization expression such
|
|
6
|
+
as `$('Authentication').item.json.REF_USER` can reference it directly.
|
|
7
|
+
|
|
8
|
+
## Settings
|
|
9
|
+
|
|
10
|
+
Select your existing **Prod** Microsoft SQL credential. Its Database MUST be the
|
|
11
|
+
application database containing `dbo.DECRYPTBYPASSPHRASE_Somepharm`. The user
|
|
12
|
+
lookup is explicitly against `DROITSMK_LA.dbo.T_USERS`.
|
|
13
|
+
|
|
14
|
+
| Setting | Default expression/value |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| Timestamp | `{{ $json.formQueryParameters?.randomValue ?? "" }}` |
|
|
17
|
+
| Encrypted Value | `{{ $json.formQueryParameters?.crypt ?? "" }}` |
|
|
18
|
+
| AD Username | `{{ $json.headers?.["x-forwarded-preferred-username"] ?? "" }}` |
|
|
19
|
+
| Maximum Age | 60 minutes |
|
|
20
|
+
|
|
21
|
+
Place this node immediately after the form trigger, or change the expressions
|
|
22
|
+
to refer to the relevant upstream node. Missing fields evaluate to an empty
|
|
23
|
+
string and are rejected. Custom expressions that themselves throw errors stop
|
|
24
|
+
execution. The account needs access to the decryption function and user table.
|
|
25
|
+
The existing database function is required; this package does not create it or
|
|
26
|
+
contain its passphrase.
|
|
27
|
+
|
|
28
|
+
## Outputs and wiring
|
|
29
|
+
|
|
30
|
+
- Output 0 **Authenticated**: `Auth_Result: 1`, `authenticated: true`, and REF_USER.
|
|
31
|
+
- Output 1 **Rejected**: `Auth_Result: 0`, `authenticated: false`, and `REF_USER: ""`.
|
|
32
|
+
|
|
33
|
+
IMPORTANT: the original IF true output meant rejection. Connect the old IF false
|
|
34
|
+
branch to Authenticated, and the old IF true branch to Rejected.
|
|
35
|
+
Connect Authenticated to Somepharm Authorization; its User Reference can be
|
|
36
|
+
`{{ $json.REF_USER }}`. Connect Rejected to your authentication error response.
|
|
37
|
+
Incoming JSON and binary data are retained, with item pairing preserved. Existing
|
|
38
|
+
REF_USER, Auth_Result and authenticated fields are overwritten. Other incoming
|
|
39
|
+
fields, including any stale authorization fields, are retained: perform a new
|
|
40
|
+
authorization check before accessing protected operations.
|
|
41
|
+
|
|
42
|
+
Keep On Error = Stop Workflow and Always Output Data = off. Database outages,
|
|
43
|
+
SQL errors, bad node settings and unexpected SQL results stop the execution;
|
|
44
|
+
they are not successful logins and are not sent down the Rejected output.
|
|
45
|
+
|
|
46
|
+
## Authentication rules
|
|
47
|
+
|
|
48
|
+
1. randomValue is an integer UNIX timestamp in seconds, between 0 and 2147483647,
|
|
49
|
+
matching the original SQL int constraint (through January 2038).
|
|
50
|
+
2. crypt is passed as a SQL parameter to the existing decryption function.
|
|
51
|
+
3. Decrypted text must convert to the same integer timestamp.
|
|
52
|
+
4. Timestamp is within the inclusive window [SQL UTC now minus maximum age, now].
|
|
53
|
+
Future timestamps are rejected. SQL Server supplies the time, not the browser.
|
|
54
|
+
5. AD_USER matches exactly one database row, with a REF_USER length of 4–50.
|
|
55
|
+
|
|
56
|
+
Changes from the original: values are bound parameters; malformed inputs reject
|
|
57
|
+
without SQL; TRY_CONVERT rejects nonnumeric decrypted text; duplicate AD_USER
|
|
58
|
+
rows reject rather than choosing an arbitrary account. The username limit is
|
|
59
|
+
256 characters and crypt limit is 16384 characters. The code does not impose a
|
|
60
|
+
Base64 format because the supplied custom decryption function is not available.
|
|
61
|
+
If that function throws for malformed tokens, execution stops.
|
|
62
|
+
|
|
63
|
+
The header must be set/overwritten by a trusted authentication proxy. Prevent
|
|
64
|
+
clients from reaching this endpoint directly and supplying their own identity
|
|
65
|
+
header. This token proves a timestamp, not its binding to a particular user;
|
|
66
|
+
identity relies on the proxy. This preserves the provided scheme and does not
|
|
67
|
+
add replay prevention or single-use tokens.
|
|
68
|
+
|
|
69
|
+
## Install locally
|
|
70
|
+
|
|
71
|
+
Extract the ZIP and copy its TGZ to the n8n host. Run as the n8n OS user:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
mkdir -p ~/.n8n/nodes
|
|
75
|
+
cd ~/.n8n/nodes
|
|
76
|
+
npm install /path/to/n8n-nodes-authentication-1.0.0.tgz --omit=dev
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Restart n8n. Install consistently for all main/worker instances and use persistent
|
|
80
|
+
storage for container deployments. Node.js 20+ and npm registry access are needed.
|
|
81
|
+
Meet your installed n8n version's Node.js requirement as well.
|
|
82
|
+
|
|
83
|
+
## Publish, then install through the UI
|
|
84
|
+
|
|
85
|
+
This package has NOT been published. The npm name's availability has not been
|
|
86
|
+
checked. From the extracted source folder, after choosing to publish publicly:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npm login
|
|
90
|
+
npm pkg delete private
|
|
91
|
+
npm ci
|
|
92
|
+
npm test
|
|
93
|
+
npm publish --dry-run
|
|
94
|
+
npm publish --access public
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Publishing exposes the code and database/function names. No passwords are
|
|
98
|
+
included. The example credential is a reference only and is excluded from the
|
|
99
|
+
npm tarball. Public npm publication requires your own account and authentication.
|
|
100
|
+
If another publisher owns the name, choose an available name and update the
|
|
101
|
+
example JSON node type prefix to match.
|
|
102
|
+
|
|
103
|
+
After publication, use n8n Settings > Community Nodes > Install and enter
|
|
104
|
+
`n8n-nodes-authentication@1.0.0`. Use an Owner/Admin account with community nodes
|
|
105
|
+
enabled. UI installation does not accept the ZIP as a workflow import.
|
|
106
|
+
The included examples/replacement-node.json can be pasted onto the canvas only
|
|
107
|
+
after the custom package is installed. Select your Prod credential if needed.
|
|
108
|
+
|
|
109
|
+
## Validation and sources
|
|
110
|
+
|
|
111
|
+
Run `npm ci` and `npm test`. Nine tests use mocked SQL responses and real
|
|
112
|
+
n8n-workflow/mssql modules. They cover input validation, routing, pairing, bound
|
|
113
|
+
parameters, connection failures, malformed responses and cleanup. They do not
|
|
114
|
+
execute T-SQL or the private decryption function. Live n8n loading and SQL Server
|
|
115
|
+
integration were not available. Before production, verify valid/expired/future
|
|
116
|
+
and tampered tokens, unknown/duplicate users, and exact time boundaries in Dev.
|
|
117
|
+
|
|
118
|
+
- https://learn.microsoft.com/en-us/sql/t-sql/functions/try-convert-transact-sql
|
|
119
|
+
- https://docs.n8n.io/integrations/community-nodes/installation-and-management/gui-installation
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const sql = require('mssql');
|
|
3
|
+
const { NodeOperationError } = require('n8n-workflow');
|
|
4
|
+
const { connectionConfig } = require('./connection');
|
|
5
|
+
const { validateInput, checkAuthentication } = require('./authentication');
|
|
6
|
+
class SomepharmAuthentication {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: 'Somepharm Authentication', name: 'somepharmAuthentication',
|
|
10
|
+
icon: 'file:authentication.svg', group: ['transform'], version: 1,
|
|
11
|
+
description: 'Validate an encrypted timestamp and trusted proxy username using SQL Server',
|
|
12
|
+
defaults: { name: 'Authentication', color: '#005baa' },
|
|
13
|
+
inputs: ['main'], outputs: ['main', 'main'], outputNames: ['Authenticated', 'Rejected'],
|
|
14
|
+
credentials: [{ name: 'microsoftSql', required: true }],
|
|
15
|
+
properties: [
|
|
16
|
+
{ displayName: 'Timestamp (randomValue)', name: 'timestamp', type: 'string',
|
|
17
|
+
default: '={{ $json.formQueryParameters?.randomValue ?? "" }}',
|
|
18
|
+
description: 'UNIX timestamp in seconds; missing or invalid values are rejected' },
|
|
19
|
+
{ displayName: 'Encrypted Value (crypt)', name: 'crypt', type: 'string',
|
|
20
|
+
default: '={{ $json.formQueryParameters?.crypt ?? "" }}' },
|
|
21
|
+
{ displayName: 'AD Username', name: 'username', type: 'string',
|
|
22
|
+
default: '={{ $json.headers?.["x-forwarded-preferred-username"] ?? "" }}',
|
|
23
|
+
description: 'Use only a username set by your trusted authentication proxy' },
|
|
24
|
+
{ displayName: 'Maximum Age (Minutes)', name: 'maxAgeMinutes', type: 'number', default: 60,
|
|
25
|
+
typeOptions: { minValue: 1, maxValue: 1440, numberPrecision: 0 },
|
|
26
|
+
description: 'Inclusive validity window checked against SQL Server UTC time; future timestamps are rejected' },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async execute() {
|
|
31
|
+
const items = this.getInputData();
|
|
32
|
+
const outputs = [[], []];
|
|
33
|
+
let pool;
|
|
34
|
+
let itemIndex = 0;
|
|
35
|
+
try {
|
|
36
|
+
for (itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
37
|
+
const maxAgeMinutes = Number(this.getNodeParameter('maxAgeMinutes', itemIndex));
|
|
38
|
+
if (!Number.isInteger(maxAgeMinutes) || maxAgeMinutes < 1 || maxAgeMinutes > 1440) {
|
|
39
|
+
throw new Error('Maximum Age must be an integer between 1 and 1440 minutes');
|
|
40
|
+
}
|
|
41
|
+
const input = validateInput(Object.fromEntries(['timestamp', 'crypt', 'username']
|
|
42
|
+
.map(name => [name, this.getNodeParameter(name, itemIndex)])));
|
|
43
|
+
let decision = { REF_USER: '', Auth_Result: 0 };
|
|
44
|
+
if (input) {
|
|
45
|
+
if (!pool) {
|
|
46
|
+
const credentials = await this.getCredentials('microsoftSql');
|
|
47
|
+
pool = new sql.ConnectionPool(connectionConfig(credentials));
|
|
48
|
+
pool.on('error', () => {});
|
|
49
|
+
await pool.connect();
|
|
50
|
+
}
|
|
51
|
+
decision = await checkAuthentication(pool, sql, input, maxAgeMinutes);
|
|
52
|
+
}
|
|
53
|
+
const output = {
|
|
54
|
+
json: { ...items[itemIndex].json, REF_USER: decision.REF_USER,
|
|
55
|
+
Auth_Result: decision.Auth_Result, authenticated: decision.Auth_Result === 1 },
|
|
56
|
+
pairedItem: { item: itemIndex },
|
|
57
|
+
};
|
|
58
|
+
if (items[itemIndex].binary) output.binary = items[itemIndex].binary;
|
|
59
|
+
outputs[decision.Auth_Result === 1 ? 0 : 1].push(output);
|
|
60
|
+
}
|
|
61
|
+
return outputs;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Do not include raw SQL driver error text: it may contain token or username values.
|
|
64
|
+
throw new NodeOperationError(this.getNode(),
|
|
65
|
+
'Authentication could not be completed. Check node configuration, SQL credentials, connectivity and the decryption function.',
|
|
66
|
+
{ itemIndex });
|
|
67
|
+
} finally {
|
|
68
|
+
if (pool) await pool.close().catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
module.exports = { SomepharmAuthentication };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const AUTHENTICATION_SQL = `
|
|
3
|
+
DECLARE @Decrypted varchar(max);
|
|
4
|
+
SELECT @Decrypted = [dbo].[DECRYPTBYPASSPHRASE_Somepharm](@Crypt);
|
|
5
|
+
DECLARE @TimestampUtc datetime2 = DATEADD(SECOND, @Timestamp, CAST('19700101' AS datetime2));
|
|
6
|
+
DECLARE @NowUtc datetime2 = SYSUTCDATETIME();
|
|
7
|
+
DECLARE @UserCount int;
|
|
8
|
+
DECLARE @RefUser varchar(max);
|
|
9
|
+
SELECT @UserCount = COUNT(*), @RefUser = MIN(CONVERT(varchar(max), U.REF_USER))
|
|
10
|
+
FROM DROITSMK_LA.dbo.T_USERS AS U
|
|
11
|
+
WHERE U.AD_USER = @Username;
|
|
12
|
+
DECLARE @Result smallint = 0;
|
|
13
|
+
IF TRY_CONVERT(int, @Decrypted) = @Timestamp
|
|
14
|
+
AND @TimestampUtc >= DATEADD(MINUTE, -@MaxAgeMinutes, @NowUtc)
|
|
15
|
+
AND @TimestampUtc <= @NowUtc
|
|
16
|
+
AND @UserCount = 1
|
|
17
|
+
AND LEN(@RefUser) > 3 AND LEN(@RefUser) <= 50
|
|
18
|
+
BEGIN
|
|
19
|
+
SET @Result = 1;
|
|
20
|
+
END;
|
|
21
|
+
SELECT @Result AS Auth_Result,
|
|
22
|
+
CASE WHEN @Result = 1 THEN @RefUser ELSE '' END AS REF_USER;`;
|
|
23
|
+
|
|
24
|
+
function validateInput(raw) {
|
|
25
|
+
const { timestamp, crypt, username } = raw;
|
|
26
|
+
if (!['string', 'number'].includes(typeof timestamp) || !/^\d+$/.test(String(timestamp))) return null;
|
|
27
|
+
const number = Number(timestamp);
|
|
28
|
+
if (!Number.isSafeInteger(number) || number < 0 || number > 2147483647) return null;
|
|
29
|
+
if (typeof crypt !== 'string' || !crypt.trim() || crypt.length > 16384 || /[\x00-\x1f\x7f]/.test(crypt)) return null;
|
|
30
|
+
if (typeof username !== 'string' || !username.trim() || username.length > 256 || /[\x00-\x1f\x7f]/.test(username)) return null;
|
|
31
|
+
return { timestamp: number, crypt, username };
|
|
32
|
+
}
|
|
33
|
+
async function checkAuthentication(pool, sql, input, maxAgeMinutes) {
|
|
34
|
+
const request = pool.request();
|
|
35
|
+
request.input('Timestamp', sql.Int, input.timestamp);
|
|
36
|
+
request.input('Crypt', sql.VarChar(sql.MAX), input.crypt);
|
|
37
|
+
request.input('Username', sql.NVarChar(256), input.username);
|
|
38
|
+
request.input('MaxAgeMinutes', sql.Int, maxAgeMinutes);
|
|
39
|
+
const result = await request.query(AUTHENTICATION_SQL);
|
|
40
|
+
const rows = result.recordset;
|
|
41
|
+
if (!Array.isArray(rows) || rows.length !== 1) throw new Error('Unexpected authentication response');
|
|
42
|
+
const row = rows[0];
|
|
43
|
+
if (![0, 1].includes(row.Auth_Result) || typeof row.REF_USER !== 'string' ||
|
|
44
|
+
(row.Auth_Result === 1 && (row.REF_USER.trimEnd().length <= 3 || row.REF_USER.length > 50)) ||
|
|
45
|
+
(row.Auth_Result === 0 && row.REF_USER !== '')) throw new Error('Unexpected authentication response');
|
|
46
|
+
return row;
|
|
47
|
+
}
|
|
48
|
+
module.exports = { AUTHENTICATION_SQL, validateInput, checkAuthentication };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#005baa" d="M32 3 57 13v18c0 15-13 25-25 30C20 56 7 46 7 31V13z"/><path fill="none" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" d="m20 31 8 8 17-18"/></svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
function connectionConfig(c) {
|
|
3
|
+
const config = {
|
|
4
|
+
server: c.server,
|
|
5
|
+
database: c.database || 'master',
|
|
6
|
+
user: c.user,
|
|
7
|
+
password: c.password,
|
|
8
|
+
port: c.port ?? 1433,
|
|
9
|
+
connectionTimeout: c.connectTimeout ?? 15000,
|
|
10
|
+
requestTimeout: c.requestTimeout ?? 15000,
|
|
11
|
+
options: {
|
|
12
|
+
encrypt: c.tls ?? true,
|
|
13
|
+
trustServerCertificate: c.allowUnauthorizedCerts ?? false,
|
|
14
|
+
tdsVersion: c.tdsVersion || '7_4',
|
|
15
|
+
},
|
|
16
|
+
pool: { min: 0, max: 1 },
|
|
17
|
+
};
|
|
18
|
+
if (c.domain) config.domain = c.domain;
|
|
19
|
+
return config;
|
|
20
|
+
}
|
|
21
|
+
module.exports = { connectionConfig };
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-authentication",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Somepharm SQL Server authentication with two outputs",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"n8n-community-node-package"
|
|
8
|
+
],
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/*.test.js"
|
|
18
|
+
},
|
|
19
|
+
"n8n": {
|
|
20
|
+
"n8nNodesApiVersion": 1,
|
|
21
|
+
"nodes": [
|
|
22
|
+
"dist/nodes/SomepharmAuthentication/SomepharmAuthentication.node.js"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"mssql": "12.7.2"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"n8n-workflow": "*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"n8n-workflow": "2.16.0"
|
|
33
|
+
}
|
|
34
|
+
}
|