In this challenge you will update the /login
endpoint to create and return a JWT. You will also create a Passport JWT Strategy and use it protect all the notes
, folder
and tags
endpoints. And then use the JWT to access the protected endpoints.
Add JWT_SECRET
and JWT_EXPIRY
settings to config.js
.
module.exports = {
PORT: process.env.PORT || 8080,
MONGODB_URI: process.env.MONGODB_URI || 'mongodb://localhost/noteful',
TEST_MONGODB_URI: process.env.TEST_MONGODB_URI || 'mongodb://localhost/noteful-test',
JWT_SECRET: process.env.JWT_SECRET,
JWT_EXPIRY: process.env.JWT_EXPIRY || '7d'
};
Notice that JWT_SECRET
does not have a fallback value so it must get the value from an environment variable. Let's set that up now.
There are a few ways to setup an environment variables like creating them in the shell or defining them in your .bash_profile
or .bash_rc
. We'll use the dotenv
NPM package which allows you to define environment variable on a per-project basis.
First, install the npm package npm install dotenv
. Then create a file named .env
with the following, replacing <YOUR-SECRET>
with a secret of your choosing.
JWT_SECRET=<YOUR-SECRET>
The JWT_SECRET
is used by the JWT library to create secure token. It is essential that the value remains private, never commit and push the .env
file to GitHub. Before proceeding, verify that .env
is listed in the .gitignore
file.
At the top of the config.js
, require and config the dotenv package. At the top of the file, add the following:
require('dotenv').config();
Currently the /login
endpoint returns the current user. You need to update it to return a JWT which contains the user information.
First, npm install
the jsonwebtoken
and require it. Then in /routes/auth.js
file, require config.js
and extract JWT_SECRET
and JWT_EXPIRY
using object destructuring. Then create a function which accepts a user object and calls jwt.sign()
to generate a JWT. Below is an example.
function createAuthToken (user) {
return jwt.sign({ user }, JWT_SECRET, {
subject: user.username,
expiresIn: JWT_EXPIRY
});
}
Now update the /login
endpoint, create a token by calling the createAuthToken
function and return it to the user.
router.post('/login', localAuth, (req, res) => {
const authToken = createAuthToken(req.user);
res.json({ authToken });
});
Test the endpoint using Postman.
Now that you can generate and return JWTs, you need to create a Passport JWT Strategy and use it protect endpoints.
First npm install
the passport-jwt
package.
Create a /passport/jwt.js
file. In the file, require the passport-jwt
package and extract Strategy
and ExtractJwt
from the the library. Also require the config.js
and extract JWT_SECRET
. Below is an example.
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const { JWT_SECRET } = require('../config');
Create an options object with the following settings. Then, using the options object create a new JWT strategy.
const options = {
secretOrKey: JWT_SECRET,
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
algorithms: ['HS256']
};
const jwtStrategy = new JwtStrategy(options, (payload, done) => {
done(null, payload.user);
});
Finally, export jwtStrategy
so it can be required elsewhere.
Next on server.js
, require the strategy and configure passport to utilize.
const jwtStrategy = require('./passport/jwt');
passport.use(jwtStrategy);
Protecting the Notes, Folders and Tags routers is easy. Simply add the following router.use
call to each of the router files, be sure to require Passport.
// Protect endpoints using JWT Strategy
router.use('/', passport.authenticate('jwt', { session: false, failWithError: true }));
The basic JWT structure is now ready to go. Test it out using Postman. Create a new user and login. Then, using the Bearer Token authorization and the generated authToken
, attempt to access the protected endpoints.
To finish the server-side implementation, you need to add a /refresh
endpoint that allows users to exchange older tokens with fresh ones. Add the following to the /routes/auth.js
.
const jwtAuth = passport.authenticate('jwt', { session: false, failWithError: true });
router.post('/refresh', jwtAuth, (req, res) => {
const authToken = createAuthToken(req.user);
res.json({ authToken });
});
Finally, there are 2 updates to the client. First, are capturing the authToken
and second, using it for all the api requests.
In the store.js
file, add an authToken
property and default it to an empty string.
...
return {
notes: [],
folders: [],
tags: [],
currentNote: {},
currentQuery: {
searchTerm: '',
},
authToken: '' // <<== Add this
};
In noteful.js
, find and update the handleLoginSubmit()
function. Define an store.authToken
property and set it to the incoming authToken value.
...
api.create('/api/login', loginUser)
.then(response => {
store.authToken = response.authToken; // <<== add this
store.authorized = true;
loginForm[0].reset();
return Promise.all([
api.search('/api/notes'),
api.search('/api/folders'),
api.search('/api/tags')
]);
})
...
Now, update all of the API methods to include the Authorization
header with Bearer token
.
In api.js
, add the following to each API method.
headers: { 'Authorization': `Bearer ${store.authToken}` }
Here is search
as an example.
const search = function (path, query) {
return $.ajax({
type: 'GET',
url: path,
dataType: 'json',
data: query,
headers: { 'Authorization': `Bearer ${store.authToken}` }
});
};