First of all, I know, don't reinvent the wheel... But there's a good reason for this.
So I have been implementing a generic 2 factor authentication code generation/validation algorithm in order to re-use it in multiple applications and also make it stateless (the algorithm doesn't need to store anything in the database, and it doesn't require any user data at all.)
The application using the code is responsible to set a payload and validate it within the generic 2 fa process.
So here's how it would work...
1 - Application requests a new 2FA code passing down a payload:
1.1 - payload: arbitrary data, such as a user id and application id.
1.2 - a random 7 digits code is created (using crypto secure random libraries)
1.3 - an expiration time is selected, i.e. 3 minutes from current time, expressed as epoch time in seconds.
1.4 - data: (expiration, payload, code)
1.5 - data is encrypted using AES CTR with a randomly generated IV of 128 bits and a secret key
1.6 - both encrypted data and IV are digested through an HMAC that uses another secret key, in order to sign these two values.
1.7 - final token: 'base64 encrypted data : IV : signature'
1.8 - the token is returned to the user UI (such as a mobile app or login webiste)
1.9 - the code is sent through SMS or Email
2 - Application submits the original token, and user provided code to the backend
2.1 - token is decoded, and signature is validated
2.2 - token is decrypted, expiration checked, and user code is compared against the token code
2.3 - payload is returned to the application, which performs any additional validation such as fetching a user from the user id in the payload, etc...
Is this broken? You might ask why not just store the 7 digits code in the database, but the whole idea is to make this completely stateless and generic.
Also note: The endpoint for token generation/validation should be throttled in a way that it can't be brute forced.