← API notifikasi JS Staff

Configura "Iniciar sesión con JS Staff"

Los empleados inician sesión en tu app con su cuenta de JS Staff. Tu app nunca ve su contraseña.

Lo que obtienes

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

Eso es todo. peran (rol) contiene staff, supervisor u owner. Tu app no puede leer la contraseña, el sueldo, la asistencia, los adelantos ni la cuenta bancaria de nadie.

Por qué una redirección y no una API que "envíe email+contraseña". Una API así anularía el OTP de dispositivo nuevo del que depende este portal. Todo el sentido del OTP es que una contraseña sola no basta para entrar — en el momento en que una puerta acepta solo la contraseña, un ladrón de contraseñas simplemente pasa por esa puerta.

Con una redirección, esa puerta no existe: el login siempre ocurre en nuestra página, con su OTP y sus dispositivos de confianza incluidos.

El flujo

[1] El empleado hace clic en "Iniciar sesión con JS Staff" en tu app ↓ [2] Lo rediriges a https://staff.jidanshoppu.com/sso/authorize.php?... ↓ (inicia sesión ahí + OTP si es dispositivo nuevo + pantalla de consentimiento) [3] Vuelve a sso-callback.php?code=...&state=... ↓ [4] Tu SERVIDOR intercambia code + secret en /sso/token.php ← servidor a servidor ↓ [5] Obtienes su identidad → tú creas tu propia sesión

1 · Solicita estos dos valores

Del administrador de la plataforma JS Staff (lo registran en /super/apps.php):

ValorEjemplo
client_idapp_a1b2c3d4e5f6a7b8
client_secretsec_1a2b3c… — se muestra solo una vez

Debes indicar tu redirect URI al registrarte. La regla es estricta:

Por qué es tan estricto. Esta es la barrera más importante de todo el flujo de SSO. Relájala aunque sea un poco — prefijo, subdominio, comodín — y un atacante solo necesita enviarle a tu empleado un enlace de autorización con su propia dirección de retorno. La víctima inicia sesión en nuestra página genuina como siempre, nada parece sospechoso, y el código termina en el servidor del atacante.

2 · Configuración

<?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 · Botón de inicio de sesión

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

El botón basta: <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;
Este paso debe ser del SERVIDOR, no del navegador. Un client_secret que pasa por el navegador es lo mismo que un secreto compartido con todo el mundo.

Intercambia el código de inmediato. Vive 60 segundos y es de un solo uso — viaja por la URL del navegador, y las URL quedan en el historial, en los logs del servidor y en la cabecera Referer.

5 · Mapear a una cuenta de tu lado

user_id es único a nivel global, tenant_id marca la empresa. Ambos son estables — nunca cambian aunque cambien los nombres.

No emparejes por correo. El correo puede cambiarlo el propietario, y emparejar por correo significa que una cuenta puede cambiar de manos silenciosamente en cuanto se cambia el correo. Guarda 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);

Flujo la primera vez:

  1. Busca la cuenta con jss_user_id = :user_id → ¿la encontraste? inicia sesión.
  2. ¿No la encontraste? Empareja tenant_slug con una tienda de tu lado, crea la cuenta del empleado, guarda su jss_user_id.
  3. ¿tenant_slug no reconocido? Recházalo. No crees una tienda nueva en silencio a partir de datos de SSO — cualquiera con una cuenta de JS Staff podría provocarlo.

Lo obligatorio y lo que no es necesario

Obligatorio

No necesario

Si falla

SíntomaSuele deberse a
Página "La dirección de retorno no coincide" redirect_uri difiere del registrado. Revisa la barra final, http vs https, el query string.
Página "Aplicación desconocida" client_id es incorrecto, o la app fue revocada por el administrador de la plataforma.
401 en /sso/token.php El código caducó (>60 segundos), ya se usó, client_secret es incorrecto, o redirect_uri difiere del paso 3.
Vuelve con ?error=access_denied El empleado pulsó Rechazar. No es un error.

401 usa deliberadamente el mismo mensaje para todas las causas — distinguir "código incorrecto" de "código ya usado" le da a un atacante una forma de mapear qué códigos existieron alguna vez, y no ayuda a nadie cuya integración sea correcta. Revisa las cuatro cosas de arriba una por una.

Probando

  1. Pide al administrador de la plataforma que registre una segunda app solo para pruebas.
  2. Inicia sesión con una cuenta de personal real.
  3. La pantalla de consentimiento debe aparecer una vez. La segunda vez debe pasar directo.
  4. Revoca el acceso en Mi perfil → Apps autorizadas a usar tu cuenta → la pantalla de consentimiento debe aparecer de nuevo.