- Issue token
- Apply token
- Secure routes
- SSL encryption
- Password reset
Issuing a token is akin to registering for a new account. Once issued, the client will apply the token to each API call. The token represents the user's credentials, just like a username and password, but for API calls.
- Install dependencies
- Create a
/api/v1/auth/register
route - Create a
users
table in the database and a module to work with it - Hash the password and save the hash instead of the clear-text password
- Create a JSON Web Token (JWT) and issue it to the client
-
We're going to be POSTing JSON to our endpoints so we're going to need to tell Express how to process the body of the request messages.
Show code
// server/server.js ... server.use(express.json())
-
Create a new
server/routes/auth.js
file for your auth routes. The route should expose aPOST /api/v1/auth/register
route that accepts a JSON object withusername
andpassword
properties. The/api/v1/auth
part will be defined inserver/server.js
when we apply the router as middleware. Use a namedregister
function for the route's callback instead of a typical inline anonymous function. You'll see why in a later step. You can leave the route empty at this point. We need the next step to complete it.Show code
// server/routes/auth.js const express = require('express') const router = express.Router() router.post('/register', register) function register (req, res) { const {username, password} = req.body // TODO: make sure username doesn't already exist // TODO: if not, hash the password and add the user to the database } module.exports = router
-
Before we can complete this
/register
route, we need a place to save the new user. Here is an example knex migration you can use for yourusers
table:exports.up = (knex, Promise) => { return knex.schema.createTableIfNotExists('users', table => { table.increments('id') table.string('username') table.string('hash') }) } exports.down = (knex, Promise) => { return knex.schema.dropTableIfExists('users') }
Apply a migration so your database has a
users
table like the one above.Show terminal commands that use an npm knex script
npm run knex migrate:make users # edit the migration file to be like the one above npm run knex migrate:latest
-
Now we need a way to save the new user to the database. We should also make sure we can check that the username is available. Create a
server/db/users.js
file that exports these functions:userExists(username:string):Promise<boolean>
createUser(newUser:{username:string, password:string}):Promise
For now, just save the password in the
hash
field. We'll generate the hash in a later step.Show code
// server/db/users.js const connection = require('./connection') module.exports = { createUser, userExists } function createUser (username, password, conn) { const db = conn || connection return db('users') .insert({username, hash: password}) } function userExists (username, conn) { const db = conn || connection return db('users') .count('id as n') .where('username', username) .then(count => { return count[0].n > 0 }) }
-
Let's return to our
/register
route. Add the check for username availability and add the new user if it's available. Be sure torequire
the newserver/db/users.js
file and use its functions to complete theregister
function.- If the username is already taken, send back a status
400
and this JSON object:{message: 'User exists'}
. - If the username is available, add the user and respond with a status
201
. - If there is an error, respond with a status
500
and this JSON object:{message: err.message}
.
Show code
// server/routes/auth.js const express = require('express') const {userExists, createUser} = require('../db/users') const router = express.Router() router.post('/register', register) function register (req, res) { userExists(req.body.username) .then(exists => { if (exists) { return res.status(400).send({ message: 'User exists' }) } createUser(req.body.username, req.body.password) .then(() => res.status(201).end()) }) .catch(err => { res.status(500).send({ message: err.message }) }) } module.exports = router
- If the username is already taken, send back a status
-
To make sure we can save new users, wire up the
/api/v1/auth/register
route intoserver/server.js
and, using Postman, verify that when you can post a new user, it is saved to the database. Also verify you get back the expected HTTP response code when the username is already in use.Show code
// server/server.js const express = require('express') const passport = require('passport') const authRoutes = require('./routes/auth') const server = express() server.use(passport.initialize()) server.use(express.json()) server.use('/api/v1/auth', authRoutes) module.exports = server
-
Saving clear-text passwords is huge no-no. So let's fix that using the
sodium
npm package. Installsodium
as a normal dependency.Show terminal command
npm i sodium --save
-
Write a new
hash.js
module in a newserver/auth
folder. We'll use this folder to hold some auth-related helper function. Thehash
module should export agenerate
function that takes the clear-text password as its only parameter, and use thesodium
api to return a hash of that password.Show code
// server/auth/hash.js const sodium = require('sodium').api module.exports = { generate } function generate (password) { const passwordBuffer = Buffer.from(password, 'utf8') return sodium.crypto_pwhash_str( passwordBuffer, sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE ) }
-
We want to make it difficult to ever save a clear-text password. So call
generate
from thecreateUser
function inserver/db/users.js
. Don't forget to import thehash
module.Show code
// server/db/users.js const connection = require('./connection') const hash = require('../auth/hash') module.exports ... function createUser (username, password, conn) { const passwordHash = hash.generate(password) const db = conn || connection return db('users') .insert({username, hash: passwordHash}) } function userExists ...
Start the server and use Postman to register new users. Look in your database and ensure the
hash
field for new users is a hash and not their clear-text password. -
The last step in registering a new user is to create and issue a JSON Web Token (JWT) the client can use when making future requests to protected endpoints. For this we're going to use the
jsonwebtoken
npm package.To ensure a JWT is valid, it is signed with a secret string. We normally keep that string in an environment variable on the server. Let's use the
dotenv
npm package to help us manage our environment variables.Install
jsonwebtoken
anddotenv
as a normal dependencies.Show terminal command
npm i jsonwebtoken dotenv --save
-
The
dotenv
package reads our environment variables from a.env
file. Each line in the file is a new environment variable and follows this format:NAME_OF_ENV_VAR=value_of_environment_variable
Create this file in the root of your project with a
JWT_SECRET
variable and a value of at least 20 characters.Show a sample `.env`
JWT_SECRET=a31sl86dfk862jsd54lfk123lksjhd92
-
This is important. Add the
.env
file to your.gitignore
. You don't ever want this file to be committed to your source repository.Show code
# .gitignore node_modules bundle* *.sqlite .env
-
To enable the
dotenv
package so the environment variables are available, call itsconfig
function as early as possible in the server startup code (e.g. at the top ofserver/index.js
).Show code
// server/index.js require('dotenv').config() var server = require('./server') var PORT = process.env.PORT || 3000 server.listen(PORT, function () { console.log('Listening on port', PORT) })
-
The JWT we issue should contain the user's ID and username, so we need an object that contains these properties. Export a
getUserByName
function fromserver/db/users.js
that takes a username and returns aPromise
that resolves to a user object.Show code
// server/db/users.js const connection = require('./connection') const hash = require('../auth/hash') module.exports = { createUser, userExists, getUserByName } function createUser ... function userExists ... function getUserByName (username, conn) { const db = conn || connection return db('users') .select() .where('username', username) .first() }
-
Let's put the code for signing and issuing the token in a new
server/auth/token.js
module. This module should export anissue
function. Because we're going to use it as Express middleware, it should have this signature:issue(req:Request, res:Response, next:Function)
This function should use the
username
property on the request along with the newgetUserByName
function inserver/db/users.js
to retrieve the user from the database and create a JWT. The JWT secret is available fromprocess.env.JWT_SECRET
. Thesign
function from thejsonwebtoken
package has this signature:sign(user:Object, secret:string, options:Object)
The
options
parameter has anexpiresIn
property that is in zeit/ms format.Show code for `server/auth/token.js`
// server/db/users.js const jwt = require('jsonwebtoken') const db = require('../db/users') module.exports = { issue } function issue (req, res) { db.getUserByName(req.body.username) .then(user => { const token = createToken(user, process.env.JWT_SECRET) res.json({ message: 'Authentication successful.', token }) }) } function createToken (user, secret) { return jwt.sign({ id: user.id, username: user.username }, secret, { expiresIn: '1d' }) }
-
The last step to implement user registration is to add the
token.issue
middleware function to theregister
route inserver/routes/auth
. Specifically,- Import
server/auth/token
- Add
token.issue
as the 3rd parameter torouter.post('/register')
- Add
next
as the 3rd parameter to theregister
function - After the user is created, call
.then(() => next())
instead of theres.json
call
Show code
// server/routes/auth.js const express = require('express') const {userExists, createUser} = require('../db/users') const token = require(../auth/token) const router = express.Router() router.post('/register', register, token.issue) function register (req, res, next) { userExists(req.body.username) .then(exists => { if (exists) { return res.status(400).send({ message: 'User exists' }) } createUser(req.body.username, req.body.password) .then(() => next()) }) .catch(err => { res.status(500).send({ message: err.message }) }) } module.exports = router
- Import
-
Now use Postman to register new users. You should see the JWT in the response body.
Now that the user has been issued a JWT token, it can use it for authentication when requesting a secured endpoint. Because JWTs are intended to be stateless, we effectively sign in during each API call. We do this by adding it as an Authorization
HTTP header to each request. This screenshot illustrates how to add the header using Postman:
Notice how the token returned during registration is after Bearer
in the value of the header (there is a space between Bearer
and the token value). Now let's verify that token.
- Install dependencies
- Create a middleware function to verify and decode the JWT
- Use the contents of the token
We must be able to verify the authenticity of the token provided before we trust it. We can do this because it was signed with a secret (JWT_SECRET
). Once we know we can trust it, we can decode it to extract the user's ID and username, which we will likely need to fulfil the request.
-
As we saw above, the token will come in on the
Authorization
header. Theexpress-jwt
package is capable of getting the token out of the header, verifying its authenticity and populatingreq.user
with the contents of the decoded token. We just need to give it the secret we used to sign it. Installexpress-jwt
as a normal dependency.Show terminal command
npm i express-jwt --save
-
To use
express-jwt
let's expose adecode
function from ourserver/auth/token
module. Thedecode
function will be used as Express router middleware on all routes that need authentication. Here's how we'll use it:// server/routes/example.js const token = require('../auth/token') router.get('/path', token.decode, (req, res) => { // now req.user will contain the contents of our token })`
That means the signature of the
decode
function should look like this:decode(req:Request, res:Response, next:Function)
Show start of `decode` function code
// server/auth/token.js module.exports = { issue, decode } function issue ... function decode (req, res, next) { }
The
express-jwt
package exports a function. Let's name itverifyJwt
. We're going to call it inside of ourdecode
function. TheverifyJwt
function accepts an object as a parameter where each property is an option. We only need to provide thesecret
option/property and its value should be agetSecret
function.Show more of `decode` function's code
// server/auth/token.js const verifyJwt = require('express-jwt') ... function decode (req, res, next) { verifyJwt({ secret: getSecret }) }
The
getSecret
function is described in theexpress-jwt
docs here. Basically, it takes 3 parameters, the 3rd of which is an error-first callback that accepts the secret (process.env.JWT_SECRET
) as the 2nd parameter.Show code for `getSecret` function
// server/auth/token.js function getSecret (req, payload, done) { done(null, process.env.JWT_SECRET) }
Lastly, the
verifyJwt
function returns a function with the same parameters as ourdecode
function. So let's just pass the parameters through to that function call.Show all new code in `server/auth/token`
// server/auth/token.js const verifyJwt = require('express-jwt') function decode (req, res, next) { verifyJwt({ secret: getSecret })(req, res, next) } function getSecret (req, payload, done) { done(null, process.env.JWT_SECRET) }
-
To test this is working correctly, let's create an
/api/v1/auth/username
route that returns the username of the requester. The username is encoded into the token so it will be on thereq.user
object after ourtoken.decode
function is applied.Start with creating a
GET /username
route inserver/routes/auth.js
that looks similar to the example at the beginning of step 2. In the route, respond with a JSON object that has ausername
property with a value ofreq.user.username
.Show code
// server/routes/auth.js const token = require('../auth/token') router.get('/username', token.decode, (req, res) => { res.json({ username: req.user.username }) })
Make sure you've registered a new user and captured the JWT token in the response body. Configure postman with the
Authorization
header and issue aGET
request to/api/v1/auth/username
. If all is well, the response will be:{ "username": "your username" }
Congratulations! You're verifying JWT tokens!
forthcoming
forthcoming
forthcoming