
# 로그인 트랜잭션 생성

`POST /fapi/v1/transactions` · 환경의 로그인 호스트에서 호출

애플리케이션의 로그인 트랜잭션을 시작합니다. 호스팅된 로그인 페이지, UI 컴포넌트 및 헤드리스 클라이언트는 환경의 로그인 호스트(`keenauth.app` 하위 도메인 또는 검증된 사용자 지정 도메인)에서 이 엔드포인트를 호출합니다. 응답은 트랜잭션 쿠키 `__Host-idp_ltx`(Secure, HttpOnly, SameSite=Lax, 15분간 유효)를 설정하며, 이후 단계에서는 이 쿠키를 사용합니다.

## 요청

헤더:

| 헤더 | 필수 여부 | 설명 |
| --- | --- | --- |
| `Idp-Client` | 예 | SDK 식별자, `<package>/<version> (<runtime>/<version>)`. |
| `Origin` | 예 | 정확히 `https://<sign-in host>`이어야 합니다. |

본문:

| 필드 | 타입 | 필수 여부 | 설명 |
| --- | --- | --- | --- |
| `client_id` | string | 예 | 애플리케이션의 클라이언트 ID입니다. |
| `redirect_uri` | string (URI) | 예 | 애플리케이션에 등록된 리디렉션 URI 중 하나입니다. |

## 응답

`201 Created`와 함께 트랜잭션 객체를 반환합니다.

```json
{
  "schema_version": 1,
  "state": "identifier_pending",
  "next_steps": [{ "step": "identifier", "fields": ["identifier"] }],
  "csrf_token": "<token>",
  "expires_at": "2026-10-05T10:15:00Z"
}
```

이 트랜잭션에서 상태를 변경하는 모든 요청에는 `csrf_token`을 `Idp-Csrf-Token` 헤더로 다시 전송하십시오.

인증 요청에서 SSO가 필요한 조직을 지정한 경우, 응답이 이미 `federated_redirect`(`redirect_url` 포함, 연결이 하나인 경우: 최상위 창을 해당 URL로 이동)이거나 `sso_connections`(여러 개인 경우: `sso_start` 단계에서 선택 항목을 전송)를 포함할 수 있습니다.

## DPoP 바인딩 헤드리스 트랜잭션

자체 출처에서 실행되는 자사 SPA 또는 네이티브 앱은 `DPoP` 헤더(RFC 9449)를 사용하여 헤드리스 로그인을 시작합니다. 증명은 클라이언트가 보관하는 키(브라우저에서는 WebCrypto의 추출 불가능한 키, 기기에서는 플랫폼 키 저장소)로 서명하며, `htm`은 `POST`, `htu`는 `https://<sign-in host>/fapi/v1/transactions`입니다. 이때 트랜잭션은 쿠키 대신 해당 키에 바인딩됩니다. 쿠키는 설정되지 않으며 객체에는 `csrf_token`이 없습니다. 이후 트랜잭션의 모든 요청은 같은 키로 새로 생성한 증명을 보냅니다.

본문에 추가되는 필드는 인증 엔드포인트의 규칙에 따라 검사합니다.

| 필드 | 타입 | 필수 여부 | 설명 |
| --- | --- | --- | --- |
| `code_challenge` | string | 예 | PKCE S256 챌린지(RFC 7636). |
| `code_challenge_method` | string | 예 | `S256`. |
| `scope` | string | 아니요 | 공백으로 구분된 스코프, 예: `openid profile email offline_access`. |
| `state` | string | 아니요 | 인증 코드와 함께 반환됩니다. |
| `nonce` | string | 아니요 | ID 토큰의 `nonce`입니다. |
| `organization` | string | 아니요 | 조직 ID 또는 이름입니다. |
| `ui_locales` | string | 아니요 | 선호하는 언어입니다. |
| `resource` | string (URI) | 아니요 | 리소스 지시자(RFC 8707)입니다. |

`Origin`은 로그인 호스트 또는 애플리케이션에 허용된 웹 출처 중 하나일 수 있으며, 네이티브 앱은 아무 값도 보내지 않습니다. 이 방식은 자사 애플리케이션만 시작할 수 있습니다. 인증 엔드포인트가 거부한 필드는 `422 validation_failed`와 함께 OAuth 오류 코드를 `errors[].code`에 담아 반환합니다. 서버가 `401 use_dpop_nonce`를 응답하면 `DPoP-Nonce` 응답 헤더 값으로 새 증명에 서명하고 다시 시도하십시오. `DPoP` 헤더 없이 이 필드를 보내면 `422 validation_failed`(`dpop_required`)가 반환됩니다.

## 조직 전환

로그인한 사용자에 대한 프론트 API 액세스 토큰을 보유한 애플리케이션(참조: [프론트 API 액세스 토큰](/docs/api/fapi/front-api-token))은 화면 없이 사용자를 다른 조직으로 전환할 수 있습니다. 토큰은 `Authorization: DPoP <token>`으로 보내고, 토큰 키의 증명(`ath` 포함)을 함께 전송하십시오. 본문에는 `flow`를 `organization_switch`로 설정하고, 위의 DPoP 바인딩 헤드리스 필드(토큰 애플리케이션의 `client_id`, `redirect_uri`, `code_challenge`, `state`, `nonce`, `scope`)와 `organization`(`GET /fapi/v1/me/organizations`에서 얻은 `org_…` 형식의 조직 ID)을 포함합니다.

요청은 토큰의 세션으로 즉시 결정되며, 세션을 재사용하는 로그인과 같습니다. 조직의 정책(MFA, 유휴 시간 제한, SSO, IP 목록), 동의 및 대기 중인 법적 문서가 적용됩니다. 응답은 `201`이며, `authorization_response`를 포함한 `completed` 트랜잭션입니다. 그 안의 `code`를 `code_verifier` 및 같은 키의 증명과 함께 토큰 엔드포인트에서 교환하십시오. 새 토큰에는 `org_id`로 조직이 담깁니다. 열린 트랜잭션은 남지 않으며 쿠키도 설정되지 않습니다.

```json
{
  "schema_version": 1,
  "state": "completed",
  "next_steps": [],
  "authorization_response": { "code": "<code>", "state": "<state>", "iss": "https://example.keenauth.app" },
  "expires_at": "2026-10-05T10:15:00Z"
}
```

| 상태 | 코드 | 발생 조건 |
| --- | --- | --- |
| 401 | `unauthenticated` | 토큰이 유효하지 않거나, 만료되었거나, 취소되었거나, 키에 바인딩되지 않았거나, 해당 애플리케이션이 활성 자사 애플리케이션이 아니거나, 세션이 종료된 경우입니다. |
| 422 | `validation_failed` | `flow`가 `organization_switch`가 아니거나, `client_id`가 토큰의 애플리케이션이 아니거나, `organization`이 `org_…` ID가 아니거나, 이 애플리케이션에서 해당 조직으로 전환할 수 없는 경우(`not_eligible`)입니다. |
| 403 | `authorization_failed` | 조직이 전환 과정에서 표시할 수 없는 화면을 요구하는 경우입니다. `error`에는 OAuth 오류(`login_required`, `interaction_required`, `consent_required`)가 담깁니다. `organization`을 설정하여 새 로그인을 시작하십시오. |

토큰 없이 `flow`를 `organization_switch`로 보내면 `422 validation_failed`(`access_token_required`)가 반환됩니다.

## 오류

| 상태 | 코드 | 발생 조건 |
| --- | --- | --- |
| 400 | `malformed_request` | `Idp-Client`가 없거나 본문이 올바른 JSON이 아닌 경우입니다. |
| 403 | `origin_not_allowed` | `Origin`이 로그인 호스트가 아닌 경우입니다. |
| 403 | `headless_not_allowed` | 타사 애플리케이션이 헤드리스 플로우를 시작하려고 한 경우입니다. |
| 404 | `not_found` | 해당 호스트가 어떤 환경의 로그인 호스트도 아닌 경우입니다. |
| 422 | `validation_failed` | `client_id`가 이 환경의 애플리케이션이 아니거나, `redirect_uri`가 해당 애플리케이션에 등록되지 않았거나, 필드가 유효하지 않은 경우입니다. |
| 401 | `invalid_dpop_proof` | `DPoP` 증명이 없거나, 유효하지 않거나, 재사용되었거나, 호스트가 DPoP 바인딩 트랜잭션을 제공하지 않는 경우입니다. |
| 401 | `use_dpop_nonce` | `DPoP-Nonce` 응답 헤더 값으로 새 증명에 서명하십시오. |
| 503 | `temporarily_unavailable` | 의존 서비스를 사용할 수 없습니다. 나중에 다시 시도하십시오. |

## 예제

```bash
curl -X POST https://example.keenauth.app/fapi/v1/transactions \
  -H "Idp-Client: my-app/1.0.0 (node/22.0.0)" \
  -H "Origin: https://example.keenauth.app" \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"client_id":"<client_id>","redirect_uri":"https://app.example.com/callback"}'
```

## 개인정보

이 엔드포인트는 개인정보를 처리합니다: 최종 사용자 프로필. [KeenAuth API의 개인정보](/docs/policies/personal-data)에서 저장되는 항목, 보관 기간 및 삭제 방법을 확인하실 수 있습니다. 어떤 필드에도 특수 범주의 개인정보(건강, 생체 정보, 종교, 정치적 견해, 성적 지향)를 보내지 마십시오.
