指南

Callback 片段

Callback 片段(callback-snippet)

放在回調頁(例如 callback.html)。讀取 URL 中的 code,搭配登入片段存入的 code_verifier 換取 access token。

要點

  • redirect_uri 必須與授權請求相同
  • 僅使用 client_idclient_secret 僅後端使用
  • 成功後可將 token 存 sessionStorage 供 Profile 等章節使用
  • 若使用 embed=1(iframe/popup),則不會跳轉到 redirect_uri,改由 postMessage 取得 code

callback-snippet.html

<div id="hs-callback-status">處理登入回調…</div>
<script>
(async 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 out = document.getElementById("hs-callback-status");

  const params = new URLSearchParams(location.search);
  const code = params.get("code");
  const error = params.get("error");
  if (error) {
    out.textContent = "授權失敗: " + error;
    return;
  }
  if (!code) {
    out.textContent = "缺少授權碼";
    return;
  }

  const verifier = sessionStorage.getItem(VERIFIER_KEY);
  if (!verifier) {
    out.textContent = "找不到 PKCE verifier,請重新登入";
    return;
  }

  const res = await fetch(API_BASE + "/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: REDIRECT_URI,
      code_verifier: verifier,
      client_id: CLIENT_ID,
    }),
  });
  const data = await res.json();
  if (!res.ok) {
    out.textContent = "換 token 失敗: " + (data.error || res.status);
    return;
  }

  sessionStorage.setItem("hs_access_token", data.access_token);
  out.textContent = "登入成功,token 已儲存";
  history.replaceState({}, "", location.pathname);
})();
</script>

embed=1:從 postMessage 取得 code(iframe / popup)

當授權 URL 加上 embed=1 時,授權完成會由託管頁(API 網域)向 parent/opener 發送:

  • { type: "hsystem.oauth", code, state }
  • { type: "hsystem.oauth.error", error, state }

你應在 parent(或 opener)端監聽 message,並檢查 event.origin === new URL(API_BASE).origin 後再使用 code 換 token:

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

  window.addEventListener("message", async function (e) {
    if (e.origin !== apiOrigin) return;
    const data = e.data || {};
    if (data.type === "hsystem.oauth.error") {
      console.error("授權失敗:", data.error, "state:", data.state);
      return;
    }
    if (data.type !== "hsystem.oauth" || !data.code) return;

    const verifier = sessionStorage.getItem(VERIFIER_KEY);
    if (!verifier) {
      console.error("找不到 PKCE verifier,請重新登入");
      return;
    }

    const res = await fetch(API_BASE + "/oauth/token", {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code: data.code,
        redirect_uri: REDIRECT_URI,
        code_verifier: verifier,
        client_id: CLIENT_ID,
      }),
    });
    const token = await res.json();
    if (!res.ok) {
      console.error("換 token 失敗:", token.error || res.status);
      return;
    }

    sessionStorage.setItem("hs_access_token", token.access_token);
    console.log("登入成功,token 已儲存");
  });
})();
</script>