Meningkatkan Keamanan Aplikasi Hapi JS dengan Autentikasi JWT dan Bcrypt
Keamanan aplikasi adalah faktor yang sangat penting dalam pengembangan perangkat lunak. Banyak masalah keamanan muncul karena pengembang tidak memperhatikan keamanan saat mengembangkan aplikasi.
Autentikasi adalah proses verifikasi identitas pengguna yang ingin mengakses suatu sistem atau layanan. Tahapan dalam proses authentication terdiri dari tiga bagian, yaitu :
- Identifikasi (who you are),
- Autentikasi (prove it)
- Otorisasi (are you allowed)
Identifikasi melibatkan penggunaan identitas pengguna seperti nama pengguna, autentikasi memerlukan penggunaan kredensial seperti kata sandi atau sertifikat digital untuk membuktikan identitas tersebut, sedangkan Otorisasi adalah proses penentuan apakah pengguna yang telah diidentifikasi dan diautentikasi memiliki hak akses untuk melakukan tindakan tertentu pada sistem atau layanan.
Autentikasi JWT adalah teknik autentikasi yang sangat umum digunakan dalam pengembangan aplikasi web dan mobile. JWT adalah standar industri yang mengatur cara mengirimkan dan menerima informasi autentikasi.
Bcrypt, di sisi lain, adalah algoritma hashing password yang populer dan aman. Bcrypt berfungsi untuk mengubah password pengguna menjadi string acak yang tidak dapat dibalikkan.
Dalam artikel ini, kita akan menjelaskan bagaimana menerapkan autentikasi JWT dan Bcrypt hashing pada aplikasi Hapi JS untuk meningkatkan keamanan.
Deskripsi project dan pengenalan endpoint
Project ini merupakan lanjutan dari artikel sebelumnya, Silakan buka repositori Github untuk melihat kode sebelumnya.
Pada project ini kita perlu membuat model users
dengan relasi one-to-many terhadap orders
, berikut adalah ERD pada aplikasi ini (entity relationship diagram) :
Berdasarkan relasi tersebut, setiap order dapat dikaitkan dengan satu user melalui foreign key yang merujuk pada id yang ada di tabel users.
Adapun endpoint yang akan kita buat adalah sebagai berikut:
Menginstall modul dan membuat aplikasi
Silakan buka terminal lalu jalankan perintah berikut:
npm i @joi/date bcrypt hapi-auth-jwt2 joi jsonwebtoken
Perintah diatas berfungsi untuk menginstall modul berikut:
joi
: Melakukan validasi data@joi/date
: Merupakan extend dari modul joi untuk validasi datebcrypt
: Melakukan hashing pada password.jsonwebtoken
: Mengimplementasikan JSON Web Token (JWT)hapi-auth-jwt2
: Mengintegrasikan autentikasi JWT pada aplikasi Hapi JS.
Selanjutnya silakan buat folder atau file berikut:
- Didalam folder
src
buatlah folderhelper
, dan buat fileauth.js
didalamnya. - Didalam folder
src
buatlah foldervalidator
, dan buat filevalidator.js
didalamnya. - Masuk ke dalam folder
controller
dan buatlah fileauth.controller.js
- Masuk ke dalam folder
models
dan buatlah fileusers.js
Untuk sementara, biarkan file tetap kosong.
Setelah file berhasil dibuat maka susunan folder akan menjadi seperti berikut:
- src
- config
- config.js
- controller
- auth.controller.js
- order.controller.js
- helper
- auth.js
- models
- index.js
- orders.js
- users.js
- routes
- route.js
- validator
- validator.js
- server.js
- .env
- .gitignore
- package-lock.json
- package.json
Membuat model dan menentukan relasi
Saat ini kita akan membuat model user, buka file users.js
pada folder models
, kemudian masukkan kode berikut:
const bcrypt = require("bcrypt");
const saltRounds = 10;
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"users",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
hooks: {
beforeCreate: async (user, options) => {
const hashedPassword = await bcrypt.hash(user.password, saltRounds);
user.password = hashedPassword;
},
},
}
);
return User;
};
Pada kode diatas, module bcrypt diimpor dan diterapkan pada hook beforeCreate
. Hal ini bertujuan untuk melakukan hashing pada password user sebelum disimpan ke database menggunakan bcrypt dengan menggunakan jumlah salt rounds yang telah ditentukan. Silakan kunjungi halaman resmi sequelize untuk melihat dokumentasi hooks.
Selanjutnya kita akan menentukan relasi antar table. Masih pada folder yang sama, silakan buka file index.js
. dan masukkan kode berikut setelah inisialisasi db.sequelize = sequelize
:
db.users = require("./users.js")(sequelize, DataTypes);
db.users.hasMany(db.orders, {
foreignKey: "user_id",
as: "order",
});
db.orders.belongsTo(db.users, {
foreignKey: "user_id",
as: "user",
});
Berikut adalah seluruh kode pada index.js:
const dbConfig = require("../config/config");
const { Sequelize, DataTypes } = require("sequelize");
const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
host: dbConfig.HOST,
dialect: dbConfig.dialect,
operatorsAliases: false,
});
sequelize
.authenticate()
.then(() => {
console.log("Connected to database!");
})
.catch((err) => {
console.log("Error" + err);
});
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.users = require("./users.js")(sequelize, DataTypes);
db.orders = require("./orders.js")(sequelize, DataTypes);
db.users.hasMany(db.orders, {
foreignKey: "user_id",
as: "order",
});
db.orders.belongsTo(db.users, {
foreignKey: "user_id",
as: "user",
});
db.sequelize.sync({ force: false }).then(() => {
console.log("re-sync done!");
});
module.exports = db;
Pada kode diatas, kita telah menentukan relasi table users
dan orders
dengan menggunakan fungsi association dari sequelize, yakni hasMany
dan belongsTo
. Selanjutnya dilakukan sinkronisasi model dengan database, sehingga setiap perubahan pada model akan berpengaruh pada struktur database. Silakan kunjungi halaman resmi sequelize untuk melihat dokumentasi associations.
Parameter
force: false
digunakan untuk menghindari penghapusan tabel yang telah ada, sehingga data akan tetap utuh. Namun apabila setelah dilakukan sinkronisasi relasi antar tabel masih belum terbentuk, maka ubah parameter menjadiforce: true
dengan resiko kehilangan data.
Cara kerja autentikasi JWT
Secara umum cara kerja JWT adalah sebagai berikut:
- Client melakukan login dengan memberikan kredensial.
- Server melakukan verifikasi kredensial dan, jika benar, menghasilkan token JWT yang berisi informasi pengguna.
- Server mengembalikan token JWT ke client sebagai respons dari permintaan login.
- Client menyimpan token JWT secara lokal
- Setiap kali client melakukan permintaan ke server yang memerlukan autentikasi, token JWT dikirimkan bersama permintaan.
- Server memeriksa apakah token JWT valid dan, jika ya, mengizinkan akses ke sumber daya yang diminta oleh client.
Dalam HapiJS, penggunaan hapi-auth-jwt2 terdiri dari beberapa tahapan, yaitu:
- Registrasi plugin hapi-auth-jwt2.
- Mendefinisikan skema autentikasi (contohnya : HS256) dan menentukan strategi autentikasi menggunakan plugin hapi-auth-jwt2.
- Mendefinisikan rute-rute yang memerlukan autentikasi dengan menggunakan konfigurasi auth pada rute tersebut.
Menerapkan autentikasi JWT
Silakan tambahkan secretkey pada .env
. contohnya adalah sebagai berikut:
DB_NAME = "hapi_rest_api"
DB_USER = "admintutorial"
DB_PASSWORD = "tutorial"
DB_HOST = "127.0.0.1"
DB_PORT = "5432"
JWT_SECRETKEY = "mysecretkey"
Kemudan bukalah file auth.js
dalam folder helper, dan masukkan kode berikut:
require("dotenv").config();
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const db = require("../models");
const Users = db.users;
const secretKey = process.env.JWT_SECRETKEY;
const comparePassword = async (password, hash) => {
return await bcrypt.compare(password, hash);
};
const generateToken = (payload) => {
return jwt.sign(payload, secretKey, { expiresIn: '4h' });
};
const validateToken = async (decoded, request, h) => {
try {
const user = await Users.findOne({ where: { id: decoded.id } });
if (!user) {
return { isValid: false };
}
return {
isValid: true,
credentials: {
id: user.id,
name: user.name
}
};
} catch (error) {
console.error(error);
return { isValid: false };
}
};
module.exports = { comparePassword, generateToken, validateToken, secretKey };
Di dalam auth.js
kita membuat fungsi comparePassword
dangenerateToken
yang akan digunakan untuk keperluan login, kemudian fungsi validateToken
akan digunakan untuk server.auth.strategy
pada file server.js
.
Silakan buka file server.js
, dan masukkan kode berikut:
"use strict";
const Hapi = require("@hapi/hapi");
const RoutesPlugin = require("./routes/route");
const Auth = require("./helper/auth");
const Jwt = require('hapi-auth-jwt2');
const init = async () => {
const server = Hapi.server({
port: 3000,
host: "localhost",
});
await server.register(Jwt);
server.auth.strategy('jwt', 'jwt', {
key: Auth.secretKey,
validate: Auth.validateToken,
verifyOptions: {
algorithms: ['HS256'],
},
});
server.auth.default("jwt");
await server.register(RoutesPlugin);
await server.start();
console.log(`Server started on: ${server.info.uri}`);
};
process.on("unhandledRejection", (err) => {
console.log(err);
process.exit(1);
});
init();
Pada kode diatas, kita telah mendaftarkan plugin Jwt ke dalam server Hapi dan mengkonfigurasikan strategi autentikasi menggunakan JSON Web Token (JWT). Kemudian, konfigurasi strategi autentikasi ditetapkan sebagai strategi otorisasi default untuk server Hapi dengan menggunakan server.auth.default()
.
Membuat controller
Silakan buka file auth.controller.js
pada folder controller
, lalu masukkan kode berikut:
const db = require("../models");
const Boom = require("@hapi/boom");
const Auth = require("../helper/auth");
const Users = db.users;
const AuthController = {
async register(request, h) {
try {
const { name, email, password } = request.payload;
const userExist = await Users.findOne({ where: { email: email } });
if (userExist) {
return Boom.badRequest("User already registered");
} else {
const user = await Users.create({ name, email, password });
return h.response(user).code(201);
}
} catch (err) {
return Boom.internal(err.message);
}
},
async login(request, h) {
try {
const { email, password } = request.payload;
const user = await Users.findOne({ where: { email: email } });
if (!user) {
return Boom.notFound("User not found");
}
const compare = Auth.comparePassword(password, user.password);
if (!compare) {
return Boom.badRequest("Invalid username or password");
}
const payload = { id: user.id, name: user.name };
const token = Auth.generateToken(payload)
return h.response(`Successfully logged in with token : ${token}`).code(200);
} catch (err) {
console.log(err)
return Boom.internal(err.message);
}
},
};
module.exports = AuthController;
Untuk melakukan register user dibutuhkan parameter name
, email
, dan password
. Jika user sudah terdaftar maka Boom akan mengembalikan pesan error.
Pada fungsi login, terdapat parameter email
dan password
. Fungsi yang sebelumnya telah dibuat pada auth.js
, diimpor dan digunakan pada fungsi login ini, yaitu comparePassword()
, untuk melakukan komparasi plain password dan hased password.
Untuk mendapatkan token kita menggunakan fungsi generateToken()
dengan memasukkan payload berupa id
dan name
yang diambil dari database user
Jangan menyertakan informasi sensitif pada payload jwt seperti password, karena payload dapat dibaca oleh siapa saja dan hanya dienkripsi dengan algoritma encoding seperti base64.
Menerapkan autentikasi pada route
Untuk menerapkan autentikasi para router, Hapi mendukung opsi auth
pada handler untuk menentukan strategi autentikasi yang digunakan, dalam hal ini adalah “jwt”
. Jika opsi auth
diatur, maka rute hanya dapat diakses oleh user yang terverifikasi. Kita juga bisa memberikan nilai false
pada auth
, yang artinya tidak diperlukan autentikasi untuk mengakses rute tersebut.
Silakan buka file route.js
pada folder routes, dan masukkan kode berikut:
const OrderController = require("../controller/order.controller");
const AuthController = require("../controller/auth.controller");
const routes = [
{
method: "GET",
path: "/",
handler: (request, h) => {
const credentials = request.auth.credentials;
return { message: `Hello ${credentials.name}!` };
},
options: {
auth: "jwt",
},
},
{
method: "GET",
path: "/{any*}",
handler: (request, h) => {
return "Oops! You must be lost!";
},
options: {
auth: "jwt",
},
},
{
method: "POST",
path: "/api/orders",
handler: OrderController.addOrder,
options: {
auth: "jwt",
},
},
{
method: "GET",
path: "/api/orders",
handler: OrderController.getOrder,
options: {
auth: "jwt",
},
},
{
method: "GET",
path: "/api/orders/{id}",
handler: OrderController.getOrderById,
options: {
auth: "jwt",
},
},
{
method: "PUT",
path: "/api/orders/{id}",
handler: OrderController.udpateOrder,
options: {
auth: "jwt",
},
},
{
method: "DELETE",
path: "/api/orders/{id}",
handler: OrderController.deleteOrder,
options: {
auth: "jwt",
},
},
{
method: "POST",
path: "/api/register",
handler: AuthController.register,
options: {
auth: false,
},
},
{
method: "POST",
path: "/api/login",
handler: AuthController.login,
options: {
auth: false,
},
},
];
module.exports = {
name: "routes",
version: "1.0.0",
register: async (server, options) => {
server.route(routes);
},
};
Melakukan validasi data
Validasi data penting karena dapat membantu mencegah kesalahan input data dan mampu meningkatkan keamanan aplikasi. Dalam hal ini, kita menggunakan Joi untuk melakukan validasi data.
Silakan buka file validator.js
pada folder validator
, dan masukkan kode berikut:
const Joi = require("joi").extend(require("@joi/date"));
const registerSchema = Joi.object({
name: Joi.string().min(3).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
const orderSchema = Joi.object({
product_name: Joi.string().required(),
order_date: Joi.date().format("YYYY-MM-DD").utc(),
amount: Joi.number().min(1).required(),
user_id: Joi.number().required()
});
module.exports = { registerSchema, loginSchema, orderSchema };
Dalam Hapi kita bisa menerapkan opsi validate
pada handler, sehingga kita bisa mengubah kode pada file route.js
sebelumnya menjadi seperti berikut:
const OrderController = require("../controller/order.controller");
const AuthController = require("../controller/auth.controller");
const Validator = require("../validator/validator")
const routes = [
{
method: "GET",
path: "/",
handler: (request, h) => {
const credentials = request.auth.credentials;
return { message: `Hello ${credentials.name}!` };
},
options: {
auth: "jwt",
},
},
{
method: "GET",
path: "/{any*}",
handler: (request, h) => {
return "Oops! You must be lost!";
},
options: {
auth: "jwt",
},
},
{
method: "POST",
path: "/api/orders",
handler: OrderController.addOrder,
options: {
auth: "jwt",
validate: {
payload: Validator.orderSchema
}
},
},
{
method: "GET",
path: "/api/orders",
handler: OrderController.getOrder,
options: {
auth: "jwt",
},
},
{
method: "GET",
path: "/api/orders/{id}",
handler: OrderController.getOrderById,
options: {
auth: "jwt",
},
},
{
method: "PUT",
path: "/api/orders/{id}",
handler: OrderController.udpateOrder,
options: {
auth: "jwt",
validate: {
payload: Validator.orderSchema
}
},
},
{
method: "DELETE",
path: "/api/orders/{id}",
handler: OrderController.deleteOrder,
options: {
auth: "jwt",
},
},
{
method: "POST",
path: "/api/register",
handler: AuthController.register,
options: {
auth: false,
validate: {
payload: Validator.registerSchema,
}
},
},
{
method: "POST",
path: "/api/login",
handler: AuthController.login,
options: {
auth: false,
validate: {
payload: Validator.loginSchema
}
},
},
];
module.exports = {
name: "routes",
version: "1.0.0",
register: async (server, options) => {
server.route(routes);
},
};
Melakukan testing aplikasi
Selesai sudah kode untuk aplikasi kita, sekarang saatnya melakukan testing!
Silakan buka postman, lalu buatlah folder pada collection kamu, supaya request di collection lebih rapih dan terstruktur. Berikut contohnya:
Untuk mengecek apakah autentikasi berjalan dengan benar, mari kita coba method GET dengan url http://localhost:3000/api/orders tanpa memberikan akses token.
Respon yang diterima adalah error : unauthorized, dengan status kode : 401 dan message : missing authentication.
Selanjutnya, silakan login terlebih dahulu dengan memasukkan email dan password, berikut contohnya:
Silakan copy token yang berhasil di generate. Kemudian, buka kembali method GET dengan url http://localhost:3000/api/orders. Pada pilihan menu klik authorization dan pilih Bearer Token seperti gambar berikut:
Kemudian paste token yang telah dicopy pada kolom token, tekan save(ctrl + s), lalu klik send seperti gambar berikut:
Terdapat respon dengan status 200, yang menandakan kita berhasil terauntentikasi dan mendapatkan data order sesuai dengan yang diinginkan.
Selamat!!! dengan demikian kamu telah berhasil melakukan validasi data, menerapkan autentikasi JWT, dan hashing bcrypt!
Sumber :
https://www.strongdm.com/authentication
https://sequelize.org/docs/v6/other-topics/hooks/
https://sequelize.org/docs/v6/core-concepts/assocs/
https://github.com/dwyl/hapi-auth-jwt2
Silakan kunjungi repositori Github untuk melihat seluruh kode yang telah dibahas dalam artikel ini.