Documentation menu
TOTP

Verifying a code

Two ways to check the same six digits.

Because TOTP is symmetric, whoever holds the secret can check the code. Your backend already holds it, so you can verify in your own process or ask the U2 Secured Authenticator backend to do it. Both run the identical check and return the identical answer; they differ only in what else happens.

Verify locally

validateTOTP runs the check in your own process. No network call, nothing counted against your plan, and no dependency on us being reachable — a login cannot fail because our API is down. This is the right default for most integrations.

main.go
import "time"

if !u2auth.ValidateTOTP(storedSecret, "123456", time.Now(), 0, 0) {
    // Reject the login.
}

Verify through the API

client.verifyTOTP sends the secret and code to the U2 Secured Authenticator backend, which runs the same check and additionally applies the shared brute-force lockout and records the attempt in the Activity feed of the developer portal. Choose this when you want those two things without building them yourself.

main.go
res, err := client.VerifyTOTP(storedSecret, "123456")
if err != nil {
    return err
}
if !res.Valid {
    // Reject the login.
}

verifyTOTP takes a secret, not a user identifier

This is the most common mix-up. TOTP verification is stateless — it compares a code against a secret you already hold. User identifiers are only for push.

The two paths cannot disagree

A shared set of test vectors is asserted by every SDK and by the backend itself, so a change to the tolerance window or the truncation on either side fails a test before it can ship. Whichever path you pick, the answer is the same.

The accepted time window

Codes rotate every 30 seconds, and both paths accept the previous, current and next window to tolerate clock drift between the end user’s phone and your server. In practice a code is accepted for up to about 90 seconds.

The same code can be presented more than once

Neither path keeps a replay cache, so a code stays acceptable for its whole window even after a successful verification. If your threat model includes an attacker who can observe a code in transit, treat a verification as single-use in your own session handling — bind it to one login attempt and refuse a second.

Repeated failures

On the API path, consecutive failures for the same app and secret trip a lockout and start returning 429 with a Retry-After header. Successful verifications reset the counter. Surface the retry delay to the end user rather than presenting it as a wrong code.

Local verification has no lockout — validateTOTP answers true or false and counts nothing. If you verify locally, rate-limit failed attempts yourself, and remember the counter must be shared across your backend instances to be effective.