For password reset, you just create a record with a unique token and send an email that links back to the app with the unique token in the url.
Email verification is basically the same: send an email with a link that identifies the user and hit the server with the unique token when that page loads.
I mean, you could take something as facile as the sha1 of the current microtime, and a random concatenation of the user's data from the user table and that would already require so much access that figuring out the token wouldn't even be your biggest problem.
I once collected a $3k bug bounty over this. Python's use of Mersenne Twister in the lib/random module should not be used for token generation. Mersenne twister uses a relatively small state space and is fully deterministic (it never re-seeds or mixes in new entropy). If you get a couple sequential random values you can reconstruct that state space and predict all future values. I.E. request a password reset 10x in a row and examine the tokens in the emails.
Please only use secure random number generators when creating security related tokens.
Example blog post: https://know.bishopfox.com/blog/2014/08/untwisting-mersenne-...
Edit: never mind, your article named it as a good choice
Other flaws exist like abysmally poor key spaces. If your prng has a period of 64k and the reset links are generated deterministically from the prng, you're going to have a bad time.
The sha1 of microsecond+userdata is interesting. It has the potential to work well, but it's easy to get wrong. Latency measurements, the framework you're using, and other pieces of information can reduce microsecond timings to a few bits of entropy (e.g. there are modern systems that can only measure time aligned to 15millisecond boundaries). Once you take out the PII (most of which the attacker has access to already, so it isn't buying additional entropy), in systems I've seen there isn't that much real entropy in user state (sometimes under 12 bits even with tens of millions of users), and users who haven't interacted with your system much will have much less. If your system is closed source you might buy some security through obscurity, but that never lasts, and the underlying crypto is _probably_ flimsy at best.
It wouldn't take that much effort to go through my claims and find special cases where the strategies would work well, find workarounds for the attacks mentioned, and whatnot. That isn't really the point though. What matters is that getting this right is hard, and even a system which looks good enough might have subtle flaws that render its security all but useless. Maybe if we went back and forth enough we'd find all the problems, but there are already battle-tested solutions that are almost certainly better than anything we're going to come up with here, and in any application where security matters, ignoring those drop-in solutions is probably the wrong choice.
That said, it might very well be the case that having a certain percentage of user accounts compromised is an acceptable trade-off (or even desirable? could you then charge people to monitor their accounts for suspicious activity à la Equifax?). I think that's a choice that should be made consciously though, not as an afterthought arising from a broken security model.
Which is how some people seem to approach security advice... "either it's up to my ideal standard, or it's a completely idiotic implementation that will surely be hacked in a fortnight."
You seem to have some balance and I applaud that. Security is a balancing act between the level of security, development and maintenance difficulty, and user experience and you have to negotiate an acceptable level that at least exceeds the bare minimum of security required.
Need high performance swoole can practically compete with go, but you could still put now server intensive services in go or rust.
On most apps I find myself writing my own register / login / password reset logic, and just using Auth::login($user); to get the user logged in and give them a session.
From that point onwards though, Laravel does a fantastic job and Passport is a god send.
This is why, for example, most sites will confirm/deny the existence of an account for a given email (provided by an attacker) during password reset flow. There simply aren’t great prepackaged solutions for the part beyond “hash it and put it in the db”.
This is also why sites that do 2FA frequently fall prey to the “steal the SIM and you can reset the password with no other factor” attack so commonly.
As for eMail verification, you create and store a verification ID when the account gets made (I use a Guid), and it is this that the system dumps into the verification link that gets sent to the end user. Every time the username gets changed, that Guid also gets changed (along with the verified flag getting cleared) so another verification link can get sent out that is unique. Reusing a link is always bad, because you want to ensure the user goes after the most recent link.