strapi-plugin-magic-link-v5 5.5.8 → 6.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/CHANGELOG.md +740 -0
- package/README.md +662 -928
- package/dist/admin/LicenseGuard-BgLA9go3.mjs +495 -0
- package/dist/admin/LicenseGuard-ewCjpc17.js +513 -0
- package/dist/admin/de-B1FNErPH.mjs +673 -0
- package/dist/admin/de-e75OECdq.js +673 -0
- package/dist/admin/en-CABKGbxH.js +673 -0
- package/dist/admin/en-HqDOFBCc.mjs +673 -0
- package/dist/admin/es-DpWl0Fax.js +673 -0
- package/dist/admin/es-jb94jqPR.mjs +673 -0
- package/dist/admin/fr-BQhLPvYw.js +673 -0
- package/dist/admin/fr-BseH1wrr.mjs +673 -0
- package/dist/admin/index-6ot9j1cN.js +5268 -0
- package/dist/admin/index-B390rccl.js +622 -0
- package/dist/admin/index-BPGKtruf.mjs +5266 -0
- package/dist/admin/index-BaTIQtnA.js +3869 -0
- package/dist/admin/index-CmG-93yx.js +141 -0
- package/dist/admin/index-DXe_ge4v.mjs +620 -0
- package/dist/admin/index-Dv5PVT1E.mjs +3867 -0
- package/dist/admin/index-k240ls6p.mjs +142 -0
- package/dist/admin/index.js +3 -3
- package/dist/admin/index.mjs +1 -2
- package/dist/admin/pt-CPkaveti.js +673 -0
- package/dist/admin/pt-DhlSCJn2.mjs +673 -0
- package/dist/admin/useUnsavedChangesGuard-BVZ4Mg4E.js +225 -0
- package/dist/admin/useUnsavedChangesGuard-QnkiZTLP.mjs +224 -0
- package/dist/server/i18n/de.json +32 -1
- package/dist/server/i18n/en.json +32 -1
- package/dist/server/i18n/es.json +32 -1
- package/dist/server/i18n/fr.json +32 -1
- package/dist/server/i18n/pt.json +32 -1
- package/dist/server/index.js +7092 -5979
- package/dist/server/index.mjs +7114 -5979
- package/package.json +42 -41
- package/strapi-admin.js +3 -1
- package/strapi-server.js +3 -2
- package/dist/_chunks/LicenseGuard-CiyjFnrw.js +0 -54
- package/dist/_chunks/LicenseGuard-DJ5fq2UZ.mjs +0 -55
- package/dist/_chunks/de-BSOk5suQ.js +0 -454
- package/dist/_chunks/de-CwhYs3O3.mjs +0 -454
- package/dist/_chunks/en-DnvSpXvu.js +0 -454
- package/dist/_chunks/en-gXXgGDV1.mjs +0 -454
- package/dist/_chunks/es-B-tnwByt.js +0 -407
- package/dist/_chunks/es-DG5gW3RZ.mjs +0 -407
- package/dist/_chunks/fr-BKo8t7Fx.js +0 -407
- package/dist/_chunks/fr-BQn1OHOR.mjs +0 -407
- package/dist/_chunks/index-01GHNxSr.js +0 -307
- package/dist/_chunks/index-B9Ch8Jpe.mjs +0 -5066
- package/dist/_chunks/index-Bj-erY7g.mjs +0 -143
- package/dist/_chunks/index-CGxbhJfg.js +0 -142
- package/dist/_chunks/index-DF3Edydl.mjs +0 -330
- package/dist/_chunks/index-Dufv-daf.mjs +0 -305
- package/dist/_chunks/index-I72rBTkL.js +0 -332
- package/dist/_chunks/index-SCxv154G.mjs +0 -2904
- package/dist/_chunks/index-XYWLmmEs.js +0 -5068
- package/dist/_chunks/index-zzoovbM0.js +0 -2906
- package/dist/_chunks/pt-CjEkXBAz.js +0 -407
- package/dist/_chunks/pt-trziAOzT.mjs +0 -407
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,743 @@
|
|
|
1
|
+
# [6.0.0](https://github.com/Schero94/strapi-plugin-magic-link-v5/compare/v5.6.0...v6.0.0) (2026-11-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
* build!: drop unused dependencies, require Node 22 and harden CI and release ([05eabb9](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/05eabb96baf98a8cca292f561a6e687d9417aeb8))
|
|
5
|
+
* build(deps)!: declare the users-permissions peer dependency ([fde4798](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/fde4798c273a9aa8e4d3ddd1144af8846c2eaaf6)), closes [#peerdependencies](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/peerdependencies)
|
|
6
|
+
* feat(admin-api)!: enforce read/manage RBAC, guard impersonation and harden sessions, IP bans, limits and license ([3036538](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/30365384350723d2ef38035a34f0cc81a03f8b5f))
|
|
7
|
+
* feat(admin)!: split admin RBAC into read, manage and settings actions ([8a0a5f3](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/8a0a5f3249dd8b0dd56585394dcd64d285e18a6b))
|
|
8
|
+
* feat(auth)!: harden the login core and make Magic Link e-mail only ([a1f4b89](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/a1f4b89b348fd046796658db22e9b52771478ead))
|
|
9
|
+
* feat(mfa)!: namespace TOTP by principal, add enrolment and backup codes, move to otplib ([466319e](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/466319e332c0fdc597ef393ea16a9e434f57fa87))
|
|
10
|
+
* fix(admin-api)!: require a confirmation for reset-data and keep MFA settings ([258fc76](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/258fc76c746e6a2c5e43d37108fcd8127a4e571b))
|
|
11
|
+
* fix(admin)!: rebuild the Settings view with lock-out safe MFA modes and an unsaved-changes guard ([2942a75](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/2942a75d3b0749c435e5dad4d7e449ddc2449ea6))
|
|
12
|
+
* fix(security)!: close revocation path bypass and admin limiter/settings gaps ([f63d203](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/f63d203eeb4315f304516280b22615bcc589cfda))
|
|
13
|
+
* fix(security)!: close the open adversarial review findings ([e1a19b5](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/e1a19b5ae71c9bf1789c1e338fae2b5278b34766))
|
|
14
|
+
* fix(security)!: guard required MFA, hash the send-link limiter key, tighten settings.security ([720240f](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/720240fb30927f5c1df46fd75d6ec081936f9f3a))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **admin:** complete translations, formal German and safe template placeholders ([e3e13fd](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/e3e13fde1daa9c8d24c485994e0661ba4ed34c4b))
|
|
20
|
+
* **admin:** make the Tokens list keyboard operable, readable and responsive ([8315dd2](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/8315dd2baf39cf698ec04423c0eba868b47dc45a)), closes [#0284C7](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/0284C7) [#0369A1](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/0369A1) [#16A34A](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/16A34A) [15803D/#166534](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/166534)
|
|
21
|
+
* **admin:** mask license key, gate it by settings rights and localise the License page ([a65011f](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/a65011f621fcc653c771b57a0380d14cd23f8f59))
|
|
22
|
+
* **admin:** pass browser acceptance of the OTP, JWT session and IP ban tabs ([40b1205](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/40b1205cc280e9c7d88c6ebdba419005439ae35b)), closes [#0369A1](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/0369A1) [#9333EA](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/9333EA)
|
|
23
|
+
* **admin:** pass browser acceptance of the Settings and License pages ([782bde7](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/782bde71f12c80cb29fd92a73144748c9e246eea))
|
|
24
|
+
* **admin:** rebuild OTP, JWT session and IP ban tabs on the shared foundation ([e9c85ed](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/e9c85ed55367df7941e32e12772cd2e968a13df8))
|
|
25
|
+
* **admin:** rebuild the Tokens view on the shared foundation and fix token actions ([afcd4d6](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/afcd4d6f793a342c210a1125945b8dc8ac12223e))
|
|
26
|
+
* **admin:** show IPv4-mapped bans as IPv4 and drop a stale validator ([73add59](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/73add59c2eb6cd83dbb8f9d6fd2e98b9c67f888a))
|
|
27
|
+
* **admin:** stop the reload permission race and unstick the Tokens table keyboard ([b76ff5b](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/b76ff5b8f024f15caef6a7cdd3105e8dd23e6d42))
|
|
28
|
+
* **auth:** give every legacy JWT a unique jti so session revocation is per session ([2100678](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/2100678098d8f38faf4325f3854bb2069e90a22d))
|
|
29
|
+
* **auth:** run the JWT revocation check on Content API paths only ([2189a36](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/2189a3642e944d44d1a17a3ef55b450a1d45d10d))
|
|
30
|
+
* **errors:** keep the IP-ban and tokens.issue 403 details; final docs review ([b336280](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/b3362801714792f2eed9fc776c9c224283d827be))
|
|
31
|
+
* **i18n:** merge view translations, drop unused keys and enforce id coverage ([9b949a4](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/9b949a44bd36d27e8a8ac3e40a5fc65848d0bed6))
|
|
32
|
+
* **mail:** send Magic Mail login links as untracked transactional mail ([1b42439](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/1b424395f1d72c8fead3ce98a835f458823002b3))
|
|
33
|
+
* **mfa:** keep second-factor brute-force limits on when rate limiting is off ([5ba821e](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/5ba821ec5130f5558f77cbc1b63bd6b94ba464c7))
|
|
34
|
+
* **otp:** accept the admin status filter on GET /otp/codes and return stats ([132a149](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/132a14949d204995417780ad85d3c1749870fe40))
|
|
35
|
+
* **release:** pass the npm token as fallback to trusted publishing ([72ca270](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/72ca270201ae8ddd5ac83dc7c146515066eea217))
|
|
36
|
+
* **security:** strict GCM tags, refresh-family session records, drop dead OTP mailer ([d12d215](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/d12d215e32c84564e27e48d2b970a8a80209efaa))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
* rewrite README and project site for the e-mail-only 6.0.0 release ([4744ef6](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/4744ef6035c53cd4ab3305823918cba85195844f))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Features
|
|
45
|
+
|
|
46
|
+
* **admin:** add shared admin foundation for theme, i18n, dialogs and RBAC ([4e6527b](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/4e6527b3d1de774b30c507ca3534aa7e9c218350))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### BREAKING CHANGES
|
|
50
|
+
|
|
51
|
+
* POST /magic-link/reset-data needs plugin::magic-link.settings.security and no longer returns securitySettingsKept; changing from_name, from_email or response_email needs settings.security; switching mfa_mode to required with no enrolled user needs acknowledgeNoEnrolledUsers: true; max_login_attempts is no longer accepted or stored.
|
|
52
|
+
|
|
53
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
54
|
+
* changing OTP, TOTP-parameter and rate-limit settings now
|
|
55
|
+
needs plugin::magic-link.settings.security, and reset-data without that
|
|
56
|
+
permission keeps all security settings. Grant Settings > Security to roles
|
|
57
|
+
that should keep changing them.
|
|
58
|
+
|
|
59
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
60
|
+
* changing security settings (link URLs and paths,
|
|
61
|
+
templates, template providers, account creation, link/session lifetimes,
|
|
62
|
+
context lists, MFA/OTP switches) now requires
|
|
63
|
+
plugin::magic-link.settings.security in addition to settings.update; the
|
|
64
|
+
action is never granted automatically. Confirming an unconfirmed account
|
|
65
|
+
via Magic Link clears its password and ends its sessions. IPv6 IP bans now
|
|
66
|
+
cover the whole /64 network.
|
|
67
|
+
|
|
68
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
69
|
+
* hosts must have @strapi/plugin-users-permissions ^5.24.0
|
|
70
|
+
installed (it ships with every default Strapi 5 project).
|
|
71
|
+
|
|
72
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
73
|
+
* POST /magic-link/reset-data requires the body
|
|
74
|
+
{ "confirm": "RESET" }; MFA settings survive a reset unless
|
|
75
|
+
"resetSecurity": true is sent.
|
|
76
|
+
|
|
77
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
78
|
+
* documented 6.0.0 contract. Magic Link is e-mail only and
|
|
79
|
+
needs Node.js 22+ and Strapi 5.24+; see the upgrade guide in README.md and
|
|
80
|
+
CHANGELOG.md.
|
|
81
|
+
|
|
82
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
83
|
+
* Node.js 22 or newer is required, and the plugin needs
|
|
84
|
+
Strapi 5.24.0 or newer (users-permissions session manager).
|
|
85
|
+
|
|
86
|
+
Docs: npm trusted publishers (docs.npmjs.com/trusted-publishers),
|
|
87
|
+
GitHub Actions security hardening (pin to full SHA, least-privilege
|
|
88
|
+
GITHUB_TOKEN), Strapi 5.61.0 package.json engines, npm registry
|
|
89
|
+
metadata for qrcode and @paulmillr/qr; checked 2026-11-07.
|
|
90
|
+
|
|
91
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
92
|
+
* admin routes require plugin::magic-link.read / manage /
|
|
93
|
+
settings.read / settings.update (5.x `access` is migrated to read + manage
|
|
94
|
+
when the permission rows still exist). POST /magic-link/tokens returns the
|
|
95
|
+
plaintext token only for send_email:false and only with the new
|
|
96
|
+
plugin::magic-link.tokens.issue action, sends `ttl` top-level, fails with
|
|
97
|
+
503 when the mail fails; several unused admin routes were removed; GET
|
|
98
|
+
/tokens no longer returns hashes or securityScore; revoke-jwt needs
|
|
99
|
+
sessionId xor token; IPv4-mapped addresses are matched as IPv4; IP-ban
|
|
100
|
+
denials are formatted 403s with details.code; unknown settings keys are
|
|
101
|
+
dropped; license status masks the key for settings readers and no host
|
|
102
|
+
telemetry is sent. See CHANGELOG [Unreleased] for migration steps.
|
|
103
|
+
|
|
104
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
105
|
+
* MAGIC_LINK_ENCRYPTION_KEY and MAGIC_LINK_OTP_PEPPER are
|
|
106
|
+
required (>= 32 characters) on every host; rotate short keys with
|
|
107
|
+
MAGIC_LINK_ENCRYPTION_KEY_PREVIOUS. TOTP enrolments are namespaced by
|
|
108
|
+
principal and 5.x rows without provable ownership are disabled. Required
|
|
109
|
+
MFA answers /login with requiresTOTPSetup instead of 403. /otp/verify
|
|
110
|
+
returns the common login body, the unbound OTP flow is refused while MFA
|
|
111
|
+
is active, otp-code lost the sms type and phoneNumber, admin TOTP routes
|
|
112
|
+
need a current code for re-setup, disable and backup codes. Grant the
|
|
113
|
+
`totp` actions to the Authenticated role for self-service. See CHANGELOG.
|
|
114
|
+
|
|
115
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
116
|
+
* the Settings page no longer offers the login_path,
|
|
117
|
+
callback_url, max_login_attempts and Email Designer compatibility
|
|
118
|
+
controls (the server never used them), and saves only changed fields.
|
|
119
|
+
Choosing "Magic link + authenticator app" now selects the optional
|
|
120
|
+
policy; requiring TOTP for every user needs an explicit confirmation.
|
|
121
|
+
|
|
122
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
123
|
+
* WhatsApp and SMS delivery removed; send-link requires email and rejects phoneNumber, phone, via and non-email delivery with 400; whatsapp_* and sms_* settings are deleted and otp_type only accepts 'email'.
|
|
124
|
+
* reserved context keys (requiresOTP, otpVerified, requiresTOTP, totpVerified, mfaVerified, totpLogin, userId, userDocumentId, user, expires_at, expiresAt, ttl and JWT claim names) are rejected; public context follows context_whitelist/context_blacklist.
|
|
125
|
+
* e-mail templates use {{VARIABLE}} placeholders; legacy <%= VAR %> is converted, any other <% %> code is printed as text.
|
|
126
|
+
* login links follow frontend_url+frontend_login_path > confirmationUrl > API URL; link URL settings must be https (http only for localhost); token_length must be 22..128.
|
|
127
|
+
* login responses return a content-API sanitized user, drop expiry_formatted and the TOTP challenge userId, return jwtIssued:false when use_jwt_token is false, and use the Strapi error envelope.
|
|
128
|
+
* JWTs honour jwt_token_expires_in; in users-permissions refresh mode logins return refreshToken (or the refresh cookie) from the u-p session manager.
|
|
129
|
+
* users with an enabled authenticator always get the TOTP challenge; required MFA without an authenticator returns 403.
|
|
130
|
+
* auto-registration also requires users-permissions allow_register; new users start unconfirmed; TOTP-only login no longer confirms users.
|
|
131
|
+
* /api/magic-link/passwordless/* routes and the compatibility-check policy are removed; /<prefix>/passwordless/* is handled by the rewrite middleware.
|
|
132
|
+
|
|
133
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
134
|
+
* the plugin::magic-link.access action is replaced by
|
|
135
|
+
plugin::magic-link.read and plugin::magic-link.manage, and the License page
|
|
136
|
+
requires plugin::magic-link.settings.read. Re-grant Read (and Manage) to
|
|
137
|
+
every role that had "Access the Magic Link plugin"; see CHANGELOG.
|
|
138
|
+
|
|
139
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
140
|
+
|
|
141
|
+
## [Unreleased]
|
|
142
|
+
|
|
143
|
+
Next release: **6.0.0** (major). Magic Link becomes e-mail only and is
|
|
144
|
+
hardened throughout. Every breaking change below has migration steps; the
|
|
145
|
+
short version is the upgrade guide.
|
|
146
|
+
|
|
147
|
+
### Upgrade guide (5.x to 6.0.0)
|
|
148
|
+
|
|
149
|
+
1. Run Node.js 22 or newer and Strapi 5.24.0 or newer (5.33.3+ when refresh
|
|
150
|
+
mode should read the refresh token from the httpOnly cookie).
|
|
151
|
+
2. Set `MAGIC_LINK_ENCRYPTION_KEY` and `MAGIC_LINK_OTP_PEPPER` (32+ characters
|
|
152
|
+
each) on every host. If the old encryption key was shorter, or you used the
|
|
153
|
+
development fallback, also set `MAGIC_LINK_ENCRYPTION_KEY_PREVIOUS` for the
|
|
154
|
+
first start (see "Secrets are mandatory" below).
|
|
155
|
+
3. Remove WhatsApp/SMS fields from `send-link` calls and delete a leftover
|
|
156
|
+
`.whatsapp-auth` directory.
|
|
157
|
+
4. Review admin roles: `access` became `read`, `manage`, `settings.read`,
|
|
158
|
+
`settings.update`, `tokens.issue` and `settings.security` (roles with
|
|
159
|
+
`access` get Read and Manage automatically; the others are never granted
|
|
160
|
+
automatically).
|
|
161
|
+
5. Frontend: exchange tokens with `POST /api/magic-link/login { loginToken }`,
|
|
162
|
+
handle `requiresTOTPSetup` when MFA is required, use `expires_at` instead of
|
|
163
|
+
`expiry_formatted`, and read errors from `error.key`.
|
|
164
|
+
6. Replace JavaScript in e-mail templates with `{{PLACEHOLDERS}}`.
|
|
165
|
+
7. Use `https` for `frontend_url`, `confirmationUrl` and `callback_url`
|
|
166
|
+
(http only for localhost).
|
|
167
|
+
8. Grant the `totp` actions to the Authenticated role for authenticator
|
|
168
|
+
self-service.
|
|
169
|
+
9. Admin token API clients: send `ttl` (hours) as a top-level field.
|
|
170
|
+
|
|
171
|
+
### BREAKING CHANGES (requirements)
|
|
172
|
+
|
|
173
|
+
* **Node.js 22 or newer** (`engines.node` is `>=22.0.0 <=26.x.x`, the range
|
|
174
|
+
Strapi 5.61 declares). Node 18 and 20 are no longer supported.
|
|
175
|
+
* **Strapi 5.24.0 or newer** (`@strapi/strapi`, `@strapi/utils` and, new,
|
|
176
|
+
`@strapi/plugin-users-permissions` peers `^5.24.0`). 5.24.0 is the first release with the users-permissions session
|
|
177
|
+
manager that refresh-mode logins use.
|
|
178
|
+
* **E-mail only.** WhatsApp and SMS delivery are removed from the API, the
|
|
179
|
+
settings, the admin panel, the translations and the documentation (details
|
|
180
|
+
under "login core").
|
|
181
|
+
|
|
182
|
+
### BREAKING CHANGES (admin RBAC)
|
|
183
|
+
|
|
184
|
+
* **Admin RBAC split into read, manage and settings actions.** The single
|
|
185
|
+
`plugin::magic-link.access` action is replaced by:
|
|
186
|
+
* `plugin::magic-link.read`: view tokens, OTP codes, JWT sessions and IP bans (menu link and Tokens page)
|
|
187
|
+
* `plugin::magic-link.manage`: create, block, extend and delete tokens, revoke sessions, ban and unban IPs, delete OTP codes, cleanups
|
|
188
|
+
* `plugin::magic-link.settings.read`: view the Settings and License pages (License now needs this instead of `access`)
|
|
189
|
+
* `plugin::magic-link.settings.update`: save settings, reset rate limits, change the license
|
|
190
|
+
* `plugin::magic-link.tokens.issue` (new, *Tokens > Issue login links without
|
|
191
|
+
e-mail*): needed for `POST /magic-link/tokens` with `send_email: false`,
|
|
192
|
+
which hands a working login link for the user to the admin (impersonation)
|
|
193
|
+
|
|
194
|
+
The server enforces these actions on every admin route.
|
|
195
|
+
|
|
196
|
+
Migration: on startup, roles that still hold the 5.x `access` permission
|
|
197
|
+
get **Read** and **Manage** automatically and the old permission is removed;
|
|
198
|
+
`tokens.issue` is never granted automatically. If your roles show no Magic
|
|
199
|
+
Link permission after the upgrade, open every role that had "Access the
|
|
200
|
+
Magic Link plugin" in *Settings > Administration Panel > Roles* and grant
|
|
201
|
+
**Read** (and **Manage** for roles that changed tokens, sessions, IP bans or
|
|
202
|
+
OTP codes). Grant Settings **Read** to roles that used the License page and
|
|
203
|
+
**Issue login links** only to roles that may log in as end users. Super
|
|
204
|
+
Admin gets all actions automatically.
|
|
205
|
+
* **New `plugin::magic-link.settings.security` action** (*Settings > Security*).
|
|
206
|
+
Changing a setting that can redirect login links (and the token in them),
|
|
207
|
+
put the raw token into a template, or change who may log in and with which
|
|
208
|
+
factors now needs it in addition to `settings.update`: `confirmationUrl`,
|
|
209
|
+
`callback_url`, `frontend_url`, `frontend_login_path`, `login_path`,
|
|
210
|
+
`passwordlessCompatibility`, `message_html`, `message_text`, the Email
|
|
211
|
+
Designer / Magic Mail template fields, `enabled`, `createUserIfNotExists`,
|
|
212
|
+
`user_creation_strategy`, `allow_magic_links_on_public_registration`,
|
|
213
|
+
`verify_email`, `stays_valid`, `expire_period`, `token_length`,
|
|
214
|
+
`use_jwt_token`, `jwt_token_expires_in`, `context_whitelist`,
|
|
215
|
+
`context_blacklist`, `mfa_mode`, `mfa_require_totp`,
|
|
216
|
+
`totp_as_primary_auth`, `otp_enabled`, `otp_strict_binding`, and the code
|
|
217
|
+
and brute-force rules `otp_length`, `otp_expiry`, `otp_max_attempts`,
|
|
218
|
+
`otp_resend_cooldown`, `totp_algorithm`, `totp_digits`, `totp_period`,
|
|
219
|
+
`rate_limit_enabled`, `rate_limit_max_attempts`,
|
|
220
|
+
`rate_limit_window_minutes`, `rate_limit_login_*`, and the sender identity
|
|
221
|
+
`from_name`, `from_email` and `response_email`. Sending them unchanged is allowed. A refused save
|
|
222
|
+
answers 403 (`error.details.key: settings.security.forbidden`, `fields`)
|
|
223
|
+
and writes nothing. `reset-data` needs it as well (see "Follow-up fixes"
|
|
224
|
+
below).
|
|
225
|
+
Migration: the action is never granted automatically (Super Admin has
|
|
226
|
+
it). Grant **Settings > Security** to the roles that should keep changing
|
|
227
|
+
link targets, templates, account, MFA, code or rate-limit rules; roles
|
|
228
|
+
with only `settings.update` keep subject, welcome-mail, login-info and TOTP
|
|
229
|
+
issuer changes.
|
|
230
|
+
* **Optional `allowedLinkOrigins` plugin config.** When set
|
|
231
|
+
(`config/plugins.js`, e.g. `['https://app.example.com']`), a changed
|
|
232
|
+
`confirmationUrl`, `frontend_url` or `callback_url` outside these origins
|
|
233
|
+
is rejected with 400 (`settings.linkOrigin.notAllowed`), whatever the
|
|
234
|
+
admin's permissions.
|
|
235
|
+
|
|
236
|
+
### BREAKING CHANGES (admin API)
|
|
237
|
+
|
|
238
|
+
* **Admin-created tokens.** `POST /magic-link/tokens` is validated
|
|
239
|
+
(`email`, `ttl` in hours 1-720, `send_email`, `context` as object or JSON
|
|
240
|
+
string; unknown fields and reserved context keys return 400). It uses the
|
|
241
|
+
same renderer, link builder and providers (Magic Mail, Email Designer,
|
|
242
|
+
e-mail plugin) as the public send-link. With `send_email: true` the
|
|
243
|
+
response no longer contains the plaintext token; with `send_email: false`
|
|
244
|
+
it contains `token` and `loginUrl` once and requires
|
|
245
|
+
`plugin::magic-link.tokens.issue` (403 otherwise). A failed e-mail
|
|
246
|
+
deactivates the new token and returns 503 instead of a silent success.
|
|
247
|
+
Missing users are created only when `createUserIfNotExists` is on, and
|
|
248
|
+
start unconfirmed. Admin creation is limited to 60 per admin and 15
|
|
249
|
+
minutes and no longer uses the public per-IP/per-e-mail buckets. Each issue
|
|
250
|
+
is logged with the admin id and token id (no e-mail, no token).
|
|
251
|
+
Migration: send `ttl` as a top-level field (not in `context`); grant
|
|
252
|
+
`tokens.issue` to roles that need copyable links.
|
|
253
|
+
* **Token list and actions.** `GET /magic-link/tokens` no longer returns the
|
|
254
|
+
hash, salt, lookup prefix or `challenge`, and no `meta.securityScore`; it
|
|
255
|
+
returns the newest 1000 tokens, or a page with `?page=&pageSize=`
|
|
256
|
+
(`meta.pagination`). `extend` accepts `days` 1-365 (400 otherwise) and
|
|
257
|
+
refuses used single-use links. `activate` refuses used single-use links,
|
|
258
|
+
links that completed a second-factor challenge, expired links and links
|
|
259
|
+
last used from a banned IP.
|
|
260
|
+
* **Removed admin routes** (unused by the admin UI): `GET /security-score`,
|
|
261
|
+
`GET /validate-email`, `GET /user-by-email`, `POST /tokens/:id/resend`,
|
|
262
|
+
`POST /license/auto-create`, `POST /license/create`, `POST /license/ping`.
|
|
263
|
+
* **JWT sessions.** `POST /revoke-jwt` needs exactly one of `sessionId` or
|
|
264
|
+
`token`; a client-sent `userId` is ignored and the acting admin is recorded
|
|
265
|
+
(`revokedBy`). Refresh-mode sessions cannot be restored with
|
|
266
|
+
`/unrevoke-jwt` (the users-permissions session is deleted on revocation).
|
|
267
|
+
Plaintext JWTs stored by older releases in the session list are replaced
|
|
268
|
+
by their hash on startup; records are kept 7 days after expiry.
|
|
269
|
+
* **IP addresses.** IPv4-mapped IPv6 addresses (`::ffff:192.0.2.1`) are
|
|
270
|
+
stored and matched as IPv4, so an IPv4 ban also blocks dual-stack
|
|
271
|
+
requests. Rate limits key IPv6 clients by their /64 network. Banned or
|
|
272
|
+
unverifiable requests now get a formatted 403 `PolicyError` with
|
|
273
|
+
`error.details.code` `IP_ADDRESS_BLOCKED` or `IP_BAN_CHECK_UNAVAILABLE` (was a generic
|
|
274
|
+
"Policy Failed").
|
|
275
|
+
* **IPv6 bans cover the /64.** Bans now match with the same granularity as
|
|
276
|
+
the rate limits: IPv4 exactly, IPv6 per /64 network. Banning one IPv6
|
|
277
|
+
address blocks every address of its /64; unbanning any address of that
|
|
278
|
+
/64 removes all stored bans of it. Migration: review IPv6 entries in
|
|
279
|
+
*IP bans*; an entry now also blocks its neighbours in the same /64.
|
|
280
|
+
* **Settings API.** Only known settings keys are stored and returned; unknown
|
|
281
|
+
and legacy keys (`create_new_user`, `remember_me`, `token_lifetime`, ...)
|
|
282
|
+
are dropped on the next save. `context_whitelist`/`context_blacklist`
|
|
283
|
+
strings are stored as arrays. `reset-data` writes the same defaults as a
|
|
284
|
+
fresh install (API login URL, no `noreply@example.com`).
|
|
285
|
+
* **`POST /magic-link/reset-data` needs a confirmation.** The body must be
|
|
286
|
+
`{ "confirm": "RESET" }` (400 otherwise, nothing is deleted). The MFA
|
|
287
|
+
settings (`mfa_mode`, `mfa_require_totp`, `totp_as_primary_auth`,
|
|
288
|
+
`otp_enabled`) are kept unless `"resetSecurity": true` is sent, so a reset
|
|
289
|
+
never switches MFA off silently.
|
|
290
|
+
Migration: scripts that call the route send the confirmation body.
|
|
291
|
+
* **License privacy.** The plugin no longer sends hostname, IP address, OS
|
|
292
|
+
details, a device id or admin name/e-mail to the license server; only the
|
|
293
|
+
license key is sent (on save and for the License page, cached 10 minutes).
|
|
294
|
+
Set `license: { remoteLookup: false }` in the plugin config or
|
|
295
|
+
`MAGIC_LINK_LICENSE_LOOKUP=false` to disable all license-server calls.
|
|
296
|
+
`GET /license/status` returns `licenseKeyMasked` and `lookup`
|
|
297
|
+
(`ok`, `not-found`, `unavailable`, `disabled`, `none`); the full key and
|
|
298
|
+
holder name/e-mail are only returned to admins with `settings.update`.
|
|
299
|
+
`POST /license/store-key` validates the key format.
|
|
300
|
+
|
|
301
|
+
### Security and fixes (admin API)
|
|
302
|
+
|
|
303
|
+
* Session-list and IP-ban updates are serialised, so parallel revocations,
|
|
304
|
+
logins and bans no longer overwrite each other in one process.
|
|
305
|
+
* Saving settings keeps rate-limit counters unless a limit changed;
|
|
306
|
+
`/rate-limit/stats` reports real rejection counts. Optional shared limits
|
|
307
|
+
for clusters: `rateLimit: { redis: { storeClient } }` in the plugin config
|
|
308
|
+
(rate-limiter-flexible Redis limiter with an in-memory fallback).
|
|
309
|
+
* A startup warning appears when `server.proxy.koa` is on without
|
|
310
|
+
`server.proxy.maxIpsCount` (clients could spoof their IP).
|
|
311
|
+
* Bootstrap timers are cleared in `destroy()`, and failing background jobs
|
|
312
|
+
are logged instead of causing unhandled rejections.
|
|
313
|
+
* Admin logs no longer contain settings objects, e-mail addresses or tokens.
|
|
314
|
+
* The always-on code-check limiter (`verify`, 10 checks per account and
|
|
315
|
+
15 minutes) and the admin limiter are no longer cleared by
|
|
316
|
+
`POST /rate-limit/reset` or by changing the public rate-limit values.
|
|
317
|
+
Before, a role with `settings.update` could reset them in a loop and give
|
|
318
|
+
unlimited guesses at TOTP, backup and e-mail codes. The reset now clears
|
|
319
|
+
only the public limiters.
|
|
320
|
+
* Rate-limit admin endpoints no longer log or return limiter error details
|
|
321
|
+
(a Redis error can contain the connection URL).
|
|
322
|
+
* A JWT session ended by an account claim can no longer be restored with
|
|
323
|
+
`unrevoke-jwt` (400).
|
|
324
|
+
* `GET /otp/codes` accepts the `status` filter (`active`, `expired`, `used`)
|
|
325
|
+
that the admin OTP tab sends. Before, the strict query schema rejected it
|
|
326
|
+
with 400, so every status filter failed. The response also carries
|
|
327
|
+
`stats` (`total`, `active`, `expired`, `used` over all codes), so the stat
|
|
328
|
+
cards stay filled while a search or filter is active.
|
|
329
|
+
* `GET /jwt-sessions` adds `restoreBlock` per session (`expired`,
|
|
330
|
+
`refreshMode`, `accountClaim` or `null`), with the same checks as
|
|
331
|
+
`unrevoke-jwt`. Refresh-mode sessions cannot be restored because the
|
|
332
|
+
users-permissions session manager deletes the session on revocation.
|
|
333
|
+
* `PUT /settings` and `POST /reset-data` reject security changes without
|
|
334
|
+
`settings.security` with a `PolicyError` (403) that keeps `message` and
|
|
335
|
+
`details` (`key: 'settings.security.forbidden'`, `fields`). Strapi replaced
|
|
336
|
+
the previous `ForbiddenError` with a bare "Forbidden" and dropped the
|
|
337
|
+
details, so the admin could not say what was refused.
|
|
338
|
+
* The IP-ban policy (`IP_ADDRESS_BLOCKED`) and `POST /magic-link/tokens`
|
|
339
|
+
without `tokens.issue` (`tokens.issue.forbidden`) now throw a
|
|
340
|
+
`PolicyError` (403) as well. Strapi 5.61's authorize middleware wraps the
|
|
341
|
+
policies and the controller, so the previous `ForbiddenError` reached
|
|
342
|
+
clients as a bare "Forbidden" with empty `details` (seen on a 5.61 host:
|
|
343
|
+
a banned IP got `{"name":"ForbiddenError","message":"Forbidden","details":{}}`).
|
|
344
|
+
Clients that read `error.details.code` or `error.details.key` now get it;
|
|
345
|
+
`error.name` is `PolicyError`.
|
|
346
|
+
|
|
347
|
+
### BREAKING CHANGES (login core)
|
|
348
|
+
|
|
349
|
+
* **E-mail only: WhatsApp and SMS delivery removed.** `POST /api/magic-link/send-link`
|
|
350
|
+
now requires `email`. The fields `phoneNumber`, `phone`, `via` and
|
|
351
|
+
`delivery` values other than `"email"` are rejected with a 400
|
|
352
|
+
`ValidationError` ("Only e-mail delivery is supported ..."). The response no
|
|
353
|
+
longer contains `phoneNumber` and never `delivery: "whatsapp"`; the 503
|
|
354
|
+
`WhatsAppDeliveryError` is gone. Settings keys `whatsapp_*` and `sms_*` are
|
|
355
|
+
deleted on startup, and `otp_type` only accepts `"email"` (stored `sms` or
|
|
356
|
+
`totp` values are rewritten to `email`).
|
|
357
|
+
Migration: send e-mail addresses only. Operators who used the built-in
|
|
358
|
+
WhatsApp transport of releases up to 5.2.x should securely delete the
|
|
359
|
+
`.whatsapp-auth` directory in the Strapi project root.
|
|
360
|
+
* **Reserved context keys are rejected.** `context` on send-link may not
|
|
361
|
+
contain `requiresOTP`, `otpVerified`, `requiresTOTP`, `totpVerified`,
|
|
362
|
+
`mfaVerified`, `totpLogin`, `userId`, `userDocumentId`, `user`,
|
|
363
|
+
`expires_at`, `expiresAt`, `ttl` or JWT claim names (`id`, `iat`, `exp`,
|
|
364
|
+
`nbf`, `iss`, `sub`, `aud`, `jti`, `sessionId`, `type`), at most 20 keys.
|
|
365
|
+
Public context now passes the same `context_whitelist` /
|
|
366
|
+
`context_blacklist` policy as admin-created tokens. Second-factor state is
|
|
367
|
+
stored in the new private token attribute `challenge`; old tokens whose
|
|
368
|
+
context still carries such flags cannot complete a challenge (open the link
|
|
369
|
+
again to start a new one).
|
|
370
|
+
* **E-mail templates no longer execute code.** `message_html`,
|
|
371
|
+
`message_text` and `object` are rendered with a placeholder renderer:
|
|
372
|
+
`{{LOGIN_URL}}`, `{{URL}}`, `{{CODE}}`, `{{EXPIRY_TEXT}}`, `{{EXPIRES_AT}}`,
|
|
373
|
+
`{{EMAIL}}`, `{{USER}}` (values are HTML-escaped in HTML bodies). Legacy
|
|
374
|
+
`<%= VAR %>` placeholders still render and are converted on startup;
|
|
375
|
+
`<%= URL %>?loginToken=<%= CODE %>` becomes `{{LOGIN_URL}}`. Any other
|
|
376
|
+
`<% ... %>` block is printed as text. Unknown placeholders render empty
|
|
377
|
+
instead of failing the send.
|
|
378
|
+
Migration: replace JavaScript expressions in templates with placeholders.
|
|
379
|
+
* **Login links are built by one URL builder.** Precedence:
|
|
380
|
+
`frontend_url` + `frontend_login_path`, then `confirmationUrl`, then
|
|
381
|
+
`<server.url><api.rest.prefix>/magic-link/login`. Existing query strings
|
|
382
|
+
are kept. `confirmationUrl`, `frontend_url` and `callback_url` must be
|
|
383
|
+
`https` URLs (`http` only for localhost); invalid stored values are
|
|
384
|
+
ignored when building links and rejected when saving settings. The old
|
|
385
|
+
default `confirmationUrl` (the bare server URL) is cleared on startup.
|
|
386
|
+
* **`token_length` is 22 to 128** (132+ bits). Stored smaller values are
|
|
387
|
+
raised to 22. Legacy unhashed login tokens are deactivated and no longer
|
|
388
|
+
accepted.
|
|
389
|
+
* **Login responses changed.** The user object is sanitized with Strapi's
|
|
390
|
+
content-API sanitizer (private attributes are no longer returned).
|
|
391
|
+
`expiry_formatted` was removed (format `expires_at` on the client). With
|
|
392
|
+
`use_jwt_token: false` no JWT is minted; the response carries
|
|
393
|
+
`jwtIssued: false` instead of `tokenIssued: true`. The TOTP challenge
|
|
394
|
+
response no longer includes `userId`. Errors use the Strapi envelope
|
|
395
|
+
`{ data: null, error: { status, name, message, details: { key } } }`
|
|
396
|
+
(`error.key` is kept) with translated messages.
|
|
397
|
+
* **JWT lifetime and refresh mode.** In users-permissions `legacy-support`
|
|
398
|
+
mode the JWT now expires after `jwt_token_expires_in` (`15m`, `12h`,
|
|
399
|
+
`30d`, ...). In `jwtManagement: 'refresh'` mode Magic Link logins issue a
|
|
400
|
+
refresh session through the users-permissions session manager and return
|
|
401
|
+
`refreshToken` (or set the refresh cookie when `sessions.httpOnly` or the
|
|
402
|
+
`x-strapi-refresh-cookie: httpOnly` header asks for it), exactly like
|
|
403
|
+
`/api/auth/local`; `context` is only in the response body there.
|
|
404
|
+
Revoking a session in the plugin also revokes the users-permissions
|
|
405
|
+
session, and revocation entries now live until the token expires (not a
|
|
406
|
+
fixed 30 days).
|
|
407
|
+
* **MFA precedence.** A user with an enabled authenticator always gets the
|
|
408
|
+
TOTP challenge after the link click, even when e-mail OTP is enabled
|
|
409
|
+
(previously `optional` mode silently skipped TOTP). In `required` mode a
|
|
410
|
+
user without an authenticator enrols one during the login (see "MFA"
|
|
411
|
+
below).
|
|
412
|
+
* **Auto-registration** needs `allow_magic_links_on_public_registration`,
|
|
413
|
+
`createUserIfNotExists` and users-permissions "Enable sign-ups"
|
|
414
|
+
(`allow_register`). New accounts are created unconfirmed and confirmed on
|
|
415
|
+
their first magic-link login; colliding usernames get a random suffix.
|
|
416
|
+
`username` on send-link may only contain letters, digits, `.`, `_`, `-`.
|
|
417
|
+
TOTP-only login no longer confirms accounts and honours users-permissions
|
|
418
|
+
"email confirmation".
|
|
419
|
+
* **Passwordless compatibility** is served only by the
|
|
420
|
+
`/<api prefix>/passwordless/{send-link,login}` rewrite (now honouring
|
|
421
|
+
`api.rest.prefix`); the duplicate `/api/magic-link/passwordless/*` routes
|
|
422
|
+
and the `compatibility-check` policy were removed. With the setting off,
|
|
423
|
+
the path is passed on instead of answering 404.
|
|
424
|
+
|
|
425
|
+
### BREAKING CHANGES (account claim)
|
|
426
|
+
|
|
427
|
+
* **Confirming an unconfirmed account clears its password.** When a magic
|
|
428
|
+
link, an e-mail OTP or the TOTP enrolment during login confirms an
|
|
429
|
+
account that was still unconfirmed, the plugin first ends every existing
|
|
430
|
+
session of the account (plugin JWTs and, in refresh mode, all
|
|
431
|
+
users-permissions refresh families), then confirms it with `password`,
|
|
432
|
+
`resetPasswordToken` and `confirmationToken` set to null. This stops
|
|
433
|
+
pre-account hijacking (someone registered the address with their own
|
|
434
|
+
password before the owner). users-permissions refuses password login
|
|
435
|
+
without a password. Migration: users who registered with a password and
|
|
436
|
+
then used a magic link before confirming set a new password through
|
|
437
|
+
"forgot password". Confirmed accounts are not changed.
|
|
438
|
+
|
|
439
|
+
### Security and fixes (login core)
|
|
440
|
+
|
|
441
|
+
* **Revocation could be bypassed with an upper-case path.** Strapi 5.61
|
|
442
|
+
mounts its routers with @koa/router defaults (`sensitive: false`), so
|
|
443
|
+
`GET /API/users/me` reaches the same route as `/api/users/me`. The
|
|
444
|
+
revocation middleware compared the path case-sensitively and skipped the
|
|
445
|
+
check, so a revoked JWT kept working. The Content API and admin path
|
|
446
|
+
checks now ignore case.
|
|
447
|
+
|
|
448
|
+
* **Refresh mode: revocation ends the whole refresh family.** @strapi/core
|
|
449
|
+
5.61 rotates a refresh token into a new session id with the same device
|
|
450
|
+
id; revoking only the first id left rotated tokens usable. The plugin now
|
|
451
|
+
revokes by user and device id (`invalidateRefreshToken`), deleting the
|
|
452
|
+
family including rotated rows; other devices stay logged in. Records from
|
|
453
|
+
before the device id was stored can still only revoke the first token.
|
|
454
|
+
* **Refresh mode: access tokens of a revoked family are rejected.** While a
|
|
455
|
+
user has a revoked family (until its maximum lifetime plus one
|
|
456
|
+
access-token lifespan), the revocation middleware checks the
|
|
457
|
+
users-permissions session (`isSessionActive`) of that user's access
|
|
458
|
+
tokens: 401 when it is gone, 503 when the state cannot be read. Other
|
|
459
|
+
users pay no lookup. Admin routes are never checked.
|
|
460
|
+
* **Legacy JWTs without `exp` stay revoked.** Revoking a JWT that never
|
|
461
|
+
expires (users-permissions without `expiresIn`) now blocks it permanently
|
|
462
|
+
instead of for 30 days.
|
|
463
|
+
* **Pending challenges re-check the current MFA policy.** An e-mail OTP no
|
|
464
|
+
longer completes a login after MFA became required or the user enrolled
|
|
465
|
+
an authenticator; re-opening the link starts the required challenge.
|
|
466
|
+
Login-time TOTP enrolment is refused when the user already has an
|
|
467
|
+
active authenticator.
|
|
468
|
+
* `Cache-Control: no-store` is now sent by every login, OTP and TOTP
|
|
469
|
+
endpoint, including `send-link`, `otp/send`, `otp/resend`,
|
|
470
|
+
`totp/status`, `totp/disable` and the admin TOTP status/disable routes.
|
|
471
|
+
* The built-in OTP, authenticator-confirmation and welcome mails are
|
|
472
|
+
translated (de, en, es, fr, pt) from the request's `?locale=` or
|
|
473
|
+
`Accept-Language` (FUN-27); custom login templates are unchanged.
|
|
474
|
+
* Multi-instance (AUTH-14) remains open: the session list, JWT blocklist
|
|
475
|
+
and ban list are plugin-store blobs serialised per process only. See
|
|
476
|
+
"Running behind a proxy or in a cluster" in the README.
|
|
477
|
+
|
|
478
|
+
* `POST /api/magic-link/login { loginToken }` added for frontend landing
|
|
479
|
+
pages, so tokens stay out of access logs and Referer headers; `GET` keeps
|
|
480
|
+
working. Login responses send `Cache-Control: no-store` and
|
|
481
|
+
`Referrer-Policy: no-referrer`.
|
|
482
|
+
* Single-use links are consumed with a conditional update: parallel requests
|
|
483
|
+
with one link yield exactly one session.
|
|
484
|
+
* Re-opening a link with a pending e-mail OTP no longer sends a new code
|
|
485
|
+
while the current one is valid.
|
|
486
|
+
* send-link answers before any lookup or delivery, so neither status nor
|
|
487
|
+
timing reveals whether an account exists; delivery errors are only logged.
|
|
488
|
+
* The JWT revocation check parses `Authorization` like users-permissions
|
|
489
|
+
(any case, any whitespace) and runs only for Content API requests (REST
|
|
490
|
+
under `api.rest.prefix` and the GraphQL endpoint), where it fails closed
|
|
491
|
+
with 503. Admin API requests, including plugin admin routes such as
|
|
492
|
+
`/content-manager/...`, are never checked, so a store problem cannot lock
|
|
493
|
+
administrators out.
|
|
494
|
+
* Plugin logs no longer contain e-mail addresses, URLs with tokens or
|
|
495
|
+
provider errors from the login core.
|
|
496
|
+
* Login mails sent through Magic Mail are `transactional` and carry no
|
|
497
|
+
open-tracking pixel and no tracked links (`enableTracking: false`).
|
|
498
|
+
* Sender falls back to the e-mail plugin `defaultFrom` instead of
|
|
499
|
+
`noreply@example.com`; the default expiry text reads "1 hour".
|
|
500
|
+
* Server error messages are translated (de, en, es, fr, pt); unknown
|
|
501
|
+
`?locale=` values fall back to English.
|
|
502
|
+
* Legacy-mode JWTs carry a random `jti`. Two logins of the same user within
|
|
503
|
+
one second used to get byte-identical tokens, so revoking one session in
|
|
504
|
+
the admin panel also ended the other.
|
|
505
|
+
|
|
506
|
+
### BREAKING CHANGES (MFA: e-mail OTP, TOTP, backup codes)
|
|
507
|
+
|
|
508
|
+
* **Secrets are mandatory and checked at startup.** `MAGIC_LINK_ENCRYPTION_KEY`
|
|
509
|
+
and `MAGIC_LINK_OTP_PEPPER` must both be set and at least 32 characters
|
|
510
|
+
long, also outside `NODE_ENV=production`. The only exception is
|
|
511
|
+
`MAGIC_LINK_ALLOW_DEV_SECRETS=true` on non-production hosts, which uses a
|
|
512
|
+
development fallback and logs a warning.
|
|
513
|
+
Migration: generate both with
|
|
514
|
+
`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`.
|
|
515
|
+
If your current encryption key is shorter than 32 characters (or you used
|
|
516
|
+
the development fallback), set the new key in `MAGIC_LINK_ENCRYPTION_KEY`
|
|
517
|
+
and the old value in `MAGIC_LINK_ENCRYPTION_KEY_PREVIOUS` (for the old
|
|
518
|
+
development fallback: the value of `ADMIN_JWT_SECRET`). Startup re-encrypts
|
|
519
|
+
stored TOTP secrets with the new key; remove the previous key afterwards.
|
|
520
|
+
A new pepper only invalidates pending e-mail codes and backup codes.
|
|
521
|
+
* **Authenticator enrolments are namespaced by principal.** TOTP records now
|
|
522
|
+
belong either to a users-permissions user (`up:<id>`) or to an admin user
|
|
523
|
+
(`admin:<id>`); equal numeric ids no longer share a second factor (security
|
|
524
|
+
review S-01). On startup, records from 5.x are assigned only where
|
|
525
|
+
ownership is provable: the users-permissions user or admin user with that
|
|
526
|
+
id must still have the enrolment e-mail. All other records are disabled.
|
|
527
|
+
Unconfirmed setups and the old unsalted backup codes are dropped.
|
|
528
|
+
Migration: users whose authenticator was disabled enrol again (see below).
|
|
529
|
+
* **End users manage their authenticator themselves.** New Content API
|
|
530
|
+
routes (users-permissions JWT): `GET /api/magic-link/totp/status`,
|
|
531
|
+
`POST /totp/setup` (sends an e-mail code; needs `totpCode` when an
|
|
532
|
+
authenticator is already active), `POST /totp/confirm { totpCode, emailCode }`
|
|
533
|
+
(returns 10 backup codes once), `POST /totp/disable { totpCode | backupCode }`,
|
|
534
|
+
`POST /totp/backup-codes { totpCode }`. The README routes
|
|
535
|
+
`/totp/verify` and `/totp/login` never existed; use `/totp/confirm` and
|
|
536
|
+
`/login-totp`.
|
|
537
|
+
Migration: grant the `totp` actions (status, setup, confirm, disable,
|
|
538
|
+
backupCodes) to the **Authenticated** role in *Settings > Users &
|
|
539
|
+
Permissions > Roles*.
|
|
540
|
+
* **Required MFA enrols during login.** With `mfa_mode: 'required'`, a user
|
|
541
|
+
without an authenticator no longer gets 403 `mfa.setupRequired`. `/login`
|
|
542
|
+
answers `{ requiresTOTPSetup: true, loginToken }`; the frontend calls
|
|
543
|
+
`POST /api/magic-link/totp/enroll { loginToken }` (secret and QR code) and
|
|
544
|
+
`POST /api/magic-link/totp/enroll/confirm { loginToken, totpCode }`, which
|
|
545
|
+
returns the session plus `backupCodes`.
|
|
546
|
+
Migration: handle `requiresTOTPSetup` in the frontend landing page.
|
|
547
|
+
* **TOTP parameters are stored per enrolment.** Algorithm, digits and period
|
|
548
|
+
are saved when a user enrols; changing the settings only affects new
|
|
549
|
+
enrolments. The QR code now carries issuer, algorithm, digits and period.
|
|
550
|
+
Re-enrolment stages the new secret and keeps the current authenticator
|
|
551
|
+
active until the new one is confirmed.
|
|
552
|
+
* **Backup codes work.** `verify-mfa-totp` accepts `{ loginToken, backupCode }`
|
|
553
|
+
instead of `totpCode` (one-time use; the response carries
|
|
554
|
+
`backupCodesRemaining`). Codes are 16 characters (`XXXX-XXXX-XXXX-XXXX`,
|
|
555
|
+
about 78 bits), stored as owner-bound HMACs.
|
|
556
|
+
* **Admin TOTP routes** (`/magic-link/otp/totp/*`) manage only the calling
|
|
557
|
+
admin user's own authenticator. `setup` needs `{ token }` (a current code)
|
|
558
|
+
when one is active, `verify` confirms a pending setup and returns backup
|
|
559
|
+
codes, `disable` and `backup-codes` need `{ token }`.
|
|
560
|
+
* **Unbound e-mail OTP is refused while MFA is active.** With
|
|
561
|
+
`otp_strict_binding: false`, `/otp/send` and `/otp/verify` only work when
|
|
562
|
+
`mfa_mode` is `disabled`.
|
|
563
|
+
* **`/otp/verify` issues sessions like every other login.** The response is
|
|
564
|
+
the common login body (`jwt`, sanitized `user`, filtered `context`,
|
|
565
|
+
`expires_at`, `refreshToken` in refresh mode, `otpVerified: true`); the
|
|
566
|
+
session appears in the JWT sessions list and can be revoked. Invalid
|
|
567
|
+
request bodies answer with Strapi's `ValidationError` instead of
|
|
568
|
+
"Failed to send OTP code".
|
|
569
|
+
* **OTP codes:** the `sms` type and the `phoneNumber` attribute were removed
|
|
570
|
+
from `otp-code`. The admin list (`GET /magic-link/otp/codes`) validates its
|
|
571
|
+
query (`page`, `pageSize` up to 1000, `email`, `type`, `status`), paginates on the
|
|
572
|
+
server, returns `pagination.truncated` and no longer returns code hashes
|
|
573
|
+
or token bindings.
|
|
574
|
+
|
|
575
|
+
### Security and fixes (MFA)
|
|
576
|
+
|
|
577
|
+
* TOTP uses otplib 13 instead of the unmaintained speakeasy 2.0.0. Existing
|
|
578
|
+
base32 secrets stay valid (RFC 6238 test vectors pass).
|
|
579
|
+
* Every accepted TOTP time step is recorded with a compare-and-set update,
|
|
580
|
+
also during setup confirmation, so a code cannot be used twice.
|
|
581
|
+
* E-mail OTP attempts are counted with a compare-and-set update before the
|
|
582
|
+
comparison, so parallel guesses cannot exceed `otp_max_attempts`. Codes are
|
|
583
|
+
drawn with `crypto.randomInt` (no modulo bias).
|
|
584
|
+
* TOTP-only login limits attempts per IP, per (account, IP) pair and per
|
|
585
|
+
account, so one client cannot lock a victim out, and runs a dummy
|
|
586
|
+
verification for unknown accounts (equal timing).
|
|
587
|
+
* Every second-factor code check (TOTP, backup code, e-mail OTP, enrolment
|
|
588
|
+
confirmation, admin authenticator routes) also counts against a `verify`
|
|
589
|
+
limit of 10 attempts per account and 15 minutes (then blocked for 15
|
|
590
|
+
minutes). It stays on when `rate_limit_enabled` is off, so disabling send
|
|
591
|
+
throttling never opens a brute-force path.
|
|
592
|
+
* OTP e-mails no longer read "0 minutes" for short expiries and the plugin no
|
|
593
|
+
longer logs e-mail addresses or codes.
|
|
594
|
+
* Deleting an OTP code from the admin works with the numeric id and answers
|
|
595
|
+
404 when nothing was deleted; the new `GET /magic-link/totp/enrollment-summary`
|
|
596
|
+
(Settings read) returns the number of enrolled end users.
|
|
597
|
+
|
|
598
|
+
### Admin panel
|
|
599
|
+
|
|
600
|
+
* The Tokens, OTP codes, JWT sessions, IP bans, Settings and License pages
|
|
601
|
+
were rebuilt on the Strapi design system with shared dialogs and theme
|
|
602
|
+
tokens. They have correct dark-mode colours and contrast, a consistent
|
|
603
|
+
spacing scale, labelled controls, keyboard-accessible tabs and pagination,
|
|
604
|
+
and no emoji.
|
|
605
|
+
* Every page follows the new RBAC actions. Read-only roles see disabled or
|
|
606
|
+
hidden controls, and destructive actions use confirmation dialogs instead
|
|
607
|
+
of `window.confirm`.
|
|
608
|
+
* Settings:
|
|
609
|
+
* saves only changed fields and guards unsaved changes;
|
|
610
|
+
* validates URLs, JWT lifetimes and number ranges like the server;
|
|
611
|
+
* no longer shows `login_path`, `callback_url`, `max_login_attempts` or the
|
|
612
|
+
Email Designer compatibility switch;
|
|
613
|
+
* the MFA mode is a single, lock-out safe choice.
|
|
614
|
+
* Admin token creation sends `ttl` as a top-level field and offers "do not
|
|
615
|
+
send e-mail" only with `tokens.issue`.
|
|
616
|
+
* The License page masks the key.
|
|
617
|
+
* Tokens list (browser acceptance at 320 to 1440 px, light and dark):
|
|
618
|
+
* the view, extend and delete buttons in a row work with Enter and Space
|
|
619
|
+
(the design-system table cell cancelled the native key activation), and
|
|
620
|
+
they stay in fixed columns when a token cannot be extended;
|
|
621
|
+
* dialogs return focus to the row, the search field or "New token" when
|
|
622
|
+
the element that opened them is gone (deleted row, closed details or
|
|
623
|
+
create dialog);
|
|
624
|
+
* the misleading "New user" line under every e-mail is gone (tokens have
|
|
625
|
+
no user relation);
|
|
626
|
+
* white text on the header, tab, button and dialog gradients and the status
|
|
627
|
+
badges reach 4.5:1;
|
|
628
|
+
* no orphan stat card, no horizontal table scrolling from 1024 px, no
|
|
629
|
+
clipped header buttons at 320 px, a compact pagination with "Page x of y"
|
|
630
|
+
below 520 px, and 32 px touch targets for the search clear button and the
|
|
631
|
+
page links on phones.
|
|
632
|
+
* OTP codes, JWT sessions and IP bans (browser acceptance at 320 to 1440 px,
|
|
633
|
+
light and dark):
|
|
634
|
+
* the row actions are reachable with the arrow keys again (the select-all
|
|
635
|
+
cell kept the grid focus), and Space and Enter activate them;
|
|
636
|
+
* Restore is disabled with the reason for expired, refresh-mode and
|
|
637
|
+
account-claim sessions instead of failing with an error;
|
|
638
|
+
* the Ban IP dialog and the confirm dialogs return focus to their opener,
|
|
639
|
+
or to the search field when the row is gone;
|
|
640
|
+
* the status badges reach 4.5:1 (the design-system warning and danger
|
|
641
|
+
badges are 3.6:1 and 3.9:1), the table has one frame with the header
|
|
642
|
+
band at full width, rows line up with the filter bar, and checkboxes and
|
|
643
|
+
page links are 24 px (32 px on phones);
|
|
644
|
+
* no horizontal table scrolling from 1024 px, phone cards without a
|
|
645
|
+
second frame, wrapped IPv6 addresses and no sticky hover tint on touch;
|
|
646
|
+
* the IP placeholder uses a documentation address (203.0.113.10).
|
|
647
|
+
* Settings and License (browser acceptance at 320 to 1440 px, light and dark):
|
|
648
|
+
* a refused save highlights and focuses the fields: link URLs outside
|
|
649
|
+
`allowedLinkOrigins` and security fields changed without
|
|
650
|
+
`settings.security` now get a field message instead of only a toast;
|
|
651
|
+
* "Save" and "N fields need attention" move focus to the first invalid
|
|
652
|
+
field also when its section is already open, and focus returns to the
|
|
653
|
+
field (Enter) or the section header after a save or a discard instead of
|
|
654
|
+
falling to the page body;
|
|
655
|
+
* a section with invalid fields shows "Fields to fix" in its header (the
|
|
656
|
+
design-system accordion never rendered the old description text);
|
|
657
|
+
* the template gallery cards fill their grid cells (equal widths);
|
|
658
|
+
* the "Unsaved changes" badge reaches 4.8:1 (design-system warning badge:
|
|
659
|
+
3.6:1), and the License key buttons 5.4:1 (white frost: 3.6:1);
|
|
660
|
+
* the sign-in mode cards are named by their title and described by their
|
|
661
|
+
text.
|
|
662
|
+
* All texts are translated into English, German (formal), French, Spanish
|
|
663
|
+
and Portuguese, and dates follow the admin locale. A test now checks that
|
|
664
|
+
every message id used by the admin exists in all five locales.
|
|
665
|
+
|
|
666
|
+
### BREAKING CHANGES (follow-up fixes, 2026-11-07)
|
|
667
|
+
|
|
668
|
+
* **`POST /magic-link/reset-data` needs `settings.security`.** A reset
|
|
669
|
+
deletes every login token, the JWT session list and all IP bans and resets
|
|
670
|
+
link targets, templates, sender and code rules, so `settings.update` alone
|
|
671
|
+
no longer allows it (403, `error.details.key: settings.security.forbidden`).
|
|
672
|
+
The `securitySettingsKept` response field is gone. Migration: grant
|
|
673
|
+
**Settings > Security** to the roles that run resets.
|
|
674
|
+
* **Sender identity is a security setting.** Changing `from_name`,
|
|
675
|
+
`from_email` or `response_email` needs `settings.security` in addition to
|
|
676
|
+
`settings.update`: the login mail carries the login link, so who it claims
|
|
677
|
+
to come from matters. Sending them unchanged is still allowed.
|
|
678
|
+
* **`mfa_mode: "required"` is refused while nobody is enrolled.** The settings
|
|
679
|
+
API answers 400 (`error.details.key: settings.mfa.noEnrolledUsers`,
|
|
680
|
+
`fields: ["mfa_mode"]`) when MFA would become required (also through the
|
|
681
|
+
legacy `mfa_require_totp`) and no end user has an active authenticator.
|
|
682
|
+
Send `acknowledgeNoEnrolledUsers: true` with the update when your landing
|
|
683
|
+
page handles `requiresTOTPSetup`; the flag is never stored.
|
|
684
|
+
* **`max_login_attempts` is removed.** It was validated but never read; the
|
|
685
|
+
login limiter uses `rate_limit_login_max_attempts`. A stored or sent value
|
|
686
|
+
is dropped.
|
|
687
|
+
|
|
688
|
+
### Security and fixes (follow-up, 2026-11-07)
|
|
689
|
+
|
|
690
|
+
* `send-link` keys its e-mail rate limiter with a hash of the normalised
|
|
691
|
+
address, like the OTP and TOTP paths, so no address is kept in limiter
|
|
692
|
+
memory and case variants share one bucket.
|
|
693
|
+
* Admin: the Tokens, Settings and License pages no longer show "no
|
|
694
|
+
permissions" to a Super Admin after a full page reload. Strapi 5.61
|
|
695
|
+
`useRBAC` could check while the admin permissions were still loading,
|
|
696
|
+
settle with nothing allowed and never re-check; unconditioned grants from
|
|
697
|
+
the loaded admin permissions now count.
|
|
698
|
+
* Admin: arrow keys in the Tokens table no longer get stuck on the
|
|
699
|
+
select-all header cell.
|
|
700
|
+
* Docs: refresh mode works from Strapi 5.24.0; Strapi's own
|
|
701
|
+
`/api/auth/refresh` reads the httpOnly refresh cookie only from 5.33.3.
|
|
702
|
+
|
|
703
|
+
### Build, dependencies and release
|
|
704
|
+
|
|
705
|
+
* Removed the unused `lodash` and `yup` dependencies.
|
|
706
|
+
* The root `strapi-server.js` and `strapi-admin.js` load `dist/` (they
|
|
707
|
+
pointed at sources that are not published). Removed the dead
|
|
708
|
+
`.npmignore`, `.externals.json`, `.trigger-release` and the unused
|
|
709
|
+
license-tier map `server/src/config/features.js`.
|
|
710
|
+
* ESLint 10 flat config with `@eslint/js` recommended,
|
|
711
|
+
`eslint-plugin-react-hooks` recommended and `eslint-config-prettier`, plus
|
|
712
|
+
Prettier with `lint`, `lint:fix`, `format` and `format:check` scripts.
|
|
713
|
+
* GitHub Actions are pinned to commit SHAs with least-privilege tokens.
|
|
714
|
+
* New pull-request CI on Node 22 and 24 runs lint, format check, tests,
|
|
715
|
+
build, verify and `npm pack --dry-run`.
|
|
716
|
+
* Releases install without lifecycle scripts and publish with npm trusted
|
|
717
|
+
publishing and provenance.
|
|
718
|
+
* `verify:runtime` checks the public routes, the RBAC policy on every admin
|
|
719
|
+
route and that the server bundle only loads declared dependencies.
|
|
720
|
+
|
|
721
|
+
# [5.6.0](https://github.com/Schero94/strapi-plugin-magic-link-v5/compare/v5.5.9...v5.6.0) (2026-10-15)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
### Bug Fixes
|
|
725
|
+
|
|
726
|
+
* align CI installs with lockfile settings ([77580a3](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/77580a36f00f7f41d04be953a8d19a0422d14dce))
|
|
727
|
+
* check out documentation before Pages deploy ([114ef6e](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/114ef6e3e5e4d1039d5db89ae791b633e1189793))
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
### Features
|
|
731
|
+
|
|
732
|
+
* delegate WhatsApp OTP to Magic WhatsApp ([64fa660](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/64fa66053478db45785374840f4e9f6236b428c4))
|
|
733
|
+
|
|
734
|
+
## [5.5.9](https://github.com/Schero94/strapi-plugin-magic-link-v5/compare/v5.5.8...v5.5.9) (2026-05-03)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
### Bug Fixes
|
|
738
|
+
|
|
739
|
+
* trigger release for plugin always-on refactor ([2ed54eb](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/2ed54eb228bccc84f1cd013741f98de69edf2268))
|
|
740
|
+
|
|
1
741
|
## [5.5.8](https://github.com/Schero94/strapi-plugin-magic-link-v5/compare/v5.5.7...v5.5.8) (2026-05-03)
|
|
2
742
|
|
|
3
743
|
|