# 使用 Beeuni 登录

通过一个路由让你自己的应用基于 Beeuni 身份进行访问控制，并通过 SDK 以已登录用户身份执行操作。

Canonical page: https://beeuni.ai/docs/zh/sdk/sign-in

“使用 Beeuni 登录”会将 Beeuni 作为你运行的应用的身份提供商：
可以是仪表板、内部工具，或基于 Beeuni 构建的垂直产品。用户使用其 Beeuni 账户登录，服务器知道用户身份，而应用发起的每个 Beeuni 调用都会以该用户及其角色分配运行。整个流程都在 `@beeuni/sdk` 中完成。你的应用不会在浏览器中存储 Beeuni 令牌。

这是标准的 OAuth 2.1（授权码 + PKCE），由 `api.beeuni.ai` 上的 Beeuni API 提供。

> 如果你正在构建**由 Beeuni 托管**的 App（`*.apps.beeuni.ai`），则完全不需要这些内容。Apps gate 已经对查看者完成身份验证——使用 `beeuniAppViewerToken()` / `readAppViewer()` 读取其信息，并使用 `beeuniToken()` 获取该 App 或其使用的 App 的登录令牌。请参阅下方的[App 的登录令牌](#sign-in-tokens-for-apps)。

<a id="1-register-your-app"></a>

## 1. 注册应用

前往 **Account → Tokens → OAuth apps → Register app**，或调用 SDK：

```ts
const app = await beeuni.iam.oauthClients.create(accountId, {
  name: 'Dashboards',
  client_type: 'confidential',            // 'public' for a browser/native app (PKCE only, no secret)
  redirect_uris: ['https://dashboards.example.com/api/beeuni/auth/callback'],
  scopes: ['profile', 'email', 'beeuni'],
});
// app.client_id, app.client_secret (shown once)
```

注册需要账户具备 `token.create`。重定向 URI 会逐字节进行比较；除 `localhost` 外，都必须使用 `https`。

| Scope | 授予应用的权限 |
|---|---|
| `profile` | 用户的 ID、电子邮件和账户成员关系（`GET /v1/accounts/me`）。 |
| `email` | 电子邮件地址（OIDC 形客户端的别名）。 |
| `beeuni` | 以用户身份操作整个 Beeuni API——项目、会话、文件、IAM 探测。没有此权限时，令牌仅用于身份识别。 |

<a id="2-mount-the-handler"></a>

## 2. 挂载处理程序

```ts
// lib/beeuni-auth.ts
import { createBeeuniAuth } from '@beeuni/sdk/server';

export const auth = createBeeuniAuth({
  backendUrl: 'https://api.beeuni.ai/v1',
  clientId: process.env.BEEUNI_OAUTH_CLIENT_ID!,
  clientSecret: process.env.BEEUNI_OAUTH_CLIENT_SECRET,   // omit for a public client
  redirectUri: 'https://dashboards.example.com/api/beeuni/auth/callback',
  cookieSecret: process.env.BEEUNI_AUTH_COOKIE_SECRET!,   // ≥ 32 chars; encrypts the session cookie
});
```

```ts
// app/api/beeuni/auth/[...beeuni]/route.ts  (Next.js App Router)
import { auth } from '@/lib/beeuni-auth';
const handle = (request: Request) => auth.handler(request);
export { handle as GET, handle as POST };
```

处理程序会提供 `basePath` 下的所有路由（由重定向 URI 派生——上述 `/api/beeuni/auth`）：

| 路径 | 功能 |
|---|---|
| `/signin?return_to=/path` | 启动登录（PKCE S256 + 状态值存储在有效期 10 分钟的 Cookie 中），并重定向到 Beeuni。 |
| `/callback` | 交换代码，设置加密的 `HttpOnly` 会话 Cookie，并重定向到 `return_to`。 |
| `/refresh?return_to=` | 轮换令牌对并重定向。由 `requireViewer` 使用。 |
| `/signout?return_to=` | 在 Beeuni 撤销刷新令牌并清除 Cookie。 |
| `/me` | 以 JSON 返回查看者，或返回 `401`。访问令牌过期时会内联刷新。 |
| `/proxy/*` | 以查看者身份转发到 Beeuni API。浏览器 SDK 的 `backendUrl`。 |

`return_to` 始终限定在同源路径中。

<a id="3-gate-pages-and-act-as-the-user"></a>

## 3. 保护页面并以用户身份操作

```ts
// middleware.ts — every page needs a viewer
import { auth } from '@/lib/beeuni-auth';

export async function middleware(request: Request) {
  const gate = await auth.requireViewer(request);
  if (gate.response) return gate.response;   // 302 → /refresh or /signin
}
export const config = { matcher: ['/((?!api/beeuni/auth|_next).*)'] };
```

```ts
// a server component / route handler
const viewer = await auth.viewer(request);       // { userId, email, accounts, scopes, token, expiresAt } | null
const beeuni = await auth.beeuni(request);       // request-scoped client acting as the viewer
const projects = await beeuni.projects.list();
const allowed = await beeuni.iam.can(accountId, viewer!.userId, { action: 'project.write', resourceType: 'project', resourceId });
```

`viewer()` 为只读操作，且不会消耗一次性刷新令牌；在中间件中使用 `requireViewer()`，这样对于刷新令牌仍然有效的用户，页面不会错误地以未登录状态渲染。

<a id="4-the-browser"></a>

## 4. 浏览器

```tsx
import { createBeeuni } from '@beeuni/sdk';
import { SignInWithBeeuni, useBeeuniViewer } from '@beeuni/sdk/react';

const beeuni = createBeeuni(auth.clientConfig());   // backendUrl = '/api/beeuni/auth/proxy'

function Header() {
  const { status, viewer } = useBeeuniViewer();
  if (status === 'signed-in') return <span>{viewer.email}</span>;
  return <SignInWithBeeuni className="button" />;
}
```

浏览器客户端会发送一个哨兵 bearer；`/proxy` 会在服务器上将其替换为查看者的真实令牌。`useSession`、`beeuni.project(id).sessions.*` 以及其他所有 SDK 调用都可以通过它照常工作。

<a id="what-the-user-sees"></a>

## 用户看到的内容

首次登录时，Beeuni 会显示一个列出你的应用和各项 Scope 的同意屏幕。Beeuni 会按用户和应用记住这一决定，因此之后的登录会直接重定向回来。撤销应用会删除它签发的所有令牌。

<a id="discovery"></a>

## 发现

`GET https://api.beeuni.ai/.well-known/oauth-authorization-server`（`/v1/oauth/.well-known/…` 下也提供）会发布通用 OAuth 客户端所需的端点。SDK 不需要它——SDK 会从 `backendUrl` 派生所有端点。

<a id="sign-in-tokens-for-apps"></a>

## App 的登录令牌

由 Beeuni 托管的 App 中的代码，以及它所使用的 Apps，会接受第二种令牌：Beeuni 为单个 App 签发的短期 ES256 JWT。不需要 OAuth 流程、重定向或客户端密钥。

| | 值 |
|---|---|
| Issuer | `<API origin>/v1/projects/<projectId>`，每个项目一个 |
| Discovery | `<issuer>/.well-known/openid-configuration` 和 `<issuer>/jwks.json`，公开 |
| Audience | 令牌所针对的 App 的 ID |
| Lifetime | 15 分钟 |
| Claims | `sub`、`email`、`name`、`picture`、`groups`、`group_ids`、`role`、`account_id`、`project_id`；agent 的令牌会额外包含 `kind: "agent"`，且没有 groups 或 role |

每个 App 都携带用于验证其令牌的值：`app.auth.issuer`、`app.auth.audience`、`app.auth.jwks_uri`。

```ts
import { beeuniToken, beeuniBinding, verifyBeeuniToken, requireBeeuniMember } from '@beeuni/sdk';

// In the App's browser code: a token fetcher for this App, or an App it uses.
const forThisApp = beeuniToken();
const forDb = beeuniToken({ audience: 'db' });
const db = beeuniBinding('db');   // { url: '<origin>/_beeuni/apps/db', token: forDb }

// On a server that receives the token:
const member = await verifyBeeuniToken(bearer, {
  issuer: app.auth.issuer,
  audience: app.auth.audience,
  jwks: app.auth.jwks_uri,
});
requireBeeuniMember(member, { roles: ['owner', 'admin'] });
```

在服务器 App 中，Beeuni 会注入 `BEEUNI_AUTH_ISSUER`（项目 issuer）、`BEEUNI_AUTH_AUDIENCE`（App 自身的 ID）和 `BEEUNI_AUTH_JWKS`（项目密钥集，作为 `data:` URI），因此 `verifyBeeuniToken(bearer)` 无需任何选项。不要自行设置 `BEEUNI_AUTH_*`：`BEEUNI_` 前缀已被保留，`beeuni validate` 会拒绝该前缀。

当未设置 audience（选项或 `BEEUNI_AUTH_AUDIENCE`）时，`verifyBeeuniToken` 会拒绝令牌：一个项目密钥会签发该项目中每个 App 的令牌，因此 `aud` 才能防止其他 App 的令牌进入。只有在要接受该项目中任意 App 的令牌时，才传入 `audience: false`。

`beeuniToken` 会读取 App 自身来源上的 `GET /_beeuni/token?audience=<slug or id>`。Audience 是 App 自身（默认值）或其使用的某个 App（`beeuni apps link <app> --uses <other>`）；其他任何值都会导致 `403 app_not_linked`。成员、脚本或 agent 应改为通过 API 签发令牌：`beeuni.project(projectId).apps.token(appId)` 或 `beeuni apps token <app>`。完整契约、绑定挂载和 Convex 后端设置，请参阅：[SDK → Apps](/docs/zh/sdk/apps) 和 [Apps → Apps an App uses](/docs/zh/feature-flags/apps#apps-an-app-uses-sign-in-tokens-and-bindings)。
