← API notifikasi JS Staff

Configurer « Se connecter avec JS Staff »

Les employés se connectent à votre application avec leur compte JS Staff. Votre application ne voit jamais leur mot de passe.

Ce que vous obtenez

{
  "user_id": 3,
  "nama": "Sela",
  "email": "sela@contoh.com",
  "peran": "staff",
  "tenant_id": 1,
  "tenant_nama": "Jidanshoppu",
  "tenant_slug": "jidanshoppu"
}

C’est tout. peran (rôle) contient staff, supervisor ou owner. Votre application ne peut pas lire le mot de passe, le salaire, la présence, les avances ou le compte bancaire de qui que ce soit.

Pourquoi une redirection, pas une API « envoyer e-mail+mot de passe ». Une telle API annulerait l’OTP nouvel appareil dont dépend ce portail. Tout l’intérêt de l’OTP est qu’un mot de passe seul ne suffit pas pour se connecter — dès qu’une porte accepte le mot de passe seul, un voleur de mot de passe n’a qu’à passer par cette porte.

Avec une redirection, cette porte n’existe pas : la connexion se produit toujours sur notre page, avec son OTP et ses appareils de confiance.

Le déroulement

[1] L’employé clique sur « Se connecter avec JS Staff » dans votre application ↓ [2] Vous le redirigez vers https://staff.jidanshoppu.com/sso/authorize.php?... ↓ (il se connecte là-bas + OTP si nouvel appareil + écran de consentement) [3] Il revient vers sso-callback.php?code=...&state=... ↓ [4] Votre SERVEUR échange code + secret sur /sso/token.php ← serveur à serveur ↓ [5] Vous obtenez son identité → vous créez votre propre session

1 · Demandez ces deux valeurs

Auprès de l’administrateur de la plateforme JS Staff (il l’enregistre sur /super/apps.php) :

ValeurExemple
client_idapp_a1b2c3d4e5f6a7b8
client_secretsec_1a2b3c… — affiché une seule fois

Vous devez indiquer votre redirect URI lors de l’inscription. La règle est stricte :

Pourquoi si strict. C’est la barrière la plus importante de tout le flux SSO. Assouplissez-la ne serait-ce qu’un peu — préfixe, sous-domaine, joker — et un attaquant n’a qu’à envoyer à votre employé un lien d’autorisation avec sa propre adresse de retour. La victime se connecte sur notre page authentique comme d’habitude, rien ne paraît suspect, et le code atterrit sur le serveur de l’attaquant.

2 · Configuration

<?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 · Bouton de connexion

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

Le bouton suffit : <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;
Cette étape doit venir du SERVEUR, pas du navigateur. Un client_secret qui transite par le navigateur équivaut à un secret partagé avec tout le monde.

Échangez le code immédiatement. Il vit 60 secondes et est à usage unique — il transite par l’URL du navigateur, et les URL finissent dans l’historique, les journaux du serveur et l’en-tête Referer.

5 · Correspondance avec un compte de votre côté

user_id est unique au niveau global, tenant_id identifie l’entreprise. Les deux sont stables — ils ne changent jamais, même si les noms changent.

Ne faites pas correspondre par e-mail. L’e-mail peut être changé par le propriétaire, et faire correspondre par e-mail signifie qu’un compte peut changer de mains silencieusement dès que l’e-mail est modifié. Enregistrez 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);

Flux la première fois :

  1. Recherchez le compte avec jss_user_id = :user_id → trouvé ? connectez-le.
  2. Pas trouvé ? Faites correspondre tenant_slug à une boutique de votre côté, créez le compte du membre, enregistrez son jss_user_id.
  3. tenant_slug non reconnu ? Rejetez-le. Ne créez pas silencieusement une nouvelle boutique à partir des données SSO — n’importe qui avec un compte JS Staff pourrait le déclencher.

Ce qui est obligatoire et ce qui ne l’est pas

Obligatoire

Non nécessaire

En cas d’échec

SymptômeGénéralement causé par
Page « L’adresse de retour ne correspond pas » redirect_uri diffère de celui enregistré. Vérifiez la barre oblique finale, http vs https, la chaîne de requête.
Page « Application inconnue » client_id est incorrect, ou l’application a été révoquée par l’administrateur de la plateforme.
401 sur /sso/token.php Code expiré (>60 secondes), déjà utilisé, client_secret incorrect, ou redirect_uri différent de l’étape 3.
Revient avec ?error=access_denied L’employé a cliqué sur Refuser. Ce n’est pas un bug.

401 utilise délibérément le même message pour toutes les causes — distinguer « code incorrect » de « code déjà utilisé » donnerait à un attaquant un moyen de cartographier les codes ayant existé, et n’aide personne dont l’intégration est correcte. Vérifiez les quatre points ci-dessus un par un.

Test

  1. Demandez à l’administrateur de la plateforme d’enregistrer une deuxième application uniquement pour les tests.
  2. Connectez-vous avec un vrai compte employé.
  3. L’écran de consentement doit apparaître une fois. La deuxième fois, il doit passer directement.
  4. Révoquez l’accès dans Mon profil → Applications autorisées à utiliser votre compte → l’écran de consentement doit réapparaître.