otpkit.js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/package.json +45 -0
- package/src/generate.js +14 -0
- package/src/index.js +2 -0
- package/src/utils.js +18 -0
- package/src/verify.js +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 otpkit.js
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/npm/v/otpkit.js?style=flat-square&color=blue" alt="npm version" />
|
|
3
|
+
<img src="https://img.shields.io/npm/l/otpkit.js?style=flat-square&color=green" alt="license" />
|
|
4
|
+
<img src="https://img.shields.io/npm/dt/otpkit.js?style=flat-square&color=orange" alt="downloads" />
|
|
5
|
+
<img src="https://img.shields.io/badge/node-%3E%3D14.0.0-brightgreen?style=flat-square" alt="node version" />
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
# 🔐 otpkit.js
|
|
9
|
+
|
|
10
|
+
**Lightweight, secure OTP generation and verification for Node.js applications.**
|
|
11
|
+
|
|
12
|
+
A simple, framework-agnostic library for generating and verifying one-time passwords (OTPs) with bcrypt hashing and built-in expiry management. Perfect for email/SMS verification, password resets, and two-factor authentication flows.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ✨ Features
|
|
17
|
+
|
|
18
|
+
- **🔒 Secure by Default** — OTPs are hashed using bcrypt before storage
|
|
19
|
+
- **⏱️ Built-in Expiry** — Automatic TTL (time-to-live) handling
|
|
20
|
+
- **🎯 Zero Configuration** — Works out of the box with sensible defaults
|
|
21
|
+
- **📦 Lightweight** — Minimal dependencies, small footprint
|
|
22
|
+
- **🔧 Framework Agnostic** — Works with Express, Fastify, Koa, Hono, or vanilla Node.js
|
|
23
|
+
- **🎲 Cryptographically Random** — Uses Node.js `crypto.randomInt()` for secure OTP generation
|
|
24
|
+
- **📝 TypeScript Friendly** — Clean API with predictable return types
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 📦 Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install otpkit.js
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yarn add otpkit.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add otpkit.js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🚀 Quick Start
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import { createOTP, verifyOTP } from "otpkit.js";
|
|
48
|
+
|
|
49
|
+
// Generate an OTP
|
|
50
|
+
const { otp, hash, expiresAt } = await createOTP();
|
|
51
|
+
|
|
52
|
+
console.log(otp); // "482916" → Send this to the user (email/SMS)
|
|
53
|
+
console.log(hash); // "$2a$10$..." → Store this in your database
|
|
54
|
+
console.log(expiresAt); // 1703001234567 → Unix timestamp when OTP expires
|
|
55
|
+
|
|
56
|
+
// Later, verify the OTP entered by the user
|
|
57
|
+
const result = await verifyOTP({
|
|
58
|
+
otp: "482916", // User's input
|
|
59
|
+
hash: hash, // Retrieved from database
|
|
60
|
+
expiresAt: expiresAt
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (result.valid) {
|
|
64
|
+
console.log("OTP verified successfully!");
|
|
65
|
+
} else {
|
|
66
|
+
console.log("Verification failed:", result.reason);
|
|
67
|
+
// result.reason: "OTP_EXPIRED" | "OTP_INVALID"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 📖 API Reference
|
|
74
|
+
|
|
75
|
+
### `createOTP(options?)`
|
|
76
|
+
|
|
77
|
+
Generates a new OTP with a bcrypt hash.
|
|
78
|
+
|
|
79
|
+
#### Parameters
|
|
80
|
+
|
|
81
|
+
| Parameter | Type | Default | Description |
|
|
82
|
+
|-----------|----------|---------|--------------------------------------|
|
|
83
|
+
| `length` | `number` | `6` | Length of the OTP (minimum: 4) |
|
|
84
|
+
| `ttl` | `number` | `300` | Time-to-live in seconds (5 minutes) |
|
|
85
|
+
|
|
86
|
+
#### Returns
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
{
|
|
90
|
+
otp: string, // The plaintext OTP to send to the user
|
|
91
|
+
hash: string, // Bcrypt hash to store in your database
|
|
92
|
+
expiresAt: number, // Unix timestamp (ms) when the OTP expires
|
|
93
|
+
ttl: number // The TTL value used
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Examples
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
// Default: 6-digit OTP, 5-minute expiry
|
|
101
|
+
const { otp, hash, expiresAt } = await createOTP();
|
|
102
|
+
|
|
103
|
+
// Custom: 8-digit OTP, 10-minute expiry
|
|
104
|
+
const { otp, hash, expiresAt } = await createOTP({ length: 8, ttl: 600 });
|
|
105
|
+
|
|
106
|
+
// Short-lived: 4-digit OTP, 2-minute expiry
|
|
107
|
+
const { otp, hash, expiresAt } = await createOTP({ length: 4, ttl: 120 });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### `verifyOTP(options)`
|
|
113
|
+
|
|
114
|
+
Verifies an OTP against its hash, checking both validity and expiration.
|
|
115
|
+
|
|
116
|
+
#### Parameters
|
|
117
|
+
|
|
118
|
+
| Parameter | Type | Required | Description |
|
|
119
|
+
|-------------|----------|----------|----------------------------------------|
|
|
120
|
+
| `otp` | `string` | Yes | The OTP entered by the user |
|
|
121
|
+
| `hash` | `string` | Yes | The bcrypt hash stored in your database|
|
|
122
|
+
| `expiresAt` | `number` | Yes | The expiration timestamp |
|
|
123
|
+
|
|
124
|
+
#### Returns
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
{
|
|
128
|
+
valid: boolean, // Whether the OTP is valid
|
|
129
|
+
reason: string | null // null if valid, otherwise "OTP_EXPIRED" or "OTP_INVALID"
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Examples
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
// Successful verification
|
|
137
|
+
const result = await verifyOTP({ otp: "482916", hash, expiresAt });
|
|
138
|
+
// { valid: true, reason: null }
|
|
139
|
+
|
|
140
|
+
// Expired OTP
|
|
141
|
+
const result = await verifyOTP({ otp: "482916", hash, expiresAt: Date.now() - 1000 });
|
|
142
|
+
// { valid: false, reason: "OTP_EXPIRED" }
|
|
143
|
+
|
|
144
|
+
// Invalid OTP
|
|
145
|
+
const result = await verifyOTP({ otp: "000000", hash, expiresAt });
|
|
146
|
+
// { valid: false, reason: "OTP_INVALID" }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 💡 Usage Examples
|
|
152
|
+
|
|
153
|
+
### Email Verification Flow
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
import { createOTP, verifyOTP } from "otpkit.js";
|
|
157
|
+
import { sendEmail } from "./your-email-service.js";
|
|
158
|
+
import { db } from "./your-database.js";
|
|
159
|
+
|
|
160
|
+
// Step 1: User requests email verification
|
|
161
|
+
async function requestEmailVerification(userId, email) {
|
|
162
|
+
const { otp, hash, expiresAt } = await createOTP({ ttl: 600 }); // 10 min expiry
|
|
163
|
+
|
|
164
|
+
// Store hash and expiry in database
|
|
165
|
+
await db.users.update(userId, {
|
|
166
|
+
emailVerificationHash: hash,
|
|
167
|
+
emailVerificationExpiry: expiresAt
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Send OTP to user's email
|
|
171
|
+
await sendEmail({
|
|
172
|
+
to: email,
|
|
173
|
+
subject: "Your Verification Code",
|
|
174
|
+
body: `Your verification code is: ${otp}`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Step 2: User submits the OTP
|
|
179
|
+
async function verifyEmail(userId, userOtp) {
|
|
180
|
+
const user = await db.users.findById(userId);
|
|
181
|
+
|
|
182
|
+
const result = await verifyOTP({
|
|
183
|
+
otp: userOtp,
|
|
184
|
+
hash: user.emailVerificationHash,
|
|
185
|
+
expiresAt: user.emailVerificationExpiry
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (result.valid) {
|
|
189
|
+
await db.users.update(userId, {
|
|
190
|
+
emailVerified: true,
|
|
191
|
+
emailVerificationHash: null,
|
|
192
|
+
emailVerificationExpiry: null
|
|
193
|
+
});
|
|
194
|
+
return { success: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { success: false, error: result.reason };
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Express.js Integration
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
import express from "express";
|
|
205
|
+
import { createOTP, verifyOTP } from "otpkit.js";
|
|
206
|
+
|
|
207
|
+
const app = express();
|
|
208
|
+
app.use(express.json());
|
|
209
|
+
|
|
210
|
+
// In-memory store (use Redis/DB in production)
|
|
211
|
+
const otpStore = new Map();
|
|
212
|
+
|
|
213
|
+
app.post("/api/otp/send", async (req, res) => {
|
|
214
|
+
const { phone } = req.body;
|
|
215
|
+
const { otp, hash, expiresAt } = await createOTP();
|
|
216
|
+
|
|
217
|
+
otpStore.set(phone, { hash, expiresAt });
|
|
218
|
+
|
|
219
|
+
// Send OTP via SMS service
|
|
220
|
+
console.log(`Send OTP ${otp} to ${phone}`);
|
|
221
|
+
|
|
222
|
+
res.json({ message: "OTP sent successfully" });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
app.post("/api/otp/verify", async (req, res) => {
|
|
226
|
+
const { phone, otp } = req.body;
|
|
227
|
+
const stored = otpStore.get(phone);
|
|
228
|
+
|
|
229
|
+
if (!stored) {
|
|
230
|
+
return res.status(400).json({ error: "No OTP found for this number" });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result = await verifyOTP({
|
|
234
|
+
otp,
|
|
235
|
+
hash: stored.hash,
|
|
236
|
+
expiresAt: stored.expiresAt
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (result.valid) {
|
|
240
|
+
otpStore.delete(phone);
|
|
241
|
+
return res.json({ success: true });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
res.status(400).json({ error: result.reason });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
app.listen(3000);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 🔒 Security Considerations
|
|
253
|
+
|
|
254
|
+
1. **Never log or expose the plaintext OTP** — Only send it to the user via a secure channel
|
|
255
|
+
2. **Store only the hash** — Never store the plaintext OTP in your database
|
|
256
|
+
3. **Implement rate limiting** — Prevent brute-force attacks on OTP verification
|
|
257
|
+
4. **Use HTTPS** — Always transmit OTPs over encrypted connections
|
|
258
|
+
5. **Consider attempt limits** — Lock out users after multiple failed verification attempts
|
|
259
|
+
6. **Clean up expired OTPs** — Regularly purge expired OTP records from your database
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## 🤝 Contributing
|
|
264
|
+
|
|
265
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
266
|
+
|
|
267
|
+
1. Fork the repository
|
|
268
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
269
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
270
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
271
|
+
5. Open a Pull Request
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 📄 License
|
|
276
|
+
|
|
277
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 👤 Author
|
|
282
|
+
|
|
283
|
+
**Ashutosh Swamy**
|
|
284
|
+
|
|
285
|
+
- GitHub: [@ashutoshswamy](https://github.com/ashutoshswamy)
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
<p align="center">
|
|
290
|
+
Made with ❤️ for the Node.js community
|
|
291
|
+
</p>
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "otpkit.js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight, secure OTP generation and verification for Node.js applications. Framework-agnostic, production-ready, and easy to integrate.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"otp",
|
|
7
|
+
"one-time-password",
|
|
8
|
+
"authentication",
|
|
9
|
+
"auth",
|
|
10
|
+
"security",
|
|
11
|
+
"node",
|
|
12
|
+
"nodejs",
|
|
13
|
+
"backend",
|
|
14
|
+
"login",
|
|
15
|
+
"signup",
|
|
16
|
+
"verification",
|
|
17
|
+
"password-reset",
|
|
18
|
+
"mfa",
|
|
19
|
+
"2fa",
|
|
20
|
+
"email-otp",
|
|
21
|
+
"sms-otp",
|
|
22
|
+
"bcrypt",
|
|
23
|
+
"hashing",
|
|
24
|
+
"framework-agnostic",
|
|
25
|
+
"utility"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://github.com/ashutoshswamy/otpkit.js#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/ashutoshswamy/otpkit.js/issues"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/ashutoshswamy/otpkit.js.git"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"author": "Ashutosh Swamy",
|
|
37
|
+
"type": "module",
|
|
38
|
+
"main": "src/index.js",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"bcryptjs": "^3.0.3"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/generate.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
import { generateNumericOTP } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export async function createOTP({ length = 6, ttl = 300 } = {}) {
|
|
5
|
+
const otp = generateNumericOTP(length);
|
|
6
|
+
const hash = await bcrypt.hash(otp, 10);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
otp, // send to user
|
|
10
|
+
hash, // store in DB
|
|
11
|
+
expiresAt: Date.now() + ttl * 1000,
|
|
12
|
+
ttl,
|
|
13
|
+
};
|
|
14
|
+
}
|
package/src/index.js
ADDED
package/src/utils.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
export function generateNumericOTP(length = 6) {
|
|
4
|
+
if (length < 4) {
|
|
5
|
+
throw new Error("OTP length must be at least 4");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let otp = "";
|
|
9
|
+
for (let i = 0; i < length; i++) {
|
|
10
|
+
otp += crypto.randomInt(0, 10);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return otp;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isExpired(expiresAt) {
|
|
17
|
+
return Date.now() > expiresAt;
|
|
18
|
+
}
|
package/src/verify.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
import { isExpired } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export async function verifyOTP({ otp, hash, expiresAt }) {
|
|
5
|
+
if (isExpired(expiresAt)) {
|
|
6
|
+
return { valid: false, reason: "OTP_EXPIRED" };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const match = await bcrypt.compare(otp, hash);
|
|
10
|
+
|
|
11
|
+
return match
|
|
12
|
+
? { valid: true, reason: null }
|
|
13
|
+
: { valid: false, reason: "OTP_INVALID" };
|
|
14
|
+
}
|