Skip to main content

theid 2.0

身份客户端 for devnull.cn:用 access_key / secret_key 换取 短时 EdDSA JWT。与 AWS 的 AK/SK 模型一致 —— secret key 不上网、只用于签名;与 AWS SigV4 的区别是签名的产物是一次性断言(assertion),换来的是可缓存的 Bearer 令牌。

服务端契约见 identity/docs/auth-2.0.md。

client                          identity                      resource service
  │  POST /oauth/token            │                                  │
  │  assertion = HS256(JWT, sk)   │                                  │
  ├──────────────────────────────▶│  ① 验签 + jti 单次消费           │
  │◀──────────────────────────────┤  ② EdDSA access token (15m)      │
  │  Authorization: Bearer ...    │                                  │
  ├─────────────────────────────────────────────────────────────────▶│ ③ 本地验签(JWKS 公钥)

安装

pip install theid              # 客户端:只有 requests
pip install "theid[verify]"    # 资源服务:额外装 PyJWT[crypto] 用于本地验签

凭据

优先级:显式参数 > 环境变量 > ~/.devnull/credentials。

export THEID_ACCESS_KEY=DK...
export THEID_SECRET_KEY=...
# ~/.devnull/credentials   (chmod 600)
[default]
access_key = DK...
secret_key = ...

没有任何内置兜底凭据:缺失时抛 CredentialsError,不会静默降级。 文件权限宽于 0600 会得到一条 warnings.warn。

使用

import theid

client = theid.Client()                 # 读环境变量或凭据文件
headers = client.authorization_headers()
# {'Authorization': 'Bearer eyJ...', 'Content-Type': 'application/json'}

client.whoami()                         # {'account_id': '016869757', 'roles': ['user'], ...}
client.get('/me')                       # 自动带 token;401 时刷新一次
client.token(force_refresh=True)        # 强制换新

令牌在进程内缓存,到期前 60 秒(或 TTL 的 1/4,取小者)才重新换取;多线程安全。 换 token 的 POST 在 429/5xx/网络错误上按指数退避重试(默认 2 次),每次重试都重新签名 一个新的 jti,所以不会撞上服务端的重放检测。

显式指定

client = theid.Client(
    access_key='DK...',
    secret_key='...',
    base_url='https://devnull.cn/identity',   # 断言 audience 默认同此
    audience='thedns',                        # 令牌 aud
    scope='profile:read device:read',         # 只能下放,不能提权
    timeout=5.0,
    retries=2,
)

模块级快捷方式

import theid
theid.token()
theid.authorization_headers()

进程内单例,行为与 Client() 相同。

资源服务:本地验签

from theid import JWKSClient

jwks = JWKSClient()          # 全局建一次:公钥缓存 300s,未知 kid 触发一次刷新

def require_auth(request):
    token = request.headers['Authorization'].split(' ', 1)[1]
    claims = jwks.verify(token, audience='thedns', issuer='https://devnull.cn/identity')
    if 'device:read' not in claims['scope'].split():
        raise PermissionError
    return claims['sub']     # account_id,或 'device:<device_id>'

verify_token() 是同样功能的一次性便捷函数。注意 必须显式传 issuer(生产环境), 否则 base_url 配错会扩大信任面。

Go 服务端可用任意 JWKS 库(MicahParks/keyfunc 或 lestrrat-go/jwx): 只需 GET /.well-known/jwks.json,算法 EdDSA,校验 iss/aud/exp/nbf。

错误

异常 何时 是否重试
CredentialsError 没配凭据 / 只配了一半 / 文件格式错 否
AuthenticationError 401/403:签名错、断言重放、token 过期、账号不存在 否(换凭据/修时钟)
ScopeError 400 invalid_scope:请求了没授予的 scope 否
RequestError 400/422:请求格式错 否
RateLimitError 429 且重试耗尽 是(看 retry_after)
ServiceError 5xx 或响应体不合法 是
NetworkError DNS/TLS/超时/连接重置 是
InvalidTokenError 验签失败(资源服务侧) 否

所有异常都继承 TheIDError。str(exc) 里带服务端的稳定错误码,但服务端不会告诉你 access_key 是否存在——这是刻意的。

主机信息(可选)

theid.host.system_id()     # sha256(/etc/machine-id);macOS 用 IOPlatformUUID
theid.host.hostname()
theid.host.private_ip()    # 无路由时返回 None,不再抛异常
theid.host.is_private_ip('fd00::1')   # 支持 IPv6;非法输入抛 ValueError
theid.host.public_ip(client)          # 走 identity 的 /ip,不再依赖 ipinfo

从 1.x 迁移

1.x 的 API 大部分已删除,因为它们的语义不可靠:

1.x 2.0
硬编码兜底凭据 删除;缺凭据直接报错
token() 失败返回 {'rc':205} 抛 AuthenticationError
每次调用都重新登录、无超时 缓存 + 超时 + 退避重试
nickname()(返回明文密码) 删除
ip_regulated()(硬编码 ipinfo token、失败即 False) 删除;public_ip() 改走 identity
is_private_ip() 只支持 IPv4、非法返回 False 支持 v6,非法抛 ValueError
system_id() = md5(machine-id 含换行) sha256(machine-id strip)
email() 删除(账号只有 9 位 account_id,不再收集邮箱)

1.x 老账号需要先轮换一次凭据才能用 2.0(服务端当时只存了哈希,无法反推 SK)。 新注册的账号本身就是 2.0 凭据,跳过这一步:

python3 - <<'EOF'
import hashlib, requests
ak, sk = '你的旧 AK', '你的旧 SK'
password = hashlib.sha256((hashlib.sha256(ak.encode()).hexdigest()
                           + hashlib.sha256(sk.encode()).hexdigest()).encode()).hexdigest()
r = requests.post('https://devnull.cn/identity/keys/rekey',
                  json={'access_key': ak, 'password': password}, timeout=10)
print(r.status_code, r.json())   # 新 AK/SK 只显示这一次
EOF

发布

2.0 是破坏性升级,发布前必须处理依赖它的三个包(thedns/theca/meshing 在模块顶层 import 了已删除的 email()/ip_regulated())。完整检查清单见 docs/RELEASE-2.0.md。

开发

python3 -m unittest discover -s tests -v

测试全部离线:HTTP 层是一个 requests.Session 形状的桩,断言/缓存/重试/错误映射都被覆盖。 与真实服务端的端到端契约测试在 identity 仓库的 test/test_e2e_sdk.py。

Metadata

Release files for theid 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for theid 2.0.0
File Size Uploaded Provenance
theid-2.0.0.tar.gz 23.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for theid 2.0.0
File Interpreter ABI Platform
theid-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 41.9 kB

Release files / theid-2.0.0.tar.gz

Download URL theid-2.0.0.tar.gz
Size 23.9 kB
Tags Source
SHA-256 checksum
How to use checksums
4729e8d01020679c74594c4df5339888ea0856772e77ab010492a068eff46118
BLAKE2b-256 checksum
How to use checksums
cf709b041c4d8f55f97b7619fdbb6968896a6ba3bdcf3eb35f386356ed8d3c28
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / theid-2.0.0-py3-none-any.whl

Download URL theid-2.0.0-py3-none-any.whl
Size 18.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
343d9261ffab09b585b855bbdb34dbc998e2d626fcceda0af4fd2f897723ae3f
BLAKE2b-256 checksum
How to use checksums
1db6f382f13b7c85de4127b36895ef9d236ba658ceb47161924952b593f0b4c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for theid 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page