
# 연결 생성

`POST /v1/connections` · 권한 `connections:write`

환경에 새 연결을 등록합니다. 프로토콜 설정이 필요한 연결(`social`, `enterprise`)은 `inactive` 상태로 시작합니다. 이후 `:activate`로 명시적으로 활성화하십시오. `enterprise` 연결은 `active` 상태로 생성할 수 없습니다. 새 연결은 아직 성공한 로그인 테스트를 거칠 수 없기 때문입니다. 설정에는 비밀 값을 포함하지 마십시오. 환경당 최대 100개의 연결과 20개의 일반 소셜(`oidc_generic`, `oauth2_generic`) 연결을 둘 수 있으며, 조직당 최대 20개의 연결을 둘 수 있습니다.

## 요청

JSON 본문입니다(알 수 없는 필드는 `400 unknown_field`로 거부됩니다).

| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `key` | string | 예 | 안정적인 설정 키, `^[a-z0-9_-]{1,64}$`, 환경 내에서 고유하며 변경할 수 없습니다. |
| `type` | string | 예 | `database`, `passwordless`, `passkey`, `social` 또는 `enterprise`. 변경할 수 없습니다. |
| `strategy` | string | 예 | 타입에 대한 전략입니다(예: `password`, `email_otp`, `google`, `saml`). 변경할 수 없습니다. |
| `name` | string | 예 | 1~128자, 환경 내에서 고유합니다. |
| `display_name` | string | 아니요 | 로그인 화면에 표시되는 1~128자입니다. |
| `status` | string | 아니요 | `active` 또는 `inactive`. `database`, `passwordless`, `passkey`는 기본값이 `active`이고 `social`, `enterprise`는 기본값이 `inactive`입니다. |
| `org_id` | string | 아니요 | 소유 조직(`org_…`), `enterprise`에만 해당합니다. 생략하면 환경 연결이 됩니다. 변경할 수 없습니다. |
| `allowed_email_domains` | array of strings | 아니요 | 홈 영역 검색을 통해 이 연결로 라우팅되는 이메일 도메인입니다. IDNA A-label, 소문자(예: `xn--bcher-kva.example`), 고유, 최대 100개이며 `enterprise`에서만 비어 있지 않을 수 있습니다. 조직 연결에서는 모든 도메인이 조직의 인증된 도메인이어야 하며, `active` 조직 연결에는 최소 하나가 필요합니다. |
| `allow_external_domains` | boolean | 아니요 | 목록에 없는 이메일 도메인의 사용자도 로그인하도록 허용합니다. `enterprise`에만 해당하며 기본값은 `false`입니다. |
| `jit_mode` | string | 아니요 | just-in-time 프로비저닝입니다. `create`는 최초 로그인 시 사용자를 생성하고, `create_and_update`는 ID 공급자의 프로필로 추가 업데이트하며, `off`는 기존 계정을 요구합니다. `enterprise`에만 해당합니다. 환경 연결은 기본값이 `create_and_update`이고 조직 연결은 `off`입니다. |
| `email_trust` | boolean | 아니요 | 자동 계정 연결과 just-in-time 프로비저닝을 위해 ID 공급자가 주장하는 이메일 주소를 신뢰합니다. `enterprise`에만 해당하며 기본값은 `false`입니다. 조직 연결에서 `true`이려면 비어 있지 않은 `allowed_email_domains`가 필요하며, 모든 도메인이 조직의 인증된 도메인이어야 합니다. |
| `default_role_ids` | array of strings | 아니요 | just-in-time 로그인으로 생성되는 멤버십에 환경의 기본 역할 및 연결의 그룹 역할 매핑에 더해 부여되는 환경 역할(`rol_…`, 고유, 최대 10개)입니다. 조직 `enterprise` 연결에서만 사용할 수 있습니다. 모든 역할은 이 환경의 특권이 없는 환경 역할이어야 합니다. 다른 환경의 역할, 조직 역할, 특권 역할 또는 존재하지 않는 역할은 거부됩니다. 나중에 삭제되거나 특권 역할로 바뀐 역할은 로그인 시 건너뜁니다. |
| `trust_idp_mfa` | boolean | 아니요 | ID 공급자가 주장하는 다중 인증(SAML 인증 컨텍스트, OIDC `amr`/`acr`)을 세션의 MFA 증명으로 인정합니다. `enterprise`에만 해당하며 기본값은 `false`입니다. |
| `settings` | object | 아니요 | 비밀 값을 제외한 설정으로, 최대 32 KiB입니다. 비밀처럼 보이는 키나 값은 거부됩니다. |
| `Idempotency-Key` | header | 아니요 | 요청을 재시도해도 안전하게 만드는 선택적 키(16~64자)입니다. |

## 응답

`201 Created`와 함께 연결 정보 및 버전을 담은 `ETag` 헤더가 반환됩니다.

```json
{
  "id": "conn_0k3v8m1q2x9dz4h7s5t6w0y1ab",
  "key": "google",
  "type": "social",
  "strategy": "google",
  "name": "Google",
  "display_name": "Sign in with Google",
  "status": "inactive",
  "org_id": null,
  "allowed_email_domains": [],
  "allow_external_domains": false,
  "jit_mode": null,
  "email_trust": null,
  "default_role_ids": [],
  "trust_idp_mfa": null,
  "allow_idp_initiated": null,
  "idp_initiated_default_application_id": null,
  "idp_initiated_allowed_application_ids": [],
  "settings": {},
  "created_at": "2026-10-05T09:30:00.123456Z",
  "updated_at": "2026-10-05T09:30:00.123456Z"
}
```

## 패스키 설정

`type: "passkey"`인 경우 `settings` 객체는 아래 키만 허용합니다. 그 밖의 키, 잘못된 타입 또는 범위를 벗어난 값은 `422 validation_failed`와 함께 `/settings/max_credentials_per_user` 같은 `errors[].pointer`로 거부됩니다. `null` 값은 기본값을 의미합니다. 형식 오류는 플랜 오류보다 먼저 보고됩니다.

| 키 | 타입 | 참고 |
|---|---|---|
| `rp_id` | string | 생략, `null` 또는 빈 값이면 각 로그인 호스트가 자체 rpId가 됩니다. 그 밖의 경우 환경의 `active` 커스텀 도메인 호스트 이름(소문자, IDNA A-label)이어야 하며, 다른 값은 `422 validation_failed`(`/settings/rp_id`)입니다. 환경에 패스키가 있는 경우 [패스키 rpId 변경](change-rp-id.md)을 사용하여 변경하십시오. |
| `rp_name` | string | 최대 128자입니다. |
| `passkey_first_factor_enabled`, `security_key_second_factor_enabled` | boolean | 기본값은 `true`입니다. |
| `user_verification` | string | `preferred`(기본값) 또는 `required`입니다. |
| `max_credentials_per_user` | integer | 1~20, 기본값 20입니다. 소수는 거부됩니다. |
| `require_device_bound_passkeys` | boolean | 플랜 제한 항목입니다. `true`는 Startup 플랜이 필요합니다. |
| `attestation_required` | boolean | 플랜 제한 항목입니다. `true`는 Enterprise 플랜이 필요합니다. |
| `aaguid_allowlist`, `aaguid_blocklist` | array of UUID strings | 각각 고유한 항목 최대 500개입니다(오류는 `/settings/<key>/<index>`를 가리킵니다). 플랜 제한 항목입니다. 비어 있지 않은 목록은 Enterprise 플랜이 필요합니다. |
| `related_origins` | array of strings | `https://host[:port]` 형식의 origin입니다. 플랜 제한 항목입니다. 비어 있지 않은 목록은 Enterprise 플랜이 필요합니다. |
| `passkey_enrollment_prompt` | string | `off` 또는 `after_login`(기본값)입니다. |

플랜 제한 키에 기본값이 아닌 값을 쓰면 `403 plan_feature_unavailable`과 함께 `feature` 및 `required_plan` 멤버가 반환됩니다. 플랜 권한을 사용할 수 있게 되기 전까지는 어떤 환경에서도 이 키를 설정할 수 없습니다. 기본값(`false`, `[]`, `null`)을 쓰거나 키를 제거하는 것은 언제나 허용됩니다. 기능을 사용할 수 있게 되면 모든 `related_origins` 항목은 환경의 활성 인증 커스텀 도메인이어야 하며, 그렇지 않으면 요청은 `422 related_origin_not_verified`로 실패합니다.

## 오류

| HTTP | code | 발생 조건 |
|---|---|---|
| 400 | `unknown_field` | 본문에 알 수 없는 필드가 있습니다. |
| 403 | `quota_exceeded` | 환경 또는 조직(`org_id`)의 연결 한도에 도달했습니다. |
| 409 | `conflict` | 같은 `key` 또는 `name`을 가진 연결이 이미 있습니다. |
| 409 | `connection_test_required` | 요청이 `status: active`인 `enterprise` 연결을 생성하려고 합니다. 아무것도 기록되지 않습니다. `inactive`로 생성한 뒤 로그인을 테스트하고 `:activate`로 활성화하십시오. |
| 415 | `unsupported_media_type` | 콘텐츠 타입이 `application/json`이 아닙니다. |
| 422 | `validation_failed` | 필드가 없거나 잘못되었습니다. `errors[].pointer`가 해당 필드를 알려줍니다(예: 비밀처럼 보이는 설정의 경우 `/settings/client_secret`). |
| 422 | `related_origin_not_verified` | 관련 origin이 환경의 인증된 커스텀 도메인이 아닙니다. |
| 403 | `plan_feature_unavailable` | 플랜에 설정한 항목이 포함되어 있지 않습니다(`feature`, `required_plan` 멤버). 예: `attestation_required`. |
| 401 | `unauthenticated` | 인증 정보가 없거나 잘못되었거나 취소되었습니다. |
| 403 | `insufficient_permissions` | 인증 정보에 필요한 권한이 없습니다. |
| 429 | `rate_limited` | 요청 한도를 초과했습니다. `Retry-After` 기간 이후에 다시 시도하십시오. |
| 503 | `service_unavailable` | 종속 서비스를 일시적으로 사용할 수 없습니다. `Retry-After` 기간 이후에 다시 시도하십시오. |

## 예제

```bash
curl -X POST https://api.eu.keenauth.com/v1/connections \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"google","type":"social","strategy":"google","name":"Google"}'
```
