v1auth 上下文凭据管理
凭据管理总览
凭据列表、第三方提供商目录、删除与改名;各类凭据的单独页面入口。
凭据管理是「账号自己管理自己的登录方式」:邮箱、密码、通行密钥、验证器应用、恢复码、第三方身份。全部要求会话令牌,不接受资源令牌——一个 OAuth 客户端没有理由改用户的登录方式。
通用约定
| 约定 | 说明 |
|---|---|
| 两步式新增 | 第一步(路径以 options 结尾,或 email/options)在流程起点要求 max_age=300;第二步(确认落库)只要求普通会话 |
| 删除 | 要求 max_age=300 |
| 改名 | 不要求提权 |
| 读取 | 不要求提权 |
| 最后一种登录方式 | 删除会被拒绝,避免账号被锁死 |
为什么第二步反而放宽
中间状态(待绑定邮箱、待注册挑战、待确认 secret)已经绑定 userId 且只能消费一次。
它证明的是「刚刚有人在这条会话上完成了一次验证」,再叠一层 max_age 只会让用户在两次请求之间被弹去重新登录。
凭据对象
列表与新增端点返回的每条凭据都是同一形状:
| 字段 | 说明 |
|---|---|
id | 凭据标识,删除与改名时使用 |
type | email / password / passkey / authenticator / recovery_code / oauth |
display | 界面展示文案(邮箱、设备名、提供商名) |
provider | 第三方身份的提供商;其他类型为 null |
label | 用户起的标签,可为 null |
created | 创建时间 |
verified | 是否已验证 |
primary | 是否为主凭据(目前仅邮箱有此概念) |
removable | 是否允许通过接口删除 |
端点
| 方法 | 路径 | 说明 | 页面 |
|---|---|---|---|
GET | /v1/auth/credentials | 列出本账号全部凭据 | 本页 |
GET | /v1/auth/providers | 提供商目录(公开,无账号态) | 本页 |
GET | /v1/auth/credentials/providers | 提供商目录 + 本账号绑定状态 | 本页 |
DELETE | /v1/auth/credentials/:credentialId | 删除一条凭据 | 本页 |
PATCH | /v1/auth/credentials/:credentialId | 改标签 | 本页 |
POST | /v1/auth/credentials/email/options 等 | 邮箱增删改 | 邮箱 |
PUT | /v1/auth/credentials/password | 设置或修改密码 | 密码 |
POST | /v1/auth/credentials/passkey/options、/credentials/passkey | 注册通行密钥 | 通行密钥 |
POST | /v1/auth/credentials/authenticator/options、/credentials/authenticator | 绑定验证器应用 | 验证器应用 |
POST | /v1/auth/credentials/provider/:provider | 绑定第三方身份 | 第三方身份 |
POST | /v1/auth/credentials/recovery-codes | 生成恢复码 | 恢复码 |
GET /credentials
查询参数:limit(默认 20,上限 100)、cursor。
{
"success": true,
"credentials": [
{
"id": "cred_...",
"type": "email",
"display": "[email protected]",
"provider": null,
"label": null,
"created": "2026-01-04T10:00:00.000Z",
"verified": true,
"primary": true,
"removable": false
}
],
"next_cursor": null
}按创建时间倒序。removable: false 通常表示「这是最后一种能登录的方式」。
GET /providers(公开)
{
"success": true,
"providers": [
{ "provider": "github", "display": "GitHub", "available": true }
]
}登录页在用户还没登录时就要渲染这排按钮,因此这个端点匿名可用。它不含 linked:把恒为 false 的假值放进公开响应,界面会当真值渲染,比不给更糟。
available 表示「本次部署是否配好了该提供商的客户端凭据」,不是「代码里有没有这个分支」。
GET /credentials/providers
字段与公开目录相同,额外多一个 linked:
{
"success": true,
"providers": [
{ "provider": "github", "display": "GitHub", "available": true, "linked": true }
]
}与控制台「安全设置」页配套:它需要同时知道「有哪些提供商」和「我已经绑了哪些」。
DELETE /credentials/:credentialId
{ "success": true, "message": "Credential removed" }删除后写入安全记录,并带上被删凭据的 id——事后追查时用户需要判断的正是「消失的是不是那一把」。解绑第三方身份单独记为 provider.unbind,因为它才是账号被外部控制时的处置动作。
PATCH /credentials/:credentialId
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
label | string | null | ✅ | 新标签,1–64 字符;null 表示清空,空字符串不接受 |
{ "success": true, "credential": { "id": "cred_...", "type": "passkey" } }只有通行密钥与第三方身份带标签,其他类型会被拒绝。对第三方身份而言,标签只是本地展示名,改名不会改变绑定的是哪个账号。