Pasang "Pengaman" di REST API

Kuy

Kemarin uda buat REST API sederhana tapi itu belum pake "pengaman" dan MVC (Model View Controller).

Pengaman yang di maksud adalah authentication dan authorization.

Kasarnya authentication misalnya klo kita mau buat article kan perlu tau siapa yang buat (kayak pengenal gitu lah), klo authorization misalnya klo kita mau ngelakuin action delete atau update article, apakah article yang mau di delete atau update ini punya kita atau bukan.

Contoh analogi deh, anggap atau authentication dan authorization ini kayak satpam.

Misalnya kita siswa atau mahasiswa nih, ke sekolah atau kampus kan harus make seragam sekolah atau almamater kampus biar bisa masuk ke sekolah atau kampus (tanda pengenal klo kita berpendidikan di tempat tersebut) ini adalah authentication.

Misal kita lagi ada kelas Computer Science, tapi kita malah masuk ke kelas Data Structure (nah authorization disini nih) dia(authorization) akan cek kita ini dapet kelas Data Structure atau bukan, klo bukan boleh masuk atau sebaliknya.

Sekarang gue mau buat yang MVC + pasang pengaman pake JWT(JSON Web Token) dan BcryptJS di REST API

Setup Server dan Database

Pertama-tama buat structure foldernya kayak gini dulu

npm init -y

npm i express mongoose

app.js

const express = require('express')
const mongoose = require('mongoose')

const PORT = process.env.PORT || 3000

const app = express()

mongoose
  .connect('mongodb://localhost:27017/rest-pengaman', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(_ => console.log('Connected to DB'))
  .catch(err => console.log(err))

app.use(express.json())
app.use(express.urlencoded({ extended: false }))

app.listen(PORT, _ => console.log(`Server runs on PORT ${PORT}`))

Jalanin nodemon app.js
Btw, gw install nodemon di global jadi pas npm i blahblahblah itu ga perlu install nodemon lagi, klo kalian ga install di global bisa install atau langsung dengan jalanin command npm i -D nodemon

Pastiin dapet log kayak gini

Articles

Kemarin kita uda buat CRUD Article, tapi kali ini kita akan buat dengan konsep MVC(Mode View Controller)

Pertama, buat modelnya dulu ye

Article Model

models/article.js

const { model, Schema } = require('mongoose')

const articleSchema = new Schema(
  {
    title: String,
    author: String,
    content: String,
  },
  { timestamps: true, versionKey: false },
)

const Article = model('Article', articleSchema)

module.exports = Article

Article Controller

controllers/articlesController.js

const Article = require('../models/article')

module.exports = {
  async index(req, res) {
    const articles = await Article.find()
    res.status(200).json(articles)
  },
  async create(req, res) {
    const { title, author, content } = req.body

    const article = await Article.create({ title, author, content })
    res.status(201).json(article)
  },
  async findOne(req, res) {
    const article = await Article.findById(req.params.articleId)

    res.status(200).json(article)
  },
  async delete(req, res) {
    const article = await Article.deleteOne({ _id: req.params.articleId })

    res.status(200).json(article)
  },
  async update(req, res) {
    const { title, author, content } = req.body

    const article = await Article.updateOne(
      { _id: req.params.articleId },
      {
        title,
        author,
        content,
      },
    )

    res.status(200).json(article)
  },
}

Article Routes

articles/articlesRoute.js

const router = require('express').Router()

const articlesController = require('../controllers/articlesController')

router.get('/', articlesController.index)
router.post('/', articlesController.create)
router.get('/:articleId', articlesController.findOne)
router.delete('/:articleId/delete', articlesController.delete)
router.put('/:articleId/update', articlesController.update)

module.exports = router

Kita lempar callback ke controller (params kedua), jadi di controller nerima req, resnya

articles/index.js

const router = require('express').Router()
const articleRoute = require('./articlesRoute')

router.use('/articles', articleRoute)

module.exports = router

Pake middleware => router.use()

CRUD Article kelar deh (hopefully)


Users

Karena user punya data credential (password), maka kita harus hash passwordnya agar aman

npm i bcryptjs

helpers/bcrypt.js

const bcrypt = require('bcryptjs')

const secret = bcrypt.genSaltSync(10)

const hashPassword = password => {
  return bcrypt.hashSync(password, secret)
}

const comparePassword = (password, hash) => {
  return bcrypt.compareSync(password, hash)
}

module.exports = {
  hashPassword,
  comparePassword,
}

sesuai nama functionnya, hashPassword untuk nge-hash password yang user masukkin (register), dan comparePassword untuk compare password yang user masukkin bener atau ngga (login)

Untuk pake helpers bcrypt ini, kita pake di model user, karena mongoose uda punya middleware sendiri

models/user.js

const { model, Schema } = require('mongoose')
const { hashPassword } = require('../helpers/bcrypt')

const userSchema = new Schema(
  {
    email: String,
    password: String,
  },
  { timestamps: true, versionKey: false },
)

userSchema.pre('save', function(next) {
  // jadi sebelum di save ke DB, passwordnya di hash dulu
  this.password = hashPassword(this.password)

  // setelah itu next (sama kyk middlewarenya express)
  next()
})

const User = model('User', userSchema)

module.exports = User

Schema yang akan kita pake adalah one user has many articles, so kita harus rubah model article jadi gini

const { model, Schema } = require('mongoose')

const articleSchema = new Schema(
  {
    title: String,
    content: String,
    author: {
      type: Schema.Types.ObjectId,
      ref: 'User',
    },
  },
  { timestamps: true, versionKey: false },
)

const Article = model('Article', articleSchema)

module.exports = Article

Untuk ngasih tau semacam relasi antara user dan article, lengkapnya disini

User Controller

controllers/usersController.js

const User = require('../models/user')
const { comparePassword } = require('../helpers/bcrypt')

module.exports = {
  async register(req, res) {
    const { email, password } = req.body

    const newUser = await User.create({ email, password })

    res.status(201).json(newUser)
  },
}

routes/userRoute.js

const router = require('express').Router()

const usersController = require('../controllers/usersController')

router.post('/register', usersController.register)

module.exports = router

routes/index.js

const router = require('express').Router()
const articleRoute = require('./articlesRoute')
const userRoute = require('./userRoute')

router.use('/articles', articleRoute)
router.use('/users', userRoute)

module.exports = router

Pas kita execute

curl \
-d '{"email": "[email protected]", "password": "0rm4s_n4k4l"}' \
-H 'Content-Type: application/json' \
-X POST http://localhost:3000/users/register | jq

Seharusnya uda works, dan password yang kita masukkin uda ke-hash

Article dan User uda selesai nih, sekarang kita mau klo ada orang yang mau buat article harus login sebelum buat article, kita harus pasang middleware dan jwt di aplikasi untuk mastiin uda login atau belum

npm i jsonwebtoken

helpers/jwt.js

const jwt = require('jsonwebtoken')

// secret ini seharusna di taruh di env
const secret = process.env.SECRET_JWT || 'evilfactorylabs'

// untuk generate JWT pas login, yang nanti di lempar ke client(biasanyadi taruh di localStorage xD)
const generateJwt = payload => {
  return jwt.sign(payload, secret)
}

// untuk verify apakah token dari client bener/valid atau ngga
const verifyJwt = token => {
  return jwt.verify(token, secret)
}

module.exports = {
  generateJwt,
  verifyJwt,
}

BTW, kelen bisa kasih expired token

Kita akan pasang middleware di routes, tapi kita harus buat middlewarenya dulu

middleware/auth.js

const { verifyJwt } = require('../helpers/jwt')

const authentication = (req, res, next) => {
  try {
    // req.headers access_token ini nanti di taruh di headers
    const decode = verifyJwt(req.headers.access_token)

    req.user = decode

    next()
  } catch {
    res.status(401).json({ message: 'You need to login' })
  }
}

module.exports = { authentication }

Kalo di perhatiin kita pake try catch untuk error handlingnya, nah sekarang pasang di routes

routes/articleRoute.js

const router = require('express').Router()

const articlesController = require('../controllers/articlesController')
const { authentication } = require('../middlewares/auth')

router.get('/', articlesController.index)
router.post('/', authentication, articlesController.create)
router.get('/:articleId', articlesController.findOne)
router.delete('/:articleId/delete', articlesController.delete)
router.put('/:articleId/update', articlesController.update)

module.exports = router

Klo kita buat article tanpa ngirim access_token di headers

curl -d '{"title": "ormas ga jelas", "author": "epilfooktori gaje", "content": "Sikat!"}' \
-H "Content-Type: application/json" \
-X POST http://localhost:3000/articles | jq

Berarti uda work nih, tp gimana caranya dapetin tokennya??

Harus tambahin endpoint untuk login buat usernya

controllers/usersController.js

const User = require('../models/user')
const { comparePassword } = require('../helpers/bcrypt')
const { generateJwt } = require('../helpers/jwt')

module.exports = {
  async register(req, res) {
    const { email, password } = req.body

    const newUser = await User.create({ email, password })

    res.status(201).json(newUser)
  },
  async login(req, res, next) {
    const { email, password } = req.body

    // cek dulu apakah ada user dengan email tersebut?
    const user = await User.findOne({ email })

    // jika ada, maka compare password yang dari client dengan hash dari DB
    // dan generate JWT untuk di balikkin ke client
    if (user && comparePassword(password, user.password)) {
      // payloadnya bebas, terserah kelen
      const token = generateJwt({
        id: user._id,
        email: user.email,
      })

      res.status(200).json({ token })
    }
  },
}

Tambahin di routes juga

const router = require('express').Router()

const usersController = require('../controllers/usersController')

router.post('/register', usersController.register)
router.post('/login', usersController.login)

module.exports = router

Klo coba endpointnya

curl \
-d '{"email": "[email protected]", "password": "0rm4s_n4k4l"}' \
-H 'Content-Type: application/json' \
-X POST http://localhost:3000/users/login | jq

Harusnya dapet response kyk gini (tokennya pasti ga sama sih)

Nanti buat article dengan ngirim token itu ke API kita, tapi sebelumnya kita harus rubah function create di article controller

  async create(req, res) {
    const { title, content } = req.body

    const article = await Article.create({ title, author: req.user.id, content })
    res.status(201).json(article)
  },

Authornya jadi pake user yang masuk saat ini

Jalanin ini

curl \
-d '{"title": "ormas ga jelas", "content": "Sikat!"}' \
-H "Content-Type: application/json" \
-H "access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlMjMxOGJlOGI1NTk2N2M4MjRhMzlmMyIsImVtYWlsIjoiZXZpbEBvcm1hcy5jb20iLCJpYXQiOjE1NzkzNjA3NzV9.HtFmLdNrNV7DQsKZT3iNNyW5HXhgXaKRk74o8cyF4N4" \
-X POST http://localhost:3000/articles | jq

Tokennya ganti pake token kelen sendiri

Sekarang kita mau pasang authorization, biar kita gabisa asal update atau delete article orang lain

Kita harus rubah middleware dan routes

middleware/auth.js

const { verifyJwt } = require('../helpers/jwt')
const Article = require('../models/article')

const authentication = (req, res, next) => {
  try {
    // req.headers access_token ini nanti di taruh di headers
    const decode = verifyJwt(req.headers.access_token)

    req.user = decode

    next()
  } catch {
    res.status(401).json({ message: 'You need to login' })
  }
}

const authorization = (req, res, next) => {
  const { articleId } = req.params
  const userId = req.user.id
  Article.findById(articleId).then(article => {
    if (article) {
      if (article.author == userId) {
        next()
      } else {
        json.status(403).json({ message: "You don't have permission bro" })
      }
    } else {
      json.status(404).json({ message: 'Not Found' })
    }
  })
}

module.exports = { authentication, authorization }

routes/articleRoute.js

const router = require('express').Router()

const articlesController = require('../controllers/articlesController')
const { authentication, authorization } = require('../middlewares/auth')

router.get('/', articlesController.index)
router.get('/:articleId', articlesController.findOne)
router.use(authentication)
router.post('/', articlesController.create)
router.use('/:articleId', authorization)
router.delete('/:articleId/delete', articlesController.delete)
router.put('/:articleId/update', articlesController.update)

module.exports = router

Kirim articleId lewat middlware

Terus buat user lain dulu

curl \
-d '{"email": "[email protected]", "password": "rin_ormas"}' \
-H 'Content-Type: application/json' \
-X POST http://localhost:3000/users/register | jq

Terus login pake user yang baru, terus coba delete article orang lain

curl \
-H "Content-Type: application/json" \
-H "access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlMjQ0NzMxNjhiY2JlMjhhZThhZDBkNSIsImVtYWlsIjoicmluQG9ybWFzLmNvbSIsImlhdCI6MTU3OTQzNjEzOH0.ezp4uzyzSU4NW4EigqEruw-OgOftcBaVRgD6raE50Mw" \
-X DELETE http://localhost:3000/articles/5e2448ddc5b1df2a6f7f85e1/delete | jq

Kalo pake token yang bener seharusnya bisa delete article tersebut

Source Code: https://github.com/kevanantha/rest-pengaman

Sekian Terimakasih, maaf atas kekurangannya

Menikmati tulisan ini?

Blog ini tidak menampilkan iklan, yang berarti blog ini didanai oleh pembaca seperti kamu. Gabung bersama yang telah membantu blog ini agar terus bisa mencakup tulisan yang lebih berkualitas dan bermanfaat!

Pendukung

Kamu?