← API notifikasi JS Staff

「JS Staffでログイン」を設定

従業員は自分のJS Staffアカウントであなたのアプリにログインします。あなたのアプリがパスワードを見ることは一切ありません。

得られるもの

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

それだけです。peran(役割)には staffsupervisorowner のいずれかが入ります。あなたのアプリは誰のパスワード・給与・勤怠・前借り・口座も読み取れません

なぜリダイレクトであり「メール+パスワード送信」APIではないのか。そのようなAPIは、このポータルが依存する新規デバイスOTPを無効化してしまいます。OTPの存在意義はまさにパスワードだけではログインに不十分という点にあります——パスワードだけを受け入れる扉が一つでもあれば、パスワードを盗んだ者はその扉を通るだけです。

リダイレクトを使えば、そのような扉は存在しません。ログインは常に私たちのページで行われ、OTPと信頼済みデバイスの仕組みも伴います。

フロー

[1] 従業員があなたのアプリで「JS Staffでログイン」をクリック ↓ [2] あなたが https://staff.jidanshoppu.com/sso/authorize.php?... へリダイレクト ↓ (そこでログイン+新デバイスならOTP+同意画面) [3] 本人が sso-callback.php?code=...&state=... に戻る ↓ [4] あなたの「サーバー」が /sso/token.php でcode+secretを交換 ←サーバー間通信 ↓ [5] 本人の情報を取得 → あなた自身のセッションを作成

1 · この2つの値をリクエスト

JS Staffのプラットフォーム管理者から取得します(/super/apps.phpで登録されます):

client_idapp_a1b2c3d4e5f6a7b8
client_secretsec_1a2b3c… ——一度だけ表示されます

登録時にredirect URIを申告する必要があります。ルールは厳格です:

なぜこれほど厳格なのか。これはSSOフロー全体で最も重要な防壁です。プレフィックス・サブドメイン・ワイルドカードなど少しでも緩めると、攻撃者は自分のコールバックアドレスを埋め込んだ認可リンクをあなたの従業員に送るだけで済んでしまいます。被害者はいつも通り私たちの本物のページでログインし、何も怪しく見えないまま、コードは攻撃者のサーバーに届いてしまいます。

2 · 設定

<?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 · ログインボタン

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

ボタンはこれだけで十分です: <a href="/sso-mulai.php">Masuk pakai JS Staff</a>

4 · コールバック

<?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 がブラウザを経由するのは、その秘密を全員と共有するのと同じことです。

コードはすぐに交換してください。有効期限は60秒で、一度限り使用可能です——ブラウザのURLを経由するため、URLは履歴・サーバーログ・Refererヘッダーに残ります。

5 · あなた側のアカウントへのマッピング

user_id はグローバルに一意で、tenant_id は会社を示します。どちらも安定しており、名前が変わっても決して変わりません。

メールアドレスで照合しないでください。メールアドレスはオーナーが変更できるため、メールで照合するとメールが変更された瞬間にアカウントが静かに乗っ取られる可能性があります。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);

初回のフロー:

  1. jss_user_id = :user_id でアカウントを検索 → 見つかった?ログイン。
  2. 見つからなかった?tenant_slug をあなた側の店舗と照合し、スタッフアカウントを作成して jss_user_id を保存してください。
  3. tenant_slug が未知の場合?拒否してください。SSOのデータから新しい店舗を黙って作成しないでください——JS Staffのアカウントを持つ誰でもそれを引き起こせてしまいます。

必須事項と不要な事項

必須

不要

失敗した場合

症状よくある原因
「戻りアドレスが一致しません」ページ redirect_uri が登録内容と異なります。末尾のスラッシュ、http/https、クエリ文字列を確認してください。
「不明なアプリケーション」ページ client_id が誤っているか、プラットフォーム管理者によってアプリが取り消されています。
/sso/token.php401 コードの有効期限切れ(60秒超過)、すでに使用済み、client_secret が誤っている、またはステップ3と redirect_uri が異なる。
?error=access_denied 付きで戻る 従業員が拒否を押しました。バグではありません。

401 はあえてすべての原因に同じメッセージを使っています——「コードが間違っている」と「コードが使用済み」を区別すると、攻撃者にどのコードが存在したかを推測する手がかりを与えてしまい、正しく統合できている人の助けにもなりません。上記の4点を一つずつ確認してください。

テスト

  1. プラットフォーム管理者にテスト専用の2つ目のアプリを登録してもらいましょう。
  2. 実際のスタッフアカウントでログインします。
  3. 同意画面は一度だけ表示されるはずです。2回目はそのまま通過するはずです。
  4. プロフィール → アカウントの使用を許可したアプリでアクセスを取り消してください → 同意画面が再び表示されるはずです。