Mitarbeiter melden sich mit ihrem JS-Staff-Konto bei deiner App an. Deine App sieht niemals ihr Passwort.
{
"user_id": 3,
"nama": "Sela",
"email": "sela@contoh.com",
"peran": "staff",
"tenant_id": 1,
"tenant_nama": "Jidanshoppu",
"tenant_slug": "jidanshoppu"
}
Das ist alles. peran (Rolle) enthält staff, supervisor oder owner. Deine App kann nicht das Passwort, Gehalt, die Anwesenheit, Vorschüsse oder das Bankkonto von irgendjemandem lesen.
Vom Plattform-Administrator von JS Staff (er registriert es unter /super/apps.php):
| Wert | Beispiel |
|---|---|
client_id | app_a1b2c3d4e5f6a7b8 |
client_secret | sec_1a2b3c… — wird nur einmal angezeigt |
Du musst deine Redirect-URI bei der Registrierung angeben. Die Regel ist streng:
https:// sein<?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;
Der Button genügt: <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, das durch den Browser läuft, ist dasselbe wie ein mit jedem geteiltes Geheimnis. Referer-Header. user_id ist global eindeutig, tenant_id kennzeichnet die Firma. Beide sind stabil — sie ändern sich nie, auch wenn sich Namen ändern.
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);
Ablauf beim ersten Mal:
jss_user_id = :user_id → gefunden? anmelden.tenant_slug mit einem Shop auf deiner Seite ab, erstelle das Mitarbeiterkonto, speichere dessen jss_user_id.tenant_slug unbekannt? Ablehnen. Erstelle nicht stillschweigend einen neuen Shop aus SSO-Daten — das könnte jeder mit einem JS-Staff-Konto auslösen.state: zufällig, in der Sitzung, mit hash_equals verglichen, einmalig.redirect_uri muss in Schritt 3 und 4 identisch sein.| Symptom | Meist verursacht durch |
|---|---|
| Seite „Rückgabeadresse stimmt nicht überein“ | redirect_uri weicht vom registrierten Wert ab. Prüfe abschließenden Schrägstrich, http vs. https, Query-String. |
| Seite „Unbekannte Anwendung“ | client_id ist falsch, oder die App wurde vom Plattform-Administrator widerrufen. |
401 bei /sso/token.php |
Code abgelaufen (>60 Sekunden), bereits verwendet, client_secret falsch, oder redirect_uri weicht von Schritt 3 ab. |
Kehrt zurück mit ?error=access_denied |
Der Mitarbeiter hat auf Ablehnen geklickt. Kein Bug. |
401 verwendet absichtlich dieselbe Meldung für jede Ursache — die Unterscheidung von „falscher Code“ und „Code bereits verwendet“ würde einem Angreifer verraten, welche Codes je existiert haben, und hilft niemandem, dessen Integration korrekt ist. Prüfe die vier obigen Punkte einzeln.