TokenToSea开发者文档
控制台

OAuth 与 OIDC

授权码 + PKCE 流程的完整参考:发现文档、授权请求、令牌换取与刷新、ID Token 校验、UserInfo、撤销、错误与限流。

本页内容

本页按调用顺序说明每个端点。所有地址都以 Issuer https://tokentosea.com/api/oauth 为前缀。完整可运行的服务端示例见完整示例。

前提:已在 控制台 → 开发者生态 登记应用,拿到 Client ID(机密客户端另有客户端密钥),并登记了回调地址。

流程#

  1. 生成本次登录的随机值

    为每次登录生成 state、nonce 与 PKCE code_verifier,保存在你的服务端会话中。

  2. 跳转到授权地址

    浏览器打开 /authorize,平台把用户带到 TokenToSea 授权页。用户登录(需已绑定手机号)、确认权限并设置预算。

  3. 接收回调

    平台跳回你的 redirect_uri,带上 code、state 与 iss。核对 state 与 iss。

  4. 换取令牌

    服务端用 code + code_verifier 调用 /token,得到访问令牌、刷新令牌与 ID Token。

  5. 校验身份并调用接口

    校验 ID Token 的签名与声明,用 iss + sub 关联你的本地账号;用访问令牌调用委托接口,过期前用刷新令牌续期。

发现文档#

GET /.well-known/openid-configuration

响应
{
  "issuer": "https://tokentosea.com/api/oauth",
  "authorization_endpoint": "https://tokentosea.com/api/oauth/authorize",
  "token_endpoint": "https://tokentosea.com/api/oauth/token",
  "userinfo_endpoint": "https://tokentosea.com/api/oauth/userinfo",
  "jwks_uri": "https://tokentosea.com/api/oauth/jwks",
  "revocation_endpoint": "https://tokentosea.com/api/oauth/revoke",
  "response_types_supported": ["code"],
  "response_modes_supported": ["query"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["openid", "profile", "email", "offline_access", "tools:read", "tools:invoke", "models:read", "models:invoke", "wallet:read", "grant:limits"],
  "authorization_response_iss_parameter_supported": true,
  "claims_supported": ["iss", "sub", "aud", "exp", "iat", "auth_time", "nonce", "name", "preferred_username", "email", "email_verified"]
}

授权请求#

GET /authorize

在浏览器中打开,参数放在查询字符串里:

参数必填说明
response_type必填固定为 code
client_id必填应用的 Client ID
redirect_uri必填必须与登记的某个回调地址完全一致
scope必填空格分隔,必须是应用已登记 scope 的子集,不能重复
state必填16–512 个字符的随机值,用于防 CSRF。回调时原样返回
code_challenge必填BASE64URL(SHA256(code_verifier)),43 个字符
code_challenge_method必填固定为 S256
nonce可选(使用 openid 时建议)1–512 个字符,原样写入 ID Token,用于防重放
prompt可选只接受 consent。每次授权都需要用户在授权页明确确认

code_verifier 是 43–128 个字符的随机串(A–Z a–z 0–9 - . _ ~)。

成功时返回 302,跳转到 TokenToSea 授权页(https://tokentosea.com/app/#/authorize?request=…)。授权请求在 10 分钟内有效。

参数错误时不会跳回你的回调地址,而是直接在浏览器中返回 400 JSON(避免把错误发往未经验证的地址):

响应原因
{"error":"invalid_client"}client_id 不存在,或应用已停用
{"error":"invalid_request", "error_description": "Unregistered redirect URI"}回调地址未登记
{"error":"invalid_request", "error_description": "Authorization code with S256 PKCE is required"}response_type 不是 code,或缺少有效的 S256 PKCE 参数
{"error":"invalid_request", "error_description": "Requested authentication constraints are not supported"}使用了 max_age、acr_values、id_token_hint、claims 或非 query 的 response_mode
{"error":"invalid_request", "error_description": "Only explicit consent is supported"}prompt 不是 consent
{"error":"invalid_scope"}scope 不合法或超出应用登记范围
message 为 invalid_redirect_uriredirect_uri 格式不合法
message 为 tool_invalid_state / tool_invalid_nonce / tool_invalid_client_idstate、nonce 或 client_id 缺失或长度不符

回调#

用户确认后,浏览器被重定向到:

https://your-app.example.com/callback?code=tts_code_…&state=<你的 state>&iss=https%3A%2F%2Ftokentosea.com%2Fapi%2Foauth

用户拒绝时为 ?error=access_denied&state=…&iss=…。

收到回调后:

  1. 核对 state 与会话中保存的值一致(使用常数时间比较),并在一次使用后作废;
  2. 核对 iss 等于 https://tokentosea.com/api/oauth;
  3. 有 error 时停止;
  4. 在 60 秒内用 code 换取令牌。

换取令牌#

POST /token

请求体可以是 application/x-www-form-urlencoded 或 JSON。

参数必填说明
grant_type必填authorization_code
code必填回调中的 code
redirect_uri必填与授权请求中的值完全一致
code_verifier必填生成 code_challenge 时使用的原始值
client_id必填Client ID(使用 Basic 认证时可省略)
client_secret机密客户端用 client_secret_post 时放在请求体中;也可改用 Authorization: Basic base64(client_id:client_secret)。两种方式不能同时使用。公共客户端不能发送密钥
响应
{
  "access_token": "tts_at_…",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "openid profile offline_access tools:read tools:invoke",
  "refresh_token": "tts_rt_…",
  "id_token": "eyJhbGciOiJSUzI1NiIs…"
}
字段说明
access_token访问令牌,用于 Authorization: Bearer
expires_in剩余有效秒数,最多 900;授权即将到期时会更短
scope用户实际授予的 scope,可能少于你申请的
refresh_token仅在授予 offline_access 时返回
id_token仅在授予 openid 时返回

响应带有 Cache-Control: no-store。请把令牌保存在服务端,不要写入日志或浏览器存储。

curl https://tokentosea.com/api/oauth/token \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://your-app.example.com/callback \
  -d code_verifier="$CODE_VERIFIER" \
  -d client_id="$CLIENT_ID" \
  -d client_secret="$CLIENT_SECRET"

刷新令牌#

参数必填说明
grant_type必填refresh_token
refresh_token必填最近一次拿到的刷新令牌
client_id / client_secret同上客户端认证方式与换取令牌时相同

响应格式同上,包含新的刷新令牌。旧刷新令牌立即失效;再次使用旧令牌会撤销整个授权,用户需要重新授权。因此:

  • 同一个授权的刷新请求要串行执行;
  • 收到响应后原子地替换访问令牌和刷新令牌;
  • 刷新成功会把授权有效期顺延到 30 天后;
  • 授予了 openid 时,刷新响应也包含新的 ID Token,其 auth_time 仍是用户最初登录的时间,且不含 nonce。
cURL
curl https://tokentosea.com/api/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN" \
  -d client_id="$CLIENT_ID" \
  -d client_secret="$CLIENT_SECRET"

令牌端点错误#

状态码响应原因
401{"error":"invalid_client"}Client ID 不存在、应用已停用、密钥错误、公共客户端发送了密钥,或同时使用了 Basic 与请求体密钥
400{"error":"invalid_grant"}授权码无效、过期或已使用;redirect_uri 或 code_verifier 不匹配;刷新令牌无效、过期或已被使用;授权已撤销或过期;用户账号不可用
400{"error":"unsupported_grant_type"}grant_type 不是 authorization_code 或 refresh_token

授权码或刷新令牌被重复使用时,平台会撤销整个授权以防令牌泄露。

校验 ID Token#

ID Token 是 RS256 签名的 JWT。必须全部校验:

检查项要求
签名头部 alg 为 RS256,用 JWKS 中 kid 相同、use 为 sig 的公钥验证
iss等于 https://tokentosea.com/api/oauth
aud等于你的 Client ID
exp晚于当前时间(签发后 15 分钟内有效)
iat不晚于当前时间(允许少量时钟偏差)
nonce等于授权请求中发送的值
sub字符串,用户在 TokenToSea 的稳定标识

载荷示例:

ID Token 载荷
{
  "iss": "https://tokentosea.com/api/oauth",
  "sub": "cm1…",
  "aud": "你的 Client ID",
  "exp": 1792484100,
  "iat": 1792483200,
  "auth_time": 1792483150,
  "nonce": "…",
  "at_hash": "…",
  "name": "张三",
  "preferred_username": "zhangsan"
}

用 iss + sub 关联本地账号,不要用 email 作为账号的唯一依据。name、preferred_username 仅在授予 profile 时出现;email、email_verified 仅在授予 email 且账号有邮箱时出现。

JWKS#

GET /jwks

返回 { "keys": [{ "kty": "RSA", "n": "…", "e": "AQAB", "kid": "…", "use": "sig", "alg": "RS256" }] },响应可缓存 300 秒。遇到未知的 kid 时重新获取一次。

# pip install "PyJWT[crypto]"
import hmac

import jwt

ISSUER = "https://tokentosea.com/api/oauth"
jwks_client = jwt.PyJWKClient(ISSUER + "/jwks")


def verify_id_token(id_token: str, client_id: str, expected_nonce: str) -> dict:
    signing_key = jwks_client.get_signing_key_from_jwt(id_token)
    claims = jwt.decode(id_token, signing_key.key, algorithms=["RS256"], audience=client_id, issuer=ISSUER)
    if not hmac.compare_digest(claims.get("nonce", ""), expected_nonce):
        raise ValueError("nonce mismatch")
    return claims

用户信息#

GET /userinfo

需要 Authorization: Bearer <访问令牌>,且授权包含 openid。

响应
{ "sub": "cm1…", "name": "张三", "preferred_username": "zhangsan", "email": "user@example.com", "email_verified": true }

令牌无效返回 401 {"error":"invalid_token"};缺少 openid 返回 401 {"error":"insufficient_scope","scope":"openid"}。

撤销#

POST /revoke

参数说明
token访问令牌或刷新令牌
client_id / client_secret客户端认证,同令牌端点

撤销会终止该令牌所属的整个授权(包括其他已签发的令牌)。无论令牌是否存在都返回 200,客户端认证失败时返回 401 {"error":"invalid_client"}。用户退出你的应用、或要求解除绑定时调用它。

限流与跨域#

  • /authorize、/token、/revoke 与授权页使用的请求查询接口,按来源 IP 合计每分钟最多 120 次,超出返回 429 {"error":"temporarily_unavailable","error_description":"Too many authorization requests"}。
  • /token、/revoke、/userinfo、/jwks、发现文档与 /api/v1/connect/* 返回 Access-Control-Allow-Origin: *,允许的请求头为 Authorization、Content-Type、Idempotency-Key。公共客户端(例如单页应用)可以在浏览器中完成 PKCE 流程;机密客户端的密钥只能放在服务端。

安全清单#

  • 每次登录使用新的 state、nonce、code_verifier,只用一次;
  • 回调地址使用 HTTPS 并精确登记;
  • 客户端密钥、刷新令牌只保存在服务端,加密存储,不写日志;
  • 按ID Token 校验的全部条件验证后再建立会话;
  • 刷新令牌串行使用、原子替换;
  • 不再需要时调用 /revoke。

下一步#

    输入关键词搜索全部文档与模型。