In this project we'll use a package called bcrypt
to authenticate users. Bcrypt is a package based off the popular bcrypt hashing algorithm. It exposes several functions that we can use to salt, hash, and compare our passwords without exposing the users plain-text password.
Fork
andclone
this repository.cd
into the project directory.- Run
npm install
. - Review the provided code.
- Your basic server has been built for you, we'll focus on working with bcrypt and authentication.
- Using your SQL GUI (pgweb, SQLTabs, Postico, etc.) add the following table to your database.
CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(200), user_password TEXT )
In this step, create your .env
file.
Create your .env
file in the root of your directory. It should have three properties: SESSION_SECRET
for your express-session
middleware, SERVER_PORT
for the port your server will run on, and CONNECTION_STRING
which should be your database connection. The proxy is looking for a SERVER_PORT
on 3001
.
.env
CONNECTION_STRING=string_from_heroku_db
SESSION_SECRET=some_cool_secret
SERVER_PORT=3001
With our database connected, we're ready to start handling user authentication. We need to provide an endpoint for our users to signup for our website. We'll then need to store that users email and password to our database. This is where bcrypt comes in. Remember, we have to be careful with our users data; bcrypt will hash their password before we keep it for longterm storage.
- Open
server/index.js
- Beneath your middlewares, write an endpoint with the
post
method, and a path of/auth/signup
. - This endpoint should:
- Expect to receive
email
andpassword
properties onreq.body
. - Check if the user has already signed up using the
check_user_exists
sql statement indb/
.- If the user exists, send an error back.
- If the user hasn't signed up
hash
andsalt
the password using bcryptsgenSaltSync
andhashSync
methods- You can see how to use them here.
- Add the user's email and hashed password to the database using the
create_user
sql statement found indb/
. - Put the user object on session (excluding their hashed password) so we can reference them in other endpoints in our server and send the new user's data back to the client.
- Expect to receive
index.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcryptjs');
const massive = require('massive');
const app = express();
app.use(express.json());
let { SERVER_PORT, CONNECTION_STRING, SECRET } = process.env;
app.use(session({
secret: SECRET,
resave: false,
saveUninitialized: false
}));
massive(CONNECTION_STRING).then(db => {
app.set('db', db);
});
app.post('/auth/signup', async (req, res) => {
let { email, password } = req.body;
let db = req.app.get('db')
let userFound = await db.check_user_exists([email]);
if (userFound[0]) {
return res.status(200).send('Email already exists')
}
let salt = bcrypt.genSaltSync(10);
let hash = bcrypt.hashSync(password, salt);
let createdUser = await db.create_customer([email, hash])
req.session.user = { id: createdUser[0].id, email: createdUser[0].email }
res.status(200).send(req.session.user)
});
app.listen(SERVER_PORT, () => {
console.log(`Listening on port: ${SERVER_PORT}`)
});
In this step we'll create login functionality. Bcrypt is only one of many hashing algorithms. When hashing passwords, it's important that we use the same algorithm that we used for creating hashed passwords when comparing hashed passwords. With hashing, using the same alogrithm with the same input (password here) will create the same hash. This allows us to know that a user entered the correct email and password, despite not saving their password in plain-text.
- Beneath your previous endpoint, write another post request, this time to
/auth/login
- We should do some of the same things we did in the previous endpoint:
req.body
will have email and password properties- We should check that the user is in our database
- If they're not, send an error, they probably entered their email or password wrong.
- If their email is in our database:
- Use bcrypts
compareSync
method to compare the input password onreq.body
with the usersuser_password
.- You can review use for
compareSync
here.
- You can review use for
- If the passwords match, then the user has successfully authenticated, put the user object on session (excluding their hashed password) and send it to the client.
- Use bcrypts
- The user can now
login
to your website!
index.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcryptjs');
const massive = require('massive');
const app = express();
app.use(express.json());
let { SERVER_PORT, CONNECTION_STRING, SECRET } = process.env;
app.use(session({
secret: SECRET,
resave: false,
saveUninitialized: false
}));
massive(CONNECTION_STRING).then(db => {
app.set('db', db);
});
app.post('/auth/signup', async (req, res) => {
let { email, password } = req.body;
let db = req.app.get('db')
let userFound = await db.customer_check([email]);
if (userFound[0]) {
return res.status(200).send('Email already exists')
}
let salt = bcrypt.genSaltSync(10);
let hash = bcrypt.hashSync(password, salt);
let createdUser = await db.create_customer([email, hash])
req.session.user = { id: createdUser[0].id, email: createdUser[0].email }
res.status(200).send(req.session.user)
});
app.post('/auth/login', async (req, res) => {
let { email, password } = req.body;
let db = req.app.get('db')
let userFound = await db.check_user_exists(email)
if (!userFound[0]) {
return res.status(200).send('Incorrect email. Please try again.');
}
let result = bcrypt.compareSync(password, userFound[0].user_password)
if (result) {
req.session.user = { id: userFound[0].id, email: userFound[0].email }
res.status(200).send(req.session.user)
} else {
return res.status(401).send('Incorrect email/password')
}
});
app.listen(SERVER_PORT, () => {
console.log(`Listening on port: ${SERVER_PORT}`)
});
With the essential authentication functionality done, we'll write the matching frontend functions so a user can signup and login to our site.
-
Open
src/App.js
- A few things to note:
- The functions we'll work with already exist, we're going to write the functionality.
- The inputs are already setup to update our
email
andpassword
state items as we type into them. - The buttons already have the function calls mapped to them.
- A few things to note:
-
Find the
signup
function on our App Component -
This function should:
- Make a post request to
/auth/signup
. - The body should be an object with our email and password from state assigned as properties on the object.
- When the response comes back, we should set the returned user on state and reset the username and password fields.
- Make a post request to
-
Take a look at your database to make sure the user was successfully added.
- Notice the format of the
user_password
. It looks nothing like what we typed in. That's a hashed value!
- Notice the format of the
-
Next find the
login
function -
This function will look identical to the
signup
function, except we'll be posting to the/auth/login
endpoint instead of the signup endpoint. -
It should make the post request, set the returned user on state, and reset the input fields.
App.js
async signup() {
let { email, password } = this.state;
let res = await axios.post('/auth/signup', {
email,
password
});
this.setState({ loggedInUser: res.data, email: '', password: '' });
}
async login() {
let { email, password } = this.state;
let res = await axios.post('/auth/login', {
email,
password
});
this.setState({ loggedInUser: res.data, email: '', password: '' });
}
Once a user is logged in, they need a way to logout. In this step, we'll add logout functionality, which is really just the user telling our app that we can close their session for now.
- Open
server/index.js
- Add a
get
endpoint to/auth/logout
- This endpoint should:
- Destroy the user's session.
- Send a status of 200.
index.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcryptjs');
const massive = require('massive');
const app = express();
app.use(express.json());
let { SERVER_PORT, CONNECTION_STRING, SECRET } = process.env;
app.use(session({
secret: SECRET,
resave: false,
saveUninitialized: false
}));
massive(CONNECTION_STRING).then(db => {
app.set('db', db);
});
app.post('/auth/signup', async (req, res) => {
let { email, password } = req.body;
let db = req.app.get('db')
let userFound = await db.customer_check([email]);
if (userFound[0]) {
return res.status(200).send('Email already exists')
}
let salt = bcrypt.genSaltSync(10);
let hash = bcrypt.hashSync(password, salt);
let createdUser = await db.create_customer([email, hash])
req.session.user = { id: createdUser[0].id, email: createdUser[0].email }
res.status(200).send(req.session.user)
});
app.post('/auth/login', async (req, res) => {
let { email, password } = req.body;
let db = req.app.get('db')
let userFound = await db.customer_check([email])
if (!userFound[0]) {
return res.status(200).send('Incorrect email. Please try again.');
}
let result = bcrypt.compareSync(password, userFound[0].hash_value)
if (result) {
req.session.user = { id: userFound[0].id, email: userFound[0].email }
res.status(200).send(req.session.user)
} else {
return res.status(401).send('Incorrect email/password')
}
});
app.get('/auth/logout', (req, res) => {
req.session.destroy();
res.sendStatus(200);
});
app.listen(SERVER_PORT, () => {
console.log(`Listening on port: ${SERVER_PORT}`)
});
In this step we'll write our logout functionality.
- Open
App.js
- Find the
logout
function - This function should:
- make a
get
request to/auth/logout
. - Set the
loggedInUser
on state back to an empty object.
- make a
- You should now be able to sign a user up, login, and logout!
App.js
logout() {
axios.get('/auth/logout');
this.setState({ loggedInUser: {} });
}
One final piece of server code is needed to complete our authentication process. We need a way to check that our user is logged in and pull their information into our application if they are.
- Open
index.js
- Add a
get
enpoint to/auth/user
- This endpoint should:
- Check if their is a user on session.
- If there is, send it up.
- If there isn't send an error.
index.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcryptjs');
const massive = require('massive');
const app = express();
app.use(express.json());
let { SERVER_PORT, CONNECTION_STRING, SECRET } = process.env;
app.use(session({
secret: SECRET,
resave: false,
saveUninitialized: false
}));
massive(CONNECTION_STRING).then(db => {
app.set('db', db);
});
app.post('/auth/signup', async (req, res) => {
let { email, password } = req.body;
let db = req.app.get('db')
let userFound = await db.customer_check([email]);
if (userFound[0]) {
return res.status(200).send('Email already exists')
}
let salt = bcrypt.genSaltSync(10);
let hash = bcrypt.hashSync(password, salt);
let createdUser = await db.create_customer([email, hash])
req.session.user = { id: createdUser[0].id, email: createdUser[0].email }
res.status(200).send(req.session.user)
});
app.post('/auth/login', async (req, res) => {
let { email, password } = req.body;
let db = req.app.get('db')
let userFound = await db.customer_check([email])
if (!userFound[0]) {
return res.status(200).send('Incorrect email. Please try again.');
}
let result = bcrypt.compareSync(password, userFound[0].hash_value)
if (result) {
req.session.user = { id: userFound[0].id, email: userFound[0].email }
res.status(200).send(req.session.user)
} else {
return res.status(401).send('Incorrect email/password')
}
});
app.get('/auth/logout', (req, res) => {
req.session.destroy();
res.sendStatus(200);
});
app.get('/auth/user', (req, res) => {
if (req.session.user) {
res.status(200).send(req.session.user)
} else {
res.status(401).send('please log in')
}
});
app.listen(SERVER_PORT, () => {
console.log(`Listening on port: ${SERVER_PORT}`)
});
You are now able to use form based, local authentication via bcrypt to have users signup, login, logout, and access their data. Authentication will allow your projects to be more flexible, dynamic, and engaging, and using bcrypt will help make sure that your users data is safe.
If you see a problem or a typo, please fork, make the necessary changes, and create a pull request so we can review your changes and merge them into the master repo and branch.
© DevMountain LLC, 2019. Unauthorized use and/or duplication of this material without express and written permission from DevMountain, LLC is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to DevMountain with appropriate and specific direction to the original content.