Client integration / Web SDK

用戶端接入

使用官方網頁元件整合綁定與確認流程,查看元件參數、事件和網站後端串接範例。

AuthLink 正式 API 為 https://api.getauthlink.com;網站業務後端地址仍填寫你自己的 API,官網網域不用於介面呼叫。

JavaScript 快速整合

使用 @auth-link/web,瀏覽器存取你的網站業務後端;支援預設同源模式及明確設定的跨來源模式。租戶 API key 僅儲存在後端。

查看 npm 發布頁 ↗

npm install @auth-link/web

匯入入口為 @auth-link/web、@auth-link/web/react、@auth-link/web/vue 和 @auth-link/web/styles.css。需明確匯入樣式;支援 React 18.2+ / 19、Vue 3.3+、ESM 和 TypeScript,工具鏈要求 Node 22.13+。

同源 Cookie / session 初始化

CSRF token 由網站後端為目前登入的 session 產生並儲存,使用現有框架機制或工作階段介面提供。下方 csrfToken 是伺服器模板變數,必須正確跳脫後呈現;不要在瀏覽器產生隨機值取代,也不要放入租戶金鑰。

<meta name="csrf-token" content="{{ csrfToken }}">
import { createAuthLinkClient } from '@auth-link/web';
import '@auth-link/web/styles.css';

// Read the token rendered by YOUR backend for the current login session.
// This helper is application code, not an SDK export.
export function readCsrfToken(): string {
  const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
  if (!token) throw new Error('Missing session CSRF token');
  return token;
}

export const client = createAuthLinkClient({
  baseUrl: '/api/authlink',
  csrfToken: readCsrfToken,
  authLinkOrigin: 'https://api.getauthlink.com',
});

readCsrfToken 是範例中的網站自訂函式,不是 SDK 匯出。建立和結果介面都必須驗證 CSRF 請求標頭與登入 session;只傳送標頭不等於完成驗證。登入或工作階段更新後重新取得 token,含工作階段 token 的頁面不得公開快取。

在點擊事件中綁定或確認

import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
import type { AuthLinkModal, Mode } from '@auth-link/web';
import { client } from './authlink-client';

let modal: AuthLinkModal | undefined;
let creating = false;
let createKey: string | undefined;
let creation: AbortController | undefined;

// Call startAuthLink('bind') or startAuthLink('confirm') from a button handler.
export async function startAuthLink(mode: Mode) {
  if (creating || modal) return;
  creating = true;
  creation = new AbortController();
  try {
    createKey ??= requestKey(); // Retain this key if creation fails.
    const ticket = await client.create(mode, {
      idempotencyKey: createKey, signal: creation.signal,
    });
    createKey = undefined;
    const open = mode === 'bind' ? openAuthLinkBind : openAuthLinkConfirm;
    modal = open({
      ticket, client, locale: 'zh-CN', variant: 'A',
      onConfirmed: result => {
        modal = undefined;
        console.log('Confirmed in UI:', result.requestId);
      },
      onRejected: result => { modal = undefined; console.log(result.code); },
      onExpired: () => { modal?.destroy(); modal = undefined; },
      onClose: () => { modal = undefined; },
      onError: error => console.error(error.message),
    });
  } finally { creating = false; }
}

// Call on page/component teardown. Business authorization stays on the backend.
export function destroyAuthLink() { creation?.abort(); modal?.destroy(); modal = undefined; }
建立請求放在使用者點擊事件中,不在模組載入、React effect、Vue watch 或 SSR 中自動建立。建立失敗時重用原冪等鍵;頁面卸載時取消建立並銷毀彈窗。onConfirmed 只更新介面,後端仍負責業務許可。

前後端分離與跨來源整合

baseUrl 指向網站業務後端,不是 AuthLink 租戶 API。跨來源時提供絕對 baseUrl 和精確的 allowedBackendOrigins;來源只含協定、主機及連接埠,不含路徑或萬用字元,也不能從使用者輸入取得。正式環境使用 HTTPS,本機開發可使用 HTTP 回環位址。authLinkOrigin 單獨用於核對 QR 碼中的 AuthLink 服務來源。

跨來源 Cookie / session

GET /api/session 是接入網站自行實作的範例介面,需依目前登入 session 回傳 csrfToken,設定 Cache-Control: no-store 並啟用正確 CORS。請替換為網站實際工作階段介面。

import { createAuthLinkClient } from '@auth-link/web';

/** /api/session is an example endpoint implemented by your application backend. */
export async function createSessionBackendClient(backendOrigin: string) {
  const response = await fetch(`${backendOrigin}/api/session`, {
    credentials: 'include', cache: 'no-store', redirect: 'error',
  });
  if (!response.ok) throw new Error('Session initialization failed');
  const session: unknown = await response.json();
  if (!session || typeof session !== 'object' || !('csrfToken' in session) ||
      typeof session.csrfToken !== 'string' || !session.csrfToken) {
    throw new Error('The session endpoint must return { csrfToken: string }');
  }
  let currentCsrfToken = session.csrfToken;
  return {
    client: createAuthLinkClient({
      baseUrl: `${backendOrigin}/api/authlink`,
      allowedBackendOrigins: [backendOrigin],
      credentials: 'include',
      csrfToken: () => currentCsrfToken,
    }),
    // Call after your application's session/token refresh; recreate on account changes.
    updateCsrfToken(value: string) { currentCsrfToken = value; },
  };
}

登入後呼叫 createSessionBackendClient,向元件傳入回傳的 client;工作階段更新時呼叫 updateCsrfToken。跨來源 Cookie 需要 credentials: include;Cookie 的 SameSite、Secure、domain/path 及瀏覽器限制仍然生效。登出或切換帳號時關閉舊彈窗並重新初始化。

跨來源 Bearer / JWT

import { createAuthLinkClient } from '@auth-link/web';

/** auth is your website's existing login state, updated by its login/refresh flow. */
export function createBearerBackendClient(backendOrigin: string, auth: { accessToken: string }) {
  return createAuthLinkClient({
    baseUrl: `${backendOrigin}/api/authlink`,
    allowedBackendOrigins: [backendOrigin],
    credentials: 'omit',
    accessToken: () => auth.accessToken,
  });
}

auth 來自網站既有登入狀態;登入或更新流程修改 auth.accessToken,回呼在每次建立和輪詢時讀取最新值。此值是網站使用者登入 token,絕不能使用 AuthLink 租戶 API key。僅在後端只接受 Bearer、沒有 Cookie 登入回退且 credentials 為 omit 時才可省略 CSRF;其他模式仍需 CSRF。

業務後端 CORS

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, Idempotency-Key, Authorization
Vary: Origin

網站業務後端需處理 OPTIONS 預檢,並為工作階段介面、實際回應及錯誤回應設定 CORS。攜帶 Cookie 時不能使用萬用字元來源,先驗證後端允許的來源再回傳對應標頭;新增自訂標頭時同步更新允許清單。實際請求仍須驗證登入身分、CSRF 和請求歸屬。SDK 不能取代後端開啟 CORS,也不能透過 no-cors 讀取結果。

錯誤碼

錯誤碼處理方式
BACKEND_ORIGIN_NOT_ALLOWED目標業務後端不在允許來源中,請檢查 allowedBackendOrigins。
INVALID_BACKEND_ORIGIN / INVALID_BASE_URL檢查 baseUrl 是否為有效業務介面前綴;allowedBackendOrigins 中的來源不能含路徑或萬用字元。
CSRF_REQUIRED / ACCESS_TOKEN_REQUIRED登入憑證或 CSRF token 缺失;重新取得有效工作階段。
RESERVED_HEADER不要透過 headers 覆寫 SDK 管理的驗證及請求標頭。

用戶端與彈窗參數

createAuthLinkClient 參數型別 / 預設說明
baseUrlstring / /api/authlink網站業務後端前綴;跨來源時提供完整 URL,不是 AuthLink API 網域。
allowedBackendOriginsreadonly string[] / []額外允許的精確業務後端來源,同源仍預設允許。
credentialsRequestCredentials預設 same-origin;設定 accessToken 時預設 omit;跨來源 Cookie 使用 include。
csrfToken() => string | Promise<string>每次請求讀取目前 session 的 token;Cookie 或混合驗證模式必填。
csrfHeaderNamestring / X-CSRF-Token與網站框架實際驗證的 CSRF 請求標頭名稱一致。
accessToken() => string | Promise<string>每次讀取網站使用者登入 token,不含 Bearer 前綴,不能傳租戶金鑰。
headersHeadersInit | (() => HeadersInit | Promise<HeadersInit>)額外業務請求標頭或回呼,不能覆寫 Accept、Content-Type、Authorization、Idempotency-Key 或 CSRF 標頭。
authLinkOriginstring / optional用於核對 QR 中的 AuthLink 服務來源,與業務後端允許來源分開設定。
fetchtypeof fetch / globalThis.fetch可選自訂傳輸;仍執行用戶端的目標來源驗證。
彈窗 / 元件參數 型別 / 預設 說明
ticket / client Ticket / AuthLinkClient,必填 後端建立的請求與網站查詢用戶端
locale Locale / zh-CN 支持 13 種語言;不支持的值回退簡中
variant A / B / C,預設 A A 極簡卡片;B 左右分欄;C 緊湊卡片 / 手機底部彈層
showQr boolean / false 確認彈窗是否額外顯示 QR;綁定彈窗始終顯示
intervalMs number / 1500 查詢間隔(毫秒),範圍 250–10000;不延長請求期限
onRefresh () => void / 可選 過期後由網站重新創建請求、更新 ticket
onConfirmed / onRejected (result) => void 或 Promise<void> 確認 / 拒絕後自動關閉;不觸發使用者關閉事件
onExpired / onError () => void;(Error) => void 過期或查詢 / 校驗錯誤
onClose () => void 使用者關閉時清理 ticket;destroy() 不觸發此回呼

支持語言:zh-CN, zh-TW, en, ja, bn, es, fil, hi, id, ms, pt, th, vi。取消 / 關閉只停止前端觀察,不會替使用者拒絕伺服器端請求。切換語言和佈局不會重建請求。

React 與 Vue

重用上方 authlink-client.ts。兩個範例均包含按鈕、請求建立、結果提示和彈窗清理;mode 為 bind 時綁定,為 confirm 時請求手機確認。

React 完整範例

"use client";
import { useRef, useState } from 'react';
import { requestKey } from '@auth-link/web';
import type { Mode, Ticket } from '@auth-link/web';
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
import { client } from './authlink-client';

export function AuthLinkPanel({ mode }: { mode: Mode }) {
  const [ticket, setTicket] = useState<Ticket | null>(null);
  const [busy, setBusy] = useState(false);
  const [message, setMessage] = useState('');
  const key = useRef<string | null>(null);
  const creating = useRef(false);
  function finish(message: string) {
    setTicket(null);
    setMessage(message);
  }
  function showBound(requestId: string) {
    // UI feedback only. The backend has already verified and saved the binding.
    finish(`Phone bound (request ${requestId})`);
  }
  function showConfirmed(requestId: string) {
    finish(`Confirmed (request ${requestId})`);
  }
  const start = async () => {
    if (creating.current || ticket) return;
    creating.current = true;
    setBusy(true);
    setMessage('');
    try {
      key.current ??= requestKey();
      setTicket(await client.create(mode, { idempotencyKey: key.current }));
      key.current = null;
    } catch { setMessage('Request failed; retry the same creation.'); }
    finally { creating.current = false; setBusy(false); }
  };
  const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
  return <>
    {!ticket && <button type="button" disabled={busy} onClick={() => void start()}>Start verification</button>}
    {ticket && <Component ticket={ticket} client={client}
      onConfirmed={result => result.purpose === 'bind' ? showBound(result.requestId) : showConfirmed(result.requestId)}
      onClose={() => finish('Closed')}
      onRejected={() => finish('Request rejected on the phone')}
      onExpired={() => finish('Request expired; start again')}
      onError={error => setMessage(error.message)} />}
    <p role="status">{message}</p>
  </>;
}

Vue 完整範例

<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import { requestKey } from '@auth-link/web';
import type { Confirmation, Mode, Ticket } from '@auth-link/web';
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
import { client } from './authlink-client';
const props = defineProps<{ mode: Mode }>();
const ticket = shallowRef<Ticket | null>(null);
const busy = ref(false);
const message = ref('');
let key: string | null = null;
function finish(text: string) { ticket.value = null; message.value = text; }
function confirmed(result: Confirmation) {
  finish(`${result.purpose === 'bind' ? 'Bound' : 'Confirmed'} (request ${result.requestId})`);
}
async function start() {
  if (busy.value || ticket.value) return;
  busy.value = true;
  key ??= requestKey();
  try {
    ticket.value = await client.create(props.mode, { idempotencyKey: key });
    key = null;
  } catch { message.value = 'Request failed; retry the same creation.'; }
  finally { busy.value = false; }
}
</script>

<template>
  <button v-if="!ticket" type="button" :disabled="busy" @click="start">Start verification</button>
  <AuthLinkBind v-else-if="mode === 'bind'" :ticket="ticket" :client="client"
    @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected on the phone')"
    @expired="finish('Request expired; start again')" @error="message = 'Network error; retry this request.'" />
  <AuthLinkConfirm v-else :ticket="ticket" :client="client"
    @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected on the phone')"
    @expired="finish('Request expired; start again')" @error="message = 'Network error; retry this request.'" />
  <p role="status">{{ message }}</p>
</template>

綁定和確認紀錄由後端驗證、消費並儲存後,才回傳 confirmed。範例中的 showBound 和 finish 僅更新頁面並清空 ticket;重新整理頁面後從後端讀取真實綁定狀態,不以元件 state 或 localStorage 放行業務。

建立失敗保留同一冪等鍵,重試同一次建立;成功、拒絕、過期或關閉後清空 ticket。網路錯誤保留目前彈窗供重試。保持 client 和 ticket 穩定;元件卸載會清理輪詢。useAuthLink 可用於自訂介面,Vue 的 snapshot 為 shallow ref。

網站後端介面與資料結構

下列介面由接入網站實作。驗證 session 或網站 Bearer token,Cookie 模式還需 CSRF;同時驗證請求歸屬與期限,不能作為匿名透傳代理。

網站後端接口 輸入 職責
POST /api/authlink/requests body {"purpose":"bind"} 或 confirm;Idempotency-Key;session + CSRF 或網站 Bearer token 後端從會話取得使用者,保存請求上下文並調用平台創建接口,返回公開 ticket
POST /api/authlink/requests/{id}/result body {};session + CSRF 或網站 Bearer token 檢查所有權,查詢 AuthLink;approved / consumed 後冪等消費、校驗並持久保存,再返回 confirmed

type Ticket = {
  requestId: string; purpose: 'bind' | 'confirm';
  qr: string; pairingCode: string; expiresAt: string;
  displayCode?: {
    value: string; serverTime: string; validUntil: string;
    periodSeconds: 30;
  };
};
// Pending: { requestId, status: 'new' | 'pending' | 'approved' |
//   'consumed' | 'rejected' | 'expired', displayCode? }
// Successful result after backend validation and persistence (minimum fields):
type Confirmation = {
  status: 'confirmed'; requestId: string; verificationId: string;
  purpose: 'bind' | 'confirm';
  authenticationPolicy: string; expiresAt: string;
};
// Rejection event: { status:'rejected', code:'REJECTED', requestId, purpose }

原樣回傳 displayCode,過期數字應隱藏。確認結果由官方 SDK 驗證,並與目前請求及網站工作階段對應。重新掛載元件不能取代後端儲存的確認紀錄。

生命週期與界面規則

  • 核對碼每 30 秒變化,請求最長 120 秒;數字不是獨立 TOTP 登錄密碼。
  • 請求取消 / 元件卸載後清理觀察器;成功和拒絕自動關閉,網站需要清空 ticket。
  • 前端關閉不撤銷請求,後端仍按原期限處理。不要為恢復未知結果不斷創建新請求。
  • 網頁 CSP 需允許 img-src 'self' data:。QR 在瀏覽器本地生成,不發送第三方圖片服務。
  • App 刪除綁定需顯式永久停用確認,刪除後網站帳號永久不能重綁;覆蓋更新不要卸載或清除資料。