Skip to content

Add Magic Link (passwordless) login feature#326

Draft
Sogl wants to merge 2 commits intogetgrav:developfrom
Sogl:feature/magic-link
Draft

Add Magic Link (passwordless) login feature#326
Sogl wants to merge 2 commits intogetgrav:developfrom
Sogl:feature/magic-link

Conversation

@Sogl
Copy link
Contributor

@Sogl Sogl commented Feb 25, 2026

Summary

This PR adds a Magic Link (passwordless) login feature to the Login plugin, allowing users to sign in via a one-time link sent to their email address — no password required.

Note: This PR is submitted as a draft to invite discussion on the implementation approach before requesting a formal review. Feedback on design decisions, naming conventions, or implementation details is very welcome. cc @rhukster

Dependency: This PR assumes #325 (multipart email support) is merged first, as the plain-text email alternative for the magic login email (magic-login.txt.twig) relies on the multipart sending logic introduced there. If #325 is not merged, the feature works correctly but sends HTML-only emails.

Why

Passwordless login is increasingly common and expected, particularly for sites where users may not remember (or never set) a password — for example, after OAuth registration or admin-created accounts. A magic link is also a more user-friendly alternative to password reset for infrequent visitors.

What changed

Core logic

  • login.phphandleMagicLogin(): validates token, TTL, hash, invalidates token before login to prevent race-condition reuse, then runs the standard Login::login() pipeline
  • login.phpuserLoginAuthenticateByMagic(): onUserLoginAuthenticate listener (priority 10004) that sets AUTHENTICATION_SUCCESS and stops propagation when the magic_link option is set
  • login.phpaddMagicPage(): serves the magic link request page, respects magic_link.enabled guard
  • classes/Login.phpsendMagicLoginEmail(): generates a cryptographically random token, stores its SHA-256 hash + expiry in the user file, sends the email
  • classes/Controller.phptaskMagicRequest(): handles the email form submission with IP-level and per-user rate limiting; uses anti-enumeration (neutral response regardless of account existence)

Templates & pages

  • pages/magic.md — default request page (email form)
  • templates/magic.html.twig — page template
  • templates/partials/magic-form.html.twig — the email input form
  • templates/partials/login-form.html.twig — added "Login by link" button (only shown when magic_link.enabled: true)
  • templates/emails/login/magic-login.html.twig — HTML email template
  • templates/emails/login/magic-login.txt.twig — plain-text email template

Configuration

  • blueprints.yaml — admin UI settings for the magic link section
  • login.yaml — default config values (enabled: false, ttl: 10, rate limiting)

i18n

  • languages/en.yaml — all magic link keys
  • languages/ru.yaml — Russian translations

Documentation

  • README.md — added Magic Link Login section with enabling instructions, flow description, and security notes

Security considerations

  • Token is random_bytes(32) — only its SHA-256 hash is stored in the user file
  • Token is invalidated before the login pipeline runs (prevents race-condition reuse)
  • Links expire after configurable TTL (default: 10 minutes)
  • Links are strictly single-use
  • Anti-enumeration: the same neutral response is returned whether or not an account exists
  • Rate limiting applies per IP and per user account
  • remember_me is never set via magic link login
  • twofa is respected if enabled

Backward compatibility

  • Feature is disabled by default (magic_link.enabled: false)
  • No existing behavior is changed when the feature is disabled

- Add taskMagicRequest endpoint with IP and user rate limiting
- Add handleMagicLogin handler (token validation, TTL, single-use)
- Add userLoginAuthenticateByMagic listener (priority 10004)
- Add addMagicPage handler with magic_link.enabled guard
- Add Login::sendMagicLoginEmail() method
- Add magic request page (pages/magic.md) and templates
- Add 'Login by link' button on login form (conditional on enabled)
- Add blueprints.yaml settings for magic_link section
- Add magic_link defaults to login.yaml
- Add en.yaml and ru.yaml i18n keys for magic link flow
- Fix magic-login email templates: use author instead of actor.fullname
- Security: crypto-random token, SHA-256 hash stored, invalidate before login()
- Anti-enumeration: neutral response regardless of account state
- Update README with Magic Link section
@Sogl Sogl force-pushed the feature/magic-link branch from 2f46b78 to 11d012a Compare February 25, 2026 19:52
@Sogl Sogl force-pushed the feature/magic-link branch from 847d21d to 5826eff Compare February 26, 2026 01:09
@Sogl
Copy link
Contributor Author

Sogl commented Feb 26, 2026

Added one follow-up commit with two fixes:

  1. pre-login site.login ACL check now supports access inherited via groups (Flex + legacy users),
  2. magic-link requests now detect duplicate emails and fail safely instead of choosing an arbitrary account.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant