← API notifikasi JS Staff

Configurar "Entrar com JS Staff"

Os funcionários entram no seu app com a conta JS Staff deles. Seu app nunca vê a senha deles.

O que você recebe

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

Por que um redirecionamento, não uma API "enviar e-mail+senha". Uma API assim anularia o OTP de dispositivo novo do qual este portal depende. Todo o propósito do OTP é que uma senha sozinha não basta para entrar — no momento em que uma porta aceita só a senha, um ladrão de senhas simplesmente passa por ela.

Com redirecionamento, essa porta não existe: o login sempre acontece na nossa página, com OTP e seus dispositivos confiáveis.

O fluxo

[1] O funcionário clica em "Entrar com JS Staff" no seu app ↓ [2] Você o redireciona para https://staff.jidanshoppu.com/sso/authorize.php?... ↓ (ele faz login lá + OTP se for dispositivo novo + tela de consentimento) [3] Ele volta para sso-callback.php?code=...&state=... ↓ [4] Seu SERVIDOR troca code + secret em /sso/token.php ← servidor-a-servidor ↓ [5] Você recebe a identidade dele → você monta sua própria sessão

1 · Solicite estes dois valores

Do administrador da plataforma JS Staff (eles cadastram em /super/apps.php):

ValorExemplo
client_idapp_a1b2c3d4e5f6a7b8
client_secretsec_1a2b3c… — mostrado apenas uma vez

Você deve informar seu redirect URI ao se cadastrar. A regra é rígida:

Por que tão rígido. Esta é a barreira mais importante em todo o fluxo de SSO. Afrouxe um pouco que seja — prefixo, subdomínio, curinga — e um atacante só precisa enviar ao seu funcionário um link de autorização com o próprio endereço de retorno. A vítima faz login na nossa página genuína normalmente, nada parece suspeito, e o código cai no servidor do atacante.

2 · Configuração

<?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ão de login

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

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;
Esta etapa deve ser do SERVIDOR, não do navegador. Um client_secret que passa pelo navegador é o mesmo que um segredo compartilhado com todo mundo.

Troque o código imediatamente. Ele vive 60 segundos e é de uso único — passa pela URL do navegador, e URLs ficam no histórico, nos logs do servidor e no cabeçalho Referer.

5 · Mapeando para uma conta do seu lado

user_id é único globalmente, tenant_id marca a empresa. Ambos são estáveis — nunca mudam mesmo que os nomes mudem.

Não faça a correspondência pelo e-mail. O e-mail pode ser alterado pelo proprietário, e associar pelo e-mail significa que uma conta pode trocar de dono silenciosamente assim que o e-mail é alterado. Guarde o 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:

  1. Procure a conta com jss_user_id = :user_id → encontrou? faça login.
  2. Não encontrou? Associe o tenant_slug a uma loja do seu lado, crie a conta do funcionário, guarde o jss_user_id dele.
  3. 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.

O que é obrigatório e o que não é necessário

Obrigatório

Não necessário

Se falhar

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

Testando

  1. Peça ao admin da plataforma para cadastrar um segundo app só para testes.
  2. Faça login com uma conta de funcionário real.
  3. A tela de consentimento deve aparecer uma vez. Na segunda vez, deve passar direto.
  4. Revogue o acesso em Meu Perfil → Apps autorizados a usar sua conta → a tela de consentimento deve aparecer de novo.