Skip to content

Commit cf281ac

Browse files
committed
refactor: change otp api
1 parent 5c9cc15 commit cf281ac

File tree

3 files changed

+101
-104
lines changed

3 files changed

+101
-104
lines changed

README.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -283,21 +283,12 @@ The OTP utility provides a simple and secure way to generate and verify one-time
283283

284284
It's implemented based on [RFC 4226](https://tools.ietf.org/html/rfc4226) and [RFC 6238](https://tools.ietf.org/html/rfc6238).
285285

286-
### Create OTP Generator
287-
288-
To create an OTP generator, use the createOTP function. It allows you to specify the SHA hash algorithm (default: "SHA-1") and the number of digits in the OTP (default: 6).
289-
290-
```ts
291-
import { createOTP } from "@better-auth/utils/otp";
292-
293-
const { generateHOTP, generateTOTP, verifyTOTP } = createOTP("SHA-256", 6);
294-
```
295-
296286
### Generating HOTP
297287

298288
HOTP generates a one-time password based on a counter value and a secret key. The counter should be incremented for each new OTP.
299289

300290
```ts
291+
import { generateHOTP } from "@better-auth/utils/otp";
301292
const secret = "my-super-secret-key";
302293
const counter = 1234;
303294
const otp = generateHOTP(secret, counter);
@@ -308,6 +299,7 @@ const otp = generateHOTP(secret, counter);
308299
TOTP generates a one-time password based on the current time and a secret key. The time step is typically 30 seconds.
309300

310301
```ts
302+
import { generateTOTP } from "@better-auth/utils/otp";
311303
const secret = "my-super-secret-key"
312304
const otp = generateTOTP(secret);
313305
```
@@ -317,13 +309,15 @@ const otp = generateTOTP(secret);
317309
Verify a TOTP against the secret key and a specified time window. The default time window is 30 seconds.
318310

319311
```ts
312+
import { verifyTOTP } from "@better-auth/utils/otp";
320313
const secret = "my-super-secret-key"
321314
const isValid = verifyTOTP(secret, otp);
322315
```
323316

324317
You can also specify the time window in seconds.
325318

326319
```ts
320+
import { verifyTOTP } from "@better-auth/utils";
327321
const isValid = verifyTOTP(secret, otp, { window: 60 });
328322
```
329323

src/otp.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { describe, it, expect, vi } from "vitest";
2-
import { createOTP } from "./otp";
2+
import { generateHOTP, generateTOTP, verifyTOTP } from "./otp";
33

4-
const { generateHOTP, generateTOTP, verifyTOTP } = createOTP();
54
describe("HOTP and TOTP Generation Tests", () => {
65
it("should generate a valid HOTP for a given counter", async () => {
76
const key = "1234567890";
@@ -47,7 +46,7 @@ describe("HOTP and TOTP Generation Tests", () => {
4746
const seconds = 30;
4847
const digits = 6;
4948

50-
const otp1 = await generateTOTP(secret, { digits, seconds });
49+
const otp1 = await generateTOTP(secret, { digits, period: seconds });
5150
vi.useFakeTimers();
5251
await vi.advanceTimersByTimeAsync(30000);
5352
const otp2 = await generateTOTP(secret, { digits });
@@ -57,7 +56,6 @@ describe("HOTP and TOTP Generation Tests", () => {
5756
it("should verify correct TOTP against generated value", async () => {
5857
const secret = "1234567890";
5958
const totp = await generateTOTP(secret, { digits: 6 });
60-
6159
const isValid = await verifyTOTP(totp, { secret });
6260
expect(isValid).toBe(true);
6361
});

src/otp.ts

Lines changed: 95 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,109 @@
11
import { hmac } from "./hmac";
22
import type { SHAFamily } from "./type";
33

4-
export const createOTP = (
5-
hash: SHAFamily = "SHA-1",
6-
digits = 6,
7-
seconds = 30,
8-
) => {
9-
const defaultSeconds = seconds;
10-
const defaultDigits = digits;
11-
async function generateHOTP(
12-
secret: string,
13-
{
14-
counter,
15-
digits,
16-
}: {
17-
counter: number;
18-
digits?: number;
19-
},
20-
) {
21-
const _digits = digits ?? defaultDigits;
22-
if (_digits < 1 || _digits > 8) {
23-
throw new TypeError("Digits must be between 1 and 8");
24-
}
25-
const buffer = new ArrayBuffer(8);
26-
new DataView(buffer).setBigUint64(0, BigInt(counter), false);
27-
const bytes = new Uint8Array(buffer);
28-
const hmacResult = new Uint8Array(await hmac.sign(secret, {
29-
data: bytes,
30-
hash
31-
}));
32-
const offset = hmacResult[hmacResult.length - 1] & 0x0f;
33-
const truncated =
34-
((hmacResult[offset] & 0x7f) << 24) |
35-
((hmacResult[offset + 1] & 0xff) << 16) |
36-
((hmacResult[offset + 2] & 0xff) << 8) |
37-
(hmacResult[offset + 3] & 0xff);
38-
const otp = truncated % 10 ** _digits;
39-
return otp.toString().padStart(_digits, "0");
40-
}
41-
async function generateTOTP(
42-
secret: string,
43-
{
44-
seconds = defaultSeconds,
45-
digits = defaultDigits,
46-
}: {
47-
seconds?: number;
48-
digits?: number;
49-
},
50-
) {
51-
const milliseconds = seconds * 1000;
52-
const counter = Math.floor(Date.now() / milliseconds);
53-
return await generateHOTP(secret, { counter, digits });
4+
const defaultPeriod = 30;
5+
const defaultDigits = 6;
6+
7+
export async function generateHOTP(
8+
secret: string,
9+
{
10+
counter,
11+
digits,
12+
hash = "SHA-1",
13+
}: {
14+
counter: number;
15+
digits?: number;
16+
hash?: SHAFamily;
17+
},
18+
) {
19+
const _digits = digits ?? defaultDigits;
20+
if (_digits < 1 || _digits > 8) {
21+
throw new TypeError("Digits must be between 1 and 8");
5422
}
23+
const buffer = new ArrayBuffer(8);
24+
new DataView(buffer).setBigUint64(0, BigInt(counter), false);
25+
const bytes = new Uint8Array(buffer);
26+
const hmacResult = new Uint8Array(await hmac.sign(secret, {
27+
data: bytes,
28+
hash
29+
}));
30+
const offset = hmacResult[hmacResult.length - 1] & 0x0f;
31+
const truncated =
32+
((hmacResult[offset] & 0x7f) << 24) |
33+
((hmacResult[offset + 1] & 0xff) << 16) |
34+
((hmacResult[offset + 2] & 0xff) << 8) |
35+
(hmacResult[offset + 3] & 0xff);
36+
const otp = truncated % 10 ** _digits;
37+
return otp.toString().padStart(_digits, "0");
38+
}
39+
40+
export async function generateTOTP(
41+
secret: string,
42+
{
43+
period = defaultPeriod,
44+
digits = defaultDigits,
45+
}: {
46+
period?: number;
47+
digits?: number;
48+
},
49+
) {
50+
const milliseconds = period * 1000;
51+
const counter = Math.floor(Date.now() / milliseconds);
52+
return await generateHOTP(secret, { counter, digits });
53+
}
5554

56-
async function verifyTOTP(
57-
otp: string,
58-
{
59-
window = 1,
60-
digits = defaultDigits,
61-
secret,
62-
seconds = defaultSeconds,
63-
}: {
64-
seconds?: number;
65-
window?: number;
66-
digits?: number;
67-
secret: string;
68-
},
69-
) {
70-
const milliseconds = seconds * 1000;
71-
const counter = Math.floor(Date.now() / milliseconds);
72-
for (let i = -window; i <= window; i++) {
73-
const generatedOTP = await generateHOTP(secret, {
74-
counter: counter + i,
75-
digits,
76-
});
77-
if (otp === generatedOTP) {
78-
return true;
79-
}
55+
56+
export async function verifyTOTP(
57+
otp: string,
58+
{
59+
window = 1,
60+
digits = defaultDigits,
61+
secret,
62+
period = defaultPeriod,
63+
}: {
64+
period?: number;
65+
window?: number;
66+
digits?: number;
67+
secret: string;
68+
},
69+
) {
70+
const milliseconds = period * 1000;
71+
const counter = Math.floor(Date.now() / milliseconds);
72+
for (let i = -window; i <= window; i++) {
73+
const generatedOTP = await generateHOTP(secret, {
74+
counter: counter + i,
75+
digits,
76+
});
77+
if (otp === generatedOTP) {
78+
return true;
8079
}
81-
return false;
8280
}
81+
return false;
82+
}
8383

84-
/**
84+
/**
8585
* Generate a QR code URL for the OTP secret
8686
*/
87-
function generateQRCode(
87+
export function generateQRCode(
88+
{
89+
issuer,
90+
account,
91+
secret,
92+
digit = defaultDigits,
93+
period = defaultPeriod,
94+
}: {
8895
issuer: string,
8996
account: string,
9097
secret: string,
91-
) {
92-
const url = new URL("otpauth://totp");
93-
url.searchParams.set("secret", secret);
94-
url.searchParams.set("issuer", issuer);
95-
url.searchParams.set("account", account);
96-
return url.toString();
98+
digit?: number,
99+
period?: number,
97100
}
98-
return {
99-
generateHOTP,
100-
generateTOTP,
101-
verifyTOTP,
102-
generateQRCode
103-
};
104-
};
101+
) {
102+
const url = new URL("otpauth://totp");
103+
url.searchParams.set("secret", secret);
104+
url.searchParams.set("issuer", issuer);
105+
url.searchParams.set("account", account);
106+
url.searchParams.set("digits", digit.toString());
107+
url.searchParams.set("period", period.toString());
108+
return url.toString();
109+
}

0 commit comments

Comments
 (0)