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
모든 클라이언트

위와 같으며, 내 계정에 이 기능이 켜지면 그 사람이 내 추천 사용자로 연결될 수 있습니다.

클라이언트 등록

방법은 세 가지입니다. 나만의 앱이라면 내 클라이언트를 등록하세요. 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)이며, 로그인이 필요 없습니다. 대부분의 어시스턴트가 이 방식으로 등록합니다. 한 주소에서 시간당 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(포트만 달라도 됨)여야 하며, 프래그먼트는 붙일 수 없습니다. 클라이언트 하나에 최대 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초 동안 유효하고 한 번만 쓸 수 있습니다. 두 번 보내거나 잘못된 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가 내 계정에 켜 줄 수 있습니다. 켜져 있든 아니든, 클라이언트가 직접 로그인한 사람의 액세스 토큰으로 그 사람을 내 추천 코드에 연결할 수 있습니다. 내 code와 함께 POST /user/referral/redeem을 호출하세요.

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

오류

토큰, 취소, 등록 엔드포인트는 API의 일반 형식이 아닌 OAuth 고유의 오류 본문 {"error": "invalid_grant", "error_description": "…"}으로 응답합니다. 분기는 설명이 아니라 항상 error로 하세요. 토큰 엔드포인트의 모든 응답에는 Cache-Control: no-store가 붙습니다.

  • invalid_grant: 코드가 이미 사용되었거나 만료되었거나, verifier가 틀렸거나, 리프레시 토큰이 유예 기간이 지난 뒤 다시 사용되었습니다. 그 사람을 다시 로그인시키세요.

  • invalid_target: resource가 토큰의 대상이 될 수 있는 주소가 아니거나, 존재하지 않는 추천 코드를 가리킵니다.

  • invalid_client: 클라이언트의 자격 증명이 틀렸거나 없습니다. 모든 종류가 같은 방식으로 실패합니다.

  • invalid_scope: 갱신 요청이 그 사람이 허용한 것보다 많은 범위를 요청했습니다.