In the previous post, we dealt with the persistence of the JWT tokens. We went over how to maintain a secure session with the in-memory approach and the use of http-only tokens for the refresh operation.
But how did we get to a state where the user is able to authenticate in the first place?
How did the user reach a state where he can login? In other words: how do we securely save users on the DB, enabling the user to set his own password, etc?
User handling on a multi-tenant application
Today, most SaaS multi-tenant applications include (or should include) an invite mechanism.
The idea is that each administrator on the SaaS account can collaborate with other users and invite them to the SaaS application under a different role.
Sounds familiar, right? We’re all used to getting emails inviting us to collaborate on an account. After we click on the email we land on a “new password screen”, set our new password, and Boom! Our account is activated.
But what happens behind the scenes on this flow?
The flow (which is orchestrated by the backend) for user invite:
- Create a passwordless user entity including 128-bit uuid as the activation token
- Save the user on the DB (while hashing the activation token)
- Send the activation email including the activation link
- Upon user activation: validate the activation link and the password (strength and length)
- Invalidate the activation link on the DB
- Store the password (hashed) on the DB for the user
Because user flows are very sensitive in terms of security, there are some issues to consider:
- Passwords are ALWAYS saved hashed on the DB. Why? In case of a DB hijack, even if the attacker finds the email on the DB, he cannot trace back the password in order to take over the account.
- Activation links MUST be invalidated after usage. Why? In case the activation email is stolen, the attacker cannot change the password of an existing user after the user has already activated.
- Activation links are hashed on the DB. Why? For the same reason that we hash passwords. In case our DB is hijacked we want to make sure that the attacker has no way to take over a non-activated account.
Hashing? Encrypting? Salting? What does it all mean?
As you’ve probably noticed, we keep mentioning “hashing”. When discussing the major items related to the activation flows for users on the DB, we’re “hashing” the passwords and activation token on the DB.
But what exactly does it mean?
Why are we hashing? And Is it the same thing as encrypting?
What is hashing?
Hashing is a one-way function where data is mapped to a fixed-length value. One of the primary uses of hashing is for authentication use cases.
The idea is simple. Pass a string via a hash function and get an output of hash values.
Seems pretty simple. And the implementation (Node.JS sample below) is very easy as well:
But is it enough?
Let’s assume we have 100 users on the DB. John and Bob both used User123456! as their password.
In that case both of their passwords will be the same on the DB (one-way hash):
John (firstname.lastname@example.org) →
Bob (email@example.com) →
So in case John’s password has leaked and the attacker has access to our DB, that means that the attacker can now login on behalf of Bob as well…
How should we protect ourselves? How about adding some salt?
Salt is a term in cryptography which means “adding random data as an additional input to a one-way function that hashes data, passwords or passphrase”.
Most of the use cases we find for salting one-way functions is for safeguarding passwords when storing them on the DB.
Again, this is rather easy to implement (with performance cost) using Node.JS (this sample uses the great bcrypt library):
Running the hashing algorithm using salt now generates the following:
Security loophole closed! ????
How NOT to salt
There are a few common errors when using salt mechanisms which you should try to avoid:
One of the more common mistakes would be to use the same salt (either by hardcoding it to the source code OR by generating it once and reusing it).
This brings us straight back to the hashing problem, where we might have salted the one-way function but still all users with the same password will have the same value on the DB. In case of a DB hijack and a password breach, our users will all be exposed to an account takeover.
So the trick, of course, is to use a new salt for every password / token save to the database
An additional common mistake is using salt expressions that are too short. While there is a price to pay in terms of performance, using a short salt exposes the application to brute-force attacks using a lookup table for possible salt values.
Wanna get mathematical? Let’s say we use a salt of only three ASCII characters. This brings us to 857,375 possible salts (95 *95 *95).
Sounds a lot right? But if each lookup table contains only 1MB of the most common passwords, collectively they will be only 837GB, which is not a lot considering 1000GB hard drives can be bought for under $100 today.
So, How to salt properly? Follow the three simple guidelines below and you should be fine:
- Generate random salts using a CSPRNG (Cryptographically Secure Pseudo-Random Number Generator)
- Generate a new random unique salt for EACH password hashed
- Generate LONG salts
In this post we discussed how to handle passwords and activation tokens on your multi-tenant SaaS application and how to store them on the database using hashing and salting mechanisms.
At Frontegg, we leverage these methods in order to securely manage these essential SaaS flows using the secured SaaS building blocks we’re developing for our customers. If you have any hashing questions or salt considerations, we’re the guys to ask!