使用 Beeuni 登录
通过一个路由让你自己的应用基于 Beeuni 身份进行访问控制,并通过 SDK 以已登录用户身份执行操作。
“使用 Beeuni 登录”会将 Beeuni 作为你运行的应用的身份提供商:
可以是仪表板、内部工具,或基于 Beeuni 构建的垂直产品。用户使用其 Beeuni 账户登录,服务器知道用户身份,而应用发起的每个 Beeuni 调用都会以该用户及其角色分配运行。整个流程都在 @beeuni/sdk 中完成。你的应用不会在浏览器中存储 Beeuni 令牌。
这是标准的 OAuth 2.1(授权码 + PKCE),由 api.beeuni.ai 上的 Beeuni API 提供。
1. 注册应用
前往 Account → Tokens → OAuth apps → Register app,或调用 SDK:
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 探测。没有此权限时,令牌仅用于身份识别。 |
2. 挂载处理程序
// 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
});
// 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 始终限定在同源路径中。
3. 保护页面并以用户身份操作
// 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).*)'] };
// 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(),这样对于刷新令牌仍然有效的用户,页面不会错误地以未登录状态渲染。
4. 浏览器
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 调用都可以通过它照常工作。
用户看到的内容
首次登录时,Beeuni 会显示一个列出你的应用和各项 Scope 的同意屏幕。Beeuni 会按用户和应用记住这一决定,因此之后的登录会直接重定向回来。撤销应用会删除它签发的所有令牌。
发现
GET https://api.beeuni.ai/.well-known/oauth-authorization-server(/v1/oauth/.well-known/… 下也提供)会发布通用 OAuth 客户端所需的端点。SDK 不需要它——SDK 会从 backendUrl 派生所有端点。
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。
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 和 Apps → Apps an App uses。