250 lines
16 KiB
Markdown
250 lines
16 KiB
Markdown
# v0.2 登录与鉴权规格
|
||
|
||
本文是 HWLAB `v0.2` 登录、认证和应用层鉴权入口的长期规格。它把 Keycloak OIDC、浏览器 Web session、CLI/API/AgentRun 共用的用户 API key 和 HWLAB 内部 `AuthPrincipal` 归一到同一套口径,避免把本地账号密码、浏览器 cookie 和设备专用系统 key 混用。
|
||
|
||
基础设施实施跟踪见 [pikasTech/HWLAB#788](https://github.com/pikasTech/HWLAB/issues/788)。用户角色、Code Agent session owner、device pod grant 和资源授权矩阵见 [spec-user-access.md](spec-user-access.md);本文只定义“如何登录、如何恢复 actor、如何把请求归一成 actor”。
|
||
|
||
## 设计目标
|
||
|
||
- Web 用户通过 Keycloak OIDC 登录或注册,HWLAB 不再把本地账号密码表单作为目标登录体验。
|
||
- Web 使用 `hwlab-cloud-api` 发行的 httpOnly `hwlab_session`,每个 session token 最长 24 小时;第一版不做复杂 refresh token 管理,但必须可撤销、可重新登录轮换。
|
||
- CLI 是纯 CLI 体验,不打开浏览器、不跳转 Web、不做 device-code flow;标准凭据是环境变量 `HWLAB_API_KEY`。
|
||
- AgentRun runner 内的 `hwpod` 也必须使用同一类用户 API key 认证,映射到发起 Code Agent session 的 `users.id`;不再引入单独的 `HWLAB_DEVICE_POD_API_KEY`。
|
||
- 每个用户在首次登录后自动拥有一个默认 API key;用户也可以在 Web 中创建、查看、失效或重新生成 API key。
|
||
- API key 长效有效,除非用户或管理员手动 revoke/regenerate;它不跟 Web session 的 24 小时过期绑定。
|
||
- Keycloak 只做身份认证和账号注册;HWLAB 的 `users.role`、`users.status`、device pod grant 和 Code Agent owner 仍是应用层授权 source of truth。
|
||
- device-pod 权限只由 `users.role/status`、Code Agent session owner 和 `device_pod_grants` 控制;不存在“对所有正式 device pod 授权”的共享 runner key。
|
||
|
||
## 系统边界
|
||
|
||
| 组件 | 职责 |
|
||
| --- | --- |
|
||
| Keycloak | 独立 `keycloak` namespace 中的身份提供方,提供公网 HTTPS OIDC issuer、登录页、注册页和用户 subject。 |
|
||
| Keycloak Postgres | Keycloak 专用数据库,短期可与 Keycloak 同在 `keycloak` namespace;不复用 HWLAB v0.2 业务 Postgres。 |
|
||
| `hwlab-cloud-api` | OIDC callback、session token、API key、用户映射、`AuthPrincipal` 和应用层授权收口。 |
|
||
| `hwlab-cloud-web` | 浏览器 UI 和同源代理;未登录时引导到 Keycloak,不持有 Keycloak client secret,不直接访问 Postgres。 |
|
||
| `hwlab-cli` / AgentRun `hwpod` | 读取 `HWLAB_API_KEY`,向 Cloud Web 同源 API 或 Cloud API 发送 `Authorization: Bearer hwl_live_...`,恢复到同一个用户 actor。 |
|
||
| `hwlab-edge-proxy` / FRP | 只转发 cookie/header 和 HTTP 流量,不注入伪 actor,不做业务授权。 |
|
||
| HWLAB v0.2 Postgres | 保存 HWLAB 用户映射、session、API key、resource grant 和 runtime durable state。 |
|
||
|
||
Keycloak 部署不进入 `hwlab-v02` namespace。`hwlab-v02` 只把 Keycloak 当成外部 OIDC issuer 消费,使用 issuer URL、client id 和 client secret SecretRef 完成 callback 校验。
|
||
|
||
## 公网 HTTPS 与 issuer
|
||
|
||
Keycloak 必须有稳定公网 HTTPS issuer;OIDC redirect URI、cookie secure policy 和浏览器登录都依赖这一点。短期全自动方案使用主 server Caddy + Let's Encrypt + nip.io 域名,并通过 FRP 把 G14 `keycloak` namespace 的 Keycloak Service 暴露到主 server 本地端口。
|
||
|
||
目标 issuer:
|
||
|
||
```text
|
||
https://auth.74-48-78-17.nip.io/realms/hwlab
|
||
```
|
||
|
||
约束:
|
||
|
||
- Keycloak admin console、account console、OIDC discovery 和 JWKS 都必须走 HTTPS;不要用裸 HTTP issuer。
|
||
- `hwlab-cloud-api` 只记录 issuer、client id、redirect URI 和 SecretRef 名称,不在日志、issue 或 ConfigMap 中输出 client secret。
|
||
- Keycloak 的 `hostname`、proxy header 和 frontend URL 必须与公网 issuer 一致,避免 callback 后 token issuer mismatch。
|
||
- `hwlab-cloud-api`、`hwlab-cloud-web` 和 CLI 仍保持 v0.2 原入口 `19666/19667`;Keycloak 只新增 auth 域名,不替代 HWLAB Cloud Web。
|
||
|
||
## 注册策略
|
||
|
||
短期小范围测试允许 Keycloak 开启自助注册,并且不要求邮箱或手机校验。该策略只有在以下条件同时满足时成立:
|
||
|
||
- 新注册用户默认只映射为 HWLAB `user` 角色,不自动成为 `admin`。
|
||
- 新用户没有 device pod grant,也不能看到或使用任何 device pod,直到 `admin` 在 HWLAB 中授权。
|
||
- Keycloak 账号状态必须能被管理员禁用;HWLAB `users.status='disabled'` 也必须能独立阻断 session 和 API key。
|
||
- 如果公网注册出现垃圾账号或撞库迹象,第一优先级是关闭 Keycloak self-registration 或增加邀请码/管理员审核;不要把防滥用逻辑塞进 device pod 授权。
|
||
|
||
邮箱和手机字段可以作为 profile 信息保存,但第一版不要求验证。后续如果进入更大范围公网使用,应至少补充邮箱验证、注册限流或邀请制中的一种。
|
||
|
||
## 身份映射
|
||
|
||
Keycloak subject 不直接当业务授权 ID。`hwlab-cloud-api` 在 OIDC callback 中按 `issuer + sub` 查找或创建 HWLAB `users` 记录,然后所有业务授权都使用 `users.id`。
|
||
|
||
推荐扩展 `users`:
|
||
|
||
```sql
|
||
ALTER TABLE users ADD COLUMN IF NOT EXISTS auth_provider TEXT NOT NULL DEFAULT 'local';
|
||
ALTER TABLE users ADD COLUMN IF NOT EXISTS keycloak_issuer TEXT;
|
||
ALTER TABLE users ADD COLUMN IF NOT EXISTS keycloak_sub TEXT;
|
||
ALTER TABLE users ADD COLUMN IF NOT EXISTS email TEXT;
|
||
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TEXT;
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_keycloak_subject
|
||
ON users(keycloak_issuer, keycloak_sub)
|
||
WHERE keycloak_issuer IS NOT NULL AND keycloak_sub IS NOT NULL;
|
||
```
|
||
|
||
映射规则:
|
||
|
||
- 首次 OIDC 登录:按 `issuer + sub` 不存在则创建 `users`,默认 `role='user'`、`status='active'`。
|
||
- 已存在本地 bootstrap admin 时,可以由 admin 在 HWLAB 内手动绑定 Keycloak subject 或提升 Keycloak 用户为 `admin`;Keycloak 注册本身不得自动提权。
|
||
- Keycloak profile 更新只同步 `username/display_name/email` 这类展示信息,不覆盖 HWLAB `role/status`。
|
||
- `disabled` 用户不能创建 Web session,不能通过 API key 鉴权,也不能继续 Code Agent session。
|
||
|
||
## AuthPrincipal
|
||
|
||
所有用户态请求最终归一成 `AuthPrincipal`。业务模块只消费 `AuthPrincipal`,不直接判断 Keycloak token、cookie 或 API key 原文。
|
||
|
||
```ts
|
||
type AuthPrincipal = {
|
||
userId: string;
|
||
username: string;
|
||
displayName: string;
|
||
role: "admin" | "user";
|
||
status: "active" | "disabled";
|
||
authMethod: "web-session" | "api-key" | "legacy-local-session";
|
||
sessionId?: string;
|
||
apiKeyId?: string;
|
||
keycloakIssuer?: string;
|
||
keycloakSub?: string;
|
||
scopes: string[];
|
||
};
|
||
```
|
||
|
||
认证顺序必须显式区分两类正式用户凭据和 legacy fallback:
|
||
|
||
1. `Authorization: Bearer hwl_live_...` 或 `HWLAB_API_KEY` 映射出的 header 是用户 API key,适用于 CLI 和 AgentRun runner 内 `hwpod`。
|
||
2. `hwlab_session` cookie 是 Web session。
|
||
3. 本地账号密码 session token 只作为 legacy/debug fallback。
|
||
|
||
现有 `Authorization: Bearer <session-token>` 兼容入口只能保留为 legacy/debug,不得阻塞 `hwl_live_` 前缀的用户 API key 识别。
|
||
|
||
## Web 登录流程
|
||
|
||
```text
|
||
browser -> Cloud Web /
|
||
-> no valid hwlab_session
|
||
-> GET /auth/oidc/login?returnTo=/
|
||
-> 302 Keycloak login/register
|
||
-> Keycloak callback with code
|
||
-> hwlab-cloud-api validates OIDC code/issuer/JWKS
|
||
-> upsert users by issuer+sub
|
||
-> ensure default API key exists for the user
|
||
-> create user_sessions token hash, expires_at <= now + 24h
|
||
-> Set-Cookie hwlab_session=httpOnly; Secure; SameSite=Lax
|
||
-> redirect returnTo
|
||
```
|
||
|
||
Web 页面不要求用户输入或复制 API key。浏览器请求通过 `hwlab_session` 恢复 `AuthPrincipal`;需要 CLI 时,用户在 Web 的 API key 页面查看或重新生成自己的 key。
|
||
|
||
Session 规则:
|
||
|
||
- 每次 OIDC 登录或 callback 成功都发行新的 session token,数据库只保存 token hash。
|
||
- 单个 session token 最长 24 小时;过期、用户 disabled 或 logout 后必须返回 `401 auth_session_invalid`。
|
||
- `/auth/logout` 撤销 HWLAB session,并可选择跳转 Keycloak logout;即使 Keycloak 全局会话仍有效,HWLAB 已撤销的 session 也不能继续使用。
|
||
- 第一版不实现长期 refresh token 存储;用户过期后重新走 Keycloak 登录。
|
||
|
||
## CLI API key 流程
|
||
|
||
CLI 和 AgentRun `hwpod` 标准认证方式:
|
||
|
||
```bash
|
||
export HWLAB_API_KEY=hwl_live_xxx
|
||
bun tools/hwlab-cli/bin/hwlab-cli.ts client auth whoami
|
||
```
|
||
|
||
约束:
|
||
|
||
- CLI 不跳转浏览器,不依赖 Web cookie,不要求 username/password 交互。
|
||
- `hwlab-cli` 默认从 `HWLAB_API_KEY` 读取 key,并发送 `Authorization: Bearer <key>`。
|
||
- AgentRun runner 的 transient env 只允许注入映射到当前 Code Agent session owner 的 `HWLAB_API_KEY`;可以使用该用户默认 key,也可以使用同一 `api_keys` 表中为该用户创建的 runner 专用 key,但绝不能使用跨用户共享的 device-pod 系统 key。
|
||
- `client auth status` 必须显示 endpoint、是否检测到 `HWLAB_API_KEY`、key prefix 和用户摘要;不得输出完整 API key。
|
||
- `client auth whoami` 或 `client request GET /v1/users/me` 必须能用 API key 返回与 Web 同一用户的 `AuthPrincipal` 摘要。
|
||
- `client auth login --username ...` 和本地 cookie profile 属于 legacy 兼容入口;目标体验不再把它作为一等 CLI 登录。
|
||
- `hwpod` 在 runner 中必须自动使用 `HWLAB_RUNTIME_API_URL` 和 `HWLAB_API_KEY` 直连 `hwlab-cloud-api`,并让 cloud-api 按该 key 对应用户的 role/grant 做授权。
|
||
|
||
API key 数据结构:
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS api_keys (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL DEFAULT 'Default API key',
|
||
key_prefix TEXT NOT NULL,
|
||
key_hash TEXT,
|
||
display_secret TEXT,
|
||
scopes_json TEXT NOT NULL DEFAULT '[]',
|
||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')),
|
||
created_at TEXT NOT NULL,
|
||
last_used_at TEXT,
|
||
revoked_at TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id, status, created_at DESC);
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix);
|
||
```
|
||
|
||
短期小范围测试允许 `display_secret` 保存可重复展示的 API key 明文,满足“用户不必只在生成时保存一次”的体验;但完整 key 不得写入日志、issue、ConfigMap、Kubernetes Secret、trace 或 CLI 默认输出。生产化时应删除 `display_secret`,改成只保存 `key_hash`,并只在生成时显示一次。
|
||
|
||
API key 行为:
|
||
|
||
- 首次 OIDC 登录后自动创建默认 key;如果默认 key 被 revoke,用户可在 Web 中重新生成。
|
||
- 默认 key、手动创建的 key 和 AgentRun runner 使用的 key 都继承该用户的 `role/status/grant`,不额外绕过权限矩阵。
|
||
- revoke 后同一 key 立刻返回 `401 api_key_invalid`,不能等 Web session 过期。
|
||
- 每次 API key 成功认证应更新 `last_used_at`,但不得记录完整 key。
|
||
|
||
## API 接口
|
||
|
||
| 接口 | 说明 |
|
||
| --- | --- |
|
||
| `GET /auth/oidc/login?returnTo=...` | 发起 Keycloak OIDC 登录,保存 state/nonce 并 302 到 Keycloak。 |
|
||
| `GET /auth/oidc/callback` | 校验 code、issuer、nonce 和 state,映射用户,发行 24 小时 Web session。 |
|
||
| `GET /auth/session` | 从 Web cookie 或兼容 session 恢复 actor;API key 请求可返回 `authMethod='api-key'` 摘要。 |
|
||
| `POST /auth/logout` | revoke 当前 Web session 并清 cookie。 |
|
||
| `POST /auth/login` | 本地账号密码 legacy/bootstrap fallback;目标 Web/CLI 登录不依赖该入口。 |
|
||
| `GET /v1/users/me` | 返回当前 `AuthPrincipal` 的脱敏摘要。 |
|
||
| `GET /v1/api-keys` | 列出当前用户 API key metadata;短期测试可返回默认 key 的 displayable 状态。 |
|
||
| `GET /v1/api-keys/default` | 返回或创建默认 API key;短期测试允许返回完整 key 明文给当前用户本人。 |
|
||
| `POST /v1/api-keys` | 创建一个用户 API key。 |
|
||
| `POST /v1/api-keys/{id}/regenerate` | 重新生成指定 API key,旧 secret 立即失效。 |
|
||
| `DELETE /v1/api-keys/{id}` | revoke 指定 API key。 |
|
||
|
||
所有 API key 管理接口都要求当前 `AuthPrincipal` 是同一用户本人或 `admin`;普通用户不能查看或生成其他人的 key。响应默认只返回 `key_prefix`、`id`、`name`、`status`、`last_used_at` 和短期测试允许的本人完整 key。
|
||
|
||
## 与资源授权的关系
|
||
|
||
登录和认证只回答“请求是谁”。资源授权仍由 [spec-user-access.md](spec-user-access.md) 定义:
|
||
|
||
- `admin` 可以管理用户、device pod profile/grant,并跨用户查看或取消 Code Agent session。
|
||
- `user` 只能访问自己的 Code Agent session 和被授权的 device pod。
|
||
- Web session、CLI API key 和 AgentRun runner 内 `hwpod` API key 得到同一个 `users.id` 时,应看到相同 device pod grant 和账号 workspace。
|
||
- Keycloak realm role、group 或 claim 不直接决定 HWLAB device pod 权限;最多作为创建/绑定用户时的输入线索。
|
||
|
||
## 测试规格
|
||
|
||
## T1
|
||
|
||
阅读 docs/reference/spec-v02-auth.md,然后用 CLI 或 curl 验证 `https://auth.74-48-78-17.nip.io/realms/hwlab/.well-known/openid-configuration` 可访问,issuer 与配置完全一致,JWKS URL 为 HTTPS,响应不包含 admin password、client secret 或数据库连接串。
|
||
|
||
## T2
|
||
|
||
阅读 docs/reference/spec-v02-auth.md,然后从浏览器访问 `http://74.48.78.17:19666/`。未登录时必须进入 Keycloak 登录/注册;注册新用户不要求邮箱或手机验证,callback 后回到 Workbench,`GET /auth/session` 返回 active `user` actor 和 24 小时内过期的 session,不返回 session token 原文。
|
||
|
||
## T3
|
||
|
||
阅读 docs/reference/spec-v02-auth.md,然后登录 Web 后打开 API key 管理入口,确认默认 API key 已自动存在;短期测试中可重复查看完整 key。用 `HWLAB_API_KEY=<key> bun tools/hwlab-cli/bin/hwlab-cli.ts client auth whoami` 验证 CLI 无浏览器跳转即可恢复同一用户。
|
||
|
||
## T4
|
||
|
||
阅读 docs/reference/spec-v02-auth.md,然后 revoke 或 regenerate API key,再用旧 `HWLAB_API_KEY` 请求 `/v1/users/me` 或 `/v1/device-pods`,必须返回 `401 api_key_invalid`;同一用户重新登录 Web session 不应恢复旧 key。
|
||
|
||
## T5
|
||
|
||
阅读 docs/reference/spec-v02-auth.md,然后分别使用 Web session 和同一用户的 CLI API key 访问 `/v1/device-pods`、创建 Code Agent session 和读取自己的 trace/result,确认授权结果一致;另一个普通用户的 API key 不能读取该 session,admin 可以跨用户查看。
|
||
|
||
## T6
|
||
|
||
阅读 docs/reference/spec-v02-auth.md,然后检查 cloud-api 日志、trace、CLI 默认输出和 issue 复现材料,确认不出现 Keycloak client secret、session token、完整 API key、历史 `HWLAB_DEVICE_POD_API_KEY` 或 Postgres URL 原文;AgentRun runner 中也不得存在跨用户共享的 device-pod 系统 key。
|
||
|
||
## 规格的实现情况
|
||
|
||
| 规格项 | 状态 | 说明 |
|
||
| --- | --- | --- |
|
||
| Keycloak 独立 namespace 与公网 HTTPS issuer | 目标状态 | 实施跟踪见 #788;短期建议 `auth.74-48-78-17.nip.io` + Caddy/Let's Encrypt + FRP。 |
|
||
| Keycloak 自助注册且不强制邮箱/手机验证 | 目标状态 | 只适合小范围测试;默认 `user`、无 device pod grant。 |
|
||
| Web OIDC login/callback | 目标状态 | 当前源码仍以 `/auth/login` 本地账号密码为主。 |
|
||
| Web session 24 小时轮换 | 目标状态 | 当前 `internal/cloud/access-control.ts` 为 7 天本地 session,后续需改为 24 小时。 |
|
||
| CLI/AgentRun `HWLAB_API_KEY` 一等登录 | 目标状态 | 当前 CLI 主要保存 cookie session,AgentRun/device-pod 仍有旧 shared key 口径;目标是统一 env API key 映射到用户,无浏览器跳转,无跨用户 device-pod key。 |
|
||
| 默认 API key 自动生成和 Web 可查看 | 目标状态 | 小范围测试允许重复查看明文;生产化再改为 hash-only。 |
|
||
| `AuthPrincipal` 归一 | 目标状态 | 后续实现必须把 Web session、CLI API key 和 AgentRun `hwpod` API key 都归一成同一用户 actor。 |
|
||
| `admin/user` 与 device pod grant 授权 | 部分实现 | 现有 cloud-api 已有本地用户、session、grant 和 Code Agent owner 绑定;资源授权继续按 spec-user-access 收敛。 |
|