skillhub/docs/23-cas-integration.md
XiaoSeS 2c1bcf3b22 fix(auth): harden CAS identity link flow
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
2026-07-31 12:01:38 +08:00

179 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CAS 2.0/3.0 接入指南
本文说明如何把一个 CAS Provider Instance 接入 SkillHub 统一身份核心。CAS 登录默认关闭,
配置不完整或 Authority 状态异常时不会出现在登录方法目录中。
## 1. 前置条件
- SkillHub 的公开入口和 CAS Server 使用 HTTPS。
- Redis 可用。CAS state 使用 Redis 原子消费,以支持多 Pod 和重放保护。
- CAS Server 已允许下面的精确 Service URL:
```text
https://<skillhub-host>/api/v1/auth/cas/<provider-code>/callback
```
- 已确认可作为稳定身份键的 CAS principal 或 immutable attribute。
`provider-code` 和 `authority` 是持久身份绑定的一部分。产生 Identity Binding 后不能通过
普通配置变更切换到另一个身份域。
## 2. 配置
| 环境变量 | 必填 | 默认值 | 说明 |
|---|---:|---|---|
| `SKILLHUB_AUTH_CAS_ENABLED` | 是 | `false` | 显式启用 CAS |
| `SKILLHUB_AUTH_CAS_PROVIDER_CODE` | 是 | `cas` | Provider Instance code,需长期稳定 |
| `SKILLHUB_AUTH_CAS_DISPLAY_NAME` | 是 | `CAS` | 登录页展示名 |
| `SKILLHUB_AUTH_CAS_AUTHORITY` | 是 | 空 | 稳定 CAS 身份域 ID |
| `SKILLHUB_AUTH_CAS_SERVER_URL` | 是 | 空 | CAS Server 根地址,例如 `https://cas.example.com/cas` |
| `SKILLHUB_AUTH_CAS_SERVICE_URL` | 是 | 空 | 精确 SkillHub callback,不含 query/fragment |
| `SKILLHUB_AUTH_CAS_PROTOCOL_VERSION` | 是 | `3.0` | `2.0` 或 `3.0` |
| `SKILLHUB_AUTH_CAS_SUBJECT_TYPE` | 是 | `cas_principal` | 统一身份核心中的 Subject type |
| `SKILLHUB_AUTH_CAS_ATTRIBUTES_SUBJECT` | 否 | 空 | 为空时使用 CAS principal;否则使用唯一 immutable attribute |
| `SKILLHUB_AUTH_CAS_ATTRIBUTES_DISPLAY_NAME` | 否 | 空 | display name attribute |
| `SKILLHUB_AUTH_CAS_ATTRIBUTES_EMAIL` | 否 | 空 | email attribute,固定按 asserted 处理 |
| `SKILLHUB_AUTH_CAS_ATTRIBUTES_AVATAR_URL` | 否 | 空 | avatar URL attribute |
| `SKILLHUB_AUTH_CAS_CONNECT_TIMEOUT` | 否 | `PT5S` | 连接超时 |
| `SKILLHUB_AUTH_CAS_READ_TIMEOUT` | 否 | `PT10S` | 请求总超时 |
| `SKILLHUB_AUTH_CAS_STATE_TTL` | 否 | `PT5M` | 登录 state 有效期,最大 15 分钟 |
| `SKILLHUB_AUTH_CAS_MAX_RESPONSE_BYTES` | 否 | `1048576` | validation 响应上限,1 KiB–1 MiB |
CAS 返回的普通 email attribute 不是 verified email,不能用于
`EMAIL_DOMAIN` 准入或静默账号绑定。
### Docker Compose
编辑 `.env.release`:
```dotenv
SKILLHUB_AUTH_CAS_ENABLED=true
SKILLHUB_AUTH_CAS_PROVIDER_CODE=cas-main
SKILLHUB_AUTH_CAS_DISPLAY_NAME=Corporate CAS
SKILLHUB_AUTH_CAS_AUTHORITY=corp-cas
SKILLHUB_AUTH_CAS_SERVER_URL=https://cas.example.com/cas
SKILLHUB_AUTH_CAS_SERVICE_URL=https://skills.example.com/api/v1/auth/cas/cas-main/callback
SKILLHUB_AUTH_CAS_PROTOCOL_VERSION=3.0
SKILLHUB_AUTH_CAS_SUBJECT_TYPE=cas_principal
SKILLHUB_AUTH_CAS_ATTRIBUTES_DISPLAY_NAME=displayName
SKILLHUB_AUTH_CAS_ATTRIBUTES_EMAIL=mail
```
启动前运行:
```bash
./scripts/validate-release-config.sh .env.release
```
### Helm
```yaml
auth:
cas:
enabled: true
providerCode: cas-main
displayName: Corporate CAS
authority: corp-cas
serverUrl: https://cas.example.com/cas
serviceUrl: https://skills.example.com/api/v1/auth/cas/cas-main/callback
protocolVersion: "3.0"
subjectType: cas_principal
connectTimeout: PT5S
readTimeout: PT10S
stateTtl: PT5M
maxResponseBytes: 1048576
attributes:
subject: ""
displayName: displayName
email: mail
avatarUrl: ""
```
Chart 在渲染阶段拒绝 HTTP endpoint、缺失 URL 和 provider code 不匹配的 callback。
### Kustomize
修改 `deploy/k8s/base/configmap.yaml` 中的 `auth-cas-*` 字段,然后重新应用 overlay。
CAS 配置不包含 client secret;若企业扩展引入凭证,必须放入 Kubernetes Secret,不能放
在 ConfigMap。
## 3. 身份映射
默认映射:
```text
primary subject = CAS principal
subject type = cas_principal
email assurance = PROVIDER_ASSERTED
aliases = none
```
如果 CAS principal 可能随用户名变更,必须配置
`SKILLHUB_AUTH_CAS_ATTRIBUTES_SUBJECT` 指向 CAS Server 明确定义且实际返回的 immutable
attribute,并为该属性选择稳定的 `subject-type`。属性缺失、多值或空值时登录 fail closed。
不要把 email、display name 或临时用户名当作稳定 Subject。
## 4. 验证
1. 匿名请求登录方法目录:
```bash
curl -fsS 'https://skills.example.com/api/v1/auth/methods'
```
只在 Provider 为 `READY` 时应出现 `CAS_REDIRECT`。
2. 在浏览器点击 CAS 登录,确认重定向到 CAS `/login`,其 `service` 解码后与配置的
callback 加一次性 `state` 完全一致。
3. 完成 CAS 登录,确认返回原始 `returnTo` 或默认页面,并能请求
`/api/v1/auth/me`。
4. 再次请求相同 callback,必须得到 `casInvalidState` 或 CAS Ticket 验证失败,不能建立
第二个 Session。
5. 检查应用日志中不包含 `ST-` Ticket、validation URL、上游完整响应或用户属性。
6. 禁用 Provider 后,方法目录不再显示 CAS,且点击旧 URL 不应连接 CAS Server。
7. 在账号安全页验证 CAS Identity Link:已绑定 CAS 可以完成 fresh reauthentication,
READY intent 可以通过目标 CAS 创建 Binding;失败后应回到同一 intent,成功后不能重放。
建议分别验证 CAS 2 XML、CAS 3 JSON、CAS 3 XML fallback、无效 Ticket、错误 Service、
超时、TLS 失败、XXE、超大响应和 Redis 不可用。
CAS callback 按协议会在 query string 中携带一次性 `ticket` 和 `state`。官方 Web
镜像不记录 query string 或 Referer,应用请求日志会对它们进行隐藏;如果前面还有
Ingress、负载均衡、WAF、APM 或其他反向代理,必须将该 callback 路径的 query string
和 Referer 关闭记录或至少对 `ticket`、`state` 脱敏。上线前应使用唯一测试 Ticket
检查完整日志链路,确认没有原文残留。重复 state 会分类为 `REPLAY_DETECTED` 并写入
不含 state/Ticket 的安全审计。
## 5. 升级与回滚
- 新配置默认关闭,不影响现有本地密码、GitHub、GitLab 或 OIDC 登录。
- 本次接入不修改 `PlatformPrincipal` 或现有 Session 序列化结构。
- 旧版本回滚后会忽略 CAS 环境变量;现有非 CAS 登录仍可使用。
- CAS 已产生 Binding 后回滚会暂时失去 CAS 登录入口,但不会删除账号、Binding 或业务
数据。
- 修改 CAS endpoint 可以保持同一 Authority;修改 Authority 必须走统一身份设计中的
显式 Authority 迁移流程。
## 6. 安全实现说明
实现阶段验证了 Spring Security CAS 所依赖的 Apereo Java CAS Client。其 4.1.1
`AbstractUrlBasedTicketValidator` 仍会在 DEBUG 记录包含 Ticket 的 validation URL 和完整
响应;CAS 2 自定义属性解析路径也没有满足本项目要求的全部 XML parser fail-closed
设置。因此当前实现没有调用该库的 URL validator,而是使用受限 JDK HTTP transport 和
严格 parser:
- 禁止 redirect;
- 明确连接、请求和响应大小上限;
- XML 禁止 DOCTYPE、外部实体、外部 DTD 和外部 schema;
- Ticket、完整响应和属性不写日志或异常;
- CAS 3 优先 JSON,并支持受限 XML fallback。
上游参考:
- [Spring Security CAS reference](https://docs.spring.io/spring-security/reference/servlet/authentication/cas.html)
- [Apereo validator logging](https://github.com/apereo/java-cas-client/blob/cas-client-4.1.1/cas-client-core/src/main/java/org/apereo/cas/client/validation/AbstractUrlBasedTicketValidator.java)
- [Apereo CAS 2 attribute parser](https://github.com/apereo/java-cas-client/blob/cas-client-4.1.1/cas-client-core/src/main/java/org/apereo/cas/client/validation/Cas20ServiceTicketValidator.java)
如果上游版本修复这些问题,可以在保持错误分类、响应上限和无敏感日志测试的前提下重新
评估替换 transport/parser。