クライアント連携
公式 Web コンポーネントで登録と確認を組み込みます。設定項目、イベント、サイト側バックエンドとの連携例を紹介します。
AuthLink の本番 API は https://api.getauthlink.com です。業務バックエンドには自サイトの API を指定し、AuthLink 公式サイトのドメインは API 呼び出しに使いません。
JavaScript クイックスタート
@auth-link/web からサイトの業務バックエンドに接続します。既定は同一オリジンで、明示的に許可したクロスオリジン接続にも対応します。テナント API キーはバックエンドでのみ保管してください。
npm install @auth-link/web
インポート先は @auth-link/web、@auth-link/web/react、@auth-link/web/vue、@auth-link/web/styles.css です。CSS は明示的に読み込んでください。React 18.2+ / 19、Vue 3.3+、ESM、TypeScript に対応し、ツールチェーンには Node 22.13+ が必要です。
同一オリジンの Cookie / session 設定
CSRF トークンはサイトのバックエンドが現在のログインセッション用に生成・保存し、フレームワークやセッション API から提供します。下の csrfToken はサーバーテンプレート変数で、出力時にエスケープが必要です。ブラウザーで生成した値やテナント API キーに置き換えないでください。
<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 のエクスポートではありません。作成と結果取得の両 API で CSRF ヘッダーをログインセッションと照合してください。送信だけでは検証になりません。セッション変更後はトークンを更新し、トークンを含むページを公開キャッシュしないでください。
クリック操作から登録・確認する
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 は連携サイト側で実装するサンプル API です。ログインセッションの csrfToken を Cache-Control: no-store と適切な CORS 設定で返します。実際のセッション API に置き換えてください。
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 が必要で、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 を更新し、コールバックは作成・ポーリングのたびに最新値を読みます。サイトのユーザーログイントークンを使い、AuthLink テナント API キーは絶対に使わないでください。CSRF を省略できるのは credentials が omit で、バックエンドが Cookie へのフォールバックなしに Bearer のみで認証する場合です。
業務バックエンドの 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 プリフライトを処理し、セッション API、実際の応答、エラー応答に CORS ヘッダーを設定してください。Cookie 使用時にワイルドカードは使えません。バックエンドの許可リストと照合してヘッダーを返し、独自ヘッダーも許可リストに追加します。実際の要求ではログイン、CSRF、所有権を検証してください。SDK は CORS 設定を代行せず、no-cors で結果を読むこともできません。
エラーコード
| エラーコード | 対処 |
|---|---|
BACKEND_ORIGIN_NOT_ALLOWED | 業務バックエンドが許可されていません。allowedBackendOrigins を確認してください。 |
INVALID_BACKEND_ORIGIN / INVALID_BASE_URL | baseUrl が有効な業務 API の接頭辞か確認してください。allowedBackendOrigins にはパスやワイルドカードを含めません。 |
CSRF_REQUIRED / ACCESS_TOKEN_REQUIRED | ログインまたは CSRF トークンがありません。有効なセッションを取得してください。 |
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> | 要求ごとに現在のセッショントークンを読みます。Cookie または混合認証では必須です。 |
csrfHeaderName | string / X-CSRF-Token | バックエンドのフレームワークが検証する CSRF ヘッダー名に合わせます。 |
accessToken | () => string | Promise<string> | 要求ごとにサイトのユーザーログイントークンを読みます。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 | UI は 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 は安定した参照を使い、アンマウント時にポーリングを停止します。独自 UI には useAuthLink を使え、Vue の snapshot は shallow ref です。
業務バックエンドの API とデータ型
以下の API は連携サイトで実装します。セッションまたはサイトの Bearer トークンを認証し、Cookie モードでは CSRF も検証します。要求の所有権と期限も確認し、匿名の中継 API にしないでください。
| サイトのバックエンド API | 入力 | 役割 |
|---|---|---|
POST /api/authlink/requests
|
body {"purpose":"bind"} または confirm、Idempotency-Key;session + CSRF またはサイトの Bearer トークン |
セッションからユーザーを取得し、リクエストの情報を保存してプラットフォームの作成 API を呼び出し、公開 ticket を返します。 |
POST /api/authlink/requests/{id}/result
|
body {};session + CSRF またはサイトの Bearer トークン |
所有者を確認して 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 で結果を検証し、現在のリクエストとサイトのセッションに対応させてください。コンポーネントの再表示はバックエンドの確認記録の代わりにはなりません。
ライフサイクルと UI の規則
- コードは 30 秒ごとに更新され、リクエストは最大 120 秒です。この数字は単独で使える TOTP ログインパスワードではありません。
- キャンセル・アンマウント時に監視を解除します。確認・拒否後は自動で閉じるため、サイト側で ticket をクリアしてください。
- 画面を閉じてもリクエストは失効せず、元の期限が適用されます。結果が不明な状態を復旧するためにリクエストを繰り返し作成しないでください。
- サイトの CSP で次を許可してください:
img-src 'self' data:。QR はブラウザー内で生成し、外部の画像サービスには送信しません。 - アプリでの削除には永久失効の明示的な確認が必要です。同じサイトのアカウントは再登録できません。更新時はアンインストールやデータ削除をせず、上書きしてください。