DEV Community

Cover image for Solid Start auth – the secure way (with BCrypt & PSQL)
anes
anes

Posted on • Updated on

Solid Start auth – the secure way (with BCrypt & PSQL)

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
Enter fullscreen mode Exit fullscreen mode

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;
},
Enter fullscreen mode Exit fullscreen mode

To check what data we exactly have when a user registers, we can console.log(user):
Screenshot of console displaying that the server returns us a username and a password
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);
  //...
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
      //...
    },
    //...
  },
};
Enter fullscreen mode Exit fullscreen mode

And this should yield us a very long and random looking string when we register a new user:
Screenshot of a console log showing a digested password
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)
Enter fullscreen mode Exit fullscreen mode

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" }];
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 ".";
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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):
Screenshot of logs displaying a user with a salt and an encrypted password
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"
}]
Enter fullscreen mode Exit fullscreen mode

Now if we restart the server and try to log into our base user we should quickly see:
Screenshot of a logged in user
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;
Enter fullscreen mode Exit fullscreen mode

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;
    },
Enter fullscreen mode Exit fullscreen mode

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),
    }
    //... 
  },
  //...
};
Enter fullscreen mode Exit fullscreen mode

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:
Image of our newly created user with 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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Fixing vite issues

If you check your console you should see a lot of red:
Vite error messages caused by the BCrypt library
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']
  }
});
Enter fullscreen mode Exit fullscreen mode

Now your console should be error-free and the code should run:
Shows user @aneshodza being logged in

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>"
});
Enter fullscreen mode Exit fullscreen mode

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'],
  }
});
Enter fullscreen mode Exit fullscreen mode

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 *;`;
},
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
},
Enter fullscreen mode Exit fullscreen mode

This will return a user. Now if we try to log into the application:
Image of logged in user
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 :)

Top comments (0)