Time to secure API in Javascript
Today you will learn how to build an API using Javascript (Node + Express) that is going to be secure. What we will cover is more about the techniques, they are easily applicable in other languages as well. Let’s start!
Introduction to HTTP and API
HTTP protocol is stateless which means connection is closed after finishing any request and no state is preserved. In other words, everything we need to say needs to be send in each and every request. Same rules apply to API built over the HTTP. Such an API can be called REST API, or RESTful. The difference between the two is subtle, where REST is a set or rules applicable to any protocol, and RESTful is just a specific application in the HTTP.
Securing the API
Building a secure API is a bit more flexible and easier than securing a giant application, or a set of applications. We have 2 options basically. We can build an own solution, or we may use an existing one, based on our needs. If we need OpenID support, MFA (Multi-Factor Authentication), SSO (Single Sign-On), and lot of more than just basic authentication or authorization, we might want to check existing solution like Auth0 or Okta.
We will do the easier scenario here. We will have a simple API that we want to secure.
Meaning of secure
What having a secure API means, is to basically restrict the access to it. It can be:
- restriction to access, modify or delete data by a specified user only (ie. moderator or admin), or,
- control of the integrity (ie. only I should be able to update my comment and no one else), or,
- any kind of rules we wish to apply.
A secure API is the one were those actions are strictly controlled, and secure, whereas a non-secured API can cause leak of information or giving control to someone, who shouldn’t be having.
Replacing passwords
Usually when we are restricted to access something we supply our email and password, we login. Similar thing is happening in the API world but there’s an important difference. We don’t want to send our password over and over again in every request. Remember, HTTP is stateless so we have to send something that identifies us in every request we do.
What we can do instead is to authenticate just once, using the password we have, and use something else in the subsequent requests. Something that identifies us as efficiently as our password, but being more secure. The answer are tokens.
Using secure tokens
Tokens are a secure way to identify the user making the request. It is basically a special kind of string that is used in the request. Let’s see how is token more secure:
- It contains information that can not be modified by an unknown party. Password is not exposed nor included.
- It can have an expiration date, which makes it impossible to use later.
- It can be invalidated at any time by changing the application secret, more on that later.
Tokens are given to users after they successfully authenticate. It is then transmitted (attached in the request headers) instead of a password.
Tokens can include information of our choice, like the user name, his role (ie. moderator) and other to save us from touching the database, as example. But be careful, tokens should not contain any sensitive information, meaning no password, phone number or address.
We are going to use a special kind of tokens in our case. They are called JWT tokens, more on that next.
JWT tokens
JWT (JSON Web Tokens) tokens are a special (Base64 encoded) string. An example:
// JWT example:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhcmFAaGFua2VyLmNvbSJ9.lguq6B2cVsqayjwv5z4nolQ4GAa7ZsxEFcCqloISKhQ
In the token above we might spot two dots, meaning it can be split into 3 parts. Dot is a special character that is safe to use as a separator. Reason is explained next.
Base64 characters table consists of 64 characters which are [A-Z, a-z, 0-9, +, and /] and dot is not among them. And so dot will not interfere with the message, there will be always two dots and no more.
JWT token is after it is decoded composed of 3 parts and yes, it is a JSON as mentioned earlier. These 3 parts are: header, payload, and a verifying signature.
JWT tokens can be signed (the example above), encrypted, or both (signed first, then encrypted). Encryption can by symmetric or asymmetric. If we don’t include any sensitive information in the token, which is the preferred way of use, signing is enough. This is the style we will follow.
A Base64 string can be decoded by any and JWT token is no exception. If we decode the token above, we get a header:
{
"alg": "HS256",
"typ": "JWT"
}
And we also get the payload:
{
"email": "sara@hanker.com"
}
You might think if we can disclose it so easily, we might be able to change it easily as well; to pretend we are someone else. Fortunately, this is not doable.
The last piece, a verifying signature, is used to protect us against fake tokens. Every token received is verified and the signature is then compared with the received verifying signature. If those signatures differ, the token was malformed. This token will then be rejected, and user (impersonated) will not be able to perform the action.
To create and verify the signature we use an application secret, a key usually set by an environment variable with a name of our choice, ie. SECRET. This SECRET needs to stay private and only administrator should know it. In case the SECRET gets compromised, we need to change it. By doing so we also invalidate all the tokens out there.
Now when we have the necessary information and an idea how securing might be done, let’s head on, and create an example API, and let’s secure it using JWT tokens.
Example application
We will build a simple API that will represent our new venture, “An online book store”. Our focus will be securing the API using the JWT tokens.
Let’s discuss the API functionality first. In the API, we will work with the books. We have decided anyone can see the books, or a book detail.
Next, any authenticated user can submit a book review and let’s say, this will be sole role to the users only and no moderators or admins should be able to interfere in posting reviews.
As we unveiled, besides unauthenticated users we will have 3 user roles who needs to authenticate: user, moderator, and admin, in this particular order. We will use JWT tokens to verify their identities.
Lastly, moderator will be able to add a new book, what can be also done by a higher role like admin. Admin will have special ability to change user’s role, like promoting user to moderator. It should not be able to devote other admin.
And that’s it! We have now defined the application we are going to implement. What we will do along the way is storing all the data in the memory, to keep things simple, to focus on prototyping, and to focus on what’s important here, securing the API.
Creating the project
As usual, initial step is to create the project. We choose a simple descriptive name, books-api.
$ mkdir books-api
$ cd books-api
Next, we create a package.json
file:
{
"name": "books-api",
"version": "1.0.0",
"dependencies": {
"body-parser": "1.18.3",
"express": "4.16.3",
"jsonwebtoken": "8.3.0"
}
}
Then, we install the listed dependencies:
$ npm install
In the time of writing, we’ll be using Node 10.10.0.
We can now move forward to define an essential part of our API, and that’s data.
Defining data
As working with books, it comes to no surprise we start by defining sample books, and put them into the ./data/memory/books.js
file:
const books = [
{
title: 'Hamlet',
slug: 'hamlet',
author: 'William Shakespeare',
summary: {},
reviews: []
},
{
title: 'The Adventures of Huckleberry Finn',
slug: 'the-adventures-of-huckleberry-finn',
author: 'Mark Twain',
summary: {},
reviews: []
},
{
title: 'Great Expectations',
slug: 'great-expectations',
author: 'Charles Dickens',
summary: {
ratings: 1,
averageRating: 4
},
reviews: [
{
rating: 4,
title: 'Great read!',
text: 'I was surprised by how many ...',
reviewer: 'Joe'
}
]
}
]
module.exports = books
Every book contains basic information including slug that will help to retrieve the book in a friendly url. Books can also receive reviews. By adding a new review, we expect summary to be updated.
Now, let’s define the user roles. A good location will be ./data/memory/roles.js
:
// Every role has a unique number
// Higher role = Higher number
const roles = {
user: 1,
moderator: 2,
admin: 9
}
module.exports = roles
Every role will be represented by a number. We can then reference those numbers at any place we need. Keep in mind we need to make role names unique. Numbers needs to be unique too.
Let’s move on to define sample users, and put them in ./data/memory/users.js
:
const roles = require('./roles')
const users = [
{
name: 'Frank',
email: 'frank@books.com',
password: '0RE0jZ25HVXBCsRTC/J+kQ==.dExF9fx3g2o/jnYBGisdeUjGAHd8sOenG/1U65fqonM=', // ireadbooks
role: roles.user
},
{
name: 'Joe',
email: 'joe@books.com',
password: '0/2OQNbV06VEiYuF8F8JEw==.62X7Zo96iEiqiINBFQJAP9Ve8hU8xEsK8LxaSmNJleA=', // helloiamjoe
role: roles.moderator,
},
{
name: 'Emily',
email: 'emily@books.com',
password: 'PElk7dv589VtfnIH8YoZ9A==.GSrjuulRGkn7MfToYhpkdYjRR78yQuPvGo9Zq2R1dQY=', // candoalmostanything
role: roles.admin
}
]
module.exports = users
Interesting to see how we assign roles by reference, specified in the file created earlier. Passwords are properly unreadable and unrecoverable; something we will cover later.
Now when we have our data defined we step forward to prepare objects to manipulate data easily, we call those objects DAO.
DAO objects
We now create objects that will handle all manipulation with the data. They carry a name DAO (Data access object).
DAO will also help API to stay lean. We start by managing the books, more in ./data/booksDao.js
:
const books = require('./memory/books')
const { round, slugize } = require('./helpers')
const getAllBooks = () => books
const getBookBySlug = slug => {
let foundBook = books.find(
book => book.slug === slug
)
return foundBook
}
const reviewBook = (slug, { rating, title, text }) => {
let reviewedBook = getBookBySlug(slug)
if (!reviewedBook) { return }
reviewedBook.reviews.push({ rating, title, text })
reviewedBook.summary = updateSummary(reviewedBook)
return reviewedBook
}
const getAllRatings = book => {
const ratings = book.reviews.map(
review => review.rating
)
return ratings
}
const updateSummary = book => {
const ratings = getAllRatings(book)
const sum = ratings.reduce((a, b) => a + b, 0)
return {
ratings: ratings.length,
averageRating: round(sum / ratings.length)
}
}
const addBook = ({ title, author }) => {
let bookToAdd = {
title, author, slug: slugize(title),
summary: {},
reviews: []
}
books.push(bookToAdd)
return bookToAdd
}
module.exports = {
getAllBooks,
getBookBySlug,
reviewBook,
addBook
}
This DAO will help up to do all the book-related operations we required when designing the API in beginning. We can see there is also a use of helpers that we going to define right next, in ./data/helpers.js
:
const round = value =>
Math.round(value * 100) / 100
const slugize = value =>
value.toLowerCase().split(' ').join('-')
module.exports = {
round,
slugize
}
Now we can continue to manage the users, more in ./data/usersDao.js
:
const users = require('./memory/users')
const roles = require('./memory/roles')
const getUserByEmail = email => {
let foundUser = users.find(
user => user.email === email
)
return foundUser
}
const setUserRole = (email, roleName) => {
let newRole = roles[roleName]
if (!newRole) { return }
let user = getUserByEmail(email)
if (!user) { return }
if (user.role === roles.admin) { return }
user.role = newRole
return user
}
module.exports = {
getUserByEmail,
setUserRole
}
All the data manipulation is now complete. It is separated and will help API to stay clean. What we need to do next is to build filtering and validation around the data that can be used by the API as well.
Filters
We can see we are returning user by the usersDao. This is all fine. But there are some situations like sending user back to the client, or storing (encoding) the user in the token (in payload), where we need to strip sensitive information.
To cover the described situations we are making a place for filters in ./data/filters.js
:
const filterUser = user => {
const { name, role } = user || {}
return { name, role }
}
module.exports = {
filterUser
}
Our filter will take the user and return only safe attributes, name and role. Sensitive password and email will be dropped.
Filters are helping us on information that are coming out. Now we cover validation that controls the data coming in.
Validation
Validation is used to define rules that should be met before storing the record to make sure it is having the right structure.
In our case we need to check on new review, new book, or role change. This is covered in ./data/validation.js
:
const roles = require('./memory/roles')
const isNumber = (value) =>
Number.isInteger(value)
const isInRange = (value, from, to) =>
value >= from && value <= to
const isNonEmptyString = (value) =>
typeof value === 'string' && value.length > 0
const validateReview = review => {
const ratingIsNumber = isNumber(review.rating)
const ratingIsInRange = isInRange(review.rating, 1, 5)
const havingTitle = isNonEmptyString(review.title)
const havingText = isNonEmptyString(review.text)
if (!ratingIsNumber) { throw 'rating must be a number!' }
if (!ratingIsInRange) { throw 'rating must be in range of 1 and 5!' }
if (!review.title) { throw 'title is missing!' }
if (!review.text) { throw 'text is missing!' }
return true
}
const validateBook = book => {
const havingTitle = isNonEmptyString(book.title)
const havingAuthor = isNonEmptyString(book.author)
if (!havingTitle) { throw 'title is missing!' }
if (!havingAuthor) { throw 'author is missing!' }
return true
}
const validateRoleChange = (email, role) => {
const havingEmail = isNonEmptyString(email)
const havingRole = isNonEmptyString(role)
const roleExists = role in roles
if (!havingEmail) throw 'email is missing!'
if (!havingRole) throw 'role is missing!'
if (!roleExists) throw 'role is invalid!'
return true
}
module.exports = {
validateReview,
validateBook,
validateRoleChange
}
When creating a new review, its rating should be numeric and between 1 and 5. New book should be having title and author. These are just some of the rules we have defined.
Checking on data
By now you should be having all work done in the data folder. It should look as follows:
+ data
+ memory
books.js
roles.js
users.js
booksDao.js
filters.js
helpers.js
usersDao.js
validation.js
Data is done. Now we cover areas on users, passwords, authentication and authorization. In the final, putting things together to build the API will be very straightforward.
Managing users
Building a robust application can take a lot of effort to manage the users properly. If this is the case, we can safe us a lot of time by using an existing solution, like already mentioned Auth0 or Okta.
In our case and for learning purposes too, we store users in the memory. It is something that can be easily replaced by a database at any given point. Despite the storage, one is certain, plain passwords must not enter the database. This is something we look on next.
Hashed password
Passwords need to be stored in non-readable and non-recoverable way. Passwords need to be stored hashed.
This is how we store passwords:
salt.passwordHash
If we look at Frank’s password:
0RE0jZ25HVXBCsRTC/J+kQ==.dExF9fx3g2o/jnYBGisdeUjGAHd8sOenG/1U65fqonM=
The password is Base64 encoded string and again, we can see a dot that serves as a separator.
Before the dot we have the salt. Salt is usually generated and unique for every user. It is then used to hash the password. In this way even the same passwords will result in a different hash. Salt makes it much harder to succeed in a brute-force attack.
After the dot we spot the hashed password. Hashed using the salt as mentioned. To compute the hash, we use a hash function.
Hash function
It is a one-way function that takes the input (ie. password) and produces irreversible result. No way back of knowing the original password.
A classic hash function is SHA 256. It is fast and good for checksums. But its speed can also become a vulnerability in cracking the passwords.
Attacker may perform a brute-force attack using password dictionary to try all common passwords for each user in hunt for real password. Rate of attacker’s success is defined by his computational power. All he needs is TIME. But there is something we can do about it.
Several password-based hash functions were found to stop the attacker. Common ones are bcrypt, scrypt, or PBKDF2. Difference is they are VERY SLOW. Hashing is happening in rounds, in thousands of loops, making the brute-force attack ineffective.
In our case we are going to stick to SHA 256 as of learning purposes, using crypto module that comes with Node.
Checking the password
Only way to confirm the provided password is correct is to hash it using the same salt as the hashed password and then compare the hashes. If the match, password is correct.
Password management will be saved at ./auth/pwd.js
:
const crypto = require('crypto')
const genSalt = () => {
return crypto.randomBytes(16).toString('base64')
}
const hash = (password, salt) => {
const passwordHash = crypto.createHmac('sha256', salt)
.update(password)
.digest('base64')
return salt + '.' + passwordHash
}
const compare = (password, passwordHash) => {
const salt = passwordHash.split('.')[0]
return hash(password, salt) === passwordHash
}
module.exports = {
genSalt,
hash,
compare
}
Generated salt is here random 16 bytes encoded to Base64. Salt and hashed password are separated by a famous dot.
Proving the password is correct is part user authentication. First, we cover the difference between authentication and authorization.
Authentication v Authorization
Authentication is about checking the user who he is, his identity (ie. Frank and not Joe). This can be done by receiving user’s email and password, common credentials.
Authorization is about checking the user what he can or cannot do (ie. can review a book). In our scenario it is done by checking the user’s role. In general it is not limited and may be any rules we set. It could be the role, gender, age, badges, stats, and so on.
Authenticating the user
As described earlier need to check user by email and password. This is done in ./auth/authenticate.js
:
const usersDao = require('../data/usersDao')
const pwd = require('./pwd')
const authenticate = (email, password) => {
const user = usersDao.getUserByEmail(email)
if (!user) { return }
const match = pwd.compare(password, user.password)
if (!match) { return }
return user
}
module.exports = authenticate
User is found by his email, his plain password is then compared to the hashed one. Keep in mind, password is transmitted in plain format and it is advised to use HTTPS.
Now we can move to authorization.
Authorizing the user
User rights will be determined by his role. We defined 3 roles: user, moderator, and admin. By default any higher role (higher number) is allowed to do what lower roles can (lower number). As example, at least moderator is needed to add a book, and as admin meets that criteria, he can add a book too.
On top of that we will provide option exclusive. It will override the default behavior and lock access to a specific role only.
Authorization is done in ./auth/authorize.js
:
const authorize = (userRole, requiredRole, exclusive) =>
exclusive
? userRole === requiredRole
: userRole >= requiredRole
module.exports = authorize
Ground to authorize the user is prepared now. Need to be authenticate first is expected. It is time to look more at tokens.
Issuing tokens
If user provided valid credentials, he can be successfully authenticated and a new token we be returned to him. We say the token will be issued.
Issued token is then put in the request headers to make an authenticated request. Used header is:
Authorization: Bearer <token>
It is necessary to check every received token for its validity. Any invalid token gets rejected. If user provided wrong credentials in the first place, no token will be issued. Both ways shut the door and stops the user to access the restricted.
Implementing tokens
All we need to do around tokens like issuing the token, decoding it, and verifying it, was already implemented. Decided to use jsonwebtoken that we listed in our dependencies.
To make it easier we put token control in one place, in ./auth/token.js
:
const jwt = require('jsonwebtoken')
const SECRET = process.env.SECRET
const issue = payload =>
jwt.sign(payload, SECRET)
const verify = token =>
jwt.verify(token, SECRET)
const read = header => {
if (!header) { return }
const parts = header.split(' ')
const isBearer = parts[0] === 'Bearer'
const token = parts[1]
return isBearer && token
}
module.exports = {
issue,
verify,
read
}
SECRET which can be seen is necessary to issue (sign) and verify the token. It is used to build a verifying signature.
Checking on auth
We have done tremendous work. All our data is ready and now even auth is ready:
+ auth
authenticate.js
authorize.js
pwd.js
token.js
It is time to put all parts together into the API we wanted.
The API
Composing our API is a smooth step. All crucial work has been done and now it’s about putting the pieces together. We begin by conceiving the responses our API might deliver.
Common responses
All our API responses will be in JSON. What will differ is their content and status code. Status code is represented by a 3-digit and is used to indicate the type of response.
To avoid duplicity and handle common responses easily, we define them in ./responses/index.js
:
const ok = (res, obj) =>
res.json(obj)
const created = (res, obj) =>
res.status(201).json(obj)
const wrongRequest = (res, message) =>
res.status(400).json({ message })
const unauthorized = res =>
res.status(401).json()
const notFound = res =>
res.status(404).json()
module.exports = {
ok,
created,
wrongRequest,
unauthorized,
notFound
}
401 Bad Request will be returned any time we miss to include required field, or include field with unacceptable value.
401 Unauthorized will be returned if user provides wrong credentials, doesn’t include the token when needed, or includes token that is fake.
It is time to handle the responses.
API handlers
As the name implies, handlers are functions to handle something. In case of Express, they handle the request and provide the response. In example, request can be asking for all books, while the response is a returned list of books in JSON.
Every handler in Express is a potential middleware. In a more general sense it is a type of handler that doesn’t have to return the response necessarily. It can intercept on top of other handlers. A typical use is logging, authentication, and authorization.
Restricting the access
Authentication will check the user’s credentials and return the token if successful. Authorization will read the token from the headers, verify it, decompose the user’s role from the token, check if the role is sufficient, and if yes, it will let the request to continue to the resource. All this is done in ./handlers/auth.js
:
const authenticate = require('../auth/authenticate')
const authorize = require('../auth/authorize')
const { unauthorized, ok } = require('../responses')
const { issue, verify, read } = require('../auth/token')
const { filterUser } = require('../data/filters')
const token = (req, res, next) => {
const { email, password } = req.body
const user = authenticate(email, password)
if (!user) { return unauthorized(res) }
const payload = filterUser(user)
const token = issue(payload)
return ok(res, { token, payload })
}
const restrict = (requiredRole, exclusive) => (req, res, next) => {
const token = read(req.headers.authorization)
if (!token) { return unauthorized(res) }
const decoded = verify(token)
if (!decoded) { return unauthorized(res) }
const isAuthorzied = authorize(decoded.role, requiredRole, exclusive)
if (!isAuthorzied) { return unauthorized(res) }
req.user = decoded
next()
}
module.exports = {
token,
restrict
}
It is worth mentioning we are filtering user’s sensitives before encoding to the token. Also, you can see 401 is heavily used to cover all negative outcomes.
Handling books
All functionality we require on books is already made by our DAO. What we need now is to make appropriate handlers to it.
const booksDao = require('../data/booksDao')
const { badRequest, notFound, created } = require('../responses')
const { validateReview, validateBook } = require('../data/validation')
const getAllBooks = (req, res) => {
const books = booksDao.getAllBooks()
res.json(books)
}
const getBookBySlug = (req, res) => {
const slug = req.params.slug
const book = booksDao.getBookBySlug(slug)
res.json(book)
}
const reviewBook = (req, res) => {
const slug = req.params.slug
const reviewer = req.user.name
const { rating, title, text } = req.body
const review = {
rating, title, text, reviewer
}
try { validateReview(review) }
catch (e) { return badRequest(res, e) }
const reviewedBook = booksDao.reviewBook(slug, review)
if (!reviewedBook) { return notFound(res) }
return created(res, reviewedBook)
}
const addBook = (req, res) => {
const { title, author } = req.body
const bookToAdd = { title, author }
try { validateBook(bookToAdd) }
catch (e) { return badRequest(res, e) }
const addedBook = booksDao.addBook(bookToAdd)
return created(res, addedBook)
}
module.exports = {
getAllBooks,
getBookBySlug,
reviewBook,
addBook
}
Those are just handlers. They are not aware of any required authentication or authorization, and that’s how it should be.
As we already know handlers (middlewares) can be put one top of the other, they can be chained. That is something we will see in the ending.
Handling users
Handling users is similar to handling books in a way it is not aware of authentication or authorization either. Our only requirement on users is to be able to change users role, to promote or devote the user. See ./handlers/users.js
:
const usersDao = require('../data/usersDao')
const { badRequest, ok } = require('../responses')
const { validateRoleChange } = require('../data/validation')
const { filterUser } = require('../data/filters')
const setUserRole = (req, res) => {
const { email, role } = req.body
try { validateRoleChange(email, role) }
catch (e) { return badRequest(res, e) }
const user = usersDao.setUserRole(email, role)
return ok(res, filterUser(user))
}
module.exports = {
setUserRole
}
We can see we are validating the received input, in this case for presence of an email, and a role. We also check if the role exists, otherwise it should not be able to assign it. As we said in the beginning, only admin should be allowed to perform this action.
Accessing handlers
To make all handlers easily accessible we make a file that puts them together, ./handlers/index.js
:
const auth = require('./auth')
const books = require('./books')
const users = require('./users')
module.exports = {
auth,
books,
users
}
Checking on handlers
At this point we should be having handlers in a following hierarchy:
+ handlers
auth.js
books.js
index.js
users.js
It is time to do the very last step now.
Entry-point
In this very moment we are ready to prepare the server. It means we define all the endpoints, and point them to desired handlers. We also specify which endpoints should be restricted, and who can access them.
All is done in the ./index.js
:
const express = require('express')
const bodyParser = require('body-parser')
const roles = require('./data/memory/roles')
const { auth, books, users } = require('./handlers')
const app = express()
const port = 3000
app.use(bodyParser.json())
// Issues the token
app.post('/token', auth.token)
// All books, Book by a slug
app.get('/books', books.getAllBooks)
app.get('/books/:slug', books.getBookBySlug)
// Add a book review
app.post('/books/:slug/review',
auth.restrict(roles.user, true),
books.reviewBook
)
// Add a book
app.post('/books',
auth.restrict(roles.moderator),
books.addBook
)
// Set user's role
app.patch('/role',
auth.restrict(roles.admin),
users.setUserRole
)
// Start the server
app.listen(port, () => {
console.log(`Books API running on port ${port}!`)
})
If you are delighted how easy it is to read, like a book, it is because we followed good SOC (Separation of concerns) along the way.
Starting the server
Our server (API) can be run with an easy command:
$ SECRET="overTheMountains42" node index.js
If all works, a message should be shown to the console:
# Books API running on port 3000!
If so is true, congratulations!
During the development we might prefer nodemon to start the server, then any file changes will cause server to restart automatically.
Now, it is time to play.
Testing the API
In this very last step we prove the API. It involves making the requests, and checking the responses.
Restricted endpoints should be tested too, to see they accept only desired role(s). Promoting or devoting user (by admin) should translate to his access, once he requests new token.
To test the API we can use Postman, Insomnia, or curl. Insomnia is a good option that you may use.
Getting all books can look like this:
curl --request GET \
--url 'http://localhost:3000/books' \
--header 'content-type: application/json'
To get the token for Frank so he can review a book:
curl --request POST \
--url http://localhost:3000/token \
--header 'content-type: application/json' \
--data '{
"email": "frank@books.com",
"password": "ireadbooks"
}
'
To let Frank to review the book:
curl --request POST \
--url http://localhost:3000/books/hamlet/review \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiRnJhbmsiLCJyb2xlIjoxLCJpYXQiOjE1Mzc0NTkzNzN9._wlKlRGb-qoB-R-TFmNSxbOJ_fozYQ_xZ7Q89eNppGQ' \
--header 'content-type: application/json' \
--data '{
"rating": 5,
"title": "Good book!",
"text": "This was a very good read ..."
}'
Cover remaining scenarios as an enjoyable exercise. In Example application section you can see the described functionality, and back in Entry-point is the list of all endpoints.
Conclusion
We have successfully built a fully functional API that is having data stored in the memory. Used DAO objects to easily manipulate the data, making it also easier to change the data layer at any point.
What we focused on and learned to do in the first place is to secure the API using tokens, to restrict the access, and to add support for user groups (roles).
We chose to build a sample API to practice all we need. As a good example we chose a books venture. Some endpoints were public, some restricted.
We have covered user authentication, most importantly issuing the token. Token was then sent in the headers of each request. We have also covered authorization that limited the access based on the role.
And that would be all for today. I hope you enjoyed reading the article as I have enjoyed writing it. In case you see any places that could be improved, any places that could be described more or less, please let me know and we will make it better together.
— Pavel.