Los empleados inician sesión en tu app con su cuenta de JS Staff. Tu app nunca ve su contraseña.
{
"user_id": 3,
"nama": "Sela",
"email": "sela@contoh.com",
"peran": "staff",
"tenant_id": 1,
"tenant_nama": "Jidanshoppu",
"tenant_slug": "jidanshoppu"
}
Eso es todo. peran (rol) contiene staff, supervisor u owner. Tu app no puede leer la contraseña, el sueldo, la asistencia, los adelantos ni la cuenta bancaria de nadie.
Del administrador de la plataforma JS Staff (lo registran en /super/apps.php):
| Valor | Ejemplo |
|---|---|
client_id | app_a1b2c3d4e5f6a7b8 |
client_secret | sec_1a2b3c… — se muestra solo una vez |
Debes indicar tu redirect URI al registrarte. La regla es estricta:
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;
El botón basta: <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 que pasa por el navegador es lo mismo que un secreto compartido con todo el mundo. Referer. user_id es único a nivel global, tenant_id marca la empresa. Ambos son estables — nunca cambian aunque cambien los nombres.
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);
Flujo la primera vez:
jss_user_id = :user_id → ¿la encontraste? inicia sesión.tenant_slug con una tienda de tu lado, crea la cuenta del empleado, guarda su jss_user_id.tenant_slug no reconocido? Recházalo. No crees una tienda nueva en silencio a partir de datos de SSO — cualquiera con una cuenta de JS Staff podría provocarlo.state: aleatorio, en la sesión, comparado con hash_equals, de un solo uso.redirect_uri debe ser idéntico en los pasos 3 y 4.| Síntoma | Suele deberse a |
|---|---|
| Página "La dirección de retorno no coincide" | redirect_uri difiere del registrado. Revisa la barra final, http vs https, el query string. |
| Página "Aplicación desconocida" | client_id es incorrecto, o la app fue revocada por el administrador de la plataforma. |
401 en /sso/token.php |
El código caducó (>60 segundos), ya se usó, client_secret es incorrecto, o redirect_uri difiere del paso 3. |
Vuelve con ?error=access_denied |
El empleado pulsó Rechazar. No es un error. |
401 usa deliberadamente el mismo mensaje para todas las causas — distinguir "código incorrecto" de "código ya usado" le da a un atacante una forma de mapear qué códigos existieron alguna vez, y no ayuda a nadie cuya integración sea correcta. Revisa las cuatro cosas de arriba una por una.