← API notifikasi JS Staff

Настройка «Войти через JS Staff»

Сотрудники входят в ваше приложение через аккаунт 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. Ваше приложение не может читать чей-либо пароль, зарплату, посещаемость, авансы или банковский счёт.

Почему редирект, а не API «отправить email+пароль». Такой API свёл бы на нет OTP для нового устройства, на который опирается портал. Весь смысл OTP в том, что одного пароля недостаточно для входа — как только появляется дверь, принимающая только пароль, похититель пароля просто проходит через неё.

С редиректом такой двери не существует: вход всегда происходит на нашей странице, вместе с OTP и доверенными устройствами.

Процесс

[1] Сотрудник нажимает «Войти через JS Staff» в вашем приложении ↓ [2] Вы перенаправляете его на https://staff.jidanshoppu.com/sso/authorize.php?... ↓ (он входит там + OTP при новом устройстве + экран согласия) [3] Он возвращается на sso-callback.php?code=...&state=... ↓ [4] Ваш СЕРВЕР обменивает code + secret на /sso/token.php ← сервер-сервер ↓ [5] Вы получаете его личность → вы создаёте свою сессию

1 · Запросите эти два значения

От администратора платформы JS Staff (они регистрируют его в /super/apps.php):

ЗначениеПример
client_idapp_a1b2c3d4e5f6a7b8
client_secretsec_1a2b3c… — показывается только один раз

При регистрации нужно указать ваш redirect URI. Правило строгое:

Почему это так строго. Это самый важный барьер во всём процессе SSO. Ослабьте его хоть немного — префикс, поддомен, маска — и злоумышленнику достаточно прислать вашему сотруднику ссылку авторизации со своим адресом обратного вызова. Жертва входит на нашей настоящей странице как обычно, ничего не выглядит подозрительным, а код попадает на сервер злоумышленника.

2 · Конфигурация

<?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';

3 · Кнопка входа

<?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>

4 · Callback

<?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, проходящий через браузер, — это то же самое, что секрет, известный всем.

Обменяйте код немедленно. Он живёт 60 секунд и одноразовый — он проходит через URL браузера, а URL попадают в историю, серверные логи и заголовок Referer.

5 · Сопоставление с аккаунтом на вашей стороне

user_id уникален глобально, tenant_id обозначает компанию. Оба стабильны — не меняются, даже если меняются имена.

Не сопоставляйте по email. Email может изменить владелец, и сопоставление по email означает, что аккаунт может незаметно перейти к другому владельцу в момент изменения email. Сохраняйте 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);

Процесс при первом входе:

  1. Найдите аккаунт с jss_user_id = :user_id → нашли? войдите.
  2. Не нашли? Сопоставьте tenant_slug с магазином на вашей стороне, создайте аккаунт сотрудника, сохраните его jss_user_id.
  3. tenant_slug не распознан? Отклоните. Не создавайте молча новый магазин из данных SSO — это может спровоцировать кто угодно с аккаунтом JS Staff.

Что обязательно, а что нет

Обязательно

Не требуется

Если не работает

СимптомОбычно причина
Страница «Адрес возврата не совпадает» redirect_uri отличается от зарегистрированного. Проверьте завершающий слэш, http/https, строку запроса.
Страница «Приложение неизвестно» client_id неверен, или приложение отозвано администратором платформы.
401 на /sso/token.php Код истёк (>60 секунд), уже использован, client_secret неверен, или redirect_uri отличается от шага 3.
Возврат с ?error=access_denied Сотрудник нажал Отклонить. Это не баг.

401 намеренно использует одно и то же сообщение для всех причин — различение «неверный код» и «код уже использован» даёт злоумышленнику способ узнать, какие коды вообще существовали, и не помогает тем, у кого интеграция верна. Проверьте четыре пункта выше по очереди.

Тестирование

  1. Попросите администратора платформы зарегистрировать второе приложение специально для тестов.
  2. Войдите под настоящим аккаунтом сотрудника.
  3. Экран согласия должен появиться один раз. Во второй раз он должен пропускаться сразу.
  4. Отзовите доступ в Мой профиль → Приложения, которым разрешено использовать ваш аккаунт → экран согласия должен появиться снова.