指南

登入片段

登入片段(login-snippet)

將以下片段嵌入您的頁面,在使用者點擊「登入」時導向 HSystem 託管授權頁。需搭配 Callback 片段處理回調。

要點

  • 使用 OAuth 授權碼 + PKCE(S256)
  • redirect_uri 必須與 Auth 白名單完全一致
  • code_verifier 存於 sessionStorage,回調頁讀取後換 token
  • iframe 嵌入可加 embed=1:成功不跳轉 redirect_uri,改以 postMessage 回傳 code
  • Google 登入為 HSystem 平臺統一 Google(開發者不需建立 App Google Client);且屬於 bind-first:終端用戶需先在 /account 綁定 Google,否則會得到 google_oauth_not_bound

login-snippet.html

<!-- 登入按鈕 -->
<button type="button" id="hs-login">以 HSystem 登入</button>
<script>
(function () {
  const API_BASE = "{{api_base}}";
  const CLIENT_ID = "{{client_id}}";
  const REDIRECT_URI = "{{web_origin}}/callback.html";
  const VERIFIER_KEY = "hs_pkce_verifier";

  function b64url(bytes) {
    let s = "";
    for (const b of bytes) s += String.fromCharCode(b);
    return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  }

  async function startOAuthLogin() {
    const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
    sessionStorage.setItem(VERIFIER_KEY, verifier);
    const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
    const challenge = b64url(new Uint8Array(hash));
    const url = new URL(API_BASE + "/oauth/authorize");
    url.searchParams.set("client_id", CLIENT_ID);
    url.searchParams.set("redirect_uri", REDIRECT_URI);
    url.searchParams.set("response_type", "code");
    url.searchParams.set("code_challenge", challenge);
    url.searchParams.set("code_challenge_method", "S256");
    url.searchParams.set("state", crypto.randomUUID());
    location.href = url.toString();
  }

  document.getElementById("hs-login").addEventListener("click", startOAuthLogin);
})();
</script>

iframe 模式(embed=1)

若你希望把託管登入頁嵌入 iframe(例如彈窗內),請在授權 URL 加上 embed=1

  • 成功:iframe 會對 parent 發送 postMessage({ type: "hsystem.oauth", code, state }, targetOrigin)
  • 失敗postMessage({ type: "hsystem.oauth.error", error, state }, targetOrigin)
  • 安全targetOrigin 只會是 App 已登記的 redirect_uri/allowed_origins 白名單;不會使用 *

另外,若在 iframe 內點擊 Google 登入,託管頁會優先用 popup/新分頁開啟 Google(保留 window.opener)。授權完成後,API 端回傳的 embed HTML 會先對 window.opener、再對 window.parent 發送相同的 postMessage,且在存在 opener 時會嘗試自動關閉視窗。

iframe 風格 query(URL 參數)

嵌入託管頁時可在 authorize → hosted 鏈路上的 query 保留下列參數(跨 /hosted/loginregisterconsent 會自動帶上):

| 參數 | 說明 | 範例 | |------|------|------| | theme | lightdark(預設跟系統;iframe 內可手動切換) | theme=dark | | primary | 主色 hex | primary=%2314b8a6 | | bg / surface / text / muted | 背景/卡片/文字 hex | surface=%23ffffff | | radius | sm / md / lg | radius=lg | | logo | 公司 logo(僅 https://) | logo=https%3A%2F%2Fcdn.example.com%2Flogo.png | | logo_alt | logo 替代文字 | logo_alt=MyApp |

未帶 logo 時顯示 HSystem 預設標誌。Google 登入按鈕為官方樣式,不受 primary 影響。

開發者試用頁(Docs Lab)可即時調整上述 query 並預覽 iframe 外觀。

示例(authorize URL 片段):

&embed=1&theme=dark&primary=%232dd4bf&logo=https%3A%2F%2Fcdn.example.com%2Flogo.png

iframe-login-snippet.html

<button type="button" id="hs-login">以 HSystem 登入(iframe)</button>

<div id="hs-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5);">
  <div style="width:min(520px, 92vw); height:min(720px, 92vh); margin:4vh auto; background:#0b1220; border-radius:16px; overflow:hidden;">
    <iframe
      id="hs-frame"
      title="HSystem Hosted Login"
      style="width:100%; height:100%; border:0;"
      referrerpolicy="no-referrer"
    ></iframe>
  </div>
</div>

<script>
(function () {
  const API_BASE = "{{api_base}}";
  const CLIENT_ID = "{{client_id}}";
  const REDIRECT_URI = "{{web_origin}}/callback.html";
  const VERIFIER_KEY = "hs_pkce_verifier";

  const apiOrigin = new URL(API_BASE).origin;
  const modal = document.getElementById("hs-modal");
  const frame = document.getElementById("hs-frame");

  function b64url(bytes) {
    let s = "";
    for (const b of bytes) s += String.fromCharCode(b);
    return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  }

  async function startIframeOAuth() {
    const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
    sessionStorage.setItem(VERIFIER_KEY, verifier);
    const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
    const challenge = b64url(new Uint8Array(hash));

    const url = new URL(API_BASE + "/oauth/authorize");
    url.searchParams.set("client_id", CLIENT_ID);
    url.searchParams.set("redirect_uri", REDIRECT_URI);
    url.searchParams.set("response_type", "code");
    url.searchParams.set("code_challenge", challenge);
    url.searchParams.set("code_challenge_method", "S256");
    url.searchParams.set("state", crypto.randomUUID());
    url.searchParams.set("embed", "1");

    modal.style.display = "block";
    frame.src = url.toString();
  }

  window.addEventListener("message", function (e) {
    if (e.origin !== apiOrigin) return; // 只信任 API 來源
    const data = e.data || {};
    if (data.type === "hsystem.oauth") {
      // 取得 code 後,請看 Callback 章節以 code+verifier 換 token
      modal.style.display = "none";
      console.log("OAuth code:", data.code, "state:", data.state);
    }
    if (data.type === "hsystem.oauth.error") {
      modal.style.display = "none";
      console.error("OAuth error:", data.error, "state:", data.state);
    }
  });

  document.getElementById("hs-login").addEventListener("click", startIframeOAuth);
})();
</script>

目前為模擬模式,回應為範例資料

登入 Dev 以真實測試

體驗託管登入頁

模擬預覽:下方為終端用戶在 OAuth 流程中看到的 HSystem 託管登入畫面。

https://auth.hsystem.halphastech.com/hosted/login
無法載入登入預覽,請確認 App 已啟用 Auth。
模擬預覽

登入 Dev 並從 App 頁面帶入 ?app= 即可切換為可互動的真實 OAuth 流程。