@auth-link/web 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -4
- package/LICENSE +21 -0
- package/README.md +611 -132
- package/dist/index.js +50 -11
- package/dist/index.js.map +2 -2
- package/dist/react.js.map +2 -2
- package/dist/types/core.d.ts +13 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/vue.js +12 -3
- package/dist/vue.js.map +2 -2
- package/docs/README.en.md +172 -25
- package/docs/README.ja.md +194 -47
- package/docs/README.zh-CN.md +246 -61
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -28,41 +28,146 @@ Compatibility: React 18.2+ / 19, Vue 3.3+, TypeScript, ESM. Install only the fra
|
|
|
28
28
|
### Browser integration
|
|
29
29
|
|
|
30
30
|
1. Configure your backend's AuthLink tenant credentials.
|
|
31
|
-
2. Implement the two same-origin
|
|
31
|
+
2. Implement the two application backend endpoints below (same-origin or explicitly allowed cross-origin), authenticated to the current website user.
|
|
32
32
|
3. Create a request on a user action and pass its ticket to a component.
|
|
33
33
|
4. Your backend verifies and consumes the result before returning a confirmed response.
|
|
34
34
|
|
|
35
|
-
The browser can call this library directly. Tenant credentials and result consumption belong on your backend; never embed a tenant API key in frontend code. The supplied client
|
|
35
|
+
The browser can call this library directly. Tenant credentials and result consumption belong on your backend; never embed a tenant API key in frontend code. The supplied client defaults to same-origin and also supports explicitly allowed cross-origin backends. You can supply a custom `AuthLinkClient` to integrate another transport while preserving server-side credential handling and verification.
|
|
36
36
|
|
|
37
|
-
### Create a client
|
|
37
|
+
### Create a client (default same-origin session mode)
|
|
38
|
+
|
|
39
|
+
`readCsrfTokenFromYourCms` is application code, not an SDK export. It reads a CSRF token issued by your own backend for the current logged-in session; it must not generate one in the browser or return an AuthLink API key. The name does not require a CMS.
|
|
40
|
+
|
|
41
|
+
Reuse your framework's existing CSRF mechanism. Otherwise, generate a cryptographically random token on the server (for example, Node's `randomBytes(32).toString('base64url')`), persist it in the session, and render it into the authenticated page with HTML attribute escaping. `{{ csrfToken }}` below is a server-template variable, not a literal value. Do not publicly cache pages containing session tokens.
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<meta name="csrf-token" content="{{ csrfToken }}">
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The SDK reads the token for every request, sends it as `X-CSRF-Token`, and includes same-origin session cookies. Your backend must validate that header against the token in the same session on both creation and result routes, reject missing/mismatched tokens, and check the expected request origin. Sending a header alone does not provide CSRF protection. Obtain a fresh token after login/session changes; a SPA may supply a token returned by its own same-origin session bootstrap endpoint. If your framework expects another header name, set `csrfHeaderName` or provide a custom `AuthLinkClient`.
|
|
48
|
+
|
|
49
|
+
Create `authlink-client.ts`:
|
|
38
50
|
|
|
39
51
|
```ts
|
|
40
52
|
// authlink-client.ts
|
|
41
|
-
import { createAuthLinkClient
|
|
53
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
42
54
|
import '@auth-link/web/styles.css';
|
|
43
55
|
|
|
56
|
+
// The application server renders this meta element using the current session's
|
|
57
|
+
// CSRF token. This function only reads that token; it does not generate one.
|
|
58
|
+
export function readCsrfTokenFromYourCms(): string {
|
|
59
|
+
const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
|
|
60
|
+
if (!token) throw new Error('Missing CSRF token; load the authenticated application page first.');
|
|
61
|
+
return token;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
export const client = createAuthLinkClient({
|
|
45
65
|
baseUrl: '/api/authlink',
|
|
46
|
-
csrfToken:
|
|
47
|
-
|
|
48
|
-
if (!token) throw new Error('Missing CSRF token');
|
|
49
|
-
return token;
|
|
50
|
-
},
|
|
66
|
+
csrfToken: readCsrfTokenFromYourCms,
|
|
67
|
+
// Replace this example with the configured AuthLink service origin.
|
|
51
68
|
authLinkOrigin: 'https://auth.example.com',
|
|
52
69
|
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Replace `https://auth.example.com` with the configured AuthLink origin. Create requests only in user event handlers, as in the React example below.
|
|
73
|
+
|
|
74
|
+
### Separate frontend and backend origins (since 0.0.2)
|
|
75
|
+
|
|
76
|
+
The default is still same-origin `/api/authlink`. For direct cross-origin requests, configure an absolute `baseUrl` and exact `allowedBackendOrigins`, for example frontend `https://www.example.com` and application API `https://api.example.com`. Subdomains and different ports are different origins. Entries must be origins only, without paths, wildcards or embedded credentials, and must come from trusted configuration. HTTPS is required except for explicitly allowed HTTP loopback development origins (localhost, 127.0.0.1, [::1]).
|
|
77
|
+
|
|
78
|
+
The SDK validates the destination before evaluating credentials and refuses redirects. `authLinkOrigin` separately validates the AuthLink service origin encoded in the QR; it is not the application API allowlist.
|
|
79
|
+
|
|
80
|
+
#### Cross-origin session cookies
|
|
81
|
+
|
|
82
|
+
This complete initialization function uses an example `GET /api/session` endpoint that **your application implements**, returning `{ "csrfToken": "current-session-token" }` for the authenticated session with `Cache-Control: no-store` and CORS enabled. Replace that path with your existing session endpoint.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
86
|
+
|
|
87
|
+
/** /api/session is an example endpoint implemented by your application backend. */
|
|
88
|
+
export async function createSessionBackendClient(backendOrigin: string) {
|
|
89
|
+
const response = await fetch(`${backendOrigin}/api/session`, {
|
|
90
|
+
credentials: 'include', cache: 'no-store', redirect: 'error',
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) throw new Error('Session initialization failed');
|
|
93
|
+
const session: unknown = await response.json();
|
|
94
|
+
if (!session || typeof session !== 'object' || !('csrfToken' in session) ||
|
|
95
|
+
typeof session.csrfToken !== 'string' || !session.csrfToken) {
|
|
96
|
+
throw new Error('The session endpoint must return { csrfToken: string }');
|
|
97
|
+
}
|
|
98
|
+
let currentCsrfToken = session.csrfToken;
|
|
99
|
+
return {
|
|
100
|
+
client: createAuthLinkClient({
|
|
101
|
+
baseUrl: `${backendOrigin}/api/authlink`,
|
|
102
|
+
allowedBackendOrigins: [backendOrigin],
|
|
103
|
+
credentials: 'include',
|
|
104
|
+
csrfToken: () => currentCsrfToken,
|
|
105
|
+
}),
|
|
106
|
+
// Call after your application's session/token refresh; recreate on account changes.
|
|
107
|
+
updateCsrfToken(value: string) { currentCsrfToken = value; },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
After login, call `await createSessionBackendClient('https://api.example.com')` and pass the returned `client` to the existing components. Call the returned `updateCsrfToken(newToken)` when your session refreshes its CSRF token. Close old dialogs and recreate session state on logout/account changes. Cross-origin cookies require `credentials: 'include'`, but their SameSite, Secure, domain/path and browser third-party cookie restrictions still apply. Cross-origin does not necessarily mean cross-site. Truly cross-site cookies typically require `SameSite=None; Secure`, and browser policy may still block them.
|
|
113
|
+
|
|
114
|
+
#### Cross-origin Bearer / JWT
|
|
115
|
+
|
|
116
|
+
Pass your website's existing mutable login state. Its login/refresh flow updates `auth.accessToken`; the callback reads it on every creation and poll.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
120
|
+
|
|
121
|
+
/** auth is your website's existing login state, updated by its login/refresh flow. */
|
|
122
|
+
export function createBearerBackendClient(backendOrigin: string, auth: { accessToken: string }) {
|
|
123
|
+
return createAuthLinkClient({
|
|
124
|
+
baseUrl: `${backendOrigin}/api/authlink`,
|
|
125
|
+
allowedBackendOrigins: [backendOrigin],
|
|
126
|
+
credentials: 'omit',
|
|
127
|
+
accessToken: () => auth.accessToken,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This must be the website user's login token, never an AuthLink tenant API key. The SDK adds `Authorization: Bearer ...`. With `accessToken`, credentials default to `omit` and CSRF can be omitted for a backend that exclusively authenticates Bearer tokens without cookie fallback. Explicit `include` or `same-origin` credentials require a CSRF provider even with Bearer authentication.
|
|
53
133
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
134
|
+
#### Backend CORS
|
|
135
|
+
|
|
136
|
+
Handle `OPTIONS` preflight and return appropriate CORS headers on preflight, actual responses and errors, including the session bootstrap endpoint. For a frontend at `https://www.example.com`:
|
|
137
|
+
|
|
138
|
+
```http
|
|
139
|
+
Access-Control-Allow-Origin: https://www.example.com
|
|
140
|
+
Access-Control-Allow-Credentials: true
|
|
141
|
+
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
|
142
|
+
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, Idempotency-Key, Authorization
|
|
143
|
+
Vary: Origin
|
|
58
144
|
```
|
|
59
145
|
|
|
60
|
-
|
|
146
|
+
Credentialed CORS cannot use `Access-Control-Allow-Origin: *`. Match a server-side origin allowlist before returning the current allowed origin; do not reflect arbitrary origins. Preflight generally has no login credentials: validate its origin/method/headers, then authenticate the actual request and validate CSRF for cookies and request ownership. Add custom CSRF/extra headers to `Access-Control-Allow-Headers`. The SDK cannot enable backend CORS or read results using `no-cors`. See [MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS).
|
|
147
|
+
|
|
148
|
+
#### Transport options
|
|
149
|
+
|
|
150
|
+
| Option | Behavior |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `baseUrl` | Application API prefix, defaults to `/api/authlink`; absolute for cross-origin |
|
|
153
|
+
| `allowedBackendOrigins` | Additional exact application backend origins; same-origin remains allowed |
|
|
154
|
+
| `credentials` | Defaults to `same-origin`, or `omit` with `accessToken`; use `include` for cross-origin cookies |
|
|
155
|
+
| `csrfToken` | Per-request sync/async token callback, required for cookie/session mode |
|
|
156
|
+
| `csrfHeaderName` | Defaults to `X-CSRF-Token`, configurable for framework integration |
|
|
157
|
+
| `accessToken` | Per-request sync/async website login token callback, without the `Bearer ` prefix |
|
|
158
|
+
| `headers` | Extra `HeadersInit`, or sync/async callback returning it |
|
|
159
|
+
| `authLinkOrigin` | Trusted QR service origin, independent of the application API origin |
|
|
160
|
+
| `fetch` | Optional implementation; destination validation still applies |
|
|
161
|
+
|
|
162
|
+
Custom headers cannot override SDK-managed `Accept`, `Content-Type`, `Authorization`, `Idempotency-Key` or the configured CSRF header (`RESERVED_HEADER`). Use `accessToken` for Bearer auth and `csrfHeaderName` to adapt framework CSRF validation. Import `ClientOptions` from the root entry for configuration types.
|
|
163
|
+
|
|
164
|
+
Unlisted origins produce `BACKEND_ORIGIN_NOT_ALLOWED` (replacing `CMS_ORIGIN_REQUIRED`); invalid allowlist entries or URLs produce `INVALID_BACKEND_ORIGIN` / `INVALID_BASE_URL`. Missing login/CSRF tokens produce `ACCESS_TOKEN_REQUIRED` / `CSRF_REQUIRED`. Cancellation and the 10-second timeout also cover async credential providers. Browser CORS failures normally surface as `NETWORK_ERROR`; inspect browser developer tools for details.
|
|
61
165
|
|
|
62
166
|
### Open a dialog directly (React, Vue or plain JavaScript)
|
|
63
167
|
|
|
64
168
|
```ts
|
|
65
169
|
import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
|
|
170
|
+
import { client } from './authlink-client';
|
|
66
171
|
import '@auth-link/web/styles.css';
|
|
67
172
|
|
|
68
173
|
// Call in your button handler, after your backend creates the request.
|
|
@@ -81,22 +186,62 @@ The dialog adapts to mobile widths, has a scrollable height, keyboard Escape and
|
|
|
81
186
|
|
|
82
187
|
### React
|
|
83
188
|
|
|
189
|
+
`showBound(requestId)` is application UI code: it sets a message and clears the ticket to unmount the dialog. The example also defines all rejection, expiry and close handlers. Use `<AuthLinkPanel mode="bind" />` or `<AuthLinkPanel mode="confirm" />`.
|
|
190
|
+
|
|
84
191
|
```tsx
|
|
192
|
+
"use client";
|
|
193
|
+
import { useRef, useState } from 'react';
|
|
194
|
+
import { requestKey } from '@auth-link/web';
|
|
195
|
+
import type { Mode, Ticket } from '@auth-link/web';
|
|
85
196
|
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
|
|
86
|
-
import type { Ticket, Confirmation } from '@auth-link/web';
|
|
87
197
|
import { client } from './authlink-client';
|
|
88
198
|
|
|
89
|
-
export function
|
|
90
|
-
const
|
|
91
|
-
|
|
199
|
+
export function AuthLinkPanel({ mode }: { mode: Mode }) {
|
|
200
|
+
const [ticket, setTicket] = useState<Ticket | null>(null);
|
|
201
|
+
const [busy, setBusy] = useState(false);
|
|
202
|
+
const [message, setMessage] = useState('');
|
|
203
|
+
const key = useRef<string | null>(null);
|
|
204
|
+
const creating = useRef(false);
|
|
205
|
+
function finish(message: string) {
|
|
206
|
+
setTicket(null);
|
|
207
|
+
setMessage(message);
|
|
208
|
+
}
|
|
209
|
+
function showBound(requestId: string) {
|
|
210
|
+
// UI feedback only. The backend has already verified and saved the binding.
|
|
211
|
+
finish(`Phone bound (request ${requestId})`);
|
|
212
|
+
}
|
|
213
|
+
function showConfirmed(requestId: string) {
|
|
214
|
+
finish(`Confirmed (request ${requestId})`);
|
|
215
|
+
}
|
|
216
|
+
const start = async () => {
|
|
217
|
+
if (creating.current || ticket) return;
|
|
218
|
+
creating.current = true;
|
|
219
|
+
setBusy(true);
|
|
220
|
+
setMessage('');
|
|
221
|
+
try {
|
|
222
|
+
key.current ??= requestKey();
|
|
223
|
+
setTicket(await client.create(mode, { idempotencyKey: key.current }));
|
|
224
|
+
key.current = null;
|
|
225
|
+
} catch { setMessage('Request failed; retry the same creation.'); }
|
|
226
|
+
finally { creating.current = false; setBusy(false); }
|
|
92
227
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
228
|
+
const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
|
|
229
|
+
return <>
|
|
230
|
+
{!ticket && <button type="button" disabled={busy} onClick={() => void start()}>Start verification</button>}
|
|
231
|
+
{ticket && <Component ticket={ticket} client={client}
|
|
232
|
+
onConfirmed={result => result.purpose === 'bind' ? showBound(result.requestId) : showConfirmed(result.requestId)}
|
|
233
|
+
onClose={() => finish('Closed')}
|
|
234
|
+
onRejected={() => finish('Request rejected on the phone')}
|
|
235
|
+
onExpired={() => finish('Request expired; start again')}
|
|
236
|
+
onError={error => setMessage(error.message)} />}
|
|
237
|
+
<p role="status">{message}</p>
|
|
238
|
+
</>;
|
|
97
239
|
}
|
|
98
240
|
```
|
|
99
241
|
|
|
242
|
+
The backend must verify, consume and persist the binding/confirmation before returning `confirmed`. `showBound` does not grant access or save a binding. You can instead show a toast, reload your own account-status API or navigate to a management page. Reload real binding state from the backend after a page refresh; never treat a callback, local state or localStorage as authorization. The example retains the same creation idempotency key after a failed request.
|
|
243
|
+
|
|
244
|
+
|
|
100
245
|
Mounting either component opens a dialog; unmount it in `onClose` (for example, clear the selected ticket). SSR renders no dialog and makes no requests. Omit `locale` for Simplified Chinese. Use `locale="zh-CN"` for Simplified Chinese or another code from the language table. Keep `client` and `ticket` stable between renders. The React entry preserves `"use client"`; SSR rendering does not create requests.
|
|
101
246
|
|
|
102
247
|
### Vue
|
|
@@ -177,7 +322,7 @@ Rejection returns `{ status: 'rejected', code: 'REJECTED', requestId, purpose }`
|
|
|
177
322
|
|
|
178
323
|
### Backend contract
|
|
179
324
|
|
|
180
|
-
The default prefix is `/api/authlink`. Both endpoints
|
|
325
|
+
The default prefix is `/api/authlink`. Both endpoints authenticate the website user using session cookies plus the configured CSRF header, or Bearer authentication. Creation also sends a stable `Idempotency-Key`. Derive the account from the validated session/token rather than a browser-supplied account ID.
|
|
181
326
|
|
|
182
327
|
```ts
|
|
183
328
|
import type { Ticket, Result } from '@auth-link/web';
|
|
@@ -204,7 +349,7 @@ import type { Ticket, Result } from '@auth-link/web';
|
|
|
204
349
|
|
|
205
350
|
Creation returns the public ticket from AuthLink: `requestId`, `purpose`, `qr`, `pairingCode`, `expiresAt` and the current `displayCode`. The display frame contains `value`, `serverTime`, `validUntil`, `periodSeconds: 30`. A confirmation ticket requires a display frame.
|
|
206
351
|
|
|
207
|
-
The result endpoint verifies
|
|
352
|
+
The result endpoint verifies the authenticated user and request ownership (plus CSRF for cookie mode) and forwards pending states and the current display frame. States `new`, `pending`, `approved` and `consumed` alone do not trigger success. Before returning `confirmed`, your backend must consume the AuthLink result with its saved state and stable consume idempotency key, check the complete receipt and persist confirmation for the correct session. Persist idempotence using request/verification IDs.
|
|
208
353
|
|
|
209
354
|
### Languages
|
|
210
355
|
|
|
@@ -230,6 +375,8 @@ Missing or unsupported locale codes fall back to Simplified Chinese. The panel s
|
|
|
230
375
|
|
|
231
376
|
Explicitly import `@auth-link/web/styles.css`. Override `--authlink-foreground`, `--authlink-background`, `--authlink-accent` and `--authlink-muted` on `:root` or `.authlink-dialog`; dialogs are mounted under `document.body`. All component classes use the `authlink-` prefix.
|
|
232
377
|
|
|
378
|
+
React and Vue accept `className` for the dialog. Vue also accepts standard string, array or object `class` attributes, combined with `className`. Refresh buttons use the latest `onRefresh` callback supplied by the parent.
|
|
379
|
+
|
|
233
380
|
| Import | Contents |
|
|
234
381
|
| --- | --- |
|
|
235
382
|
| `@auth-link/web` | Client, request key, supported locales, types and core tools |
|
|
@@ -249,7 +396,7 @@ Explicitly import `@auth-link/web/styles.css`. Override `--authlink-foreground`,
|
|
|
249
396
|
|
|
250
397
|
### License
|
|
251
398
|
|
|
252
|
-
|
|
399
|
+
MIT. See `LICENSE` for the full license text and `THIRD_PARTY_NOTICES.md` for bundled third-party attribution.
|
|
253
400
|
|
|
254
401
|
---
|
|
255
402
|
|
|
@@ -258,29 +405,6 @@ UNLICENSED. Public availability of the package does not grant an open-source lic
|
|
|
258
405
|
**AuthLink 官方前端组件包**,帮助网站接入手机绑定与 App 确认流程,支持 React 18/19、Vue 3.3+、TypeScript 和 ESM。
|
|
259
406
|
|
|
260
407
|
|
|
261
|
-
### 直接调用弹窗(React、Vue 或原生 JavaScript)
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
|
|
265
|
-
import '@auth-link/web/styles.css';
|
|
266
|
-
|
|
267
|
-
// 在按钮事件中创建请求,再打开弹窗;client 为前文的同源客户端。
|
|
268
|
-
const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
|
|
269
|
-
const modal = openAuthLinkConfirm({
|
|
270
|
-
ticket, client, locale: 'zh-CN',
|
|
271
|
-
onConfirmed: result => console.log(result.requestId),
|
|
272
|
-
onRejected: result => console.log(result.code), // REJECTED,此时弹窗已关闭
|
|
273
|
-
onClose: () => console.log('已关闭'),
|
|
274
|
-
});
|
|
275
|
-
// 绑定使用 openAuthLinkBind({ ticket: bindingTicket, client })。
|
|
276
|
-
// modal.close(); modal.setLocale('ja');
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
React / Vue 的 AuthLinkBind、AuthLinkConfirm 挂载后也直接打开弹窗,不再内嵌在页面。React 用 onClose、Vue 用 @close 清空 ticket 并卸载组件。服务端渲染不创建弹窗或请求。支持手机窄屏、内容滚动、关闭按钮及 Escape;要求浏览器支持原生 dialog.showModal()。弹窗挂到 document.body,主题变量请配置在 :root 或 .authlink-dialog。
|
|
280
|
-
|
|
281
|
-
关闭只停止前端查询,不拒绝或撤销服务端请求;请求仍按原有效期结束。确认成功或拒绝后弹窗自动关闭,分别回调 onConfirmed 或 onRejected,不触发用户关闭回调。直接调用时,页面卸载须执行 modal.destroy() 清理,它不触发用户关闭回调。切换语言用 modal.setLocale(),不会重建鉴权请求。
|
|
282
|
-
|
|
283
|
-
|
|
284
408
|
### 功能
|
|
285
409
|
|
|
286
410
|
- **扫码绑定**:网站显示二维码,用户使用 AuthLink App 扫码并确认绑定。
|
|
@@ -294,7 +418,7 @@ React / Vue 的 AuthLinkBind、AuthLinkConfirm 挂载后也直接打开弹窗,
|
|
|
294
418
|
|
|
295
419
|
### 接入步骤
|
|
296
420
|
|
|
297
|
-
1.
|
|
421
|
+
1. 在应用后台完成站点租户配置,并实现本文的创建请求、查询结果两个业务后台接口(同源或显式配置的跨域后台)。
|
|
298
422
|
2. 安装包、导入样式,创建指向应用后台的客户端。
|
|
299
423
|
3. 用户点击绑定或验证时创建请求,将返回的 ticket 交给 React/Vue 组件。
|
|
300
424
|
4. 用户在 App 确认后,应用后台验证并消费结果,组件收到可信确认事件。
|
|
@@ -317,46 +441,226 @@ React / Vue 的 AuthLinkBind、AuthLinkConfirm 挂载后也直接打开弹窗,
|
|
|
317
441
|
npm install @auth-link/web
|
|
318
442
|
```
|
|
319
443
|
|
|
320
|
-
###
|
|
444
|
+
### 创建应用客户端(默认同源 Cookie 模式)
|
|
445
|
+
|
|
446
|
+
`readCsrfTokenFromYourCms` 是接入方函数,不是包里的导出;名称不要求与你的网站框架一致,也不要求使用 CMS。它只读取**你的网站后台提供的 CSRF token**,用于保护携带登录 cookie 的请求,不是 AuthLink 租户 API key、登录 cookie 或手机核对码。
|
|
447
|
+
|
|
448
|
+
下面采用页面 meta 标签的方式。后台需要先准备 token:
|
|
449
|
+
|
|
450
|
+
1. 优先复用网站框架现有的 CSRF 机制;没有现成机制时,在服务端为当前登录 session 生成并保存密码学安全随机 token,例如 Node 的 `randomBytes(32).toString('base64url')`。不要在浏览器随机生成一个值,也不要使用所有用户共用的固定字符串。
|
|
451
|
+
2. 服务端使用当前 session 的 token 渲染下面的标签,正确转义 HTML 属性;`{{ csrfToken }}` 表示服务器模板变量,不是可直接照抄的 token。包含会话 token 的页面不能被公开缓存。
|
|
452
|
+
3. 创建和查询接口都读取 `X-CSRF-Token`,与同一登录 session 保存的 token 校验;缺失、不匹配或请求来源不符合网站要求时拒绝请求。仅让前端发送这个头,并不会自动启用后台校验。
|
|
453
|
+
4. 登录、退出或会话/token 更新后,让页面重新取得当前 token。SPA 也可以由网站实现同源会话初始化接口返回 token,再通过 `csrfToken` 回调提供;这不是 SDK 自带接口。
|
|
454
|
+
|
|
455
|
+
```html
|
|
456
|
+
<!-- 由你的网站后台渲染到当前用户的页面 head 中 -->
|
|
457
|
+
<meta name="csrf-token" content="{{ csrfToken }}">
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
新建 `authlink-client.ts`:
|
|
321
461
|
|
|
322
462
|
```ts
|
|
323
|
-
|
|
463
|
+
// authlink-client.ts
|
|
464
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
324
465
|
import '@auth-link/web/styles.css';
|
|
325
466
|
|
|
326
|
-
//
|
|
467
|
+
// The application server renders this meta element using the current session's
|
|
468
|
+
// CSRF token. This function only reads that token; it does not generate one.
|
|
469
|
+
export function readCsrfTokenFromYourCms(): string {
|
|
470
|
+
const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
|
|
471
|
+
if (!token) throw new Error('Missing CSRF token; load the authenticated application page first.');
|
|
472
|
+
return token;
|
|
473
|
+
}
|
|
474
|
+
|
|
327
475
|
export const client = createAuthLinkClient({
|
|
328
476
|
baseUrl: '/api/authlink',
|
|
329
|
-
csrfToken:
|
|
330
|
-
|
|
477
|
+
csrfToken: readCsrfTokenFromYourCms,
|
|
478
|
+
// Replace this example with the configured AuthLink service origin.
|
|
479
|
+
authLinkOrigin: 'https://auth.example.com',
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
把 `https://auth.example.com` 换成实际 AuthLink 服务来源。SDK 会在每次请求时调用读取函数,自动把值放入 `X-CSRF-Token`,并携带同源登录 cookie;前端不需要读取 HttpOnly 登录 cookie。若网站框架默认使用其他 CSRF 头名,可用 `csrfHeaderName` 配成后台期望的头名,或者实现自定义 `AuthLinkClient`。
|
|
484
|
+
|
|
485
|
+
`create` 只传用途,网站账号由后台登录 session 决定。客户端通常只初始化一次;用户点击按钮时才创建请求。完整调用见下方 React 示例,不要把 `client.create()` 放在模块加载、React effect 或 Vue watch 中。
|
|
486
|
+
|
|
487
|
+
### 前后端分离与跨域后台(0.0.2 起)
|
|
488
|
+
|
|
489
|
+
默认仍使用同源 `/api/authlink`。直接访问另一个后台来源时,同时配置完整 `baseUrl` 和 `allowedBackendOrigins`,例如网页在 `https://www.example.com`,业务后台在 `https://api.example.com`。来源是协议、主机和端口的组合;同一主域名下不同子域名也属于跨域。
|
|
490
|
+
|
|
491
|
+
`allowedBackendOrigins` 是前端明确授权的**业务后台来源**,不能带路径、通配符、用户名或密码,不能来自用户输入。生产环境使用 HTTPS;HTTP 仅允许 localhost / 127.0.0.1 / [::1] 开发环境,端口必须匹配。SDK 会先校验目标再读取凭据,并拒绝 HTTP 重定向。`authLinkOrigin` 则用于校验二维码中的 AuthLink 服务来源,两者作用不同,可按需要同时配置。
|
|
492
|
+
|
|
493
|
+
#### 跨域 Cookie / session
|
|
494
|
+
|
|
495
|
+
下面给出完整的初始化函数。`GET /api/session` 是接入网站自己实现的示例会话接口,不是包或 AuthLink 服务自带的接口;它应根据现有登录 cookie 返回 `{ "csrfToken": "当前会话的真实CSRF token" }`,设置 `Cache-Control: no-store` 并配置下文的 CORS。部署时将路径换成网站实际的会话初始化接口。
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
499
|
+
|
|
500
|
+
/** /api/session is an example endpoint implemented by your application backend. */
|
|
501
|
+
export async function createSessionBackendClient(backendOrigin: string) {
|
|
502
|
+
const response = await fetch(`${backendOrigin}/api/session`, {
|
|
503
|
+
credentials: 'include', cache: 'no-store', redirect: 'error',
|
|
504
|
+
});
|
|
505
|
+
if (!response.ok) throw new Error('Session initialization failed');
|
|
506
|
+
const session: unknown = await response.json();
|
|
507
|
+
if (!session || typeof session !== 'object' || !('csrfToken' in session) ||
|
|
508
|
+
typeof session.csrfToken !== 'string' || !session.csrfToken) {
|
|
509
|
+
throw new Error('The session endpoint must return { csrfToken: string }');
|
|
510
|
+
}
|
|
511
|
+
let currentCsrfToken = session.csrfToken;
|
|
512
|
+
return {
|
|
513
|
+
client: createAuthLinkClient({
|
|
514
|
+
baseUrl: `${backendOrigin}/api/authlink`,
|
|
515
|
+
allowedBackendOrigins: [backendOrigin],
|
|
516
|
+
credentials: 'include',
|
|
517
|
+
csrfToken: () => currentCsrfToken,
|
|
518
|
+
}),
|
|
519
|
+
// Call after your application's session/token refresh; recreate on account changes.
|
|
520
|
+
updateCsrfToken(value: string) { currentCsrfToken = value; },
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
登录后调用 `await createSessionBackendClient('https://api.example.com')`,把返回的 `client` 交给现有 React/Vue 组件。网站刷新 CSRF token 时调用返回的 `updateCsrfToken(newToken)`;退出或切换账号时关闭旧弹窗,清理旧状态并重新初始化。`credentials: 'include'` 才会让跨域请求携带后台 cookie,但仍受 cookie 的 SameSite、Secure、Domain/Path 及浏览器第三方 Cookie 策略约束;跨域不一定跨站。真正跨站的 cookie 通常需要 `SameSite=None; Secure`,浏览器仍可能阻止第三方 Cookie。不能把 `include` 当成绕过 Cookie 策略的开关。
|
|
526
|
+
|
|
527
|
+
#### 跨域 Bearer token / JWT
|
|
528
|
+
|
|
529
|
+
传入网站自己的登录状态对象;网站完成登录或刷新后更新这个对象的 `accessToken`。回调会在每次创建和轮询时重新读取最新 token,无需重建弹窗。
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
533
|
+
|
|
534
|
+
/** auth is your website's existing login state, updated by its login/refresh flow. */
|
|
535
|
+
export function createBearerBackendClient(backendOrigin: string, auth: { accessToken: string }) {
|
|
536
|
+
return createAuthLinkClient({
|
|
537
|
+
baseUrl: `${backendOrigin}/api/authlink`,
|
|
538
|
+
allowedBackendOrigins: [backendOrigin],
|
|
539
|
+
credentials: 'omit',
|
|
540
|
+
accessToken: () => auth.accessToken,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
`accessToken` 必须是接入网站的用户登录 token,不能是 AuthLink 租户 API key。SDK 自动添加 `Authorization: Bearer ...`。纯 Bearer 模式默认 `credentials: 'omit'`,可不传 `csrfToken`;后台应仅按 Bearer token 认证,不回退到 Cookie 登录。若显式设置 `credentials: 'include'` 或 `'same-origin'` 混用 Cookie,仍须提供并校验 `csrfToken`。
|
|
546
|
+
|
|
547
|
+
#### 后台 CORS 配置
|
|
548
|
+
|
|
549
|
+
后台需处理浏览器发出的 `OPTIONS` 预检,并在预检、实际响应和错误响应中返回相应 CORS 头。会话初始化接口也需要这些设置。以下假设前端来源是 `https://www.example.com`:
|
|
550
|
+
|
|
551
|
+
```http
|
|
552
|
+
Access-Control-Allow-Origin: https://www.example.com
|
|
553
|
+
Access-Control-Allow-Credentials: true
|
|
554
|
+
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
|
555
|
+
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, Idempotency-Key, Authorization
|
|
556
|
+
Vary: Origin
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
携带 Cookie 时不能把 `Access-Control-Allow-Origin` 配为 `*`;多个允许来源应匹配白名单后返回当前来源,不能无条件反射任意 Origin。预检通常不携带登录凭据,因此先验证 Origin、请求方法和请求头,再对实际请求验证登录身份、CSRF(Cookie 模式)和归属。若改了 CSRF 头名或添加自定义头,也要加入 `Access-Control-Allow-Headers`。SDK 不能替后台开启 CORS,也不能用 `no-cors` 读取验证结果。见 [MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)。
|
|
560
|
+
|
|
561
|
+
#### 请求客户端配置
|
|
562
|
+
|
|
563
|
+
| 配置 | 说明 |
|
|
564
|
+
| --- | --- |
|
|
565
|
+
| `baseUrl` | 业务后台前缀,默认 `/api/authlink`;跨域时填写完整地址 |
|
|
566
|
+
| `allowedBackendOrigins` | 额外允许的精确后台来源数组,默认只允许当前网页同源 |
|
|
567
|
+
| `credentials` | Cookie 模式默认 `same-origin`;提供 `accessToken` 时默认 `omit`;跨域 Cookie 显式设置 `include` |
|
|
568
|
+
| `csrfToken` | 每次请求读取 CSRF token 的同步或异步函数;Cookie 模式必填 |
|
|
569
|
+
| `csrfHeaderName` | CSRF 请求头名,默认 `X-CSRF-Token`,例如可设为 `X-XSRF-TOKEN` |
|
|
570
|
+
| `accessToken` | 每次请求读取网站 Bearer token 的同步或异步函数;不要包含 `Bearer ` 前缀 |
|
|
571
|
+
| `headers` | 额外的 `HeadersInit` 或返回它的同步/异步函数,可传工作区等自定义头 |
|
|
572
|
+
| `authLinkOrigin` | 核对二维码中的 AuthLink 服务来源,与业务后台来源独立 |
|
|
573
|
+
| `fetch` | 可选 fetch 实现;仍受目标来源和协议校验约束 |
|
|
574
|
+
|
|
575
|
+
SDK 管理 `Accept`、`Content-Type`、`Authorization`、`Idempotency-Key` 和配置的 CSRF 头,`headers` 不能覆盖这些字段,否则报 `RESERVED_HEADER`。用 `accessToken` 设置 Bearer token,用 `csrfHeaderName` 适配后台框架。`ClientOptions` 可从根入口导入为 TypeScript 类型。
|
|
576
|
+
|
|
577
|
+
未授权来源报 `BACKEND_ORIGIN_NOT_ALLOWED`(替代旧版的 `CMS_ORIGIN_REQUIRED`),非法来源/地址分别报 `INVALID_BACKEND_ORIGIN` / `INVALID_BASE_URL`;缺少当前登录 token 报 `ACCESS_TOKEN_REQUIRED`,缺少必要 CSRF 报 `CSRF_REQUIRED`。凭据读取也受请求取消和 10 秒超时控制。网络层的 CORS 错误由浏览器拦截,SDK 通常只能报告 `NETWORK_ERROR`,具体原因需查看浏览器开发者工具。
|
|
578
|
+
|
|
579
|
+
### 直接调用弹窗(React、Vue 或原生 JavaScript)
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
|
|
583
|
+
import { client } from './authlink-client';
|
|
584
|
+
import '@auth-link/web/styles.css';
|
|
585
|
+
|
|
586
|
+
// 在按钮事件中创建请求,再打开弹窗;client 为上文 authlink-client.ts 导出的同源客户端。
|
|
587
|
+
const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
|
|
588
|
+
const modal = openAuthLinkConfirm({
|
|
589
|
+
ticket, client, locale: 'zh-CN',
|
|
590
|
+
onConfirmed: result => console.log(result.requestId),
|
|
591
|
+
onRejected: result => console.log(result.code), // REJECTED,此时弹窗已关闭
|
|
592
|
+
onClose: () => console.log('已关闭'),
|
|
331
593
|
});
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
const ticket = await client.create('bind', { idempotencyKey: key });
|
|
335
|
-
// 创建响应丢失时复用 key 重试,不能每次重试都生成新 key。
|
|
594
|
+
// 绑定使用 openAuthLinkBind({ ticket: bindingTicket, client })。
|
|
595
|
+
// modal.close(); modal.setLocale('ja');
|
|
336
596
|
```
|
|
337
597
|
|
|
338
|
-
|
|
598
|
+
React / Vue 的 AuthLinkBind、AuthLinkConfirm 挂载后也直接打开弹窗,不再内嵌在页面。React 用 onClose、Vue 用 @close 清空 ticket 并卸载组件。服务端渲染不创建弹窗或请求。支持手机窄屏、内容滚动、关闭按钮及 Escape;要求浏览器支持原生 dialog.showModal()。弹窗挂到 document.body,主题变量请配置在 :root 或 .authlink-dialog。
|
|
599
|
+
|
|
600
|
+
关闭只停止前端查询,不拒绝或撤销服务端请求;请求仍按原有效期结束。确认成功或拒绝后弹窗自动关闭,分别回调 onConfirmed 或 onRejected,不触发用户关闭回调。直接调用时,页面卸载须执行 modal.destroy() 清理,它不触发用户关闭回调。切换语言用 modal.setLocale(),不会重建鉴权请求。
|
|
601
|
+
|
|
339
602
|
|
|
340
603
|
### React
|
|
341
604
|
|
|
605
|
+
下面的 `showBound` 也是接入方函数,负责更新页面提示和清空 `ticket`,从而卸载弹窗;它不向后台提交“绑定成功”,也不保存绑定。绑定及确认记录必须在后台查询结果接口验证、消费并保存后,才返回 `confirmed`。
|
|
606
|
+
|
|
607
|
+
这个示例包含按钮、请求创建、结果提示和弹窗清理,不再依赖未定义的 `showBound`、`showRejected` 等函数。使用 `<AuthLinkPanel mode="bind" />` 绑定,使用 `<AuthLinkPanel mode="confirm" />` 请求手机确认。
|
|
608
|
+
|
|
342
609
|
```tsx
|
|
610
|
+
"use client";
|
|
611
|
+
import { useRef, useState } from 'react';
|
|
612
|
+
import { requestKey } from '@auth-link/web';
|
|
613
|
+
import type { Mode, Ticket } from '@auth-link/web';
|
|
343
614
|
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
|
|
344
|
-
import
|
|
345
|
-
import { client } from './authlink-client'; // 上文创建的客户端
|
|
346
|
-
|
|
347
|
-
export function BindPanel({ ticket }: { ticket: Ticket }) {
|
|
348
|
-
return <AuthLinkBind ticket={ticket} client={client} locale="zh-CN"
|
|
349
|
-
onConfirmed={result => showBound(result.requestId)}
|
|
350
|
-
onRejected={() => showRejected()} onExpired={() => showExpired()}
|
|
351
|
-
onError={error => showNetworkError(error.message)} />;
|
|
352
|
-
}
|
|
615
|
+
import { client } from './authlink-client';
|
|
353
616
|
|
|
354
|
-
export function
|
|
355
|
-
|
|
356
|
-
|
|
617
|
+
export function AuthLinkPanel({ mode }: { mode: Mode }) {
|
|
618
|
+
const [ticket, setTicket] = useState<Ticket | null>(null);
|
|
619
|
+
const [busy, setBusy] = useState(false);
|
|
620
|
+
const [message, setMessage] = useState('');
|
|
621
|
+
const key = useRef<string | null>(null);
|
|
622
|
+
const creating = useRef(false);
|
|
623
|
+
function finish(message: string) {
|
|
624
|
+
setTicket(null);
|
|
625
|
+
setMessage(message);
|
|
626
|
+
}
|
|
627
|
+
function showBound(requestId: string) {
|
|
628
|
+
// UI feedback only. The backend has already verified and saved the binding.
|
|
629
|
+
finish(`手机绑定成功(请求 ${requestId})`);
|
|
630
|
+
}
|
|
631
|
+
function showConfirmed(requestId: string) {
|
|
632
|
+
finish(`手机确认成功(请求 ${requestId})`);
|
|
633
|
+
}
|
|
634
|
+
const start = async () => {
|
|
635
|
+
if (creating.current || ticket) return;
|
|
636
|
+
creating.current = true;
|
|
637
|
+
setBusy(true);
|
|
638
|
+
setMessage('');
|
|
639
|
+
try {
|
|
640
|
+
key.current ??= requestKey();
|
|
641
|
+
setTicket(await client.create(mode, { idempotencyKey: key.current }));
|
|
642
|
+
key.current = null;
|
|
643
|
+
} catch { setMessage('请求未完成,可重试同一次创建。'); }
|
|
644
|
+
finally { creating.current = false; setBusy(false); }
|
|
645
|
+
};
|
|
646
|
+
const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
|
|
647
|
+
return <>
|
|
648
|
+
{!ticket && <button type="button" disabled={busy} onClick={() => void start()}>开始验证</button>}
|
|
649
|
+
{ticket && <Component ticket={ticket} client={client}
|
|
650
|
+
onConfirmed={result => result.purpose === 'bind' ? showBound(result.requestId) : showConfirmed(result.requestId)}
|
|
651
|
+
onClose={() => finish('已关闭')}
|
|
652
|
+
onRejected={() => finish('手机已拒绝本次请求')}
|
|
653
|
+
onExpired={() => finish('请求已过期,请重新发起')}
|
|
654
|
+
onError={error => setMessage(error.message)} />}
|
|
655
|
+
<p role="status">{message}</p>
|
|
656
|
+
</>;
|
|
357
657
|
}
|
|
358
658
|
```
|
|
359
659
|
|
|
660
|
+
`requestId` 用于展示或追踪本次请求,不是授权凭据。`showBound` 也可以按网站需求显示 toast、重新查询已有的账号状态接口或导航到绑定管理页。刷新页面后,应重新从后台读取真实绑定状态,不能只依赖 React state 或 localStorage。其他业务接口仍需在后台检查权限。
|
|
661
|
+
|
|
662
|
+
创建失败时示例保留同一个幂等键;再次点击会重试同一次创建。创建成功后清空幂等键,新的用户操作才创建新请求。关闭、成功、拒绝和过期都会清空 ticket;网络错误保留弹窗,供其重试当前请求。
|
|
663
|
+
|
|
360
664
|
确认组件默认只显示动态数字;`showQr` 可额外显示扫码入口。绑定组件始终显示二维码。`onRefresh` 可提供重新创建请求的函数;仅过期后显示该按钮。组件自身的“重试”只恢复当前请求,不创建第二个请求。
|
|
361
665
|
|
|
362
666
|
未指定 `locale` 时使用简体中文;不支持的语言代码也回退到简体中文。组件根元素设置对应的 `lang`,语言切换不会重新创建鉴权请求。
|
|
@@ -367,25 +671,47 @@ React 入口保留 `"use client"`,可用于 Next.js 客户端组件。导入
|
|
|
367
671
|
|
|
368
672
|
```vue
|
|
369
673
|
<script setup lang="ts">
|
|
674
|
+
import { ref, shallowRef } from 'vue';
|
|
675
|
+
import { requestKey } from '@auth-link/web';
|
|
676
|
+
import type { Confirmation, Mode, Ticket } from '@auth-link/web';
|
|
370
677
|
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
|
|
371
|
-
import
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
678
|
+
import { client } from './authlink-client';
|
|
679
|
+
const props = defineProps<{ mode: Mode }>();
|
|
680
|
+
const ticket = shallowRef<Ticket | null>(null);
|
|
681
|
+
const busy = ref(false);
|
|
682
|
+
const message = ref('');
|
|
683
|
+
let key: string | null = null;
|
|
684
|
+
function finish(text: string) { ticket.value = null; message.value = text; }
|
|
685
|
+
function confirmed(result: Confirmation) {
|
|
686
|
+
finish(`${result.purpose === 'bind' ? '已绑定' : '已确认'}(请求 ${result.requestId})`);
|
|
687
|
+
}
|
|
688
|
+
async function start() {
|
|
689
|
+
if (busy.value) return;
|
|
690
|
+
busy.value = true;
|
|
691
|
+
key ??= requestKey();
|
|
692
|
+
try {
|
|
693
|
+
ticket.value = await client.create(props.mode, { idempotencyKey: key });
|
|
694
|
+
key = null;
|
|
695
|
+
} catch { message.value = '请求未完成,可重试同一次创建。'; }
|
|
696
|
+
finally { busy.value = false; }
|
|
697
|
+
}
|
|
376
698
|
</script>
|
|
377
699
|
|
|
378
700
|
<template>
|
|
379
|
-
<
|
|
380
|
-
|
|
701
|
+
<button v-if="!ticket" type="button" :disabled="busy" @click="start">开始验证</button>
|
|
702
|
+
<AuthLinkBind v-else-if="mode === 'bind'" :ticket="ticket" :client="client"
|
|
703
|
+
@confirmed="confirmed" @close="finish('已关闭')" @rejected="finish('手机已拒绝本次请求')"
|
|
704
|
+
@expired="finish('请求已过期,请重新发起')" @error="message = '网络异常,请重试。'" />
|
|
381
705
|
<AuthLinkConfirm v-else :ticket="ticket" :client="client"
|
|
382
|
-
|
|
706
|
+
@confirmed="confirmed" @close="finish('已关闭')" @rejected="finish('手机已拒绝本次请求')"
|
|
707
|
+
@expired="finish('请求已过期,请重新发起')" @error="message = '网络异常,请重试。'" />
|
|
708
|
+
<p role="status">{{ message }}</p>
|
|
383
709
|
</template>
|
|
384
710
|
```
|
|
385
711
|
|
|
386
712
|
拒绝时返回 `{ status: 'rejected', code: 'REJECTED', requestId, purpose }`。`REJECTED` 是 SDK 业务状态码,不是 HTTP 状态码或授权凭证。组件自动关闭,不再显示“已拒绝 / 重新发起”面板,也不触发 `onClose`;提示和重新发起由接入网站在回调中决定。React / Vue 请在 `onRejected` / `@rejected` 中清空 ticket、卸载组件。无 UI 的 hook 仍保留 `snapshot.status === 'rejected'`,供自定义界面使用。
|
|
387
713
|
|
|
388
|
-
|
|
714
|
+
此示例在过期后清空 ticket,用户可再次点击按钮发起请求。如果需要在过期弹窗内提供重新发起按钮,可自行传入 `onRefresh`,在回调中创建新请求并更新 ticket。`useAuthLink(() => options)` composable 与 React 的 `useAuthLink(options)` hook 可用于自定义界面,返回 `snapshot` 和 `retry`;Vue 的 snapshot 是浅 ref。
|
|
389
715
|
|
|
390
716
|
### 选择弹窗样式
|
|
391
717
|
|
|
@@ -447,19 +773,19 @@ openAuthLinkConfirm({ ticket, client, variant: 'B' });
|
|
|
447
773
|
| `@auth-link/web/vue` | Vue 组件和 `useAuthLink` |
|
|
448
774
|
| `@auth-link/web/styles.css` | 组件样式,需显式导入 |
|
|
449
775
|
|
|
450
|
-
|
|
776
|
+
### 应用后台接口约定
|
|
451
777
|
|
|
452
778
|
默认前缀 `/api/authlink`;下面两个接口由接入方实现,**不是直接把浏览器请求转发到租户密钥 API**。
|
|
453
779
|
|
|
454
780
|
#### `POST /requests`
|
|
455
781
|
|
|
456
|
-
|
|
782
|
+
浏览器按所选模式携带网站登录凭据:Cookie 模式发送 session cookie 和配置的 CSRF 头,Bearer 模式发送 `Authorization`。创建还发送稳定的 `Idempotency-Key`,body:
|
|
457
783
|
|
|
458
784
|
```json
|
|
459
785
|
{"purpose":"bind"}
|
|
460
786
|
```
|
|
461
787
|
|
|
462
|
-
|
|
788
|
+
后台从经过验证的 session 或 Bearer token 取得用户 ID,创建并持久保存 state、创建/消费幂等键、用户/session 关联,然后调用 AuthLink 服务端创建请求。返回其公开 ticket:
|
|
463
789
|
|
|
464
790
|
```ts
|
|
465
791
|
type Ticket = {
|
|
@@ -478,7 +804,7 @@ type Ticket = {
|
|
|
478
804
|
|
|
479
805
|
#### `POST /requests/{requestId}/result`
|
|
480
806
|
|
|
481
|
-
|
|
807
|
+
同样验证登录身份(Cookie 模式还需 CSRF)和请求归属。后台调用 AuthLink 状态接口,将 `displayCode` 原样返回,不能缓存跨过 `validUntil`。新代码由后台实时获取,不是前端自行生成。
|
|
482
808
|
|
|
483
809
|
尚未完成时返回 `{requestId,status,displayCode?}`;状态可为 new / pending / approved / consumed / rejected / expired。`approved` 不能触发成功事件。
|
|
484
810
|
|
|
@@ -509,7 +835,9 @@ type Ticket = {
|
|
|
509
835
|
|
|
510
836
|
### 样式与语言
|
|
511
837
|
|
|
512
|
-
CSS 单独导入,所有类使用 `authlink-`
|
|
838
|
+
CSS 单独导入,所有类使用 `authlink-` 前缀。弹窗挂载到 `document.body`,可在 `:root` 或 `.authlink-dialog` 覆盖 `--authlink-foreground`、`--authlink-background`、`--authlink-accent`、`--authlink-muted`。面板语言见下表,默认简体中文。
|
|
839
|
+
|
|
840
|
+
React / Vue 均支持 `className`;Vue 还支持字符串、数组和对象形式的 `class`,与 `className` 合并。“重新发起”按钮使用父组件最新传入的 `onRefresh` 回调。
|
|
513
841
|
|
|
514
842
|
|
|
515
843
|
| Code | Language |
|
|
@@ -534,33 +862,16 @@ CSS 单独导入,所有类使用 `authlink-` 前缀。可在宿主容器覆盖
|
|
|
534
862
|
|
|
535
863
|
React 18.2+ / 19、Vue 3.3+、TypeScript、ESM。React 和 Vue 是可选 peer,只安装所用框架。构建工具环境需 Node 22.13+。
|
|
536
864
|
|
|
865
|
+
### 许可证
|
|
866
|
+
|
|
867
|
+
MIT。完整许可文本见 `LICENSE`,打包包含的第三方库归属说明见 `THIRD_PARTY_NOTICES.md`。
|
|
868
|
+
|
|
537
869
|
---
|
|
538
870
|
|
|
539
871
|
## 日本語
|
|
540
872
|
|
|
541
873
|
**AuthLink のスマートフォン登録とアプリでの確認**を Web サイトに組み込む React / Vue コンポーネントです。登録用 QR コード、同期する 6 桁の照合コード、カウントダウン、確認結果を表示します。特定のサイト基盤には依存しません。
|
|
542
874
|
|
|
543
|
-
### 関数でダイアログを開く(React / Vue / JavaScript)
|
|
544
|
-
|
|
545
|
-
```ts
|
|
546
|
-
import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
|
|
547
|
-
import '@auth-link/web/styles.css';
|
|
548
|
-
|
|
549
|
-
const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
|
|
550
|
-
const modal = openAuthLinkConfirm({
|
|
551
|
-
ticket, client, locale: 'ja',
|
|
552
|
-
onConfirmed: result => console.log(result.requestId),
|
|
553
|
-
onClose: () => console.log('Closed'),
|
|
554
|
-
});
|
|
555
|
-
// 登録: openAuthLinkBind({ ticket: bindingTicket, client })
|
|
556
|
-
// modal.close(); modal.setLocale('en');
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
React / Vue コンポーネントもマウント時にダイアログを開きます。React の onClose、Vue の @close で ticket を消去してアンマウントしてください。SSR はダイアログも通信も生成しません。スマートフォンの画面幅、スクロール、閉じるボタン、Escape に対応し、ネイティブ dialog.showModal() が必要です。body 直下に表示するため、テーマ変数は :root または .authlink-dialog に設定します。
|
|
560
|
-
|
|
561
|
-
閉じてもサーバーのリクエストを拒否・取消しません。元の有効期限は維持されます。承認・拒否時は自動で閉じ、onConfirmed または onRejected を通知します。ユーザー操作の onClose は通知しません。関数で開いた場合はページ破棄時に modal.destroy() を呼んで通信と UI を解除してください。destroy() は onClose を通知しません。modal.setLocale() はリクエストを作り直さずに言語を切り替えます。
|
|
562
|
-
|
|
563
|
-
|
|
564
875
|
### 主な機能
|
|
565
876
|
|
|
566
877
|
- ブラウザー内で生成するスマートフォン登録用 QR コード。
|
|
@@ -583,55 +894,221 @@ npm install @auth-link/web
|
|
|
583
894
|
### 接続の流れ
|
|
584
895
|
|
|
585
896
|
1. サイトのバックエンドに AuthLink のテナント資格情報を設定します。
|
|
586
|
-
2.
|
|
897
|
+
2. 下記の業務バックエンド(同一オリジンまたは明示的に許可した別オリジン)の作成・結果取得 API を実装します。
|
|
587
898
|
3. ユーザー操作でリクエストを作成し、取得した ticket をコンポーネントに渡します。
|
|
588
899
|
4. バックエンドが結果を検証・消費した後に確認完了を返します。
|
|
589
900
|
|
|
590
|
-
ブラウザーからこのライブラリを呼び出せますが、テナント API
|
|
901
|
+
ブラウザーからこのライブラリを呼び出せますが、テナント API キーと結果の消費処理は必ずサーバー側に置きます。標準クライアントは既定の同一オリジンと、明示的に許可した別オリジンに対応します。独自の通信方式では `AuthLinkClient` を実装できますが、資格情報と検証をブラウザーに移さないでください。
|
|
902
|
+
|
|
903
|
+
### クライアント作成(既定の同一オリジン Cookie モード)
|
|
904
|
+
|
|
905
|
+
`readCsrfTokenFromYourCms` は SDK のエクスポートではなく、接続先サイトで実装する関数です。現在のログインセッションに対してバックエンドが発行した CSRF token を読み取ります。CMS は必須ではありません。ブラウザーで値を生成したり、AuthLink の API キーを返したりしないでください。
|
|
906
|
+
|
|
907
|
+
既存フレームワークの CSRF 機構を優先してください。独自に実装する場合は、サーバーで暗号学的乱数(Node の `randomBytes(32).toString('base64url')` など)を生成し、セッションに保存して HTML 属性をエスケープしたうえでページに埋め込みます。下記の `{{ csrfToken }}` はサーバーテンプレートの変数です。このページを公開キャッシュしないでください。
|
|
908
|
+
|
|
909
|
+
```html
|
|
910
|
+
<meta name="csrf-token" content="{{ csrfToken }}">
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
SDK は各リクエストで token を読み取り、`X-CSRF-Token` と同一オリジンのセッション cookie を送信します。作成 API と結果 API の両方で、バックエンドが同じセッションの token と照合し、欠落・不一致や不正なリクエスト元を拒否する必要があります。ログインやセッション変更後は最新 token を取得してください。SPA ではサイト独自の同一オリジン初期化 API から取得する方法も使えます。別のヘッダー名を使うフレームワークでは`csrfHeaderName` または独自 `AuthLinkClient` で適合します。
|
|
591
914
|
|
|
592
|
-
|
|
915
|
+
`authlink-client.ts` を作成します:
|
|
593
916
|
|
|
594
917
|
```ts
|
|
595
918
|
// authlink-client.ts
|
|
596
|
-
import { createAuthLinkClient
|
|
919
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
597
920
|
import '@auth-link/web/styles.css';
|
|
598
921
|
|
|
922
|
+
// The application server renders this meta element using the current session's
|
|
923
|
+
// CSRF token. This function only reads that token; it does not generate one.
|
|
924
|
+
export function readCsrfTokenFromYourCms(): string {
|
|
925
|
+
const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
|
|
926
|
+
if (!token) throw new Error('Missing CSRF token; load the authenticated application page first.');
|
|
927
|
+
return token;
|
|
928
|
+
}
|
|
929
|
+
|
|
599
930
|
export const client = createAuthLinkClient({
|
|
600
931
|
baseUrl: '/api/authlink',
|
|
601
|
-
csrfToken:
|
|
602
|
-
|
|
603
|
-
if (!token) throw new Error('Missing CSRF token');
|
|
604
|
-
return token;
|
|
605
|
-
},
|
|
932
|
+
csrfToken: readCsrfTokenFromYourCms,
|
|
933
|
+
// Replace this example with the configured AuthLink service origin.
|
|
606
934
|
authLinkOrigin: 'https://auth.example.com',
|
|
607
935
|
});
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
`https://auth.example.com` は実際の AuthLink オリジンに置き換えてください。リクエストは下記の React 例のようにユーザー操作で作成します。
|
|
939
|
+
|
|
940
|
+
### フロントエンドとバックエンドの別オリジン対応(0.0.2 以降)
|
|
941
|
+
|
|
942
|
+
既定は同一オリジンの `/api/authlink` です。別オリジンの業務 API に直接接続するには完全な `baseUrl` と `allowedBackendOrigins` を指定します。例:フロントエンド `https://www.example.com`、バックエンド `https://api.example.com`。サブドメインやポートが異なる場合も別オリジンです。許可リストにはパス、ワイルドカード、ユーザー名・パスワードを含めず、信頼する固定設定を使います。HTTPS が必須で、開発用 HTTP loopback(localhost / 127.0.0.1 / [::1])のみ例外です。
|
|
943
|
+
|
|
944
|
+
SDK は認証情報を読む前に接続先を検証し、リダイレクトを拒否します。`authLinkOrigin` は QR 内の AuthLink サービスオリジンの検証用で、業務 API の許可リストとは別です。
|
|
945
|
+
|
|
946
|
+
#### 別オリジンの Cookie / session
|
|
947
|
+
|
|
948
|
+
下記の `GET /api/session` はサイトで実装する例示 API で、SDK が提供する API ではありません。ログイン session の `{ "csrfToken": "現在のtoken" }` を返し、`Cache-Control: no-store` と CORS を設定します。既存サイトの API パスに適合してください。
|
|
949
|
+
|
|
950
|
+
```ts
|
|
951
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
952
|
+
|
|
953
|
+
/** /api/session is an example endpoint implemented by your application backend. */
|
|
954
|
+
export async function createSessionBackendClient(backendOrigin: string) {
|
|
955
|
+
const response = await fetch(`${backendOrigin}/api/session`, {
|
|
956
|
+
credentials: 'include', cache: 'no-store', redirect: 'error',
|
|
957
|
+
});
|
|
958
|
+
if (!response.ok) throw new Error('Session initialization failed');
|
|
959
|
+
const session: unknown = await response.json();
|
|
960
|
+
if (!session || typeof session !== 'object' || !('csrfToken' in session) ||
|
|
961
|
+
typeof session.csrfToken !== 'string' || !session.csrfToken) {
|
|
962
|
+
throw new Error('The session endpoint must return { csrfToken: string }');
|
|
963
|
+
}
|
|
964
|
+
let currentCsrfToken = session.csrfToken;
|
|
965
|
+
return {
|
|
966
|
+
client: createAuthLinkClient({
|
|
967
|
+
baseUrl: `${backendOrigin}/api/authlink`,
|
|
968
|
+
allowedBackendOrigins: [backendOrigin],
|
|
969
|
+
credentials: 'include',
|
|
970
|
+
csrfToken: () => currentCsrfToken,
|
|
971
|
+
}),
|
|
972
|
+
// Call after your application's session/token refresh; recreate on account changes.
|
|
973
|
+
updateCsrfToken(value: string) { currentCsrfToken = value; },
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
ログイン後に `await createSessionBackendClient('https://api.example.com')` を呼び、返された `client` を既存コンポーネントに渡します。CSRF token 更新時は `updateCsrfToken(newToken)` を呼びます。ログアウトやアカウント切替時は古いダイアログを閉じ、状態を再初期化します。別オリジン Cookie には `credentials: 'include'` が必要ですが、SameSite、Secure、domain/path、ブラウザーの第三者 Cookie 制限も適用されます。別オリジンと cross-site は同じではありません。cross-site Cookie には通常 `SameSite=None; Secure` が必要で、ブラウザーによっては引き続き拒否されます。
|
|
979
|
+
|
|
980
|
+
#### Bearer token / JWT
|
|
981
|
+
|
|
982
|
+
サイトの既存ログイン状態を渡してください。ログイン・更新処理で `auth.accessToken` を更新すると、SDK は作成・ポーリングのたびに最新値を読み取ります。
|
|
983
|
+
|
|
984
|
+
```ts
|
|
985
|
+
import { createAuthLinkClient } from '@auth-link/web';
|
|
986
|
+
|
|
987
|
+
/** auth is your website's existing login state, updated by its login/refresh flow. */
|
|
988
|
+
export function createBearerBackendClient(backendOrigin: string, auth: { accessToken: string }) {
|
|
989
|
+
return createAuthLinkClient({
|
|
990
|
+
baseUrl: `${backendOrigin}/api/authlink`,
|
|
991
|
+
allowedBackendOrigins: [backendOrigin],
|
|
992
|
+
credentials: 'omit',
|
|
993
|
+
accessToken: () => auth.accessToken,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
これはサイト利用者のログイン token であり、AuthLink のテナント API キーではありません。SDK が `Authorization: Bearer ...` を付加します。`accessToken` 指定時は既定で `credentials: 'omit'` となり、Cookie にフォールバックせず Bearer のみで認証するバックエンドでは CSRF を省略できます。`include` / `same-origin` を明示して Cookie を併用する場合は CSRF の提供・検証も必要です。
|
|
999
|
+
|
|
1000
|
+
#### バックエンドの CORS
|
|
1001
|
+
|
|
1002
|
+
`OPTIONS` プリフライトを処理し、実リクエスト・エラー応答・session 初期化 API にも必要な CORS ヘッダーを返します。フロントエンドが `https://www.example.com` の例:
|
|
1003
|
+
|
|
1004
|
+
```http
|
|
1005
|
+
Access-Control-Allow-Origin: https://www.example.com
|
|
1006
|
+
Access-Control-Allow-Credentials: true
|
|
1007
|
+
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
|
1008
|
+
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, Idempotency-Key, Authorization
|
|
1009
|
+
Vary: Origin
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
Cookie を使う場合 `Access-Control-Allow-Origin: *` は使用できません。バックエンドの許可リストと一致する Origin のみ返し、任意の値を反射しないでください。プリフライトには通常ログイン認証情報がないため、まず Origin・メソッド・ヘッダーを検証し、実リクエストで本人確認・CSRF(Cookie モード)・所有者を検証します。独自ヘッダーや CSRF ヘッダー名は `Access-Control-Allow-Headers` に追加します。SDK はサーバーの CORS 設定を代行できず、`no-cors` で結果を読めません。[MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) を参照してください。
|
|
1013
|
+
|
|
1014
|
+
#### クライアント設定
|
|
608
1015
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1016
|
+
| 設定 | 説明 |
|
|
1017
|
+
| --- | --- |
|
|
1018
|
+
| `baseUrl` | 業務 API の接頭辞、既定 `/api/authlink`;別オリジン時は完全 URL |
|
|
1019
|
+
| `allowedBackendOrigins` | 追加許可する正確な業務 API オリジンの配列 |
|
|
1020
|
+
| `credentials` | 既定 `same-origin`、`accessToken` 指定時 `omit`;別オリジン Cookie は `include` |
|
|
1021
|
+
| `csrfToken` | リクエストごとの同期/非同期取得関数;Cookie モードは必須 |
|
|
1022
|
+
| `csrfHeaderName` | 既定 `X-CSRF-Token`、フレームワークに合わせて変更可能 |
|
|
1023
|
+
| `accessToken` | サイトのログイン token 取得関数;`Bearer ` 接頭辞は付けない |
|
|
1024
|
+
| `headers` | 追加 `HeadersInit`、またはそれを返す同期/非同期関数 |
|
|
1025
|
+
| `authLinkOrigin` | QR 内の信頼するサービスオリジン。業務 API オリジンとは別 |
|
|
1026
|
+
| `fetch` | 任意の fetch 実装;接続先検証は維持 |
|
|
1027
|
+
|
|
1028
|
+
追加ヘッダーは SDK 管理の `Accept`、`Content-Type`、`Authorization`、`Idempotency-Key`、設定済み CSRF ヘッダーを上書きできません(`RESERVED_HEADER`)。`accessToken` と `csrfHeaderName` を使用してください。設定型 `ClientOptions` はルートエントリーからインポートできます。
|
|
1029
|
+
|
|
1030
|
+
未許可オリジンは `BACKEND_ORIGIN_NOT_ALLOWED`(旧 `CMS_ORIGIN_REQUIRED` に代わる)、不正な許可設定/URL は `INVALID_BACKEND_ORIGIN` / `INVALID_BASE_URL`、token 不足は `ACCESS_TOKEN_REQUIRED` / `CSRF_REQUIRED` となります。非同期の認証情報取得にも取消しと 10 秒タイムアウトが適用されます。CORS の詳細な失敗理由は開発者ツールで確認してください。SDK では通常 `NETWORK_ERROR` になります。
|
|
1031
|
+
|
|
1032
|
+
### 関数でダイアログを開く(React / Vue / JavaScript)
|
|
1033
|
+
|
|
1034
|
+
```ts
|
|
1035
|
+
import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
|
|
1036
|
+
import { client } from './authlink-client';
|
|
1037
|
+
import '@auth-link/web/styles.css';
|
|
1038
|
+
|
|
1039
|
+
const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
|
|
1040
|
+
const modal = openAuthLinkConfirm({
|
|
1041
|
+
ticket, client, locale: 'ja',
|
|
1042
|
+
onConfirmed: result => console.log(result.requestId),
|
|
1043
|
+
onClose: () => console.log('Closed'),
|
|
1044
|
+
});
|
|
1045
|
+
// 登録: openAuthLinkBind({ ticket: bindingTicket, client })
|
|
1046
|
+
// modal.close(); modal.setLocale('en');
|
|
613
1047
|
```
|
|
614
1048
|
|
|
615
|
-
|
|
1049
|
+
React / Vue コンポーネントもマウント時にダイアログを開きます。React の onClose、Vue の @close で ticket を消去してアンマウントしてください。SSR はダイアログも通信も生成しません。スマートフォンの画面幅、スクロール、閉じるボタン、Escape に対応し、ネイティブ dialog.showModal() が必要です。body 直下に表示するため、テーマ変数は :root または .authlink-dialog に設定します。
|
|
1050
|
+
|
|
1051
|
+
閉じてもサーバーのリクエストを拒否・取消しません。元の有効期限は維持されます。承認・拒否時は自動で閉じ、onConfirmed または onRejected を通知します。ユーザー操作の onClose は通知しません。関数で開いた場合はページ破棄時に modal.destroy() を呼んで通信と UI を解除してください。destroy() は onClose を通知しません。modal.setLocale() はリクエストを作り直さずに言語を切り替えます。
|
|
1052
|
+
|
|
616
1053
|
|
|
617
1054
|
### React
|
|
618
1055
|
|
|
1056
|
+
`showBound(requestId)` はサイトの表示更新用関数です。メッセージを更新し、ticket を消去してダイアログをアンマウントします。下記には拒否・期限切れ・閉じる処理も含まれます。`<AuthLinkPanel mode="bind" />` または `<AuthLinkPanel mode="confirm" />` で使います。
|
|
1057
|
+
|
|
619
1058
|
```tsx
|
|
1059
|
+
"use client";
|
|
1060
|
+
import { useRef, useState } from 'react';
|
|
1061
|
+
import { requestKey } from '@auth-link/web';
|
|
1062
|
+
import type { Mode, Ticket } from '@auth-link/web';
|
|
620
1063
|
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
|
|
621
|
-
import type { Ticket, Confirmation } from '@auth-link/web';
|
|
622
1064
|
import { client } from './authlink-client';
|
|
623
1065
|
|
|
624
|
-
export function
|
|
625
|
-
const
|
|
626
|
-
|
|
1066
|
+
export function AuthLinkPanel({ mode }: { mode: Mode }) {
|
|
1067
|
+
const [ticket, setTicket] = useState<Ticket | null>(null);
|
|
1068
|
+
const [busy, setBusy] = useState(false);
|
|
1069
|
+
const [message, setMessage] = useState('');
|
|
1070
|
+
const key = useRef<string | null>(null);
|
|
1071
|
+
const creating = useRef(false);
|
|
1072
|
+
function finish(message: string) {
|
|
1073
|
+
setTicket(null);
|
|
1074
|
+
setMessage(message);
|
|
1075
|
+
}
|
|
1076
|
+
function showBound(requestId: string) {
|
|
1077
|
+
// UI feedback only. The backend has already verified and saved the binding.
|
|
1078
|
+
finish(`Phone bound (request ${requestId})`);
|
|
1079
|
+
}
|
|
1080
|
+
function showConfirmed(requestId: string) {
|
|
1081
|
+
finish(`Confirmed (request ${requestId})`);
|
|
1082
|
+
}
|
|
1083
|
+
const start = async () => {
|
|
1084
|
+
if (creating.current || ticket) return;
|
|
1085
|
+
creating.current = true;
|
|
1086
|
+
setBusy(true);
|
|
1087
|
+
setMessage('');
|
|
1088
|
+
try {
|
|
1089
|
+
key.current ??= requestKey();
|
|
1090
|
+
setTicket(await client.create(mode, { idempotencyKey: key.current }));
|
|
1091
|
+
key.current = null;
|
|
1092
|
+
} catch { setMessage('Request failed; retry the same creation.'); }
|
|
1093
|
+
finally { creating.current = false; setBusy(false); }
|
|
627
1094
|
};
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1095
|
+
const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
|
|
1096
|
+
return <>
|
|
1097
|
+
{!ticket && <button type="button" disabled={busy} onClick={() => void start()}>Start verification</button>}
|
|
1098
|
+
{ticket && <Component ticket={ticket} client={client}
|
|
1099
|
+
onConfirmed={result => result.purpose === 'bind' ? showBound(result.requestId) : showConfirmed(result.requestId)}
|
|
1100
|
+
onClose={() => finish('Closed')}
|
|
1101
|
+
onRejected={() => finish('Request rejected on the phone')}
|
|
1102
|
+
onExpired={() => finish('Request expired; start again')}
|
|
1103
|
+
onError={error => setMessage(error.message)} />}
|
|
1104
|
+
<p role="status">{message}</p>
|
|
1105
|
+
</>;
|
|
632
1106
|
}
|
|
633
1107
|
```
|
|
634
1108
|
|
|
1109
|
+
バックエンドは結果を検証・消費・保存してから `confirmed` を返します。`showBound` は権限付与や紐付け保存を行いません。通知、サイトのアカウント状態 API の再取得、管理ページへの移動などに置き換えられます。ページ再読み込み後はサーバーから状態を取得し、コールバックや localStorage を認可に使わないでください。作成失敗時には同じ冪等キーを再利用します。
|
|
1110
|
+
|
|
1111
|
+
|
|
635
1112
|
`locale` を省略すると簡体字中国語です。日本語は `locale="ja"`、簡体字中国語は `locale="zh-CN"` を指定します。client と ticket はレンダー間で安定したオブジェクトにしてください。React エントリーは `"use client"` を保持し、SSR ではリクエストを作成しません。
|
|
636
1113
|
|
|
637
1114
|
### Vue
|
|
@@ -711,7 +1188,7 @@ React / Vue では表示中にも変更でき、リクエストやポーリン
|
|
|
711
1188
|
|
|
712
1189
|
### バックエンド API
|
|
713
1190
|
|
|
714
|
-
既定の接頭辞は `/api/authlink` です。両 API
|
|
1191
|
+
既定の接頭辞は `/api/authlink` です。両 API で Cookie + 設定した CSRF ヘッダー、または Bearer token による本人確認とリクエスト所有者を検証してください。作成時は `Idempotency-Key` も送信されます。サイトのユーザー ID は検証済みセッション/token から決定します。
|
|
715
1192
|
|
|
716
1193
|
```ts
|
|
717
1194
|
import type { Ticket, Result } from '@auth-link/web';
|
|
@@ -762,7 +1239,9 @@ import type { Ticket, Result } from '@auth-link/web';
|
|
|
762
1239
|
|
|
763
1240
|
### スタイルとエントリー
|
|
764
1241
|
|
|
765
|
-
`@auth-link/web/styles.css`
|
|
1242
|
+
`@auth-link/web/styles.css` を明示的に読み込みます。ダイアログは `document.body` に追加されるため、`:root` または `.authlink-dialog` で `--authlink-foreground`、`--authlink-background`、`--authlink-accent`、`--authlink-muted` を上書きします。クラス名の接頭辞は `authlink-` です。
|
|
1243
|
+
|
|
1244
|
+
React / Vue はダイアログの `className` に対応します。Vue は文字列・配列・オブジェクト形式の `class` も受け取り、`className` と結合します。再作成ボタンは親が渡した最新の `onRefresh` を呼び出します。
|
|
766
1245
|
|
|
767
1246
|
| エントリー | 内容 |
|
|
768
1247
|
| --- | --- |
|
|
@@ -775,7 +1254,7 @@ import type { Ticket, Result } from '@auth-link/web';
|
|
|
775
1254
|
|
|
776
1255
|
- 数字はアプリとの目視照合用です。数字だけで認証や承認はできません。
|
|
777
1256
|
- ブラウザーに生成 seed、端末秘密鍵、テナント秘密情報を渡しません。
|
|
778
|
-
- リクエストは最大 120 秒。コード更新で期限は延長されません。サービスとアプリはモバイルプロトコル
|
|
1257
|
+
- リクエストは最大 120 秒。コード更新で期限は延長されません。サービスとアプリはモバイルプロトコル 7 に対応する必要があります。
|
|
779
1258
|
- サーバー時刻と単調増加時計で残り時間を計算し、通信が止まっても期限切れの数字は隠します。更新境界で短い待ち時間が生じる場合があります。
|
|
780
1259
|
- アンマウントや ticket 変更で監視を中止します。インスタンス内のイベント重複防止とは別に、バックエンドで永続的な冪等処理が必要です。
|
|
781
1260
|
- フロントエンドのイベントは UI 更新用です。保護 API はバックエンドの信頼できる記録で判断します。
|
|
@@ -783,4 +1262,4 @@ import type { Ticket, Result } from '@auth-link/web';
|
|
|
783
1262
|
|
|
784
1263
|
### ライセンス
|
|
785
1264
|
|
|
786
|
-
|
|
1265
|
+
MIT。ライセンス全文は `LICENSE`、同梱ライブラリの帰属表示は `THIRD_PARTY_NOTICES.md` を参照してください。
|