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/otp v1.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 Skew of 1 in pquerna/otp's ValidateOpts opens 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/totp v1.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 (never math/rand).
  • The pquerna/otp default generates 20 random bytes (= 160-bit) via io.Reader in GenerateOpts.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 clockDriftSteps offset after each successful validation? The RFC describes this pattern, but no Go TOTP library implements it.
  • Open question: What is the optimal balance between Skew value 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-otp repository appears to be a near-identical fork of pquerna/otp with 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-guardian support multiple strategies, but no single Go library unifies TOTP enrollment and WebAuthn credential registration in a single API.

Practical takeaways

  1. Use pquerna/otp v1.5.0 as your TOTP library. It's stable, widely deployed, and compatible with Google/Microsoft Authenticator.
  2. Set Skew = 1 in ValidateOpts. Never increase it beyond 1 without a documented reason. RFC 6238 explicitly limits this to one backward step.
  3. 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.
  4. Store TOTP secrets encrypted at rest, using a server-side key management system (KMS or equivalent). Never log or expose raw secrets.
  5. Generate secrets with crypto/rand via GenerateOpts.Rand (or manually with crypto/rand.Read).
  6. Always require the first factor (password) before validating TOTP. TOTP alone isn't an authentication mechanism; it's a second factor.
  7. Issue single-use recovery codes at enrollment time and store their hashes (not plaintext) in the database.
  8. Track and record per-user clock drift after each successful TOTP validation, applying the recorded offset on subsequent checks (RFC 6238 §6).
  9. Implement replay protection by recording the last_used_totp_timestamp per user and rejecting identical codes within the same time window.
  10. Log every TOTP attempt (timestamp, user ID, IP, result) for audit and incident response.
  11. 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