13 April 2019
Teknik Autentikasi berbasis HMAC dengan JavaScript dan PHP

Tulisan kali ini akan membahas bagaimana menggunakan Hash-Based Message Authentication Code (HMAC) dalam autentikasi pengguna aplikasi berbasis web. HMAC merupakan salah satu jenis Message Authentication Code (MAC) yang dibangkitkan dengan menggunakan kombinasi fungsi hash dan kunci kriptografi.

Pengantar

Secara umum, MAC digunakan untuk memvalidasi informasi, yaitu dengan membandingkan hash yang dibangkitkan dari informasi dengan hash yang dikirim bersama informasi tersebut. Jika terdapat perbedaan antara kedua hash maka dapat dipastikan informasi yang diterima telah mengalami perubahan.

Berikut contoh hasil pembangkitan hash menggunakan algoritma MD5 (diambil dari website https://www.md5online.org) dengan menggunakan text asli Hello World dan Hello world (perhatikan huruf w kecil) :

Hello World : b10a8db164e0754105b7a99be72e3fe5
Hello world : 3e25960a79dbc69b674cd4ec67a72c62

Berikut ini contoh lain hasil pembangkitan hash menggunakan algoritma HMAC SHA1 (diambil dari website https://www.freeformatter.com/hmac-generator.html) dengan menggunakan text asli Ini adalah pesan rahasia dan kunci p455word dan P455word (perhatikan huruf P besar)

Ini adalah pesan rahasia [p455word] : 26fc072b6566163d46a7f474564b4f764df95646
Ini adalah pesan rahasia [P455word] : 049be12068015f50fd986a7da9552b9785e984e6

Autentikasi dalam perangkat lunak adalah proses verifikasi user yang akan menggunakan perangkat lunak tersebut. Cara umum yang digunakan yaitu mencocokkan username/password yang diinput oleh user dengan username/password yang tersimpan dalam database. Dalam perangkat lunak berbasis web, proses ini mengharuskan perpindahan informasi dari web browser ke web server. Informasi tersebut jika tidak melalui suatu proses pengacakan akan mudah dibaca oleh pihak lain.

Disini kita akan coba menggunakan HMAC dalam proses autentikasi user dalam aplikasi web. Sebagai gambaran umum, proses autentikasi terbagi menjadi beberapa tahap, yaitu :

  1. Di sisi client, user akan memasukkan username dan password. Kode javascript kemudian menyusun informasi dalam format JSON yang terdiri atas username, waktu login (timestamp), dan url tujuan. Informasi ini di-hash menggunakan fungsi HMAC-SHA512 dan password user sebagai kuncinya.

  2. Informasi beserta hash dikirimkan ke aplikasi server menggunakan metode HTTP POST. Di sisi server, informasi yang diterima di-hash kembali menggunakan fungsi HMAC-SHA512 dan password yang tersimpan, kemudian hasilnya dicocokkan dengan hash yang diterima dari client. Perbedaan pada kedua hash, mengindikasikan password yang dimasukkan salah

Pengkodean

Sisi Client : HTML & JavaScript

Untuk mempermudah pengkodean, kita akan menggunakan library jQuery dan CryptoJS. Secara umum ada 2 kode yang akan dikerjakan di sisi client, yaitu : kode HTML untuk halaman web yang berisi form untuk menerima informasi login dari user dan kode javascript untuk memproses dan mengirim informasi login ke server.

Struktur kode HTML yang digunakan adalah sebagai berikut :

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="style.css"/>
    <title>HMAC Auth Sample</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.js"></script>
  </head>
  <body>
    <div class="container">
      <h3>HMAC Authentication Sample</h3>
      <fieldset>
        <legend>Masukkan Username & Password</legend>
        <form class="form">
          <div class="field">
            <label for="username">Username :</label>
            <input type="text" id="username"/>
          </div>
          <div class="field">
            <label for="password">Password :</label>
            <input type="password" id="password"/>
          </div>
          <div class="field">
            <button type="button" id="btn-login">Login</button>
          </div>
        </form>
      </fieldset>
    </div>
    <script src="script.js"></script>
  </body>
</html>

Proses autentikasi dipicu pada saat user mengklik tombol login. Disini kita perlu mendefinisikan event listener untuk tombol login tersebut.

$('#btn-login').on('click', (e) => {
  // kode selanjutnya ditulis disini
});

Ambil nilai pada input username dan password, jika nilai salah satunya adalah text kosong maka proses login dibatalkan

let username = $('#username').val();
let password = $('#password').val();
if((username==='')||(password==='')) return false;

Selanjutnya kita susun informasi login yang terdiri atas username, URL dari halaman login, serta timestamp yang menunjukkan waktu login.

let url = 'http://127.0.0.1:8000/login.php';
let auth = {
  username: username,
  url: url,
  timestamp: Date.now()
};

Kita bangkitkan hash dari informasi tersebut menggunakan fungsi CryptoJS.HmacSHA512() dan password yang sudah dimasukkan sebelumnya.

let hash = CryptoJS.HmacSHA512(JSON.stringify(auth), password);

Dari sini kita siapkan data yang akan dikirim ke server. Data ini terdiri atas informasi login dan hash dari informasi tersebut. Keduanya menggunakan format encoding Base64

let data = {
  auth: btoa(JSON.stringify(auth)),
  hash: hash.toString(CryptoJS.enc.Base64)
};

Terakhir data tersebut dikirim ke server menggunakan fungsi jQuery.ajax().

jQuery.ajax({
  url: url,
  method: 'POST',
  data: JSON.stringify(data),
  dataType: 'json',
  success: (data, status, xhr) => {
    alert(data.message);
  },
  error: (xhr, status, err) => {
    let data = JSON.parse(xhr.responseText);
    alert(data.message);
  }
});

Kode javascript selengkapnya adalah sebagai berikut :

$('#btn-login').on('click', (e) => {

    let username = $('#username').val();
    let password = $('#password').val();

    if((username == '')||(password == '')) return false;

    let url = 'http://127.0.0.1:8000/login.php';

    let auth = {
        username: username,
        url: url,
        timestamp: Date.now()
    };

    let hash = CryptoJS.HmacSHA512(JSON.stringify(auth), password);

    let data = {
        auth: btoa(JSON.stringify(auth)),
        hash: hash.toString(CryptoJS.enc.Base64)
    };

    jQuery.ajax({
        url: url,
        method: 'POST',
        data: JSON.stringify(data),
        dataType: 'json',
        success: (data, status, xhr) => {
            alert(data.message);
        },
        error: (xhr, status, err) => {
            let data = JSON.parse(xhr.responseText);
            alert(data.message);
        }
    });

});

Sisi Server : PHP

Sebagaimana didefinisikan pada fungsi jQuery.ajax() dalam kode javascript sebelumnya, response yang akan diterima dari sisi server akan diperlakukan sebagai objek javascript. Dalam kode PHP ini kita perlu menentukan bahwa response yang akan dikirim akan menggunakan tipe JSON.

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

Disini juga kita perlu tentukan bahwa URL ini hanya melayani metode HTTP POST. Selain metode tersebut, kode PHP akan mengembalikan error 405 dengan pesan "Method not Allowed".

if($_SERVER["REQUEST_METHOD"] != "POST") {
  http_response_code(405);
  echo json_encode(["message" => "method not allowed"]);
  return false;
}

Dari sini kita bisa lanjutkan dengan membaca konten request dan memeriksa kelengkapan data yang diperlukan. Jika data yang diperlukan tidak lengkap, kode PHP akan mengembalikan error 400 dengan pesan "Incomplete Request".

$body = json_decode(file_get_contents("php://input"), $assoc=true);
if((!isset($body["auth"])) || (!isset($body["hash"]))) {
    http_response_code(400);
    echo json_encode(["message" => "incomplete request"]);
    return false;
}

Informasi login yang tersimpan dalam $body["auth"] dikirim dari client dalam format encoding Base64. Disini kita perlu mengembalikan informasi login tersebut ke bentuk aslinya. Kode dibawah akan men-decode informasi login dari JSON menjadi associative array.

$auth = json_decode(base64_decode($body["auth"]), $assoc=true);

Seterusnya kita perlu memeriksa waktu pengiriman informasi dengan membandingkan nilai yang tersimpan dalam $auth["timestamp"] dengan waktu saat ini. Jika selisih dari kedua nilai tersebut lebih besar dari 5000 (satuan milidetik), kita kembalikan response error dengan kode 403 (forbidden) dengan pesan "expired request". Selain itu kita juga perlu memeriksa nilai $auth["url"] dengan URL kode PHP saat ini.

$current = time() * 1000;
$ts = $auth["timestamp"];

if(($current - $ts) > 5000) {
    http_response_code(403);
    echo json_encode(["message" => "expired request"]);
    return false;
}

$url = "http://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];

if($auth["url"] != $url) {
    http_response_code(403);
    echo json_encode(["message" => "wrong url"]);
    return false;
}

Jika semua pengujian sebelumnya telah memenuhi syarat, kita bisa lanjutkan dengan mencari user yang login berdasarkan username yang disebutkan. Normalnya proses ini dilakukan dengan mengirimkan perintah query ke database, tapi dalam kode ini kita hanya menggunakan array yang terdiri atas 2 elemen data user. Proses pencarian dilakukan dengan menggunakan perulangan dan menguji nilai username dari tiap elemen user. Jika user tidak ditemukan, kode PHP akan mengembalikan response error 403 dengan pesan "user tidak ditemukan".

$users = [
    [ "username" => "budi",     "password" => "inibudisaja" ],
    [ "username" => "admin" ,   "password" => "n1md4" ]
];

$current_user = null;
for($i=0; $i<count($users); $i++) {
    if($users[$i]["username"] == $auth["username"]) {
        $current_user = $users[$i];
        break;
    }
}

if(!$current_user) {
    http_response_code(403);
    echo json_encode(["message" => "user tidak ditemukan"]);
    return false;
}

Setelah mendapatkan user, berikutnya kita tinggal validasi password dari user tersebut dengan mencocokkan hash dari informasi login dengan hash yang dikirimkan dari client. Langkah pertama adalah dengan meng-encode informasi login (variabel $auth) dari associative array menjadi JSON

$jsAuth = json_encode($auth, JSON_UNESCAPED_SLASHES);

Informasi login dalam format JSON tadi, di-hash menggunakan fungsi PHP hash_hmac() dan password yang tersimpan di sisi server. Proses ini akan menghasilkan hash dalam bentuk text binary yang harus di-encode lagi menjadi Base64 (hash yang dikirim dari client juga menggunakan Base64).

$hash = base64_encode(hash_hmac("sha512", $jsAuth, $current_user["password"], $raw_output=true));

Selebihnya kita tinggal mencocokkan hash dalam bentuk Base64 tadi dengan hash yang dikirim dari client, jika kedua hash tidak sama, maka password yang dimasukkan oleh user salah, sebaliknya login kita nyatakan benar.

if($hash != $body["hash"]) {
    http_response_code(403);
    echo json_encode(["message" => "password yang anda masukkan salah"]);
    return false;
}

echo json_encode([ "message" => "login berhasil" ]);

Kode PHP selengkapnya adalah sebagai berikut :

<?php

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

    if($_SERVER["REQUEST_METHOD"] != "POST") {
        http_response_code(405);
        echo json_encode(["message" => "method not allowed"]);
        return false;
    }

    $body = json_decode(file_get_contents("php://input"), $assoc=true);

    if((!isset($body["auth"])) || (!isset($body["hash"]))) {
        http_response_code(400);
        echo json_encode(["message" => "incomplete request"]);
        return false;
    }

    $auth = json_decode(base64_decode($body["auth"]), $assoc=true);

    $current = time() * 1000;
    $ts = $auth["timestamp"];

    if(($current - $ts) > 5000) {
        http_response_code(403);
        echo json_encode(["message" => "expired request"]);
        return false;
    }

    $url = "http://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];

    if($auth["url"] != $url) {
        http_response_code(403);
        echo json_encode(["message" => "wrong url"]);
        return false;
    }

    $users = [
        [ "username" => "budi",     "password" => "inibudisaja" ],
        [ "username" => "admin" ,   "password" => "n1md4" ]
    ];

    $current_user = null;
    for($i=0; $i<count($users); $i++) {
        if($users[$i]["username"] == $auth["username"]) {
            $current_user = $users[$i];
            break;
        }
    }

    if(!$current_user) {
        http_response_code(403);
        echo json_encode(["message" => "user tidak ditemukan"]);
        return false;
    }

    $jsAuth = json_encode($auth, JSON_UNESCAPED_SLASHES);

    $hash = base64_encode(hash_hmac("sha512", $jsAuth, $current_user["password"], $raw_output=true));

    if($hash != $body["hash"]) {
        http_response_code(403);
        echo json_encode(["message" => "password yang anda masukkan salah"]);
        return false;
    }

    echo json_encode([ "message" => "login berhasil" ]);

?>