|
| 1 | +# Express Passwordless Example |
| 2 | + |
| 3 | +This example demonstrates how to use the `@auth0/auth0-server-js` package to |
| 4 | +authenticate users with **passwordless** authentication in an Express |
| 5 | +application. It covers all three flows: |
| 6 | + |
| 7 | +- **Email OTP** — a one-time code sent by email. |
| 8 | +- **SMS OTP** — a one-time code sent by SMS (requires a configured SMS provider). |
| 9 | +- **Magic link** — a sign-in link sent by email; clicking it completes login. |
| 10 | + |
| 11 | +Unlike the interactive (redirect) login flow, passwordless OTP is a two-step, |
| 12 | +non-redirect flow: |
| 13 | + |
| 14 | +1. **Start** — the user submits an email or phone number, and Auth0 sends a |
| 15 | + one-time code. No session is created yet. |
| 16 | +2. **Verify** — the user submits the received code, which is exchanged for |
| 17 | + tokens, and a session cookie is established. |
| 18 | + |
| 19 | +The **magic link** flow is redirect-based: the link lands on `/auth/callback` |
| 20 | +with an authorization `code` and `state`, which the SDK exchanges (without PKCE) |
| 21 | +to establish the session. |
| 22 | + |
| 23 | +## Install dependencies |
| 24 | + |
| 25 | +Install the dependencies using npm: |
| 26 | + |
| 27 | +```bash |
| 28 | +npm install |
| 29 | +``` |
| 30 | + |
| 31 | +## Configuration |
| 32 | + |
| 33 | +Rename `.env.example` to `.env` and configure your tenant: |
| 34 | + |
| 35 | +```env |
| 36 | +AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN |
| 37 | +AUTH0_CLIENT_ID=YOUR_AUTH0_CLIENT_ID |
| 38 | +AUTH0_CLIENT_SECRET=YOUR_AUTH0_CLIENT_SECRET |
| 39 | +AUTH0_SESSION_SECRET=YOUR_AUTH0_SESSION_SECRET |
| 40 | +APP_BASE_URL=http://localhost:3000 |
| 41 | +``` |
| 42 | + |
| 43 | +The `AUTH0_SESSION_SECRET` is the key used to encrypt the session cookie. You |
| 44 | +can generate a secret using `openssl`: |
| 45 | + |
| 46 | +```shell |
| 47 | +openssl rand -hex 64 |
| 48 | +``` |
| 49 | + |
| 50 | +The `APP_BASE_URL` is the URL that your application is running on. When |
| 51 | +developing locally, this is most commonly `http://localhost:3000`. |
| 52 | + |
| 53 | +### Tenant prerequisites |
| 54 | + |
| 55 | +- Enable the **Passwordless** connection(s) you want to use (Email and/or SMS) |
| 56 | + in the Auth0 Dashboard (Authentication > Passwordless). |
| 57 | +- The application must be a **Regular Web Application** with the |
| 58 | + **Passwordless OTP** grant enabled. |
| 59 | +- For SMS, a working SMS provider must be configured on the tenant. Email is the |
| 60 | + simplest path for a runnable demo; SMS is optional. |
| 61 | +- For the **magic link** flow: add `http://localhost:3000/auth/callback` to the |
| 62 | + application's **Allowed Callback URLs**, and enable the tenant setting |
| 63 | + `allow_magiclink_verify_without_session`. Without it the click fails with |
| 64 | + "The link must be opened on the same device and browser." This is a tenant |
| 65 | + setting nested under `universal_login.passwordless` (NOT a top-level `flags` |
| 66 | + entry — sending it under `flags` returns `400 Additional properties not allowed`): |
| 67 | + |
| 68 | + ```bash |
| 69 | + auth0 api patch tenants/settings \ |
| 70 | + --data '{"universal_login":{"passwordless":{"allow_magiclink_verify_without_session":true}}}' |
| 71 | + ``` |
| 72 | + |
| 73 | +With the configuration in place, start the example with: |
| 74 | + |
| 75 | +```bash |
| 76 | +npm run start |
| 77 | +``` |
| 78 | + |
| 79 | +## Routes |
| 80 | + |
| 81 | +- `/`: Home route, displaying a message depending on authentication state. |
| 82 | +- `/private`: A private route accessible only to authenticated users. |
| 83 | +- `GET /auth/login`: Renders the form to choose a channel (email/SMS) and enter |
| 84 | + an identifier. |
| 85 | +- `POST /auth/start`: Calls `startPasswordless({ connection })` to send the |
| 86 | + one-time code. |
| 87 | +- `POST /auth/verify`: Calls `completePasswordless({ connection })` to exchange |
| 88 | + the code for tokens and establish the session, then redirects home. |
| 89 | +- `POST /auth/start-link`: Calls `startPasswordless({ connection: 'email', send: 'link' })` |
| 90 | + to email a magic link (renders a "check your email" page; no session yet). |
| 91 | +- `GET /auth/callback`: Calls `completePasswordlessMagicLink` to validate the |
| 92 | + `state`, exchange the code (no PKCE), establish the session, then redirects home. |
| 93 | +- `GET /auth/logout`: Logs the user out. |
| 94 | + |
| 95 | +## Error handling |
| 96 | + |
| 97 | +The passwordless error classes are re-exported by `@auth0/auth0-server-js`, so |
| 98 | +the routes branch with `instanceof` and keep their dependencies to the server SDK |
| 99 | +only: |
| 100 | + |
| 101 | +- `PasswordlessStartError` — sending the code failed (bad email/phone, SMS |
| 102 | + provider error, rate limit). |
| 103 | +- `PasswordlessVerifyError` — the code was wrong, expired, or rate-limited. |
| 104 | + |
| 105 | +MFA has no dedicated error class. It is a `PasswordlessVerifyError` carrying |
| 106 | +`cause.error === 'mfa_required'`, narrowed with the re-exported |
| 107 | +`isMfaRequiredError`. MFA is out of scope for this example; a message is shown. |
| 108 | + |
| 109 | +## Security notes |
| 110 | + |
| 111 | +This is a minimal example and omits protections a production app should add: |
| 112 | + |
| 113 | +- **CSRF protection.** The `POST /auth/start`, `/auth/verify`, and |
| 114 | + `/auth/start-link` routes trigger side effects with cookie-based sessions and |
| 115 | + carry no CSRF token. Add CSRF protection (e.g. a CSRF middleware and/or |
| 116 | + `SameSite=Strict` session cookies) before shipping. |
0 commit comments