This guide provides practical examples for every feature in genotp-go, organized from simple to advanced. All examples use standard international English and match the actual library API.
- Secret Generation
- HOTP (Counter-Based)
- TOTP (Time-Based)
- Builder Pattern
- Config Pattern
- Base32 Encoding
- Provisioning URI
- Context Binding
- Secret Providers
- HMAC Providers
- Batch Verification
- Verifier (Replay & Rate Limiting)
- Clock Skew Detection
- Metrics
- Production Recommendations
Always use crypto/rand-backed functions. Never generate OTP secrets with math/rand.
secret, err := genotp.CreateSecret()
if err != nil {
log.Fatal(err)
}kg := &genotp.KeyGen{}
// 256-bit secret (32 bytes)
secret, err := kg.GenerateSecret(256)
if err != nil {
log.Fatal(err)
}
// Or fill an existing buffer
buf := make([]byte, 32)
if err := kg.FillSecret(buf); err != nil {
log.Fatal(err)
}Persist the secret encrypted at rest. Call ClearSecret() on HOTP / TOTP instances when they are no longer needed.
secret, _ := genotp.CreateSecret()
hotp, err := genotp.NewHOTP(secret, genotp.SHA1, 6)
if err != nil {
log.Fatal(err)
}
counter := uint64(0)
// Generate
code, err := hotp.Generate(counter)
if err != nil {
log.Fatal(err)
}
fmt.Println("Code:", code)
// Verify
ok, err := hotp.Verify(code, counter)
if err != nil {
log.Fatal(err)
}
if ok {
fmt.Println("Valid")
}Use this when the client counter may have drifted ahead (e.g., user pressed the button multiple times without verifying).
matchedCounter, ok, err := hotp.VerifyWithResync(code, serverCounter, 10)
if err != nil {
log.Fatal(err)
}
if ok {
fmt.Printf("Matched at counter %d\n", matchedCounter)
// Update serverCounter to matchedCounter + 1
}Bind the OTP to a specific session or device. An attacker who intercepts the code cannot use it from a different context.
ctx := genotp.NewOtpContextBuilder().
Session("sess-abc123").
IP("192.168.1.100").
Build()
code, err := hotp.GenBound(counter, ctx)
if err != nil {
log.Fatal(err)
}
// Later, verify with the same context
ok, err := hotp.VerifyBound(userCode, counter, ctx)hotp.ClearSecret()secret, _ := genotp.CreateSecret()
totp, err := genotp.NewTOTP(secret, genotp.SHA1, 6, 30)
if err != nil {
log.Fatal(err)
}
// Generate using current time
code, err := totp.Generate(nil)
if err != nil {
log.Fatal(err)
}
// Verify with a window of 1 period (current +/- 30s)
ok, err := totp.Verify(code, nil, 1)
if err != nil {
log.Fatal(err)
}fixedTime := uint64(1728000000) // Unix timestamp
code, _ := totp.Generate(&fixedTime)
ok, _ := totp.Verify(code, &fixedTime, 0)ctx := genotp.NewOtpContextBuilder().
Device("device-xyz").
Origin("https://example.com").
Build()
code, _ := totp.GenBound(ctx, nil)
ok, _ := totp.VerifyBound(userCode, ctx, nil, 1)Prefer coarse, application-defined location labels instead of raw GPS coordinates. This improves stability and keeps OTP verification resilient to location jitter.
ctx := genotp.NewOtpContextBuilder().
Session("login-abc123").
Region("id-lmg-bluluk").
GeoBucket("grid-a1").
DistanceClass(genotp.DistanceClassNearby).
Build()
code, _ := totp.GenBound(ctx, nil)
ok, _ := totp.VerifyBound(userCode, ctx, nil, 1)Valid distance classes are:
genotp.DistanceClassSameAreagenotp.DistanceClassNearbygenotp.DistanceClassFar
Use a SecretProvider when your application stores OTP secrets in wrapped or
externally-managed form and only wants to resolve them at the moment of use.
provider := genotp.SecretProviderFunc(func() ([]byte, error) {
// Example: decrypt with KMS, unwrap from HSM-adjacent storage,
// or fetch from an external secret manager.
return loadUserSecret()
})
totp, err := genotp.NewTOTPFromSecretProvider(provider, genotp.SHA256, 6, 30)
if err != nil {
log.Fatal(err)
}
code, err := totp.Generate(nil)
if err != nil {
log.Fatal(err)
}Notes:
- This is an additive API. Existing
NewHOTP(...)andNewTOTP(...)flows are unchanged. - In this v1 model, the provider resolves secret bytes per OTP operation.
- Temporary secret buffers are zeroed after use.
- This is best suited for wrapped-secret / KMS-unlock flows.
- A future
HMACProviderstyle abstraction can support true non-exportable HSM-backed keys without changing this API.
Use an HMACProvider when the OTP key must remain non-exportable and all HMAC
operations must be delegated to an HSM, KMS-native MAC API, or remote signing
service.
provider := genotp.HMACProviderFunc(func(algorithm genotp.Algorithm, message []byte) ([]byte, error) {
return signWithHSM(algorithm, message)
})
hotp, err := genotp.NewHOTPFromHMACProvider(provider, genotp.SHA1, 6)
if err != nil {
log.Fatal(err)
}
code, err := hotp.Generate(42)
if err != nil {
log.Fatal(err)
}Notes:
- This is an additive API. Existing constructors and
SecretProvidermode are unchanged. - The library never resolves raw secret bytes in this mode.
- The provider must return a full HMAC output matching the requested algorithm.
- This is the preferred path for true non-exportable HSM-backed OTP keys.
See provider_adapters.md for concrete adapter
patterns such as AWS KMS decrypt-backed SecretProvider and Vault Transit
HMACProvider.
Use batch verification when your application needs explicit per-item OTP checks for bulk workflows such as webhook authentication, queued internal jobs, or transaction processing.
The v1 batch APIs are intentionally simple:
- They are additive. Existing single-item verify APIs are unchanged.
- They preserve the exact semantics of single-item verification.
- They run sequentially and return one result per input item.
- They do not add hidden concurrency, replay behavior, or global short-circuiting.
results := hotp.VerifyBatch([]genotp.HOTPVerifyRequest{
{Code: "755224", Counter: 0},
{Code: "287082", Counter: 1},
})
for _, result := range results {
if result.Err != nil {
log.Printf("verify error: %v", result.Err)
continue
}
log.Printf("ok=%v", result.OK)
}results := totp.VerifyBoundBatch([]genotp.TOTPVerifyBoundRequest{
{
Code: codeA,
Context: ctxA,
Time: &timeVal,
Window: 1,
},
{
Code: codeB,
Context: ctxB,
Time: &timeVal,
Window: 1,
},
})Use batch verification for clearer orchestration and per-item reporting. For provider-backed modes, it also gives adapter authors a stable surface to add their own higher-level batching outside the library when their backend supports it.
See the Clock Skew Detection section for the recommended pattern.
Use builders when you want a fluent, readable configuration.
secret, _ := genotp.CreateSecret()
hotp, err := genotp.NewHotpBuilder().
Secret(secret).
Algorithm(genotp.SHA256).
Digits(6).
Build()
if err != nil {
log.Fatal(err)
}totp, err := genotp.NewTotpBuilder().
Secret(secret).
Algorithm(genotp.SHA256).
Digits(6).
Period(30).
Build()
if err != nil {
log.Fatal(err)
}Use configs when you want to predefine settings and reuse them.
// Predefine a config
cfg := genotp.NewTotpConfig().
WithAlgorithm(genotp.SHA256).
WithDigits(6).
WithPeriod(30)
// Instantiate multiple TOTPs from the same config
for _, secret := range userSecrets {
totp, err := genotp.NewTotpFromConfig(secret, cfg)
if err != nil {
continue
}
// use totp ...
}The library provides RFC 4648 Base32 encoding without padding. This is the standard format used by authenticator apps.
secret, _ := genotp.CreateSecret()
// Encode for QR code / manual entry
encoded := genotp.EncodeBase32(secret)
fmt.Println(encoded) // e.g., "JBSWY3DPEHPK3PXP"
// Decode back to bytes
dst := make([]byte, 64)
n, err := genotp.DecodeBase32(dst, encoded)
if err != nil {
log.Fatal(err)
}
decoded := dst[:n]DecodeBase32 automatically strips whitespace, hyphens, and padding characters.
Generate otpauth:// URIs for authenticator apps.
secret, _ := genotp.CreateSecret()
b32 := genotp.EncodeBase32(secret)
uri := genotp.NewOtpAuthUri(genotp.TotpType, "ACME:alice@example.com", b32).
Issuer("ACME").
Algorithm(genotp.SHA1).
Digits(6).
Period(30).
Build()
fmt.Println(uri)
// otpauth://totp/ACME:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME&algorithm=SHA1&digits=6&period=30uri := genotp.NewOtpAuthUri(genotp.HotpType, "ACME:alice@example.com", b32).
Issuer("ACME").
Algorithm(genotp.SHA1).
Digits(6).
Counter(0).
Build()Context binding prevents phishing and replay by coupling an OTP to a specific environment.
ctx := genotp.NewOtpContextBuilder().
IP(clientIP).
Device(deviceID).
Session(sessionID).
Origin("https://example.com").
Custom("team", "engineering").
Build()Origin normalization is automatic: scheme + host only, lowercased, with query, fragment, and trailing slash removed.
- HMAC binding (
GenBound/VerifyBound): The context bytes are mixed into the HMAC. The same secret + counter + different context yields a completely different code. - Verifier-stored (
VerifyWithContext): The code is standard, but the server validates the context match before accepting.
Use HMAC binding when you want the code itself to be context-sensitive. Use verifier-stored when you need standard codes that must also pass a context check on the server.
Verifier is the recommended way to validate OTPs in production. It adds replay protection and rate limiting on top of the basic code match.
verifier := genotp.NewVerifier(5) // max 5 failed attempts
// expectedCode comes from your HOTP/TOTP.Generate
ok := verifier.VerifyWithReplayProtection(userCode, expectedCode)
if !ok {
if verifier.IsRateLimited() {
fmt.Println("Too many attempts. Try again later.")
} else {
fmt.Println("Invalid or reused code.")
}
}ctx := genotp.NewOtpContextBuilder().IP(clientIP).Build()
ok := verifier.VerifyWithContext(userCode, expectedCode, ctx, ctx)In a web application, create one Verifier per user session or account. Store it in a map or cache keyed by user ID.
type UserSession struct {
Verifier *genotp.Verifier
Secret []byte
}
// On login attempt
session := sessions[userID]
expected, _ := totp.Generate(nil)
ok := session.Verifier.VerifyWithReplayProtection(userCode, expected)
if ok {
session.Verifier.ResetAttempts()
}NewVerifier uses an in-memory replay store. For multi-replica deployments (Kubernetes, ECS, etc.), provide a distributed backend.
// Implement genotp.ReplayStore with Redis SET NX EX
distributedStore := NewRedisReplayStore(redisClient)
verifier := genotp.NewVerifierWithStore(
5, // max attempts
distributedStore, // shared across replicas
90*time.Second, // replay TTL
)See distributed_replay_protection.md for a Redis implementation example.
Use ClockSkewDetector to detect and compensate for time drift between the client device and the server.
totp, _ := genotp.NewTOTP(secret, genotp.SHA1, 6, 30)
detector := genotp.NewClockSkewDetector(64)
ok, _ := totp.VerifyTracking(userCode, nil, 1, detector)
if ok {
report := detector.Report()
fmt.Printf("Mean offset: %.2f periods\n", report.MeanOffset)
fmt.Printf("Recommendation: %s\n", report.Recommend)
}detector := genotp.NewClockSkewDetector(64)
detector.EnableAutoAdjust()
// After enough samples (>= 16), the detector automatically compensates
ok, _ := totp.VerifyTracking(userCode, nil, 1, detector)report := detector.Report()
switch report.Recommend {
case genotp.ConsistentDrift:
log.Println("Client clock is drifting. Consider widening the window.")
case genotp.WidenWindowOrCheckNtp:
log.Println("Many edge hits. Check NTP sync or increase window.")
case genotp.NoActionNeeded:
// Everything is fine
case genotp.InsufficientData:
// Not enough samples yet
}Track generation and verification counts for observability.
metrics := genotp.NewMetrics()
// In your HOTP/TOTP wrapper
func (s *Service) GenerateTOTP(secret []byte) (string, error) {
code, err := s.totp.Generate(nil)
if err != nil {
metrics.IncrementError()
return "", err
}
metrics.IncrementTotpGeneration()
return code, nil
}
func (s *Service) VerifyTOTP(secret []byte, code string) (bool, error) {
ok, err := s.totp.Verify(code, nil, 1)
if err != nil {
metrics.IncrementError()
return false, err
}
if ok {
metrics.IncrementTotpVerification()
}
return ok, nil
}
// Read metrics
fmt.Printf("Generations: %d\n", metrics.GetTotpGenerations())
fmt.Printf("Verifications: %d\n", metrics.GetTotpVerifications())
fmt.Printf("Errors: %d\n", metrics.GetErrors())Never ignore errors from constructors or methods. Invalid secrets, algorithms, or counters return concrete errors.
hotp, err := genotp.NewHOTP(secret, algo, digits)
if err != nil {
return fmt.Errorf("init HOTP: %w", err)
}Never rely solely on Verify(). Always wrap verification with Verifier for replay protection and rate limiting.
Store secrets encrypted in your database. Decrypt only when needed for generation or verification.
Do not share a single Verifier across all users. Create one per user or session to prevent cross-user rate-limit interference.
If you run multiple server instances, use NewVerifierWithStore with Redis, etcd, or SQL as the backend.
For high-security flows (banking, admin panels), use GenBound / VerifyBound or VerifyWithContext to bind codes to IP + device + origin.
Call ClearSecret() when an HOTP / TOTP instance is no longer needed. Note that Go's garbage collector may retain memory pages; for extreme security, manage secret lifetimes with sync.Pool or pinned buffers.
SHA1 is the default for compatibility with older authenticator apps. For new systems, prefer SHA256 or SHA512.
totp, _ := genotp.NewTOTP(secret, genotp.SHA256, 6, 30)Export Metrics to your monitoring stack (Prometheus, StatsD, etc.) to detect anomalies in OTP generation or verification rates.