Email based OTP Login System using Node.js
Table of Contents
Goal
To produce a login system to authenticate users that is simpler and more secure than traditional username/password login systems. We will make an implementation using Node.js and Express.
Terminology
-
UPL - traditional Username/Password Login system
-
EOTP - Email based One-Time-Password login system
What is Authentication?
Authentication is a way to prove who you are. This can be done in several ways:
- By revealing what you know (Knowledge-based):
- Username/password
- PIN
- Security questions
- By revealing what you hold (Possession-based):
- Physical items
- Security token
- Mobile phone (identified using unique attributes like IMEI number)
- Passport or Driver’s License (or other physical ID cards)
- Digital items
- Authenticator app via a Time-Based One-Time-Password (TOTP)
- One-Time-Password delivered via
- SIM card
- via an SMS
- via a Voice Call
- Physical or Digital items
- Digital Certificate
- QR Code
- Physical items
- By revealing what you are (Inherence-based):
- Fingerprints or Hand Geometry
- Voiceprint
- Face recognition
- Retina or Iris scan
- Behaviour (like typing rhythm)
- By revealing where you are (Location-based):
- GPRS location
- Wi-Fi router location
- IP address geolocation
Authentication can be achieved using one or more of the mechanisms. Depending on the application, the authentication system may choose to use more than one authentication category. This is called two factor authentication (2FA). It is less convenient for the user but adds more security.
We are focusing only on email OTP authentication. It is very likely that the user will have access to their email and the application they are being authenticated for, since they both only require internet access. OTP via SMS, for instance, introduces another point of failure as the user may not have SMS access, but has internet access.
EOTP vs UPL
EOTP advantages over UPL
Simplicity
The simplicity of EOTP implementation leads to a reduced attack surface area. There is no need for password resets or forgotten password mechanisms.
Human Behaviour
One of the main challenges with UPL systems is human behaviour. Users often:
- Choose weak or easily guessable passwords (susceptible to dictionary attacks).
- Use the same password across multiple platforms.
- Are tempted to store passwords on paper or digitally.
Lifespan
OTPs are not permanent. Even if an attacker discovers the password they will only have a short window in which to take advantage of their discovery. Username/password generally have a long lifespan. If a short timespan is enforced upon them users have greater temptation to store their passwords, opening additional attack vectors.
UPL advantages over EOTP
- Familiarity
- No email dependency: users can still login even if they do not have an email address, or loose email access.
Background
I was using the iOS app that comes with my Roidme Eve robot vacuum cleaner and was impressed by its login system. I’m so used to using the conventional UPL system that I didn’t really think of any other way of implementing it. The Roidme app doesn’t store the password but rather sends a verification code to your email which you enter at the app’s login screen in order to enter the app. I found it quick, convenient, and simple. Since I had recently gone through the process of designing a UPL system I immediately recognised the potential to greatly simplify the user interface complexity, and likewise, the code complexity.
There has also been a spate of password thefts and in light of this and the 650+ hacked websites listed on ’;–have i been pwned? I realised that a user authentication system that does not store passwords (or even hashes of the passwords) would be hugely beneficial to internet users around the world.
UX
UPL Flow
The traditional UPL flow looks something like this. It requires 8 UI screens to be designed:
EOTP Flow
Our EOTP approach gives a much simpler flow with only 3 UI screens to be designed:
Sequence Diagrams
EOTP
sequenceDiagram
actor User
participant Browser
participant Email
participant Server
participant Database
autonumber
User->>Browser: Enter login page url
Browser->>Server: Request login page
Server->>Browser: Return login page
User->>Browser: Enter email address
User->>Browser: Request verification code
Browser->>Server: Request verification code (email)
Note over Server: Generate salt
Server->>Browser: Return salt
Note over Server: Generate verification code
Server->>Email: Email verification code
Note over Server: hash = SHA256(pepper + Argon2(salt + verification code))
Server->>Database: Save (hash, salt)
Email->>User: Retrieve verification code
User->>Browser: Enter verification code
Note over Browser: hash1 = Argon2(salt + verification code)
Browser->>Server: Submit (hash1)
%% Note right of Browser: Hashed to prevent server-side<br />credential harvesting
Note over Server: hash2 = SHA256(pepper + hash1)
Server->>Database: Exists (hash2) ?
Database->>Server: Result
alt Exists
Server->>Browser: Return dashboard page
else Doesn't Exist
Server->>Browser: Return error message
end
Database Schema
UPL Fields
Field Name | Data Type |
---|---|
password_hash |
TEXT |
password_salt |
TEXT |
-
password_hash
- UPL systems can store plaintext passwords, but this is a major security weakness as if the database is compromised - by internal or external actors - all the users’ accounts will be accessible by the attacker.In order to mitigate this risk, passwords should be hashed with a strong one-way hash function (e.g. SHA256 or SHA512) and stored as hashes in the database. This way the plain text is not viewable, nor decodable, as the hash function is one-way only.
-
password_salt
- One-way hashes do still have the vulnerability of rainbow table attacks on the hashes. To mitigate these, each password needs to be stored with its own random salt - hence thepassword_salt
field.
EOTP Fields
Field Name | Data Type |
---|---|
verification_code_hash |
TEXT |
verification_code_salt |
TEXT |
-
verificationCode
- A 12-character base64 string. This gives 72 bits of entropy which is considered strong and sufficient for securing financial information.The
verificationCode
cannot be saltedNote: The
verificationCode
should be removed from the database upon successful login to minimise risks of phishing attacks, replay attacks, email breaches and database breaches.
Common Fields
Field Name | Data Type |
---|---|
email |
TEXT |
token |
TEXT |
-
email
- The user’s email address is not strictly necessary in a UPL system, but without it there would be no mechanism for password recovery. The user’s email address is always required for EOTP. -
token
- Both database schemas will use a random token that is stored in the database and also in a browser cookie upon successful login in order to identify the logged-in user between requests and can also persist between browser sessions.The
token
/cookie will be a 12-character base64 string. This gives 72 bits of entropy which is considered strong and sufficient for securing financial information.
Hashing vs Encryption
Encryption is a two-way process. You can encrypt passwords with a private key, and decrypt them with the same private key. If the private key is discovered by an outsider then they can decrypt your passwords also.
Hashing is a one-way process. A well designed hashing algorithm cannot be reversed. If you hash passwords the original password can never be recovered again. The exception being for weak passwords which are vulnerable to brute force (attacks short passwords), dictionary (attacks common passwords) and rainbow table attacks (attacks short and/or common passwords).
Client-Side Hashing
EOTP
For extra security we can hash the incoming verification code from the client before it is sent. This will prevent server-side credential harvesting.
Password Character Sets
To create a strong and secure password we recommend using the full list of 95 printable ASCII characters that are easily typed on a standard keyboard:
Character Type | Count | Characters |
---|---|---|
Uppercase letters | 26 | ABCDEFGHIJKLMNOPQRSTUVWXYZ |
Lowercase letters | 26 | abcdefghijklmnopqrstuvwxyz |
Digits | 10 | 0123456789 |
Special characters | 32 | !"#$%&"()*+,-./:;<=>?@[\]^_`{|}~ |
Space | 1 | |
Total | 95 |
Including a mix of these character types when creating a password helps to improve its strength and security. The more diverse the character set used, the harder it is for an attacker to guess or crack it using brute force or dictionary attacks in the case where the password hashes, their salts and the pepper have been discovered.
In the case where user table or the pepper remain secure the passwords are guaranteed safe regardless of password strength.
Password Strength
Ideally we would like users to have strong passwords (e.g. 10+ random characters). Enforcing strong passwords can introduce usability issues as it’s difficult to remember long random passwords. It may introduce new security issues. Users may potentially store them electronically or write them down which adds further attack vectors. They will also be more likely to forget them, introducing more customer support requests.
Using salts and a pepper any length password is safe while the pepper remains undiscovered. If the salts and pepper are found out the password hashes will be vulnerable to dictionary and brute force attacks.
Even with Argon2 slow hashing weak passwords (short, common or dictionary based) could be discovered.
We recommend 8+ ASCII characters to thwart brute force attacks.
We recommend a filter to filter out dictionary words or combinations of them to thwart dictionary attacks. The ideal filter would cover all the potential words and combinations an attacker would use. If this is the case even in the case of a database breach and discovered pepper the user passwords would still be safe.
Session Cookie
A secure token
gets created on the server when a user correctly enters their verification code. The cookie is sent with the attributes HttpOnly
, Secure
and SameSite
. It gets stored in the browser’s cookie cache.
-
HttpOnly
- This attribute prevents JavaScript from accessing the cookie, which can help protect against cross-site scripting (XSS) attacks. By marking a cookie as HttpOnly, it can only be accessed by the server only and not client-side scripts. -
Secure
- The Secure attribute ensures that the cookie is only sent over HTTPS connections. This prevents the cookie from being transmitted over insecure HTTP connections, protecting it from eavesdropping and man-in-the-middle attacks. -
SameSite
- With the “Strict” setting, the cookie is only sent for requests from the browser that come pages under our domain name. This prevents Cross-Site Request Forgery (CSRF).
The cookie is sent to the server with every browser request. The server will cross reference the cookie’s value with the token
stored in the database to make sure that the user is still logged in.
EOTP vs UPL
Here is a comparison of EOTP vs UPL. The issues are listed in rough order of importance. Green cells indicate a positive outcomes and red cells a negative outcome. Technically we can see a many more advantages in using the EOTP vs the UPL system.
Issue | EOTP | UPL |
---|---|---|
Credential theft | Lower risk, temporary verification codes. A new random verification code is created each time the user requests one and it is removed from the database once they have successfully logged in. Codes should be removed after a few minutes for added security. If all credentials are stolen all users can be safely logged out in order to invalidate the stolen credentials. |
Higher risk, static passwords can be stolen and remain valid until the user changes their password. |
Backend security complexity | Simplified security requirements | Requires secure storage and handling of passwords |
Phishing attacks | Lower susceptibility | Higher susceptibility |
Brute force and dictionary attacks | Reduced attack surface | Vulnerable to such attacks |
Compliance with data protection regulations | Potentially simpler compliance | Requires secure storage and handling of passwords |
System admin | Reduced maintenance overhead for system administrators | Requires regular monitoring and updating of password policies |
Password-related support | Reduced volume of password-related support requests | Higher volume of password-related support requests |
Password management | No need to remember or manage passwords | Requires remembering and managing passwords |
Adaptability to different user groups | More accessible and user-friendly for users with cognitive or memory impairments | May be less accessible for some user groups |
Security for shared devices | Reduced risk of password theft | Higher risk of password theft on shared or public devices |
Risk of password reuse | Eliminates concern of password reuse | Higher risk of account compromise due to password reuse |
One-time-use verification codes | Each verification code is generated for a single login attempt and expires after a short period of time or upon successful login | N/A |
Login speed | Potentially slower, requires email verification code | Faster for users with memorized credentials |
Offline access | Requires email access to retrieve verification code | Login possible with stored credentials |
Email dependency | Dependent on email service availability | Independent of email services |
User trust | May need time to gain user trust | Well-established and trusted by users |
Familiarity | Less familiar, may require user education and adaptation | Widely used and familiar, easier adoption |
Email security awareness | Encourages users to secure their email accounts | No direct impact on email security awareness |
User registration | Faster and more seamless registration process | Potentially slower registration and authentication process |
Password change process | No need for a password change process | Requires password change process |
Password logging
Both UPL and EOTP are vulnerable to server-side password logging attacks. This is where the server logs (or otherwise displays) the incoming password from the client (or verification code in the case of EOTP).
To prevent this in UPL the user’s password salt can be sent to the client with which the password can be hashed client-side before sending to the server. The password hashes are then compared
Implementation Checklist
-
User email validation: Ensure that the user provides a valid email address during the login process.
-
Email delivery: Send the generated verification code to the user’s email address securely and promptly.
Verification code
-
Unique verification code generation: Generate unique codes for each login attempt.
-
Code expiration: Implement a time limit for verification code validity (e.g., 10-15 minutes).
-
Verification code input: Provide a user interface for the user to input the received verification code.
-
Code validation: Verify the inputted code against the stored code for the email address, considering the expiration time.
-
Authentication: Grant access to the user upon successful code validation.
-
User session management: Create and manage user sessions after successful authentication, including session expiration and handling of concurrent sessions.
-
User data protection: Ensure the secure storage and handling of user data (e.g., email addresses).
-
Logging and monitoring: Implement logging and monitoring for login attempts, successful logins, and other relevant events.
-
Error handling: Handle errors gracefully, providing informative error messages to users when appropriate.
-
Responsive: Test on differing screen sizes on mobile and desktop devices.
Attack Vectors
Online
- Try username / password
- Mitigation
- rate limit
- Mitigation
Offline
- Crack username / password from database
- Phishing
- Shoulder surfing
EOTP
Server-Side Credential Harvesting | Brute Force | Rainbow Tables | Pass-the-Hash | Same Hash | Replay Attack | Sign Up DOS | |
---|---|---|---|---|---|---|---|
Client side hashing | Mitigated | ||||||
Cpu + memory intensive hash (e.g. Argon2 with large hash 256/512 bits) | |||||||
Salt | |||||||
Server side hashing | |||||||
Rate limiting | |||||||
HTTPS / WSS | |||||||
Email Verification |
UI
Next Steps
Reducing the Verification Code Size
At the moment we use 72 bits of entropy for the verification code. This prevents brute force attacks. The disadvantage is that it can be inconvenient for the user trying to login as they need to either copy-paste the code or type all 12 base64 characters. Other minor problems that would need to be worked around are that characters such as 0
/ O
or 1
/ I
can potentially look similar to each other and cause frustration if not identified correctly by the user (we could provide a clickable link that contains the verification code in the email but this introduces an additional phishing attack vector). Additionally there is a small chance that random offensive words could be accidentally created with the base64 character set.
A more convenient way would be using a 6-digit code that would not have any of the aforementioned problems. Using 6 digits would mean that the system would be vulnerable to brute-force attacks as it would be easy to automate 999999 login attempts in a short space of time. In order to prevent this, we can create a rate limiter that would allow only a small number of incorrect login attempts over time per user. Using the proposed 6-digit code will improve the user experience at the cost of some code additional complexity. In this case we consider it worth implementing considering the trade offs.
Web Authentication API
A new option is using the Web Authentication API. It is supported by all major browsers now and has the potential improve the usability and security of our login system further.
References:
- Web Authentication: An API for accessing Public Key Credentials Level 2, W3C Recommendation, 2021, https://www.w3.org/TR/webauthn-2
- Web Authentication API, MDN Web Docs, 2023, https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API
- WebAuthn, https://webauthn.guide