A mobile authentication app keeps one small secret that should never leave the device.
A mobile authentication app has one job, and it is smaller than people expect. Hold a shared secret, read the clock, turn the two into a six digit code. Everything hard about the category sits around that job rather than inside it: where the secret lives, how it got there, and what happens the day the phone goes in a river. If you are adding a second factor to something you already run, app security basics covers the ground underneath this page.
Below: how a TOTP app derives a code with no network, where the seed belongs on iOS and Android, how enrolment and recovery work, and what an authenticator app will not do for you.
See how wide the validation window isThe short version
The algorithm is settled. The storage and the recovery are the product.
Time based one time passwords are a published standard with reference code attached. Nobody is asking you to invent them. You can be correct on the algorithm in an afternoon and still ship something nobody should trust.
The parts that decide whether the app is any good are duller. Whether the seed sits in the device keychain or in plain storage any code on the phone can read. Whether a user with a new handset re-enrols eleven accounts by hand. Whether a support person can clear a factor, and whether that reset is itself the way in.
How a TOTP app turns the time into a code
A code is an HMAC of a counter, and the counter is the clock divided by a time step. RFC 6238 puts the default time step at 30 seconds and counts from the Unix epoch. Phone and server reach the same counter without exchanging a byte. Truncation on top turns the hash into the digits on screen.
The hash is not fixed. The standard is built on HMAC-SHA-1 and says implementations may use HMAC-SHA-256 or HMAC-SHA-512. Own both ends and you can pick a stronger one. If your codes have to work in somebody else's authenticator app, you are taking SHA-1, six digits and 30 seconds, because that is what the installed base accepts.
Clocks drift, and drift is what generates support tickets. The standard recommends a specific limit on how many time steps a prover may be out of synch before rejection. For network delay it recommends at most one time step. A wider window is kinder to users and leaves more codes live at once. That trade is the whole tuning exercise.
Notice what is missing. The phone contacts nothing to produce a code, so a 2FA app works in a tunnel, on a plane, with the SIM removed. Any network call you add is there for enrolment or recovery, not for the code.
RFC 6238, TOTP: Time-Based One-Time Password Algorithm
Try it
How wide the validation window really is
A server never accepts one code. It accepts a run of them. Widen the window and fewer people call about a wrong code, but more codes are live at the same moment.
3 codes accepted at once
Anything issued inside a 90 second span passes. A phone whose clock is 45 seconds out is rejected, because that is past the 30 seconds of slack on each side. The standard says keep this tight, so widening it is a support decision you make on purpose.
Where the seed is stored decides how bad the worst day is
The seed is a bearer credential. Anything that can read it mints valid codes forever, quietly, from anywhere, and nothing in the protocol notices. So storage is not a tidy-up task at the end of the build. It is the build.
On iOS, Expo's SecureStore writes to keychain services as a generic password item. On Android it writes to SharedPreferences encrypted with the Android Keystore system. It is not supported on web. That matters if you pictured the same code running in a browser, because there the fallback is not weaker, it is absent.
Two options on that API are product decisions in technical clothes. Set keychainAccessible to WHEN_UNLOCKED_THIS_DEVICE_ONLY and the entry is not migrated to a new device when restoring from a backup. That is right for a seed. It is also what sends every upgrading user back through enrolment. Turn on requireAuthentication and a device unlock sits in front of the value. On iOS that prompt appears when reading or updating an existing value, not when creating one.
Worth checking rather than assuming for an AI built app. In a capture of a real Newly build, the generated project listed expo-secure-store among the plugins in its app.json, and had picked SecureStore for on device storage without being asked. So the keychain path is genuinely there. What we did not confirm is that any given build sets the two options above, so read those lines in your own code.
Enrolment, and the day the phone is gone
A secret arrives on the phone one of two ways: a QR code the camera reads, or a string somebody types. Most apps accept the same otpauth URI, carrying the issuer, the account name, the base32 secret, and optional overrides for algorithm, digits and period. The scanner is a day of work. Deciding who may be shown that QR code, and after what proof, is the security design.
Recovery is where homemade factors fall over. A second factor nobody can recover locks people out of their own accounts. A second factor anyone can recover by asking nicely is not a factor. Choose before launch: backup codes shown once at enrolment, a second enrolled device, or a named person who may clear the factor, with a log line recording it.
If codes have to appear on a tablet as well as a phone, the seed exists in two places. That turns into a question about where the secret is stored off the device, and who can read it there. Answer that before you promise anyone sync.
Most of the time this is not a standalone product anyway. The thing people sign into is a portal. The enrolment screen belongs beside the other account settings in that client portal app, not in a separate download nobody wants to install.
What an authenticator app will not do for you
A code proves someone holds the seed. It does not prove they meant to sign into the site in front of them. A convincing fake login page can ask for the code and spend it inside the same 30 seconds. The real answer to phishing is a credential bound to the site, such as a passkey.
Approval prompts, the ones that say approve this sign in on your phone, are a different architecture. They need a server that knows a login is pending, and a push aimed at the right device. So they do not work offline and they do need a backend. Server sent push is the path. No device local alarm raises a prompt by itself while the app is closed.
Shared devices break the model outright. One handset on a workshop bench used by nine people cannot carry nine personal seeds. What you want there is closer to a kiosk mode app, with per person sign in on top of a locked down shell.
The app is also not the policy. Which accounts require a second factor, what the support desk may reset, how long a session survives after a code is accepted: all of that lives on the server. The phone is only where one number gets derived.
What each second factor actually gives you
| Approach | Works with no signal | Secret stays on the device | Survives a lost phone | You control enrolment |
|---|---|---|---|---|
| SMS codes | No | no secret on the device | Yes | No |
| A third party authenticator | Yes | unless cloud backup is on | only with that backup | No |
| TOTP inside your own app | Yes | Yes | only if you build recovery | Yes |
| Push approval prompts | No | Yes | re-enrol on the new device | Yes |
| Passkeys | Yes | Yes | platform dependent | partly |
Building one around your own accounts
Existing authenticators are free, widely audited and already on the phone. For a plain second factor on a normal login, use one. They stop fitting when the factor is part of a product rather than part of a login. When the code screen has to sit inside the app your customer already has. When enrolment is tied to something your business verified in person. When the device is locked down and cannot install from a public store.
Newly is an AI app builder. You describe the app, including the storage and recovery rules you just settled. It writes a real React Native and Expo project you own, runs it on a cloud simulator while it builds, and uploads iOS builds to TestFlight. Plans start at $25 a month and there is no free plan. iOS release builds go out through App Store Connect with your own Apple Developer account, and Android goes to Google Play internal testing from the Deploy tab, or out as a standalone APK. It is not an identity provider and it will not decide your policy.
Whatever route you take, keep the scope brutal. One secret per account, held in the keychain, a code on screen, an enrolment flow, a recovery path. Nothing else. Every extra screen in an authenticator is another way to leak the thing it was built to protect.
Questions people ask about authenticator apps
An app that holds a shared secret for one or more accounts and turns it into a short code proving possession of that device. Most implement TOTP, so the code comes from the secret plus the current time. There is no network call at the moment you read it.
Describe the factor you actually need
Write down where the secret lives, who may enrol one, and what a lost phone costs you, then build the app around those three answers.
Start building