▲ | sholladay 9 days ago | |
From a design perspective, the reason this flaw exists is because the code can be typed on any machine and sent through any intermediary. More secure schemes are possible without much effort. Magic links have some pros/cons but overall I think they are better. Here is what I do when the user logs in and email verification is needed: 1. Generate a UUID on the server. 2. Save the UUID on the client using the Set-Cookie response header. - The cookie value is symmetrically encrypted and authenticated via HMAC or AES-GCM by the server before it is set, such that it can only be decrypted by the server, and only if the cookie value has not been tampered with. This is very easy to do in hapi.js and any other framework that has encrypted cookies. - Use all the tricks to safeguard the cookie from being intercepted and cloned. For example, use a name with the __Host- prefix and these attributes: Secure; HttpOnly; SameSite=Lax; 3. The server sends an email to the user with a link like https://site.com/verify?code=1234, where 1234 is the UUID. 4. The user clicks the link and has their email verified. - When the link is clicked, the browser sends the Cookie header automatically, the server decrypts it and compares it to the UUID in the URL and if that succeeds, the email has been verified. Again, this is very easy in hapi.js, as it handles the decryption step. - Including the UUID in the magic link signals that there is _supposed_ to be a cookie present, so if the cookie is missing or it doesn't match, we can alert the user. It also proves knowledge of the email, since only the email has access to the UUID in unencrypted form. 5. The server unsets the cookie, by responding with a Set-Cookie header that marks it as expired. 6. The server begins a session and logs the user in, either on the page that was opened via the link or the original page that triggered the verification flow (whichever you think is less likely to be an attacker, probably the former). Note that there are some tradeoffs here. The upside is that the user doesn't need to remember or type anything, making it harder to make mistakes or be taken advantage of. The downside is that the friction of having to use the same device for email and login may be a problem in some situations. Also, some email software may open a different browser when the link is clicked, which will cause the cookie to be missing. I handle this by detecting the missing cookie and showing a message suggesting the user may need to copy-paste the link to their already open browser, which will work even if they open a new tab to do it (except for incognito mode, where some browsers use a per-tab cookie jar). Lastly, no cookie is 100% safe from being stolen and cloned. For example, a social engineering attack could involve tricking the user into sharing their link and Set-Cookie header. But we've made it much more difficult. They need two pieces of information, each of which generally can't be intercepted, or used even if intercepted, by intermediary sites. |