Membereskan Masalah Authorization Code Flow di Laravel Passport

Cerita ini bermula dari tulisan integrasi custom OAuth2 dengan Moodle menggunakan Laravel Passport. Terdapat masalah (bug) saat menggunakan mekanisme Authorization Code Flow di Laravel Passport.

Mari kita ibaratkan Moodle sebagai penyedia layanan (Service Provider) dan OAuth2 ini sebagai penyedia identitas (Indentity Provider). Moodle dengan situs moodle.tld dan OAuth2 dengan situs oauth2.tld.

Apa? OAuth2 sebagai penyedia identitas? Ya, kamu ngga salah baca. Sebenarnya yang benar adalah menggunakan OpenID Connect (Semacam lapisan kecil untuk di atas OAuth2). Cuma, demi kenyamanan bersama saya anggap OAuth2 sebagai penyedia identitas.

Jika Anda penasaran apa bedanya OAuth2 dengan OpenID Connect silakan tonton video berjudul “An Illustrated Guide to OAuth and OpenID Connect” dari tim Okta Dev.

Kembali ke topik, jadi begini kondisi sebelumnya yang terjadi ketika menghubungkan melakukan autentikasi di Moodle dengan custom OAuth2.

Kondisi ini terjadi dalam satu tab di browser

  1. Pengguna menuju ke halaman login moodle moodle.tld/login.
  2. Pengguna mengklik tombol login Testing OAuth.
  3. Pengguna diarahkan ke website OAuth2 untuk melakukan otorisasi. Namun, karena pengguna belum login ke website OAuth2 maka di arahkan ke halaman login oauth2 oauth2.tld/login.
  4. Setelah berhasil login ke website OAuth2, mestinya di arahkan ke tautan melakukan otorisasi oauth/authorize?client_id=4&response_type=code&redirect_uri=https%3A%2F%2Fmoodle.tld%2Fadmin%2Foauth2callback.php&state=%2Fauth%2Foauth2%2Flogin.php%3Fwantsurl%3Dhttps%253A%252F%252Fmoodle.tld%252F%26sesskey%3DEmccWYKIqQ%26id%3D1&scope=identity . Namun, yang terjadi adalah pengguna berada di halaman home sebagai penanda bahwa dia telah login sehingga mekanisme otorisasi jadi terputus di tengah jalan. Jadinya, mentok sampai di sini.
  5. Akhirnya, pengguna mengulang menuju ke halaman login moodle moodle.tld/login.
  6. Pengguna mengulang mengklik tombol login Testing OAuth.
  7. Pengguna di arahkan ke website OAuth2 untuk melakukan otorisasi. Karena pengguna sudah login sebelumnya maka langsung di arahkan tautan melakukan otorisasi

    oauth/authorize?client_id=4&response_type=code&redirect_uri=https%3A%2F%2Fmoodle.tld%2Fadmin%2Foauth2callback.php&state=%2Fauth%2Foauth2%2Flogin.php%3Fwantsurl%3Dhttps%253A%252F%252Fmoodle.tld%252F%26sesskey%3DEmccWYKIqQ%26id%3D1&scope=identity

    untuk persetujuan apakah Moodle diijinkan untuk mendapatkan akses identitas milik kita dari OAuth2 server tadi.

  8. Pengguna mengklik tombol Autorize untuk memberikan ijin Moodle mendapatkan data identitas kita.

Halaman permintaan otorisasi di Laravel Passport

  1. Pengguna di arahkan ke halaman pengguna Moodle moodle.tld/my.

Di atas membutuhkan 9 langkah untuk login ke Moodle menggunakan OAuth2. Hal yang mengganjal terjadi pada langkah ke-empat. Jika kita berhasil memperbaiki bug di masalah otorisasi di Laravel Passport maka hanya dibutuhkan 6 langkah saja.

Pertama, kita analisa tautan mengarah ke halaman otorisasi. Tautannya adalah oauth/authorize?blablabla. Kita cek di Laravel Passport, apakah ada middleware yang menunggangi tautan oauth/authorize?

Kita jalankan perintah php artisan route:list dan ternyata tautan oauth/authorize ditunggangi oleh middlewre web dan auth. Kita fokus ke middleware auth karena dia bertugas sebagai pencegat jika user belum login maka akan diarahkan ke tautan login.

Hasil dari PHP artisan route list untuk oauth authorize

Middleware auth berasal dari file Authenticate.php yang terletak pada direktori app/Http/Middleware/Authenticate.php. Kita perhatikan isi method redirectTo().

protected function redirectTo($request)
{
    if (! $request->expectsJson()) {
        return route('login');
    }
}

Kalau request di atas tidak meminta respon berupa JSON maka arahkan ke tautan login. Dari sinilah kita bisa membedah isi dari method redirectTo() menjadi seperti di bawah ini.

protected function redirectTo($request)
{
    if (! $request->expectsJson()) {
      // Start modified line
        if ($request->path() === 'oauth/authorize') {
            if (isset($request->query()['client_id'])) {
                $params = array(
                    'client_id' => $request->query()['client_id'],
                    'return_to' => \Request::getRequestUri(),
                );
                return route('login', $params);
            } else {
                return route('login');
            }
        }
        // End modified line
        return route('login');
    }
}
  1. Cek apakah request yang dituju bukan meminta respon JSON?
  2. Jika iya, cek apakah path url nya adalah oauth/authorize?
  3. Jika benar, cek apakah ada query param bernama client_id?
  4. Jika ada, buat sebuah array bernama $params yang isinya adalah client_id dan return_to.
  5. Kemudian, tempelkan sebagai query string param di route login route('login', $params). Sehingga jadinya seperti ini: login?client_id=4&return_to=/oauth/authorize?blablabla.

Jika di langkah ketiga tidak ada query param bernama client_id maka arahkan dia ke route login tanpa query string param.

Perhatikan pula bahwa route login di laravel ini ada dua method yakni GET dan POST. Yang di atas tadi mengarah ke route login dengan method GET.

Berikutnya, kita override method showLoginForm() dari file LoginController.php di direktori app/Http/Controllers/Auth/LoginController.php. File LoginController.php ini dihasilkan dari perintah php artisan make:auth.

public function showLoginForm(Request $request)
{
    return view('auth.login', array('request_uri' => \Request::getRequestUri()));
}

Di method showLoginForm() akan menampilkan view bernama login.blade.php yang terletak di direktori resources/views/auth/login.blade.php dan menyisipkan data bernama request_uri. Nilai dari request_uri ini adalah login?client_id=4&return_to=/oauth/authorize?blablabla.

Berikutnya, kita modifikasi ganti nilai dari action pada form elemen di file login.blade.php.

<!--- Sebelum --->
<form method="POST" action="{{ route('login') }}">
<!--- Sesudah --->
<form method="POST" action="{{ $request_uri }}">

Berikutnya, kita perlu memodifikasi method login() di file LoginController agar ketika user berhasil login maka di arahkan ke tautan otorisasi.

public function login(Request $request)
{
    // Kita berasumsi bahwa pengguna sudah terautentikasi 
    // dan berikutnya akan diarahkan ke halaman mana berdasarkan request
    if (isset($request->query()['return_to'])) {
        return redirect($request->query()['return_to']);
    } else {
        return redirect('/home');
    }
}

Yup, selesai. Sekarang silakan Anda coba tes alur yang saya buat di bawah ini.

  1. Pengguna menuju ke halaman login moodle moodle.tld/login.
  2. Pengguna mengklik tombol login Testing OAuth.
  3. Pengguna diarahkan ke website OAuth2 untuk melakukan otorisasi. Namun, karena pengguna belum login ke website OAuth2 maka di arahkan ke halaman login oauth2 oauth2.tld/login?client_id=4&return_to=/oauth/authorize?blablabla.
  4. Setelah berhasil login ke website OAuth2, pengguna di arahkan ke tautan melakukan otorisasi oauth/authorize?client_id=4&response_type=code&redirect_uri=https%3A%2F%2Fmoodle.tld%2Fadmin%2Foauth2callback.php&state=%2Fauth%2Foauth2%2Flogin.php%3Fwantsurl%3Dhttps%253A%252F%252Fmoodle.tld%252F%26sesskey%3DEmccWYKIqQ%26id%3D1&scope=identity . untuk persetujuan apakah Moodle diijinkan untuk mendapatkan akses identitas milik kita dari OAuth2 server tadi.

  5. Pengguna mengklik tombol Autorize untuk memberikan ijin Moodle mendapatkan data identitas kita.

Halaman permintaan otorisasi di Laravel Passport

  1. Pengguna di arahkan ke halaman pengguna Moodle moodle.tld/my.

Saya mencari jawaban di issue Laravel Passport terkait Passport doesn’t redirect back to authorization form after login di issue nomor #248 dan #703. Namun, tidak ketemu jawabannya.

Alhasil saya teringat pernah mengimplementasikan login ke web PHPBali dengan Github OAuth2 menggunakan Socialite. Dengan bermodalkan inspect network, ketelitian dalam membaca url address bar, mengecek middleware yang menunggangi oauth/authorize dan bermain fungsi dd() yang disediakan oleh Laravel akhirnya permasalahan berhasil diselesaikan.