Skip to content

Commit

Permalink
refactor: change otp api
Browse files Browse the repository at this point in the history
  • Loading branch information
Bekacru committed Dec 10, 2024
1 parent 5c9cc15 commit cf281ac
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 104 deletions.
14 changes: 4 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,21 +283,12 @@ The OTP utility provides a simple and secure way to generate and verify one-time

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

### Create OTP Generator

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).

```ts
import { createOTP } from "@better-auth/utils/otp";

const { generateHOTP, generateTOTP, verifyTOTP } = createOTP("SHA-256", 6);
```

### Generating HOTP

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

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

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

```ts
import { verifyTOTP } from "@better-auth/utils/otp";
const secret = "my-super-secret-key"
const isValid = verifyTOTP(secret, otp);
```

You can also specify the time window in seconds.

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

Expand Down
6 changes: 2 additions & 4 deletions src/otp.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { describe, it, expect, vi } from "vitest";
import { createOTP } from "./otp";
import { generateHOTP, generateTOTP, verifyTOTP } from "./otp";

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

const otp1 = await generateTOTP(secret, { digits, seconds });
const otp1 = await generateTOTP(secret, { digits, period: seconds });
vi.useFakeTimers();
await vi.advanceTimersByTimeAsync(30000);
const otp2 = await generateTOTP(secret, { digits });
Expand All @@ -57,7 +56,6 @@ describe("HOTP and TOTP Generation Tests", () => {
it("should verify correct TOTP against generated value", async () => {
const secret = "1234567890";
const totp = await generateTOTP(secret, { digits: 6 });

const isValid = await verifyTOTP(totp, { secret });
expect(isValid).toBe(true);
});
Expand Down
185 changes: 95 additions & 90 deletions src/otp.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,109 @@
import { hmac } from "./hmac";
import type { SHAFamily } from "./type";

export const createOTP = (
hash: SHAFamily = "SHA-1",
digits = 6,
seconds = 30,
) => {
const defaultSeconds = seconds;
const defaultDigits = digits;
async function generateHOTP(
secret: string,
{
counter,
digits,
}: {
counter: number;
digits?: number;
},
) {
const _digits = digits ?? defaultDigits;
if (_digits < 1 || _digits > 8) {
throw new TypeError("Digits must be between 1 and 8");
}
const buffer = new ArrayBuffer(8);
new DataView(buffer).setBigUint64(0, BigInt(counter), false);
const bytes = new Uint8Array(buffer);
const hmacResult = new Uint8Array(await hmac.sign(secret, {
data: bytes,
hash
}));
const offset = hmacResult[hmacResult.length - 1] & 0x0f;
const truncated =
((hmacResult[offset] & 0x7f) << 24) |
((hmacResult[offset + 1] & 0xff) << 16) |
((hmacResult[offset + 2] & 0xff) << 8) |
(hmacResult[offset + 3] & 0xff);
const otp = truncated % 10 ** _digits;
return otp.toString().padStart(_digits, "0");
}
async function generateTOTP(
secret: string,
{
seconds = defaultSeconds,
digits = defaultDigits,
}: {
seconds?: number;
digits?: number;
},
) {
const milliseconds = seconds * 1000;
const counter = Math.floor(Date.now() / milliseconds);
return await generateHOTP(secret, { counter, digits });
const defaultPeriod = 30;
const defaultDigits = 6;

export async function generateHOTP(
secret: string,
{
counter,
digits,
hash = "SHA-1",
}: {
counter: number;
digits?: number;
hash?: SHAFamily;
},
) {
const _digits = digits ?? defaultDigits;
if (_digits < 1 || _digits > 8) {
throw new TypeError("Digits must be between 1 and 8");
}
const buffer = new ArrayBuffer(8);
new DataView(buffer).setBigUint64(0, BigInt(counter), false);
const bytes = new Uint8Array(buffer);
const hmacResult = new Uint8Array(await hmac.sign(secret, {
data: bytes,
hash
}));
const offset = hmacResult[hmacResult.length - 1] & 0x0f;
const truncated =
((hmacResult[offset] & 0x7f) << 24) |
((hmacResult[offset + 1] & 0xff) << 16) |
((hmacResult[offset + 2] & 0xff) << 8) |
(hmacResult[offset + 3] & 0xff);
const otp = truncated % 10 ** _digits;
return otp.toString().padStart(_digits, "0");
}

export async function generateTOTP(
secret: string,
{
period = defaultPeriod,
digits = defaultDigits,
}: {
period?: number;
digits?: number;
},
) {
const milliseconds = period * 1000;
const counter = Math.floor(Date.now() / milliseconds);
return await generateHOTP(secret, { counter, digits });
}

async function verifyTOTP(
otp: string,
{
window = 1,
digits = defaultDigits,
secret,
seconds = defaultSeconds,
}: {
seconds?: number;
window?: number;
digits?: number;
secret: string;
},
) {
const milliseconds = seconds * 1000;
const counter = Math.floor(Date.now() / milliseconds);
for (let i = -window; i <= window; i++) {
const generatedOTP = await generateHOTP(secret, {
counter: counter + i,
digits,
});
if (otp === generatedOTP) {
return true;
}

export async function verifyTOTP(
otp: string,
{
window = 1,
digits = defaultDigits,
secret,
period = defaultPeriod,
}: {
period?: number;
window?: number;
digits?: number;
secret: string;
},
) {
const milliseconds = period * 1000;
const counter = Math.floor(Date.now() / milliseconds);
for (let i = -window; i <= window; i++) {
const generatedOTP = await generateHOTP(secret, {
counter: counter + i,
digits,
});
if (otp === generatedOTP) {
return true;
}
return false;
}
return false;
}

/**
/**
* Generate a QR code URL for the OTP secret
*/
function generateQRCode(
export function generateQRCode(
{
issuer,
account,
secret,
digit = defaultDigits,
period = defaultPeriod,
}: {
issuer: string,
account: string,
secret: string,
) {
const url = new URL("otpauth://totp");
url.searchParams.set("secret", secret);
url.searchParams.set("issuer", issuer);
url.searchParams.set("account", account);
return url.toString();
digit?: number,
period?: number,
}
return {
generateHOTP,
generateTOTP,
verifyTOTP,
generateQRCode
};
};
) {
const url = new URL("otpauth://totp");
url.searchParams.set("secret", secret);
url.searchParams.set("issuer", issuer);
url.searchParams.set("account", account);
url.searchParams.set("digits", digit.toString());
url.searchParams.set("period", period.toString());
return url.toString();
}

0 comments on commit cf281ac

Please sign in to comment.