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 チャレンジを付けて認可エンドポイントへ送り、戻ってきたときのコードを受け取り、トークンと交換し、アクセストークンで 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 | あなたが所有するクライアントのみ |
|
Atli の MCP サーバー https://mcp.heyatli.com | すべてのクライアント | アシスタントが MCP ツールを通じてユーザーに代わって操作できます。 |
あなたのパートナー用アドレス https://mcp.heyatli.com/r/YOUR_CODE | すべてのクライアント | 同上。加えて、あなたのアカウントでこの機能がオンになっていれば、ユーザーをあなたの紹介ユーザーとして紐づけられます。 |
クライアントを登録する
方法は 3 つあります。独自のアプリには、独自のクライアントを登録してください。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)で、サインインは不要です。ほとんどのアシスタントはこの方法で登録します。1 つのアドレスから 1 時間に 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(ポートのみ異なってもよい)で、フラグメントは付けられません。1 つのクライアントに最大 10 個まで登録できます。
ユーザーをサインインさせる
PKCE(S256)を使った認可コードフローです。パブリックでも機密でも、すべてのクライアントがこれを使います。
PKCE のペアを作る
verifier はコード交換のために保持し、その SHA-256 チャレンジをリクエストに付けて送ります。サインインのたびに新しいペアと新しい
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 秒で、使えるのは 1 回だけです。2 回送ったり、間違った 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 用トークンは、/login のトークンと同じく、すべての API ルートでユーザーとして動作します。中身は変わる可能性があるため、不透明なものとして扱ってください。
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 はコードが存在しないことを表します。紐づけられるのは、紹介者がおらずまだ購入していないユーザーだけで、1 回限りです。
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エラー
トークン、取り消し、登録の各エンドポイントは、API の通常のものではなく、OAuth 独自のエラー形式 {"error": "invalid_grant", "error_description": "…"} で応答します。分岐には説明文ではなく必ず error を使ってください。トークンエンドポイントのレスポンスにはすべて Cache-Control: no-store が付きます。
invalid_grant:コードが使用済みか期限切れ、verifier が間違っている、またはリフレッシュトークンが猶予期間を過ぎてから再使用されました。ユーザーにもう一度サインインしてもらってください。invalid_target:resourceがトークンの対象にできるアドレスではないか、存在しない紹介コードを指しています。invalid_client:クライアントの認証情報が間違っているか、ありません。どの種類でも同じように失敗します。invalid_scope:更新で、ユーザーが許可した範囲を超えて要求しました。