Skip to content

Commit 3ae85be

Browse files
docs(examples): add express passwordless example (#199)
* feat(auth0-server-js): add passwordless authentication Add server-side passwordless session flows on top of auth0-auth-js: - startPasswordlessEmail/Sms + loginWithPasswordlessEmail/Sms (OTP, session write) - startPasswordlessMagicLink + completePasswordlessMagicLink (no-PKCE, state-owned txn) - TransactionData.codeVerifier made optional for magic-link transactions - EXAMPLES.md passwordless section Depends on: feat/passwordless-auth-js * fix(auth0-server-js): restrict send to code, guard state and redirectUri - StartPasswordlessEmailOptions.send restricted to 'code' (kill magic-link footgun) - remove SendEmailOptions cast (now type-safe) - guard empty redirectUri -> PasswordlessStartError - type-guard transaction state (typeof string) - tests: FT-2 code-default, FT-15b non-string state, FT-15c redirectUri * fix(auth0-server-js): align passwordless contract with nextjs-auth0 Collapse the email/sms-split passwordless surface into connection-discriminated methods mirroring @auth0/nextjs-auth0 passwordless.start()/verify(): - startPasswordless({connection,...}) (was startPasswordlessEmail/Sms + startPasswordlessMagicLink) - completePasswordless({connection,...}) (was loginWithPasswordlessEmail/Sms; code -> verificationCode) - completePasswordlessMagicLink kept separate (no-PKCE state handling) * docs(examples): add express passwordless example Runnable Express + EJS example covering all passwordless flows: - email OTP, SMS OTP (needs provider), and magic link - registers the example in the root README Depends on: feat/passwordless-server-js * fix(examples): correct MFA detection, typed router, OTP/CSRF hardening - MFA detection via cause.error==='mfa_required' (dead code removed) - augment express-serve-static-core Request; drop @ts-expect-error, typed Router - read PORT from env; OTP min/max length 6 - README: MFA + CSRF/security notes * fix(examples): use connection-discriminated passwordless methods * fix: fix review comments * fix(examples): address passwordless example review feedback - Drop Passkey* re-exports from auth0-server-js (residue from #198); keep Passwordless* and isMfaRequiredError which the example now consumes directly - Replace duplicated string-code branching with instanceof PasswordlessStartError / PasswordlessVerifyError and isMfaRequiredError; non-SDK errors surface as 500 - Correct README error-handling section (errors ARE re-exported) and fence the env config block as env instead of ts - Remove unused ts-node devDep, drop declaration/declarationMap and ts-node tsconfig block, and resolve express.static against __dirname Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b698c9e commit 3ae85be

20 files changed

Lines changed: 748 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The following examples can be found in the examples directory:
2222
- [Fastify API Example](./examples/example-fastify-api/README.md)
2323
- [NestJS API Example](./examples/example-nestjs-api/README.md)
2424
- [Express Web App Example](./examples/example-express-web/README.md)
25+
- [Express Passwordless Example](./examples/example-express-passwordless/README.md)
2526

2627
Before running the examples, you need to install the dependencies for the monorepo and build all the packages.
2728

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
AUTH0_DOMAIN=
2+
AUTH0_CLIENT_ID=
3+
AUTH0_CLIENT_SECRET=
4+
AUTH0_SESSION_SECRET=
5+
APP_BASE_URL=http://localhost:3000
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "example-express-passwordless",
3+
"version": "1.0.0",
4+
"description": "Passwordless (email/SMS one-time code) example using @auth0/auth0-server-js",
5+
"type": "module",
6+
"scripts": {
7+
"start": "tsx src/index.ts --project tsconfig.json",
8+
"build": "tsc --project tsconfig.json"
9+
},
10+
"devDependencies": {
11+
"@types/cookie-parser": "^1.4.8",
12+
"@types/ejs": "^3.1.5",
13+
"@types/express": "^5.0.1",
14+
"@types/express-ejs-layouts": "^2.5.4",
15+
"tsx": "^4.19.2",
16+
"typescript": "~5.8.3"
17+
},
18+
"dependencies": {
19+
"@auth0/auth0-server-js": "*",
20+
"cookie-parser": "^1.4.7",
21+
"dotenv": "^16.4.7",
22+
"ejs": "^3.1.10",
23+
"express": "^5.1.0",
24+
"express-ejs-layouts": "^2.5.1"
25+
}
26+
}
8.93 KB
Loading

0 commit comments

Comments
 (0)