Os funcionários entram no seu app com a conta JS Staff deles. Seu app nunca vê a senha deles.
{
"user_id": 3,
"nama": "Sela",
"email": "sela@contoh.com",
"peran": "staff",
"tenant_id": 1,
"tenant_nama": "Jidanshoppu",
"tenant_slug": "jidanshoppu"
}
Só isso. peran (cargo) contém staff, supervisor ou owner. Seu app não pode ler a senha, salário, presença, adiantamentos ou conta bancária de ninguém.
Do administrador da plataforma JS Staff (eles cadastram em /super/apps.php):
| Valor | Exemplo |
|---|---|
client_id | app_a1b2c3d4e5f6a7b8 |
client_secret | sec_1a2b3c… — mostrado apenas uma vez |
Você deve informar seu redirect URI ao se cadastrar. A regra é rígida:
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;
O botão basta: <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 que passa pelo navegador é o mesmo que um segredo compartilhado com todo mundo. Referer. user_id é único globalmente, tenant_id marca a empresa. Ambos são estáveis — nunca mudam mesmo que os nomes mudem.
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);
Fluxo na primeira vez:
jss_user_id = :user_id → encontrou? faça login.tenant_slug a uma loja do seu lado, crie a conta do funcionário, guarde o jss_user_id dele.tenant_slug não reconhecido? Rejeite. Não crie uma loja nova silenciosamente a partir de dados do SSO — qualquer pessoa com conta JS Staff poderia provocar isso.state: aleatório, na sessão, comparado com hash_equals, uso único.redirect_uri deve ser idêntico nas etapas 3 e 4.| Sintoma | Geralmente causado por |
|---|---|
| Página "Endereço de retorno não corresponde" | redirect_uri difere do que foi cadastrado. Verifique a barra final, http vs https, query string. |
| Página "Aplicativo desconhecido" | client_id está errado, ou o app foi revogado pelo admin da plataforma. |
401 em /sso/token.php |
Código expirado (>60 segundos), já usado, client_secret errado, ou redirect_uri diferente da etapa 3. |
Volta com ?error=access_denied |
O funcionário clicou em Recusar. Não é um bug. |
401 usa deliberadamente a mesma mensagem para todas as causas — distinguir "código errado" de "código já usado" dá ao atacante uma forma de mapear quais códigos já existiram, e não ajuda ninguém cuja integração esteja correta. Verifique as quatro coisas acima uma por uma.