# MiniSearch Verification Token System: Secure Search with verifiedTokens and handleTokenVerification

> Learn how MiniSearch secures its search endpoint with a verification token system. Explore verifiedTokens and handleTokenVerification for robust protection and rate limiting.

- Repository: [Victor Nogueira/minisearch](https://github.com/felladrin/minisearch)
- Tags: how-to-guide
- Published: 2026-03-01

---

**MiniSearch protects its search endpoint using an Argon2-based verification token system that caches successful authentications in an in-memory `verifiedTokens` Set and handles HTTP responses through `handleTokenVerification` to enforce rate limits and prevent brute-force attacks.**

The felladrin/minisearch repository implements a lightweight authentication layer to protect its search API from unauthorized access and abuse. The verification token system combines cryptographic token validation using Argon2 with in-memory session caching and strict per-token rate limiting to balance security with performance. This architecture stores secrets in temporary files, caches successful verifications in a process-level Set, and uses dedicated helper functions to standardize HTTP responses across the application.

## Token Generation and Secret Storage

MiniSearch lazily generates a cryptographically random token on first access and persists it to a temporary file for subsequent server restarts. In [`server/searchToken.ts`](https://github.com/felladrin/minisearch/blob/main/server/searchToken.ts), the system checks for existing tokens at `temp-dir/minisearch-token` before generating new secrets, ensuring persistence across process lifecycles without requiring external databases.

```typescript
// server/searchToken.ts
import { regenerateSearchToken, getSearchToken } from "./server/searchToken";

// Generate a new secret token (e.g., after security rotation)
regenerateSearchToken();
console.log("Token stored at temp-dir/minisearch-token");

// Read the current token for client requests
const token = getSearchToken();

```

The token file lives in the operating system's temporary directory, making it suitable for containerized deployments where filesystem persistence is acceptable but database dependencies are undesirable.

## Core Verification Logic in verifyTokenAndRateLimit

The `verifyTokenAndRateLimit()` function in [`server/verifyTokenAndRateLimit.ts`](https://github.com/felladrin/minisearch/blob/main/server/verifyTokenAndRateLimit.ts) serves as the primary gatekeeper for all protected requests. This async function validates incoming tokens against the stored secret using Argon2 hashing from `hash-wasm`, checks the in-memory `verifiedTokens` cache to avoid redundant cryptographic operations, and enforces consumption-based rate limiting.

### Argon2 Hash Verification

When a token arrives via the `Authorization: Bearer <token>` header, the system first checks the `verifiedTokens` Set for existing validation. If absent, it compares the submitted token against the stored secret using Argon2id, adding the token to the cache only upon successful verification.

```typescript
// Conceptual flow from server/verifyTokenAndRateLimit.ts
const verifiedTokens = new Set<string>();

async function verifyTokenAndRateLimit(token: string | null) {
  if (!token) return { isAuthorized: false, statusCode: 400, error: "Missing token." };
  
  // Check in-memory cache first
  if (verifiedTokens.has(token)) {
    // Skip expensive Argon2 verification
    return checkRateLimit(token);
  }
  
  // Verify using Argon2 if not cached
  const isValid = await verifyArgon2(token, storedSecret);
  if (!isValid) return { isAuthorized: false, statusCode: 401, error: "Invalid token." };
  
  verifiedTokens.add(token); // Cache for subsequent requests
  return checkRateLimit(token);
}

```

### Per-Token Rate Limiting

After cryptographic verification, the system applies rate limiting using `RateLimiterMemory` from **rate-limiter-flexible**. Each token receives a bucket of 10 requests per 10-second window. Successful verification consumes one point; exhausted buckets return **429 Too Many Requests**.

```typescript
import { RateLimiterMemory } from "rate-limiter-flexible";

const rateLimiter = new RateLimiterMemory({
  points: 10,
  duration: 10, // seconds
});

```

This configuration prevents brute-force attacks against the Argon2 hash while ensuring legitimate clients can perform burst searches without unnecessary latency.

## In-Memory Token Caching via verifiedTokens

The [`server/verifiedTokens.ts`](https://github.com/felladrin/minisearch/blob/main/server/verifiedTokens.ts) module exports a process-level `Set<string>` that maintains the list of tokens that have successfully passed Argon2 verification during the current server session. This cache eliminates the computational overhead of repeated hash comparisons for active clients.

```typescript
// server/verifiedTokens.ts
const verifiedTokens = new Set<string>();

export function getVerifiedTokensAmount() { 
  return verifiedTokens.size; 
}

export function isVerifiedToken(token: string) { 
  return verifiedTokens.has(token); 
}

export function addVerifiedToken(token: string) { 
  return verifiedTokens.add(token); 
}

```

**Important**: This Set exists only in memory and does not persist across process restarts. When the Node.js process terminates or restarts, the `verifiedTokens` collection initializes empty, requiring clients to re-verify against the Argon2 hash on their next request.

## HTTP Response Standardization with handleTokenVerification

The `handleTokenVerification()` function in [`server/handleTokenVerification.ts`](https://github.com/felladrin/minisearch/blob/main/server/handleTokenVerification.ts) provides a thin HTTP layer that translates verification results into proper JSON responses. This utility ensures consistent error formatting and status codes across all protected endpoints.

```typescript
// server/handleTokenVerification.ts
export async function handleTokenVerification(token: string | null, response: ServerResponse) {
  const { isAuthorized, statusCode, error } = await verifyTokenAndRateLimit(token);
  
  if (!isAuthorized && statusCode && error) {
    response.statusCode = statusCode; // 400, 401, or 429
    response.setHeader("Content-Type", "application/json");
    response.end(JSON.stringify({ error }));
    return { shouldContinue: false };
  }
  
  return { shouldContinue: true };
}

```

When integrated into an HTTP server, this function returns `{ shouldContinue: false }` to signal that the response has already been sent and request processing should halt. Valid tokens return `{ shouldContinue: true }`, allowing the endpoint to execute the actual search logic.

### Wiring the Verification Layer

```typescript
import { createServer } from "node:http";
import { handleTokenVerification } from "./server/handleTokenVerification";

const server = createServer(async (req, res) => {
  const token = req.headers.authorization?.replace(/^Bearer\s+/, "") ?? null;
  
  const { shouldContinue } = await handleTokenVerification(token, res);
  if (!shouldContinue) return; // Error response already sent
  
  // Proceed with search implementation
  res.end(JSON.stringify({ results: [] }));
});

```

## Summary

- **MiniSearch stores secrets lazily** in `temp-dir/minisearch-token` and reads them via `getSearchToken()` from [`server/searchToken.ts`](https://github.com/felladrin/minisearch/blob/main/server/searchToken.ts).
- **Argon2 verification** occurs only for uncached tokens; successful validations are stored in the in-memory `verifiedTokens` Set to optimize performance.
- **Rate limiting** enforces 10 requests per 10 seconds per token using `RateLimiterMemory`, returning HTTP 429 when limits are exceeded.
- **`handleTokenVerification()`** standardizes HTTP responses, returning `{ shouldContinue: false }` to halt processing on authentication failures with appropriate status codes (400, 401, 429).

## Frequently Asked Questions

### How does MiniSearch store the secret verification token?

The system lazily generates a random token on first access and persists it to a temporary file at `temp-dir/minisearch-token` within the operating system's temp directory. The `regenerateSearchToken()` function in [`server/searchToken.ts`](https://github.com/felladrin/minisearch/blob/main/server/searchToken.ts) handles creation, while `getSearchToken()` reads the existing token from disk on subsequent calls.

### What happens to verified tokens when the server restarts?

The `verifiedTokens` Set is stored only in memory and does not persist across process restarts. When the Node.js process terminates, the cache clears completely. Clients must re-verify their tokens using Argon2 comparison on the first request after a server restart, after which the tokens are re-added to the fresh in-memory cache.

### How does the rate limiting work per token?

Each token receives an independent rate limit bucket via `RateLimiterMemory` configured with `points: 10` and `duration: 10`. Every successful verification consumes one point from the bucket. When a token exhausts its 10 requests within the 10-second window, `verifyTokenAndRateLimit()` returns a 429 status code with the error message "Too many requests."

### Why does the system use Argon2 instead of simple string comparison?

According to the felladrin/minisearch source code, the system uses Argon2id hashing via `hash-wasm` to prevent timing attacks and protect against brute-force attempts if the temporary token file is compromised. While the `verifiedTokens` cache provides fast subsequent lookups, the initial verification uses slow cryptographic hashing to mitigate security risks inherent in storing secrets on disk.