Karyawan masuk ke aplikasimu dengan akun JS Staff-nya. Aplikasimu tidak pernah melihat passwordnya.
{
"user_id": 3,
"nama": "Sela",
"email": "sela@contoh.com",
"peran": "staff",
"tenant_id": 1,
"tenant_nama": "Jidanshoppu",
"tenant_slug": "jidanshoppu"
}
Cuma itu. peran berisi staff, supervisor, atau owner. Aplikasimu tidak bisa membaca password, gaji, absen, kasbon, atau rekening siapa pun.
Ke admin platform JS Staff (mereka mendaftarkannya di /super/apps.php):
| Nilai | Contoh |
|---|---|
client_id | app_a1b2c3d4e5f6a7b8 |
client_secret | sec_1a2b3c… — cuma ditampilkan sekali |
Kamu harus menyebutkan redirect URI-mu saat mendaftar. Aturannya keras:
https://<?php // sso-config.php — JANGAN di-commit. Isi dari env var. const SSO_BASE = 'https://staff.jidanshoppu.com'; const SSO_CLIENT_ID = 'app_xxxxxxxxxxxxxxxx'; const SSO_SECRET = 'sec_xxxxxxxxxxxxxxxx'; const SSO_REDIRECT = 'https://order.jidanshoppu.com/sso-callback.php';
<?php
// sso-mulai.php
require 'sso-config.php';
session_start();
/*
* `state` WAJIB. Acak, disimpan di sesi, dicocokkan lagi di callback.
* Tanpa ini, penyerang bisa mengirim link callback berisi kode MILIK DIA ke korban —
* korban jadi login sebagai akun penyerang, lalu mengetik data ke akun itu tanpa sadar.
*/
$_SESSION['sso_state'] = bin2hex(random_bytes(16));
$url = SSO_BASE . '/sso/authorize.php?' . http_build_query([
'client_id' => SSO_CLIENT_ID,
'redirect_uri' => SSO_REDIRECT,
'state' => $_SESSION['sso_state'],
]);
header('Location: ' . $url);
exit;
Tombolnya cukup: <a href="/sso-mulai.php">Masuk pakai JS Staff</a>
<?php
// sso-callback.php
require 'sso-config.php';
session_start();
// --- Orangnya menolak di layar persetujuan ---
if (isset($_GET['error'])) {
exit('Login dibatalkan.');
}
// --- Cocokkan state DULU, sebelum menyentuh code ---
$state = (string)($_GET['state'] ?? '');
if ($state === ''
|| !isset($_SESSION['sso_state'])
|| !hash_equals($_SESSION['sso_state'], $state)) {
// hash_equals, bukan ===: perbandingan biasa berhenti di huruf pertama yang beda,
// dan selisih waktunya bisa dipakai menebak nilainya.
exit('State tidak cocok. Ulangi dari awal.');
}
unset($_SESSION['sso_state']); // sekali pakai
$code = (string)($_GET['code'] ?? '');
if ($code === '') {
exit('Kode tidak ada.');
}
// --- Tukar kode jadi identitas (SERVER ke server) ---
$ch = curl_init(SSO_BASE . '/sso/token.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => http_build_query([
'client_id' => SSO_CLIENT_ID,
'client_secret' => SSO_SECRET,
'code' => $code,
'redirect_uri' => SSO_REDIRECT, // harus sama dengan langkah 3
]),
]);
$body = curl_exec($ch);
$kode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($kode !== 200) {
// Jangan tampilkan $body apa adanya ke pengguna — itu pesan untuk kamu, bukan dia.
error_log('SSO gagal: ' . $kode . ' ' . $body);
exit('Login gagal. Coba lagi.');
}
$data = json_decode((string)$body, true);
if (!is_array($data) || empty($data['ok'])) {
exit('Login gagal. Coba lagi.');
}
$u = $data['user'];
// --- Mulai dari sini urusan kamu ---
session_regenerate_id(true); // cegah session fixation
$_SESSION['jss_user_id'] = $u['user_id'];
$_SESSION['jss_tenant_id'] = $u['tenant_id'];
$_SESSION['nama'] = $u['nama'];
$_SESSION['peran'] = $u['peran'];
header('Location: /app.php');
exit;
client_secret yang lewat browser sama saja dengan secret yang dibagikan ke semua orang. Referer. user_id unik global, tenant_id menandai perusahaannya. Dua-duanya stabil — tidak pernah berubah walau namanya diganti.
jss_user_id.ALTER TABLE staff ADD COLUMN jss_user_id INTEGER NULL; ALTER TABLE staff ADD COLUMN jss_tenant_id INTEGER NULL; CREATE UNIQUE INDEX uq_staff_jss ON staff (jss_user_id);
Alur pertama kali:
jss_user_id = :user_id → ketemu? login.tenant_slug ke toko di sisimu, buat akun stafnya, simpan jss_user_id-nya.tenant_slug tidak dikenal? Tolak. Jangan membuat toko baru diam-diam dari data SSO — siapa pun yang punya akun JS Staff bisa memicunya.state: acak, di sesi, dicocokkan dengan hash_equals, sekali pakai.redirect_uri sama persis di langkah 3 dan 4.| Gejala | Sebabnya biasanya |
|---|---|
| Halaman "Alamat balik tidak cocok" | redirect_uri beda dengan yang didaftarkan. Cek trailing slash, http vs https, query string. |
| Halaman "Aplikasi tidak dikenal" | client_id salah, atau aplikasinya dicabut admin platform. |
401 di /sso/token.php |
Kode kedaluwarsa (>60 detik), sudah dipakai, client_secret salah, atau redirect_uri beda dengan langkah 3. |
Balik dengan ?error=access_denied |
Karyawannya menekan Tolak. Bukan bug. |
401 sengaja memakai pesan yang sama untuk semua sebab — membedakan "kode salah" dari "kode sudah dipakai" memberi penyerang cara memetakan kode mana yang pernah ada, dan tidak membantu siapa pun yang integrasinya benar. Cek keempat hal di atas satu per satu.