A shared secret and a time step
The app and server calculate a code from a shared secret and a time counter. The usual step is 30 seconds; no new message from the server is needed for each code.
Keep clocks aligned
A clock difference can cause a correct-looking code to fail. A small validation window accommodates modest drift, but accepting too many steps expands the opportunity for guessing.
Reject reused codes
Record successful use so the same time step cannot authenticate again. A setup flow should confirm a valid code before activating the additional login requirement.