OAuth
通过标准 OAuth 2.1,让用户用 Atli 账户登录你的客户端,并由客户端代表他们操作:支持发现、PKCE、轮换的刷新令牌和撤销。任何客户端都能使用:应用、机器人、网站、服务器或 AI 助手。
何时使用
AI 助手就是这样一种客户端。它们连接 Atli 的 MCP 服务器后,会自行以这种方式让用户登录,因此你无需为它们构建任何东西。所有客户端都使用同一个流程。
任何代表用户操作的客户端:手机应用、网站、机器人或服务器,用用户自己的 Atli 账户向其展示 eSIM 或为其购买。注册一个你自己的客户端,它的令牌即可用于 Atli 的 API。
通过你拥有的客户端,或在 MCP 服务器上你的合作伙伴地址登录的用户,可以作为你的推荐用户关联到你。此功能默认关闭,Atli 可以为你的账户开启。无论是否开启,你的客户端都可以自己把它带来的用户记在你名下:请参阅下文的“把你带来的用户记在你名下”。
只有能在别处再次登录的账户才能连接:有电子邮箱的账户(包括通过 Google、Apple 或 Facebook),或使用 Telegram 或 WhatsApp 的账户。
工作原理
授权服务器是 Atli 的 API。登录和授权同意页面位于 heyatli.com 的 /connect,因此你的应用永远不会看到密码。请从元数据中读取每个端点,不要写死在代码里。本页的地址会随 沙盒 和 生产环境 切换而变化。
先注册一次客户端。然后对每个用户:带着 PKCE challenge 将其引导到授权端点,接收其返回时带回的授权码,用它换取令牌,再用访问令牌调用 API。令牌过期时刷新,用户退出登录时撤销。
curl https://api.heyatli.com/.well-known/oauth-authorization-server
{
"issuer": "https://api.heyatli.com",
"authorization_endpoint": "https://api.heyatli.com/oauth2/authorize",
"token_endpoint": "https://api.heyatli.com/oauth2/token",
"revocation_endpoint": "https://api.heyatli.com/oauth2/revoke",
"registration_endpoint": "https://api.heyatli.com/oauth2/register",
"jwks_uri": "https://api.heyatli.com/oauth2/jwks",
"scopes_supported": ["api", "offline_access"],
"code_challenge_methods_supported": ["S256"],
"client_id_metadata_document_supported": true,
…
}令牌的用途
每个请求都通过 resource(RFC 8707)指明其令牌所针对的地址,令牌只在该地址有效。不在以下列表中的地址,或不存在的推荐码,都会以 invalid_target 被拒绝,因此两者都无法被猜出。
| 地址 | 谁可以申请 | 令牌的作用 |
|---|---|---|
Atli 的 API https://api.heyatli.com | 仅限你拥有的客户端 | 用于 API 的 Bearer 令牌,以该用户的身份操作,与 |
Atli 的 MCP 服务器 https://mcp.heyatli.com | 任何客户端 | 让助手通过 MCP 工具代表用户操作。 |
你的合作伙伴地址 https://mcp.heyatli.com/r/YOUR_CODE | 任何客户端 | 同上,并且在你的账户开启此功能后,用户可以作为你的推荐用户关联到你。 |
注册客户端
有三种方式。如果是你自己的应用,请注册你自己的客户端:只有这种客户端可以申请 Atli 的 API。
你自己的客户端
以你自己的身份登录后,调用
POST /user/oauth-clients。最多可以有 10 个。对于运行在他人设备上的程序(例如手机应用或用户浏览器中的网页),选择public;对于能安全保管密钥的服务器,选择confidential。机密客户端的
client_secret只出现在这次响应中,别处都没有。如果丢失,请用POST /user/oauth-clients/{id}/rotate-secret替换。你的账户需要有电子邮箱,因为客户端需要一个推荐码;如果你还没有推荐码,系统会为你创建一个。curl https://api.heyatli.com/user/oauth-clients \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "Travel Bot", "redirect_uris": ["https://bot.example/oauth/callback"], "client_type": "confidential" }' { "client_id": "atli_client_7f3a9c2e…", "client_secret": "…", "client_type": "confidential", … }动态注册
POST /oauth2/register(RFC 7591),无需登录。大多数助手都是这样注册的。同一地址每小时最多注册 10 次,从未有人连接过的客户端会在 30 天后被删除。curl https://api.heyatli.com/oauth2/register \ -H 'Content-Type: application/json' \ -d '{ "client_name": "Travel Bot", "redirect_uris": ["https://bot.example/oauth/callback"] }' { "client_id": "atli_client_…", "token_endpoint_auth_method": "none", … }客户端 ID 元数据文档
使用你托管的一个
https地址作为client_id,在该地址提供描述客户端的 JSON 文档。有人登录时 Atli 会读取它,因此无需注册。MCP 规范更推荐这种方式,而不是动态注册。// Served at https://bot.example/oauth/client.json { "client_id": "https://bot.example/oauth/client.json", "client_name": "Travel Bot", "redirect_uris": ["https://bot.example/oauth/callback"], "token_endpoint_auth_method": "none" }
重定向 URI 需要完全匹配。每个都必须是
https,或者是localhost、127.0.0.1或[::1]上的http(此时只有端口可以不同),且不能带片段。一个客户端最多可有 10 个。
让用户登录
带 PKCE(S256)的授权码流程,所有客户端都使用它,无论公开还是机密。
生成一对 PKCE 值
保留 verifier 用于授权码交换,并在请求中发送其 SHA-256 challenge。每次登录都生成新的一对值和新的
state。const base64url = (bytes) => btoa(String.fromCharCode(...bytes)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const verifier = base64url(crypto.getRandomValues(new Uint8Array(32))); const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)); const challenge = base64url(new Uint8Array(digest)); const state = base64url(crypto.getRandomValues(new Uint8Array(16)));引导用户去登录
在用户的浏览器中打开授权端点。
scope为api,加上offline_access会额外返回刷新令牌。用户在 heyatli.com/connect 登录并同意,该页面会显示你的客户端名称、它会把用户送回哪里,以及用户是否会成为某人的推荐用户。请求的有效期为 10 分钟。const url = new URL('https://api.heyatli.com/oauth2/authorize'); url.search = new URLSearchParams({ response_type: 'code', client_id: CLIENT_ID, redirect_uri: 'https://bot.example/oauth/callback', scope: 'api offline_access', state, code_challenge: challenge, code_challenge_method: 'S256', // Atli's API, for a client you own; or the MCP server, https://mcp.heyatli.com resource: 'https://api.heyatli.com', }).toString(); window.location.assign(url);接收用户带回的授权码
用户会带着
code、你的state和iss返回你的重定向 URI。请检查state是你发出的,并且iss是签发方。如果用户拒绝,返回的会是error=access_denied。https://bot.example/oauth/callback?code=4bd81f…&state=…&iss=https%3A%2F%2Fapi.heyatli.com https://bot.example/oauth/callback?error=access_denied&state=…&iss=https%3A%2F%2Fapi.heyatli.com用授权码换取令牌
发送授权码,同时附上 verifier、相同的
redirect_uri和resource,以及客户端的凭据(如果有)。授权码有效期为 60 秒且只能使用一次:重复发送或使用错误的 verifier 发送,都会终止该用户的访问权限,因为授权码可能已落入他人之手。访问令牌的有效期为
expires_in秒,即 15 分钟。刷新令牌总会一并返回。curl https://api.heyatli.com/oauth2/token \ -u 'CLIENT_ID:CLIENT_SECRET' \ -d grant_type=authorization_code \ -d code=THE_CODE \ -d code_verifier=THE_VERIFIER \ -d redirect_uri=https://bot.example/oauth/callback \ -d client_id=CLIENT_ID \ -d resource=https://api.heyatli.com { "access_token": "…", "token_type": "Bearer", "expires_in": 900, "refresh_token": "…", "scope": "api offline_access" }
保持登录状态
访问令牌过期后,用刷新令牌申请新的。你可以申请更窄的 scope,但不能申请更宽的。
刷新令牌每次使用后都会变化,所以每次都要保存新的令牌。如果某次响应没有送达,在短时间内再次发送旧令牌会得到相同的响应。超过这段时间后,旧令牌就意味着它被复制过,该用户的访问权限将被终止。
curl https://api.heyatli.com/oauth2/token \
-u 'CLIENT_ID:CLIENT_SECRET' \
-d grant_type=refresh_token \
-d refresh_token=THE_REFRESH_TOKEN \
-d client_id=CLIENT_ID \
-d resource=https://api.heyatli.com使用令牌
以 Authorization: Bearer ACCESS_TOKEN 的形式发送,只发送到该令牌签发时针对的地址,不要发往别处。
用于 Atli API 的令牌在所有 API 路由上都以该用户的身份操作,与 /login 返回的令牌一样。请将其视为不透明的值:其内容可能会变化。
用于 MCP 服务器的令牌是使用 EdDSA 签名的 JWT。服务器根据其 kid,用 /oauth2/jwks 中的密钥进行验证,并检查其 aud 是否为自己的地址。令牌中不含电子邮箱或其他个人信息:只有 sub(用户的 ID)、scope、grant_id 和各项时间。
curl https://api.heyatli.com/esims \
-H 'Authorization: Bearer ACCESS_TOKEN'把你带来的用户记在你名下
将通过你的客户端或在你的合作伙伴地址登录的用户关联到你,默认是关闭的,Atli 可以为你的账户开启。无论是否开启,你的客户端都可以自己用已登录用户的访问令牌,把该用户关联到你的推荐码:调用 POST /user/referral/redeem 并传入你的 code。
result 说明结果:ok 表示已关联,already 表示对方已有推荐人,notnew 表示对方以前购买过,own 表示这是你自己的推荐码,notfound 表示推荐码不存在。只有没有推荐人且尚未购买的用户才能被关联,并且只能关联一次。
curl https://api.heyatli.com/user/referral/redeem \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{ "code": "YOUR_CODE" }'
{
"success": true,
"result": "ok",
"message": "…",
"discount_percent": 10
}退出登录并撤销访问
当用户退出你的应用时,撤销其刷新令牌。这会终止该用户在你客户端中的访问权限,连同其下的所有令牌。即使是它不认识的令牌,也会返回 200,因此无法借此探测哪些令牌存在。
用户也可以自行查看已连接的应用并撤销其访问权限(GET /user/oauth-grants、DELETE /user/oauth-grants/{id})。此后你的客户端将无法获取新令牌,因此请将刷新被拒视为已退出登录。
curl https://api.heyatli.com/oauth2/revoke \
-u 'CLIENT_ID:CLIENT_SECRET' \
-d token=THE_REFRESH_TOKEN \
-d client_id=CLIENT_ID错误
令牌、撤销和注册端点使用 OAuth 自己的错误格式 {"error": "invalid_grant", "error_description": "…"} 进行响应,而不是 API 通常的格式。请根据 error 进行分支判断,不要依据描述文字。令牌端点的每个响应都带有 Cache-Control: no-store。
invalid_grant:授权码已被使用或已过期,verifier 不正确,或刷新令牌在宽限期过后被再次使用。请让用户重新登录。invalid_target:resource不是令牌可以针对的地址,或指向不存在的推荐码。invalid_client:客户端凭据错误或缺失。所有类型的客户端都以同样的方式失败。invalid_scope:刷新请求申请的范围超出了用户允许的范围。