passport-magic-code
Version:
A passwordless passport strategy to send a magic code (One time password) to let the user authenticate themselves.
212 lines (155 loc) โข 4.83 kB
Markdown
# ๐ passport-magic-code
A flexible, plug-and-play Passport strategy for **passwordless login**, registration, and callback-based authentication using magic codes (OTP-like).
Built with:
- โ
**TypeScript** and **Zod** for validation and type-safety
- โก๏ธ Customizable storage (in-memory, database, etc.)
- ๐ฌ Pluggable logic for sending and processing codes
- ๐ง Simple interface that handles login, registration, and callbacks
## โ ๏ธ Zod Compatibility Notice
- Version `^1.0.0` uses **Zod v3**
- Version `^2.0.0` and later uses **Zod v4**
## ๐ฆ Install
```bash
npm install passport-magic-code
```
## โจ Features
- ๐ Magic code authentication (e.g. 6-digit OTP via email or SMS)
- ๐ฌ Bring your own code delivery function (email, SMS, etc.)
- ๐ง Validates schema and logic with `zod`
- โ๏ธ Works with any Express-based app using `passport`
- โณ Code expiration and single-use handling
## ๐ Usage Example
```ts
import { Strategy as MagicCodeStrategy } from "passport-magic-code";
import { v4 as uuidv4 } from "uuid";
const magicCode = new MagicCodeStrategy(
{
secret: process.env.MAGIC_CODE_SECRET,
codeLength: 6,
userPrimaryKey: "email",
codeField: "code",
expiresIn: 15, // minutes
storage: {
codes: {},
set: async (key, value) => {
await db.otps.create({ code: key, value });
},
get: async (key) => {
return (await db.otps.findOne({ code: key }))?.value;
},
delete: async (key) => {
await db.otps.deleteOne({ code: key });
},
},
},
// sendCode(user, code, options)
async ({ email, ...user }, code, { action }) => {
const existingUser = await db.users.findOne({ email });
if (action === "login" && !existingUser) return;
if (action === "register" && (existingUser || !email)) {
return {
error: "User already exists",
statusCode: 400,
};
}
await sendEmail({
to: email,
subject: "Your Login Code",
html: `<p>Your code is: <strong>${code}</strong></p>`,
});
},
// callback(user, options)
async ({ email, ...user }) => {
let account = await db.users.findOne({ email });
if (!account) {
account = await db.users.create({
id: uuidv4(),
email,
...user,
createdAt: new Date(),
});
await db.orgs.create({
uid: account.id,
id: "personal",
profile: {
name: "Personal",
description: "Your default organization",
},
});
}
return account;
}
);
```
## ๐งฉ Configuration
### Required Args
| Option | Type | Default | Description |
| ---------------- | --------------- | ------- | ------------------------------------------- |
| `secret` | `string` | โ | Secret used internally (min 16 chars) |
| `codeLength` | `number` | `4` | Length of the OTP code |
| `storage` | `MemoryStorage` | โ | Your code storage interface (see below) |
| `expiresIn` | `number` | `30` | Minutes until code expires |
| `userPrimaryKey` | `string` | `email` | Field used to identify the user |
| `codeField` | `string` | `code` | Field to look for code in body/query/params |
## ๐ฆ Storage Interface
Implement a `MemoryStorage` object like so:
```ts
const storage = {
codes: {},
async set(key, value) {
// Save to DB or in-memory store
},
async get(key) {
// Return the stored value
},
async delete(key) {
// Delete the key after it's used
},
};
```
## ๐ `sendCode(user, code, options)`
Use this function to send the generated code to the user via:
- ๐ง Email
- ๐ฑ SMS
- ๐ Push notification
This function is **called during login/register actions**.
## ๐ `callback(user, options)`
This is where you:
- Lookup or create the user
- Return the user object to `passport`
- Attach session or token logic as needed
This function is **called when the user submits the correct code**.
## ๐ API
### Actions (`options.action`)
- `"login"`: Login flow (fail silently if user not found)
- `"register"`: Register flow (fail if user exists or info is incomplete)
- `"callback"`: Validate code and complete login
### Strategy Usage
```ts
passport.use("magic-code", magicCode);
app.post(
"/auth/send",
passport.authenticate("magic-code", { action: "login" })
);
app.post(
"/auth/callback",
passport.authenticate("magic-code", { action: "callback" })
);
```
## ๐งช Development
- Built in TypeScript
- Schema validation with Zod
- Fully type-safe, async/await-first
## ๐ License
MIT ยฉ 2025