Fully Serverless DERN Stack TODO App Pt. 2 - Building out our API
Adam Katora
Posted on March 2, 2022
Part 2 - Building out our API & Auth system
If you're just joining us, in Part 1 of this series, we setup a simple express.js application, then used Claudia.js to deploy our app to AWS.
Here in Part 2, we'll be building out enough of our application that by the end you'll have a small, but functional, REST API. Since Part 1. was a lot of boilerplate Claudia.js setup, I've tried to get this Part 2 out as quickly as possible so that you can start to get an idea of what our final app will look like.
As such, I haven't been able to fully go through this write-up myself to ensure that there's no bugs in the code, and add in helpful screenshots. That'll be coming soon. I'm going to make sure the Github repo for this write-up is up to date first, so if you run into any issues, try checking there first for working code examples.
With all that out of the way, let's move on to the fun stuff, developing some features for our app. Mainly, a simple Auth system. We'll start by adding the Dynamoose package so writing some data models. We'll also add morgan, a logger middleware so that we can get info about incoming requests in the console.
From the /backend
folder run the following:
npm install dynamoose morgan
Next, inside the /backend/src
create a models
directory where we'll store our dynamoose models.
cd src
mkdir models
We're going to try to keep our app simple, so we'll create 2 models. 1.) Will be a User model, with a very (read NOT production ready) basic auth system. 2.) Will be a Todo model to store information about User's Todos.
From inside the models folder create two new files for each of the models. I like to follow a [ModelName].model.js
naming convention in my Express.js apps.
cd models
touch User.model.js
touch Todo.model.js
Now, it's time to build out our models. If you've used Mongoose before, the syntax and schema of Dynamoose models should look very familiar to you.
Type the following code for our User model.
User.model.js
const dynamoose = require("dynamoose");
const userSchema = new dynamoose.Schema({
"id": String, // UUIDv4 ID
"username": String,
"password": String,
}, {
"timestamps": true
})
const User = dynamoose.model("User", userSchema)
module.exports = User
We start by importing the dynamoose library with require("dynamoose")
. Next, we define our model's schema with the dynamoose.Schema()
. The first Object we pass into dynamoose.Schema()
contains all the fields and their associated "attribute types" (aka data types) for our model.
You can read about the available attribute types here.
For right now, we're just going to create fields for id
, username
, and password
.
I've mentioned this already, and I think it goes without saying but just to cover all my bases here, I wouldn't use this auth implementation in a production app. There's much better and more secure IdP services out there for developers. AWS has their Cognito IdP service, and Auth0 is another good choice. Both offer a fairly generous free-tier to allow you to get started quickly and eventually grow into a paid plan.
We also pass a second object to the .Schema()
method, with some additional Schema Settings. We're setting "timestamps" to true which will automatically add createdAt & updatedAt timestamps.
Finally, we use the dynamoose.model()
method, to create a new const User
. The first param passed to .model
is a string. This is what our model will be called. The second param we pass to .model
is the object containing our SchemaDefinition and SchemaSettings, which in our case we stored in the userSchema
const.
At the bottom of the file, we have a standard module.exports
so that we can import the User
model in other files.
With that created. Let's add the following to our Todo.model.js
file.
backend/src/models/Todo.model.js
const dynamoose = require("dynamoose");
const todoSchema = new dynamoose.Schema({
"id": String, //UUIDv4
"user": Object,
"title": String,
"notes": String,
"dueDate": String,
"status": String,
}, {
"timestamps": true
})
const Todo = dynamoose.model("Todo", todoSchema)
module.exports = Todo
Our Todo
model is very similar to our User
model with one major difference. We added a field for user
with a type of Object
. We might end up changing this around later on, but that's one of the beauties of NoSQL databases, we don't have to get bogged down in too much data-modelling early on.
Now that we have our Models in place, we need to start building out how our API will interact with our models. I like to structure my Express.js apps in a bit of an MVC pattern (in this case React will be our V - view layer), and also create "Service Layers". If those two things don't make sense to you, no worries, just follow along and hopefully the project structure and code should help you make sense of those terms as we go along.
Also, if you've been following along this far, I'm going to assume you're comfortable with making new directories & files, so I'll just explain what new dirs and files we're creating, then at the end show the project structure instead of showing the bash command to create each new file.
Back inside the /src
directory, make directories for routes
, controllers
, and services
. Inside /src/routes
create an index.js
file and an auth.routes.js
file. Inside the /src/contollers
directory create a file Auth.controller.js
. Inside the /src/services
directory create an Auth.services.js
file.
With all of those files created, this is what our project structure should look like now:
backend/
- node_modules/
- src/
- controllers/
- Auth.controller.js
- models/
- Todo.model.js
- User.model.js
- routes/
- Auth.routes.js
- index.js
- services/
- Auth.service.js
- app.js
- app.local.js
- claudia.json
- lambda.js
- package-lock.json
- package.json
With those files created, let's get our router setup.
Let's start by editing our src/app.js
file. Make the following changes so that your app.js file looks like this:
/src/app.js
const express = require("express")
const app = express()
// morgan for logging
const morgan = require("morgan")
app.use(morgan('dev'))
// Import Routes
app.use(require("./routes"))
module.exports = app;
First, we start by adding the morgan logging middleware. This will handle automatically logging to the console what requests our app receives, useful for both development and catching things that go wrong in production.
Next, we tell our app to handle all routes from our ./routes/index.js
file. You'll notice that we didn't explicitly reference the /.routes/index.js
file though, just the dir name.
Let's go ahead and implement our routes file now. Inside /src/routes/index.js
add the following code:
/src/routes/index.js
const router = require('express').Router();
const authRoutes = require('./Auth.routes')
// Moved our API Root GET "Hello world!" here
router.get('/', (req, res) => res.send('Hello world!'))
// Import Auth routes
router.use('/api/auth', authRoutes)
module.exports = router;
We've moved our API Root GET request to this file to keep it organized with the other routes. We'll keep it now for testing,
In the second line of /src/routes/index.js
we require() our ./Auth.routes.js
file and store it as a const, authRoutes
. We haven't implemented that file yet either, so let's do that now.
Inside /src/routes/Auth.routes.js
file, add the following code:
/src/routes/Auth.routes.js
const router = require("express").Router()
// TODO - implement this route fully
// POST - /api/auth/register
router.post('/register', (req, res) => res.send("/register") )
module.exports = router;
This creates a POST
endpoint for /api/auth/register
which simply returns a string "/register" back to the requester.
With the boilerplate for our routing system mostly complete. This would be a good time to test everything is working before we continue much further.
Back in Postman, let's first test our "Hello world!" request to make sure that's still working from the new routes/index.js
file.
Make sure the local dev server is running with:
npm run dev
Then use Postman to make a GET
request to http://localhost:3000/
(In part 1 I promoted this to a variable {{BASE_URL}}
, I'll be referencing that moving forward)
You should see the following output:
$ npm run dev
> dern-backend@1.0.0 dev
> nodemon src/app.local.js
[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/app.local.js`
App is listening on port 3000.
GET / 200 1.582 ms - 12
You'll notice the output is the same as before, except the morgan middleware logged our GET
request. In Postman you should see the return value of "Hello world!"
Let's also test our /api/auth/register
endpoint is working. Create a new POST
request in Postman for that endpoint.
In Postman you should see "/register" as the response value, and the console should have logged the new POST
request:
$ npm run dev
...
POST /api/auth/register 200 2.381 ms - 9
The next step is to setup our Controllers, these are the C in MV*C*. To briefly explain the job of Controllers, they receive the HTTP request data from the application Router. The Controller
TODO - Explain this better
Add the following code to our /src/controllers/Auth.controller.js
file:
/src/controllers/Auth.controller.js
// Register New User
exports.register = async function(req, res) {
// req validation would be handled here
const newUserInput = req.body
// TODO - Auth Service Register User
res.json(newUserInput)
}
The controller is mostly a placeholder right now, but we're saving the request body into a const newUserInput
. However, we haven't implemented the express.json() middleware in order to be able to access the req.body object.
In /src/app.js
add this to lines 4 & 5
/src/app.js
// Using express.json() to read req.body
app.use(express.json())
(If you've previously used body-parser for Express.js this has essentially replaced that)
Next, update the /src/routes/Auth.routes.js
file to the following to send the request to our new Controller:
/src/routes/Auth.routes.js
const router = require("express").Router()
const authController = require("../controllers/Auth.controller")
// POST - /api/auth/register
router.post('/register', authController.register)
module.exports = router;
Since this is the first time in our application that we're dealing with request body data, this is a good opportunity to test that as well.
You should still have a POST {{BASE_URL}}/api/auth/register
request. Click on the "Body" tab for that request, and click the gray dropdown box that says "none". Change that value from "none" to "raw", then in the Blue Text dropdown that appears, select "JSON".
Set the body value to the following:
{
"username": "adam",
"password": "adamPass"
}
With all that set, run the request. In the console, you should see our POST
request logged. Additionally, the API response should just be the request body returned back to you.
With that working, we can now implement the Service Layer of our application. To briefly explain the job of the service layer, the service layer is where the bulk of our application's business logic exists. This is where we'll put our Dynamoose calls to perform CRUD operations, and handle logic for validating users' accounts, passwords, etc.
A major benefit of moving our business logic out of the controller (or even worse, the routes) and into a service layer, is that is makes our code much more modular and re-usable.
Let's take the Auth service we're about to implement for example. We want Users to be able to register for our app. We also want them to be able to login. However, wouldn't it be a nice feature, if after a User successfully registers for our app, they're automatically logged in.
If we were to keep all of that logic inside the controllers, we would have to copy/paste the login into the register controller as well. Not terrible at first, but it can quickly become a pain to maintain that duplicate code in two places, and goes directly against the DRY Principle (Don't Repeat Yourself).
Again, don't worry if that doesn't all make sense right now, we'll implement the service layer so you can see how it all works together.
We'll need two more packages for our Auth implementation. From the /backend
folder install the bcryptjs, and uuid packages with the following:
npm install bcryptjs uuid
We'll add the following AWS SDK configuration settings to /src/app.js
. Below app.use(express.json())
add the following:
...
// Dynamoose configuration
const dynamoose = require("dynamoose")
dynamoose.aws.sdk.config.update({"region": "us-east-1"});
Side Note: Regarding AWS Authentication & Configuration -
On my dev machine, I export the Access Key, Secret Key, and Session Token into my terminal, which allows my application to quickly interact with AWS Cli & SDK services without too much configuration. If you know how to do this and can follow along as such, great.
This is what you would type into a bash terminal to export those variables:
export AWS_ACCESS_KEY_ID="[ACCESS_KEY_ID]"
export AWS_SECRET_ACCESS_KEY="[ACCESS_KEY]"
export AWS_SESSION_TOKEN="[SESSION_TOKEN]"
Otherwise, for readers newer to AWS, I think it's probably simpler and more straight forward to configure that information in our app via code.
A major caveat of doing so is that our application will have to access sensitive information, ie our AWS ACCESS_KEY & SECRET_ACCESS_KEY. You should never hard-code sensitive information like keys & secrets into your application. Later on in this write-up, I install and configure dotenv so we can sign our JWTs with a secret.
You'll need to install with npm the dotenv
package. Then, update your app.js file to include dotenv and configure it, ideally as early as possible in your application.
// Dotenv config
const dotenv = require('dotenv');
dotenv.config();
dynamoose.aws.sdk.config.update({
"accessKeyId": process.env.AWS_ACCESS_KEY_ID
"secretAccessKey": process.env.AWS_SECRET_ACCESS_KEY,
"region": "us-east-1",
});
Don't forget, you'll need a .env
file in the /backend
folder with the following values:
AWS_ACCESS_KEY_ID=[YOUR ACCESS KEY]
AWS_SECRET_ACCESS_KEY=[YOUR SECRET KEY]
I still have to build out and test a working example for this, but check the github repo for pt. 2 to see the latest code examples if you're running into issues implementing this.
Then add the following to the /src/services/Auth.service.js
file:
/src/services/Auth.service.js
// Import our Dynamoose User model, Bcrypt for password hashing and uuidv4
const User = require("../models/User.model")
const bcrypt = require("bcryptjs")
const {v4: uuidv4} = require("uuid")
exports.registerUser = async function(newUserInfo) {
// newUserInfo is req.body - so it should be a JSON object ie {"username":"adam","password":"adamPass"}
// First, check is there's already a user registered with this username
var existingUser
try {
// Runs a DynamoDB scan and returns the result
existingUser = await User.scan({username: {eq: newUserInfo.username}}).exec()
} catch (err) {
console.log(err)
throw new Error(err)
}
// If there already is a User, throw an Error
if(existingUser.count > 0) {
throw new Error("EXISTING_USER_ERROR")
}
// User doesn't already exist, so let's register them
var newUser
try {
const uuid = uuidv4()
const salt = await bcrypt.genSalt(10)
const hashedPass = await bcrypt.hash(newUserInfo.password, salt)
newUser = await User.create({
"id": uuid,
"username": newUserInfo.username,
"password": hashedPass
})
} catch (err) {
console.log(err)
throw new Error(err)
}
// TODO loginUser(newUser) -> return JWT w/ newUser
return newUser
}
exports.loginUser = async function(userInfo) {
// userInfo should be a JSON Object {"username":"adam","password":"adamPass"}
// First, Check if the User even exists - In contrast to the above, in this case we do want there to be an existing User
var existingUser
try {
existingUser = await User.scan({username: {eq: userInfo.username}}).exec()
} catch (err) {
console.log(err)
throw new Error(err)
}
// If User doesn't exist, throw an error
if(existingUser.count == 0) {
throw new Error("INVALID_LOGIN_CREDENTIALS")
}
// Check if the supplied password matches the bcrypt hashed password saved in the User record
var validPass
try {
// bcyrpt.compare will return true / false depending on if the passwords match
// User.scan() always returns an array, hence why we specify existingUser[0].password below
validPass = await bcrypt.compare(userInfo.password, existingUser[0].password)
} catch (err) {
console.log(err)
throw new Error(err)
}
// If validPass is false, throw an error
if(!validPass) {
throw new Error("INVALID_LOGIN_CREDENTIALS")
}
// TODO - JWTs - We do need someway for our user to stay logged in after all
return {"message": "Login Successful"}
}
Update the /src/controllers/Auth.controller.js
file:
/src/controllers/Auth.controller.js
const authService = require('../services/Auth.service')
// Register New User
exports.register = async function(req, res) {
// req validation would be handled here - We're just assuming the request is properly formed
// fine for a proof-of-concept, terrible in practice
const newUserInput = req.body
var newUser
try {
newUser = await authService.registerUser(newUserInput)
} catch (err) {
console.log(err)
if(err.message == "EXISTING_USER_ERROR") {
return res.status("422").json({"message":"User already exists"})
// If you don't include the above return, the code will continue executing
// and hit the throw new Error("REGISTER_USER_ERROR") below, causing our app to crash
}
throw new Error(err)
}
res.json(newUser)
}
exports.login = async function(req, res) {
const userInput = req.body
var existingUser
try {
existingUser = await authService.loginUser(userInput)
} catch (err) {
console.log(err)
if(err.message == "INVALID_LOGIN_CREDENTIALS") {
return res.status("401").json({"message":"Invalid username or password"})
}
throw new Error(err)
}
res.json(existingUser)
}
Lastly, don't forget to add a /api/auth/login
endpoint to the /src/routes/Auth.routes.js
file, add this on lines 7 & 8 below the existing /api/auth/register
endpoint:
// POST - /api/auth/login
router.post('/login', authController.login)
This is the first substantial bit of code we've written, so let's take a moment to examine what everything does. Also, I've written this to use async/await as opposed to callbacks since I think it's cleaning and easier to understand. If you're not familiar with the syntax here's some documentation that might help clarify
Starting with the Auth.service.js
file, we imported our Dynamoose User model that we created earlier, we also imported bcrypt for hashing passwords, and uuidv4 to generate ids for our DynamoDB records.
Then, we created a function registerUser
which accepts a single Object, newUserInfo
, as a parameter. There's no type checking, or input validation implemented, but newUserInfo
should consist of a string username
and password
. Next in the registerUser function, we check if there is already a User registered with the supplied username, if there is we return a named error "EXISTING_USER_ERROR".
If a user doesn't already exist, we precede with User creation by generating a uuid, salting & hashing the new user's password, then finally using the User.create() method (which is part of Dynamoose) to store the new user as a record in our DynamoDB table.
Once that is complete we return the newUser Object in the response body with a default status code of 200.
You'll notice that above the return line, I left a TODO comment indicating where we'll eventually call the AuthService login function (in this case it's in the same file). We'll be adding in JWT for frontend auth soon, but I wanted to include that to illustrate the benefit of implementing a service layer.
For the loginUser function in our Auth Service, the code is very similar to the registerUser function, except instead of throwing an error if a user exists, we throw an error if the user doesn't exist.
We also use the bcrypt.compare function to see if the user supplied a valid password. Since Dynamoose.scan()
returns an array, in our case the existingUser variable, we have to specify existingUser[0].password
when supplying the hashed password to bcrypt, otherwise existingUser.password would be undefined.
In our Auth Controller file, /src/controllers/Auth.controller.js
, we imported our Auth Service file and saved it as a const authService
. We then updated, the Controller's register
function to make a call to the Auth Service's registerUser
function.
If the Auth Service call returns an "EXISTING_USER_ERROR" error to us, we send a 422 status and error message as a response. An important thing to note about Express is that it will continue to execute code even after a call to res.send()
, or res.json()
is made. That is why we include the return
statement immediately before res.status("422")...
is called. If we didn't have the return statement, Express would continue to the next line throw new Error(err)
and throw an error that would crash our app, even though we handled the error correctly.
Try removing the return
statement from that line and sending a couple test requests if you want to see how that works.
In the Auth Controller login
function, we make a call to the Auth Service loginUser
function, and same as with register, either handle the named error, or send the return value of the authService.loginUser()
call in the response.
The last thing we updated was to add the new login endpoint /api/auth/login
to Auth.routes.js
which should be pretty self-explanatory.
With all that new code added our app is starting to shape up. We currently have a way to register new users, and also a way to validate returning users accounts & passwords. The final piece missing, as I mentioned earlier is some sort of authentication token so our Express REST API can know when it's dealing with an authenticated user vs an unauthenticated one.
Quick aside on JWT's for API Authentication
Without trying to go into too much detail about JWTs (JSON Web Tokens) or REST API Authentication methods here, I want to briefly explain what it is we'll be doing to add JWTs to our app, and why I chose them.
Oftentimes, I feel that a lot of developers (especially in tutorials) will use JWTs just because it's latest shiny new JS toy, or because it's JS based Auth token and their writing a tutorial in JS.
While there tons more developers that choose JWTs (or different tokens) for the right reasons, I think it's beneficial to explain the pros & cons they offer and why I'm using it here.
JWTs are cryptographically signed using a secret key that (hopefully) only our app has access to. That means we can generate a JWT for our client, and when they send it back to us, we can verify wether or not the JWT was created by us.
That also means that we never have to make a call to the database, or even store our client's JWTs in a database, in order for them to be used.
This is both a pro and a con of JWTs. Assume for a minute that a hacker get's ahold of a client's JWT, they can now interact with our app as that compromised user. You might think that a simple solution is to just invalidate that JWT or add it to a denylist
, but remember, we don't have either of those.
The only way to invalidate that Token would be to change the secret key our app is signing JWTs with, which would affect every user and JWT.
Since our app is simple and more of a proof-of-concept right now, we're fine using JWTs as long as we're aware of the potential security concerns. Additionally, not having to make a database call to verify a user's authentication status will work well for our current application setup.
Let's go ahead and add JWT authentication into our app. Thanks to Danny Denenberg for a nice guide on simple JWT implementation in Express. We'll need to install two new packages, jsonwebtoken to read and create JWTs and dotenv to store our JWTs secret key in a .env file.
npm install jsonwebtoken dotenv
We are also going to create a new directory in our /src/
folder, called utils
to store our JWT related code. Inside the newly create /src/utils
directory. Create a file JWTauth.js
.
Finally, in the /backend
directory (aka the project root), create a new file .env
. Note, if you put your .env
file inside /src/
it won't work and you'll get undefined
when you try to acccess any env variables.
/backend/.env
JWT_SECRET=secret
(In a real app you wouldn't want to use "secret" as your JWT secret, you also wouldn't want to publish that anywhere, ie Github, etc.)
Update our /src/app.js
file to read our new .env file, add the following to lines 4, 5 & 6 of app.js
/src/app.js
// Dotenv config
const dotenv = require('dotenv');
dotenv.config();
Add the following code to the new /src/utils/JWTAuth.js
file:
/src/utils/JWTAuth.js
const jwt = require('jsonwebtoken')
exports.generateAccessToken = function (username) {
return jwt.sign({uid: username}, process.env.JWT_SECRET, {expiresIn: "2h"})
}
exports.authenticateToken = function (req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if(token == null) {
return res.sendStatus(401)
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if(err) {
console.log(err)
return res.status(403)
}
req.user = user
next()
})
}
Finally, let's update our Register User and Login User functions in the Auth Service to generate JWTs for authenticated users.
Add this on line 5 of /src/services/Auth.service.js
, it come immediately after the previous require()
imports.
/src/services/Auth.services.js
const jwtAuth = require('../utils/JWTauth')
Now, we can call the jwtAuth.generateAccessToken()
function inside our Service Layer to get a valid JWT for our client.
First, we'll update the loginUser
function in Auth Service to generate our JWT.
Update the final 3 lines in the loginUser function, this should start with our placeholder comment // TODO - JWTs....
, you can remove that comment now.
/src/services/Auth.services.js - loginUser()
...
var authToken = await jwtAuth.generateAccessToken(existingUser[0].username)
return {token: authToken}
Additionally, update the final 3 lines of our registerUser function in the Auth Service to make a call to loginUser.
/src/services/Auth.service.js - regiserUser()
...
var authToken = await exports.loginUser({"username": newUser.username, "password": newUserInfo.password})
return authToken
With that code added, we can now successfully register users, then log them in and return a valid JWT. Existing users can also login with a valid username / password combination, and recieve a new valid JWT.
We've come along way to building the Auth component of our app and we're almost done. The final step is to add a new protected route
that will implement our authenticateToken()
middleware function we defined in the JWTauth.js
file.
Open up /src/routes/Auth.routes.js
and update it so that is looks like the following:
/src/routes/Auth.routes.js
const router = require("express").Router()
const authController = require("../controllers/Auth.controller")
const jwtAuth = require('../utils/JWTauth')
// POST - /api/auth/register
router.post('/register', authController.register)
// POST - /api/auth/login
router.post('/login', authController.login)
// PROTECTED ROUTE - ALL /api/auth/protected
router.all('/protected', jwtAuth.authenticateToken, authController.protected)
module.exports = router;
You'll notice that we added a new ALL
(this just means it will accept any valid HTTP request) endpoint at /api/auth/protected
, and added two functions after the route declaration. The first function is our jwtAuth.authenticateToken
which acts as middleware. That means that any request sent to the /api/auth/protected
endpoint will first be sent to jwtAuth.authenticateToken
before being sent to authController.protected
. We haven't implemented the protected
function in our authController
so let's do that now.
Add the following code to the end of our Auth Controller:
/src/controllers/Auth.controller.js
...
exports.protected = async function(req, res) {
console.log("Reached Protected Route")
res.send("/protected")
}
We should now be able to create a new user, receive a valid JWT, and use that JWT to authenticate and reach our protected endpoint.
Let's start by confirming the endpoint is inaccessible to unauthenticated users.
Back in Postman, create a new request to the endpoint /api/auth/protected
. Since we used the router.all() for this endpoint you can make the request a GET
or a POST
or whatever else you'd like.
Send the request through, and you should see a response "Unauthorized" with status code 401.
Next, let's test registering a new user, which will in turn test the login function, by updating the body of our POST
/api/auth/register
request to the following:
(since our app checks the username field for existing users, we're updating that here.)
{
"username": "adam2",
"password": "adamPass"
}
After sending that request through, you should get response similar to the following:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJhZGFtMiIsImlhdCI6MTY0NjI0MTI5MiwiZXhwIjoxNjQ2MjQ4NDkyfQ.-vaQXL5KzgeJ4Wer_sdMa-hWmovCezCRxXevEZvurfE"
}
If you want to examine the JWT, head on over to JWT.io and copy and paste the token value into the editor. Since the secret this token was generated with is just "secret", again that's a TERRIBLE IDEA in production, you should be able to verify the token as well.
With our newly created JWT, let's copy the value, ie just this part:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJhZGFtMiIsImlhdCI6MTY0NjI0MTI5MiwiZXhwIjoxNjQ2MjQ4NDkyfQ.-vaQXL5KzgeJ4Wer_sdMa-hWmovCezCRxXevEZvurfE
And then add it to our Postman /api/auth/protected
request in the authorization
header. One thing to note about working with JWTs in Auth headers, is that the token itself is usually prefixed by the term "Bearer". So in Postman >> Headers >> type in "Authorization" for the header name then add the following for the value:
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJhZGFtMiIsImlhdCI6MTY0NjI0MTI5MiwiZXhwIjoxNjQ2MjQ4NDkyfQ.-vaQXL5KzgeJ4Wer_sdMa-hWmovCezCRxXevEZvurfE
With that header added, resend the request. If everything goes well, instead of the "Unauthorized" response, you should now see a response body "/protected" which is what we returned in our authController.protected
function. You'll also notice we should have console logged the line "Reached Protected Route" to our dev console. I added this to demonstrate that the jwtAuth.authenticateToken
stops further code execution in the case of unauthorized users.
And with that, we have now implemented a Auth system, albeit a simple one, for our application. Since we covered so much ground in this section, I think this would be a good place to take a pause. In the next section, we'll start back up with deploying our newly updated app onto AWS, and test out any issues that might occur in the cloud that we're not running into on our local dev machine.
I also decided on a new name for our Todo App, "git-er-dern", which has a 2:3 pun to word ratio. Quite impressive in my humble opinion.
Posted on March 2, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.