- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Two-Factor Authentication: How Those Six-Digit TOTP Codes Actually Work
About Post
Put your phone in airplane mode. Open your authenticator app. The six-digit codes keep changing every 30 seconds, and they still work when you type them into a website.
No internet. No message from the server. And yet your phone and a server on the other side of the world agree on the same random-looking number, at the same moment.
It feels like magic the first time you notice it. It's actually one of the neatest small ideas in security, and once you understand it, you'll also understand exactly where two-factor authentication is strong and where it isn't.
The trick: a shared secret and a shared clock
The codes come from TOTP, Time-based One-Time Passwords, defined in RFC 6238. The whole scheme needs only two ingredients that both sides have:
- A shared secret. A random key the server generates when you enable 2FA. It's inside that QR code you scan, and it's the only time it ever travels between the two sides.
- The current time. Your phone and the server both know roughly what time it is.
Each side runs the same calculation, secret + current time → six digits, independently. If the results match, you've proven you have the secret, without sending the secret itself.
Think of two spies who were handed the same codebook before they parted ways. Each day they turn to that day's page. No phone call needed.
Step 1: what's in the QR code
The QR code you scan is just a URI:
otpauth://totp/MyApp:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&period=30&digits=6
The secret is Base32 encoded (letters A–Z and digits 2–7), because it was designed to be typed by hand if the camera fails. The other parameters are usually defaults: 30-second periods, 6 digits, SHA-1.
Step 2: turn time into a counter
TOTP doesn't use the exact time. It divides the Unix timestamp by 30 and throws away the remainder:
$counter = intdiv(time(), 30);
Everyone in the same 30-second window gets the same counter. That's why the code changes every 30 seconds, and why it doesn't need perfectly synchronised clocks, just clocks that are roughly right.
Step 3: HMAC, then squeeze it into six digits
The counter and the secret go into HMAC-SHA1, a keyed hash. You can't reverse it to get the secret, and you can't predict the output without the secret. That's the security of the whole scheme.
HMAC gives you 20 bytes. To get six digits, the algorithm uses "dynamic truncation": the last 4 bits of the hash pick an offset, it reads 4 bytes from there, and takes the result modulo 1,000,000. Here's the whole thing in PHP (simplified: $secret is the raw, already Base32-decoded key):
function totp(string $secret, ?int $time = null): string
{
$counter = intdiv($time ?? time(), 30);
$hash = hash_hmac('sha1', pack('J', $counter), $secret, true);
$offset = ord($hash[19]) & 0x0F;
$number = ((ord($hash[$offset]) & 0x7F) << 24)
| (ord($hash[$offset + 1]) << 16)
| (ord($hash[$offset + 2]) << 8)
| ord($hash[$offset + 3]);
return str_pad((string) ($number % 1_000_000), 6, '0', STR_PAD_LEFT);
}
pack('J', ...) writes the counter as an 8-byte big-endian number, which is what the spec expects. The & 0x7F drops the sign bit so the result is always positive. That's genuinely the entire algorithm. In a real app you'd use a well-tested library (Laravel Fortify includes 2FA) rather than this function, but it's worth seeing how small it is.
Verifying a code on the server
The verification side is where most of the real-world care goes:
- Allow a small window. Accept the current counter and one step either side, so a slightly slow phone clock or a slow typist still works.
- Compare with
hash_equals(), not===, to avoid timing differences. - Block replays. Store the last counter that was used successfully and reject the same one again. Otherwise a code someone shoulder-surfed is valid for its whole window.
- Rate limit attempts. Six digits is a million combinations. Without a limit, that's a brute-force target, not a second factor.
- Encrypt the secret at rest. Anyone with the database and the secret can generate valid codes forever.
Recovery codes: the part everybody skips
People lose phones. If 2FA has no recovery path, your support team becomes the recovery path, and a support agent who can be talked into disabling 2FA is a weaker link than any algorithm.
The standard answer is a set of single-use recovery codes, shown once when 2FA is enabled. Treat them like passwords: store them hashed, mark each as used, and let users regenerate them. And make the "show them once, save them now" moment clear in the UI.
Why SMS codes are the weakest option
SMS codes look similar to the user, but the security model is different. The code travels over the phone network, so anyone who controls your number gets your code:
- SIM swapping: an attacker convinces a mobile carrier to move your number to their SIM.
- Interception: weaknesses in the telecom signalling network can allow messages to be redirected.
- Lock screen previews: the code pops up on a phone that's lying on a desk.
SMS 2FA is still much better than nothing. But if you're building 2FA, offer an authenticator app first and SMS as a fallback.
The honest limitation: TOTP proves you have the secret. It doesn't prove you're on the real website. A convincing phishing page can ask for your password and your current code, and replay both to the real site within the 30 seconds.
Where passkeys come in
That phishing gap is exactly what passkeys (built on WebAuthn) close. Instead of a code you type, your device holds a private key, and the browser only lets it sign a challenge for the website it was created for. A lookalike domain gets nothing, because there's nothing for the user to type and hand over.
Passkeys are the direction the industry is moving, and they're worth offering if your users' devices support them. But TOTP will be around for years: it's simple, works offline, and every authenticator app speaks it.
If you remember one thing
A TOTP code is just HMAC(secret, time ÷ 30), cut down to six digits. The algorithm is the easy part. The security lives in everything around it: rate limits, replay protection, encrypted secrets and a recovery flow that support can't be tricked into bypassing.
If you've implemented 2FA, which part took you longest: the code itself, the recovery flow, or convincing users to turn it on?

Be first to comment it...