TOTP Implementation in Go: Best Practices
Bottom line
The Go ecosystem has one clear de-facto standard TOTP library - github.com/pquerna/otp (v1.5.0, Apache 2.0) - which implements both TOTP (RFC 6238) and HOTP (RFC 4226). Is used in production by major platforms including GitHub.
A simpler alternative, github.com/fosskers/totp (v1.0.1, MIT), provides a minimal RFC 6238-only implementation. For full authentication frameworks, github.com/shaj13/go-guardian/v2 integrates TOTP as one of many strategies. Correct Go implementation requires: using crypto/rand for secret generation, validating with a Skew of 1 (±30 s) for clock tolerance, encrypting secrets at rest, enforcing rate limiting, always pairing TOTP with a first factor (password), and providing recovery codes. TOTP itself has well-documented inherent weaknesses - phishing, adversary-in-the-middle, seed-key theft. Brute-force - most recently showed by Microsoft's "AuthQuake" vulnerability (fixed September 2024), which arose from extending the validation window to ~3 minutes without rate limiting.
Key findings
- Finding:
pquerna/otpv1.5.0 is the industry-standard Go TOTP library. It has shipped stable for >24 months (v1.0 released December 2015), supports both TOTP (RFC 6238) and HOTP (RFC 4226), and defaults to SHA1, 6 digits, 30-second period, and 20-byte secrets - all compatible with Google Authenticator. (Source-stated. Pquerna/otp GitHub releases and pkg.Go.Dev). - Finding: RFC 6238 recommends a maximum of one backward time step (±30 s with the standard 30 s period) for clock drift tolerance. A
Skewof 1 inpquerna/otp'sValidateOptsopens a window of three time steps (previous + current + next ≈ 90 s). This is the recommended setting. (Source-stated: RFC 6238 §5.2 and §6) - Finding: The AuthQuake vulnerability (disclosed January 2025, Oasis Security) proved that extending the TOTP validity window beyond the RFC recommendation and omitting rate limiting creates a brute-forceable system. Microsoft's MFA accepted codes for ~3 minutes, giving attackers a 3 % chance of guessing the correct code within one window and >50 % probability after ~70 minutes - all without triggering user alerts. Microsoft deployed a fix (stricter rate limiting) in September 2024. (Source-stated: WorkOS/AuthQuake report)
- Finding: TOTP is inherently vulnerable to phishing and adversary-in-the-middle (AiTM) attacks because the 6-digit code is entered manually. The 2023 Microsoft 365 AiTM campaign and 2022 Revolut breach are documented real-world examples. TOTP is never a enough standalone second factor; it must be paired with a strong first factor. (Source-stated: LinkedIn/Covr Security analysis)
- Finding: Recovery codes aren't optional. RFC 6238 §5.1 explicitly requires a fallback mechanism; industry practice (GitHub, Microsoft) is to issue single-use backup codes at 2FA enrollment. (Source-stated: pquerna/otp README, RFC 6238 §5.1)
- Finding:
fosskers/totpv1.0.1 is the only actively maintained minimal TOTP library in the Go ecosystem (last release August 2025; v1.0.1 fixed a digits-argument bug). It's MIT-licensed, 8-digit default output, and supports SHA1/SHA256/SHA512. (Source-stated: fosskers/totp GitHub releases)
Background
What is TOTP. TOTP (RFC 6238, May 2011, IETF) extends the HOTP algorithm (RFC 4226) by replacing the event counter with the current Unix time. A shared secret (typically 160 bits, Base32-encoded) and the current time step T = (UnixTime − T₀) / X (default X = 30 s) are fed into HMAC-SHA1. The output is truncated to produce a 6-digit code that changes every 30 seconds. TOTP is an open standard; Google Authenticator, Microsoft Authenticator, Authy, and others are all compatible clients.
Key organizations and people. RFC 6238 was authored by David M'Raihi (Verisign), Salah Machani (Diversinet), Mingliang Pei (Symantec), and Johan Rydell (Portwise). The most widely used Go implementation is maintained by Paul Querna (pquerna). Colin Woodbury (fosskers) maintains the competing minimal library. go-guardian is maintained by shaj13.
Current state
| Library | Latest version | License | TOTP/HOTP | Last release |
|---|---|---|---|---|
pquerna/otp |
v1.5.0 | Apache 2.0 | Both | May 2025 |
fosskers/totp |
v1.0.1 | MIT | TOTP only | Aug 2025 |
epikur-io/go-otp |
- | Apache 2.0 | Both | Stale / fork |
go-guardian |
v2 | MIT | TOTP via 2FA strategy | Active |
pquerna/otp is the most widely adopted; it's packaged in Debian (golang-github-pquerna-otp) and Fedora, and its API is considered the community standard. No security advisories have been published against any version.
Technical or implementation details
Algorithm (from RFC 6238 and pquerna/otp)
T = floor(CurrentUnixTime / Period) // Period = 30 s (default)
HMAC = HMAC-SHA1(secret, T as 8-byte big-endian uint64)
offset = last 4 bits of HMAC
truncated = (HMAC[offset:offset+4] & 0x7FFFFFFF) as uint32
code = truncated mod 10^Digits // Digits = 6 (default)
The same code applies to SHA256 and SHA512 as the HMAC hash function.
pquerna/otp API
Key generation:
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "MyApp",
AccountName: "[email protected]",
// Period: 30, // default
// SecretSize: 20, // default
// Digits: otp.DigitsSix, // default
// Algorithm: otp.AlgorithmSHA1, // default
})
secret := key.Secret() // Base32 string, store this
image := key.Image(200, 200) // QR code PNG
Validation:
valid, err := totp.ValidateCustom(code, secret, time.Now(), totp.ValidateOpts{
Period: 30, // must match generation
Skew: 1, // RFC-recommended: ±1 time step
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
})
ValidateCustom checks up to Skew time steps before and after the current step. With Skew = 1, it checks three 30-second windows (90 s total). The Go library inanzzz/otp example explicitly documents this: Window=1 means "previous – current – next".
Secrets
- Generate with
crypto/rand(nevermath/rand). - The
pquerna/otpdefault generates 20 random bytes (= 160-bit) viaio.ReaderinGenerateOpts.Rand. - Store encrypted at rest; never expose raw secrets to the client.
- A 160-bit Base32 secret yields ≈ 1.46 × 10⁴⁸ possible values; collisions are astronomically unlikely and are harmless anyway since TOTP validation is always scoped to a specific user's secret.
Clock drift and resynchronization
RFC 6238 §6 explicitly recommends that the validator track and record clock drift per token. After a successful validation, the server can record how many time steps the client was off and apply that offset on subsequent validations. This avoids needing a large Skew window. pquerna/otp doesn't implement automatic drift recording; implement it at the application layer.
OTPAuth URI format
otpauth://totp/{Issuer}:{AccountName}?secret={Base32Secret}&issuer={Issuer}&algorithm=SHA1&digits=6&period=30
All standard authenticator apps parse this URI from a QR code.
Evidence, comparisons, and related context
Library comparison
| Feature | pquerna/otp |
fosskers/totp |
go-guardian |
|---|---|---|---|
| TOTP | ✅ | ✅ | ✅ (via 2FA strategy) |
| HOTP | ✅ | ❌ | ✅ |
| QR code gen | ✅ (via key.Image()) |
❌ | ❌ |
| Custom skew | ✅ | ✅ | ✅ |
| SHA-256/512 | ✅ | ✅ | ✅ |
| Full auth framework | ❌ | ❌ | ✅ (JWT, OAuth2, LDAP, …) |
| Secret encryption | ❌ (app-layer) | ❌ (app-layer) | ❌ (app-layer) |
| Rate limiting | ❌ (app-layer) | ❌ (app-layer) | ❌ (app-layer) |
Note: No Go TOTP library provides secret encryption or rate limiting at the library level. These must be implemented in the application.
AuthQuake case study (Microsoft MFA, June 2024)
The AuthQuake vulnerability disclosed by Oasis Security (January 2025) is the most significant real-world TOTP failure in recent years. Microsoft's Azure AD MFA accepted TOTP codes for about 3 minutes (vs. The RFC-recommended 90-second maximum), and had no rate limiting. This combination meant an attacker had a 3 % success probability per code window, crossing 50 % after ~70 minutes of brute-force attempts, without triggering any user notification. Microsoft deployed a fix on September 10, 2024, adding rate limiting that activates for ~12 hours after suspicious activity is detected. This case is directly relevant: any Go TOTP implementation must explicitly set a Skew ≤ 1 and implement independent rate limiting.
Secret collisions
A 160-bit TOTP secret has ≈ 1.46 × 10⁴⁸ possible values. Even generating billions of secrets, the probability of collision is effectively zero. If two users somehow shared a secret, they would generate the same 6-digit code simultaneously - but since TOTP validation always loads the per-user secret from the database and checks only that one secret, no cross-user leakage occurs.
Limitations and critiques
| Limitation | Evidence | Severity |
|---|---|---|
| Phishing | TOTP codes are manually typed; the LinkedIn/Covr Security analysis documents Revolut (2022) and Microsoft 365 AiTM (2023) breaches. | High |
| Adversary-in-the-middle | AiTM proxy attacks capture and relay TOTP codes in real time before they expire. AuthQuake and Microsoft 365 campaign are documented examples. | High |
| Seed key theft | Malware (e.g., RedLine Stealer 2023) can extract TOTP seed files from infected devices. Once the seed is stolen, all future codes are compromised. | High |
| Extended validity window | Using Skew > 1 or a custom Period > 30 s widens the brute-force window exponentially. AuthQuake is the proof. | High |
| No rate limiting in libraries | Neither pquerna/otp nor any other Go TOTP library ships with built-in rate limiting. Implementations that omit this are vulnerable. |
High |
| No built-in replay protection | RFC 6238 §5.1 says the verifier MUST NOT accept the same OTP twice after successful validation, but Go libraries do not track used codes. Application-layer storage of the last-used timestamp is required. | Medium |
| Single OTP type | TOTP protects only the "something you have" factor. Without a strong first factor (password or passkey), TOTP provides essentially no security. | High |
| No transaction context | TOTP validates identity only, not what the user is doing. Session hijacking after TOTP authentication is unmitigated by TOTP alone. | Medium |
| Clock-dependency | Both client and server clocks must be approximately synchronized. The inanzzz example shows how Window=0 breaks validation under even slight drift. | Low–Medium |
Open questions
- Open question: How should a Go application layer implement per-user drift recording (RFC 6238 §6 resynchronization) - i.E., storing and applying a per-user
clockDriftStepsoffset after each successful validation? The RFC describes this pattern, but no Go TOTP library implements it. - Open question: What is the optimal balance between
Skewvalue and rate-limit threshold in production? The AuthQuake case showed that even a 90-second window is safe with rate limiting. The safe threshold when both are configured independently isn't empirically established for Go applications. - Open question: The
epikur-io/go-otprepository appears to be a near-identical fork ofpquerna/otpwith no visible divergences. Is it actively maintained? Current search results don't show any recent releases or changes. - Open question: For high-value targets, what migration path should applications adopt from TOTP to phishing-resistant methods (WebAuthn/FIDO2) in Go? Libraries like
go-guardiansupport multiple strategies, but no single Go library unifies TOTP enrollment and WebAuthn credential registration in a single API.
Practical takeaways
- Use
pquerna/otpv1.5.0 as your TOTP library. It's stable, widely deployed, and compatible with Google/Microsoft Authenticator. - Set
Skew = 1inValidateOpts. Never increase it beyond 1 without a documented reason. RFC 6238 explicitly limits this to one backward step. - Implement rate limiting at the authentication endpoint independently of the TOTP library. Lock out a user/IP after 5–10 failed OTP attempts within 15 minutes. This is the single most important mitigation against brute-force attacks.
- Store TOTP secrets encrypted at rest, using a server-side key management system (KMS or equivalent). Never log or expose raw secrets.
- Generate secrets with
crypto/randviaGenerateOpts.Rand(or manually withcrypto/rand.Read). - Always require the first factor (password) before validating TOTP. TOTP alone isn't an authentication mechanism; it's a second factor.
- Issue single-use recovery codes at enrollment time and store their hashes (not plaintext) in the database.
- Track and record per-user clock drift after each successful TOTP validation, applying the recorded offset on subsequent checks (RFC 6238 §6).
- Implement replay protection by recording the
last_used_totp_timestampper user and rejecting identical codes within the same time window. - Log every TOTP attempt (timestamp, user ID, IP, result) for audit and incident response.
- For new systems, consider WebAuthn/FIDO2 passkeys as the primary 2FA method - TOTP's phishing and AiTM weaknesses are structural and can't be fully patched.
Sources used
- Primary - library source: pquerna/otp - GitHub (Apache 2.0, stable since 2015)
- Primary - library docs: totp package - pkg.go.dev (v1.5.0 API reference)
- Primary - library releases: pquerna/otp releases - GitHub (via Kimi) (v1.5.0 confirmed May 2025)
- Primary - RFC specification: RFC 6238 - TOTP: Time-Based One-Time Password Algorithm (IETF, May 2011)
- Primary - alternative library: fosskers/totp - GitHub releases (v1.0.1, Aug 2025)
- Primary - auth framework: go-guardian - GitHub (v2, MIT, full auth framework with 2FA strategy)
- Technical - implementation walkthrough: Security: Implementing TOTP 2FA in Go - DevOps-DB (complete code + QR + validation flow)
- Technical - from-scratch implementation: Writing a TOTP client in Go - Redowan's Reflections (RFC 6238 annotated implementation)
- Technical - library building guide: Creating an OTP library in Go - inanzzz (Window/clock-drift logic explained with full test suite)
- Tutorial - full web app: How to Implement 2FA with TOTP in Golang - Permify (step-by-step web app with routes, sessions, QR)
- Critique - vulnerability analysis: The Hidden Vulnerabilities of TOTP - Covr Security / LinkedIn (phishing, AiTM, seed theft, transaction context)
- Critique - AuthQuake primary source: AuthQuake: Microsoft MFA vulnerable to TOTP brute-force - WorkOS (Oasis Security) (3 % per window, >50 % after 70 min, fixed Sept 2024)
- Best practices: Best Practices for OTP Implementation - soulfx (14-point checklist)
- Source gap noted:
epikur-io/go-otpREADME is a verbatim copy ofpquerna/otp; no release history or divergence could be confirmed - treat as fork with unconfirmed maintenance status.