# OAuth 앱

> OAuth 앱을 등록하고, 사용자를 Nodaro 동의 화면으로 보내고, 코드를 90일짜리 액세스 토큰으로 교환해 각 사용자를 대신해 API를 호출하세요.

Source: https://nodaro.ai/ko/docs/developers/oauth

**OAuth 앱**은 내 제품이 다른 Nodaro 사용자를 대신해 Nodaro API를 호출하게 해 줍니다. 각 사용자는 동의 화면에서 내 앱을 승인하고, 내 서버는 그 결과로 받은 코드를 액세스 토큰으로 교환합니다. 그 토큰으로 하는 모든 호출은 사용자가 허락한 범위 안에서 그 사용자로서 작동합니다. Nodaro는 표준 OAuth 2.0 인증 코드 흐름을 구현하며, 시크릿을 안전하게 보관할 수 없는 클라이언트를 위해 PKCE도 지원합니다.

개발자 앱은 Nodaro Cloud와 Business 에디션 설치에서 사용할 수 있습니다.

## OAuth와 개인 API 토큰
| 만들려는 것 | 사용할 것 | 토큰 형식 |
| --- | --- | --- |
| 내 계정을 사용하는 스크립트, cron 작업, CI 작업 또는 백엔드 | 개인 API 토큰 | `ndr_` 뒤에 16진수 64자 |
| 사용자마다 자신의 Nodaro 계정이 있는 호스팅 제품 | OAuth 앱 | `ndr_app_` 뒤에 16진수 64자 |

내 계정 하나만 인증하고, 동의 화면과 사용자별 해지가 필요 없을 때는 **개인 API 토큰**을 사용하세요. [인증](https://nodaro.ai/docs/developers/api/authentication)을 참조하세요.

다음과 같은 경우에는 **OAuth**를 사용하세요.

- 웹 앱, SaaS, 마켓플레이스 같은 호스팅 제품을 만들고, 사용자마다 자신의 Nodaro 계정이 있습니다.
- 각 사용자가 자신의 계정이 할 수 있는 일 중 일부만 내 앱에 허락합니다.
- 각 사용자가 다른 앱에 영향을 주지 않고 언제든 내 앱의 접근을 끊을 수 있습니다.

## 흐름의 작동 방식
1. 사용자가 내 사이트에서 **Nodaro에 연결**을 클릭합니다.
2. 내 사이트가 `client_id`, `redirect_uri`, `scope`, `state`를 담아 브라우저를 Nodaro의 `/oauth/authorize` 페이지로 보냅니다.
3. 필요하면 로그인을 거친 뒤 Nodaro가 동의 화면을 보여 줍니다. 화면에는 내 앱의 이름, 로고, 요청한 범위, 그리고 접근 권한을 줄 계정이 표시됩니다. **다른 계정 사용**은 사용자를 로그아웃시킨 뒤 같은 화면으로 돌아옵니다.
4. 사용자가 **허용**을 클릭하면 Nodaro가 일회용 인증 코드를 만듭니다.
5. 브라우저가 `?code=...&state=...`를 담아 내 `redirect_uri`로 돌아옵니다.
6. 내 서버가 `client_id`와 `client_secret`으로 `POST /v1/oauth/token`에서 코드를 액세스 토큰으로 교환합니다.
7. 내 서버가 `Authorization: Bearer ndr_app_...`로 사용자를 대신해 Nodaro API를 호출합니다.

## 앱 등록
### 개발자 앱 열기
대상 Nodaro 인스턴스에서 **설정 › 개발자 앱**(`/settings/developer-apps`)을 열고 **앱 만들기**를 클릭하세요.

### 양식 작성
이름, 리디렉션 URI, 범위를 입력하세요. 각 필드는 아래 표에 설명되어 있습니다.

### 클라이언트 시크릿 저장
**앱이 생성되었습니다** 대화 상자에 `app_`로 시작하는 **클라이언트 ID**와 `sec_`로 시작하는 **클라이언트 시크릿**이 표시됩니다. 시크릿은 딱 한 번만 표시됩니다. 대화 상자를 닫기 전에 비밀 저장소에 복사하세요. Nodaro는 시크릿의 해시만 저장하므로 다시 보여 줄 수 없습니다.

| 필드 | 필수 | 규칙 |
| --- | --- | --- |
| **이름** | 예 | 1~100자. 동의 화면에 표시됩니다. |
| **설명** | 아니요 | 최대 500자. 동의 화면에서 이름 아래에 표시됩니다. |
| **리디렉션 URI** | 예 | 1~10개, 한 줄에 하나씩. 각각 `https://` 주소이거나, 개발용 `http://localhost` 주소입니다. Nodaro는 바이트 단위로 비교하며, 와일드카드는 지원하지 않습니다. |
| **허용된 출처** | 아니요 | 경로, 쿼리, 프래그먼트가 없는 순수 출처를 최대 5개까지. 내 프런트엔드가 브라우저에서 Nodaro를 호출할 때(CORS)만 필요합니다. |
| **요청 범위** | 예 | 하나 이상. 내 앱이 요청할 수 있는 최대치입니다. 사용자는 더 적게 허락할 수 있고, 내 앱은 그 이상을 요청할 수 없습니다. |
| **홈페이지 URL**, **로고 URL** | 아니요 | 앱을 만든 뒤 앱 페이지에서 설정하세요. 각각 `https://` 또는 `http://localhost` 주소입니다. 정사각형 로고가 가장 보기 좋습니다. |

### 앱 관리
- 시크릿을 잃어버렸거나 정기적인 보안 관리를 위해 앱 페이지에서 **시크릿을 재발급하세요**. 이전 시크릿은 즉시 작동을 멈추므로, 실행 중인 모든 서비스의 구성을 바로 업데이트하세요.
- **앱을 삭제하면** 그 앱에 발급된 모든 액세스 토큰이 해지됩니다. 앱을 승인했던 사용자는 다시 연결해야 합니다.
- **사용자당 앱 5개.** 스스로 등록한 MCP 클라이언트는 같은 목록에 표시되지만 이 한도에는 포함되지 않습니다.
- **코드에서는** SDK의 `client.developerApps`로 앱을 만들고, 업데이트하고, 삭제하고, 시크릿을 재발급합니다. [SDK](https://nodaro.ai/docs/developers/sdk)를 참조하세요.

## 범위
범위는 내 앱이 요청하는 하나의 권한입니다. 사용하는 범위만 요청하세요. 사용자는 요청된 모든 범위를 동의 화면에서 보며, 목록이 짧을수록 신뢰를 얻습니다.

| 범위 | 동의 화면 문구 | 허용되는 일 |
| --- | --- | --- |
| `workflows:read` | 워크플로 읽기 | 워크플로를 나열하고, 읽고, 내보냅니다. |
| `workflows:write` | 워크플로 생성 및 수정 | 워크플로를 만들고, 업데이트하고, 삭제하고, 가져오고, 이동하며, 하위 워크플로를 만듭니다. |
| `workflows:execute` | 사용자를 대신해 워크플로 실행 | 워크플로와 게시된 앱을 실행하고, MCP로 단일 생성 노드를 실행하고, 프롬프트 마법사를 사용합니다. |
| `jobs:read` | 작업 상태 및 결과 읽기 | 작업과 그 상태, 결과를 읽고, 생성 세션을 읽고, [Webhook 엔드포인트](https://nodaro.ai/docs/developers/api/webhooks#job-events)를 관리합니다. |
| `assets:read` | 업로드한 에셋 읽기 | 갤러리, 업로드, 즐겨찾기, 앱 실행, 캐릭터, 장소, 오브젝트, 크리처와 사용자의 팔레트, 무드보드를 읽습니다. |
| `assets:write` | 내 계정에 에셋 업로드 | 미디어를 업로드하고, 작업을 즐겨찾기에 추가하고 평가하고, 캐릭터, 장소, 오브젝트를 만들고 업데이트하고, 팔레트와 무드보드를 쓰고, 노드 프리셋과 그 폴더, 즐겨찾기를 쓰고, 생성 세션을 만들고 정리합니다. |
| `credits:read` | 크레딧 잔액 보기 | 크레딧 잔액과 크레딧 거래 내역을 읽습니다. |
| `apps:read` | 게시된 앱 읽기 | 게시된 앱을 나열하고 그 입력을 읽습니다. |
| `pipelines:read` | 파이프라인 읽기 | 스토리 → 동영상 파이프라인과 그 상태, 대기 중인 승인을 읽습니다. |
| `pipelines:execute` | 사용자를 대신해 파이프라인 실행(크레딧이 사용될 수 있음) | 파이프라인을 시작하고, 단계를 실행하고, 한 단계에서 브랜치합니다. |
| `pipelines:approve` | 사용자를 대신해 파이프라인 단계 승인 | 단계 출력을 승인하고, 단계 채팅과 장면 도우미를 사용합니다. |
| `presets:read` | 저장된 프리셋 읽기 | 사용자의 노드 프리셋, 프리셋 폴더, 즐겨찾기한 프리셋을 읽습니다. 기본 제공 프리셋 카탈로그는 공개되어 있어 범위가 필요 없습니다. 프리셋을 쓰려면 `assets:write`가 필요합니다. |
| `workspaces:read` | 소속된 워크스페이스 보기 | 사용자의 워크스페이스를 나열합니다. |
| `workspaces:write` | 사용할 워크스페이스 선택 | 앱이 작업할 워크스페이스를 선택합니다. |

- 일부 범위는 REST 경로를, 일부는 [MCP 도구](https://nodaro.ai/docs/mcp/tools)를, 일부는 둘 다를 제한합니다. MCP 서버는 토큰에 해당 범위가 없는 도구를 모두 숨깁니다.
- 경로에 필요한 범위가 없는 토큰은 `403 insufficient_scope`를 받으며, 빠진 범위는 `missingScope`에 들어 있습니다. [오류](#errors)를 참조하세요.
- 조직 기능이 생기기 전에 발급된 토큰에는 워크스페이스 범위가 추가되지 않습니다. 이 범위를 허락하려면 사용자가 내 앱을 다시 승인해야 합니다.
- 게시된 앱을 실행하려면 실행을 시작하는 `workflows:execute`와 진행 상황을 읽는 `jobs:read`가 필요합니다.

## 사용자를 동의 화면으로 보내기
사용자가 **Nodaro에 연결**을 클릭하면 브라우저를 다음 URL로 보내세요.

```text
https://nodaro.example.com/oauth/authorize?
client_id=app_...&
redirect_uri=https://yourapp.com/oauth/callback&
response_type=code&
scope=workflows:read+workflows:execute&
state=<random CSRF token>
```

| 매개변수 | 규칙 |
| --- | --- |
| `client_id` | 내 앱의 클라이언트 ID입니다. |
| `redirect_uri` | 등록한 리디렉션 URI 중 하나와 바이트 단위로 정확히 같아야 합니다. 일치하지 않으면 `400 invalid_redirect_uri`로 거부됩니다. |
| `response_type` | 항상 `code`입니다. 동의 화면은 다른 값을 거부합니다. |
| `scope` | 요청하는 범위로, 공백이나 `+`로 구분합니다. 앱의 요청 범위에 포함된 것이어야 합니다. |
| `state` | 인증할 때마다 새로 만들어 사용자 세션에 보관하는 무작위 토큰입니다. Nodaro는 이 값을 그대로 돌려주며, 내 쪽에서 반드시 확인해야 합니다. |

서버에서 `state`를 만드세요.

```ts

// In your /connect handler:
const state = randomBytes(32).toString("hex")
req.session.oauthState = state

const url = new URL("https://nodaro.example.com/oauth/authorize")
url.searchParams.set("client_id", process.env.NODARO_CLIENT_ID!)
url.searchParams.set("redirect_uri", "https://yourapp.com/oauth/callback")
url.searchParams.set("response_type", "code")
url.searchParams.set("scope", "workflows:read workflows:execute")
url.searchParams.set("state", state)
res.redirect(url.toString())
```

- **사용자가 취소를 클릭하면** Nodaro는 `error=access_denied`, `error_description`, 그리고 내 `state`를 담아 `redirect_uri`로 리디렉션합니다. 오류가 아니라 정상적인 결과로 처리하세요.
- **리디렉션 URI가 등록되지 않았으면** 동의 화면은 오류 페이지를 보여 주고, 취소를 눌러도 허용을 눌러도 아무 곳으로도 리디렉션하지 않습니다.

## 코드를 토큰으로 교환
사용자가 **허용**을 클릭하면 Nodaro가 브라우저를 내 콜백으로 보냅니다.

```text
https://yourapp.com/oauth/callback?code=ndr_code_...&state=<your state>
```

1. **먼저 `state`를 확인하세요.** 사용자 세션에 있는 값과 다르면 중단하세요. 이것이 사이트 간 요청 위조를 막는 장치입니다.
2. **코드는 서버에서 교환하세요.** 이 호출을 브라우저에서 하면 개발자 도구를 여는 누구나 내 `client_secret`을 읽을 수 있으므로, 절대 브라우저에서 하지 마세요.

**TypeScript SDK**

```ts

// The token endpoint is public: your client ID and secret authenticate
// the request, so the client needs no token of its own.
const client = createClient({
baseUrl: "https://nodaro.example.com",
auth: new StaticTokenAuth(""),
})

const tokens = await client.oauth.exchangeCode({
client_id: process.env.NODARO_CLIENT_ID!,
client_secret: process.env.NODARO_CLIENT_SECRET!,
code: req.query.code as string,
redirect_uri: "https://yourapp.com/oauth/callback",
})
// tokens.access_token: "ndr_app_..."
// tokens.scope:        the scopes the user granted, separated by spaces
// tokens.expires_in:   7776000 (seconds, that is 90 days)
// tokens.token_type:   "Bearer"
```

**curl**

```bash
curl -X POST https://nodaro.example.com/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
"grant_type": "authorization_code",
"client_id": "app_...",
"client_secret": "sec_...",
"code": "ndr_code_...",
"redirect_uri": "https://yourapp.com/oauth/callback"
}'
```

응답은 표준 OAuth 필드 이름을 사용합니다.

```json
{
"access_token": "ndr_app_...",
"token_type": "Bearer",
"scope": "workflows:read workflows:execute",
"expires_in": 7776000
}
```

- **코드는 한 번만 작동합니다.** 같은 코드를 두 번째로 교환하면 `400 invalid_grant`가 반환됩니다.
- **코드는 발급 후 10분이 지나면 만료됩니다.** 콜백이 코드를 받는 즉시 교환하세요.
- **토큰 엔드포인트는 JSON 본문과 폼 인코딩 본문을 모두 받습니다.** `application/x-www-form-urlencoded`로 전송하는 표준 OAuth 클라이언트도 `client_secret_post` 방식으로 그대로 작동합니다.

## 공개 클라이언트: PKCE
모바일 앱, 단일 페이지 앱, CLI 도구는 `client_secret`을 안전하게 보관할 수 없습니다. 이런 클라이언트는 대신 PKCE를 사용합니다. Nodaro는 `S256` 방식만 지원하며, `plain`은 `400 invalid_request`로 거부됩니다.

### 검증자와 챌린지 만들기
리디렉션하기 전에 엔트로피가 높은 무작위 `code_verifier`를 만드세요. 검증자의 SHA-256 해시를 base64url로 인코딩해 `code_challenge`를 만드세요.

### 인증 요청에 챌린지 보내기
인증 URL에 `code_challenge`와 `code_challenge_method=S256`을 추가하세요.

```text
https://nodaro.example.com/oauth/authorize?
client_id=app_...&
redirect_uri=https://yourapp.com/oauth/callback&
response_type=code&
scope=workflows:read+workflows:execute&
state=<random CSRF token>&
code_challenge=<base64url SHA-256 of the verifier>&
code_challenge_method=S256
```

### 토큰 교환에 검증자 보내기
`client_secret` 대신 `code_verifier`를 보내세요.

```bash
curl -X POST https://nodaro.example.com/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
"grant_type": "authorization_code",
"client_id": "app_...",
"code": "ndr_code_...",
"redirect_uri": "https://yourapp.com/oauth/callback",
"code_verifier": "<the original verifier>"
}'
```

기밀 클라이언트는 시크릿과 PKCE 검증자를 함께 보낼 수 있습니다. Nodaro는 전송된 값을 각각 확인합니다.

## 토큰으로 API 호출
사용자마다 그 사용자의 액세스 토큰으로 클라이언트를 하나씩 만드세요.

```ts

const userClient = createClient({
baseUrl: "https://nodaro.example.com",
auth: new StaticTokenAuth(tokens.access_token),
})

// Every call acts as the user who authorized your app, within the granted scopes.
const projects = await userClient.projects.list()
const workflows = await userClient.workflows.list(projects.data[0].id)
const run = await userClient.workflows.run(workflows.data[0].id)
```

SDK 없이 호출할 때는 각 REST 호출의 `Authorization: Bearer` 헤더에 토큰을 보내세요. 엔드포인트는 [REST API](https://nodaro.ai/docs/developers/api) 레퍼런스에 있습니다.

## 토큰이 만료될 때
액세스 토큰은 **90일** 동안 유효합니다. Nodaro는 **갱신 토큰을 발급하지 않습니다**. 토큰 종류도 하나, 저장할 곳도 하나, 만료 규칙도 하나인 대신, 90일마다 동의 화면을 거쳐야 합니다.

- 토큰이 만료되거나 해지되면 API 호출이 `401`을 반환합니다. 사용자를 다시 `/oauth/authorize`로 보내세요.
- 사용자가 내 앱을 다시 승인하면 Nodaro는 그 승인을 업데이트하고 새 토큰을 발급합니다. 이전 토큰은 만료되거나 해지될 때까지 유효하므로, 어떤 토큰이 사용 중인지 관리하세요.
- 범위를 추가하려면 더 넓은 `scope`를 담은 인증 URL로 사용자를 보내세요. 기존 승인의 범위가 넓어집니다.

## 토큰을 안전하게 저장하기
- **토큰은 서버에 보관하세요.** 액세스 토큰을 `localStorage`, `sessionStorage` 또는 JavaScript가 읽을 수 있는 쿠키에 절대 넣지 마세요.
- 플랫폼이 지원한다면 **저장된 토큰을 암호화하세요**. Nodaro는 각 토큰의 SHA-256 해시만 보관하므로, 토큰이 유출될 수 있는 유일한 경로는 내 데이터베이스의 유출입니다.
- **토큰을 사용자 사이에 공유하지 마세요.** 각 토큰은 한 Nodaro 사용자의 것입니다. 다른 사용자의 맥락에서 토큰을 사용하는 것은 권한 버그입니다.

## 토큰 해지
사용자가 내 앱에서 로그아웃하거나, 내 플랫폼에서 계정을 삭제하거나, 내 앱 설정에서 **Nodaro 연결 해제**를 클릭하면 토큰을 해지하세요.

**TypeScript SDK**

```ts
await client.oauth.revoke(tokens.access_token)
// { success: true }
```

**curl**

```bash
curl -X POST https://nodaro.example.com/v1/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{ "token": "ndr_app_..." }'
```

해지 엔드포인트는 존재하지 않는 토큰에도 항상 `200`으로 응답하므로, 누구도 이 엔드포인트로 추측한 토큰이 유효한지 확인할 수 없습니다. 해지된 뒤에는 그 토큰으로 하는 호출이 `401`을 반환합니다.

### 사용자가 접근을 해지할 때
사용자도 직접 내 앱의 접근을 끝낼 수 있습니다. **설정 › 연결된 앱**(`/settings/connected-apps`)에는 사용자 계정에 접근할 수 있는 모든 앱과 AI 어시스턴트가 나열됩니다. 각 항목에는 이름과 종류, 연결된 시각, 마지막으로 사용된 시각, 보유한 범위가 표시됩니다. **접근 취소**는 확인을 요청한 다음, 그 승인과 그에 따라 발급된 모든 토큰을 즉시 끝냅니다.

그 뒤로는 내 토큰으로 하는 호출이 `401`을 반환합니다. 만료된 토큰과 같이 처리하세요. 사용자를 다시 동의 화면으로 보내면 됩니다.

이 승인 목록을 보거나 해지할 수 있는 것은 로그인한 브라우저 세션뿐입니다. OAuth 액세스 토큰이나 개인 API 토큰은 `GET /v1/me/connected-apps`와 `POST /v1/me/connected-apps/:id/revoke`에서 `401`을 받으므로, 앱은 목록을 읽을 수도 바꿀 수도 없습니다. 코드에서 내 앱의 접근을 끝내려면 위의 해지 엔드포인트를 사용하세요.

SDK로 만든 브라우저 앱은 사용자 본인의 세션(`supabaseAuth`)으로 `client.connectedApps()`와 `client.revokeConnectedApp(authorizationId)`를 호출해 이 승인 목록을 읽고 해지합니다. API 키나 앱 토큰은 둘 다에서 `401`을 받으므로, CLI와 MCP 서버는 이 기능을 제공하지 않습니다.

## 오류
토큰 엔드포인트는 표준 OAuth 오류로 응답합니다.

| 상태 | 오류 | 발생 시점 | 해결 방법 |
| --- | --- | --- | --- |
| `400` | `invalid_request` | 본문 형식이 잘못되었거나 필드가 빠졌습니다. | 요청을 수정하세요. |
| `401` | `invalid_client` (`Unknown client`) | `client_id`와 일치하는 등록된 앱이 없습니다. | 구성에 있는 클라이언트 ID를 확인하세요. |
| `401` | `invalid_client` (`Bad client_secret`) | 시크릿이 틀렸습니다. 재발급 후 이전 시크릿을 쓰는 경우가 많습니다. | 현재 시크릿을 불러오고 서비스를 다시 시작하세요. |
| `400` | `invalid_grant` | 코드가 10분보다 오래되었거나, 이미 사용되었거나, `redirect_uri`가 인증 요청의 값과 다릅니다. | 똑같은 URI로 사용자를 다시 동의 화면으로 보내세요. |

인증 단계에서는 다음 오류를 동의 화면 자체에 표시하며, 오류를 담아 리디렉션하지 않습니다.

| 상태 | 코드 | 발생 시점 | 해결 방법 |
| --- | --- | --- | --- |
| `400` | `invalid_redirect_uri` | `redirect_uri`가 앱에 등록되지 않았습니다. | 앱 페이지에서 URI를 추가하세요. |
| `400` | `invalid_scope` | 요청한 범위가 앱의 요청 범위에 없습니다. | 먼저 앱에 범위를 추가하세요. |
| `404` | `invalid_client` | `client_id`를 알 수 없거나 앱이 정지되었습니다. | 클라이언트 ID와 앱 상태를 확인하세요. |

액세스 토큰으로 하는 API 호출은 다음과 같이 실패할 수 있습니다.

| 상태 | 코드 | 발생 시점 | 해결 방법 |
| --- | --- | --- | --- |
| `401` | `unauthorized` | 토큰이 만료되었거나, 해지되었거나, 형식이 잘못되었습니다. | 사용자를 다시 동의 화면으로 보내세요. |
| `403` | `insufficient_scope` | 토큰에 경로가 필요로 하는 범위가 없습니다. 본문의 `missingScope`에 그 범위가 들어 있습니다. | 더 넓은 범위로 사용자를 동의 화면으로 보내세요. |

SDK는 `403`을 타입이 지정된 `missingScope`가 있는 `ForbiddenError`로 발생시키므로, 클릭 한 번으로 다시 동의하는 흐름을 제공할 수 있습니다.

```ts

try {
await userClient.workflows.run(workflowId)
} catch (err) {
if (err instanceof ForbiddenError && err.missingScope) {
// Send the user back to /oauth/authorize with the broader scope list.
redirectToConsent({ scopes: [...currentScopes, err.missingScope] })
return
}
throw err
}
```

## 보안 점검 목록
- **모든 곳에 HTTPS.** Nodaro 인스턴스, 내 앱, 모든 리디렉션 URI가 `https://`를 사용합니다. `http://localhost`는 로컬 개발 전용입니다.
- **모든 콜백에서 `state`를 확인하세요.** 인증할 때마다 새로 만들고 사용자 세션에 보관하세요.
- **`client_secret`은 서버에 두세요.** 브라우저 앱에 번들링하거나, 로그에 남기거나, 오류 메시지에 그대로 보여 주지 마세요.
- **시크릿을 재발급하세요.** 최소 1년에 한 번, 그리고 유출이 의심되면 즉시 재발급하세요.
- **내 리디렉션 URI만 등록하세요.** 실제로 사용하는 주소만 등록하세요.
- **가장 작은 범위 집합을 요청하세요.** 범위는 그것이 필요한 기능을 만들 때 추가하세요.
- **로그아웃할 때 해지하세요.** 사용자가 내 앱에서 로그아웃하면 해지 엔드포인트를 호출해, 그 토큰이 재사용되지 않게 하세요.
- **`missingScope`를 처리하세요.** 일반적인 “권한이 거부되었습니다” 페이지 대신 다시 동의하는 흐름을 제공하세요.

## 로컬에서 흐름 테스트
가장 빠른 테스트 루프에는 Nodaro Cloud나 Business 에디션 설치처럼 **개발자 앱**이 있는 Nodaro 인스턴스와, 내 컴퓨터에서 실행하는 작은 콜백 서버가 필요합니다.

### 테스트 앱 등록
리디렉션 URI가 `http://localhost:8080/cb`이고 범위가 `workflows:read`, `workflows:execute`, `jobs:read`인 앱을 만드세요.

### 포트 8080에서 콜백 서버 실행
```ts

const NODARO_URL = "https://app.nodaro.ai" // or your Business edition install

const app = express()

app.get("/cb", async (req, res) => {
const client = createClient({ baseUrl: NODARO_URL, auth: new StaticTokenAuth("") })
const tokens = await client.oauth.exchangeCode({
client_id: process.env.NODARO_CLIENT_ID!,
client_secret: process.env.NODARO_CLIENT_SECRET!,
code: req.query.code as string,
redirect_uri: "http://localhost:8080/cb",
})
res.json(tokens)
})

app.listen(8080)
```

### 인증 URL을 열고 허용 클릭
인스턴스에서 `/oauth/authorize?client_id=app_...&redirect_uri=http://localhost:8080/cb&response_type=code&scope=workflows:read+workflows:execute&state=test123`을 여세요. **허용**을 클릭하면 콜백 서버가 토큰 JSON을 보여 줍니다.

### 실제 경로 호출
`GET /v1/projects/<id>/workflows` 같은 경로에 토큰을 사용하고, 데이터와 함께 `200`으로 응답하는지 확인하세요.

## MCP 클라이언트용 탐색과 동적 등록
Claude, ChatGPT, Cursor 같은 MCP 클라이언트는 OAuth 엔드포인트를 스스로 찾고, 사람이 개발자 앱 페이지를 방문하지 않아도 실행 중에 스스로 등록할 수 있습니다. 사용자 측 설정은 [클라이언트 연결](https://nodaro.ai/docs/mcp/connect)을 참조하세요.

### 탐색 문서
| 엔드포인트 | 표준 | 용도 |
| --- | --- | --- |
| `GET /.well-known/oauth-authorization-server` | RFC 8414 | 인증, 토큰 발급, 등록, 해지를 어디서 하는지 알려 줍니다. |
| `GET /.well-known/oauth-protected-resource` | RFC 9728 | MCP 리소스 `https://mcp.nodaro.ai/mcp`를 그 인증 서버에 연결합니다. |

일부 엄격한 클라이언트는 `/mcp` 접미사가 붙은 형태를 먼저 시도하므로, 각 문서는 이 접미사가 붙은 경로로도 제공됩니다. 네 경로 모두 Nodaro 호스트와 MCP 호스트 양쪽에서 확인됩니다. 발급자는 인스턴스의 `PUBLIC_URL`이며, Nodaro Cloud에서는 `https://app.nodaro.ai`입니다.

인증 서버 메타데이터는 다음을 알립니다.

- 인증, 토큰, 등록, 해지 엔드포인트
- `code` 응답 유형과 `authorization_code` 권한 부여 유형
- `S256` 방식만 지원하는 PKCE
- 토큰 엔드포인트의 `client_secret_post` 인증 방식
- `scopes_supported`에 있는 모든 범위

### 동적 클라이언트 등록
클라이언트는 `POST /v1/oauth/register`(RFC 7591)로 스스로 등록하고, `ndr_dcr_`로 시작하는 `client_id`와 `client_secret`을 받습니다. 이 엔드포인트는 IP 주소당 분당 10개 요청을 받습니다. 인스턴스 운영자는 `MCP_DYNAMIC_REGISTRATION`을 설정합니다.

| 모드 | 동작 |
| --- | --- |
| `allowlist` (기본값) | `MCP_DCR_ALLOWLIST`에 있는 클라이언트 이름만 등록할 수 있습니다. 나머지는 `403 client_not_allowed`를 받습니다. |
| `open` | 모든 클라이언트가 등록할 수 있습니다. 단, 클라이언트 이름과 리디렉션 URI 조합당 24시간 안에 사용되지 않은 등록은 5개까지입니다(`429 too_many_open_registrations`). |
| `off` | 등록이 비활성화됩니다(`403 dcr_disabled`). 운영자가 대신 고정된 클라이언트 ID와 시크릿을 나눠 줍니다. |

스스로 등록한 클라이언트는 이름을 직접 정했으므로, 동의 화면은 Nodaro가 그 클라이언트를 확인하지 않았다고 사용자에게 경고합니다. 클라이언트가 선언한 범위는 참고용입니다. 실제 관문은 사용자의 동의이며, 클라이언트는 유효한 범위라면 무엇이든 요청할 수 있습니다. 운영자 설정은 [셀프 호스팅 설치의 MCP](https://nodaro.ai/docs/self-hosting/mcp)를 참조하세요.

## 셀프 호스팅 설치의 Figma용 플러그인
Figma용 Nodaro 플러그인은 Figma 안에서 실행되므로 리디렉션을 받을 수 없습니다. 대신 기기 방식의 핸드셰이크로 연결합니다. 플러그인이 사용자에게 짧은 코드를 보여 주고, 사용자는 일반 동의 화면에서 플러그인을 승인하고 그 코드를 입력하며, 그러면 플러그인이 토큰을 받습니다. 이 토큰은 `jobs:read`, `assets:read`, `assets:write`, `credits:read` 범위를 가진 일반 개발자 앱 토큰이며, 다른 토큰처럼 해지할 수 있습니다.

플러그인이 내 설치에 연결할 수 있게 하려면 다음을 하세요.

1. **개발자 앱**에서 또는 `POST /v1/developer-apps`로 개발자 앱을 등록하세요. 리디렉션 URI에 `<PUBLIC_URL>/v1/oauth/plugin/callback`을 추가하고, 위의 범위 네 개를 요청하세요.
2. 서버의 `FIGMA_PLUGIN_OAUTH_CLIENT_ID`에 앱의 클라이언트 ID를 넣으세요.

이 설정이 없으면 플러그인의 모든 연결 경로가 `503 plugin_connect_not_configured`로 응답합니다. 앱에 콜백 URI나 범위가 빠져 있으면 경로가 `503 plugin_connect_misconfigured`로 응답하며, 서버 로그에 무엇이 빠졌는지 기록됩니다.

## 자주 묻는 질문

### 개인 API 토큰 대신 OAuth가 필요한 경우는 언제인가요?

내 제품이 다른 사람들의 Nodaro 계정에서 작동하고, 각 사용자가 내 앱을 승인하고 언제든 접근을 끊을 수 있어야 할 때 OAuth를 사용하세요. 내 계정만 사용하는 스크립트와 서버에는 개인 API 토큰이 더 간단합니다.

### Nodaro OAuth 액세스 토큰은 얼마 동안 유효한가요?

90일입니다. Nodaro는 갱신 토큰을 발급하지 않으므로, 토큰이 만료되거나 해지되면 API 호출이 401을 반환하고, 사용자를 다시 동의 화면으로 보내야 합니다.

### Nodaro는 PKCE를 지원하나요?

네, S256 방식만 지원합니다. 모바일 앱, 단일 페이지 앱, CLI 도구는 클라이언트 시크릿 대신 인증 요청에 code_challenge를, 토큰 교환에 그와 짝이 맞는 code_verifier를 보냅니다.

### 클라이언트 시크릿을 잃어버리면 어떻게 되나요?

Nodaro는 시크릿의 해시만 저장하므로 시크릿을 다시 보여 줄 수 없습니다. 앱 페이지에서 시크릿을 재발급해 새 시크릿을 받으세요. 이전 시크릿은 즉시 작동을 멈춥니다.

### 개발자 앱은 몇 개까지 등록할 수 있나요?

사용자당 5개입니다. 스스로 등록한 MCP 클라이언트는 같은 목록에 표시되지만 이 한도에는 포함되지 않습니다.

### 사용자가 내 앱에 준 접근 권한을 회수할 수 있나요?

네. 사용자는 설정 › 연결된 앱에서 자신의 계정에 접근할 수 있는 모든 앱을 해지할 수 있습니다. 그 승인과 그에 따라 발급된 모든 토큰이 즉시 작동을 멈추고, 내 호출은 401을 반환합니다. 사용자를 다시 동의 화면으로 보내세요.
