Skip to content

Conversation

@BGZStephen
Copy link
Contributor

Summary

This PR creates a new POST API endpoint /rest/users/:id/invite-link to support generating signed invite links with expirations to provide a tamper proof mechanism for inviting and accepting invites

Related Linear tickets, Github issues, and Community forum posts

PAY-4390

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

@n8n-assistant n8n-assistant bot added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Jan 6, 2026
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

@codecov
Copy link

codecov bot commented Jan 6, 2026

Codecov Report

❌ Patch coverage is 92.85714% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/cli/src/controllers/users.controller.ts 92.85% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@blacksmith-sh

This comment has been minimized.

@currents-bot
Copy link

currents-bot bot commented Jan 6, 2026

E2E Tests: n8n tests passed after 9m 30.8s

🟢 620 · 🔴 0 · ⚪️ 27 · 🟣 4

View Run Details

Run Details

  • Project: n8n

  • Groups: 2

  • Framework: Playwright

  • Run Status: Passed

  • Commit: 47327c6

  • Spec files: 142

  • Overall tests: 647

  • Duration: 9m 30.8s

  • Parallelization: 16

Groups

GroupId Results Spec Files Progress
multi-main:e2e:isolated 🟢 57 · 🔴 0 · ⚪️ 0 9 / 9
multi-main:e2e 🟢 563 · 🔴 0 · ⚪️ 27 · 🟣 4 133 / 133


This message was posted automatically by currents.dev | Integration Settings

@BGZStephen BGZStephen requested review from a team and geemanjs and removed request for a team January 6, 2026 15:33
Copy link
Contributor

@geemanjs geemanjs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This a clever solution to that bug. I quite like it!

  1. Please tag me on the other side of the PR where we validate and check the token 😄...

From a security point of view I have some more questions:

  1. Should it be possible to revoke an invitation? This feels like something we should be able to support
  2. Is this safe from a replay attack? i.e. someone reusing the same token multiple times? JWT's are stateless after-all.

If we wanted some added protection (this prevents both above) an option might be to create a unique inviteId, drop that into the db with a state pending and put it on the JWT also.

| id         | state    | createdAt | lastUpdatedAt | 
| <inviteId> | pending  |           |               |
| <inviteId> | complete |           |               |
| <inviteId> | revoked  |           |               |

When they signup we extract the inviteId from the JWT and check it's state. If we want to revoke the invite we can manage that on the inviteId table. Alternatively, we can delete the inviteId from the table once it's complete/revoked - that probably scales better (and we can extend to emit events if someone wants the extra information in a SIEM)

  1. Should we maybe pass the expiresIn through the body of the controller to let the user configure how long the link is valid for when they create the link. Default it to a sensible 7 days in an input box. Otherwise, I fear in 6 months time we'll get a request to reduce from 90d to something shorter and/or be able to configure it via an env var or something 👀

Copy link
Contributor

@guillaumejacquart guillaumejacquart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good ! 2 small comments. Also I'm wondering, we will replace the invitation controller route with this one eventually ? sending the invite by email as well (eventually) ?

'user:delete',
'user:list',
'user:resetPassword',
'user:generateInviteLink',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add this scope to all custom roles with user:create scope (the current role used for invitation) in a migration file ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me add this to the list of tickets, will do this in a follow up once the rest of the code is in place

const inviterId = req.user.id;
const inviteeId = req.params.id;

const targetUser = await this.userRepository.findOne({ where: { id: inviteeId } });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do all that in the users service ? It already has urlService dependency, so would only need the new jwtService dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I'll move this as part of the other tickets in this feature

@BGZStephen
Copy link
Contributor Author

This a clever solution to that bug. I quite like it!

  1. Please tag me on the other side of the PR where we validate and check the token 😄...

From a security point of view I have some more questions:

  1. Should it be possible to revoke an invitation? This feels like something we should be able to support
  2. Is this safe from a replay attack? i.e. someone reusing the same token multiple times? JWT's are stateless after-all.

If we wanted some added protection (this prevents both above) an option might be to create a unique inviteId, drop that into the db with a state pending and put it on the JWT also.

| id         | state    | createdAt | lastUpdatedAt | 
| <inviteId> | pending  |           |               |
| <inviteId> | complete |           |               |
| <inviteId> | revoked  |           |               |

When they signup we extract the inviteId from the JWT and check it's state. If we want to revoke the invite we can manage that on the inviteId table. Alternatively, we can delete the inviteId from the table once it's complete/revoked - that probably scales better (and we can extend to emit events if someone wants the extra information in a SIEM)

  1. Should we maybe pass the expiresIn through the body of the controller to let the user configure how long the link is valid for when they create the link. Default it to a sensible 7 days in an input box. Otherwise, I fear in 6 months time we'll get a request to reduce from 90d to something shorter and/or be able to configure it via an env var or something 👀

Should it be possible to revoke an invitation

Currently no, it's not functionality that exists at the moment, but might be nice to add in the future.

  1. Is this safe from a replay attack? i.e. someone reusing the same token multiple times? JWT's are stateless after-all.

Yes, as once the user has used this token and provided a password, the signup flow will no longer be accessible, we currently check if the user has completed the sign up flow to avoid this.

If we wanted some added protection (this prevents both above) an option might be to create a unique inviteId, drop that into the db with a state pending and put it on the JWT also.

We had preferred not adding or modifying the DB mostly due to the hassle of migrations, in an ideal world we would have a table for invites, but it's not strictly necessary at the moment

  1. Should we maybe pass the expiresIn through the body of the controller to let the user configure how long the link is valid for when they create the link. Default it to a sensible 7 days in an input box. Otherwise, I fear in 6 months time we'll get a request to reduce from 90d to something shorter and/or be able to configure it via an env var or something 👀

I think it's OK to just have a default (at the moment there is no expiry, so this moves us in the right direction) if there is appetite from a customer perspective, we can always add this later

@BGZStephen BGZStephen merged commit 7b74533 into master Jan 7, 2026
56 of 57 checks passed
@BGZStephen BGZStephen deleted the PAY-4390-invite-links-generated-as-jwt branch January 7, 2026 06:52
@n8n-assistant n8n-assistant bot mentioned this pull request Jan 12, 2026
@n8n-assistant
Copy link
Contributor

n8n-assistant bot commented Jan 12, 2026

Got released with [email protected]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team Released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants