By the end of this lesson, you should be able to authorize your routes with JWTs and securely store passwords.
- Implement a basic authentication setup using JSON web tokens
- Describe the authentication process
- Store passwords with bcrypt
- Create signin and login routes that return JWTs
- Describe the difference between authentication and authorizations
- Authorize certain routes and information
-
Fork & clone
-
cp nodemon.sample.json nodemon.json
-
Create a new Cluster on MongoDB Atlas titled something like
general-purpose
or reuse one you already have. -
Update the
MONGO_DB_CONNECTION
in yournodemon.json
file with the full connection string. Make sure you include the password you set up for the database and change the name of the database fromtest
to something lkeexclusive_party_dev
. -
npm install
-
npm run reset-db
-
npm run dev
Once installation is working, take a look at the existing code to make sure you understand what is happening. Then, try making requests to the API.
- Take a look at the
db/seeds.js
file.
-
Question: Describe what this code is doing and what its purpose is.
-
Your Answer: This is giving us some dummy data to start working with versus having to manually create collections of documents every time we start a new app.
- Imagine that as a user, you enter your username and password into a site in order to signup.
-
Question: What happens next?
-
Your Answer:
- Checks to make sure that you entered a valid username and password.
- Checks to make sure that the username is available to sign up with.
- Saves an encrypted record of the account information in the DB.
- Sets a cookie for returning users.
- Imagine that as a user, you are now logging back into that same website.
-
Question: How does the website verify that you are indeed the same user?
-
Your Answer:
- Reads to make sure the username exists and is correct
- Reads the password to make sure that is correct
- Imagine that as a logged-in user, you try to go to a route you are not supposed to (e.g. /admin).
-
Question: How does the website know you are or are not allowed on a specific route?
-
Your Answer: Checks to make sure you have the correct permissions set on your account to access.
-
Question: Describe the difference between authentication and authorization.
-
Your Answer:
- Authentication is checking to make sure you are who you say you are.
- Authorization permits a user to do what they want to do.
- Build a new model called
Guest
. TheGuest
model should have the following fields:username
,password
- Create a new route at
POST /api/signup
at/api/routes/auth.js
. This route should:- Create a new
Guest
, pullingusername
andpassword
from the request body - Return a success message with the user's information (for now)
- Create a new
-
Question: This code is currently very insecure. Why?
-
Your Answer: It's not encypted and the account information is just available.
-
Question: What would happen if three different users tried to sign up with the same username? How can we prevent that?
-
Your Answer: They would be able to all sign up. We need to add some sort of check to see if a user document/record already exists.
-
Question: Why are we making our route
POST /api/signup
as opposed toPOST /api/users
? -
Your Answer: We could potentially use
POST /api/users
to query information about users. In designing a well organized API, usingPOST /api/signup
route indicates that is acutally used for a very specific thing, in this case, sign up.
-
We need a way to securely store a password in our database. Install node.bcrypt.js, require it in your new routes file, and use the
bcrypt.hash()
method to encrypt the password before storing it. Test your signup process to make sure the password is hashed.NOTE 1: It is not uncommon for issues to arise while trying to install bcrypt. If you encounter issues during the lesson trying to get it to install, check the Install via NPM section of the documentation to see if you can find any help. If not, take notes until we move pass bcrypt and we can get it resolved during a break or after class.
NOTE 2: Use the promisified
bcrypt.hash()
method instead of the callback version.
-
Question: Describe what is meant by the term
saltRounds
. If you need help, the documentation has some explanation. This StackOverflow answer also might help.NOTE: We will not go into this too deeply for the sake of brevity, however this is a really interesting topic! I would encourage you to look into this more on your own, if you're interested.
-
Your Answer:
saltRounds
incrementally encrypts your data via timestamps.
-
Right now, users can create new accounts with the same username. Update your code so that before we create a guest, we check to see whether or not a guest already exists with that username. If it does, return an error.
NOTE: While it is possible to use the
unique: true
constraint on our model, it requires a bit of extra configuration to get working properly. See the Advanced section below for more information on how to do this!
- Now that we can signup a user, we want them to be able to login to our site. Build a
POST /login
route that expects a username and password in the request body. Use thebcrypt.compare()
function to compare the incoming plain text password with the hashed password stored in MongoDB. If the username or password is incorrect, return a non-specific error message (e.g. "Login credientials incorrect.") with a status code of 401. If the username and password combination is correct, return a temporary success message (e.g. "You are now logged in") and a status code of 201.
-
Question: Why is it important to give a non-specific error message as opposed to a message like "Password incorrect?"
-
Your Answer: If we were to give a more specific message, it could be used by an attacker to keep attempting to access sensitive information.
- The above process can be a bit tricky. Take a moment to annotate your code with comments, explaining each step of your code.
- On subsequent requests to our API, how will we know a user is logged in? We need to provide them with something so that we later know they have indeed successfully logged in. There are a few different strategies for this, but we will be using JWTs (pronounced "jots"). Take a moment to read the section titled "What is the JSON Web Token structure?"
-
Question: In your own words, describe the three parts of a JWT.
-
Your Answer:
At a high level, it is a JSON object that allows for safe and secure transfer of information between parties. It's encryption can be configured.
- We will implement JWTs using the jsonwebtoken package. Install this package and include it at the top of your
auth.js
file.
-
Question: Which of our current routes will require us to use the
jsonwebtoken
library? (i.e. When will we be creating or decoding JWTs?) -
Your Answer: We will need to use this on the
/signup
and/login
routes. -
Question: JWTs allow for custom information (i.e. payload) to be returned back to the client. What kind of information do you think would be useful to send back to our client?
-
Your answer: User information like ID, username, settings, permissions.
-
Question: The custom information (i.e. payload) inside of JWT can be easily decoded. What kind of information should we not store inside of a JWT?
-
Your Answer: Passwords and any account or sensitive information.
-
Add the following code to
/login
route and then respond with the token when a user successfully is able to login. NOTE: In the example below, I assume you've required the package and assigned it to ajsonwebtoken
variable.const payload = { id: guest._id }; const options = { expiresIn: "1 day" }; const token = jsonwebtoken.sign(payload, "MYSECRETPASSCODE", options);
-
Question: The
.sign()
method takes three arguments. Describe each argument in your own words, using the above code as an example. -
Your Answer:
payload
is the data. It's the object literal, buffer or string the represents valid JSON,secretOrPrivatedKey
is a string, buffer or object that contains secret HMAC or PEM encoded private key for RSA and ECDSA - https://www.npmjs.com/package/jsonwebtoken.options
give you a number of different built-in methods, like an expiration for example, that you could place a custom time on.
- Right now our secret is not so secret. Add a new environment variable to your
nodemon.json
file that stores the secret code. Then, use it in yourauth.js
file. NOTE: Make sure to restart your server!
- Modify the
/signup
route to return a token in place of the user information.
- We are now responding with JWTs when a user is properly authenticated. We will use those JWTs to authorize whether or not someone is allowed to visit certain routes or gain certain information.
-
Question: Describe the difference between authentication and authorization, given the above context.
-
Your Answer: In this case, authentication means that the user has a valid JWT. authorization means the user can then access specific routes or aspects of an app depending on their permissions level.
-
Add the following route to the top of your
auth.js
file. Then, make a request to this route in Postman.router.get("/profile", async (req, res, next) => { try { const token = req.headers.authorization.split("Bearer ")[1]; const payload = jsonwebtoken.verify(token, SECRET_KEY); const guest = await Guest.findOne({ _id: payload.id }).select( "-__v -password" ); const status = 200; res.json({ status, guest }); } catch (e) { console.error(e); const error = new Error("You are not authorized to access this route."); error.status = 401; next(error); } });
-
Question: What happens? Why?
-
Your Answer: The user's JWT is checked to make sure it's valid. Then if the user has the right permissions they are authorized to see a guest profile and the password information is hiding via the Mongoose
.select()
built-in method. If they don't have permissions, then they will see an error message stating "You are not authorized to access this route.".
- In order to successfully access this route, we will need to send over the token in the HTTP Authorization Header. The typical way to do this is by sending a Bearer token. To do this in Postman, go to the "Authorization" tab, select "Bearer Token" as the Type, and then enter your token.
-
Question: What happens? Why?
-
Your Answer: Once a valid Bearer Token is added to Postman, the user would have access to the profile route.
- There is a lot going on in the above code. Take a moment to annotate each line so you are able to confirm your understanding of what is happening.
Our final step is to authorize users to view certain routes or information. With a partner, complete the following:
-
Using route-level middleware, protect the
GET /api/parties/exclusive
route. If a user provides a token, they are able to see all of the exclusive parties. If not, return a 401 Unauthorized message. -
In the
GET /api/parties
route, return all parties if a user is authorized. Otherwise, return only those parties whereexclusive: false
. -
In the
GET /api/parties/:id
route, return the party if the user is authorized. If the user is not authorized, only return the party if it is not exclusive. Otherwise, return a 401 Unauthorized message.
If you want to build a new index, you will have to connect directly to MongoDB using your command line due to the fact that we are using a free tier from MongoDB Atlas. It's not too hard!
-
From the "Overview" tab on your MongoDB cluster, click the "Connect" button.
-
Click "Connect from Mongo Shell"
-
Follow the instructions for installing MongoDB locally on your system
-
Copy the command in the final step of the instructions and paste that into your command line
-
Enter your password as defined in your
nodemon.json
file -
Once connected, try running the
show dbs
command -
Connect to your database using the
use <database-name>
command -
Run the following:
db.guests.createIndex({ "username": 1}, { unique: true })
- Type
exit
to leave the shell