DEV Community

Muhammad Faqih Muntashir
Muhammad Faqih Muntashir

Posted on • Edited on

Membuat Otentikasi JWT dengan PHP Native

Kode dari hasil tutorial ini bisa didownload di sini:
https://github.com/itsfaqih/jwt-php-native

Persiapan

Sebelum mengikuti tutorial ini, pastikan kalian sudah meng-install requirements di bawah ini.

Requirements

Catatan: Apache diperlukan karena penggunaan function getallheaders() untuk mendapatkan request header. Jika kalian ingin menggunakan web server lainnya, harap menyesuaikan.

Membuat Proyek

Install Library

Pertama, kita buat direktori untuk proyek ini:



mkdir jwt-php-native
cd jwt-php-native


Enter fullscreen mode Exit fullscreen mode

Jika kalian menggunakan XAMPP, atau laragon, atau yang lainnya, bisa langsung dibuat di dalam direktorinya masing-masing (htdocs, www, dll).

Di tutorial ini saya hanya akan menggunakan built-in web server dari php saja.

Setelah masuk ke direktori, kita buat file composer.json melalui perintah di bawah.



composer init


Enter fullscreen mode Exit fullscreen mode

Saat menjalankan perintah di atas, kamu akan mendapati prompt untuk mengisi nama package, deskripsi, author, dll, itu bisa kalian isi sesuai keinginan kalian, atau bisa juga langsung enter semua saja. Hingga bagian dependency, kita ketikkan "no" dulu.



Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]? no
Would you like to define your dev dependencies (require-dev) interactively [yes]? no


Enter fullscreen mode Exit fullscreen mode

Lalu tekan enter saat konfirmasi.



Do you confirm generation [yes]?


Enter fullscreen mode Exit fullscreen mode

Kemudian kita install library yang dibutuhkan, yaitu library dotenv dan library untuk membuat jwt-nya:



composer require vlucas/phpdotenv firebase/php-jwt


Enter fullscreen mode Exit fullscreen mode

Jika kalian penasaran mengapa kita juga butuh menginstall library dotenv, karena agar mempermudah kita menerapkan penggunaan environment variable, yang mana ini adalah cara terbaik untuk menyimpan data sensitif dari aplikasi. (Bisa baca detailnya di sini)

Selesai meng-install kedua library tersebut, maka akan ada direktori vendor dan file composer di dalam proyek kita:

jwt-php-01

Membuat Custom Environment Variable

Ini adalah fitur utama dari library dotenv tadi. Kita dapat membuat custom environment variable dengan mudah melalui file ".env".

Mari kita buat file tersebut, kemudian isi dengan ACCESS_TOKEN_SECRET dan REFRESH_TOKEN_SECRET:

jwt-php-02

Nilai dari kedua token tersebut sebenarnya bebas. Kalian bisa mengisinya dengan apapun yang kalian inginkan, dengan syarat keduanya tidak boleh sama.

Namun karena secret key di sini bersifat rahasia layaknya password, akan lebih baik menggunakan random string panjang yang sulit ditebak. Terlebih lagi kedua secret itu cukup disimpan dan tidak perlu diingat.

Dalam tutorial ini saya menggunakan password generator untuk membuat secret key-nya.

jwt-php-03

Membuat Endpoint Otentikasi (Login)

Kita buat file "login.php". File ini akan digunakan sebagai jalur user melakukan otentikasi JWT melalui request method POST.

Di sini saya hanya menggunakan data mock/dummy, tapi jika kalian cukup bersemangat, bisa juga buat database, buat tabel, buat koneksi, dst.

Import Library
Sebelum menulis apapun, baiknya kita mengimport library yang nantinya akan digunakan:



// Import script autoload agar bisa menggunakan library
require_once('./vendor/autoload.php');
// Import library
use Firebase\JWT\JWT;
use Dotenv\Dotenv;


Enter fullscreen mode Exit fullscreen mode

Lalu kita load custom environment variable kita:



$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();


Enter fullscreen mode Exit fullscreen mode

Atur Content-Type
Agar response yang dikirim ke user dibaca sebagai JSON, kita perlu mengatur response header bagian Content-Type seperti ini:



header('Content-Type: application/json');


Enter fullscreen mode Exit fullscreen mode

Validasi Method Request
Karena proses login ini menggunakan method POST, yang perlu kita lakukan pertama kali adalah memeriksa apakah method yang digunakan oleh user sudah sesuai atau belum:



// Cek method request apakah POST atau tidak
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  http_response_code(405);
  exit();
}


Enter fullscreen mode Exit fullscreen mode

Dengan kode di atas, apabila user tidak mengakses login.php menggunakan method POST, maka akan mendapatkan error kode 405, "method not allowed", yang berarti method yang digunakan tidak diizinkan.

Validasi Format Data
Umumnya JWT diterapkan pada sistem microservice (menggunakan API) sehingga data yang diterima oleh backend saat proses login ini bukan lah berupa form data (karena tidak melalui form), melainkan JSON.

Nilai input form data diambil menggunakan variabel $_POST, sedangkan untuk raw JSON diambil menggunakan cara di bawah:



// Ambil JSON yang dikirim oleh user
$json = file_get_contents('php://input');
// Decode json tersebut agar mudah mengambil nilainya
$input = json_decode($json);


Enter fullscreen mode Exit fullscreen mode

Setelah men-decode JSON tersebut, barulah kita melakukan validasi data yang ada di dalamnya:



// Jika tidak ada data email atau password
if (!isset($input->email) || !isset($input->password)) {
  http_response_code(400);
  exit();
}


Enter fullscreen mode Exit fullscreen mode

Dengan kode di atas, apabila user tidak memasukkan email atau password, maka akan mendapatkan error kode 400, "bad request", yang berarti request yang dikirim oleh user tidak valid.

Otentikasi User
Seperti yang saya katakan sebelumnya, di sini saya menggunakan data mock, jadi untuk data user saya buat menjadi variabel seperti ini:



// Cuma data mock/dummy, bisa diganti dengan data dari database
$user = [
  'email' => 'johndoe@example.com',
  'password' => 'qwerty123'
];


Enter fullscreen mode Exit fullscreen mode

Kemudian kita cocokkan dengan data yang dikirim oleh user tadi:



// Jika email atau password tidak sesuai
if ($input->email !== $user['email'] || $input->password !== $user['password']) {
  echo json_encode([
    'message' => 'Email atau password tidak sesuai'
  ]);
  exit();
}


Enter fullscreen mode Exit fullscreen mode

Dengan demikian, jika data email atau password yang dikirim itu salah, maka user akan mendapatkan pesan "Email atau password tidak sesuai".

Selanjutnya kita tinggal perlu membuat dan mengirim JWT ke user saat login berhasil.

Buat variabel untuk menyimpan waktu kadaluarsa access token-nya:



// 15 * 60 (detik) = 15 menit
$expired_time = time() + (15 * 60);


Enter fullscreen mode Exit fullscreen mode

Waktu kadaluarsa access token tidak perlu dibuat terlalu lama karena alasan keamanan.

Setelah itu kita buat variabel payload yang mana akan jadi payload token kita:



$payload = [
  'email' => $input->email,
  'exp' => $expired_time
];


Enter fullscreen mode Exit fullscreen mode

Sebenarnya di dalam payload ini kita hanya perlu email saja, namun karena cara kerja dari library JWT yang kita pakai, kita perlu key 'exp' untuk mengatur waktu kadaluarsa token-nya.

Selanjutnya tinggal generate token menggunakan library-nya, dan kirim ke user:



$access_token = JWT::encode($payload, $_ENV['ACCESS_TOKEN_SECRET']);
echo json_encode([
  'accessToken' => $access_token,
  'expiry' => date(DATE_ISO8601, $expired_time)
]);


Enter fullscreen mode Exit fullscreen mode

Sampai sini biasanya sudah cukup dan sudah bisa bekerja dengan baik.

Tapi jika kalian tertarik untuk lanjut mempelajari tentang in memory token, yang mana akan saya buat tutorial selanjutnya, kalian bisa ikuti langkah penambahan refresh token di bawah (opsional).

Menambahkan Refresh Token di Http-only Cookie



// Ubah waktu kadaluarsa lebih lama (1 jam)
$payload['exp'] = time() + (60 * 60);
$refresh_token = JWT::encode($payload, $_ENV['REFRESH_TOKEN_SECRET']);
// Simpan refresh token di http-only cookie
setcookie('refreshToken', $refresh_token, $payload['exp'], '', '', false, true);


Enter fullscreen mode Exit fullscreen mode

Cookie adalah data yang disimpan oleh browser user dan akan selalu diselipkan setiap user membuat request selanjutnya tepat setelah cookie tersebut ditambahkan.

Menggunakan http-only berarti cookie tersebut tidak dapat diakses melalui javascript, sehingga bisa mencegah tercurinya refresh token.

Membuat Endpoint Protected Resource

Protected Resource di sini maksudnya adalah data yang dilindungi. Sebagai contoh: konten yang hanya bisa dilihat oleh user yang sudah login.

Dalam contoh ini, saya membuat file "games.php" yang isinya adalah daftar game. Daftar game ini nantinya hanya bisa diakses jika user mengirimkan access token yang valid.

Import Library
Sama seperti sebelumnya, kita import library-nya terlebih dahulu:



// Import script autoload agar bisa menggunakan library
require_once('./vendor/autoload.php');
// Import library
use Firebase\JWT\JWT;
use Dotenv\Dotenv;


Enter fullscreen mode Exit fullscreen mode

Jangan lupa custom environment variable kita:



$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();


Enter fullscreen mode Exit fullscreen mode

Atur Content-Type
Seperti sebelumnya juga, kita atur response header Content-Type-nya:



header('Content-Type: application/json');


Enter fullscreen mode Exit fullscreen mode

Validasi Method Request
Seperti sebelumnya, kita juga memvalidasi method yang digunakan oleh user saat request. Bedanya di sini harus menggunakan GET:



if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
  http_response_code(405);
  exit();
}


Enter fullscreen mode Exit fullscreen mode

Verifikasi Token dan Mengembalikan Data
Jika saat login yang divalidasi adalah format data dan kebenaran email & passwordnya, di sini kita memverifikasi token yang dikirim oleh user.

User akan mengirim token melalui header authorization, yang berarti kita perlu membaca header request-nya:



$headers = getallheaders();
if (!isset($headers['Authorization'])) {
  http_response_code(401);
  exit();
}


Enter fullscreen mode Exit fullscreen mode

Kode di atas akan memeriksa keberadaan header authorization. Jika tidak ada, maka user akan mendapatkan error 401, "Unauthorized", yang berarti tidak memiliki otoritas atau hak untuk mengaksesnya.

Kemudian kita ambil token yang ada pada header. Karena dalam header authorization berisi "Bearer ", maka kita perlu menghapus string "Bearer":



list(, $token) = explode(' ', $headers['Authorization']);


Enter fullscreen mode Exit fullscreen mode

Lalu kita verifikasi token tersebut di dalam try catch statement, karena method verifikasi dan decode dari library yang kita gunakan akan melempar sebuah exception apabila tidak valid:



try {
  // Men-decode token. Dalam library ini juga sudah sekaligus memverfikasinya
  JWT::decode($token, $_ENV['ACCESS_TOKEN_SECRET'], ['HS256']);
// Data game yang akan dikirim jika token valid
  $games = [
    [
      'title' => 'Dota 2',
      'genre' => 'Strategy'
    ],
    [
      'title' => 'Ragnarok',
      'genre' => 'Role Playing Game']
    ]
  ];
echo json_encode($games);
} catch (Exception $e) {
  // Bagian ini akan jalan jika terdapat error saat JWT diverifikasi atau di-decode
  http_response_code(401);
  exit();
}


Enter fullscreen mode Exit fullscreen mode

Menguji Proyek

Kita jalankan terlebih dahulu:



// Jalan di command prompt atau terminal
php -S localhost:8000


Enter fullscreen mode Exit fullscreen mode

Jika kalian menggunakan XAMPP atau sejenisnya, dan sudah menyimpan di direktorinya (seperti htdocs), maka bisa cukup jalankan web servernya (Apache atau lainnya).

Kemudian buka Postman, dan langsung kita coba lakukan login melalui http://localhost:8000/login.php (jangan lupa sesuaikan port-nya), dengan method POST:

jwt-php-04

Lalu isi body dengan raw JSON berupa data email dan password, kemudian klik "Send":

jwt-php-05

Jika berhasil, maka kita akan mendapatkan response semacam ini:

jwt-php-06

Dari response itu, kita copy dan simpan accessToken. Kemudian kita coba buka endpoint "/games.php" tanpa mengatur apapun, maka kita akan mendapat status 401, yang berarti kita tidak memiliki akses ke resource tersebut:

jwt-php-07

Sekarang kita isi bagian authorization dengan type bearer dan diisi token tadi:

jwt-php-08

Hasilnya data game akan dikembalikan ke user:

jwt-php-09

Penutup

Sekian pembahasan yang cukup panjang ini tentang pembuatan otentikasi JWT dengan PHP native.

Jika kita perhatikan, belum ada peran refresh token di sini.

Fungsi refresh token sendiri adalah untuk memperbarui access token user, karena access token memiliki umur yang pendek (15 menit dalam tutorial ini).

Alasan mengapa belum ada penggunaan refresh tokennya, ini memang saya sengaja karena cara implementasi di sini (menggunakan http-only cookie) akan lebih masuk akal jika dibahas pada tutorial selanjutnya.

Terima kasih sudah membaca, semoga bisa bermanfaat.

Top comments (9)

Collapse
 
brili99 profile image
Muhammad Brilian Erranaomi

Kalau hasilnya error mungkin bisa direvisi, mungkin karena beda versi library jadi error.

untuk file login.php
$access_token = JWT::encode($payload, $_ENV['ACCESS_TOKEN_SECRET'], 'HS256');
$refresh_token = JWT::encode($payload, $_ENV['REFRESH_TOKEN_SECRET'], 'HS256');

sedangkan file games.php, ditambahkan
use Firebase\JWT\Key;
ubah decode :
JWT::decode($token, new Key($_ENV['ACCESS_TOKEN_SECRET'], 'HS256'));

Semoga membantu

Collapse
 
hand24guns profile image
hand24guns

setelah update untuk login udah bisa keluar tokennya tapi ketika di games.php errornya ini :



Fatal error: Uncaught TypeError: Firebase\JWT\Key: :__construct(): Argument #2 ($algorithm) must be of type string, array given, called in D:\laragon\www\trial\jwt-php-native\games.php on line 27 and defined in D:\laragon\www\trial\jwt-php-native\vendor\firebase\php-jwt\src\Key.php: 21
Stack trace:

0 D:\laragon\www\trial\jwt-php-native\games.php(27): Firebase\JWT\Key->__construct('aRtNtwtKniKeGFl...', Array)

1 {main

}
thrown in D:\laragon\www\trial\jwt-php-native\vendor\firebase\php-jwt\src\Key.php on line 21

mohon bantuannya

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
putude profile image
putude

Susah nyari penjelasan selengkap ini. Terima kasih sudah berbagi ilmu.

Collapse
 
areablogger profile image
Admin Areablogger

Gan, untuk refresh token-nya apakah sebaiknya simpan di database saja agar ketika silent authorization, kita tetap cocokan refresh token-nya dulu dengan database baru di kasi access token?

Collapse
 
itsfaqih profile image
Muhammad Faqih Muntashir • Edited

tidak perlu, cukup lakukan validasi JWT refresh token-nya saja.
jika validasi lolos maka bisa dipastikan bahwa JWT tersebut sah, karena hampir tidak mungkin untuk orang bisa memalsukan token, kecuali mereka tahu secret key yang kita gunakan.

inilah salah satu spesialnya JWT, kita tidak perlu menggunakan database untuk memeriksa sah tidaknya token tersebut.

Collapse
 
maulgats profile image
idris maulana

Kapan nih mas melanjutkan tutorial “pengguanaan refresh tokennya”, suka sekali dengan penyampaiannya, mudah dipahami.

Collapse
 
itsfaqih profile image
Muhammad Faqih Muntashir

terima kasih :D
sayangnya lagi sibuk-sibuknya sama tugas akhir kuliah nih. kemungkinan juli nanti saya lanjut

Collapse
 
topan_wahyudi profile image
Topan Wahyudi

Bang, bagaimana cara nya otentikasi jwt dengan oauth2?