@flink-app/otp-auth-plugin 0.12.1-alpha.40

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +879 -0
  3. package/dist/OtpAuthPlugin.d.ts +64 -0
  4. package/dist/OtpAuthPlugin.js +231 -0
  5. package/dist/OtpAuthPluginContext.d.ts +10 -0
  6. package/dist/OtpAuthPluginContext.js +2 -0
  7. package/dist/OtpAuthPluginOptions.d.ts +112 -0
  8. package/dist/OtpAuthPluginOptions.js +2 -0
  9. package/dist/OtpInternalContext.d.ts +11 -0
  10. package/dist/OtpInternalContext.js +2 -0
  11. package/dist/functions/initiate.d.ts +18 -0
  12. package/dist/functions/initiate.js +104 -0
  13. package/dist/functions/verify.d.ts +20 -0
  14. package/dist/functions/verify.js +142 -0
  15. package/dist/handlers/PostOtpInitiate.d.ts +7 -0
  16. package/dist/handlers/PostOtpInitiate.js +70 -0
  17. package/dist/handlers/PostOtpVerify.d.ts +7 -0
  18. package/dist/handlers/PostOtpVerify.js +86 -0
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.js +24 -0
  21. package/dist/repos/OtpSessionRepo.d.ts +13 -0
  22. package/dist/repos/OtpSessionRepo.js +145 -0
  23. package/dist/schemas/InitiateRequest.d.ts +8 -0
  24. package/dist/schemas/InitiateRequest.js +2 -0
  25. package/dist/schemas/InitiateResponse.d.ts +8 -0
  26. package/dist/schemas/InitiateResponse.js +2 -0
  27. package/dist/schemas/OtpSession.d.ts +25 -0
  28. package/dist/schemas/OtpSession.js +2 -0
  29. package/dist/schemas/VerifyRequest.d.ts +6 -0
  30. package/dist/schemas/VerifyRequest.js +2 -0
  31. package/dist/schemas/VerifyResponse.d.ts +12 -0
  32. package/dist/schemas/VerifyResponse.js +2 -0
  33. package/dist/utils/otp-utils.d.ts +43 -0
  34. package/dist/utils/otp-utils.js +95 -0
  35. package/examples/basic-usage.ts +145 -0
  36. package/package.json +37 -0
  37. package/spec/OtpAuthPlugin.spec.ts +159 -0
  38. package/spec/OtpSessionRepo.spec.ts +194 -0
  39. package/spec/otp-utils.spec.ts +172 -0
  40. package/spec/support/jasmine.json +7 -0
  41. package/src/OtpAuthPlugin.ts +163 -0
  42. package/src/OtpAuthPluginContext.ts +11 -0
  43. package/src/OtpAuthPluginOptions.ts +135 -0
  44. package/src/OtpInternalContext.ts +12 -0
  45. package/src/functions/initiate.ts +86 -0
  46. package/src/functions/verify.ts +123 -0
  47. package/src/handlers/PostOtpInitiate.ts +28 -0
  48. package/src/handlers/PostOtpVerify.ts +42 -0
  49. package/src/index.ts +17 -0
  50. package/src/repos/OtpSessionRepo.ts +47 -0
  51. package/src/schemas/InitiateRequest.ts +8 -0
  52. package/src/schemas/InitiateResponse.ts +8 -0
  53. package/src/schemas/OtpSession.ts +25 -0
  54. package/src/schemas/VerifyRequest.ts +6 -0
  55. package/src/schemas/VerifyResponse.ts +12 -0
  56. package/src/utils/otp-utils.ts +89 -0
  57. package/tsconfig.dist.json +4 -0
  58. package/tsconfig.json +24 -0
package/README.md ADDED
@@ -0,0 +1,879 @@
1
+ # OTP Auth Plugin
2
+
3
+ A flexible OTP (One-Time Password) authentication plugin for Flink that supports both SMS and email delivery methods with MongoDB session storage, JWT token generation, and configurable security settings.
4
+
5
+ ## Features
6
+
7
+ - OTP code generation with configurable length (4-8 digits)
8
+ - Support for both SMS and email delivery
9
+ - Configurable code expiration (TTL)
10
+ - Rate limiting with maximum verification attempts
11
+ - Session locking after too many failed attempts
12
+ - MongoDB session storage with automatic TTL cleanup
13
+ - Built-in HTTP endpoints for OTP flow
14
+ - TypeScript support with full type safety
15
+ - Cryptographically secure code generation
16
+ - Identifier validation and normalization
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @flink-app/otp-auth-plugin @flink-app/jwt-auth-plugin
22
+ ```
23
+
24
+ ## Prerequisites
25
+
26
+ ### 1. JWT Auth Plugin Dependency
27
+
28
+ This plugin requires `@flink-app/jwt-auth-plugin` to be installed and configured. The OTP Auth Plugin uses the JWT Auth Plugin to generate authentication tokens after successful verification.
29
+
30
+ ### 2. SMS/Email Delivery
31
+
32
+ You need an SMS or email delivery mechanism. This can be another Flink plugin or external service:
33
+
34
+ - **SMS**: Use `@flink-app/sms-plugin` or services like Twilio, AWS SNS
35
+ - **Email**: Use `@flink-app/email-plugin` or services like SendGrid, AWS SES
36
+
37
+ ### 3. MongoDB Connection
38
+
39
+ The plugin requires MongoDB to store OTP sessions.
40
+
41
+ ## Quick Start
42
+
43
+ ```typescript
44
+ import { FlinkApp } from "@flink-app/flink";
45
+ import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
46
+ import { otpAuthPlugin } from "@flink-app/otp-auth-plugin";
47
+ import { smsPlugin } from "@flink-app/sms-plugin";
48
+ import { emailPlugin } from "@flink-app/email-plugin";
49
+ import { Context } from "./Context";
50
+
51
+ const app = new FlinkApp<Context>({
52
+ name: "My App",
53
+
54
+ // JWT Auth Plugin MUST be configured first
55
+ auth: jwtAuthPlugin({
56
+ secret: process.env.JWT_SECRET!,
57
+ getUser: async (tokenData) => {
58
+ return await app.ctx.repos.userRepo.getById(tokenData.userId);
59
+ },
60
+ rolePermissions: {
61
+ user: ["read", "write"],
62
+ admin: ["read", "write", "delete"],
63
+ },
64
+ }),
65
+
66
+ db: {
67
+ uri: process.env.MONGODB_URI!,
68
+ },
69
+
70
+ plugins: [
71
+ // SMS and email plugins for delivery
72
+ smsPlugin({
73
+ provider: "twilio",
74
+ accountSid: process.env.TWILIO_ACCOUNT_SID!,
75
+ authToken: process.env.TWILIO_AUTH_TOKEN!,
76
+ fromNumber: process.env.TWILIO_FROM_NUMBER!,
77
+ }),
78
+ emailPlugin({
79
+ provider: "sendgrid",
80
+ apiKey: process.env.SENDGRID_API_KEY!,
81
+ fromEmail: "noreply@myapp.com",
82
+ }),
83
+
84
+ // OTP Auth Plugin
85
+ otpAuthPlugin({
86
+ codeLength: 6, // 6-digit code
87
+ codeTTL: 300, // 5 minutes
88
+ maxAttempts: 3, // Lock after 3 failed attempts
89
+
90
+ // Callback to send the OTP code
91
+ onSendCode: async (code, identifier, method) => {
92
+ if (method === "sms") {
93
+ await app.ctx.plugins.sms.send({
94
+ to: identifier,
95
+ body: `Your verification code is: ${code}. Valid for 5 minutes.`,
96
+ });
97
+ } else {
98
+ await app.ctx.plugins.email.send({
99
+ to: identifier,
100
+ subject: "Your Verification Code",
101
+ text: `Your verification code is: ${code}. Valid for 5 minutes.`,
102
+ html: `<p>Your verification code is: <strong>${code}</strong></p><p>Valid for 5 minutes.</p>`,
103
+ });
104
+ }
105
+ return true;
106
+ },
107
+
108
+ // Callback to retrieve user by identifier
109
+ onGetUser: async (identifier, method) => {
110
+ if (method === "sms") {
111
+ return await app.ctx.repos.userRepo.findOne({ phoneNumber: identifier });
112
+ } else {
113
+ return await app.ctx.repos.userRepo.findOne({ email: identifier });
114
+ }
115
+ },
116
+
117
+ // Callback after successful verification
118
+ onVerifySuccess: async (user, identifier, method) => {
119
+ const token = await app.ctx.auth.createToken({ userId: user._id, email: user.email }, user.roles);
120
+
121
+ return {
122
+ user: {
123
+ id: user._id,
124
+ email: user.email,
125
+ phoneNumber: user.phoneNumber,
126
+ name: user.name,
127
+ },
128
+ token,
129
+ };
130
+ },
131
+ }),
132
+ ],
133
+ });
134
+
135
+ await app.start();
136
+ ```
137
+
138
+ ## Configuration
139
+
140
+ ### OtpAuthPluginOptions
141
+
142
+ | Option | Type | Required | Default | Description |
143
+ | --------------------------- | ---------- | -------- | ---------------- | --------------------------------------------- |
144
+ | `codeLength` | `number` | No | `6` | Number of digits in OTP code (4-8) |
145
+ | `codeTTL` | `number` | No | `300` | Code validity in seconds (default: 5 minutes) |
146
+ | `maxAttempts` | `number` | No | `3` | Max verification attempts before lock |
147
+ | `onSendCode` | `Function` | Yes | - | Callback to send OTP code |
148
+ | `onGetUser` | `Function` | Yes | - | Callback to retrieve user by identifier |
149
+ | `onVerifySuccess` | `Function` | Yes | - | Callback after successful verification |
150
+ | `keepSessionsSec` | `number` | No | `86400` | Session retention in seconds (24 hours) |
151
+ | `otpSessionsCollectionName` | `string` | No | `"otp_sessions"` | MongoDB collection name |
152
+ | `registerRoutes` | `boolean` | No | `true` | Register built-in HTTP endpoints |
153
+
154
+ ### Callback Functions
155
+
156
+ #### onSendCode
157
+
158
+ Send the OTP code to the user via SMS or email.
159
+
160
+ ```typescript
161
+ onSendCode: async (
162
+ code: string,
163
+ identifier: string,
164
+ method: "sms" | "email",
165
+ payload?: Record<string, any>
166
+ ) => Promise<boolean>
167
+ ```
168
+
169
+ **Parameters:**
170
+
171
+ - `code` - The generated OTP code (e.g., "123456")
172
+ - `identifier` - User identifier (phone number or email)
173
+ - `method` - Delivery method ("sms" or "email")
174
+ - `payload` - Optional custom data from initiation
175
+
176
+ **Returns:** `true` if sent successfully, `false` otherwise
177
+
178
+ **Example:**
179
+
180
+ ```typescript
181
+ onSendCode: async (code, identifier, method, payload) => {
182
+ if (method === "sms") {
183
+ await ctx.plugins.sms.send({
184
+ to: identifier,
185
+ body: `Your code: ${code}`,
186
+ });
187
+ } else {
188
+ await ctx.plugins.email.send({
189
+ to: identifier,
190
+ subject: "Your verification code",
191
+ text: `Your code is: ${code}`,
192
+ });
193
+ }
194
+ return true;
195
+ };
196
+ ```
197
+
198
+ #### onGetUser
199
+
200
+ Retrieve user by their identifier (phone number or email).
201
+
202
+ ```typescript
203
+ onGetUser: async (
204
+ identifier: string,
205
+ method: "sms" | "email",
206
+ payload?: Record<string, any>
207
+ ) => Promise<any | null>
208
+ ```
209
+
210
+ **Parameters:**
211
+
212
+ - `identifier` - User identifier (normalized phone/email)
213
+ - `method` - Delivery method ("sms" or "email")
214
+ - `payload` - Optional custom data from initiation
215
+
216
+ **Returns:** User object or `null` if not found
217
+
218
+ **Example:**
219
+
220
+ ```typescript
221
+ onGetUser: async (identifier, method) => {
222
+ if (method === "sms") {
223
+ return await ctx.repos.userRepo.findOne({ phoneNumber: identifier });
224
+ } else {
225
+ return await ctx.repos.userRepo.findOne({ email: identifier });
226
+ }
227
+ };
228
+ ```
229
+
230
+ #### onVerifySuccess
231
+
232
+ Generate JWT token and return user data after successful verification.
233
+
234
+ ```typescript
235
+ onVerifySuccess: async (
236
+ user: any,
237
+ identifier: string,
238
+ method: "sms" | "email",
239
+ payload?: Record<string, any>
240
+ ) => Promise<{
241
+ user: any;
242
+ token: string;
243
+ }>
244
+ ```
245
+
246
+ **Parameters:**
247
+
248
+ - `user` - User object from `onGetUser`
249
+ - `identifier` - User identifier
250
+ - `method` - Delivery method
251
+ - `payload` - Optional custom data from initiation
252
+
253
+ **Returns:** Object with user and JWT token
254
+
255
+ **Example:**
256
+
257
+ ```typescript
258
+ onVerifySuccess: async (user, identifier, method, payload) => {
259
+ // Generate JWT token
260
+ const token = await ctx.auth.createToken({ userId: user._id, email: user.email }, user.roles);
261
+
262
+ // Update last login time
263
+ await ctx.repos.userRepo.updateOne(user._id, {
264
+ lastLoginAt: new Date(),
265
+ lastLoginMethod: method,
266
+ });
267
+
268
+ return {
269
+ user: {
270
+ id: user._id,
271
+ email: user.email,
272
+ name: user.name,
273
+ },
274
+ token,
275
+ };
276
+ };
277
+ ```
278
+
279
+ ## OTP Authentication Flow
280
+
281
+ ### Complete Flow
282
+
283
+ 1. User enters phone number or email
284
+ 2. Client calls `/otp/initiate` with identifier and method
285
+ 3. Plugin generates OTP code and creates session
286
+ 4. Plugin calls `onSendCode` to deliver code
287
+ 5. User receives code via SMS or email
288
+ 6. User enters code in client
289
+ 7. Client calls `/otp/verify` with session ID and code
290
+ 8. Plugin validates code and checks attempts/expiration
291
+ 9. Plugin calls `onGetUser` to fetch user
292
+ 10. Plugin calls `onVerifySuccess` to generate JWT token
293
+ 11. Plugin returns user and token to client
294
+ 12. Client stores JWT token for authenticated requests
295
+
296
+ ### Initiate OTP
297
+
298
+ ```
299
+ POST /otp/initiate
300
+ ```
301
+
302
+ **Request Body:**
303
+
304
+ ```json
305
+ {
306
+ "identifier": "+46701234567",
307
+ "method": "sms",
308
+ "payload": {
309
+ "returnUrl": "/dashboard"
310
+ }
311
+ }
312
+ ```
313
+
314
+ **Response:**
315
+
316
+ ```json
317
+ {
318
+ "data": {
319
+ "sessionId": "a1b2c3d4e5f6...",
320
+ "expiresAt": "2025-01-02T12:35:00.000Z",
321
+ "ttl": 300
322
+ }
323
+ }
324
+ ```
325
+
326
+ ### Verify OTP
327
+
328
+ ```
329
+ POST /otp/verify
330
+ ```
331
+
332
+ **Request Body:**
333
+
334
+ ```json
335
+ {
336
+ "sessionId": "a1b2c3d4e5f6...",
337
+ "code": "123456"
338
+ }
339
+ ```
340
+
341
+ **Response (Success - 200):**
342
+
343
+ ```json
344
+ {
345
+ "data": {
346
+ "status": "success",
347
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
348
+ "user": {
349
+ "id": "507f1f77bcf86cd799439011",
350
+ "email": "user@example.com",
351
+ "name": "John Doe"
352
+ }
353
+ }
354
+ }
355
+ ```
356
+
357
+ **Response (Invalid Code - 401):**
358
+
359
+ ```json
360
+ {
361
+ "data": {
362
+ "status": "invalid_code",
363
+ "message": "Invalid verification code",
364
+ "remainingAttempts": 2
365
+ }
366
+ }
367
+ ```
368
+
369
+ **Response (Locked - 403):**
370
+
371
+ ```json
372
+ {
373
+ "data": {
374
+ "status": "locked",
375
+ "message": "Too many failed attempts. Session is locked.",
376
+ "remainingAttempts": 0
377
+ }
378
+ }
379
+ ```
380
+
381
+ **Response (Expired - 403):**
382
+
383
+ ```json
384
+ {
385
+ "data": {
386
+ "status": "expired",
387
+ "message": "Verification code has expired"
388
+ }
389
+ }
390
+ ```
391
+
392
+ **Response (Not Found - 404):**
393
+
394
+ ```json
395
+ {
396
+ "data": {
397
+ "status": "not_found",
398
+ "message": "Session not found or expired"
399
+ }
400
+ }
401
+ ```
402
+
403
+ ## Context API
404
+
405
+ The plugin exposes methods via `ctx.plugins.otpAuth`:
406
+
407
+ ### initiate
408
+
409
+ Programmatically initiate OTP authentication.
410
+
411
+ ```typescript
412
+ const result = await ctx.plugins.otpAuth.initiate({
413
+ identifier: "+46701234567",
414
+ method: "sms",
415
+ payload: { customData: "value" },
416
+ });
417
+
418
+ // Returns: { sessionId, expiresAt, ttl }
419
+ ```
420
+
421
+ ### verify
422
+
423
+ Programmatically verify OTP code.
424
+
425
+ ```typescript
426
+ const result = await ctx.plugins.otpAuth.verify({
427
+ sessionId: "a1b2c3d4e5f6...",
428
+ code: "123456",
429
+ });
430
+
431
+ // Returns: { status, token?, user?, remainingAttempts?, message? }
432
+ ```
433
+
434
+ ## Identifier Normalization
435
+
436
+ The plugin automatically normalizes identifiers for consistency:
437
+
438
+ ### Phone Numbers
439
+
440
+ - Removes spaces, dashes, parentheses: `+46 (70) 123-4567` → `+46701234567`
441
+ - Preserves `+` prefix for country codes
442
+ - Accepts various formats: `+1 (555) 123-4567`, `555-123-4567`, `5551234567`
443
+
444
+ ### Email Addresses
445
+
446
+ - Converts to lowercase: `User@Example.COM` → `user@example.com`
447
+ - Trims whitespace
448
+ - Basic validation: must contain `@` and domain
449
+
450
+ ## Security Best Practices
451
+
452
+ ### 1. Code Length and TTL
453
+
454
+ Balance security and usability:
455
+
456
+ ```typescript
457
+ otpAuthPlugin({
458
+ codeLength: 6, // 6 digits = 1 million combinations
459
+ codeTTL: 300, // 5 minutes - short enough to prevent reuse
460
+ maxAttempts: 3, // Lock after 3 tries
461
+ });
462
+ ```
463
+
464
+ **Recommendations:**
465
+
466
+ - **4 digits**: Only for low-security scenarios (10,000 combinations)
467
+ - **6 digits**: Good balance for most use cases (1,000,000 combinations)
468
+ - **8 digits**: High security (100,000,000 combinations)
469
+
470
+ ### 2. Rate Limiting
471
+
472
+ Implement rate limiting on initiation to prevent SMS/email abuse:
473
+
474
+ ```typescript
475
+ import rateLimit from "express-rate-limit";
476
+
477
+ app.use(
478
+ "/otp/initiate",
479
+ rateLimit({
480
+ windowMs: 15 * 60 * 1000, // 15 minutes
481
+ max: 3, // 3 requests per window
482
+ message: "Too many OTP requests. Please try again later.",
483
+ })
484
+ );
485
+ ```
486
+
487
+ ### 3. User Verification
488
+
489
+ Always verify user exists before sending codes:
490
+
491
+ ```typescript
492
+ onSendCode: async (code, identifier, method) => {
493
+ // Check if user exists first
494
+ const user = await ctx.repos.userRepo.findOne({
495
+ [method === "sms" ? "phoneNumber" : "email"]: identifier,
496
+ });
497
+
498
+ if (!user) {
499
+ // Don't reveal user doesn't exist - silently fail
500
+ return true;
501
+ }
502
+
503
+ // Send code only if user exists
504
+ await sendCode(code, identifier, method);
505
+ return true;
506
+ };
507
+ ```
508
+
509
+ ### 4. HTTPS Only
510
+
511
+ Always use HTTPS in production to prevent code interception.
512
+
513
+ ### 5. Secrets Management
514
+
515
+ Never commit secrets to version control:
516
+
517
+ ```bash
518
+ # .env
519
+ JWT_SECRET=your_jwt_secret
520
+ TWILIO_ACCOUNT_SID=...
521
+ TWILIO_AUTH_TOKEN=...
522
+ SENDGRID_API_KEY=...
523
+ ```
524
+
525
+ ### 6. Session Cleanup
526
+
527
+ The plugin automatically cleans up old sessions using MongoDB TTL indexes. Configure retention based on compliance needs:
528
+
529
+ ```typescript
530
+ otpAuthPlugin({
531
+ keepSessionsSec: 86400, // 24 hours (default)
532
+ // keepSessionsSec: 3600, // 1 hour for stricter compliance
533
+ // keepSessionsSec: 604800, // 7 days for audit trails
534
+ });
535
+ ```
536
+
537
+ ### 7. Audit Logging
538
+
539
+ Log authentication attempts for security monitoring:
540
+
541
+ ```typescript
542
+ onVerifySuccess: async (user, identifier, method) => {
543
+ await ctx.repos.auditLogRepo.create({
544
+ userId: user._id,
545
+ action: "otp_login",
546
+ method,
547
+ identifier,
548
+ timestamp: new Date(),
549
+ ip: req.ip,
550
+ });
551
+
552
+ const token = await ctx.auth.createToken({ userId: user._id }, user.roles);
553
+ return { user, token };
554
+ };
555
+ ```
556
+
557
+ ## Client Integration Examples
558
+
559
+ ### React Web App
560
+
561
+ ```typescript
562
+ import React, { useState } from "react";
563
+
564
+ function OtpLogin() {
565
+ const [phone, setPhone] = useState("");
566
+ const [sessionId, setSessionId] = useState("");
567
+ const [code, setCode] = useState("");
568
+ const [step, setStep] = useState<"enter-phone" | "enter-code">("enter-phone");
569
+
570
+ const handleInitiate = async () => {
571
+ const response = await fetch("/otp/initiate", {
572
+ method: "POST",
573
+ headers: { "Content-Type": "application/json" },
574
+ body: JSON.stringify({
575
+ identifier: phone,
576
+ method: "sms",
577
+ }),
578
+ });
579
+
580
+ const { data } = await response.json();
581
+ setSessionId(data.sessionId);
582
+ setStep("enter-code");
583
+ };
584
+
585
+ const handleVerify = async () => {
586
+ const response = await fetch("/otp/verify", {
587
+ method: "POST",
588
+ headers: { "Content-Type": "application/json" },
589
+ body: JSON.stringify({
590
+ sessionId,
591
+ code,
592
+ }),
593
+ });
594
+
595
+ const { data } = await response.json();
596
+
597
+ if (data.status === "success") {
598
+ // Store token and redirect
599
+ localStorage.setItem("jwt_token", data.token);
600
+ window.location.href = "/dashboard";
601
+ } else {
602
+ alert(data.message || "Verification failed");
603
+ }
604
+ };
605
+
606
+ return (
607
+ <div>
608
+ {step === "enter-phone" && (
609
+ <div>
610
+ <h1>Login with SMS</h1>
611
+ <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Phone number" />
612
+ <button onClick={handleInitiate}>Send Code</button>
613
+ </div>
614
+ )}
615
+
616
+ {step === "enter-code" && (
617
+ <div>
618
+ <h1>Enter Verification Code</h1>
619
+ <input type="text" value={code} onChange={(e) => setCode(e.target.value)} placeholder="6-digit code" maxLength={6} />
620
+ <button onClick={handleVerify}>Verify</button>
621
+ </div>
622
+ )}
623
+ </div>
624
+ );
625
+ }
626
+ ```
627
+
628
+ ### React Native App
629
+
630
+ ```typescript
631
+ import React, { useState } from "react";
632
+ import { View, TextInput, Button, Alert } from "react-native";
633
+ import AsyncStorage from "@react-native-async-storage/async-storage";
634
+
635
+ function OtpLogin() {
636
+ const [phone, setPhone] = useState("");
637
+ const [sessionId, setSessionId] = useState("");
638
+ const [code, setCode] = useState("");
639
+ const [step, setStep] = useState<"enter-phone" | "enter-code">("enter-phone");
640
+
641
+ const handleInitiate = async () => {
642
+ const response = await fetch("https://api.myapp.com/otp/initiate", {
643
+ method: "POST",
644
+ headers: { "Content-Type": "application/json" },
645
+ body: JSON.stringify({
646
+ identifier: phone,
647
+ method: "sms",
648
+ }),
649
+ });
650
+
651
+ const { data } = await response.json();
652
+ setSessionId(data.sessionId);
653
+ setStep("enter-code");
654
+ };
655
+
656
+ const handleVerify = async () => {
657
+ const response = await fetch("https://api.myapp.com/otp/verify", {
658
+ method: "POST",
659
+ headers: { "Content-Type": "application/json" },
660
+ body: JSON.stringify({
661
+ sessionId,
662
+ code,
663
+ }),
664
+ });
665
+
666
+ const { data } = await response.json();
667
+
668
+ if (data.status === "success") {
669
+ await AsyncStorage.setItem("jwt_token", data.token);
670
+ // Navigate to home screen
671
+ } else {
672
+ Alert.alert("Error", data.message || "Verification failed");
673
+ }
674
+ };
675
+
676
+ return (
677
+ <View>
678
+ {step === "enter-phone" && (
679
+ <>
680
+ <TextInput value={phone} onChangeText={setPhone} placeholder="Phone number" keyboardType="phone-pad" />
681
+ <Button title="Send Code" onPress={handleInitiate} />
682
+ </>
683
+ )}
684
+
685
+ {step === "enter-code" && (
686
+ <>
687
+ <TextInput value={code} onChangeText={setCode} placeholder="6-digit code" keyboardType="number-pad" maxLength={6} />
688
+ <Button title="Verify" onPress={handleVerify} />
689
+ </>
690
+ )}
691
+ </View>
692
+ );
693
+ }
694
+ ```
695
+
696
+ ## Custom Payload Usage
697
+
698
+ Pass custom data through the OTP flow:
699
+
700
+ ```typescript
701
+ // Initiate with payload
702
+ const { data } = await fetch("/otp/initiate", {
703
+ method: "POST",
704
+ body: JSON.stringify({
705
+ identifier: "user@example.com",
706
+ method: "email",
707
+ payload: {
708
+ action: "password-reset",
709
+ returnUrl: "/new-password",
710
+ },
711
+ }),
712
+ });
713
+
714
+ // Access payload in callbacks
715
+ onVerifySuccess: async (user, identifier, method, payload) => {
716
+ if (payload?.action === "password-reset") {
717
+ // Handle password reset flow
718
+ return {
719
+ user,
720
+ token: await ctx.auth.createToken({ userId: user._id }, ["password-reset"]),
721
+ };
722
+ }
723
+
724
+ // Regular login flow
725
+ return {
726
+ user,
727
+ token: await ctx.auth.createToken({ userId: user._id }, user.roles),
728
+ };
729
+ };
730
+ ```
731
+
732
+ ## Error Handling
733
+
734
+ Handle errors gracefully in your callbacks:
735
+
736
+ ```typescript
737
+ onSendCode: async (code, identifier, method) => {
738
+ try {
739
+ if (method === "sms") {
740
+ await ctx.plugins.sms.send({
741
+ to: identifier,
742
+ body: `Your code: ${code}`,
743
+ });
744
+ } else {
745
+ await ctx.plugins.email.send({
746
+ to: identifier,
747
+ subject: "Verification code",
748
+ text: `Your code: ${code}`,
749
+ });
750
+ }
751
+ return true;
752
+ } catch (error) {
753
+ // Log error but don't expose details
754
+ console.error("Failed to send OTP:", error);
755
+ return false; // Return false to indicate failure
756
+ }
757
+ };
758
+ ```
759
+
760
+ ## TypeScript Types
761
+
762
+ ```typescript
763
+ import {
764
+ OtpAuthPluginOptions,
765
+ OtpSession,
766
+ InitiateRequest,
767
+ InitiateResponse,
768
+ VerifyRequest,
769
+ VerifyResponse,
770
+ OtpAuthPluginContext,
771
+ } from "@flink-app/otp-auth-plugin";
772
+
773
+ // OTP Session
774
+ interface OtpSession {
775
+ _id?: string;
776
+ sessionId: string;
777
+ identifier: string;
778
+ method: "sms" | "email";
779
+ code: string;
780
+ attempts: number;
781
+ maxAttempts: number;
782
+ status: "pending" | "verified" | "expired" | "locked";
783
+ createdAt: Date;
784
+ expiresAt: Date;
785
+ verifiedAt?: Date;
786
+ payload?: Record<string, any>;
787
+ }
788
+
789
+ // Initiate request/response
790
+ interface InitiateRequest {
791
+ identifier: string;
792
+ method: "sms" | "email";
793
+ payload?: Record<string, any>;
794
+ }
795
+
796
+ interface InitiateResponse {
797
+ sessionId: string;
798
+ expiresAt: Date;
799
+ ttl: number;
800
+ }
801
+
802
+ // Verify request/response
803
+ interface VerifyRequest {
804
+ sessionId: string;
805
+ code: string;
806
+ }
807
+
808
+ interface VerifyResponse {
809
+ status: "success" | "invalid_code" | "expired" | "locked" | "not_found";
810
+ token?: string;
811
+ user?: any;
812
+ remainingAttempts?: number;
813
+ message?: string;
814
+ }
815
+ ```
816
+
817
+ ## Troubleshooting
818
+
819
+ ### Code Not Received
820
+
821
+ **Issue:** User doesn't receive OTP code
822
+
823
+ **Solution:**
824
+
825
+ - Check `onSendCode` callback logs for errors
826
+ - Verify SMS/email service credentials
827
+ - Check identifier format (phone/email validation)
828
+ - Ensure user exists in database (if checking in `onSendCode`)
829
+
830
+ ### Session Not Found
831
+
832
+ **Issue:** Verify returns "Session not found"
833
+
834
+ **Solution:**
835
+
836
+ - Session may have expired (check `codeTTL`)
837
+ - Verify `sessionId` is passed correctly from initiate response
838
+ - Check MongoDB connection and collection name
839
+
840
+ ### Too Many Attempts
841
+
842
+ **Issue:** Session locked after failed verifications
843
+
844
+ **Solution:**
845
+
846
+ - User must request a new code via `/otp/initiate`
847
+ - Consider implementing admin unlock functionality
848
+ - Adjust `maxAttempts` if too restrictive
849
+
850
+ ### Invalid Identifier
851
+
852
+ **Issue:** Initiate fails with "Invalid phone number/email format"
853
+
854
+ **Solution:**
855
+
856
+ - Ensure phone numbers include country code: `+46701234567`
857
+ - Validate email format: `user@example.com`
858
+ - Check identifier normalization in logs
859
+
860
+ ## Production Checklist
861
+
862
+ - [ ] Configure HTTPS for all endpoints
863
+ - [ ] Set secure JWT secret in environment variables
864
+ - [ ] Configure SMS/email service with production credentials
865
+ - [ ] Implement rate limiting on `/otp/initiate`
866
+ - [ ] Set appropriate `codeTTL` (recommend 5 minutes)
867
+ - [ ] Set appropriate `maxAttempts` (recommend 3)
868
+ - [ ] Set appropriate `codeLength` (recommend 6)
869
+ - [ ] Implement audit logging for OTP attempts
870
+ - [ ] Set up monitoring for failed verifications
871
+ - [ ] Test OTP flow for both SMS and email
872
+ - [ ] Configure session retention (`keepSessionsSec`)
873
+ - [ ] Implement user existence check in `onSendCode`
874
+ - [ ] Set up alerts for high failure rates
875
+ - [ ] Document OTP flow for support team
876
+
877
+ ## License
878
+
879
+ MIT