Metadata-Version: 2.5
Name: blade-auth-client
Version: 0.4.23
Summary: Shared Casdoor authentication client for Blade ecosystem services.
Project-URL: Homepage, https://pypi.org/project/blade-auth-client/
Author-email: yangliu35 <so.2liu@gmail.com>
License-Expression: MIT
Requires-Python: >=3.12
Requires-Dist: httpx
Requires-Dist: pydantic-settings
Requires-Dist: pydantic>=2
Requires-Dist: pyjwt[crypto]
Requires-Dist: pyyaml
Requires-Dist: sqlalchemy
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Requires-Dist: respx; extra == 'dev'
Requires-Dist: ruff<0.12,>=0.11; extra == 'dev'
Provides-Extra: fastapi
Requires-Dist: fastapi; extra == 'fastapi'
Requires-Dist: python-multipart; extra == 'fastapi'
Requires-Dist: starlette; extra == 'fastapi'
Provides-Extra: socketio
Requires-Dist: python-socketio; extra == 'socketio'
Description-Content-Type: text/markdown

# blade-auth-client

`blade-auth-client` 是 Blade 生态的共享 Casdoor 认证客户端包。它抽离了统一的 token 校验、FastAPI 装配和 Socket.IO 握手接口，供各个服务复用。

## 安装

```bash
pip install blade-auth-client[fastapi,socketio]
```

## Casdoor 配置要求

Casdoor 的 application、scope 和 redirect URI 约束见 [docs/casdoor-setup.md](docs/casdoor-setup.md)。

## 快速上手(0.4.0+)

```python
from contextlib import asynccontextmanager

from fastapi import FastAPI, Depends
from blade_auth_client import AuthConfig, BladeAuth, CasdoorClaims, LazyProvisioner

class MyProvisioner(LazyProvisioner["MyUser"]):
    async def provision_user(self, claims: CasdoorClaims) -> "MyUser":
        # 首次登录:建用户并绑定 claims.sub;非首次:查出来刷字段
        ...

auth = BladeAuth(
    AuthConfig.from_yaml("configs/oauth_config.yaml"),
    provisioner=MyProvisioner(),
)

@asynccontextmanager
async def lifespan(app):
    try:
        # Optional discovery/prewarm belongs inside try, so startup failures also close the pool.
        yield
    finally:
        await auth.aclose()

app = FastAPI(lifespan=lifespan)
app.include_router(auth.router, prefix="/api/v1/auth")

@app.get("/me")
def me(user = auth.require()):
    return user
```

详细流程见 [`docs/接入指南.md`](docs/接入指南.md);多 app 共享鉴权 / BladeAuth 设计背景见 [`docs/设计/设计-API暴露面收敛.md`](docs/设计/设计-API暴露面收敛.md)。

### HTTP 资源所有权

`BladeAuth` 默认首次 HTTP 请求时创建一个池，JWKS、OIDC、PAT 和撤销校验共用。
应用停止接收请求并等待在途请求完成后调用 `await auth.aclose()`；它先停止自有
JWKS 后台刷新，再关闭池，重复调用安全。初始化不分配池；discovery/prewarm 失败
仍需在 `finally` 中关闭。Mock 模式不创建网络客户端。

注入 `http_client=client` 时，全部路径借用该实例和它的默认超时，SDK 不关闭它。
独立构造 `OidcClient`、`JwksCache`、`TokenVerifier` 的自有池同样需要 `aclose()`；
向 `TokenVerifier` 注入的 `jwks_cache` / `revocation` 仍由调用方管理。
独立 `SessionClient`、`OrgClient`、PAT 和撤销检查器未注入 client 时继续按请求创建
并自动关闭，已有调用者无需增加关闭代码；需复用池时注入应用管理的 client。
不调用关闭入口会让自有连接一直保留到进程退出。

### 从 0.3.x 升级

- 推荐迁到 `BladeAuth` facade(上面那段)。老零件(`OidcClient` / `TokenVerifier` / `create_auth_router` / `make_require_auth_dep` / 等)0.4.0 仍可从顶层 import,但会 `DeprecationWarning`,0.5.0 移除。
- `LazyProvisioner.ensure_user` → **`provision_user`**。老名字继续可用且 SDK 内部自动回落,调用时 warn 一次。

## Web 会话与组织

系统里有两类长期凭据，生命周期不同，别混用：

| | web 令牌 | PAT |
|---|---|---|
| 是什么 | 密码登录换来的 RS256 JWT | 不透明的 `sk-blade-v3-…` |
| 代表 | *某人这次登录* | *某人这个身份* |
| `logout` 后 | 立即失效 | 不受影响 |
| 适合 | 网页 / 客户端会话 | 脚本、服务间调用 |

会话用 web 令牌、自动化用 PAT。拿 PAT 当会话使，用户点了登出也退不掉；拿 web
令牌做自动化，用户一登出你的定时任务就断了。

```python
from blade_auth_client import SessionClient, OrgClient
from blade_auth_client.session import AccountLockedError
from blade_auth_client.errors import InvalidTokenError

sc = SessionClient("http://blade-oauth:19000")

try:
    token = await sc.login("alice", "secret")
except AccountLockedError:      # 429：连续失败被冻结
    ...
except InvalidTokenError:       # 401：账号或密码不对
    ...

me = await sc.me(token.access_token)     # SessionUser（含 source / source_id）
await sc.logout(token.access_token)      # 服务端拉黑，之后一律 401
```

`logout` 只作用于传入的这一枚令牌：同一用户在别处的会话、以及他的 PAT 都不受
影响。需要「所有设备下线」用管理端的 `DELETE /api/v1/users/{id}/sessions`。
重复登出不报错——登出表达的是意图，令牌本来就无效同样满足这个意图。

本地口令校验不过时，blade-oauth 会转发给管理员配置的外部认证源，认证通过即自动
建号；对调用方没有区别，拿到的始终是 blade-oauth 自己签的令牌。`SessionUser.source`
告诉你这个账号从哪来（`admin_api` / `bootstrap` / `external` / `legacy`）。

```python
oc = OrgClient("http://blade-oauth:19000")

roster = await oc.my_members(token)              # 我同组织的人，普通用户可调
orgs = await oc.list(admin_token)                # 需要 admin
org = await oc.get(admin_token, "org-a")
members = await oc.members(admin_token, "org-a")
```

`my_members` 返回**脱敏视图**——`OrgMember` 刻意没有邮箱、手机、额度等字段，
避免花名册被当成全公司通讯录来爬。注意判断 `OrgRoster.truncated`：花名册上限
500 人，服务端会如实告诉你名单是否被截断。

## 托管素材

配置中心存 `asset_id`，不要存 OAuth 的公开 IP。项目后端用 service key 拉字节，再在自己的源上代理给浏览器：

```python
from blade_auth_client import AuthConfig, BladeAuth

auth = BladeAuth(AuthConfig.from_yaml("oauth_config.yaml"), provisioner=MyProvisioner())

# 推荐：面向浏览器的代理使用硬上限读取。SDK 会流式累计并在超限时立即关闭响应。
limited = await auth.assets.read_limited(asset_id, max_bytes=10 * 1024 * 1024)
body = limited.content
etag = limited.etag

# 或者：调用方需要自行管理响应时使用 open()，不要与 read_limited() 重复调用。
response, meta = await auth.assets.open(asset_id)  # oauth.service_key 需要 asset:read
try:
    body = await response.aread()
finally:
    await response.aclose()

config = await auth.assets.public_config("agent")
asset_id = config.assets["logo_url"].id
product_name = config.values.get("product_name")
```

服务名支持 `oauth`、`os`、`agent`、`hub`、`gateway`。返回动态的 `values` 和
`assets` manifest；具体键由配置 schema 中的 `public: true` 控制，SDK 不枚举业务字段。
共享的 `branding.*` 配置会被同名的服务配置覆盖。

`oauth.service_key` 配在 yaml 的 `oauth:` 段，打 `/api/v1/internal/assets/:id`。

## 版本策略

`0.0.x` 版本仅用于占位发布和联调验证，不承诺可用性。`0.2.0` 起采用仅校验 `iss` + 签名 + `exp` 的简化策略。

## 发布

仓库已经预留了 GitHub Actions 发布流水线：

- PR 校验：`.github/workflows/python-sdk-ci.yml`
- PyPI 发布：`.github/workflows/python-sdk-publish.yml`

首次配置 Trusted Publishing 时，在 PyPI 项目的 Publishing 页面添加 GitHub publisher，填写：

- Owner: `blade-hq`
- Repository name: `blade-oauth`
- Workflow name: `python-sdk-publish.yml`

日常发版流程：

```bash
# 1. 修改 sdk/python/pyproject.toml 里的 version

# 2. 推送代码到默认分支后，创建并推送同版本 tag
git tag blade-auth-client-vX.Y.Z
git push origin blade-auth-client-vX.Y.Z
```

发布 workflow 会校验 tag 版本与 `pyproject.toml` 一致，然后自动构建并发布到 PyPI。
