用戶端接入
使用官方網頁元件整合綁定與確認流程,查看元件參數、事件和網站後端串接範例。
AuthLink 正式 API 為 https://api.getauthlink.com;網站業務後端地址仍填寫你自己的 API,官網網域不用於介面呼叫。
JavaScript 快速整合
使用 @auth-link/web,瀏覽器存取你的網站業務後端;支援預設同源模式及明確設定的跨來源模式。租戶 API key 僅儲存在後端。
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; }
前後端分離與跨來源整合
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 參數 | 型別 / 預設 | 說明 |
|---|---|---|
baseUrl | string / /api/authlink | 網站業務後端前綴;跨來源時提供完整 URL,不是 AuthLink API 網域。 |
allowedBackendOrigins | readonly string[] / [] | 額外允許的精確業務後端來源,同源仍預設允許。 |
credentials | RequestCredentials | 預設 same-origin;設定 accessToken 時預設 omit;跨來源 Cookie 使用 include。 |
csrfToken | () => string | Promise<string> | 每次請求讀取目前 session 的 token;Cookie 或混合驗證模式必填。 |
csrfHeaderName | string / X-CSRF-Token | 與網站框架實際驗證的 CSRF 請求標頭名稱一致。 |
accessToken | () => string | Promise<string> | 每次讀取網站使用者登入 token,不含 Bearer 前綴,不能傳租戶金鑰。 |
headers | HeadersInit | (() => HeadersInit | Promise<HeadersInit>) | 額外業務請求標頭或回呼,不能覆寫 Accept、Content-Type、Authorization、Idempotency-Key 或 CSRF 標頭。 |
authLinkOrigin | string / optional | 用於核對 QR 中的 AuthLink 服務來源,與業務後端允許來源分開設定。 |
fetch | typeof 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 刪除綁定需顯式永久停用確認,刪除後網站帳號永久不能重綁;覆蓋更新不要卸載或清除資料。