One very important aspect of authentication is session management. Doing this wrong (or naively), can have catastrophic effects for your app!!

Let me provide an example: It's common knowledge that JWTs are very common. A lot of people who use JWTs, implement them as access tokens for their APIs. JWTs also require a shared secret key - what if this is stolen? Then an attacker can use that to hijack any user's account very trivially, and you may not even realise that it's happened! This is far worse than anyone getting hold of hashed passwords from your database.

That being said, I use the following flow for session management:
- User logs in, the backend issues a short-lived (~1 hour) JWT and a long-lived refresh token and sends them to the frontend.
- The frontend sends the JWT for each API call while it's still valid
- In the event that the JWT has expired, the frontend should then use the refresh token to get a new JWT AND a new refresh token (rotating refresh token - see https://tools.ietf.org/html/rfc6749#section-10.4)
- If the refresh token expires, then the user has to login again.

While this sounds quite straightforward, the key here is to use rotating refresh tokens - that's what actually makes it fare more secure than just using simple refresh tokens (i'd argue that it's almost the same level of security as just using a long lived access token)

Some of the benefits of this approach:
- You can detect token theft! If an attacker gets hold of the refresh / access token, because they keep changing, you can detect if an old token is used which is a strong singal of theft (see the RFC link above)
- You can change the JWT secret key without logging any users out: Once you change the key, all JWTs are instantly invalidated. But then your frontend client can simply use its refresh token to get a new access token signed with the new signing key (along with a new refresh token).
- Allow your users to be logged in for however long you want without compromising security.

Some implementation gotchas:
- When changing the refresh token, be sure to not invalidate the older token unless your backend is sure that the frontend has received the new token. This can be confirmed by the frontend using the new access / refresh token. This is important since if not done, and if the user is in a bad networked area, it can lead to them being logged out.
- See this blog and specifically this race condition: https://medium.com/hackernoon/the-best-way-to-securely-manag...

If you do not want to implement this on your own, you can also check out https://supertokens.io - It provides an end-to-end implementation of the above taking care of all race conditions and network failure issues. It also prevents other common web attacks which are on the OWASP top 10 list.

-----------------------

In terms of user authentication for the login part, I prefer using a no-password method - email or SMS OTP. The reason for this is that I do not have to care about managing user passwords (though that's not too difficult), I do not have to build forgot password flows, and most importantly, users don't have to remember yet another password.

I also only allow 3 attempts for OTPs per OTP. So after sending the OTP, if a user fails to input the correct one 3 times, then I revoke the old OTP and send a new one (this is so that someone can't simply brute force their way into an account). If the user login is successful, then I revoke all OTPs for that user. If the user clicks on sending the OTP again, then I send a different OTP (but the old one is still valid). This allows me to have a an OTP timeout of say 1 hour - which is more than enough!

According to me, this coupled with the above session management flow, is perfect!