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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| theid-2.0.0.tar.gz | 23.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.