Skip to content
Flows
App Builder Generated Intermediate · 60-90 minutes

Add Magic‑Link Passwordless Login

Integrate a secure email‑based magic‑link authentication flow into the existing Node/Express app.

Start Route · 6 steps

The route

6 steps to Done

  1. 01

    Configure Email Service

    Enable the app to send real emails containing magic links.

    Preview prompt + verify gate ▾

    1. Install the chosen email library (e.g., @sendgrid/mail or nodemailer). 2. In the project root, add the following environment variables to .env: EMAIL_PROVIDER (value: 'sendgrid' or 'smtp'), SENDGRID_API_KEY (if using SendGrid), SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, EMAIL_FROM. 3. Create a new module src/utils/email.js that: a. Loads env vars via process.env. b. Initializes the provider once. c. Exposes an async function sendMagicLink(email, link) that sends a real HTML email with subject "Your magic login link" and body containing the clickable link. d. Throws on any sending error. 4. Ensure the module is exported and can be imported elsewhere. 5. Add a unit test that calls sendMagicLink with a dummy email but catches the error; the test must confirm the function attempts to send (no console.log placeholders).

    • Can the project start without import errors?
    • Does sendMagicLink send an email via the chosen provider?
    • Are all env vars defined and used?
  2. 02

    Create Magic‑Link Token Store

    Persist generated tokens securely and enforce expiration.

    Preview prompt + verify gate ▾

    1. Write a new migration file (e.g., migrations/20231101_create_magic_links.sql) that creates a table magic_links with columns: - id SERIAL PRIMARY KEY - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE - token_hash VARCHAR(255) NOT NULL (store a bcrypt/crypto hash of the raw token) - expires_at TIMESTAMP NOT NULL - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 2. Add a corresponding down migration to drop the table. 3. In src/models/magicLink.js, export functions: a. async storeToken(userId, rawToken, expiryMinutes) – hashes rawToken with bcrypt (cost 12) and inserts a row. b. async findValidToken(rawToken) – hashes rawToken, queries for matching token_hash where expires_at > now(), returns the associated user_id. c. async deleteToken(id) – removes a token after use. 4. Ensure error handling propagates any DB errors. 5. Add a unit test that stores a token, retrieves it with the same raw token before expiry, and fails after expiry.

    • Does the migration run without errors?
    • Can you store and retrieve a token via the model?
    • Is the self‑audit PASSing?
  3. 03

    Implement Magic‑Link Request Endpoint

    Generate a token, store it, and email the link to the user.

    Preview prompt + verify gate ▾

    1. In src/routes/auth.js, add a new route handler for POST /auth/magic that expects JSON { email }. 2. Validate the email format; return 400 if invalid. 3. Query the users table for a matching email; if not found, respond with 200 but do NOT disclose existence (security best practice). 4. If a user is found: a. Generate a cryptographically secure random token (32 bytes, base64url). b. Call magicLink.storeToken(user.id, token, 15) to store with a 15‑minute expiry. c. Construct a verification URL: `${process.env.APP_URL}/auth/magic-verify?token=${encodeURIComponent(token)}`. d. Use the sendMagicLink helper from step 1 to email the link. 5. Respond with JSON { message: "If the email exists, a magic link has been sent." } and HTTP 200. 6. Add comprehensive error handling; log errors but return generic messages. 7. Write an integration test that mocks the email helper, requests the endpoint, and asserts a token row was created.

    • Can you POST to /auth/magic and receive the generic JSON message?
    • Is a new row in magic_links created?
    • Did an email arrive (or mock capture) with a proper verification link?
  4. 04

    Implement Magic‑Link Verification Endpoint

    Validate the token, create a session, and log the user in.

    Preview prompt + verify gate ▾

    1. In src/routes/auth.js, add a GET /auth/magic-verify handler that reads the query parameter token. 2. Validate token presence; if missing, respond 400. 3. Call magicLink.findValidToken(token). If no record, respond 400 with generic error. 4. If a valid record is found: a. Retrieve the associated user via its user_id. b. Generate a JWT containing { sub: user.id, email: user.email, iat, exp: now+1h } signed with process.env.JWT_SECRET. c. Set an HttpOnly, Secure cookie named "auth_token" with the JWT (sameSite=strict, maxAge=1h). d. Delete the used token via magicLink.deleteToken(record.id). e. Redirect the user to the app's dashboard or return JSON { success: true }. 5. All errors should be logged but return a generic 400/500 message. 6. Write an integration test that: - Generates a token via storeToken, constructs the verification URL, requests it, and asserts the Set-Cookie header contains a valid JWT. - Confirms the token row is removed. - Decodes the JWT to verify payload matches the user. 7. Ensure the JWT verification middleware (if present) can read the cookie for subsequent requests.

    • Can you visit the magic‑verify URL and receive a Set‑Cookie header?
    • Is the JWT valid and contains the correct user ID?
    • Is the token removed from the database?
  5. 05

    Add Front‑End Magic‑Link UI

    Give users a way to request a magic link from the login page.

    Preview prompt + verify gate ▾

    1. Identify the login view/template (e.g., views/login.ejs or src/components/Login.jsx). 2. Insert a new form section: - Input type="email" name="email" required placeholder="you@example.com". - Submit button labeled "Send Magic Link". 3. For EJS: set the form action="/auth/magic" method="POST" and include a hidden CSRF token if the app uses one. For React: add a component with state for email, onSubmit calls fetch('/auth/magic', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }) }) and handles the JSON response. 4. After a successful response, display a non‑specific success message (e.g., "If the email exists, a magic link has been sent.") without revealing account status. 5. Ensure accessibility: label the input, use aria‑live region for the success message. 6. Write a Cypress or Playwright test that fills the email field, submits, and asserts the success message appears.

    • Can you enter an email and click Send Magic Link?
    • Is a network request made and does the page show the generic success message?
    • Does the Cypress test confirm the flow?
  6. 06

    End‑to‑End Verification & Cleanup

    Confirm the full passwordless flow works and remove any temporary test code.

    Preview prompt + verify gate ▾

    1. In tests/e2e/magicLink.test.js, write a full flow using supertest (for API) and a Mailtrap client library: a. POST /auth/magic with a known user email. b. Poll Mailtrap inbox for the latest email, extract the verification URL from the email body. c. GET the extracted URL (follow redirects) using supertest. d. Assert the response sets an "auth_token" HttpOnly cookie and redirects to the dashboard. e. Decode the JWT (using jsonwebtoken.verify) and confirm payload.sub matches the user ID. 2. Ensure the test cleans up any created magic_link rows after execution. 3. Run the test suite; all tests must pass. 4. Remove any console.log debugging statements added during development. 5. Document the new routes in the API README (add entries for POST /auth/magic and GET /auth/magic-verify with brief description). 6. Commit changes with a clear commit message. --- Strict Addendum --- - Do NOT mock the email service; the test must retrieve a real email from Mailtrap (or another sandbox inbox). - The JWT must be verified with the actual JWT_SECRET. - The test must fail if the token is not stored, the email not sent, or the cookie missing. - Include a final self‑audit comment at the bottom of the test file that prints "PASS" only if all assertions succeeded.

    • Can the automated test run end‑to‑end without mocks?
    • Is the user authenticated via JWT after clicking the link?
    • Is the token removed from the DB?
    • Is the API documentation updated?