---
title: "OAuth로 연결"
description: "API 키를 공유하지 않고 MCP 클라이언트와 ThunderPhone CLI를 인증합니다."
---

OAuth를 사용하면 앱이 사용자가 승인한 권한으로 하나의 ThunderPhone 조직에 연결할 수 있습니다. 디렉터리 클라이언트는 기본적으로 OAuth를 사용해야 합니다. 수동으로 구성한 Bearer 토큰이 필요한 클라이언트에서는 조직 API 키를 계속 사용할 수 있습니다.

## 연결 승인

MCP 클라이언트에서 연결을 시작합니다. ThunderPhone에 로그인하고, 앱 이름과 요청된 권한을 확인한 다음 조직을 선택하고 **승인**을 선택합니다. 연결을 시작하지 않았거나 앱을 신뢰하지 않는 경우 **거부**를 선택합니다. 앱 이름은 개발자가 제공하며 검증 배지가 아닙니다.

읽기 권한은 명시된 데이터를 노출하며, `calls:read`가 요청된 경우 녹음 및 트랜스크립트도 포함됩니다. 쓰기 권한은 리소스를 변경하거나 삭제할 수 있습니다. 통화, 캠페인, 전화번호 구매 및 배포는 비용을 발생시키거나 프로덕션에 영향을 줄 수 있습니다. 조직 역할은 계속 적용됩니다.

CLI 연결의 경우 터미널에 표시된 확인 링크를 열고, 8자 코드를 비교한 다음 조직을 선택하고 승인합니다. 링크를 여는 것만으로는 접근 권한이 부여되지 않습니다. 코드는 15분 후 만료됩니다.

## 앱 연결 해제

대시보드에서 **조직 → API 키 → 승인된 앱**을 열고 **철회**를 선택합니다. 그러면 접근 토큰과 새로 고침 토큰을 포함하여 현재 조직에 대해 선택한 승인이 철회됩니다. 다시 승인하려면 앱에서 다시 연결합니다.

## MCP 클라이언트용 검색

서버 URL `https://api.thunderphone.com/v1/mcp`을 사용합니다. 유효한 인증이 없는 요청은 다음과 함께 `401`을 반환합니다.

```http
WWW-Authenticate: Bearer resource_metadata="https://api.thunderphone.com/.well-known/oauth-protected-resource"
```

해당 문서를 가져온 후 `https://api.thunderphone.com/.well-known/oauth-authorization-server`에서 인가 서버의 메타데이터를 가져옵니다. 리소스 메타데이터는 `/.well-known/oauth-protected-resource/v1/mcp`에서도 사용할 수 있습니다. 엔드포인트를 직접 구성하지 말고 반환된 엔드포인트를 사용합니다. 리소스 식별자는 `https://api.thunderphone.com/v1/mcp`입니다.

서버는 **S256 PKCE**를 사용하는 인가 코드, 순환 갱신 토큰, 공개 동적 클라이언트 등록, 폐기 및 디바이스 인가 방식을 지원합니다. 클라이언트 시크릿이나 암시적 그랜트는 없습니다. OpenID 검색은 `/.well-known/openid-configuration`에서도 사용할 수 있으며, 동일한 인가 서버 필드와 `subject_types_supported: ["public"]`, userinfo 엔드포인트를 포함합니다. ID 토큰과 클라이언트 ID 메타데이터 문서(CIMD)는 지원되지 않습니다.

### 공개 클라이언트 등록

`POST /v1/oauth/register`로 JSON을 전송합니다.

```json
{
  "client_name": "My MCP client",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}
```

반환된 `client_id`를 저장합니다. 리디렉션은 HTTPS를 사용하거나 `127.0.0.1` 또는 `localhost`에서 HTTP를 사용해야 합니다. 포트와 경로를 포함한 정확한 콜백 URI를 등록합니다. 프래그먼트와 포함된 자격 증명은 거부됩니다. 선택 사항인 `client_uri`와 `logo_uri`는 HTTPS를 사용해야 하며, ThunderPhone은 등록 중에 이를 가져오지 않습니다. 등록에는 속도 제한이 적용됩니다. 등록된 클라이언트는 만료되지 않으며 연결이 폐기되어도 등록 상태가 유지됩니다. HTTPS 콜백에는 `https://chatgpt.com/connector/oauth/<id>` 및 `https://chatgpt.com/connector_platform_oauth_redirect`가 포함됩니다. 각 연결은 자체 클라이언트를 등록할 수 있습니다.

### 인가 코드

`client_id`, 정확히 등록된 `redirect_uri`, `response_type=code`, 임의의 `state`, `scope`, `code_challenge`, `code_challenge_method=S256`, `resource=https://api.thunderphone.com/v1/mcp`와 함께 검색된 인가 엔드포인트를 엽니다. 새롭고 엔트로피가 높은 PKCE 검증자의 패딩 없는 base64url SHA-256 다이제스트로 챌린지를 계산합니다. 코드를 교환하기 전에 반환된 `state`와 `iss`를 확인합니다. 거부 및 프로토콜 오류를 포함한 모든 인가 응답은 검색된 `issuer`와 정확히 일치하는 `iss`로 발급자를 식별합니다. 잘못된 클라이언트 또는 콜백에 대한 오류는 해당 콜백으로 리디렉션하지 않고 로컬에서 반환됩니다.

폼 인코딩을 사용하여 `POST /v1/oauth/token`에서 교환합니다(JSON도 허용됨).

```text
grant_type=authorization_code
client_id=<your client id>
code=<single-use authorization code>
redirect_uri=<exact registered redirect URI>
code_verifier=<original PKCE verifier>
resource=https://api.thunderphone.com/v1/mcp
```

선택 사항인 `resource` 매개변수는 인가 및 토큰 요청 모두에서 허용됩니다(갱신 및 디바이스 교환 포함). 생략하면 검색된 MCP 리소스가 기본값으로 사용됩니다. 제공되는 경우 해당 리소스와 정확히 일치해야 하며, 다른 값은 `invalid_target`을 반환합니다. 액세스 토큰에는 해당 대상이 포함되며, MCP는 대상이 없거나 다른 토큰을 `401` 및 검색 챌린지와 함께 거부합니다.

인가 요청과 코드는 10분 후 만료됩니다. 모든 토큰 응답에는 `access_token`, `token_type`(`Bearer`), `expires_in`(기본값 3600초), `refresh_token`, `scope`, `organization_id`, `organization_name`이 포함됩니다. 액세스 토큰은 `Authorization: Bearer` 헤더를 통해서만 전송합니다. 토큰을 URL, 로그, 소스 제어 또는 채팅에 절대 넣지 마십시오.

### 갱신 및 폐기

토큰 엔드포인트에서 `grant_type=refresh_token`, `client_id`, `refresh_token`, `resource`를 사용하여 갱신합니다. 새 갱신 토큰을 원자적으로 저장하고 이전 토큰 사용을 중지합니다. 갱신 토큰은 성공적인 갱신 없이 30일이 지나면 만료됩니다. 선택 사항인 `scope`는 부여된 권한을 축소할 수 있습니다. `offline_access`는 항상 포함되며 갱신 토큰은 항상 발급됩니다. `scope` 없이 초기 요청을 하면 `offline_access`만 부여되므로 클라이언트는 필요한 권한을 요청해야 합니다.

코드 재사용 및 순환 갱신 토큰 재사용은 전체 인가를 폐기합니다. 클라이언트 내에서 갱신을 직렬화합니다. 성공적으로 교환된 자격 증명을 재전송하는 것은 안전한 재시도 전략이 아닙니다.

연결을 해제하려면 `token`과 `client_id`를 `POST /v1/oauth/revoke`로 전송합니다. 두 토큰 중 하나를 폐기하면 해당 토큰에서 발급된 모든 토큰을 포함하여 해당 인가가 폐기됩니다. 알 수 없는 토큰은 존재 여부를 공개하지 않고 성공을 반환합니다. 대시보드 세션은 `GET /v1/oauth/grants`에서 자체 인가 내역을 나열하고 `DELETE /v1/oauth/grants/<id>`에서 하나를 폐기할 수 있으며, `X-ThunderPhone-Org`는 조직을 선택합니다.

### 디바이스 인가

디바이스 인가는 사전 등록된 클라이언트로 제한되며, 동적으로 등록된 클라이언트는 `unauthorized_client`를 받습니다. 사전 등록된 공개 클라이언트 `thunderphone-cli`는 디바이스 인가 및 갱신을 지원합니다. `client_id=thunderphone-cli`와 `scope`를 `POST /v1/oauth/device/code`로 전송합니다. 사용자에게 `user_code`와 `verification_uri`를 표시하거나 `verification_uri_complete`를 엽니다.

`grant_type=urn:ietf:params:oauth:grant-type:device_code`, `client_id`, `device_code`를 사용하여 토큰 엔드포인트를 폴링하고, 반환된 `interval`(5초) 이상 대기합니다. `authorization_pending`에서는 계속합니다. `slow_down`에서는 이후 모든 요청에 대해 응답에서 반환된 새 `interval`(5초 증가)을 사용합니다. `access_denied`, `expired_token` 또는 기타 오류가 발생하면 중지합니다. 디바이스 코드를 자동으로 승인하지 마십시오.

사전 등록된 `thunderphone-mcp` 클라이언트는 모든 포트에서 `http://127.0.0.1/callback` 및 `http://localhost/callback`을 허용합니다. 스킴, 호스트, 경로 및 쿼리가 일치해야 하며, 토큰 교환은 인가에서 사용한 정확한 리디렉션 URI(포트 포함)를 사용해야 합니다. 동적으로 등록된 클라이언트는 포트를 포함하여 정확한 리디렉션 URI 일치를 요구합니다. 다른 경로가 필요하면 다른 콜백을 동적으로 등록합니다.

### 계정 식별 및 워크스페이스 도메인 검사

클라이언트에 필요한 작업 범위와 함께 `openid email`을 모두 요청합니다. Bearer 헤더에 액세스 토큰을 넣어 검색된 `userinfo_endpoint`(`GET /v1/oauth/userinfo`)를 호출합니다. 성공적인 응답에는 다음이 포함됩니다.

```json
{
  "sub": "123",
  "email": "person@example.com",
  "email_verified": true,
  "name": "Example User",
  "org_id": 456
}
```

`sub`는 안정적인 사용자 식별자이고, `org_id`는 동의 중에 선택된 조직입니다. 이 엔드포인트는 두 ID 범위를 모두 요구하며 하나라도 없으면 `403`을 반환합니다. 검증되지 않은 이메일이 신뢰된다고 주장하는 대신, 계정에 검증된 이메일 프로필이 없으면 `error=access_denied`와 함께 `403`을 반환합니다. 유효하지 않거나 만료되었거나 폐기되었거나 대상이 잘못된 토큰은 `401`을 반환합니다. 사람 세션 토큰과 조직 API 키는 userinfo를 호출할 수 없습니다. ID 토큰은 발급되지 않습니다.

## 사용 가능한 권한

| 범주 | 범위 |
| --- | --- |
| 에이전트 및 에이전트 가져오기 | `agents:read`, `agents:write` |
| 통화 | `calls:read`, `calls:write` |
| 전화번호, 차단된 발신자 및 VoIP | `numbers:read`, `numbers:write` |
| 지식 | `knowledge:read`, `knowledge:write` |
| 캠페인 | `campaigns:read`, `campaigns:write` |
| 통합, 웹훅 엔드포인트, MCP 서버 | `integrations:read`, `integrations:write` |
| 음성 | `voices:read` |
| 테스트 시나리오, 테스트 실행, 검증 | `testing:read`, `testing:write` |
| 결제 | `billing:read` |
| 계정 식별 정보 및 인증된 이메일 | `openid`, `email` (userinfo에 둘 다 필요) |
| 지속 연결 | `offline_access` (항상 포함) |

GET, HEAD 및 OPTIONS는 읽기 스코프를 사용하며, 다른 메서드는 쓰기 스코프를 사용합니다. 음성 및 청구 변경은 OAuth를 통해 사용할 수 없습니다. `POST /v1/voices/preview`는 구성을 변경하지 않고 음성을 미리 듣기 때문에 `voices:read`를 사용합니다. MCP 도구는 기본 REST 작업의 스코프를 적용합니다. 승인하는 사용자가 두 조직에 모두 속해 있더라도 조직 간 이전은 거부됩니다. API 키 관리 및 사용자 계정 설정을 포함한 다른 API 범주는 OAuth를 통해 사용할 수 없습니다. REST 작업에 필요한 권한이 없으면 `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."`와 함께 `403`이 반환됩니다. 유효하지 않거나 만료된 액세스 토큰은 `401`을 반환합니다.


### MCP 도구 인증 신호

`tools/list`의 모든 도구에는 해당 도구의 REST 작업 스코프와 함께 `securitySchemes: [{"type": "oauth2", "scopes": ["agents:read"]}]`가 포함됩니다. 공개 문서 도구는 빈 스코프 목록을 사용하며 여전히 인증된 연결이 필요합니다.

도구에 필요한 스코프가 없는 유효한 토큰은 `isError: true`를 포함하는 JSON-RPC `result`, `content`의 설명 텍스트 및 `_meta["mcp/www_authenticate"]`와 함께 HTTP `200`을 받습니다. 후자는 `resource_metadata`, `error="insufficient_scope"`, `error_description` 및 필요한 `scope`가 포함된 Bearer 챌린지를 담은 배열입니다. 도구는 실행되지 않습니다. 이 챌린지를 사용하여 확장된 동의를 요청합니다. 누락되었거나 유효하지 않은 인증은 계속해서 `WWW-Authenticate`와 함께 HTTP `401`을 반환하며, REST 스코프 실패는 계속해서 HTTP `403`을 반환합니다.

## 자격 증명 보존

API는 스테이징 및 프로덕션에서 매시간 `python manage.py oauth_cleanup`를 실행합니다. 이 작업은 만료된 인증 요청과 디바이스 코드를 제거합니다. 만료된 액세스 토큰, 인증 코드 및 리프레시 토큰 해시는 해당 권한 부여가 취소되거나 전체 토큰 계열이 만료된 후에만 정리됩니다. 사용된 해시는 토큰 계열에 사용할 수 있는 리프레시 토큰, 인증 코드, 승인된 디바이스 코드 또는 액세스 토큰이 있는 동안 유지되므로 정리 작업으로 재생 공격 감지를 비활성화할 수 없습니다.
