A02:2021 - Cryptographic Failures
For the information about Cryptographic Failures, visit page (opens in a new tab)
JWT Authentication
CWE-256: Plaintext Storage of a Password
It has implications in other categories, such as the previous "A1 Broken Access Control" if the attacker can use the obtained credentials to gain unauthorized access to the system.
Our application is using JWT authentication. This means that the user is authenticated by a token that is sent with
every request. The token is signed with a secret key that is only known to the server and is stored in the local
storage.
Even though JWT is a popular mechanism (but still not the most secure one) for authentication and session
management in web applications,
some vulnerabilities can be exploited if not configured properly.
BONUS: If you want to learn more about JWT, visit its official website (opens in a new tab).
Password stored as plain text
Storing a password as plain text was never a good idea and in a world where we can hash the password in one line of code, there is no reason to do it.
Register
- Navigate to
jwtAuth.ts
file. - In the method handling the post request to register route, uncomment the lines after 3rd step.
const salt = await bcrypt.genSalt(10);
const bcryptPassword = await bcrypt.hash(password, salt);
- Replace the parameter
password
withbcryptPassword
in the query.
Those adjustments will hash the password before storing it in the database when the user is registered.
Login
- In the method handling the post request to login route, uncomment the
line:
const validPassword = await bcrypt.compare(password, user.rows[0].password);
- Remove the line
const validPassword = password === user.rows[0].password;
- By this change, the password will be hashed before comparing it with the password stored in the database.
JWT token
- In the source code, navigate to
jwtGenerator
method definition injwtGenerator.ts
file.
The method generates a token using the payload object and a secret key defined in a .env file. - Make sure that field in the payload object is unique to the user and cannot be easily guessed by an attacker.
In our case, we used the user`s password hash, which is a good practice.
Expiration
Currently, the token is valid for 1 hour. When logged in to the application in some shared device (coffee shop, library, etc.) and the user forgets to log out, the token will be valid for the remaining time.
Set the expiration time to a shorter period (e.g. 5-15 minutes) to reduce the risk of unauthorized access.
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "15m" });
After the expiration time, the user will be logged out and will have to log in again.
There are some more complex and sophisticated ways to handle this issue, but for this tutorial, we will keep only the expiration time.
Secret key
The secret key is stored in the .env file. The key is used to sign the token and is only known to the server. it should be kept secret to prevent unauthorized parties from creating or modifying tokens. If the key is weak or easily guessable, an attacker can use it to sign their own JWT tokens, impersonate legitimate users, or modify the payload of existing tokens.
In .env
file (server/.env), replace the JWT_SECRET
value with strong password (use password genetator (opens in a new tab))
There are two more things to consider:
- The secret key should be stored in a secure location. In our case, it is stored in the .env file using the dotenv library. Also, as the app is not deployed and just running locally, we do not have to explicitly check if the .env file can be accessed publicly.
- Last but not least, the secret key should be changed periodically by implementing secret rotation.