OAuth

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
あなたが所有するクライアントのみ

/login で得られるものと同じく、ユーザーとして動作する API 用の Bearer トークンです。

Atli の MCP サーバー
https://mcp.heyatli.com
すべてのクライアント

アシスタントが MCP ツールを通じてユーザーに代わって操作できます。

あなたのパートナー用アドレス
https://mcp.heyatli.com/r/YOUR_CODE
すべてのクライアント

同上。加えて、あなたのアカウントでこの機能がオンになっていれば、ユーザーをあなたの紹介ユーザーとして紐づけられます。

クライアントを登録する

方法は 3 つあります。独自のアプリには、独自のクライアントを登録してください。Atli の API を要求できるのはこの種類だけです。

  1. 独自のクライアント

    自分のアカウントでサインインした状態で 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",
      …
    }
  2. 動的登録

    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",
      …
    }
  3. クライアント 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)を使った認可コードフローです。パブリックでも機密でも、すべてのクライアントがこれを使います。

  1. 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)));
  2. サインインに送り出す

    ユーザーのブラウザで認可エンドポイントを開きます。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);
  3. 戻ってきたコードを受け取る

    ユーザーは 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
  4. コードをトークンと交換する

    コードを、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:更新で、ユーザーが許可した範囲を超えて要求しました。