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 | 모든 클라이언트 | 위와 같으며, 내 계정에 이 기능이 켜지면 그 사람이 내 추천 사용자로 연결될 수 있습니다. |
클라이언트 등록
방법은 세 가지입니다. 나만의 앱이라면 내 클라이언트를 등록하세요. 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 챌린지를 요청과 함께 보내세요. 로그인할 때마다 새 쌍과 새
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용 토큰은 /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: 갱신 요청이 그 사람이 허용한 것보다 많은 범위를 요청했습니다.