跨应用集成
Glint 支持委托授权模型:其他 OAuth 应用可以代表其用户访问 Glint 的数据,无需在 Glint 内部单独注册应用。整个授权链完全通过 Prism 完成。
工作原理
Glint 是 App A(资源提供方)。其他任何应用是 App B(消费方)。该流程依赖 Prism 的跨应用权限范围委托机制:
app:<glint_client_id>:<inner_scope>例如,若 Glint 的 Prism Client ID 为 prism_abc123,则读取权限范围如下:
app:prism_abc123:read_todos完整授权流程
Glint 内部无需注册任何内容。 App B 在 Prism 自己的控制台中注册 Glint 的 scope,用户通过 Prism 的标准授权页面授予访问权限。
前置条件
- 一个已完成 Prism OAuth 配置的 Glint 实例
- Glint 的 Prism Client ID(可在"设置 → 应用配置"中查看)
- 可访问 Glint 所使用的 Prism 实例,并拥有一个 App B OAuth 客户端
第一步 — 在 Prism 中定义权限范围(Glint 管理员操作)
Glint 的所有者必须首先在 Prism 的控制台中定义 App B 可以申请的权限范围,无需在 Glint 的 UI 中操作。
登录 Prism,打开 Glint 的应用设置,进入 Permissions(权限) 选项卡,添加如下权限范围定义:
| Scope 键名 | 建议标题 | 说明 |
|---|---|---|
read_todos | 读取待办事项 | 查看已加入团队中的待办分组和待办事项 |
write_todos | 创建和编辑待办 | 创建和更新待办事项 |
delete_todos | 删除待办事项 | 删除待办事项 |
可根据实际需求定义任意数量的 scope。
可选地,可以设置访问规则来限制哪些应用或用户可以注册这些 scope:
app_allow— 仅允许特定 App B 的client_id申请你的 scopeowner_allow— 仅允许特定 Prism 用户 ID 将你的 scope 添加到其应用的allowed_scopes
若未设置任何 allow 规则,则默认所有应用均可使用。
第二步 — 在 App B 中注册 Scope(App B 开发者操作)
在 App B 的 Prism 控制台 → 设置 → 应用权限中,输入 Glint 的 Client ID 并选择 inner scope(如 read_todos)。这会将以下内容添加到 App B 的 allowed_scopes:
app:prism_abc123:read_todos也可通过 API 完成:
curl -X PATCH https://prism.example.com/api/apps/<appB_id> \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"allowed_scopes": [
"openid", "profile",
"app:prism_abc123:read_todos"
]
}'第三步 — 在授权 URL 中请求 Scope
App B 将用户重定向至 Prism 登录时,需在 scope 参数中包含 Glint 的 scope:
https://prism.example.com/api/oauth/authorize
?client_id=<appB_client_id>
&redirect_uri=https://appb.example.com/callback
&response_type=code
&scope=openid+profile+app%3Aprism_abc123%3Aread_todos
&code_challenge=...
&code_challenge_method=S256用户会在 Prism 的授权页面看到一张权限卡片,展示第一步中定义的 Glint scope 标题和说明。
第四步 — 交换授权码并调用 Glint
用户批准后,完成标准的 Token 交换流程,获得的 access token 的 scope 字段将包含 app:prism_abc123:read_todos。
调用 Glint 时以 Bearer Token 的形式传入:
const response = await fetch(
`https://glint.example.com/api/cross-app/teams/${teamId}/sets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
const { sets } = await response.json();团队成员关系解析
Glint 的跨应用接口以团队为作用域。为处理请求,Glint 需要验证用户(通过 introspect Token 的 sub 字段识别)是否为所请求团队的成员。
Glint 按以下顺序解析团队成员关系:
- KV 缓存(快速路径)—— 当用户直接登录 Glint 时自动写入,有效期 1 小时。若用户曾登录过 Glint,则优先使用此缓存。
- 实时 Prism 请求(兜底路径)—— 若 Bearer Token 包含
teams:readscope,Glint 会实时调用 Prism 的/api/oauth/me/teams并将结果缓存。
若两种方式均失败,请求将返回 403 并附带说明信息。
App B 推荐的 Scope 组合
openid profile teams:read app:prism_abc123:read_todosGlint 界面中的应用令牌警告
当用户登录 Glint 时,若 Glint 检测到当前 access token 是由外部应用颁发的(即 token 的 client_id 与 Glint 自身的 client_id 不符),Glint 将显示一个模态警告:
通过应用令牌访问 —— 本次会话使用的是由外部应用颁发的令牌,而非直接为您颁发。如果您未预期此情况,请立即退出登录。
用户可选择继续或退出。这是一项安全保护措施,在正常的跨应用使用场景下(App B 在服务端使用令牌,用户通过 Glint 自身的流程单独登录)不会出现此警告。
可用 Scope
| Scope 键名 | 允许的操作 |
|---|---|
read_todos | 列出待办分组;列出任意已加入团队中的待办事项 |
write_todos | 创建待办事项;更新待办标题和完成状态 |
delete_todos | 删除待办事项 |
所有操作仍受 Glint 的团队权限规则约束。若用户的团队角色缺少某项权限(如 create_todos),即使拥有有效的 write_todos scope,API 也会返回 403。
错误参考
| 状态码 | 含义 |
|---|---|
401 | 缺少或格式错误的 Authorization 头,或 Token 已失效/过期 |
403 | Token 缺少所需 scope;或用户不是该团队成员 |
403 | 无法获取团队成员关系——请在 scope 中包含 teams:read,或让用户先登录 Glint 一次 |
404 | 待办分组或待办事项不存在,或不属于所请求的团队 |
安全说明
- Glint 始终通过 Prism 的 introspect 接口验证 Token,从不直接信任 Token 的载荷内容。
- App B 的
client_secret不参与此流程;仅client_id用于标识 scope 命名空间。 - 若要限制哪些应用可以使用 Glint 的 scope,请在向外公开
client_id前在 Prism 中设置app_allow规则。 - 在 Prism 中撤销用户对 App B 的授权,同样会移除其对 Glint 资源的访问权限,无需额外操作。
完整示例(TypeScript)
async function getGlintSets(
accessToken: string,
glintBaseUrl: string,
teamId: string,
) {
const res = await fetch(
`${glintBaseUrl}/api/cross-app/teams/${teamId}/sets`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
if (res.status === 401) throw new Error("Token 无效或缺失");
if (res.status === 403) {
const { error } = await res.json();
throw new Error(`权限不足: ${error}`);
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { sets } = await res.json();
return sets as Array<{ id: string; name: string }>;
}
async function createGlintTodo(
accessToken: string,
glintBaseUrl: string,
teamId: string,
setId: string,
title: string,
) {
const res = await fetch(
`${glintBaseUrl}/api/cross-app/teams/${teamId}/sets/${setId}/todos`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title }),
},
);
if (!res.ok) {
const { error } = await res.json();
throw new Error(`创建待办失败: ${error}`);
}
const { todo } = await res.json();
return todo;
}