Solid Start auth – the secure way (with BCrypt & PSQL)
anes
Posted on November 28, 2022
What is this about?
This article is about the new meta framework Solid Start and authentication, primarily making it a secure application. I already wrote an article about how to store data in a PostgreSQL database with Solid Start. This tutorial builds on top of that knowledge, so if you haven't read that one yet, I advise you to do so.
Getting started
Note: If the templates have changed in any way, which they probably will, I either advise you to read trough a newer tutorial or start with this code. If the templates stayed the same, continue with the setup.
First, we need to create a folder in which the setup will happen. We can do that with mkdir secure-auth-with-solid-start
and then directly go into that folder with cd secure-auth-with-solid-start
. To initialize our project we type npm init solid
, which brings us into the setup wizard. To have a foundation we pick the "with auth" template:
✔ Which template do you want to use? › with-auth
✔ Server Side Rendering? … yes
✔ Use TypeScript? … yes
Next we have to install our packages by typing yarn
. You can also use npm install
, but I got used to yarn.
Writing the application
In this part we will actually make our application and secure it properly. I already have an article about how passwords are secured properly: "how (not) to store passwords"
Let's hash! (Next chapter uses BCrypt)
Important: This chapter does it by hand so the reader gets a better understanding of what is happening. If you are here for the BCrypt part, please skip this chapter. You can obviously also read trough both.
To start with our hashing we need to find the functions responsible for logging in and registering. We can find that in the file, that also exports our database (src/db/index.ts
).
First, we will go over the create function:
async create({ data }) {
let user = { ...data, id: users.length };
users.push(user);
return user;
},
To check what data we exactly have when a user registers, we can console.log(user)
:
Note, that you need to restart the server to see these changes.
What else do we need? Well, we also need a salt. This is one possible way to do it:
async create({ data }) {
let salt = Math.random().toString(32).slice(2);
//...
}
Next we need to digest. Unlike in ruby, javascript does not have a native Sha256 function, which is why we will make use of crypto-js. I added it with yarn add crypto-js
.
Taking a quick look into the documentation of crypto-js shows how easy it actually is to do:
import sha256 from 'crypto-js/sha256';
const hashDigest = sha256(nonce + message);
We will implement exactly this code into our /db/index.ts
file:
import sha256 from "crypto-js/sha256";
export const db = {
user: {
async create({ data }) {
//...
const digestedPassword = sha256(data.password + salt)
.toString();
console.log('our digested password:', encryptedPassword);
//...
},
//...
},
};
And this should yield us a very long and random looking string when we register a new user:
Finally we need modify the user we will save and print it below:
let user = {
username: data.username,
id: users.length,
salt: salt,
digested_password: digestedPassword
}
console.log(user)
Now on the next line (users.push(user)
) we should get a typescript error. That is because our initial user has different attributes, so don't get scared. We will fix that later.
One last thing that we are still lacking is a pepper, so we will create that one. I just made a export const
above the let users
initialization of the array:
export const pepper: string = "make_sure_the_pepper_is_long"
+ "_and_secure_so_that_it_is"
+ "_hard_to_guess";
let users = [{ id: 0, username: "kody", password: "twixrox" }];
We export it, because we will need this pepper in another file and we don't want to hard code.
Finally also add to the sha256
algorithm:
const digestedPassword = sha256(data.password + salt + pepper)
.toString();
Next, we will change the login function to match the system we have with the register. The login is located in /src/db/session.ts
, which is why we exported the pepper. Now if we go into session.ts
we see on line three:
import { db } from ".";
Which we change to also import the pepper
and don't forget to import the Sha265 algorithm on the next line:
import { db, pepper } from ".";
import sha256 from "crypto-js/sha256";
Finally we can rewrite our login function. What it does is digest the user input and then match that up with the password in our database, so that we never have to get the plain text password back (on line 15 of session.ts
):
export async function login({ username, password }: LoginForm) {
const user = await db.user.findUnique({ where: { username } });
if (!user) return null;
const digestedInput = sha256(password
+ user.salt
+ pepper)
.toString();
const isCorrectPassword = digestedInput === user.digested_password;
if (!isCorrectPassword) return null;
return user;
}
Here you will see another TypeScript error, but we will clear those later. Now we can create a new user with the password "123456" and check the logs for his attributes (make sure to restart the server):
We can copy those attributes to create a new base user, which works with our digestion. For that we change the users
array in /src/db/index.ts/
:
let users = [{
id: 0,
username: "@aneshodza",
salt: "qm9616pd3eg",
digested_password: "a5c594cb0938b5d118f0c4d0e4fbf4a64838c2390da1334a85cef73955008fd1"
}]
Now if we restart the server and try to log into our base user we should quickly see:
It works!
Next, we want to do this with BCrypt, but if you want to have the source code to this you can get it here
Now with BCrypt
We want to bring our website to industry standards, which is why we will use BCrypt, the probably most used library for encryption and digestion.
Step one is to go trough the "setting up" chapter again, because we will do this in a separate application.
Now we want to get to using BCrypt. If you didn't read the first part: The register function is in /src/db/index.ts
and the login is in src/db/session.ts
.
Let us start off by installing the package. I use yarn so I install it with yarn add bcrypt
.
We will work in parallel with the documentation, so I advise you to keep this open on another tab.
We start off in the /src/db/index.ts
file, where the register is located. Step one is to require
the BCrypt object and set a const
for the salt rounds:
import bcrypt from 'bcrypt';
const saltRounds = 10;
Salt rounds are the "cost factor" of the digestion. That tells us how often the string gets digested, so the bigger this number, the harder it is to brute force but also the even create the hashes. Having 10 rounds should definitely be sufficient.
Next, we want to create the user with a digested password. BCrypt already offers us a method for that, which we will use at the top of our create function:
create({ data }) {
const user = {
id: users.length,
username: data.username,
digested_password: bcrypt.hashSync(data.password, saltRounds),
};
console.log('user', user);
users.push(user);
return user;
},
You should see a typescript error. To fix that just delete the first user that the application gave us.
Now we are still missing a pepper. With BCrypt it is not really necessary, but to keep good practice we will still use it. For that we just create a const
and append it to our initial string:
export const pepper:string = "this_is_some_really_secure_pepper";
export const db = {
//...
async create({ data }) {
const user = {
//...
digested_password: bcrypt.hashSync(data.password + pepper, saltRounds),
}
//...
},
//...
};
We export the pepper because we will need it in another file later.
Now if you log the created user you will see a digested password:
Having secured it with an additional pepper, we can finally do the login. For that we go into our login
function in /src/db/session.ts
. First we import bcrypt at the top just like we did in the other file, so we can just use a preset BCrypt function to do it for us:
export async function login({ username, password }: LoginForm) {
const user = await db.user.findUnique({ where: { username } });
if (!user) return null;
let result = bcrypt.compareSync(password + pepper, user.digested_password);
if (!result) return null;
return user;
}
Now you should still see an error, because we didn't import pepper yet. Where you imported db
, you just add pepper
next to it and it should work.
Where is our salt?
BCrypt has its own system for salting, where they have the salt including the salt rounds encoded in one string. Next to that they also encode a lot of other information in it, so that the complex library works off of a single string:
$2y$10$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
| | | |
| | | hash-value = K0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
| | |
| | salt = nOUIs5kJ7naTuTFkBy1veu
| |
| cost-factor = 10 = 2^10 iterations
|
hash-algorithm = 2y = BCrypt
Fixing vite issues
If you check your console you should see a lot of red:
We can fix that by going into our /vite.config.js
and telling it to not optimize this library:
import solid from "solid-start/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [solid()],
optimizeDeps: {
exclude: ['bcrypt']
}
});
Now your console should be error-free and the code should run:
Creating the database connection
This is the part in which you should have read my first article. There I explain how this works in depth.
Setting up
We will use the PostgreSQL npm package. Because I use yarn I will install it with yarn add postgres
. If you use any other package manager, use the command that is needed there.
Creating the schema
Now we need to create our database and the table. We will first connect to the postgres-cli with psql -U <username> -d postgres
, which will throw us into the postgres database. There we create a database and a 'application_user' table with the before used attributes:
postgres=# CREATE DATABASE solid_start_auth_made_secure;
CREATE DATABASE
postgres=# \c solid_start_auth_made_secure
You are now connected to database "solid_start_auth_made_secure".
solid_start_auth_made_secure=# CREATE TABLE application_users (
id serial primary key,
username varchar(255),
digested_password varchar(255)
);
CREATE TABLE
Now if we print everything on the table we should see following:
solid_start_auth_made_secure=# SELECT * FROM application_users;
id | username | digested_password
----+----------+-------------------
(0 rows)
Connecting a client to our database
Next, we have to connect our database to the backend. We will use the same library we did in "Solid Start with PostgreSQL", so we have to install it again with yarn add postgres
Then we go into src/db/index.ts
, where we create an object which contains our database connection at the top of the file:
import postgres from "postgres";
const sql = postgres({
host: "localhost",
port: 5432,
database: "solid_start_auth_made_secure",
username: "<USERNAME>"
});
Now you should see some Big integer literals
error in your terminal. That error is caused by vite. We fix it by telling the vite.config.ts
file to not optimize this:
import solid from "solid-start/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [solid()],
optimizeDeps: {
exclude: ['bcrypt', 'postgres'],
}
});
Restart the server and voilà: The error is gone.
Querying the database
First, we store the users in our database. For that we go into /src/db/index.ts
where we use the npm library to change it to the user being stored in the database:
async create({ data }) {
return await sql`INSERT INTO application_users (username, digested_password)
VALUES (${data.username}, ${bcrypt.hashSync(data.password + pepper, saltRounds).toString()})
RETURNING *;`;
},
Now if we register a user and query our database:
solid_start_auth_made_secure=# SELECT * FROM application_users;
id | username | digested_password
----+------------+--------------------------------------------------------------
4 | @aneshodza | $2b$10$.IyUhM832d24cD.uWuQrUubuQLkGQWw76Ot5C/r0XGniS666L0hvO
(1 row)
Now we also need to rewrite our findUnique
function inside of the same file so it searches the db for users:
async findUnique({ where: { username = undefined, id = undefined } }) {
if (id !== undefined && id.toString() !== 'NaN') {
// return users.find((user) => user.id === id);
const result = await sql`SELECT * FROM application_users WHERE id = ${id};`;
return result.at(0);
} else if (username !== undefined) {
// return users.find((user) => user.username === username);
const result = await sql`SELECT * FROM application_users WHERE username = ${username} LIMIT 1;`;
return result.at(0);
}
return null;
},
This will return a user. Now if we try to log into the application:
It works!
If you want the source code to this approach, here you go
Conclusion
I will say the same as I did in my first tutorial. While solid start had a solid start ;), it is still in very early development and there even the team of solid start agrees: Don't use it in production software yet. I think with more native support of e.g. databases we could do a lot more than now. Happy hacking :)
Posted on November 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.