Skip to content

feat(auth): passwordless email magic-link login#133

Open
mikedegeofroy wants to merge 1 commit into
mainfrom
claude/friendly-mcnulty-dd6914
Open

feat(auth): passwordless email magic-link login#133
mikedegeofroy wants to merge 1 commit into
mainfrom
claude/friendly-mcnulty-dd6914

Conversation

@mikedegeofroy

Copy link
Copy Markdown
Contributor

Summary

Adds opt-in passwordless "magic link" login for the in-memory auth mode. The user enters their email on the login screen, the framework emails a single-use link, and following it establishes a session — no password typed. It composes with password login (both render on the login card) and is inert in the OIDC / resource-server modes, where the IdP owns sign-in.

POST /api/auth/magic/request {email}   → find user, mint single-use token, store hash, email link  (always 202)
GET  /api/auth/magic/verify?token=…    → consume token (atomic, single-use), mint session, 302 into the app

What's included

Backend (onec-auth-starter, new com.onec.auth.magic package)

  • MagicLinkController — the two endpoints; verify saves the SecurityContext to the session exactly like /api/auth/login.
  • MagicLinkService — 256-bit SecureRandom tokens; only the SHA-256 hash is persisted; no account enumeration (request always 202).
  • JdbcMagicLinkTokenStoreonec_auth_magic_link table (JDBI + SqlDialect, mirroring MailOutbox); atomic, expiry-checked, single-use consumption so links validate across nodes.
  • Pluggable SPIs (@ConditionalOnMissingBean): MagicLinkTokenStore, MagicLinkUserLookup, MagicLinkSender. Default delivery emails the link via onec-mail-starter (added compileOnly, same optional-cross-starter pattern mail uses for print).

Config

  • Optional email on onec.auth.users[*] — opt-in per account (mirrors how an SSO backend keys a user by an external identity). Email-shaped usernames work without it.
  • onec.auth.magic-link.* block: enabled (default false), token-validity (15m), base-url, redirect-path, subject.

Login UI (onec-ui-starter)

  • AuthMethods.magicLinkEnabled (backward-compatible new field) → LoginDivBuilder renders an onec-magic-link DivKit block.
  • React widget + bridge mirroring the password onec-login-form, portaled on the login route.

Auto-config ordering fix

  • The auth auto-config declared before = SecurityAutoConfiguration, which ordered it ahead of DataSourceAutoConfiguration / the mail auto-config, so @ConditionalOnBean(DataSource/MailService) evaluated before those beans existed (the app failed to start with the feature on, even with a datasource). Now ordered after them.

Requirements (fail-fast at startup if missing)

  • The onec-mail-starter with a configured provider (default sender), or a custom MagicLinkSender bean.
  • A DataSource (token persistence), or a custom MagicLinkTokenStore bean.

Test plan

  • 18 new tests: token store (single-use/expiry/purge), service round-trip + no-enumeration, controller (MockMvc: 202 always, redirect + session, single-use), mail sender, and auto-config wiring + fail-fast guidance.
  • ./gradlew check green across all 12 modules; frontend type-checks (tsc -b) + builds + vitest pass.
  • Live end-to-end against the example app (launch-args only, no committed example change): login card showed both blocks → request for a known address wrote one email / unknown wrote none (both 202) → following the link signed in as ana@rentals.local (ROLE_CLEANER) → replaying the link was rejected to /login?error=link.
  • Docs synced per the repo rule: docs/ARCHITECTURE.md, docs/CONFIGURATION.md (regenerated; checkConfigDocs passes), onec-auth-starter/README.md, onec-ui-starter/README.md.

Notes

  • Off by default; the example app config is unchanged.
  • Magic-link establishes a session only (no remember-me cookie) — deliberate; easy to add for parity.

🤖 Generated with Claude Code

Opt-in passwordless sign-in for the in-memory auth mode: the user enters
their email on the login screen, the framework emails a single-use link,
and following it establishes a session — no password typed.

- Endpoints POST /api/auth/magic/request and GET /api/auth/magic/verify
  (auth-starter, new com.onec.auth.magic package); verify mints the session
  the same way /api/auth/login does, then redirects into the app.
- JDBC-backed single-use token store (onec_auth_magic_link): only the token
  hash is persisted, consumption is atomic + expiry-checked, so links
  validate across every node of a horizontally-scaled deployment.
- Pluggable SPIs (MagicLinkTokenStore / MagicLinkUserLookup / MagicLinkSender);
  default delivery emails the link via onec-mail-starter (compileOnly).
- Opt-in per account via onec.auth.users[*].email; whole feature gated by
  onec.auth.magic-link.enabled (off by default). Request returns 202 whether
  or not the address is registered (no account enumeration).
- AuthMethods.magicLinkEnabled (backward-compatible) drives the DivKit login
  card + a React onec-magic-link widget.
- Order the auth auto-config after DataSource/mail auto-configs so the
  @ConditionalOnBean wiring sees those beans (fixes startup failure when the
  feature is enabled).
- Docs synced (ARCHITECTURE, CONFIGURATION, both starter READMEs); 18 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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