← API notifikasi JS Staff

Pasang "Masuk pakai JS Staff"

Karyawan masuk ke aplikasimu dengan akun JS Staff-nya. Aplikasimu tidak pernah melihat passwordnya.

Yang kamu dapat

{
  "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.

Kenapa redirect, bukan API "kirim email+password". API seperti itu akan membatalkan OTP perangkat baru yang dipakai portal ini. Seluruh gunanya OTP adalah password saja tidak cukup untuk masuk — begitu ada satu pintu yang menerima password saja, pencuri password tinggal lewat pintu itu.

Dengan redirect, tidak ada pintu seperti itu: loginnya selalu terjadi di halaman kami, lengkap dengan OTP dan perangkat tepercayanya.

Alurnya

[1] Karyawan klik "Masuk pakai JS Staff" di aplikasimu ↓ [2] Kamu lempar dia ke https://staff.jidanshoppu.com/sso/authorize.php?... ↓ (dia login di sana + OTP kalau perangkat baru + layar persetujuan) [3] Dia balik ke sso-callback.php?code=...&state=... ↓ [4] SERVER kamu tukar code + secret ke /sso/token.php ← server-ke-server ↓ [5] Dapat identitasnya → kamu buat sesi kamu sendiri

1 · Minta dua nilai ini

Ke admin platform JS Staff (mereka mendaftarkannya di /super/apps.php):

NilaiContoh
client_idapp_a1b2c3d4e5f6a7b8
client_secretsec_1a2b3c… — cuma ditampilkan sekali

Kamu harus menyebutkan redirect URI-mu saat mendaftar. Aturannya keras:

Kenapa sekeras itu. Ini pagar terpenting di seluruh alur SSO. Kalau dilonggarkan sedikit saja — prefix, subdomain, wildcard — penyerang cukup mengirim link authorize dengan alamat baliknya sendiri ke karyawanmu. Korban login di halaman asli kami seperti biasa, tidak ada yang mencurigakan, dan kodenya mendarat di server penyerang.

2 · Konfigurasi

<?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 · Tombol masuk

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

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;
Langkah ini harus dari SERVER, bukan browser. client_secret yang lewat browser sama saja dengan secret yang dibagikan ke semua orang.

Tukar kodenya segera. Umurnya 60 detik dan sekali pakai — dia lewat URL browser, dan URL mendarat di history, log server, dan header Referer.

5 · Memetakan ke akun di sisimu

user_id unik global, tenant_id menandai perusahaannya. Dua-duanya stabil — tidak pernah berubah walau namanya diganti.

Jangan mencocokkan lewat email. Email bisa diganti owner, dan mencocokkan lewat email berarti akun bisa berpindah tangan diam-diam begitu emailnya diubah. Simpan 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:

  1. Cari akun dengan jss_user_id = :user_id → ketemu? login.
  2. Tidak ketemu? Cocokkan tenant_slug ke toko di sisimu, buat akun stafnya, simpan jss_user_id-nya.
  3. tenant_slug tidak dikenal? Tolak. Jangan membuat toko baru diam-diam dari data SSO — siapa pun yang punya akun JS Staff bisa memicunya.

Yang wajib & yang tidak perlu

Wajib

Tidak perlu

Kalau gagal

GejalaSebabnya 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.

Menguji

  1. Minta admin platform mendaftarkan aplikasi kedua khusus tes.
  2. Login pakai akun staff betulan.
  3. Layar persetujuan harus muncul sekali. Kedua kalinya harus langsung lewat.
  4. Cabut izinnya di Profil Saya → Aplikasi yang boleh memakai akun kamu → layar persetujuan harus muncul lagi.