Сотрудники входят в ваше приложение через аккаунт JS Staff. Ваше приложение никогда не видит их пароль.
{
"user_id": 3,
"nama": "Sela",
"email": "sela@contoh.com",
"peran": "staff",
"tenant_id": 1,
"tenant_nama": "Jidanshoppu",
"tenant_slug": "jidanshoppu"
}
Это всё. peran (роль) содержит staff, supervisor или owner. Ваше приложение не может читать чей-либо пароль, зарплату, посещаемость, авансы или банковский счёт.
От администратора платформы JS Staff (они регистрируют его в /super/apps.php):
| Значение | Пример |
|---|---|
client_id | app_a1b2c3d4e5f6a7b8 |
client_secret | sec_1a2b3c… — показывается только один раз |
При регистрации нужно указать ваш redirect URI. Правило строгое:
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;
Достаточно кнопки: <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, проходящий через браузер, — это то же самое, что секрет, известный всем. Referer. user_id уникален глобально, tenant_id обозначает компанию. Оба стабильны — не меняются, даже если меняются имена.
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);
Процесс при первом входе:
jss_user_id = :user_id → нашли? войдите.tenant_slug с магазином на вашей стороне, создайте аккаунт сотрудника, сохраните его jss_user_id.tenant_slug не распознан? Отклоните. Не создавайте молча новый магазин из данных SSO — это может спровоцировать кто угодно с аккаунтом JS Staff.state: случайный, в сессии, сверяется через hash_equals, одноразовый.redirect_uri должен быть одинаковым на шагах 3 и 4.| Симптом | Обычно причина |
|---|---|
| Страница «Адрес возврата не совпадает» | redirect_uri отличается от зарегистрированного. Проверьте завершающий слэш, http/https, строку запроса. |
| Страница «Приложение неизвестно» | client_id неверен, или приложение отозвано администратором платформы. |
401 на /sso/token.php |
Код истёк (>60 секунд), уже использован, client_secret неверен, или redirect_uri отличается от шага 3. |
Возврат с ?error=access_denied |
Сотрудник нажал Отклонить. Это не баг. |
401 намеренно использует одно и то же сообщение для всех причин — различение «неверный код» и «код уже использован» даёт злоумышленнику способ узнать, какие коды вообще существовали, и не помогает тем, у кого интеграция верна. Проверьте четыре пункта выше по очереди.