# ark-oauth-client 中文文档

规范地址：https://npmcn.com/package/ark-oauth-client/

版本：2\.1\.7
npm 元数据更新时间：2026-11-03T03:15:57.464Z
中文译文更新时间：2026-11-03T04:13:08.714Z
软件包许可：MIT

本文由上游 README 机器翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/ark-oauth-client
原文对照：https://npmcn.com/package/ark-oauth-client/?lang=original
项目主页：https://github.com/ir-dev/ark-oauth-oidc/tree/main/ark_oauth_client_nodejs#readme
源码仓库：https://github.com/ir-dev/ark-oauth-oidc

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：199 kB，22 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=20\.0\.0
- 运行依赖：0 个
- 首次发布：2026-11-01T18:38:20.499Z
- 最新版本发布：2026-11-03T03:15:57.114Z
- 已发布版本数：8（近 12 个月 8 个）

## 中文简介

用于Node.js应用程序的OAuth 2.1 / OpenID Connect客户端，为Ark.oAuth.Oidc构建。授权码+ PKCE与状态，nonce， iss和at\_hash检查，JWKS旋转，静默刷新，客户端凭证，设备授予，PAR，自省，撤销

## 安装

```sh
npm install ark-oauth-client
```

也可以使用：`pnpm add ark-oauth-client`、`yarn add ark-oauth-client`、`bun add ark-oauth-client`

## README

# ark-oauth-client

OAuth 2.1 / OpenID连接客户端 **node . js** 应用程序与 [ARK身份服务器](https://npmcn.com/package/README.md).

只有一个URL需要配置——发行者， `{BaseUrl}/{TenantId}`． 端点、签名密钥和 功能是从提供者的发现文档中读取的，因此应用程序不必这样做 当键旋转或端点移动时重新部署。

没有运行时依赖项。一切都建立在 `node:crypto` 全球的 `fetch`.

```bash
npm install ark-oauth-client
```

* * *

## 目录

-   [快速启动](https://npmcn.com/package/ark-oauth-client/#readme-quick-start)
-   [它实现了什么](https://npmcn.com/package/ark-oauth-client/#readme-what-it-implements)
-   [配置](https://npmcn.com/package/ark-oauth-client/#readme-configuration)
-   [中间件](https://npmcn.com/package/ark-oauth-client/#readme-the-middleware)
-   [帐户切换共享浏览器](https://npmcn.com/package/ark-oauth-client/#readme-account-switching-on-a-shared-browser)
-   [加载appsettings.json](https://npmcn.com/package/ark-oauth-client/#readme-loading-appsettingsjson)
-   [保护API](https://npmcn.com/package/ark-oauth-client/#readme-protecting-an-api)
-   [会话](https://npmcn.com/package/ark-oauth-client/#readme-sessions)
-   [直接使用客户端](https://npmcn.com/package/ark-oauth-client/#readme-using-the-client-directly)
-   [检查设置](https://npmcn.com/package/ark-oauth-client/#readme-checking-the-setup)
-   [错误](https://npmcn.com/package/ark-oauth-client/#readme-errors)
-   [注册客户端](https://npmcn.com/package/ark-oauth-client/#readme-registering-the-client)
-   [安全笔记](https://npmcn.com/package/ark-oauth-client/#readme-security-notes)
-   [故障排除](https://npmcn.com/package/ark-oauth-client/#readme-troubleshooting)
-   [开发](https://npmcn.com/package/ark-oauth-client/#readme-development)

* * *

## 快速启动

```js
import express from 'express';
import { arkExpress } from 'ark-oauth-client';

const app = express();

const auth = arkExpress({
  authority: 'https://idp.example.com/my_idp',   // {BaseUrl}/{TenantId}
  clientId: 'my-app',
  clientSecret: process.env.ARK_CLIENT_SECRET,   // omit for public clients
  redirectUri: 'https://app.example.com/signin-oidc',
  postLogoutRedirectUri: 'https://app.example.com/',
  secret: process.env.ARK_SESSION_SECRET         // signs the session cookie
});

app.use(auth);                                    // serves /login, /signin-oidc and /logout

app.get('/', (req, res) => {
  res.send(req.ark.isAuthenticated ? `Hello ${req.ark.user.name}` : '<a href="/login">Sign in</a>');
});

app.get('/billing', auth.requireClaims('billing.admin'), (req, res) => res.send('billing'));

app.listen(3000);
```

这就是整个积分。授权码+ PKCE， `state`， `nonce`， `iss`， `at_hash`， JWKS旋转，静默令牌刷新和签出在下面处理。

此流和所有其他流的可运行版本都在 [`examples/`](https://npmcn.com/package/ark-oauth-client/examples/) - - - - - - `examples/web-app.js` 根本不需要依赖项。

* * *

## 它实现了什么

| 规范 | 客户做什么 |
| --- | --- |
| OpenID连接发现1.0 | 读取和缓存 `/.well-known/openid-configuration`;验证 `issuer` 与配置的权限匹配 |
| RFC 8414元数据 | 又回到 `oauth-authorization-server` 其他提供者的拼写 |
| 授权码（RFC 6749） | 唯一的互动流程；隐式和混合在OAuth 2.1中消失了，不再提供 |
| Pkce （rfc 7636） | 总是发送, `S256` 只有，验证者来自 `crypto.randomBytes` |
| OpenID连接核心1.0 | ID令牌验证：签名， `iss`， `aud`， `azp`， `exp`， `nbf`， `iat`， `nonce`， `at_hash`， `c_hash`， `auth_time`/`max_age` |
| RFC 7517 JWKS | 缓存，并在不可见的对象上重新获取 `kid`，因此两相键旋转不需要重新启动 |
| RFC 9068访问令牌 | `typ: at+jwt` 强制执行，因此ID令牌不能在其位置上呈现给API |
| 刷新（RFC 6749§6） | 旋转感知，范围缩小，每次每个会话刷新一次 |
| 客户端凭证 | 使用每个客户端和作用域缓存 |
| 设备授权（RFC 8628） | 轮询处理 `authorization_pending` 和 `slow_down` |
| 标准（rfc 9126） | `usePar: true`，或在租户需要时自动执行 |
| 自省（RFC 7662） | 使用客户端身份验证 |
| 撤销（RFC 7009） | 签到的时候到了，换洗代币家族真的结束了 |
| RP-Initiated Logout 1.0 | `id_token_hint`， `post_logout_redirect_uri`， `state` |
| RFC 9207 `iss` | 检查每个授权响应 |
| 动态注册（RFC 7591/7592） | 注册，读取和删除 |
| 客户端身份验证 | `client_secret_basic`， `client_secret_post`， `private_key_jwt`， `none` |

Ark的每用户每客户端授权声明(`ark_claims` 在访问令牌中)显示为 `req.ark.claims`，是什么？ `requireClaims()` 检查。

* * *

## 配置

只有 `authority` 和 `clientId` 是必需的; `redirectUri` 交互式登录也是如此。

| 选项 | 默认的 | 意义 |
| --- | --- | --- |
| `authority` | \- - - - - - | 发行者URL： `{BaseUrl}/{TenantId}` |
| `authServerUrl` + `tenantId` | \- - - - - - | 替代 `authority`;为你而加入，为你而匹配。. NET客户端的配置形状 |
| `clientId` | \- - - - - - | 向租户注册的客户端id |
| `clientSecret` | `null` | 仅限保密客户。忽略SPA、本机和CLI客户端 |
| `tokenEndpointAuthMethod` | 派生的 | `client_secret_basic` 当一个秘密被设定， `private_key_jwt` 当键为时，否则为 `none` |
| `privateKeyJwt` | `null` | `{ privateKey, kid, alg }` - PEM、JWK或 `KeyObject` |
| `redirectUri` | \- - - - - - | 绝对的，没有碎片， `https` 除非回送。服务器匹配它 **完全** |
| `postLogoutRedirectUri` | \- - - - - - | 还必须注册 |
| `scopes` | `openid profile email offline_access` | `offline_access` 是什么获得刷新令牌 |
| `audience` | `null` | 预期 `aud` 在验证API中的访问令牌时 |
| `usePar` | `false` | 通过反向通道推送授权参数 |
| `responseMode` | `query` | `query`， `fragment` 或者 `form_post` |
| `prompt`， `acrValues`， `extraAuthorizationParams` | \- - - - - - | 传递给 `/authorize` |
| `clockToleranceSeconds` | `60` | 空间上 `exp`， `nbf` 和 `iat` |
| `requireHttps` | `true` | 只 `false` 促进地方发展 |
| `requireTokenHashes` | `true` | 需要 `at_hash`/`c_hash`;方舟总会送他们去 |
| `timeoutMs` | `10000` | 每个请求超时 |
| `metadataTtlMs`， `jwksTtlMs` | `300000` | 缓存生存期，与服务器匹配 `max-age=300` |
| `jwksMinRefreshIntervalMs` | `10000` | 多么快就看不见 `kid` 可能触发另一个JWKS取回 |
| `fetch` | 全球 | 注入您自己的代码用于跟踪、代理或测试 |

这里的错误是在构建时发现的，而不是在用户登录时发现的 `redirectUri` 用一个片段， 纯http授权， `private_key_jwt` 没有钥匙，a `clientSecret` 与 `tokenEndpointAuthMethod: 'none'`.

* * *

## 中间件

`arkExpress(options)` 返回一个普通的connect样式的中间件。它与Express，和 `@fastify/middie`, `koa-connect`，并与一个裸露 `http.createServer` -包取决于 都没有。

它占据了这些路径，并让其他所有东西通过：

| 路径 | 目的 |
| --- | --- |
| `loginPath` （`/login`） | 开始登录。 `?returnTo=/somewhere` 返回到那个页面——只有同源路径 |
| `callbackPath` (路径 `redirectUri`,否则 `/signin-oidc`） | 完成它。 **永远不要在这里添加自己的路线** |
| `logoutPath` （`/logout`） | 撤销刷新令牌，销毁会话，清除cookie，然后重定向到提供者的 `end_session_endpoint` |
| `/ark/no-access`， `/ark/switch-user`， `/ark/sign-out` | 拒绝访问的页面和它的两个post -参见 [账户切换](https://npmcn.com/package/ark-oauth-client/#readme-account-switching-on-a-shared-browser) |

### `req.ark`

| 成员 |  |
| --- | --- |
| `isAuthenticated` |  |
| `user` | ID令牌声明，加上UserInfo时 `fetchUserInfo: true` |
| `sub` |  |
| `claims` | 方舟授权声明- **授权什么** |
| `scopes` | 授予范围 |
| `tokens` | 这个 `TokenSet` |
| `await accessToken()` | 访问令牌，如果即将到期，首先更新 |
| `await authorize(headers?)` | 返回带有 `Authorization: Bearer …` 设置，用于下游呼叫 |
| `hasClaim(…)`， `hasScope(…)` |  |
| `await login({ returnTo, prompt, loginHint })`， `await logout({ returnTo })` | 从你自己的路线驱动流 |
| `await switchUser({ returnUrl })` | 放弃这个环节，用 `prompt=login` |
| `await signOutEverywhere()`， `await signOutLocally()` | 提供程序会话，或者仅此应用程序 |
| `deniedAccount()` | 上次拒绝是关于谁的，为了你自己被拒绝访问的页面 |

读取访问令牌 `accessToken()` 在每次调用时，而不是保留它：它被更新 在您下面，它被更新时使用的刷新令牌将在此发生的那一刻退役。

### 警卫

```js
app.get('/account',  auth.requireAuth(),                     handler);
app.get('/billing',  auth.requireClaims('billing.admin'),    handler);
app.get('/reports',  auth.requireScopes('reports.read'),     handler);
app.get('/admin',    auth.requireAuth({ claims: ['tenant.root'], scopes: ['admin'] }), handler);
```

未经过身份验证的 **浏览器** 请求被重定向到登录页面 `returnTo`． 任何东西 其他- - - - - - `fetch`XHR，一个移动客户端 `401` 使用RFC 6750挑战，因为重定向 对登录页面的后台请求产生CORS错误，而不是调用者可以执行的任何操作 上。丢失索赔的登录用户将获得 `403`，而不是重定向循环——对于浏览器来说 带有出路的拒绝访问页面；其他的，请参考RFC 6750 `insufficient_scope` 的身体。

### 中间件的选择

| 选项 | 默认的 |  |
| --- | --- | --- |
| `secret` | \- - - - - - | **要求**， 16+字符。对会话cookie进行签名。每个实例的值相同 |
| `client` | \- - - - - - | 重用现有的 `ArkOAuthClient` 而不是在这里配置一个 |
| `store` | `MemorySessionStore` | 看到 [会话](https://npmcn.com/package/ark-oauth-client/#readme-sessions) |
| `cookieName` | `ark_session` |  |
| `cookie` | `{ httpOnly: true, secure: true, sameSite: 'Lax', path: '/' }` | `secure: false` 仅适用于本地HTTP |
| `sessionTtlSeconds` | `28800` | 匹配服务器的默认会话生存期 |
| `refreshLeewaySeconds` | `120` | 在访问令牌到期之前更新它。. NET和Python窗口 |
| `fetchUserInfo` | `false` | 也叫 `/userinfo` 并将其合并到 `req.ark.user` |
| `defaultReturnTo` | `/` |  |
| `errorPath` | `null` | 签到失败的地方是什么 `?auth_error=…`． 否则就是普通的400 |
| `onError` | `null` | `(error, req, res, next)` -完全控制而不是 `errorPath` |
| `trustProxy` | `true` | 荣誉 `X-Forwarded-Proto` + `X-Forwarded-Host` （两者都有，或者两者都没有）。当应用程序前面没有设置时，关闭它 |
| `accountSwitch` | 所有从 | 看到 [下面](https://npmcn.com/package/ark-oauth-client/#readme-account-switching-on-a-shared-browser) |
| `events` | `{}` | `onEvaluateAccess`， `onAccessDenied` |

`responseMode: 'form_post'` 是一个跨站点POST，浏览器发送时没有 `SameSite=Lax` 饼干;中间件拒绝从该组合开始，而不是每次登录都失败。使用 `cookie: { sameSite: 'None', secure: true }` 使用它，或者保持默认值 `query`.

* * *

## 帐户切换共享浏览器

单点登录是浏览器范围的。一旦一个人在任何地方登录，提供者的会话 响应该浏览器中所有其他客户端的授权请求：下一个人打开它 应用程序，以静默方式登录 **作为第一人称**她被告知他们无权进入，也确实没有 出路，因为登录链接返回到相同的会话并给出相同的答案。

解决方案在所有三个方舟客户端都是一样的， **默认关闭**：

```js
const auth = arkExpress({
  authority, clientId, secret,
  accountSwitch: {
    requireArkClaims: true,          // refuse, at the callback, an account with no ark_claims here
    appDisplayName: 'Billing Portal',
    supportEmail: 'help@example.com'
  },
  events: {
    // Optional: your own entitlement rule, replacing the configured one.
    onEvaluateAccess: ({ arkClaims, allowedByConfiguration }) => allowedByConfiguration || arkClaims.includes('tenant.root'),
    // Optional: log it, or draw your own page (respond, or set denied.handled = true).
    onAccessDenied: (denied, req, res) => console.warn('access denied', denied.email, denied.reason)
  }
});
```

-   **`requireArkClaims`** 将授权检查移动到回调。提供者颁发的帐户 否 `ark_claims` 因为被拒绝了 _之前_ 会话是写入的——浏览器永远不会保持会话 对于不能使用该应用程序的人—并且它的刷新令牌被撤销。 `requiredClaims` 将其缩小到特定的值。 **集 `requiredClaims` 在实践中:** 方舟服务器发出 整个访问映射列表为 `ark_claims`、身分名称(`sub`， `email`)包括在内，所以a 没有授权声明的映射仍然满足“any” `ark_claims`＂．
-   **`/ark/no-access`** 命名已登录的帐户并提供 **以不同的用户登录**， 这对 `prompt=login` 因此，提供者绘制其登录表单而不是回答 从本届会议开始。它是一个独立的页面-没有脚本，严格的CSP， `ark-oauth-client <version>` 在页脚。一条有守卫的路线，一个登录的浏览器在这里也没有土地所有权。
-   **`/ark/switch-user`** 和 **`/ark/sign-out`** 只接受同源 `POST`． 返回URL为 保持同源路径。 `endProviderSessionOnSwitch: true` 使切换一个完整的提供商 登记，为一个亭子。

| `accountSwitch` 选项 | 默认的 |  |
| --- | --- | --- |
| `enabled` | `true` | 为端点提供服务，并向页面发送浏览器的403 |
| `requireArkClaims` | `false` | 使案例可恢复的检查 |
| `requiredClaims` | `[]` | 至少其中一个（不区分大小写）；空表示任何 |
| `accessDeniedPath`， `switchUserPath`， `signOutPath` | `/ark/…` | 仅限本地路径 |
| `serveDefaultPage` | `true` | 当 `accessDeniedPath` 是你自己的路线；读 `req.ark.deniedAccount()` 有 |
| `appDisplayName` | 客户机id |  |
| `showSignedInAccount`， `allowFullSignOut` | `true` |  |
| `endProviderSessionOnSwitch` | `false` |  |
| `prompt` | `login` |  |
| `homePath` | `/` | 用于丢失或非站点返回URL的回退 |
| `supportUrl`， `supportEmail` | \- - - - - - | 在页面上显示为“需要访问？ |

页面上的帐户在五分钟内从回呼开始， `HttpOnly` 饼干密封 AES-256-GCM下的密钥来源于 `secret` - Python客户端的格式，字节对字节。

* * *

## 加载appsettings.json

一个Ark应用程序通常已经有一个 `ark_oauth_client` 部分。 `ArkAuthConfig.fromMapping` 读取它不变- c# (`ClientId`)，骆驼或蛇的拼写，整个文件或只是部分：

```js
import { readFileSync } from 'node:fs';
import { arkExpress, ArkAuthConfig } from 'ark-oauth-client';

const settings = JSON.parse(readFileSync('appsettings.json', 'utf8'));
app.use(arkExpress({ ...ArkAuthConfig.fromMapping(settings), secret: process.env.ARK_SESSION_SECRET }));
```

`Authority` (或 `AuthServerUrl` + `TenantId`)， `ClientId`， `ClientSecret`， `Scopes`， `CallbackPath`， `LoginPath`， `LogoutPath`， `AuthErrorPath`， `CookieName`， `RoleClaimType`， `RequireHttpsMetadata`， `ExpireMins`， `PostLogoutRedirectUri` 和 `AccountSwitch` 是阅读。v1密钥(`RsaPublic`， `Issuer`， `Tenants`， `RedirectUri`)被故意忽略：重定向URI是从 `CallbackPath`，如。. NET处理程序。

* * *

## 保护API

```js
import { arkBearer } from 'ark-oauth-client';

const bearer = arkBearer({
  authority: 'https://idp.example.com/my_idp',
  clientId: 'my-api',
  audience: 'my_idp_api'          // omit to skip the audience check
});

app.use('/api', bearer);
app.get('/api/me', (req, res) => res.json({ sub: req.ark.sub, claims: req.ark.claims }));
app.get('/api/reports', bearer.require({ claims: ['reports.read'] }), handler);
```

验证是针对缓存的JWKS进行的本地验证，因此请求不需要往返于标识 服务器。 `optional: true` 允许匿名请求通过 `req.ark.isAuthenticated === false`.

* * *

## 会话

令牌永远不会到达浏览器。cookie携带一个不透明的会话id和一个HMAC； 访问令牌、刷新令牌和声明留在服务器端的存储中。

`MemorySessionStore` 是默认值，适用于一个进程。在负载平衡器后面，或者跨 重新启动，用这些方法提供一个共享存储-任何对象：

```js
const redisStore = {
  async get(id)                { const v = await redis.get(`ark:${id}`); return v ? JSON.parse(v) : null; },
  async set(id, data, ttl)     { await redis.set(`ark:${id}`, JSON.stringify(data), { EX: ttl }); },
  async destroy(id)            { await redis.del(`ark:${id}`); },
  async touch(id, ttl)         { await redis.expire(`ark:${id}`, ttl); }
};

const auth = arkExpress({ /* … */, store: redisStore });
```

有一件事你要知道。这个服务器 **旋转刷新令牌**，并提出了一个退休的 被视为全家失窃。对进程内的并发刷新进行序列化 你;而在进程之间则不是这样，因此共享存储还应该在进程周围携带一个短锁 如果您的流量可以在同一实例上对同一会话发出两个请求，则刷新 即时。

* * *

## 直接使用客户端

对于cli、worker、tify/Koa/Hapi或任何中间件没有覆盖的流。

```js
import { ArkOAuthClient } from 'ark-oauth-client';

const client = new ArkOAuthClient({ authority, clientId, clientSecret, redirectUri });
```

### 手动授权码

```js
// 1. starting a sign-in
const tx = await client.createAuthorizationUrl({ returnTo: '/dashboard' });
// store tx (state, nonce, codeVerifier) server-side, keyed to this browser
redirect(tx.url);

// 2. on the callback
const tokens = await client.handleCallback(req.query, tx);
tokens.accessToken; tokens.idToken; tokens.refreshToken;
tokens.claims;        // validated ID token claims
tokens.arkClaims();   // Ark authorization claims
```

`state`， `nonce` 和 `codeVerifier` 是整个流程的安全性。把它们放在 用户无法读取或编辑-隐藏的表单字段或普通cookie将攻击者准确地传递给攻击者 进行检查的三个值。

### 刷新

```js
const next = await client.refresh(tokens.refreshToken);   // store what comes back: the old one is now dead
```

### 客户端凭证

```js
const token = await client.clientCredentials({ scopes: ['reports.read'] });   // cached until near expiry
```

### 设备格兰特

```js
const authorization = await client.deviceAuthorization({ scopes: ['openid', 'profile'] });
console.log(`Go to ${authorization.verification_uri} and enter ${authorization.user_code}`);
const tokens = await client.pollDeviceToken(authorization);
```

### 其余的

```js
await client.userInfo(accessToken);
await client.introspect(token, { tokenTypeHint: 'refresh_token' });
await client.revoke(refreshToken);
await client.endSessionUrl({ idTokenHint: tokens.idToken, state });
await client.verifyAccessToken(token, { scopes: ['reports.read'], arkClaims: ['billing.admin'] });
await client.verifyIdToken(idToken, { nonce });
await client.pushAuthorizationRequest(params);
await client.registerClient(metadata, initialAccessToken);
```

* * *

## 检查设置

```js
const report = await client.checkSetup();
if (report.problems.length) console.warn(report.problems);

// Inside the app: the redirect URI this request would really send, and who is signed in.
app.get('/setup', async (req, res) => res.json(await auth.checkSetup(req)));
```

将本地配置与提供商的实时元数据配对，并返回不一致的内容： 错误的端口、停止的提供程序、颁发者不匹配、此客户端未注册的范围等 租户不提供身份验证方法，需要PAR，但未启用PAR。把它渲染成健康值 页面，或运行 `node examples/setup-check.js` 在CI。没有它，这些症状的第一个症状就是 `invalid_request` 在用户正在浏览的页面上。

* * *

## 错误

每个错误都带有决定下一步操作所需的内容，而不是要grep的字符串。

| 类 | 意义 | 通常的反应 |
| --- | --- | --- |
| `ArkConfigError` | 这个应用程序配置错误 | 修复和重新部署；在可能的情况下扔到建筑中 |
| `ArkOAuthError` | 服务器拒绝。 `.error` 是RFC 6749代码， `.status` HTTP状态； `.endpoint` URL | 分支 `.error` |
| `ArkTokenError` | 令牌验证失败——签名、发行者、受众、到期、 `nonce`， `at_hash` | 从来没有重试。答案不可信 |
| `ArkCallbackError` | 授权响应不属于此客户端启动的请求 | 将其视为CSRF或混淆尝试 |
| `ArkNetworkError` | 呼叫未完成：拒绝，DNS，超时 | 后退重试 |

```js
import { ArkOAuthError } from 'ark-oauth-client';

try {
  await client.refresh(token);
} catch (error) {
  if (error instanceof ArkOAuthError && error.error === 'invalid_grant') signInAgain();
  else throw error;
}
```

* * *

## 注册客户端

在管理控制台中 `/{tenant}/admin`，或从生成的设置页面 `/{tenant}/oauth2/integrate/{client_id}`：

| 场 | 价值 |
| --- | --- |
| 重定向的URI | 到底是你的 `redirectUri`,如。 `https://app.example.com/signin-oidc` |
| 注销后重定向URI | 你的 `postLogoutRedirectUri` |
| 格兰特类型 | `authorization_code`， `refresh_token` |
| 作用域 | `openid`， `profile`， `email`， `offline_access` |
| 身份验证方法 | `none` 对于公共客户来说， `client_secret_basic` 为了保密 |

然后 **给每个用户访问客户端的权限**． Ark显式地将用户映射到客户端，而不使用 映射登录失败的方式看起来就像一个错误的密码。

服务器端Node应用程序不需要租户的任何内容 `CorsOrigins` -那张单子是给 浏览器客户端赎回自己的代码。

* * *

## 安全笔记

-   **令牌永远不会到达浏览器。** cookie是一个不透明的id加上一个签名；它是 `HttpOnly`， `Secure` 和 `SameSite=Lax`.
-   **在登录时替换会话id**，所以在身份验证之前植入的id永远不会是正确的 最终被认证。
-   **`state` 在常数时间内比较**, `iss` 在每个授权响应中检查。
-   **一个失踪的 `nonce` 或者 `at_hash` 失败和犯错一样艰难。** 缺席是最容易核实的 Skip是最容易攻击的。
-   **`alg: none` 被拒绝**，并且在令牌自己的标头之前确定可接受的算法 是阅读。
-   **JWKS取回是速率限制的**，和 `kid` 已经知道的缺席永远不会触发另一个 获取-否则伪造的令牌流将该客户端变成指向的请求放大器 您的身份服务器。
-   **`returnTo` 只接受同源路径**，因此登录路由不能成为打开的重定向。
-   **签出撤销刷新令牌** 在本地会议结束前；换账户也是如此 还有一次拒绝登录。
-   **刷新是共享的，它的结果到达每个等待的请求。** 旋转打开时，请求 持有已退出的刷新令牌将再次呈现它并撤销整个家族。
-   **体是有界的，形参是字符串。** 超过64 kb的回调或帐户表单是 拒绝; `state` 它只被看作是自己的财产。
-   **错误文本不会到达未转义的标头。** 伪造的令牌 `kid` 不能注入标头或 使流程崩溃 `WWW-Authenticate` 挑战。
-   **不能替换额外的授权参数** `state`， `nonce`， PKCE挑战或 重定向的URI。

* * *

## 故障排除

| 症状 | 导致 |
| --- | --- |
| `redirect_uri does not match a registered value` | 注册值的不同之处是字符方案、端口、尾斜杠。匹配是精确的，有一个环回端口的雕刻 |
| 登录失败，好像密码是错误的 | 用户没有到该客户端的访问映射 |
| 共享浏览器上的下一个用户将以前一个用户的身份登录 | SSO。打开 `accountSwitch.requireArkClaims`;拒绝访问页面提供 `prompt=login` |
| `responseMode 'form_post' needs cookie: { sameSite: 'None' … }` 在启动时 | 看到 [中间件的选择](https://npmcn.com/package/ark-oauth-client/#readme-middleware-options) |
| `this sign-in could not be matched to a request from this browser` | 登录过期（10分钟），或者使用内存存储重新启动进程 |
| `invalid_grant` 在第二次刷新时 | 给出了一个退役的刷新令牌；这个家庭被撤销了。总是存储刷新返回的内容 |
| `the provider … identifies itself as '…'` | `authority` 不是发行人。它是 `{BaseUrl}/{TenantId}`，包括租户身份证 |
| `no key with kid '…' is published` | 来自不同提供商的令牌-或多于 `jwksMinRefreshIntervalMs` 到现在还没有取回 |
| `unable to verify the first certificate` 在开发 | 提供商的开发证书不受Node信任。相信它，或设置 `NODE_TLS_REJECT_UNAUTHORIZED=0` **只在本地** |

* * *

## 开发

```bash
npm test          # 135 tests, no install required
```

这套房正对着 `test/stub-idp.js`它是Ark服务器的进程内镜像 它的电线行为：相同的路径，RS256与a `kid` 旋转两个键， `at+jwt` 访问令牌 `ark_claims`、带有 `at_hash`/`c_hash`， `iss` 关于授权 RFC 6749§5.2错误体。 它在您自己的测试中也很有用。

**要求:** Node 20或更新版本。ESM;在节点22.12+上 `require()` 这个包也可以工作。

**许可:** 麻省理工学院。

## 常见问题

### ark-oauth-client 是什么？

用于Node\.js应用程序的OAuth 2\.1 / OpenID Connect客户端，为Ark\.oAuth\.Oidc构建。授权码\+ PKCE与状态，nonce， iss和at\_hash检查，JWKS旋转，静默刷新，客户端凭证，设备授予，PAR，自省，撤销

### 如何安装 ark-oauth-client？

在项目目录运行 npm install ark-oauth-client，或使用 pnpm add / yarn add / bun add。

### ark-oauth-client 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### ark-oauth-client 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### ark-oauth-client 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=20\.0\.0。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### ark-oauth-client 可以商用吗？

ark-oauth-client 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### ark-oauth-client 还在维护吗？

最新版本 2\.1\.7 发布于 2026/11/03（今天）；至今共发布 8 个版本，其中过去 12 个月发布了 8 个；首次发布于 2026/11/02。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

