After a user successfully logs into an app, we’d like to allow the client to make requests on behalf of the user without having to send the user’s credentials each time.
To accomplish this, when the user logs in, the API server will share with the client a token that represents the user. The client will then save the token (e.g. in localStorage) and include the token in subsequent requests to identify the user who is making the request.
The token will be an encrypted string that contains the user’s unique Id, and will be encrypted using a private key that only the API server has access to.
When the client wishes to send a request on behalf of the user after the user is logged in, the client will send the token in the Authorization property as a Bearer token in the request’s headers object as shown below.
headers:{
Authorization:`Bearer ${token}`
}
When the API server receives a request from an endpoint that requires the user to be logged in, the API server will look for the bearer token in the headers object. If the bearer token is not found, the API server will return to the client a response that has a 401 status (Unauthenticated) code. If a bearer token is found, the API server will decode the token to retrieve the user’s Id and uses it to determine in the user has access to the resource they are requesting.
Create a Secret Key
To create an authentication token we need a secret key. When we are developing we can put the secret key inside our .env file. Add a variable named JSON_WEB_TOKEN_SECRET to your .env file and set it equal to a long string.
JSON_WEB_TOKEN_SECRET=this_is_a_secret_that_no_one_will_ever_guess
When you deploy your API server to a production environment you’ll need to set the environmental variable in your server’s control panel.
Whenever we need to access the environmental variable in the API server we’ll access it using process.env.JSON_WEB_TOKEN_SECRET.
Define generateAuthToken
We’ll be using the jsonwebtoken npm module to create and decode authentication tokens. If you haven’t already, install the jsonwebtoken npm module in your node project.
At the top of your User schema file, import jwt from the jsonwebtoken module.
import jwt from 'jsonwebtoken'
After your User schema definition, but before you call mongoose.model(), add the following instance method.
userSchema.methods.generateAuthToken = async function () {
const user = this;
const token = jwt.sign(
{
_id: user._id.toString(),
type: "User"
},
process.env.JSON_WEB_TOKEN_SECRET
);
return token;
};
Line 2 sets user equal to the user document on which generateAuthToken() was called.
Lines 2-4 create a token by calling jwt.sign(), passing to it a payload object which will be encrypted in the token and the private key.
Create a Login Endpoint
When a user logs in we’ll need to compare their password with the password hash stored in the User collection. To compare the two we’ll use the bcrypt npm module. Add the following import statement at the top of your user router file.
import bcrypt from 'bcrypt'
Add an endpoint inside your user router using the code below. This endpoint requires a username and password in the body of the request.
router.post('/user/login', async (req, res) => {
try {
let user = await User.findOne({ username: req.body.username });
if (!user) {
return res.status(400).send('Invalid credentials')
}
const isMatch = await bcrypt.compare(req.body.password, user.password);
if (!isMatch) {
return res.status(400).send('Invalid credentials')
}
const authToken = await user.generateAuthToken()
if (user.authTokens.length == 5) {
user.authTokens.shift()
}
user.authTokens.push(authToken)
await user.save()
res.status(200).send({ user, authToken })
}
catch (error) {
console.log(error)
res.status(500).send('Internal server error')
}
})
Let’s walk through the code.
Line 3 attempts to find a document in the User collection that has a username field with a value equal to the username provided by the client.
If findOne() returns undefined, then in lines 5-7 we return a response to the client with a status code of 400.
Next, on line 9, we check the password provided by the client by calling bcrypt.compare().
If the password does not match the hashed password then (lines 10-12) we return a response to the client with a status code of 400.
On line 14 we generate an authentication token by calling the instance method generateAuthToken() which we define below.
On lines 16-18 we shift out a token in the authTokens array if there are 5 tokens in the array. Then on line 19 we push the new token into the array.
On line 21 we save the user document in the database and one line 22 we return a response with status code 200 and the user document and token in the response body.
Test the Endpoint
Open Postman and select the Environments tab on the left side of the app. Choose the environment you created for the app. Then add a variable named authToken.
Now select the Collections tab on the left side of the app and create a new request for the login endpoint.
- Method: POST
- Endpoint: {{url}}/user/login
- Auth: No Auth
- Body: { username: “USERNAME“, “password”: “PASSWORD” }
Replace USERNAME and PASSWORD with the username and password that you used to create an account.
Add the following code in the scripts tab to save in Postman (simulating what the client will do) the authToken that is returned by the API server.
if (pm.response.code === 200) {
pm.environment.set('authToken', pm.response.json().authToken)
}
Save the request configuration and send a request to your API server. You should receive a request with a status code of 200 along with the user data and the authToken in a separate property.
Test with invalid credentials. You should receive a request with a status code of 400.
Push to GitHub
Commit your source code changes in VSC and push your changes to GitHub.