Not op, but I've written several apps with a similar setup to what was described. Both email verification and password resets are pretty simple to implement yourself.
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.
The hardest thing is probably making sure you're appropriately using an appropriate api for generating unpredictable tokens. Generating random tokens is a trade off between speed and unpredictability and some easy-to-find random number apis make the wrong trade off.
Most any language's built in psuedorandom number generator is going to be sufficiently random that you will have no trouble.
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.
> Most any language's built in psuedorandom number generator is going to be sufficiently random that you will have no trouble.
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.
Many mainstream languages have separate cryptographically secure rngs. The standard built-in rngs have tons of flaws for crypto work. A plausible attack vector here is something akin to a chosen plaintext attack -- request a stream of password resets to accounts the attacker controls to find the current state of the prng. Usually a couple readings suffice for the language builtins to uncover the rest of the random stream. With that newfound knowledge, it's game over, and the attacker controls the password reset links for _anyone_ requesting a reset -- polling the future reset links to account for other uses of the language's rng and to keep the attacker's internal rng stream in sync with your service.
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.
I agree with everything you said. Use what the professionals have created. My point is just that some of these facile methods like my example are worse implementations and should be upgraded. But they aren't completely useless and just as bad as a 4 character password stored in plaintext on the server.
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.
Laravel has verification and reset baked into their default auth, need oauth or jwt for a mobile app? Laravel passport has you covered. Fb/Google? Socialite.
Need high performance swoole can practically compete with go, but you could still put now server intensive services in go or rust.
I'm a full time Laravel dev and love Laravel, but it's default auth is one of my least favorite parts. I personally feel like it's abstracted to an extreme, and to the point where you have to take it or leave it wholesale.
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.
My understanding is that most people roll their own (for the minority case of users not logging in via Google or Facebook identity).
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.
Password reset involves a separate table to record user’s requests to reset, which also acts as an audit log of attempts (successful or not).
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.
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.