OAuth

Shadow 实现了标准的 OAuth 2.0 授权码流程。第三方应用可以通过范围化权限请求访问用户数据。

应用管理

创建 OAuth 应用

POST /api/oauth/apps
字段类型必填说明
namestring应用名称
descriptionstring描述
redirectUrisstring[]允许的重定向 URI
scopesstring[]请求的权限范围
iconUrlstring应用图标 URL

响应:

{
  "app": {
    "id": "uuid",
    "clientId": "...",
    "clientSecret": "...",
    "name": "My App",
    "redirectUris": ["https://example.com/callback"]
  }
}
const { app } = await client.createOAuthApp({
  name: 'My App',
  redirectUris: ['https://example.com/callback'],
  scopes: ['read:user', 'read:servers'],
})
result = client.create_oauth_app(
    name="My App",
    redirectUris=["https://example.com/callback"],
    scopes=["read:user", "read:servers"],
)
app = result["app"]

列出 OAuth 应用

GET /api/oauth/apps

列出当前用户拥有的所有 OAuth 应用。

const apps = await client.listOAuthApps()
apps = client.list_oauth_apps()

更新 OAuth 应用

PATCH /api/oauth/apps/:appId
await client.updateOAuthApp('app-id', { name: 'Renamed App' })
client.update_oauth_app("app-id", name="Renamed App")

删除 OAuth 应用

DELETE /api/oauth/apps/:appId
await client.deleteOAuthApp('app-id')
client.delete_oauth_app("app-id")

重置客户端密钥

POST /api/oauth/apps/:appId/reset-secret
const { clientSecret } = await client.resetOAuthAppSecret('app-id')
result = client.reset_oauth_app_secret("app-id")
new_secret = result["clientSecret"]

授权流程

Shadow 有两个 authorize 入口:

  • 浏览器入口:GET /app/oauth/authorize?...。把用户重定向到这里;嵌入式 App 需要用顶层弹窗打开。
  • API 校验/批准:GET/POST /api/oauth/authorize。这是 Shadow Web UI 和 SDK helper 使用的接口,不要把最终用户直接送到这里,也不要放进 iframe。

Shadow Web 会返回 frame-ancestors 'none',OAuth 授权页不能被第三方 iframe 嵌入。应用 需要用 popup 完成授权,并在 iframe sandbox 中允许 allow-popups-to-escape-sandbox,callback 成功后再刷新自己的本地 session。

第一步:重定向到授权页面

将用户浏览器重定向到授权页面:

GET /app/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=SCOPE&state=STATE
参数类型必填说明
response_typestring必须为 code
client_idstring你的应用客户端 ID
redirect_uristring必须匹配已注册的 URI
scopestring空格分隔的权限范围
statestring随机状态用于 CSRF 保护
const authorizeUrl = new URL('/app/oauth/authorize', 'https://shadowob.com')
authorizeUrl.searchParams.set('response_type', 'code')
authorizeUrl.searchParams.set('client_id', 'your-client-id')
authorizeUrl.searchParams.set('redirect_uri', 'https://example.com/callback')
authorizeUrl.searchParams.set('scope', 'user:read servers:read')
authorizeUrl.searchParams.set('state', crypto.randomUUID())
window.location.assign(authorizeUrl.toString())
from urllib.parse import urlencode

params = urlencode({
    "response_type": "code",
    "client_id": "your-client-id",
    "redirect_uri": "https://example.com/callback",
    "scope": "user:read servers:read",
    "state": state,
})
authorize_url = f"https://shadowob.com/app/oauth/authorize?{params}"

第二步:用户批准

POST /api/oauth/authorize
字段类型必填说明
clientIdstring客户端 ID
redirectUristring重定向 URI
scopestring批准的权限范围
statestring必须匹配请求中的 state

响应: 返回包含授权码的 redirectTo URL。多数第三方 App 不应该直接调用该接口;Shadow 授权页会在用户确认后调用它。

const { redirectTo } = await client.approveOAuthAuthorization({
  clientId: 'your-client-id',
  redirectUri: 'https://example.com/callback',
  scope: 'read:user',
  state: 'random-state',
})
result = client.approve_oauth_authorization(
    clientId="your-client-id",
    redirectUri="https://example.com/callback",
    scope="read:user",
    state="random-state",
)
redirect_url = result["redirectTo"]

第三步:用授权码交换令牌

POST /api/oauth/token
字段类型必填说明
grant_typestringauthorization_coderefresh_token
codestring条件授权码(用于 authorization_code
client_idstring客户端 ID
client_secretstring客户端密钥
redirect_uristring条件必须匹配授权请求
refresh_tokenstring条件用于 refresh_token 授权

响应:

{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "...",
  "scope": "read:user"
}
const tokens = await client.exchangeOAuthToken({
  grantType: 'authorization_code',
  code: 'auth-code',
  clientId: 'your-client-id',
  clientSecret: 'your-secret',
  redirectUri: 'https://example.com/callback',
})
tokens = client.exchange_oauth_token(
    grant_type="authorization_code",
    code="auth-code",
    client_id="your-client-id",
    client_secret="your-secret",
    redirect_uri="https://example.com/callback",
)
access_token = tokens["access_token"]

获取用户信息

GET /api/oauth/userinfo

使用 OAuth 访问令牌返回已认证用户的资料。

curl -H "Authorization: Bearer ACCESS_TOKEN" https://shadowob.com/api/oauth/userinfo

授权管理

列出授权

GET /api/oauth/consents

列出用户已授权的所有应用。

const consents = await client.listOAuthConsents()
consents = client.list_oauth_consents()

撤销授权

POST /api/oauth/revoke
await client.revokeOAuthConsent('app-id')
client.revoke_oauth_consent("app-id")

资源 API(OAuth 令牌)

这些端点使用 OAuth 访问令牌Authorization: Bearer <access_token>)并需要对应的权限范围。

服务器

方法端点权限范围说明
GET/api/oauth/serversservers:read列出用户的服务器
POST/api/oauth/serversservers:write创建新服务器
POST/api/oauth/servers/:id/inviteservers:write邀请用户加入服务器

频道

方法端点权限范围说明
GET/api/oauth/servers/:id/channelschannels:read列出服务器中的频道
POST/api/oauth/channelschannels:write创建频道

消息

方法端点权限范围说明
GET/api/oauth/channels/:id/messagesmessages:read获取消息历史
POST/api/oauth/channels/:id/messagesmessages:write发送消息

工作区

方法端点权限范围说明
GET/api/oauth/workspaces/:idworkspaces:read获取工作区信息

Buddy

方法端点权限范围说明
POST/api/oauth/buddiesbuddies:create创建 Buddy
POST/api/oauth/buddies/:id/messagesbuddies:manageBuddy 发送消息

可用权限范围

权限范围说明
user:read读取基本资料(用户名、显示名称、头像)
user:email读取邮箱地址
servers:read查看服务器列表
servers:write创建服务器、邀请用户
channels:read查看频道列表
channels:write创建频道
messages:read读取消息历史
messages:write发送消息
attachments:read查看附件
attachments:write上传附件
workspaces:read查看工作区信息
workspaces:write修改工作区文件
buddies:create创建 Buddy
buddies:manage管理 Buddy 并发送消息
TIP

参阅 平台应用了解使用 OAuth API 构建完整应用的示例。