Skip to content

Commit 3931b72

Browse files
authored
Merge pull request #4 from bennu/feat/two-factor-auth
Feat/two factor auth
2 parents 0f9213c + 83a35a6 commit 3931b72

File tree

4 files changed

+348
-1
lines changed

4 files changed

+348
-1
lines changed

src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,13 @@ export {
88
cleanRut,
99
calculateVerificationDigit,
1010
RutValidationResult
11-
} from './validate-chilean-rut'
11+
} from './validate-chilean-rut'
12+
13+
/**
14+
* Two Factor Authentication Code(2FA)
15+
* A TypeScript package to generate 2FA codes.
16+
*/
17+
18+
export {
19+
generateMinutelyTwoFactor,
20+
} from "./two-factor-generator/two-factor-generator"

src/two-factor-generator/README.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Two-Factor Authentication Generator
2+
3+
A time-based two-factor authentication code generator for TypeScript/JavaScript applications.
4+
5+
## Features
6+
7+
-**Time-based generation** - Creates unique codes per minute using Santiago timezone
8+
-**Configurable length** - Supports codes from 4 to 8 digits
9+
-**Consistent algorithm** - Uses standardized multiplier-based calculation
10+
-**TypeScript support** - Full type safety and IntelliSense
11+
12+
## Installation
13+
14+
```bash
15+
npm install @bennu-cl/commons-js
16+
```
17+
18+
## Usage
19+
20+
### Basic Usage
21+
22+
```typescript
23+
import { generateMinutelyTwoFactor } from '@bennu-cl/commons-js'
24+
25+
// Generate 4-digit code (default)
26+
const code = generateMinutelyTwoFactor()
27+
console.log(code) // "1234"
28+
29+
// Generate custom length code
30+
const code6 = generateMinutelyTwoFactor(6)
31+
console.log(code6) // "123456"
32+
33+
const code8 = generateMinutelyTwoFactor(8)
34+
console.log(code8) // "12345678"
35+
```
36+
37+
### Error Handling
38+
39+
```typescript
40+
import { generateMinutelyTwoFactor } from '@bennu-cl/commons-js'
41+
42+
try {
43+
const code = generateMinutelyTwoFactor(3) // Invalid length
44+
} catch (error) {
45+
console.error(error.message) // "Length must be between 4 and 8."
46+
}
47+
```
48+
49+
## API Reference
50+
51+
### `generateMinutelyTwoFactor(length?: number): string`
52+
53+
Generates a time-based two-factor authentication code based on the current minute.
54+
55+
**Parameters:**
56+
57+
- `length` (optional): Length of the code to generate
58+
- **Type**: `number`
59+
- **Default**: `4`
60+
- **Range**: `4` to `8`
61+
62+
**Returns:** `string`
63+
64+
A numeric code of the specified length.
65+
66+
**Throws:** `Error`
67+
68+
If the length parameter is outside the valid range (4-8).
69+
70+
## Examples
71+
72+
### User Authentication
73+
74+
```typescript
75+
import { generateMinutelyTwoFactor } from '@bennu-cl/commons-js'
76+
77+
function generateUserVerificationCode(): string {
78+
return generateMinutelyTwoFactor(6)
79+
}
80+
81+
// Use in authentication flow
82+
const verificationCode = generateUserVerificationCode()
83+
console.log(`Your verification code: ${verificationCode}`)
84+
```
85+
86+
### Different Code Lengths
87+
88+
```typescript
89+
import { generateMinutelyTwoFactor } from '@bennu-cl/commons-js'
90+
91+
// Generate codes of different lengths
92+
const codes = {
93+
short: generateMinutelyTwoFactor(4), // "1234"
94+
medium: generateMinutelyTwoFactor(6), // "123456"
95+
long: generateMinutelyTwoFactor(8) // "12345678"
96+
}
97+
98+
console.log(codes)
99+
```
100+
101+
### Consistent Generation
102+
103+
```typescript
104+
import { generateMinutelyTwoFactor } from '@bennu-cl/commons-js'
105+
106+
// Multiple calls within the same minute return identical codes
107+
const code1 = generateMinutelyTwoFactor(6)
108+
const code2 = generateMinutelyTwoFactor(6)
109+
110+
console.log(code1 === code2) // true (within same minute)
111+
```
112+
113+
## Algorithm
114+
115+
The function uses a deterministic algorithm to ensure consistency:
116+
117+
1. **Timestamp**: Gets current date/time in `yyyyMMddHHmm` format using `America/Santiago` timezone
118+
2. **Calculation**: Applies formula `(timestamp * 97 + 31)`
119+
3. **Formatting**: Adjusts result to requested length:
120+
- If too short: pads with leading zeros
121+
- If too long: takes the last N digits
122+
123+
## Technical Details
124+
125+
- **Consistency**: Same code generated throughout the entire minute
126+
- **Uniqueness**: Different codes generated each minute
127+
- **Format**: Numeric digits only (0-9)
128+
- **Length**: Guaranteed to match the specified parameter
129+
130+
## Use Cases
131+
132+
- 🔐 Two-factor authentication systems
133+
- ⏰ Time-based temporary codes
134+
- 🔄 Multi-system code synchronization
135+
136+
## Timezone Considerations
137+
138+
The function specifically uses `America/Santiago` timezone to ensure consistent code generation regardless of the user's local timezone. This is important for applications that need to coordinate across different geographic locations.
139+
140+
## License
141+
142+
MIT © Bennu
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Two-Factor Authentication Code Generator
3+
* Generates time-based codes that match backend implementation
4+
* @author Created by Bennu ❤️
5+
* @license MIT
6+
*/
7+
8+
/**
9+
* Generate a time-based two-factor authentication code
10+
* Uses current date/time in Santiago timezone with a multiplier algorithm
11+
* to match backend implementation
12+
*/
13+
export function generateMinutelyTwoFactor(length: number = 4): string {
14+
if (length < 4 || length > 8) {
15+
throw new Error("Length must be between 4 and 8.")
16+
}
17+
18+
// Formato "yyyyMMddHHmm" en timezone America/Santiago
19+
const now = new Date().toLocaleString("en-CA", {
20+
timeZone: "America/Santiago",
21+
year: "numeric",
22+
month: "2-digit",
23+
day: "2-digit",
24+
hour: "2-digit",
25+
minute: "2-digit",
26+
hour12: false
27+
}).replace(/[-:, ]/g, '')
28+
29+
const multiplier = 97
30+
const addend = 31
31+
32+
const value = parseInt(now)
33+
const mixed = (value * multiplier + addend)
34+
35+
const result = mixed.toString()
36+
37+
if (result.length < length) {
38+
return result.padStart(length, '0')
39+
}
40+
41+
return result.substring(result.length - length)
42+
}

test/two-factor-generator.test.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { generateMinutelyTwoFactor } from "../src/two-factor-generator/two-factor-generator"
2+
3+
describe("Two-Factor Authentication Generator", () => {
4+
describe("generateMinutelyTwoFactor", () => {
5+
describe("Length validation", () => {
6+
test("should generate code with default length of 4", () => {
7+
const code = generateMinutelyTwoFactor()
8+
expect(code).toHaveLength(4)
9+
expect(code).toMatch(/^\d+$/)
10+
})
11+
12+
test("should generate code with custom length between 4 and 8", () => {
13+
for (let length = 4; length <= 8; length++) {
14+
const code = generateMinutelyTwoFactor(length)
15+
expect(code).toHaveLength(length)
16+
expect(code).toMatch(/^\d+$/)
17+
}
18+
})
19+
20+
test("should throw error for length less than 4", () => {
21+
expect(() => generateMinutelyTwoFactor(3)).toThrow("Length must be between 4 and 8.")
22+
expect(() => generateMinutelyTwoFactor(0)).toThrow("Length must be between 4 and 8.")
23+
expect(() => generateMinutelyTwoFactor(-1)).toThrow("Length must be between 4 and 8.")
24+
})
25+
26+
test("should throw error for length greater than 8", () => {
27+
expect(() => generateMinutelyTwoFactor(9)).toThrow("Length must be between 4 and 8.")
28+
expect(() => generateMinutelyTwoFactor(10)).toThrow("Length must be between 4 and 8.")
29+
})
30+
})
31+
32+
describe("Code generation consistency", () => {
33+
test("should generate same code when called multiple times within same minute", () => {
34+
const code1 = generateMinutelyTwoFactor(6)
35+
const code2 = generateMinutelyTwoFactor(6)
36+
expect(code1).toBe(code2)
37+
})
38+
39+
test("should generate different codes for different lengths", () => {
40+
const code4 = generateMinutelyTwoFactor(4)
41+
const code6 = generateMinutelyTwoFactor(6)
42+
const code8 = generateMinutelyTwoFactor(8)
43+
44+
expect(code4).toHaveLength(4)
45+
expect(code6).toHaveLength(6)
46+
expect(code8).toHaveLength(8)
47+
48+
// Different lengths should give different codes (though 4 could be substring of 6, etc.)
49+
expect(code4).not.toBe(code6)
50+
expect(code6).not.toBe(code8)
51+
})
52+
})
53+
54+
describe("Algorithm verification", () => {
55+
test("should use correct multiplier and addend", () => {
56+
// Mock Date to control time
57+
const mockDate = new Date("2024-01-15T14:30:00.000Z")
58+
const originalDate = global.Date
59+
global.Date = jest.fn(() => mockDate) as any
60+
global.Date.now = originalDate.now
61+
62+
// The toLocaleString should format as "yyyyMMddHHmm" for Santiago timezone
63+
// For 2024-01-15T14:30:00.000Z, Santiago time would be approximately 2024-01-15 11:30 (UTC-3)
64+
const code = generateMinutelyTwoFactor(4)
65+
66+
// Verify it's a numeric string of correct length
67+
expect(code).toMatch(/^\d{4}$/)
68+
69+
// Restore original Date
70+
global.Date = originalDate
71+
})
72+
73+
test("should pad with zeros when result is shorter than requested length", () => {
74+
// Test with a scenario that might produce a short result
75+
const code = generateMinutelyTwoFactor(8)
76+
expect(code).toHaveLength(8)
77+
expect(code).toMatch(/^\d{8}$/)
78+
79+
// If the result was padded, it should start with digits
80+
expect(parseInt(code)).toBeGreaterThanOrEqual(0)
81+
})
82+
83+
test("should truncate from the end when result is longer than requested length", () => {
84+
const code4 = generateMinutelyTwoFactor(4)
85+
const code8 = generateMinutelyTwoFactor(8)
86+
87+
// The 4-digit code should be the last 4 digits of the 8-digit code
88+
expect(code8.substring(4)).toBe(code4)
89+
})
90+
})
91+
92+
describe("Time-based generation", () => {
93+
test("should generate numeric codes only", () => {
94+
for (let i = 0; i < 10; i++) {
95+
const code = generateMinutelyTwoFactor(6)
96+
expect(code).toMatch(/^\d{6}$/)
97+
expect(parseInt(code)).not.toBeNaN()
98+
}
99+
})
100+
101+
test("should use Santiago timezone", () => {
102+
// This is hard to test directly, but we can verify the function doesn't crash
103+
// and produces valid codes regardless of local timezone
104+
const code = generateMinutelyTwoFactor(5)
105+
expect(code).toHaveLength(5)
106+
expect(code).toMatch(/^\d{5}$/)
107+
})
108+
})
109+
110+
describe("Edge cases", () => {
111+
test("should handle boundary lengths correctly", () => {
112+
const code4 = generateMinutelyTwoFactor(4)
113+
const code8 = generateMinutelyTwoFactor(8)
114+
115+
expect(code4).toHaveLength(4)
116+
expect(code8).toHaveLength(8)
117+
expect(code4).toMatch(/^\d{4}$/)
118+
expect(code8).toMatch(/^\d{8}$/)
119+
})
120+
121+
test("should be deterministic within same minute", () => {
122+
const codes = []
123+
for (let i = 0; i < 5; i++) {
124+
codes.push(generateMinutelyTwoFactor(6))
125+
}
126+
127+
// All codes should be identical
128+
expect(codes.every(code => code === codes[0])).toBe(true)
129+
})
130+
})
131+
132+
describe("Backend compatibility", () => {
133+
test("should use same algorithm as backend", () => {
134+
// This tests the exact algorithm: (value * 97 + 31)
135+
const code = generateMinutelyTwoFactor(6)
136+
137+
// Verify it's a valid 6-digit numeric string
138+
expect(code).toMatch(/^\d{6}$/)
139+
expect(parseInt(code)).toBeGreaterThanOrEqual(0)
140+
expect(parseInt(code)).toBeLessThan(1000000)
141+
})
142+
143+
test("should format timestamp correctly", () => {
144+
// The function should use "yyyyMMddHHmm" format
145+
// We can't easily test the exact formatting without mocking,
146+
// but we can verify the output is consistent
147+
const code1 = generateMinutelyTwoFactor(5)
148+
const code2 = generateMinutelyTwoFactor(5)
149+
150+
expect(code1).toBe(code2)
151+
})
152+
})
153+
})
154+
})

0 commit comments

Comments
 (0)