n8n-nodes-microsoft-sso-form 0.1.0 → 0.2.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 CHANGED
@@ -12,6 +12,8 @@ Form trigger for n8n protected by Microsoft Entra ID sign-in (OIDC auth code + P
12
12
  ## n8n setup
13
13
  Create a **Microsoft SSO Form API** credential (tenant GUID, client ID/secret, 32+ char session secret). Set `WEBHOOK_URL` to your public HTTPS URL. Add the node, configure fields and Access Control (domain / group object IDs).
14
14
 
15
+ Password reset: with **Enable Password Reset** on, the signed-in page shows the user's email, a "Reset password" link (new + confirm password), and Sign out. Saving outputs `{ action: 'passwordReset', newPassword, submittedAt, user }` to the next node. The password is plain text in the execution data, so apply it immediately (e.g. Graph `PATCH /users/{id}` with `passwordProfile`) and consider disabling "Save successful executions" for that workflow.
16
+
15
17
  Output: submitted fields plus `submittedAt` and `user` `{oid,name,email,tenantId,groups}`.
16
18
 
17
19
  ## Install
@@ -139,14 +139,14 @@ class MicrosoftSsoForm {
139
139
  httpMethod: 'GET',
140
140
  responseMode: 'onReceived',
141
141
  path: '={{$parameter["path"]}}',
142
- isFullPath: false,
142
+ isFullPath: true,
143
143
  },
144
144
  {
145
145
  name: 'default',
146
146
  httpMethod: 'POST',
147
147
  responseMode: 'onReceived',
148
148
  path: '={{$parameter["path"]}}',
149
- isFullPath: false,
149
+ isFullPath: true,
150
150
  },
151
151
  ],
152
152
  properties: [
@@ -456,30 +456,72 @@ class MicrosoftSsoForm {
456
456
  return redirect(authUrl, [cookie(LOGIN_COOKIE, loginCookie, cookiePath, 600)]);
457
457
  }
458
458
  const fields = this.getNodeParameter('formFields.values', []) ?? [];
459
- // ---- GET form: render ----
459
+ const resetEnabled = this.getNodeParameter('enablePasswordReset', true);
460
+ const minLen = this.getNodeParameter('minPasswordLength', 8);
461
+ const blank = (f) => !f.fieldLabel && !f.fieldName && f.fieldType !== 'html';
462
+ const header = (links) => `<div class="who"><strong>Signed in as ${esc(session.email)}</strong>${session.name ? ` (${esc(session.name)})` : ''}<br>${links}</div>`;
463
+ // ---- GET: sign out / reset page / form ----
460
464
  if (req.method === 'GET') {
465
+ if (q.action === 'logout')
466
+ return redirect(formUrl, [cookie(SESSION_COOKIE, '', cookiePath, 0)]);
467
+ if (resetEnabled && q.action === 'reset') {
468
+ return send(200, page('Reset password', `<h1>Reset password</h1>${header(`<a href="${esc(formUrl)}">Back</a> · <a href="${esc(formUrl)}?action=logout">Sign out</a>`)}
469
+ <form method="POST" action="${esc(formUrl)}">
470
+ <input type="hidden" name="_csrf" value="${esc(session.csrf)}">
471
+ <input type="hidden" name="_action" value="passwordReset">
472
+ <label>New password *</label><input type="password" name="newPassword" minlength="${minLen}" autocomplete="new-password" required>
473
+ <label>Confirm password *</label><input type="password" name="confirmPassword" minlength="${minLen}" autocomplete="new-password" required>
474
+ <button type="submit">Save</button></form>`));
475
+ }
461
476
  const title = this.getNodeParameter('formTitle', '');
462
477
  const desc = this.getNodeParameter('formDescription', '');
463
478
  const button = this.getNodeParameter('buttonLabel', 'Submit');
464
479
  const hasFile = fields.some((f) => f.fieldType === 'file');
465
- return send(200, page(title, `<h1>${esc(title)}</h1><div class="who">Signed in as ${esc(session.name)} (${esc(session.email)})</div>
466
- <p>${esc(desc)}</p>
467
- <form method="POST" action="${esc(formUrl)}"${hasFile ? ' enctype="multipart/form-data"' : ''}>
480
+ const visible = fields.map((f, i) => (blank(f) ? '' : renderField(f, i))).join('\n');
481
+ const links = (resetEnabled ? `<a href="${esc(formUrl)}?action=reset">Reset password</a> · ` : '') +
482
+ `<a href="${esc(formUrl)}?action=logout">Sign out</a>`;
483
+ const formHtml = visible.trim()
484
+ ? `<form method="POST" action="${esc(formUrl)}"${hasFile ? ' enctype="multipart/form-data"' : ''}>
468
485
  <input type="hidden" name="_csrf" value="${esc(session.csrf)}">
469
- ${fields.map(renderField).join('\n')}
470
- <button type="submit">${esc(button)}</button></form>`));
486
+ ${visible}
487
+ <button type="submit">${esc(button)}</button></form>`
488
+ : '';
489
+ return send(200, page(title, `<h1>${esc(title)}</h1>${header(links)}<p>${esc(desc)}</p>${formHtml}`));
471
490
  }
472
491
  // ---- POST: submit ----
473
492
  const body = (req.body ?? {});
474
493
  const data = (body.data && typeof body.data === 'object' ? body.data : body);
475
494
  if (data._csrf !== session.csrf)
476
495
  return denied('Invalid form token. Reload the form and try again.');
496
+ const userInfo = {
497
+ oid: session.oid,
498
+ name: session.name,
499
+ email: session.email,
500
+ tenantId: session.tenantId,
501
+ groups: session.groups,
502
+ };
503
+ if (data._action === 'passwordReset') {
504
+ if (!resetEnabled)
505
+ return denied('Password reset is not enabled.');
506
+ const newPassword = String(data.newPassword ?? '');
507
+ if (newPassword.length < minLen)
508
+ return denied(`Password must be at least ${minLen} characters.`);
509
+ if (newPassword !== String(data.confirmPassword ?? ''))
510
+ return denied('Passwords do not match.');
511
+ send(200, page('Password saved', `<h1>Password saved</h1><p>Your password reset request was submitted.</p>`));
512
+ return {
513
+ noWebhookResponse: true,
514
+ workflowData: [
515
+ [{ json: { action: 'passwordReset', newPassword, submittedAt: new Date().toISOString(), user: userInfo } }],
516
+ ],
517
+ };
518
+ }
477
519
  const json = {};
478
520
  const binary = {};
479
521
  const reqFiles = (req.files ?? body.files ?? {});
480
522
  for (let i = 0; i < fields.length; i++) {
481
523
  const f = fields[i];
482
- if (f.fieldType === 'html')
524
+ if (f.fieldType === 'html' || blank(f))
483
525
  continue;
484
526
  const key = inputName(i);
485
527
  const outKey = outputKey(f, i);
@@ -505,13 +547,8 @@ ${fields.map(renderField).join('\n')}
505
547
  json[outKey] = value ?? null;
506
548
  }
507
549
  json.submittedAt = new Date().toISOString();
508
- json.user = {
509
- oid: session.oid,
510
- name: session.name,
511
- email: session.email,
512
- tenantId: session.tenantId,
513
- groups: session.groups,
514
- };
550
+ json.action = 'form';
551
+ json.user = userInfo;
515
552
  const message = this.getNodeParameter('completionMessage', '');
516
553
  send(200, page('Submitted', `<h1>Thank you</h1><p>${esc(message)}</p>`));
517
554
  const item = { json };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-microsoft-sso-form",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "n8n form trigger node protected by Microsoft Entra ID single sign-on, with optional group restriction",
5
5
  "license": "MIT",
6
6
  "keywords": ["n8n-community-node-package", "microsoft", "entra", "sso", "form"],