A lightweight, self-hosted TOTP authenticator that runs entirely in the browser. No server, no build step, no external crypto dependencies — just open and go.
Live demo: totp-1sr.pages.dev
- Per-account TOTP — configurable algorithm (SHA-1 / SHA-256 / SHA-512), period (30 s / 60 s / 5 min / 1 h / 1 d), and digits (6 / 8)
- Issuer field for Google Authenticator compatibility
- Auto-load accounts from
accounts.jsonon first visit - Import / Export accounts as JSON or
otpauth://URIs - QR code generation for easy scanning into mobile authenticator apps
- Click-to-copy codes and passwords to clipboard
- Per-account password field — optional, hidden by default with 👁 toggle to reveal; click to copy
- Responsive layout — works on desktop and mobile with consistent UI
- Dark mode — toggle in dropdown menu, auto-detected by local time (08:00–22:00 = light), session override
- Dropdown menu — Theme, Lock, Change Password, Import, Export, Share, Reload, and Reset consolidated into one menu
- Modern UI — Inter font, JetBrains Mono for codes, gradient backgrounds, glassmorphism topbar, smooth animations
- AES-GCM encryption — set a password to encrypt secrets at rest using PBKDF2-SHA256 (600 000 iterations); vault locks on reload
- Drag-and-drop reorder in edit mode
- Swipe-to-reveal actions on mobile cards
- Countdown timer — shows remaining seconds; turns red and bold when ≤ 5 s
- Clipboard auto-clear — copied values cleared after 30 s for security
- PWA support — installable as standalone app via service worker and web manifest
- Native Web Crypto API — HMAC-based TOTP generation with zero external crypto dependencies
- CI/CD — GitHub Actions deploys to Cloudflare Pages on tag-based releases
- Version injection — shows
vDevlocally; CI replaces it with the release tag
No config file needed — the project is a static site with no build step.
# Install Wrangler (one-time)
npm install -D wrangler
# Start local dev server
npx wrangler pages dev . --port=8788Then open http://localhost:8788 in your browser.
# Login to Cloudflare (one-time, interactive browser flow)
npx wrangler login
# Deploy to production
npx wrangler pages deploy . --project-name=totp --branch=main
# Deploy to a preview branch
npx wrangler pages deploy . --project-name=totp --branch=previewAfter deploying, your app will be available at https://totp.<your-account>.pages.dev.
When running wrangler in CI or a headless environment, set these environment variables instead of wrangler login:
| Variable | Description |
|---|---|
CLOUDFLARE_API_TOKEN |
Cloudflare API token with Pages: Edit permission |
CLOUDFLARE_ACCOUNT_ID |
Your Cloudflare account ID (found in the dashboard URL) |
export CLOUDFLARE_API_TOKEN="your_api_token"
export CLOUDFLARE_ACCOUNT_ID="your_account_id"
npx wrangler pages deploy . --project-name=totp --branch=mainServe the files with any static HTTP server:
# Python
python3 -m http.server 8000
# Node
npx serve .
# BusyBox
busybox httpd -f -p 8000 -h .Then open http://localhost:8000 in your browser.
On first visit (empty localStorage), the app tries to fetch accounts.json from the server. If the file is missing or invalid, a random demo account is generated instead.
To seed your own accounts:
cp accounts.example.json accounts.json
# Edit accounts.json with your real secretsThe file supports // and # line comments.
Note:
accounts.jsonis in.gitignore— it contains real secrets and should never be committed.
Each account can store an optional associated password alongside its TOTP secret:
- Add/Edit modal: new "Password (optional)" field, hidden by default (
type="password"); click 👁 to toggle visibility - Account card: password row appears only for accounts with a password — masked as
••••••••by default; toggle to reveal/hide; click the password itself to copy (works whether masked or visible) - Import / Export: password is preserved under the
passwordkey in JSON - Optional — accounts without a password simply show no password row
The app can encrypt all stored secrets at rest using AES-256-GCM.
- Enter edit mode → click ⋮ menu → Change Password to set a password.
- The password is run through PBKDF2-SHA256 (600 000 iterations) with a random 16-byte salt to derive a 256-bit AES key.
- Accounts are encrypted with AES-GCM (random 12-byte IV) and stored in
localStorageunderaccounts_encrypted. - Plain-text account data is removed.
- On reload the vault is locked — enter your password to decrypt and resume.
To lock the vault manually, click the 🔒 lock button in the topbar (visible when encrypted and unlocked).
To remove encryption, unlock the vault and clear the password.
All cryptographic operations use the browser's native Web Crypto API (crypto.subtle) — no external libraries.
Click the theme icon in the dropdown menu (⋮ button in edit mode) to toggle between light and dark themes.
- The theme is auto-detected by local time: 08:00–22:00 = light, otherwise dark.
- Click the toggle to override for the current session.
- Theming is implemented with CSS custom properties (
data-themeattribute on<html>). - The toggle is available via the dropdown menu in edit mode (click the edit icon).
| Action | How |
|---|---|
| Export JSON | Enter edit mode → click ⋮ menu → Export → downloads authenticator-export.json with all accounts. |
| Import JSON | Enter edit mode → click ⋮ menu → Import → select a .json file matching the account format above. |
| otpauth:// URIs | Import/export supports standard otpauth://totp/… URIs for interop with other authenticator apps. |
| QR Codes | In edit mode, click the QR icon on any account to display a scannable otpauth:// QR code. Scan it with Google Authenticator, Authy, or any TOTP-compatible app. |
Share accounts via URL — no server required.
- Enter edit mode → click ⋮ menu → Share
- Enter a password and click Generate URL
- Click the URL to copy it
- Share the URL with anyone
Recipients open the URL → enter password → accounts merge into their local storage.
How it works:
accounts → lz-string.compress → AES-GCM.encrypt(password) → base64 → #data=...
- Uses the same AES-GCM + PBKDF2 (600 000 iterations) as encryption at rest
- URL data is stored entirely in the URL fragment (
#data=) — never sent to a server - After 3 failed password attempts, the URL is cleared to prevent brute force
- Imported accounts are merged with existing accounts (no duplicates by composite key: issuer + name + secret)
The project deploys to Cloudflare Pages via GitHub Actions (.github/workflows/ci.yml).
| Trigger | Branch / Ref | Deployment |
|---|---|---|
Push tag v* |
main |
Production → totp-1sr.pages.dev + GitHub Release (zip/tar.gz) |
Manual (workflow_dispatch) |
— | Preview deploy |
CI Pipeline:
- Unit Tests —
npm test - E2E Tests — Playwright (Chromium, Firefox, Mobile Chrome, Mobile Safari)
- Deploy — only runs after both test jobs pass
The workflow uses cloudflare/wrangler-action@v3 and requires two repository secrets:
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_ID
The source contains the placeholder vDev. During CI the workflow runs:
sed -i "s/vDev/${GITHUB_REF_NAME}/g" index.htmlSo a release tagged v1.2.0 will display v1.2.0 in the UI; local development always shows vDev.
For maximum compatibility with Google Authenticator and other mobile apps:
- Use SHA-1 algorithm (default)
- Use 30-second period (default)
- Use 6 digits (default)
- Set the issuer field — Google Authenticator uses it to group and label entries
The generated otpauth:// URIs and QR codes follow the Key URI Format specification.
All data lives in the browser's localStorage:
| Key | Contents |
|---|---|
accounts |
Plain-text JSON array of accounts (when encryption is off) |
accounts_encrypted |
AES-GCM ciphertext + IV (when encryption is on) |
accounts_meta |
PBKDF2 salt and iteration count |
Use the Export button regularly to back up your accounts. localStorage can be cleared by the browser.
- Pure HTML / CSS / JavaScript — no framework, no build step
- Web Crypto API (
crypto.subtle) for HMAC-SHA-1/256/512 (TOTP) and AES-GCM + PBKDF2 (encryption) - qrcode.js vendored in
lib/— QR code generation - lz-string vendored in
lib/— compression for Share URL feature - Service Worker — offline-capable PWA with cache-first strategy (
sw.js,manifest.json) - CSS custom properties for light / dark theming
- Playwright for E2E testing
tests/
unit/totp.test.js # Unit tests (Node.js)
e2e/e2e.spec.js # E2E tests (Playwright)
npm test# Install browsers (one-time)
npx playwright install --with-deps chromium firefox webkit
# Run all tests
npm run test:e2e
# Run with UI
npm run test:e2e:ui
# Run specific browser
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=mobile-chrome
npx playwright test --project=mobile-safariTest coverage:
- Account CRUD (add, edit, delete)
- Import / Export JSON
- TOTP code display and clipboard
- Dark mode toggle
- Encryption (set password, lock/unlock)
- QR code modal
- Share URL (generate URL with password, import from URL)
- Mobile responsive layout
Licensed under the GNU General Public License v3.0.
[ { "name": "user@example.com", // Account name (required) "issuer": "GitHub", // Service provider (optional, recommended) "secret": "BASE32SECRET", // Base32-encoded TOTP secret (required) "algorithm": "SHA-1", // SHA-1, SHA-256, or SHA-512 (default: SHA-1) "period": 30, // Seconds: 30, 60, 300, 3600, 86400 (default: 30) "digits": 6, // 6 or 8 (default: 6) "url": "https://github.com", // Optional link on the account name "password": "s3cret" // Optional associated password (hidden by default) } ]