Express API & Authentication
Throughout this tutorial, we'll build an Express API as the back end to a boilplate application that has users and authenticates them using Passport middleware.
Basic Express API Setup
First let's quickly set up a basic project environment.
Scaffold the Project
-
From the command line, create a new directory and switch into it with
mkdir MERN-AUTH-API && cd MERN-AUTH-API
. -
Run
git init
to initialize the repository for Git. -
Create a
.gitignore
and add the node_modules directory to it withecho node_modules > .gitignore
-
Creat a
.env
file withtouch .env
-
Create an entry point (or main file) with
touch server.js
. -
Create some directories inside your project to organize your code with
mkdir models db controllers middleware
. -
Run
npm init -y
to initialize the repository for npm. -
Install dependencies with
npm i express cors mongoose dotenv
. -
Open the directory in VS Code with
code .
. -
Create your directory folders:
MERN-AUTH-API
├── index.js
├── controllers
├── middleware
└── models
- Save, add, and commit your files:
git add .
git commit -m "Initial commit"
- Create a new repository on GitHub and copy the code in the section that reads:
…or push an existing repository from the command line
by clicking the copy icon on the right side of the code block. - Paste the code in the Terminal window.
Set up cluster on Atlas
First, let's set our app up to use Atlas instead of our local mongo database.
-
Create a an account here
-
Create a free tier cluster by following these instructions
- NOTE: Step 3's screenshots haven't been updated so it may look a little different when you do it. Just make sure to choose the free tier.
- TODO: Add notes about why you may pick one region over another.
- Go to Securty > Network Access (from menu on left of page) to Whitelist your IP address
- Whitelist your current IP address and also click the allow access from anywhere button
- Go to Security > Database Access to add a user
- NOTE: Make sure you know the password!!!
- Click "connect", then "connect your application"
- choose the NodeJS driver for step 1
- copy the connection string from step 2 (we'll use it in the next step)
Connect to MongoDB
- Create a file in the
models
directory calledindex.js
and add the following code:
const mongoose = require('mongoose');
const MONGODB_URI = `<put your connection string here>`
mongoose.connect(MONGODB_URI)
mongoose.connection.once('open', () =>
console.log(`🔗 Connected to db: ${mongoose.connection.name}`)
)
mongoose.connection.on('error', err =>
console.log('🔥 Connection failed!', err)
)
module.exports = mongoose;
Make sure to add a db username and password and name to the appropriate parts of the connection string. For example: mongodb+srv://sei:seisei@sei-mern-auth.a0ell.mongodb.net/SEI-MERN-Auth?retryWrites=true&w=majority
2.Back in the Terminal make sure you're in the MERN-AUTH-API
directory and run the file to test your connection using NodeJS with node models/index.js
. If you get a Connection failed
error or do not see Connected to db: job-board
, check and make sure that your MongoDB server is running. Otherwise, you should see output similar to the following:
(node:48059) DeprecationWarning: current URL string parser is deprecated, and will be removed
in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
(node:48059) DeprecationWarning: current Server Discovery and Monitoring engine is deprecated,
and will be removed in a future version. To use the new Server Discover and Monitoring engine,
pass option { useUnifiedTopology: true } to the MongoClient constructor.
Connected to db: job-board
- Let's get rid of the warnings by modifying the
mongoose.connect()
method like so mongoose deprecation docs:
mongoose
.connect(DB_CONNECTION_STRING, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
useFindAndModify: false
})
...
-
Back in the Terminal, type
control + C
to stop the process that's running and return to the command prompt. Now, try running theindex.js
file again withnode models/index.js
. This time you should only see the connection message. -
Great! But, we know that this API isn't always going to be run on our Atlas cluster, so we should use an environment variable for the connection string.
-
Move your connection string to be an environment variable by putting it in your
.env
file, then import and configuredotenv
at the top ofmodels/index.js
. -
You'll also need to change the
connect
argument to include theprocess.env.MONGODB_URI
.
- Finally, export your connected mongoose instance for use in other files by adding
module.exports = mongoose
The completed file will look like this:
// Import dotenv and configure it for use in this file
require('dotenv').config()
// Import Mongoose to interface with MongoDB
const mongoose = require('mongoose');
// Use Mongoose's connect method to connect to MongoDB by passing it the db URI.
// Pass a second argument which is an object with the options for the connection.
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
useFindAndModify: false
})
// If the connection is successful, give a message in the Terminal with the db name
.then(instance =>
console.log(`Connected to db: ${instance.connections[0].name}`)
)
// If the connection fails, give a message and pass along the error so we see it in
// the Terminal.
.catch(error => console.log('Connection failed!', error));
// Export the connection so we can use it elsewhere in our app, we will change this eventually
module.exports = mongoose;
- Save and close the file in VS Code and back in the Terminal, type
control + C
to stop the process that's running and return to the command prompt. Add and commit your changes!
Setup a Server
- In the
server.js
file lets create a basic Express server and get it running. We need to require Express and store it in a variable calledexpress
. Then we'll invoke express to instantiate the Express application object and store that in a variable calledapp
. Finally, we'll listen on port 8000 for requests and add a callback so we know its running. The basic server looks like this:
const express = require('express');
const app = express();
app.listen(8000, () => {
console.log('listening on port 8000');
});
-
Use
nodemon
to run the server. -
This server doesn't do anything at all, so lets build it out a bit more. We know we'll be adding our routes in here so let's require
mongoose
and while we're at it import the cors package and set up the middleware. Remember, to use a middleware in Express we need to pass it to theapp.use()
method. -
We're also going to have to use two of the built-in middleware packages since we're going to be making requests via AJAX to the server, so add
app.use(express.json())
andapp.use(express.urlencoded({ extended: true }))
. -
Lastly, again, we know that eventually we'll be running this on a remote server, so lets create a PORT environment variable. We can assign the variable the value of the PORT environment variable that will be set in Heroku OR if that environment variable doesn't exist, it should use 8000.
The completed file should look like this when done:
// Require necessary NPM packages
const express = require('express');
const mongoose = require('./models');
const cors = require('cors');
// Instantiate express application object
const app = express();
// The `.use` method sets up middleware in Express
// Set up cors middleware and make sure that it
// comes before our routes are used.
app.use(cors());
// The urlencoded middleware parses requests which use
// a specific content type (such as when using Axios)
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
// Run server on designated port
app.listen(process.env.PORT || 8000, () => {
console.log(`🎧 You're listening to the sounds of Port ${process.env.PORT || 8000} 🎧`)
})
Create the User Model
- Create a new file in the
models
directory calleduser.js
. - Create a basic user model. To keep things simple, our model is going to be super streamlined with just
email
andpassword
fields. We'll also add a timestamp option so we automatically get thecreatedAt
andupdatedAt
fields.
const mongoose = require('mongoose');
const options = {
timestamps: true
}
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
}, options)
module.exports = mongoose.model('User', userSchema);
- Add it to your
index.js
file as an export!
require('dotenv').config();
const mongoose = require('mongoose');
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
useFindAndModify: false
})
mongoose.connection.once('open', () =>
console.log(`🔗 Connected to db: ${mongoose.connection.name}`)
)
mongoose.connection.on('error', err =>
console.log('🔥 Connection failed!', err)
)
module.exports.User = require('./user');
Test User Model
Create a models/userTest.js
file and import the user model. Add mongoose code to create a new user (mongoose docs for inserting):
const db = require('./index');
db.User.create({
email: 'test@test.com',
password: 'testpassword'
}, (err, createdUser) => {
if (err) console.log('Error added test user', err)
else console.log('success!', createdUser)
process.exit()
});
Run node db/userTest.js
and check that the test user was added successfully (you'll see the console log if it did, but also look in your collection on Atlas)!
Create Routes for the User Resource
- Create a new file in the
controllers
directory calledusers.js
. - Require express, the user model, and create a router and export it:
const express = require('express');
const router = express.Router();
// routes/controllers here
module.exports = router;
You'll also need to write the controller middleware for users in server.js
:
- Stub out the routes for our user resource. They will be:
- /signup: a POST route that will create a new user in the database
- /login: a POST route that will create a new authorization token for the user
// SIGN UP
// POST /api/signup
router.post('/signup', (req, res) => {
res.send('hit signup post route')
})
// Log IN
// POST /api/signin
router.post('/login', (req, res) => {
res.send('hit login post route')
})
Import the user controllers to server.js
underneath any other middleware you have:
// Import the user resource actions
app.use('/api', require('./controllers/users'))
Test that these routes are being hit via Insomnia!
-
Import the
User
model into your users controller via theindex.js
const db = require('../models')
-
Add create to the signup controller:
router.post('/signup', (req, res, next) => {
db.User.create(req.body)
.then((createdUser) => res.send(createdUser))
.catch(err=>{
console.log('Oops, there was an error creating the user!')
})
})
Use Insomnia to sign up a new user!
- We're building an API that will send data in JSON format, so let's be legit and use the JSON response method provided by express.
router.post('/signup', (req, res, next) => {
User.create(req.body)
.then(createdUser => res.json(createdUser))
.catch(err => {console.log(err)})
})
In order for this to work, we also have to include the express JSON middleware in index.js
:
// Add `express.json` middleware which will
// parse JSON requests into JS objects before
// they reach the route files.
app.use(express.json());
Check out this medium article or the express docs to learn more about the differences between .json
and .send
.
Try signing up another user to make sure all is still functioning. You shouldn't notice any difference in behavior of your app.
Prevent Passwords from Being Sent to Clients
You may have noticed that when you created a new user, you got back a user document with the user's password. That's a huge security hole in our API right now. We can fix it using Mongoose Virtuals pretty easily though. Virtuals are used to transform data without persisting the transformation in MongoDB. We'll create a virtual that will automatically remove the password field any time we use a toJSON method (including JSON.stringify()
, Mongoose's .toJSON()
method or Express' .json()
method). Even though the field is being deleted by the virtual, it remains safe and sound in our database.
- Open the
models/User.js
file. - Update the schema as follows to add a virtual:
const options = {
timestamps: true,
toJSON: {
virtuals: true,
transform: (_doc, userDocToReturn) => {
delete userDocToReturn.password
return userDocToReturn
}
},
}
Create a new user in Insomnia. 🎉 No more password being sent! However, it seems we introduced another issue. Now, we have both an _id
and an id
field. Technically, this additional id
field is just a virtual because we used a toJSON virtual. You can verify that itʼs not storing the value in MongoDB separately. If it bugs you, you can add id: false
as a key/value pair in the options object that has the timestamps
and toJSON
properties.
Store a Hashed Password
We're breaking a cardinal rule of user security by saving the user's password in plain text. Even in a-just-for-fun, non-commercial app, we're opening ourselves up to financial liability and risking the security of users who often reuse the same password on multiple sites. So lets fix that, shall we?
When it comes to storing password data securely, the only thing we can do is not store it at all. Instead we should store a hash of the password. Hashing is a one-way function. Hashed values are not designed to be reversed to obtain the original input value like encrypted values, which are designed to be decrypted. If you apply the same hashing algorithm to the same value you'll always get the same hash though. That means we can store the hash of the password and when users sign into the system, we can hash the password they send and compare it with the hash in the database to verify that they provided the correct password.
- We'll use a popular npm package called
bcrypt
to hash our passwords, so in the Terminal runnpm i bcrypt
. - Require the
bcrypt
package in yourcontrollers/users.js
file withconst bcrypt = require('bcrypt');
. - To hash the password, we'll use the
bcrypt.hash()
method which takes two arguments. The first argument is the value we want to hash and the second is the number of salt rounds. Salting is a way to make the hash stronger. Each time the value is salted, it is transformed in some way by adding another value to it. The more times you salt, the more the original value is changed and obscured. We're going to use10
salt rounds. The bcrypt.hash()
method is asynchronous.
...
const bcrypt = require('bcrypt');
...
//Using promise chain
router.post('/signup', (req, res) => {
bcrypt.hash(req.body.password, 10)
.then(hash => db.User.create({email: req.body.email, password : hash }))
.then(createdUser => res.json(createdUser))
.catch(err => {
console.log(err)
})
})
Create a new user with a different email address in Insomnia. If you look in Mongo, you should see that the password is now hashed and looks something like this:
"password" : "$2b$10$5g62t1K7SUovJ2.XonHfy.kiDWQr/UEpR1ha8DSwAWWpBob5WXAKy"
If your passwords are hashed, add and commit your changes.
Add Authentication
In this part of the tutorial, we'll be tackling the steps needed to add authentication to our app. We'll be using Passport to simplify the authentication process. To use Passport, we need to install it in our app along with one (or more) of the over 500 strategies it offers for authentication. For this tutorial, we'll be employing a strategy which uses JSON Web Tokens (JWT).
JWT is an open standard and an excellent choice for modern applications based on REST architectures. The fundamental premise of the REST architectural style is that the server does not store any state about the client session on the server side, hence the name Representational State Transfer. In REST, every HTTP request happens in complete isolation. It is up to the client to send (i.e., transfer) whatever state is needed to carry out the request. JWT provides a lightweight approach to transferring state from the client to the server in a secure fashion.
Configure Passport
Each Passport strategy has to be configured for your specific app. Basically, Passport gives us a callback and we fill it in with any logic needed to get the user from our database that matches some bit of data that Passport extracts from a request. After configuring the strategy with the code to retrieve the user from the database, we register the strategy, and initialize Passport.
Once initialized, weʼll run the passport strategy as route middleware. When run as middleware, Passport receives the request, extracts the user information from the token, then attaches the user info back to the request object so it is accessible by whatever action (route) it hits.
- Create a new file in the
middleware
directory calledauth.js
. - Install passport
npm i passport
- import it into
auth.js
. Stub out the generic passport structure:
const passport = require('passport')
// construct the strategy (TODO)
const strategy
// 'Register' the strategy so that passport uses it
// when we call the `passport.authenticate()`
// method later in our routes
passport.use(strategy)
// Initialize the passport middleware based on the above configuration
passport.initialize()
- Install passport's JWT strategy
npm i passport-jwt
- import the
Strategy
constructor from `passport-jwt and construct the strategy:
const passport = require('passport')
const Strategy = require('passport-jwt').Strategy
// construct the strategy (will define options and findUser soon)
const strategy = new Strategy(options, findUser)
...
- Create an
options
object that we'll pass into theStrategy
constructor.
const Strategy = require('passport-jwt').Strategy
const options = {}
// construct the strategy (will define options and findUser soon)
const strategy = new Strategy(options, findUser)
- The options object requires a
secretOrKey
field
const Strategy = require('passport-jwt').Strategy
const options = {
secretOrKey: 'some string value only your app knows'
}
// construct the strategy (will define options and findUser soon)
const strategy = new Strategy(options, findUser)
Because this is a secret string, it should be stored in an
.env
file. We will do this later
- The other required option field is
jwtFromRequest
. This has to be a function that accepts the request object as the only parameter and returns either the JWT as a string or null. This is called an extractor function because extracts and deserializes the JWT from the request object. Passport provides several built-in extractor functions and we will usefromAuthHeaderAsBearerToken()
, which looks for the JWT in the authorization header with the scheme 'bearer'. Visit the passport docs on extracting the JWT from the request for more details.
const Strategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const options = {
secretOrKey: 'some string value only your app knows',
// How passport should find and extract the token from the request.
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
}
- Now we need to write the verification callback that gets passed as the second argument to the
Strategy
constructor. This callback is where we write custom code to go get the user's info from the database based on the token info. It will receive two arguments automatically:
- The deserialized JWT payload that has been extracted from the request object, which will contain the user's id
- A
done
callback that is ready to receive the user object and pass it onto our routes. It takes any errors that happen along the way as the first argument, and the user object as the second.
const db = require('../models')
const findUser = (jwt_payload, done) => {
db.User.findById(jwt_payload.id)
.then(foundUser => done(null, foundUser))
.catch(err => done(err))
}
- Each time the user logs in, we'll need to create a token for them. We will create a function that uses the jsonwebtoken
.sign
method to turn the user id and secret into a JWT using the RSA SHA256 algorithm. RSA SHA256 will first hash the user id and the secret using the SHA256 algorithm, then encrypt that hash using the RSA algorithm.
First install the package and import it into the auth.js
file:
npm install jsonwebtoken
const jwt = require('jsonwebtoken')
- Now we import bcrypt and write the function to be exported:
...
const bcrypt = require('bcrypt')
...
// Initialize the passport middleware based on the above configuration
passport.initialize()
// we will export this and use it in the login route to create a token each time a user logs in
const createUserToken = (req, user) => {
// first, we check the password using bcrypt (you'll need to import it!)
const validPassword = req.body.password ?
bcrypt.compareSync(req.body.password, user.password) : false
// The following is equivalent:
// const validPassword
// if(req.body.password) {
// validPassword = bcrypt.compareSync(req.body.password, user.password)
// } else {
// validPassword = false
// }
// if we didn't get a user or the password wasn't validated, then throw error
if(!user || !validPassword){
const err = new Error('The provided username or password is incorrect')
err.statusCode = 422
throw err
} else { // otherwise create and sign a new token
return jwt.sign({ id: user._id }, 'some string value only your app knows', {expiresIn: 3600})
}
}
- Now export
createUserToken
at the bottom ofauth.js
so we can use it in our/api/login
route. So far, yourauth.js
should look like this:
const passport = require('passport')
const Strategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')
const db = require('../models')
const options = {
secretOrKey: 'some string value only your app knows',
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
}
const findUser = (jwt_payload, done) => {
db.User.findById(jwt_payload.id)
.then(foundUser => done(null, user))
.catch(err => done(err))
}
const strategy = new Strategy(options, findUser)
passport.use(strategy)
passport.initialize()
const createUserToken = (req, user) => {
const validPassword = req.body.password ?
bcrypt.compareSync(req.body.password, user.password) : false
if(!user || !validPassword){
const err = new Error('The provided username or password is incorrect')
err.statusCode = 422
throw err
} else {
return jwt.sign(
{ id: user._id },
'some string value only your app knows',
{expiresIn: 3600}
)
}
}
module.exports = { createUserToken }
Update Auth routes to respond with token
- Import
createUserToken
into the users controller so we can call it in our routes:
const { createUserToken } = require('../middleware/auth')
- Update signup route to send back token instead of the user:
router.post('/signup', (req, res, next) => {
bcrypt.hash(req.body.password, 10)
.then(hash => db.User.create({email: req.body.email, password : hash }))
.then(createdUser => res.json({ token: createUserToken(req, createdUser) }))
.catch(err => {
console.log(err)
res.json({'error': err})
})
})
- Finish login route:
// POST /api/login
router.post('/login', (req, res)=>{
db.User.findOne({ email: req.body.email })
.then(foundUser => res.status(200).json({ token: createUserToken(req, foundUser) }))
.catch(err=> {
console.log('ERROR LOGGING IN:', err)
res.json({ 'error': err})
})
})
Now use Insomnia to try out both of these routes. You should get a token as the response!
Head over to jwt.io to see your token decoded and you can read about the anatomy of a JSON Web Token here.
- Now that we know everything is working right, let's move our secret to our
.env
and make it an environment variable calledJWT_SECRET
.
JWT_SECRET=some string value only your app knows
Make sure to change your secret to process.env.JWT_SECRET
in auth.js
where you set the options
object, and also where you call jwt.sign
Test it one more time to make sure all is good by logging in a user again (you may need to restart nodemon).
We're so close to done now! All that's left is to set up our job route to use the token! Add and commit your changes.
HTTP Status Codes
Our Express API is coming along, but before we add our User model, we need to fix some things. So far, we're kind of breaking a lot of rules when it comes to the HTTP request-response cycle. For one, we're not responding to all requests — only the ones that execute flawlessly. We're not handling any of the error cases. ExpressJS will help us out with some errors by sending a generic 500 server error, but in many cases, we're on our own and the system is simply left to hang 😦.
Another issue is that we're not setting the status codes on our responses, so every successful response is the default 200 OK
. Setting acurate status codes is not simply prescribed by REST architecture best practices. HTTP response status codes are part of the rules defined by HTTP that govern the Web! Plus, it turns out that it's really easy to change the status codes within Express APIs, so letʼs do that:
- Add a 201 status code to the sign up response:
router.post('/signup', (req, res, next) => {
bcrypt.hash(req.body.password, 10)
.then(hash => db.User.create({email: req.body.email, password : hash }))
.then(createdUser => res.status(201).json({ token: createUserToken(req, createdUser) }))
.catch(err => {
console.log(err)
res.json({'error': err})
})
})
What status code would you add to the catch clause?
Add Authorization
Along with authenticating the user, we now have to handle user authorization. What's the difference? When the user logs into the system successfully, we authenticate them based on the credentials they send (such as a proper combination of email and password). Authorization means determining whether the user is actually authorized to perform some action in the system.
With the token, we can determine which user is making a request. With that information, we can determine if the specific user making the request is authorized to carry out a specific action, such as create documents or delete or update a specific document.
BELOW THIS LINE IS DRAFT<<<<<<<<<<<<<<<<<<<<<<<<<<
Setup Postman to Run Tests Sequentially
Postman actually contains a ton of helpful features for running API tests. One feature that can be particularly helpful is the ability to set up an environment for your API that stores variables. Even better, we can automatically update the variables when we receive a response from the server. This is especially helpful when you're dealing with requests that are dependent upon data other requests. For example, we now have to set that long-ass token into the Authorization header of everyone of our POST, DELETE and PUT routes for our job resource, and since the token expires after some time, we’ll have to do it regularly.
Create Environment Variables
-
Open Postman.
-
Click on the gear icon (:gear:) on the top right side of the window just below the taskbar.
-
When the Manage Environments modal appears, click the orange Add button at the bottom of the modal.
-
Give your environment a descriptive name such as Job Board API.
-
For the first variable, name it
url
and set the initial value and current value column to:http://localhost:8000/api
. -
Add a second variable named
id
and set its initial value to an empty string (''
). -
Add a third variable named
token
and also set its initial value to an empty string (''
). -
Click the orange Add button on this screen and then close the modal by clicking the x in the upper right corner.
-
In the environments dropdown selection list choose your new Job Board API environment.
Add Test Script to the Signin Request
Now you can update your /signin
request in Postman. Instead of manually having to copy and paste the token to each of our resource request in Postman, we can have Postman automatically run some code when the response to our signin request is received and set the token in the variable we created.
- Open the signin request you created earlier in Postman.
- In the toolbar below the request URL input field, click the Tests tab and add the following code:
var data = pm.response.json();
pm.environment.set('token', data.token);
Click the blue Send button and then click the eyeball icon next to the environments dropdown. You should now see the token variable is set. 🎉
Set the Authorization Header in the Jobs Post Request
With the token stored in a variable, it’s easy for us to add it to our authenticated requests.
- Open the request you created for the post to the
/jobs
route. - In the toolbar below the request URL input field, click the Headers tab.
- Add a header with
Authorization
set for the key andBearer {{token}}
set for the value. - Back in the toolbar, click the Tests tab and add the following code:
var data = pm.response.json();
pm.environment.set('id', data._id);
Make sure youʼve still got JSON data set in the Body tab and click the blue Send button. Assuming that all went well, you should see the newly created job in the response window. You'll also have an id for the newly created job that you can easily use in your DELETE and PUT requests by changing the url on the requests to: {{url}}/jobs/{{id}}
... How awesome is that!
Handle Authentication in React
In terms of making the Fetch or Axios requests to the API in React, you'll now have to add the Authorization
header to authenticated requests, such as:
axios({
url: `${APIURL}/jobs`
method: 'POST',
headers: {
'Authorization': `Bearer ${props.token}`
},
data: job
});
/*** ALTERNATIVELY USING FETCH ***/
fetch(`${APIURL}/jobs`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${props.token}`
},
body: JSON.stringify(job)
});
One approach to handling the token is to use React's built in Context API to store the value when it is returned after the user logs in. Storing the user and accessing via useContext Hook is easier than passing around the token via props and eliminates prop drilling entirely.
Alternatively, you can create state to hold your user in the App.js, along with a method to set the user state and pass that as a prop to your signin form's handleSubmit
method. When the AJAX call to your server is successful and you get the token back from the API, you can call the method that sets the token in state in App. Now you can pass the token to all of the components that need it as props.
There are also third party state management tools that could be used to store the token. They range from very basic (react-hooks-global-state
) to complex (Redux).
Test Authenticated Routes
To write tests for authenticated routes, you'll need to use the before or beforeEach hooks in Mocha to generate a user token. You may find that using the async/await syntax is more straightforward here than using promise chains, but both will work. You can also use superagent as described in this article.
Adapted from https://git.generalassemb.ly/jmeade11/mern-auth-tutorial