A vault door with a glowing six-digit keypad, a phone showing an authenticator app on a desk nearby

Passwords get leaked. They get reused across services. They get scribbled on sticky notes and plastered to monitors. That is why the industry moved toward two-factor authentication years ago, and the most common second factor is a six-digit code that changes every thirty seconds. OTPAuth is the library that generates and validates those codes. It implements the TOTP (Time-Based One-Time Password) and HOTP (HMAC-Based One-Time Password) standards defined in RFC 6238 and RFC 4226, runs everywhere JavaScript runs, and ships with full TypeScript support. Whether you are building a login flow for a side project or an enterprise authentication system, otpauth gives you everything you need to get those six digits right.

What You Get Out of the Box

OTPAuth covers the entire OTP lifecycle with a small, focused API:

  • TOTP and HOTP generation following the official RFCs, with support for SHA1, SHA256, SHA512, and several SHA3 variants
  • Token validation with configurable time windows to handle clock drift between client and server
  • Cryptographic secret management through a dedicated Secret class that generates secure random keys
  • Google Authenticator URI format for creating QR codes that mobile authenticator apps can scan
  • Counter and timing utilities to track token intervals and know exactly when the current code expires
  • Custom HMAC support for bringing your own cryptographic implementation when the environment demands it
  • Multiple build variants -- full, slim, and bare -- so you only ship the code you actually need

The library runs on Node.js, Deno, Bun, and browsers with both ESM and CommonJS support. Its only production dependency is @noble/hashes, a well-regarded cryptographic primitives library.

Getting the Vault Door Open

Install otpauth with your preferred package manager:

npm install otpauth

or

yarn add otpauth

The default import gives you the full build with all dependencies bundled. If you prefer a lighter footprint, you can import from otpauth/slim to exclude bundled dependencies or otpauth/bare to bring your own crypto entirely.

Your First One-Time Password

Spinning Up a TOTP Instance

The most common use case is TOTP -- the kind of code you see rotating in Google Authenticator or Authy. Creating a TOTP instance takes a handful of configuration options:

import * as OTPAuth from "otpauth";

const totp = new OTPAuth.TOTP({
  issuer: "MyApp",
  label: "alice@example.com",
  algorithm: "SHA1",
  digits: 6,
  period: 30,
  secret: new OTPAuth.Secret({ size: 20 }),
});

The issuer identifies your application, the label identifies the user, and the secret is the shared key that both your server and the user's authenticator app will know. The period is how many seconds each code lives for -- 30 is the standard. Calling new OTPAuth.Secret({ size: 20 }) generates a cryptographically secure random secret of 20 bytes, which is the minimum recommended by the RFC for SHA1.

Generating and Validating Tokens

Once you have a TOTP instance, generating a token is a single method call:

const token: string = totp.generate();
console.log(token); // "482931" (changes every 30 seconds)

Validation is just as clean. The validate method returns a delta value indicating which time window the token matched, or null if it did not match at all:

const userInput = "482931";

const delta: number | null = totp.validate({
  token: userInput,
  window: 1,
});

if (delta !== null) {
  console.log("Token is valid");
} else {
  console.log("Token is invalid");
}

The window parameter defines how many time steps to check in each direction. A window of 1 means the library will accept tokens from the previous period, the current period, and the next period. This accounts for slight clock differences between the server and the user's device. Keep this value small -- the documentation recommends 1 -- and always implement server-side rate limiting to prevent brute force attempts.

Wiring Up Google Authenticator

The whole point of OTP is that users carry the second factor on their phones. To get a secret into an authenticator app, you generate a URI and encode it as a QR code:

const uri: string = totp.toString();
// otpauth://totp/MyApp:alice%40example.com?issuer=MyApp&secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30

That URI follows the Google Authenticator key URI format. Feed it to any QR code library -- otpauth deliberately does not bundle one, keeping the package focused -- and the user scans it with their authenticator app. From that moment on, their phone generates the same codes your server does.

You can also reconstruct a TOTP instance from a URI, which is useful for importing existing configurations:

const restored = OTPAuth.URI.parse(uri);

if (restored instanceof OTPAuth.TOTP) {
  const freshToken = restored.generate();
  console.log(freshToken);
}

Beyond the Basics

Working with HOTP for Counter-Based Flows

While TOTP is the standard for authenticator apps, HOTP (HMAC-Based One-Time Password) uses a counter instead of time. This is useful for hardware tokens, email-based verification codes, or any scenario where time synchronization is impractical:

const hotp = new OTPAuth.HOTP({
  issuer: "MyApp",
  label: "alice@example.com",
  algorithm: "SHA1",
  digits: 6,
  counter: 0,
  secret: new OTPAuth.Secret({ size: 20 }),
});

const code: string = hotp.generate({ counter: 0 });

const delta: number | null = hotp.validate({
  token: code,
  counter: 0,
  window: 10,
});

The key difference is that you manage the counter yourself. After each successful validation, you increment the counter on the server side and store it. The window parameter here means how many counter values ahead to check, which handles cases where the user generated codes without using them (pressing the button on a hardware token without submitting).

Preventing Token Reuse

A valid TOTP code works for an entire 30-second window. If an attacker intercepts a code and replays it within that window, a naive implementation would accept it. OTPAuth provides the tools to prevent this:

const totp = new OTPAuth.TOTP({
  issuer: "MyApp",
  label: "alice@example.com",
  algorithm: "SHA256",
  digits: 6,
  period: 30,
  secret: new OTPAuth.Secret({ size: 32 }),
});

const currentCounter: number = OTPAuth.TOTP.counter({
  period: 30,
});

// Store this counter after successful validation
// and reject any future attempts with the same or lower counter
console.log(`Current interval: ${currentCounter}`);

const remainingMs: number = OTPAuth.TOTP.remainingMilliseconds({
  period: 30,
});
console.log(`Code expires in ${remainingMs}ms`);

The static counter method tells you which time interval you are in. After a successful login, store that counter value. If someone tries to authenticate again with the same counter, reject it. The remainingMilliseconds method is handy for client-side countdown timers showing when the current code expires.

Custom HMAC for Specialized Environments

Version 9.5.0 introduced the ability to provide your own HMAC function. This is essential for environments with specific cryptographic requirements or when you want to use a platform-native implementation:

import * as OTPAuth from "otpauth/bare";

const totp = new OTPAuth.TOTP({
  issuer: "MyApp",
  label: "alice@example.com",
  algorithm: "SHA1",
  digits: 6,
  period: 30,
  secret: new OTPAuth.Secret({ size: 20 }),
  hmac: async (algorithm: string, key: Uint8Array, message: Uint8Array): Promise<Uint8Array> => {
    const cryptoKey = await crypto.subtle.importKey(
      "raw",
      key,
      { name: "HMAC", hash: algorithm.replace("-", "") },
      false,
      ["sign"]
    );
    const signature = await crypto.subtle.sign("HMAC", cryptoKey, message);
    return new Uint8Array(signature);
  },
});

The bare build (otpauth/bare) ships without any bundled cryptographic library, bringing the package size down significantly. You provide the HMAC function, and the library handles everything else. This is particularly useful in edge runtimes or environments where you want to use the Web Crypto API directly instead of a JavaScript polyfill.

Locking the Vault

OTPAuth does one thing and does it well. It generates and validates one-time passwords according to the standards that every major authenticator app in the world follows. With zero open issues on GitHub, nearly nine years of continuous development, and a clean TypeScript API that works across every JavaScript runtime, it is the kind of dependency that earns its place in your package.json without reservation.

The library respects your architecture decisions too. Need everything bundled and ready to go? Use the full build. Running in an edge environment with strict bundle size constraints? The bare build with a custom HMAC keeps things minimal. Building a traditional Node.js backend? The slim build lets your package manager deduplicate @noble/hashes if other packages use it.

Two-factor authentication is no longer optional for any application that handles user accounts. The standards are well-defined, the authenticator apps are ubiquitous, and with otpauth, the implementation is about as straightforward as it gets. Six digits, thirty seconds, one library.