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 单独用于核对二维码中的 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 删除绑定需显式永久停用确认,删除后网站账号永久不能重绑;覆盖更新不要卸载或清除数据。