Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@harder-labs/vendure-plugin-magic-link

Passwordless authentication for Vendure 3.6+ via emailed magic links.

What it does

  • Peppered SHA-256 token hashes — only the hash is stored at rest. The pepper is mixed into the SHA-256 input; rotating it invalidates every outstanding token in one move.
  • Per-IP + per-email Redis rate limiting — anti-spray on issuance, plus per-email throttle (default 3/hr) so an attacker can't drain a user's link by enumerating.
  • Anti-probe responses — the issuance endpoint returns the same shape whether the email is registered, unregistered, or rate-limited, so the API doesn't leak user enumeration.
  • Hourly cleanup taskScheduledTask that deletes expired or used tokens. Registered automatically via the plugin's configuration hook.
  • Email template — ships with a Handlebars body template that integrates with @vendure/email-plugin (you provide the template registration).

Install

yarn add @harder-labs/vendure-plugin-magic-link
# peers:
yarn add @vendure/core @vendure/email-plugin

Usage

import { MagicLinkPlugin, magicLinkEmailHandler } from '@harder-labs/vendure-plugin-magic-link';
import { EmailPlugin, defaultEmailHandlers } from '@vendure/email-plugin';

const config: VendureConfig = {
    plugins: [
        EmailPlugin.init({
            handlers: [...defaultEmailHandlers, magicLinkEmailHandler],
            // ...
        }),
        MagicLinkPlugin.init({
            pepper: process.env.MAGIC_LINK_PEPPER!,        // MUST be >= 16 chars
            storefrontBaseUrl: process.env.PUBLIC_STOREFRONT_URL!,
        }),
    ],
};

init() throws if pepper.length < 16 or storefrontBaseUrl is missing.

Env contract

Var Required? Notes
MAGIC_LINK_PEPPER yes ≥16 chars. Production reads from Secrets Manager. Rotating it invalidates all outstanding tokens.
PUBLIC_STOREFRONT_URL yes Storefront base URL — the magic-link points at ${storefrontBaseUrl}/auth/verify?token=...
REDIS_HOST, REDIS_PORT optional Defaults to localhost:6379. Used by MagicLinkRateLimitService.

GraphQL surface

Shop API:

extend type Mutation {
    requestMagicLink(input: RequestMagicLinkInput!): RequestMagicLinkResult!
}

The response shape is intentionally identical for success / unknown email / rate-limited so the API doesn't leak enumeration. Use the existing authenticate(input: { magicLink: { token: "..." } }) mutation to redeem.

Provenance

Extracted from the vendure-platform in-tree implementation (Phase 0 tasks #8 + #14). Includes the timezone-bug fix (timestamptz on expiresAt) and the per-IP/per-email rate limit strengthening that came out of the pen-test pass.