TOTP 2FA QR Codes Explained

A technical deep-dive into the otpauth:// URI format, base32 secrets, and RFC 6238 TOTP math.

What is TOTP

TOTP (Time-based One-Time Password, RFC 6238) is the algorithm behind Google Authenticator, Authy, 1Password, Bitwarden, and every other authenticator app. It produces a 6-digit code that changes every 30 seconds based on a shared secret and the current Unix time.

The otpauth URI

otpauth://totp/Issuer:account@example.com?secret=BASE32SECRET&issuer=Issuer&algorithm=SHA1&digits=6&period=30

The URI scheme is otpauth://, the type is totp (the other option is hotp, counter-based), then the label, then query parameters. Authenticator apps parse this URI and add it to the user's vault with one scan.

The label

Optional format: Issuer:account@example.com. The Issuer prefix is redundant with the issuer query param but most apps respect both.

The secret

A base32-encoded byte string (A-Z and 2-7, no padding). At least 128 bits recommended, most servers use 160 bits (32 base32 chars). Never URL-encode the secret; leave it as raw base32.

Algorithm, digits, period

algorithm: SHA1 (default, universally supported), SHA256, or SHA512. digits: 6 (default) or 8. period: 30 seconds (default) or 60. Stick with defaults unless you have a reason, some authenticator apps don't implement the non-default options.

How the code is computed

At each 30-second interval, the authenticator does: HMAC-SHA1(secret, floor(unix_time / 30)), takes the last 4 bits of the HMAC as a dynamic offset, reads 4 bytes starting at that offset, masks to 31 bits, and modulo-reduces by 106 to get a 6-digit code.

Use Abundera QR to generate one

Open the TOTP generator, fill in issuer (your brand name), account (the user's email or username), and a base32 secret. Defaults for algorithm/digits/period are correct for nearly every server. Download the PNG and email or SMS it to the user, or embed it in your enrollment flow.

Privacy note

Your TOTP secret is a shared credential between the server and the user. Abundera QR never sees either. All encoding happens in the browser. Read the full privacy policy.