---
title: Microsoft Entra ID와 함께 WIF 사용하기
url: https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure
description: Azure 관리 ID 및 Entra Workload Identity를 Claude API와 페더레이션하여 Azure 워크로드가 정적 API 키 없이 Claude를 호출할 수 있도록 합니다.
---

Azure 워크로드는 Microsoft Entra ID가 발급한 "JSON Web Token"(JSON 웹 토큰), 즉 JWT를 제시한 다음 이를 수명이 짧은 Anthropic 액세스 토큰으로 교환하여 Claude API에 인증합니다. 설정은 모든 Azure 플랫폼에서 동일한 형태를 따릅니다:

1. **토큰 대상(audience) 등록:** Microsoft Entra 테넌트에 Claude API 대상을 나타내는 앱 등록을 하나 생성합니다. 테넌트의 모든 워크로드는 이 대상에 대해 Entra 토큰을 요청합니다.
2. **플랫폼에 맞는 ID 설정:** VM, VM Scale Sets, App Service, Functions, Container Apps에서는 관리 ID(managed identity)를, AKS에서는 Entra Workload Identity를 사용합니다.
3. **Anthropic 구성:** 테넌트의 Entra 발급자(issuer)를 등록하고, 서비스 계정을 생성하고, 토큰의 클레임과 일치하는 페더레이션 규칙을 작성합니다.
4. **런타임에 교환:** 워크로드는 Entra가 발급한 토큰을 `POST /v1/oauth/token`에서 `sk-ant-oat01-...` Anthropic 액세스 토큰으로 교환하고 이를 사용하여 Claude를 호출합니다.

두 경로 모두에서 Anthropic에 제시하는 토큰은 테넌트별 Entra 발급자와 `sub` 및 `oid` 클레임에 담긴 관리 ID의 개체 ID를 포함합니다. 워크로드가 해당 토큰을 얻는 방법만 다릅니다. 워크로드가 실행되는 위치에 맞는 섹션을 선택하세요: VM, VM Scale Sets, App Service, Functions 또는 Container Apps의 경우 [관리 ID 사용](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#use-a-managed-identity), AKS의 경우 [AKS에서 Entra Workload Identity 사용](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#use-entra-workload-identity-on-aks)을 참조하세요.

## 사전 요구 사항

* [WIF 개념](https://platform.claude.com/docs/ko/manage-claude/workload-identity-federation#concepts)에 대한 이해: 서비스 계정, 페더레이션 발급자, 페더레이션 규칙.
* 관리 ID를 할당할 권한(또는 AKS에서 Entra Workload Identity를 구성할 권한)이 있는 Azure 구독.
* Microsoft Entra 테넌트에서 앱 등록 하나와 서비스 주체(service principal)를 생성할 권한(공유 Claude API 대상). Entra는 테넌트에 존재하는 대상에 대해서만 토큰을 발급하므로, 토큰 요청이 성공하려면 [토큰 대상 등록](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#register-the-token-audience) 단계가 먼저 필요합니다.
* Microsoft Entra 테넌트 ID. Azure 포털의 **Microsoft Entra ID → Overview → Tenant ID**에서 찾을 수 있습니다.
* Anthropic 조직의 Claude Console에서 서비스 계정, 페더레이션 발급자, 페더레이션 규칙을 생성할 권한.

## 토큰 대상 등록

Microsoft Entra ID는 요청된 대상이 테넌트에 서비스 주체가 있는 앱 등록으로 존재할 때만 토큰을 발급합니다. Claude API 대상을 나타내는 앱 등록을 하나 생성하세요. 테넌트의 모든 워크로드가 이에 대한 토큰을 요청할 수 있습니다. 이 등록이 없으면 토큰 요청은 "resource not found in tenant" 오류(관리 ID 엔드포인트에서는 `AADSTS50001`, Entra 토큰 엔드포인트에서는 `AADSTS500011`)와 함께 실패합니다.

```bash
# Claude API 대상(audience)을 나타내는 앱 등록을 생성합니다.
APP_ID=$(az ad app create --display-name claude-api-federation --query appId -o tsv)

# v2.0 액세스 토큰을 요청하고 api://<APP_ID> 식별자 URI를 설정합니다.
az ad app update --id "$APP_ID" \
  --identifier-uris "api://$APP_ID" \
  --set api.requestedAccessTokenVersion=2

# 테넌트에서 대상(audience)이 확인되도록 서비스 주체를 생성합니다.
az ad sp create --id "$APP_ID"
```

<Note>
  `api://<APP_ID>` 식별자 URI 형식을 사용하세요. Entra는 `https://` 식별자 URI를 자체 테넌트의 확인된 도메인으로 제한하므로, `https://api.anthropic.com`과 같은 URI는 대부분의 테넌트에서 등록할 수 없습니다. `api://<APP_ID>`는 어디서나 허용됩니다. `requestedAccessTokenVersion: 2`를 사용하면 이 대상에 대한 토큰은 v2.0이 되며, 이 가이드는 이를 전제로 합니다. v1.0 토큰을 발급하는 기존 등록을 재사용하는 경우 [토큰이 v1.0인 경우](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0)를 참조하세요.
</Note>

## 관리 ID 사용

워크로드가 VM, VM Scale Set, App Service, Functions 또는 Container Apps에서 실행되는 경우 이 경로를 사용하세요. 워크로드는 플랫폼의 로컬 토큰 엔드포인트에서 할당된 관리 ID에 대한 Entra 발급 JWT를 요청한 다음, 해당 JWT를 Anthropic과 교환합니다.

### 관리 ID 구성

<Steps>
  <Step title="관리 ID 연결">
    Azure 리소스에서 시스템 할당 또는 사용자 할당 관리 ID를 활성화하세요. Azure 포털에서 리소스를 열고 **Identity**로 이동한 다음 **System assigned**를 켜세요(또는 사용자 할당 ID를 연결하세요).

    ID가 생성된 후 **Object (principal) ID**를 기록해 두세요. 이 GUID는 발급된 토큰에서 `sub` 및 `oid` 클레임 모두에 나타나며, Anthropic 페더레이션 규칙이 이 값과 일치하는지 확인합니다. 리소스의 **Identity** 페이지에서 찾을 수 있으며, 사용자 할당 ID의 경우 관리 ID 리소스의 **Overview** 페이지에 있는 **Object (principal) ID**입니다. (관리 ID는 Microsoft Entra ID에 앱 등록이 아닌 서비스 주체만 가집니다.)
  </Step>

  <Step title="플랫폼의 토큰 엔드포인트 찾기">
    ID가 연결되면 플랫폼은 로컬 토큰 엔드포인트를 노출합니다:

    * **VM 및 VM Scale Sets:** `http://169.254.169.254/metadata/identity/oauth2/token`의 IMDS, 헤더 `Metadata: true` 및 `api-version=2018-02-01` 사용.
    * **App Service, Functions, Container Apps:** `IDENTITY_ENDPOINT` 환경 변수의 URL, 헤더 `X-IDENTITY-HEADER`를 `IDENTITY_HEADER` 값으로 설정하고 `api-version=2019-08-01` 사용. 이 플랫폼에서는 IMDS에 접근할 수 없습니다.

    리소스에 사용자 할당 관리 ID가 둘 이상 있는 경우, 토큰 요청에 `client_id=<IDENTITY_CLIENT_ID>`를 추가하여 하나를 선택하세요. Azure는 항상 이를 지정할 것을 권장합니다. 지정하지 않으면 결과는 리소스에 시스템 할당 ID도 활성화되어 있는지에 따라 달라집니다. 활성화되어 있으면 요청이 조용히 해당 ID로 대체되어 페더레이션 규칙의 `oid` 일치에 실패하고, 활성화되어 있지 않으면 두 번째 사용자 할당 ID가 연결되는 즉시 요청이 완전히 실패합니다.
  </Step>

  <Step title="샘플 토큰 디코딩">
    엔드포인트에서 토큰을 요청하고 페이로드를 디코딩하여 페더레이션 규칙이 일치해야 하는 클레임을 확인하세요. (디코딩 명령은 [실패한 교환 문제 해결](https://platform.claude.com/docs/ko/manage-claude/wif-reference#troubleshoot-a-failed-exchange)을 참조하세요.) 관리 ID에 대한 v2.0 토큰은 다음 클레임을 포함합니다:

    ```json
    {
      "iss": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
      "sub": "9f8e7d6c-1a2b-3c4d-5e6f-...",
      "aud": "<APP_ID>",
      "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...",
      "tid": "<TENANT_ID>",
      "azp": "<IDENTITY_CLIENT_ID>",
      "ver": "2.0",
      "exp": 1775527120
    }
    ```

    | 클레임   | 값                                                                                                                                                 | 일치시켜야 하는 경우                                                                                                                                                  |
    | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | `oid` | 관리 ID의 개체 ID, `sub`와 동일                                                                                                                           | 특정 관리 ID 하나를 승인하려는 경우. 이것이 기본값이며, [Anthropic 구성](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#configure-anthropic)의 규칙이 이 값과 일치합니다. |
    | `azp` | 호출하는 ID의 클라이언트 ID                                                                                                                                 | 하나의 앱 등록을 공유하는 모든 워크로드를 승인하려는 경우. 관리 ID의 경우 `azp`는 해당 ID에 고유하므로 `oid`와 동등합니다.                                                                                |
    | `aud` | 대상 앱 등록의 클라이언트 ID([토큰 대상 등록](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#register-the-token-audience)의 `<APP_ID>` GUID) | 항상. 규칙의 `audience` 필드는 토큰의 `aud` 값과 정확히 일치해야 합니다.                                                                                                            |
    | `tid` | 테넌트 ID                                                                                                                                            | 심층 방어를 원하는 경우. 발급자 URL이 이미 테넌트를 고정합니다.                                                                                                                       |

    디코딩된 토큰의 `ver` 클레임이 `1.0`이면 클레임 이름과 값이 다릅니다. 계속하기 전에 [토큰이 v1.0인 경우](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0)를 참조하세요.
  </Step>
</Steps>

### Anthropic 구성

Claude Console에서 [Workload identity](https://platform.claude.com/settings/workload-identity-federation) 페이지(`platform.claude.com/settings/workload-identity-federation`)로 이동하여 **Connect workload**를 클릭하고 **Microsoft Entra** 타일을 선택하세요. 마법사가 발급자 등록, 서비스 계정 생성, 페더레이션 규칙 생성 과정을 안내합니다.

마법사가 이러한 리소스를 대신 생성해 줍니다. 마법사에 입력하든 [Admin API](https://platform.claude.com/docs/ko/manage-claude/wif-admin-api)로 전송하든 다음 값을 사용하세요:

**페더레이션 발급자:** 마법사의 **Token issuer** 선택기에서 **v2.0 (login.microsoftonline.com)**&#xC744; 선택하세요. (선택기의 기본값은 v1이며, 이 기본값은 여전히 v1.0 토큰을 발급하는 이전 등록을 재사용하는 테넌트를 위해 존재합니다.) Entra는 테넌트별 발급자 URL에 OIDC 디스커버리 문서를 게시하므로 디스커버리 모드를 사용하세요. 페더레이션하는 각 Microsoft Entra 테넌트에는 자체 발급자 레코드가 필요합니다.

```json
{
  "name": "azure-prod-tenant",
  "issuer_url": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
  "jwks": { "type": "discovery" },
  "max_jwt_lifetime_seconds": 86400
}
```

<Warning>
  관리 ID 워크로드에는 `max_jwt_lifetime_seconds: 86400`이 필요합니다. Azure는 각 리소스의 토큰을 해당 기간 동안 캐시하고 조기 갱신을 강제할 방법을 제공하지 않기 때문에 `iat`와 `exp` 사이가 최대 24시간인 관리 ID 토큰을 발급합니다. 발급자의 기본값인 1시간은 이러한 토큰을 거부하므로, 교환이 불투명한 `401` `authentication_error` 응답(메시지 `Authentication failed`)과 함께 실패합니다. Connect workload 마법사의 Microsoft Entra 타일은 `max_jwt_lifetime_seconds`를 `7500`으로 설정하여 발급자를 생성하며 생성 중에 이를 변경할 필드를 제공하지 않으므로, 마법사를 완료한 다음 Claude Console의 [Workload identity](https://platform.claude.com/settings/workload-identity-federation) 페이지에서 **Issuers** 탭을 열고 발급자를 편집하여 값을 `86400`으로 올리세요. Admin API를 통해 발급자를 업데이트할 수도 있습니다.
</Warning>

허용되는 수명이 길어지면 유출된 Entra 토큰이 더 오래 교환 가능한 상태로 유지됩니다. 토큰이 유출되면 대응 수단은 페더레이션 규칙을 비활성화하는 것입니다. [규칙 범위 지정](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#scope-your-rule)에 설명된 대로 엄격한 `oid` 일치는 애초에 어떤 ID가 토큰을 교환할 수 있는지를 제한합니다.

**페더레이션 규칙:** 관리 ID의 개체 ID와 테넌트 ID를 일치시키세요. 이 가이드가 구성하는 v2.0 토큰의 경우 `audience` 값은 대상 앱 등록의 클라이언트 ID([토큰 대상 등록](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#register-the-token-audience)의 `<APP_ID>` GUID)입니다. 디코딩된 토큰의 정확한 `aud` 값을 사용하세요.

```json
{
  "name": "azure-inference-worker",
  "issuer_id": "fdis_...",
  "match": {
    "audience": "<APP_ID>",
    "claims": {
      "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...",
      "tid": "<TENANT_ID>"
    }
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}
```

`token_lifetime_seconds`는 Entra 토큰이 아니라 교환이 반환하는 Anthropic 액세스 토큰의 수명이며, SDK가 이를 자동으로 갱신합니다.

### 토큰 획득 및 사용

런타임에 워크로드는 Entra 토큰을 가져와 `POST /v1/oauth/token`에서 교환하고, 반환된 bearer 토큰을 사용하여 Claude를 호출합니다. 다음 예제에서 보듯이 `identity_token_provider` (typescript: `identityTokenProvider`; php: `identityProvider`; csharp: `IdentityTokenProvider`; go: `option.WithFederationTokenProvider`; java: `IdentityTokenConfig`)를 제공하면 Claude SDK가 교환 및 갱신 루프를 처리합니다. Java를 제외하고(해당 예제의 주석 참조), 이 옵션은 토큰 제공자를 받으며 SDK는 갱신할 때마다 이를 다시 호출합니다. cURL 탭은 원시 흐름을 보여줍니다.

샘플은 플랫폼의 토큰 엔드포인트에서 관리 ID 토큰을 가져옵니다. VM 및 VM Scale Sets에서는 IMDS, App Service, Functions, Container Apps에서는 `IDENTITY_ENDPOINT` 서비스입니다. `api://<APP_ID>` 리소스 값의 `<APP_ID>`를 [토큰 대상 등록](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#register-the-token-audience)의 대상 앱 등록 클라이언트 ID로 바꾸세요.

<Tip>
  워크로드에서 이미 Azure Identity 클라이언트 라이브러리를 사용하고 있다면, 토큰 엔드포인트를 직접 호출하는 대신 해당 라이브러리에서 Entra 토큰을 가져오세요(`api://<APP_ID>/.default` 스코프를 사용하는 `DefaultAzureCredential`). 예제와 같은 방식으로 `identity_token_provider` (typescript: `identityTokenProvider`; php: `identityProvider`; csharp: `IdentityTokenProvider`; go: `option.WithFederationTokenProvider`; java: `IdentityTokenConfig`)에 제공하세요. 이 라이브러리는 Entra Workload Identity를 사용하는 AKS를 포함한 모든 Azure 플랫폼에서 올바른 엔드포인트를 선택합니다.
</Tip>

<CodeGroup>
  ```bash cURL
  # 1. Entra에서 발급한 토큰(managed identity)을 가져옵니다.
  #    VM 또는 VM Scale Set에서는 IMDS를 사용하세요. 사용자 할당 ID가 여러 개인 경우
  #    &client_id=<IDENTITY_CLIENT_ID>를 추가하세요.
  ENTRA_TOKEN=$(curl -sS -H "Metadata: true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://<APP_ID>" \
    | jq -r .access_token)

  #    App Service, Functions 또는 Container Apps에서는 대신 로컬 토큰
  #    서비스를 사용하세요(해당 환경에서는 IMDS에 접근할 수 없습니다):
  # ENTRA_TOKEN=$(curl -sS -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" \
  #   "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=api://<APP_ID>" \
  #   | jq -r .access_token)

  #    Entra Workload Identity를 사용하는 AKS의 경우, 대신
  #    "Use Entra Workload Identity on AKS" 섹션의 2단계 교환 방식을 사용하세요.

  # 2. 이 토큰을 Anthropic 액세스 토큰으로 교환합니다.
  RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
    -H "content-type: application/json" \
    -d @- <<JSON
  {
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": "$ENTRA_TOKEN",
    "federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
    "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
    "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
    "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
  }
  JSON
  )

  ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

  # 3. bearer 토큰으로 Claude API를 호출합니다.
  curl https://api.anthropic.com/v1/messages \
    -H "authorization: Bearer $ACCESS_TOKEN" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello from Azure"}]
    }' | jq -r '.content[] | select(.type == "text") | .text'
  ```

  ```python Python
  import os

  import anthropic
  import requests
  from anthropic import WorkloadIdentityCredentials

  # 대상(audience) 앱 등록의 식별자 URI입니다(토큰 대상 등록 참조).
  AUDIENCE = "api://<APP_ID>"


  def fetch_entra_token() -> str:
      """Fetch a managed identity token from the platform's token endpoint."""
      # 사용자 할당 ID가 여러 개인 경우 client_id=<IDENTITY_CLIENT_ID>를
      # 요청 매개변수에 추가하여 하나를 선택하세요.
      if endpoint := os.environ.get("IDENTITY_ENDPOINT"):
          # App Service, Functions, Container Apps
          response = requests.get(
              endpoint,
              headers={"X-IDENTITY-HEADER": os.environ["IDENTITY_HEADER"]},
              params={"api-version": "2019-08-01", "resource": AUDIENCE},
              timeout=5,
          )
      else:
          # VM 또는 VM Scale Set: Azure Instance Metadata Service (IMDS)
          response = requests.get(
              "http://169.254.169.254/metadata/identity/oauth2/token",
              headers={"Metadata": "true"},
              params={"api-version": "2018-02-01", "resource": AUDIENCE},
              timeout=5,
          )
      response.raise_for_status()
      return response.json()["access_token"]


  client = anthropic.Anthropic(
      credentials=WorkloadIdentityCredentials(
          identity_token_provider=fetch_entra_token,
          federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
          organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
          service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
          workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
      ),
  )

  message = client.messages.create(
      model="claude-opus-5-5",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Hello from Azure"}],
  )
  print(next(block.text for block in message.content if block.type == "text"))
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";
  import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";

  // 대상(audience) 앱 등록의 식별자 URI입니다(Register the token audience 참조).
  const AUDIENCE = "api://<APP_ID>";

  async function fetchEntraToken(): Promise<string> {
    // App Service, Functions, Container Apps는 IDENTITY_ENDPOINT를 주입합니다.
    // VM 및 VM Scale Sets는 IMDS를 사용합니다.
    // 사용자 할당 ID가 여러 개인 경우 &client_id=<IDENTITY_CLIENT_ID>를 추가하세요.
    const identityEndpoint = process.env.IDENTITY_ENDPOINT;
    const url = identityEndpoint
      ? `${identityEndpoint}?api-version=2019-08-01&resource=${AUDIENCE}`
      : `http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=${AUDIENCE}`;
    const headers: Record<string, string> = identityEndpoint
      ? { "X-IDENTITY-HEADER": process.env.IDENTITY_HEADER! }
      : { Metadata: "true" };
    const response = await fetch(url, { headers });
    const body = (await response.json()) as { access_token: string };
    return body.access_token;
  }

  const client = new Anthropic({
    credentials: oidcFederationProvider({
      identityTokenProvider: fetchEntraToken,
      federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
      organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
      serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
      workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
      baseURL: "https://api.anthropic.com",
      fetch
    })
  });

  const message = await client.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello from Azure" }]
  });
  for (const block of message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
  ```

  ```go Go
  package main

  import (
  	"context"
  	"encoding/json"
  	"fmt"
  	"net/http"
  	"os"

  	"github.com/anthropics/anthropic-sdk-go"
  	"github.com/anthropics/anthropic-sdk-go/option"
  )

  // 대상(audience) 앱 등록의 식별자 URI입니다(토큰 대상 등록 참조).
  const audience = "api://<APP_ID>"

  // fetchEntraToken은 플랫폼의 토큰 엔드포인트에서 관리 ID 토큰을 가져옵니다.
  // 엔드포인트는 VM 및 VM Scale Sets에서는 IMDS이고, 그 외에는 IDENTITY_ENDPOINT 서비스입니다
  // (App Service, Functions, Container Apps의 경우).
  func fetchEntraToken(ctx context.Context) (string, error) {
  	// 사용자 할당 ID가 여러 개인 경우 &client_id=<IDENTITY_CLIENT_ID>를 추가하세요.
  	tokenURL := "http://169.254.169.254/metadata/identity/oauth2/token" +
  		"?api-version=2018-02-01&resource=" + audience
  	header, value := "Metadata", "true"
  	if endpoint := os.Getenv("IDENTITY_ENDPOINT"); endpoint != "" {
  		tokenURL = endpoint + "?api-version=2019-08-01&resource=" + audience
  		header, value = "X-IDENTITY-HEADER", os.Getenv("IDENTITY_HEADER")
  	}
  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil)
  	if err != nil {
  		return "", err
  	}
  	req.Header.Set(header, value)
  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		return "", fmt.Errorf("call token endpoint: %w", err)
  	}
  	defer resp.Body.Close()
  	var body struct {
  		AccessToken string `json:"access_token"`
  	}
  	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
  		return "", fmt.Errorf("decode token response: %w", err)
  	}
  	return body.AccessToken, nil
  }

  func main() {
  	client := anthropic.NewClient(
  		option.WithFederationTokenProvider(fetchEntraToken, option.FederationOptions{
  			FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
  			OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
  			ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
  			WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
  		}),
  	)

  	message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
  		Model:     anthropic.ModelClaudeOpus5_5,
  		MaxTokens: 1024,
  		Messages: []anthropic.MessageParam{
  			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from Azure")),
  		},
  	})
  	if err != nil {
  		panic(err)
  	}
  	for _, block := range message.Content {
  		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
  			fmt.Println(textBlock.Text)
  			break
  		}
  	}
  }
  ```

  ```java Java
  import com.anthropic.config.AuthenticationConfig;
  import com.anthropic.config.AuthenticationType;
  import com.anthropic.config.IdentityTokenConfig;
  import com.anthropic.config.InMemoryProfileConfigProvider;
  import com.anthropic.config.ProfileConfig;
  // ...
  import com.fasterxml.jackson.databind.ObjectMapper;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;

  void main() throws Exception {
      HttpClient http = HttpClient.newHttpClient();
      // 대상(audience) 앱 등록의 식별자 URI입니다(Register the token audience 참조).
      String audience = "api://<APP_ID>";
      // App Service, Functions, Container Apps는 IDENTITY_ENDPOINT를 주입합니다.
      // VM 및 VM Scale Sets는 IMDS를 사용합니다.
      // 사용자 할당 ID가 여러 개인 경우 &client_id=<IDENTITY_CLIENT_ID>를 추가하세요.
      String identityEndpoint = System.getenv("IDENTITY_ENDPOINT");
      HttpRequest tokenRequest = identityEndpoint != null
              ? HttpRequest.newBuilder(URI.create(identityEndpoint + "?api-version=2019-08-01&resource=" + audience))
                      .header("X-IDENTITY-HEADER", System.getenv("IDENTITY_HEADER"))
                      .build()
              : HttpRequest.newBuilder(URI.create("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=" + audience))
                      .header("Metadata", "true")
                      .build();

      // Java SDK는 함수가 아니라 토큰 자체 또는 파일 경로를 받습니다.
      // source("value")를 사용하면 path(...)에 토큰이 담깁니다. SDK는 Anthropic 액세스 토큰을
      // 갱신할 때마다 동일한 토큰을 전송하므로, 토큰이 만료되었거나
      // jti 클레임이 있는 경우 이미 사용되었다면 실패합니다. 장시간 실행되는
      // 프로세스에서는 source("file")을 전달하고 매 갱신 전에
      // 파일에 새 토큰을 기록하세요.
      var response = http.send(tokenRequest, HttpResponse.BodyHandlers.ofString());
      String entraToken = new ObjectMapper().readTree(response.body()).get("access_token").asText();

      AnthropicClient client = AnthropicOkHttpClient.builder()
              .configurationProvider(InMemoryProfileConfigProvider.of(ProfileConfig.builder()
                      .organizationId(System.getenv("ANTHROPIC_ORGANIZATION_ID"))
                      .workspaceId(System.getenv("ANTHROPIC_WORKSPACE_ID"))
                      .authentication(AuthenticationConfig.builder()
                              .type(AuthenticationType.OIDC_FEDERATION)
                              .federationRuleId(System.getenv("ANTHROPIC_FEDERATION_RULE_ID"))
                              .serviceAccountId(System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"))
                              .identityToken(IdentityTokenConfig.builder()
                                      .source("value")
                                      .path(entraToken)
                                      .build())
                              .build())
                      .build()))
              .build();

      var message = client.messages().create(MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024)
              .addUserMessage("Hello from Azure")
              .build());

      IO.println(message.content());
  }
  ```

  ```csharp C#
  using Anthropic.Credentials;
  // ...

  var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
  {
      FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!,
      OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"),
      ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"),
      WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"),
      IdentityTokenProvider = new EntraTokenProvider(),
  });
  using var client = new AnthropicClient(new ClientOptions { Credentials = credentials });

  var message = await client.Messages.Create(new()
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello from Azure" }],
  });
  foreach (var block in message.Content)
  {
      if (block.Value is TextBlock textBlock)
      {
          Console.WriteLine(textBlock.Text);
      }
  }

  class EntraTokenProvider : IIdentityTokenProvider
  {
      // 대상(audience) 앱 등록의 식별자 URI입니다(토큰 대상 등록 참조).
      private const string Audience = "api://<APP_ID>";

      private static readonly HttpClient httpClient = new();

      public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
      {
          // App Service, Functions, Container Apps는 IDENTITY_ENDPOINT를 주입합니다.
          // VM 및 VM Scale Sets는 IMDS를 사용합니다.
          // 사용자 할당 ID가 여러 개인 경우 &client_id=<IDENTITY_CLIENT_ID>를 추가하세요.
          var identityEndpoint = Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT");
          using var request = identityEndpoint is not null
              ? new HttpRequestMessage(HttpMethod.Get,
                  $"{identityEndpoint}?api-version=2019-08-01&resource={Audience}")
              {
                  Headers = { { "X-IDENTITY-HEADER", Environment.GetEnvironmentVariable("IDENTITY_HEADER") } },
              }
              : new HttpRequestMessage(HttpMethod.Get,
                  $"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource={Audience}")
              {
                  Headers = { { "Metadata", "true" } },
              };
          using var response = await httpClient.SendAsync(request, ct);
          response.EnsureSuccessStatusCode();
          using var json = await JsonDocument.ParseAsync(
              await response.Content.ReadAsStreamAsync(ct), default, ct);
          return json.RootElement.GetProperty("access_token").GetString()!;
      }
  }
  ```

  ```php PHP
  use Anthropic\Client;
  use Anthropic\Lib\Credentials\CredentialResult;
  use Anthropic\Lib\Credentials\IdentityTokenProvider;
  use Anthropic\Lib\Credentials\TokenCache;
  use Anthropic\Lib\Credentials\WorkloadIdentityCredentials;

  // 대상(audience) 앱 등록의 식별자 URI입니다(Register the token audience 참조).
  const AUDIENCE = 'api://<APP_ID>';

  class EntraIdentityToken implements IdentityTokenProvider
  {
      public function getIdentityToken(): string
      {
          // App Service, Functions, Container Apps는 IDENTITY_ENDPOINT를 주입합니다.
          // VM 및 VM Scale Sets는 IMDS를 사용합니다.
          // 사용자 할당 ID가 여러 개인 경우 &client_id=<IDENTITY_CLIENT_ID>를 추가하세요.
          $identityEndpoint = getenv('IDENTITY_ENDPOINT');
          if ($identityEndpoint !== false) {
              $url = $identityEndpoint . '?api-version=2019-08-01&resource=' . AUDIENCE;
              $header = 'X-IDENTITY-HEADER: ' . getenv('IDENTITY_HEADER');
          } else {
              $url = 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=' . AUDIENCE;
              $header = 'Metadata: true';
          }
          $context = stream_context_create([
              'http' => ['header' => $header . "\r\n"],
          ]);
          $body = json_decode(file_get_contents($url, false, $context), true);
          return $body['access_token'];
      }
  }

  $credentials = new CredentialResult(
      provider: new TokenCache(new WorkloadIdentityCredentials(
          identityProvider: new EntraIdentityToken(),
          federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'),
          organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'),
          serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'),
          workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null,
      )),
  );
  $client = new Client(credentials: $credentials);

  $message = $client->messages->create(
      model: 'claude-opus-5-5',
      maxTokens: 1024,
      messages: [['role' => 'user', 'content' => 'Hello from Azure']],
  );
  $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
  echo $textBlock->text, PHP_EOL;
  ```

  ```ruby Ruby
  require "anthropic"
  require "json"
  require "net/http"

  # 대상(audience) 앱 등록의 식별자 URI입니다(토큰 대상 등록 참조).
  AUDIENCE = "api://<APP_ID>"

  def fetch_entra_token
    # App Service, Functions, Container Apps는 IDENTITY_ENDPOINT를 주입합니다.
    # VM 및 VM Scale Sets는 IMDS를 사용합니다.
    # 사용자 할당 ID가 여러 개인 경우 &client_id=<IDENTITY_CLIENT_ID>를 추가하세요.
    if (endpoint = ENV["IDENTITY_ENDPOINT"])
      url = "#{endpoint}?api-version=2019-08-01&resource=#{AUDIENCE}"
      headers = {"X-IDENTITY-HEADER" => ENV.fetch("IDENTITY_HEADER")}
    else
      url = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=#{AUDIENCE}"
      headers = {"Metadata" => "true"}
    end
    response = Net::HTTP.get(URI(url), headers)
    JSON.parse(response).fetch("access_token")
  end

  credentials = Anthropic::WorkloadIdentityCredentials.new(
    identity_token_provider: -> { fetch_entra_token },
    federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"),
    organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"),
    service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"),
    workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"]
  )
  client = Anthropic::Client.new(credentials: credentials)

  message = client.messages.create(
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello from Azure"}]
  )
  puts message.content.find { it.type == :text }.text
  ```

  ```bash CLI
  # Entra에서 발급한 액세스 토큰을 CLI가 읽을 수 있는 파일에 기록합니다.
  # VM 또는 VM Scale Set(IMDS)의 예시입니다. App Service, Functions 또는
  # Container Apps에서는 대신 "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=api://<APP_ID>"에서
  # -H "X-IDENTITY-HEADER: $IDENTITY_HEADER"를 사용하여 가져오세요.
  # 사용자 할당 ID가 여러 개인 경우 &client_id=<IDENTITY_CLIENT_ID>를 추가하세요.
  ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp)
  trap 'rm -f "$ANTHROPIC_IDENTITY_TOKEN_FILE"' EXIT
  curl -sS -H "Metadata: true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://<APP_ID>" \
    | jq -r .access_token > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
  export ANTHROPIC_IDENTITY_TOKEN_FILE

  # ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID는 환경 변수에서 읽어옵니다.
  ant messages create \
    --model claude-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: "Hello from Azure"}'
  ```
</CodeGroup>

### 설정 확인

Azure 리소스에서 [토큰 획득 및 사용](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#acquire-and-use-the-token)에 표시된 cURL 교환을 실행하고, `POST /v1/oauth/token`이 `sk-ant-oat01-`로 시작하는 `access_token`과 초 단위의 `expires_in` 값을 포함한 `200`을 반환하는지 확인하세요. 교환이 불투명한 `401` `authentication_error` 응답(메시지 `Authentication failed`)과 함께 실패하면, Claude Console의 [인증 기록 페이지](https://platform.claude.com/settings/workload-identity-federation?tab=history)에서 거부 사유를 확인한 다음 Entra 토큰을 디코딩하고(명령은 [실패한 교환 문제 해결](https://platform.claude.com/docs/ko/manage-claude/wif-reference#troubleshoot-a-failed-exchange) 참조) 가장 일반적인 Azure 측 원인을 확인하세요:

* **발급자 불일치:** 등록된 `issuer_url`은 토큰의 `iss` 클레임과 정확히 일치해야 합니다. v2.0 토큰은 `https://login.microsoftonline.com/<TENANT_ID>/v2.0`을 포함합니다. 디코딩된 `ver` 클레임이 `1.0`이면 [토큰이 v1.0인 경우](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0)를 참조하세요.
* **토큰 수명:** 관리 ID 토큰은 `iat`와 `exp` 사이가 최대 24시간입니다. 발급자가 여전히 마법사의 `7500`(또는 1시간 기본값)을 사용하는 경우 [Anthropic 구성](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#configure-anthropic)에 설명된 대로 `max_jwt_lifetime_seconds`를 `86400`으로 올리세요.
* **대상 불일치:** 규칙의 `audience`는 토큰의 `aud`와 정확히 일치해야 합니다. 이 가이드가 구성하는 v2.0 토큰의 경우 대상 앱 등록의 클라이언트 ID입니다.
* **클레임 이름 불일치:** 토큰에 없는 클레임과 일치시키는 규칙은 절대 통과하지 않습니다. v1.0 토큰은 클라이언트 ID를 `azp`가 아닌 `appid`에 담습니다. [토큰이 v1.0인 경우](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0)를 참조하세요.

## AKS에서 Entra Workload Identity 사용

워크로드가 AKS 파드에서 실행되는 경우 이 경로를 사용하세요. Entra Workload Identity는 Kubernetes 서비스 계정을 사용자 할당 관리 ID와 페더레이션합니다. Kubernetes는 서비스 계정 토큰(AKS 클러스터의 OIDC 발급자가 서명)을 `AZURE_FEDERATED_TOKEN_FILE`의 경로로 파드에 프로젝션합니다. 이 프로젝션된 토큰은 Entra가 발급한 토큰이 아니므로, 이 페이지에서 설명하는 Entra 매개 경로를 유지하기 위해 워크로드는 2단계 교환을 수행합니다. 먼저 프로젝션된 토큰을 `https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token`(페더레이션된 `client_credentials` 그랜트)에서 Entra 발급 액세스 토큰으로 교환한 다음, 해당 Entra 토큰을 ID 토큰으로 Anthropic SDK에 전달합니다.

<Tip>
  AKS 파드는 대안으로 Entra 교환을 건너뛰고 Kubernetes가 프로젝션한 서비스 계정 토큰을 Anthropic에 직접 제시할 수 있습니다. 이 경로는 Entra 테넌트 대신 AKS 클러스터의 OIDC 발급자를 Anthropic에 등록합니다. 해당 흐름은 [Kubernetes와 함께 WIF 사용하기](https://platform.claude.com/docs/ko/manage-claude/wif-providers/kubernetes)를 참조하세요.
</Tip>

### Entra Workload Identity 구성

<Steps>
  <Step title="클러스터에서 OIDC 발급자 및 워크로드 ID 활성화">
    워크로드 ID를 활성화하면 `azure-workload-identity` 변형(mutating) 웹훅이 자동으로 설치됩니다. AKS가 아닌 클러스터에서만 수동으로 배포하세요. 이후 단계에서 생성할 페더레이션 자격 증명을 위해 클러스터의 OIDC 발급자 URL을 기록해 두세요.

    ```bash
    az aks update \
      --resource-group <RESOURCE_GROUP> \
      --name <CLUSTER_NAME> \
      --enable-oidc-issuer \
      --enable-workload-identity

    AKS_OIDC_ISSUER=$(az aks show \
      --resource-group <RESOURCE_GROUP> \
      --name <CLUSTER_NAME> \
      --query oidcIssuerProfile.issuerUrl -o tsv)
    ```
  </Step>

  <Step title="사용자 할당 관리 ID 생성">
    ID에서 두 가지 값을 기록하세요. **Client ID**는 서비스 계정 어노테이션에 들어가며(파드에 `AZURE_CLIENT_ID`로 주입됨), **Object (principal) ID**는 Anthropic 페더레이션 규칙이 일치시키는 `oid` 클레임으로 나타납니다.

    ```bash
    az identity create \
      --resource-group <RESOURCE_GROUP> \
      --name claude-inference-identity \
      --location <LOCATION>

    # 서비스 계정 어노테이션에 들어가며, 파드에 AZURE_CLIENT_ID로 주입됩니다.
    IDENTITY_CLIENT_ID=$(az identity show \
      --resource-group <RESOURCE_GROUP> \
      --name claude-inference-identity \
      --query clientId -o tsv)

    # 페더레이션 규칙이 매칭하는 oid 클레임으로 나타납니다.
    IDENTITY_OBJECT_ID=$(az identity show \
      --resource-group <RESOURCE_GROUP> \
      --name claude-inference-identity \
      --query principalId -o tsv)
    ```
  </Step>

  <Step title="어노테이션이 지정된 Kubernetes 서비스 계정 생성">
    `azure-workload-identity` 웹훅은 `azure.workload.identity/client-id` 어노테이션을 읽어 파드에 `AZURE_CLIENT_ID`를 주입하며, [토큰 획득 및 사용](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#acquire-and-use-the-token-2)의 샘플은 이를 환경에서 읽습니다.

    ```yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: claude-inference
      namespace: inference
      annotations:
        azure.workload.identity/client-id: <IDENTITY_CLIENT_ID>
    ```
  </Step>

  <Step title="관리 ID에 페더레이션 자격 증명 생성">
    페더레이션 자격 증명은 해당 특정 서비스 계정에 대해 클러스터의 OIDC 발급자를 신뢰합니다. `--audience api://AzureADTokenExchange` 값은 들어오는 Kubernetes 서비스 계정 토큰에 대한 Entra의 고정 대상이며, 앞서 등록한 Claude API 대상과는 무관합니다.

    ```bash
    az identity federated-credential create \
      --resource-group <RESOURCE_GROUP> \
      --identity-name claude-inference-identity \
      --name claude-inference-aks \
      --issuer "$AKS_OIDC_ISSUER" \
      --subject system:serviceaccount:inference:claude-inference \
      --audience api://AzureADTokenExchange
    ```
  </Step>

  <Step title="파드에 레이블을 지정하고 서비스 계정 설정">
    파드는 `azure.workload.identity/use: "true"` 레이블을 가져야 하며 어노테이션이 지정된 서비스 계정으로 실행되어야 합니다. 그러면 웹훅이 `AZURE_FEDERATED_TOKEN_FILE`, `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`를 파드에 주입합니다. `AZURE_FEDERATED_TOKEN_FILE`의 파일에는 AKS 클러스터의 OIDC 발급자가 서명한 Kubernetes 프로젝션 서비스 계정 토큰이 들어 있습니다.

    ```yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: inference-worker
      namespace: inference
      labels:
        azure.workload.identity/use: "true"
    spec:
      serviceAccountName: claude-inference
      containers:
        - name: app
          image: your-registry/inference-worker:latest
    ```
  </Step>

  <Step title="샘플 토큰 디코딩">
    Anthropic 페더레이션 규칙이 보는 토큰은 프로젝션된 파일이 아니라 `client_credentials` 교환이 반환하는 Entra 발급 토큰입니다. 레이블이 지정된 파드 내부에서 [토큰 획득 및 사용](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#acquire-and-use-the-token-2)의 cURL 샘플 1단계를 실행하고 결과를 디코딩하세요. 관리 ID 경로와 동일한 클레임 형태를 가집니다:

    ```json
    {
      "iss": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
      "sub": "9f8e7d6c-1a2b-3c4d-5e6f-...",
      "aud": "<APP_ID>",
      "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...",
      "tid": "<TENANT_ID>",
      "azp": "<IDENTITY_CLIENT_ID>",
      "ver": "2.0",
      "exp": 1775527120
    }
    ```

    `sub`와 `oid`는 관리 ID의 개체 ID이고, `aud`는 대상 앱 등록의 클라이언트 ID이며, `azp`는 관리 ID의 클라이언트 ID(`AZURE_CLIENT_ID`의 값)입니다. 수명은 관리 ID 경로와 다릅니다. `client_credentials` 토큰은 기본적으로 `iat`와 `exp` 사이가 24시간이 아닌 60\~90분의 무작위 기간입니다.
  </Step>
</Steps>

### Anthropic 구성

Claude Console에서 [Workload identity](https://platform.claude.com/settings/workload-identity-federation) 페이지(`platform.claude.com/settings/workload-identity-federation`)로 이동하여 **Connect workload**를 클릭하고 **Microsoft Entra** 타일을 선택하세요. 마법사가 발급자 등록, 서비스 계정 생성, 페더레이션 규칙 생성 과정을 안내합니다.

마법사가 이러한 리소스를 대신 생성해 줍니다. 마법사에 입력하든 [Admin API](https://platform.claude.com/docs/ko/manage-claude/wif-admin-api)로 전송하든 다음 값을 사용하세요:

**페더레이션 발급자:** 마법사의 **Token issuer** 선택기에서 **v2.0 (login.microsoftonline.com)**&#xC744; 선택하세요. (선택기의 기본값은 v1이며, 이 기본값은 여전히 v1.0 토큰을 발급하는 이전 등록을 재사용하는 테넌트를 위해 존재합니다.) Entra는 테넌트별 발급자 URL에 OIDC 디스커버리 문서를 게시하므로 디스커버리 모드를 사용하세요. 페더레이션하는 각 Microsoft Entra 테넌트에는 자체 발급자 레코드가 필요합니다.

```json
{
  "name": "azure-prod-tenant",
  "issuer_url": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
  "jwks": { "type": "discovery" },
  "max_jwt_lifetime_seconds": 7500
}
```

<Warning>
  Connect workload 마법사의 Microsoft Entra 타일은 `max_jwt_lifetime_seconds`를 `7500`(2시간 조금 넘음)으로 설정하여 발급자를 생성하며, 이는 `client_credentials` 토큰의 기본 수명인 60\~90분을 포함합니다. 테넌트 토큰 수명 정책이나 Continuous Access Evaluation(CAE)은 이 수명을 연장할 수 있습니다. 디코딩된 토큰의 `exp`에서 `iat`를 뺀 값이 7500초를 초과하면, Claude Console의 [Workload identity](https://platform.claude.com/settings/workload-identity-federation) 페이지의 **Issuers** 탭에서 발급자를 편집하고 `max_jwt_lifetime_seconds`를 그에 맞게 올리세요. 그렇지 않으면 교환이 불투명한 `401` `authentication_error` 응답(메시지 `Authentication failed`)과 함께 실패합니다. 테넌트에서 [관리 ID 사용](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#use-a-managed-identity)의 관리 ID 워크로드도 실행하는 경우, 두 경로를 모두 포함하는 해당 섹션의 `86400` 값을 사용하세요.
</Warning>

허용되는 수명이 길어지면 유출된 Entra 토큰이 더 오래 교환 가능한 상태로 유지됩니다. 토큰이 유출되면 대응 수단은 페더레이션 규칙을 비활성화하는 것입니다. [규칙 범위 지정](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#scope-your-rule)에 설명된 대로 엄격한 `oid` 일치는 애초에 어떤 ID가 토큰을 교환할 수 있는지를 제한합니다.

**페더레이션 규칙:** 관리 ID의 개체 ID와 테넌트 ID를 일치시키세요. 이 가이드가 구성하는 v2.0 토큰의 경우 `audience` 값은 대상 앱 등록의 클라이언트 ID([토큰 대상 등록](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#register-the-token-audience)의 `<APP_ID>` GUID)입니다. 디코딩된 토큰의 정확한 `aud` 값을 사용하세요.

```json
{
  "name": "azure-inference-worker",
  "issuer_id": "fdis_...",
  "match": {
    "audience": "<APP_ID>",
    "claims": {
      "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...",
      "tid": "<TENANT_ID>"
    }
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}
```

`token_lifetime_seconds`는 Entra 토큰이 아니라 교환이 반환하는 Anthropic 액세스 토큰의 수명이며, SDK가 이를 자동으로 갱신합니다.

### 토큰 획득 및 사용

런타임에 파드는 2단계 교환을 수행합니다. Kubernetes가 프로젝션한 토큰(`AZURE_FEDERATED_TOKEN_FILE`의 파일)을 페더레이션된 `client_credentials` 어설션으로 Entra의 토큰 엔드포인트에 보낸 다음, 결과로 받은 Entra 액세스 토큰을 `POST /v1/oauth/token`에서 교환합니다. 다음 예제에서 보듯이 `identity_token_provider` (typescript: `identityTokenProvider`; php: `identityProvider`; csharp: `IdentityTokenProvider`; go: `option.WithFederationTokenProvider`; java: `IdentityTokenConfig`)를 제공하면 Claude SDK가 두 번째 교환과 갱신 루프를 처리합니다. Java를 제외하고(해당 예제의 주석 참조), 이 옵션은 Entra 토큰 가져오기를 토큰 제공자로 받으며 SDK는 갱신할 때마다 이를 다시 호출합니다. cURL 탭은 원시 흐름을 보여줍니다.

샘플에는 서로 다른 두 개의 클라이언트 ID가 나타납니다. `<APP_ID>`는 [토큰 대상 등록](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#register-the-token-audience)의 대상 앱 등록 클라이언트 ID이며, 스코프 `api://<APP_ID>/.default`는 Entra에 해당 대상을 수신자로 하는 토큰을 요청합니다. `$AZURE_CLIENT_ID`는 웹훅이 주입한 관리 ID의 클라이언트 ID이며 호출자를 식별합니다. 둘을 서로 바꿔 사용하지 마세요.

<Tip>
  워크로드에서 이미 Azure Identity 클라이언트 라이브러리를 사용하고 있다면, 2단계 교환을 직접 수행하는 대신 해당 라이브러리에서 Entra 토큰을 가져오세요(`api://<APP_ID>/.default` 스코프를 사용하는 `DefaultAzureCredential`). 예제와 같은 방식으로 `identity_token_provider` (typescript: `identityTokenProvider`; php: `identityProvider`; csharp: `IdentityTokenProvider`; go: `option.WithFederationTokenProvider`; java: `IdentityTokenConfig`)에 제공하세요. 이 라이브러리는 동일한 `AZURE_FEDERATED_TOKEN_FILE`, `AZURE_CLIENT_ID`, `AZURE_TENANT_ID` 환경 변수를 읽고 Entra 교환을 처리합니다.
</Tip>

<CodeGroup>
  ```bash cURL
  # 1. Kubernetes가 프로젝션한 토큰($AZURE_FEDERATED_TOKEN_FILE 위치)을
  #    Entra에서 발급한 JWT로 교환합니다.
  ENTRA_JWT=$(curl -sS "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \
    -d grant_type=client_credentials \
    -d "client_id=$AZURE_CLIENT_ID" \
    --data-urlencode "scope=api://<APP_ID>/.default" \
    -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
    --data-urlencode "client_assertion@$AZURE_FEDERATED_TOKEN_FILE" \
    | jq -r .access_token)

  # 2. Entra JWT를 Anthropic 액세스 토큰으로 교환합니다.
  ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \
    -H "content-type: application/json" \
    -d @- <<JSON | jq -r .access_token
  {
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": "$ENTRA_JWT",
    "federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
    "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
    "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
    "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
  }
  JSON
  )

  # 3. Claude API를 호출합니다.
  curl -sS https://api.anthropic.com/v1/messages \
    -H "authorization: Bearer $ACCESS_TOKEN" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello from Azure"}]
    }' | jq -r '.content[] | select(.type == "text") | .text'
  ```

  ```python Python
  import os
  from pathlib import Path

  import anthropic
  import requests
  from anthropic import WorkloadIdentityCredentials


  def fetch_entra_token_via_federation() -> str:
      federated_token = Path(os.environ["AZURE_FEDERATED_TOKEN_FILE"]).read_text()
      response = requests.post(
          f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}/oauth2/v2.0/token",
          data={
              "client_id": os.environ["AZURE_CLIENT_ID"],
              "grant_type": "client_credentials",
              "scope": "api://<APP_ID>/.default",
              "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
              "client_assertion": federated_token,
          },
          timeout=5,
      )
      response.raise_for_status()
      return response.json()["access_token"]


  client = anthropic.Anthropic(
      credentials=WorkloadIdentityCredentials(
          identity_token_provider=fetch_entra_token_via_federation,
          federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
          organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
          service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
          workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
      ),
  )

  message = client.messages.create(
      model="claude-opus-5-5",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Hello from Azure"}],
  )
  print(next(block.text for block in message.content if block.type == "text"))
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";
  import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
  import { readFile } from "node:fs/promises";

  async function fetchEntraTokenViaFederation(): Promise<string> {
    const federatedToken = await readFile(process.env.AZURE_FEDERATED_TOKEN_FILE!, "utf8");
    const response = await fetch(
      `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`,
      {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          client_id: process.env.AZURE_CLIENT_ID!,
          grant_type: "client_credentials",
          scope: "api://<APP_ID>/.default",
          client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
          client_assertion: federatedToken
        })
      }
    );
    const body = (await response.json()) as { access_token: string };
    return body.access_token;
  }

  const client = new Anthropic({
    credentials: oidcFederationProvider({
      identityTokenProvider: fetchEntraTokenViaFederation,
      federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
      organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
      serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
      workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
      baseURL: "https://api.anthropic.com",
      fetch
    })
  });

  const message = await client.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello from Azure" }]
  });
  for (const block of message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
  ```

  ```go Go
  package main

  import (
  	"context"
  	"encoding/json"
  	"fmt"
  	"net/http"
  	"net/url"
  	"os"
  	"strings"

  	"github.com/anthropics/anthropic-sdk-go"
  	"github.com/anthropics/anthropic-sdk-go/option"
  )

  func fetchEntraTokenViaFederation(ctx context.Context) (string, error) {
  	federatedToken, err := os.ReadFile(os.Getenv("AZURE_FEDERATED_TOKEN_FILE"))
  	if err != nil {
  		return "", err
  	}
  	form := url.Values{
  		"client_id":             {os.Getenv("AZURE_CLIENT_ID")},
  		"grant_type":            {"client_credentials"},
  		"scope":                 {"api://<APP_ID>/.default"},
  		"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
  		"client_assertion":      {strings.TrimSpace(string(federatedToken))},
  	}
  	tokenURL := "https://login.microsoftonline.com/" + os.Getenv("AZURE_TENANT_ID") + "/oauth2/v2.0/token"
  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
  	if err != nil {
  		return "", err
  	}
  	req.Header.Set("content-type", "application/x-www-form-urlencoded")
  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		return "", err
  	}
  	defer resp.Body.Close()
  	var body struct {
  		AccessToken string `json:"access_token"`
  	}
  	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
  		return "", err
  	}
  	return body.AccessToken, nil
  }

  func main() {
  	client := anthropic.NewClient(
  		option.WithFederationTokenProvider(option.IdentityTokenFunc(fetchEntraTokenViaFederation), option.FederationOptions{
  			FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
  			OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
  			ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
  			WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
  		}),
  	)
  	message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
  		Model:     anthropic.ModelClaudeOpus5_5,
  		MaxTokens: 1024,
  		Messages: []anthropic.MessageParam{
  			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from Azure")),
  		},
  	})
  	if err != nil {
  		panic(err)
  	}
  	for _, block := range message.Content {
  		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
  			fmt.Println(textBlock.Text)
  			break
  		}
  	}
  }
  ```

  ```java Java
  import com.anthropic.config.AuthenticationConfig;
  import com.anthropic.config.AuthenticationType;
  import com.anthropic.config.IdentityTokenConfig;
  import com.anthropic.config.InMemoryProfileConfigProvider;
  import com.anthropic.config.ProfileConfig;
  // ...
  import com.fasterxml.jackson.databind.ObjectMapper;
  import java.net.URI;
  import java.net.URLEncoder;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.util.Map;
  import java.util.stream.Collectors;
  import static java.nio.charset.StandardCharsets.UTF_8;

  void main() throws Exception {
      var form = Map.of(
                      "client_id", System.getenv("AZURE_CLIENT_ID"),
                      "grant_type", "client_credentials",
                      "scope", "api://<APP_ID>/.default",
                      "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                      "client_assertion", Files.readString(Path.of(System.getenv("AZURE_FEDERATED_TOKEN_FILE"))))
              .entrySet().stream()
              .map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), UTF_8))
              .collect(Collectors.joining("&"));
      var request = HttpRequest.newBuilder(URI.create(
                      "https://login.microsoftonline.com/" + System.getenv("AZURE_TENANT_ID") + "/oauth2/v2.0/token"))
              .header("content-type", "application/x-www-form-urlencoded")
              .POST(HttpRequest.BodyPublishers.ofString(form))
              .build();

      // Java SDK는 함수가 아니라 토큰 자체 또는 파일 경로를 받습니다.
      // source("value")를 사용하면 path(...)에 토큰이 담깁니다. SDK는 Anthropic 액세스 토큰을
      // 갱신할 때마다 동일한 토큰을 전송하므로, 토큰이 만료되었거나
      // jti 클레임이 있는 경우 이미 사용되었다면 실패합니다. 장시간 실행되는
      // 프로세스에서는 source("file")을 전달하고 매 갱신 전에
      // 파일에 새 토큰을 기록하세요.
      var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
      String entraToken = new ObjectMapper().readTree(response.body()).get("access_token").asText();

      AnthropicClient client = AnthropicOkHttpClient.builder()
              .configurationProvider(InMemoryProfileConfigProvider.of(ProfileConfig.builder()
                      .organizationId(System.getenv("ANTHROPIC_ORGANIZATION_ID"))
                      .workspaceId(System.getenv("ANTHROPIC_WORKSPACE_ID"))
                      .authentication(AuthenticationConfig.builder()
                              .type(AuthenticationType.OIDC_FEDERATION)
                              .federationRuleId(System.getenv("ANTHROPIC_FEDERATION_RULE_ID"))
                              .serviceAccountId(System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"))
                              .identityToken(IdentityTokenConfig.builder()
                                      .source("value")
                                      .path(entraToken)
                                      .build())
                              .build())
                      .build()))
              .build();

      var message = client.messages().create(MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024)
              .addUserMessage("Hello from Azure")
              .build());

      IO.println(message.content());
  }
  ```

  ```csharp C#
  using Anthropic.Credentials;
  // ...

  var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
  {
      FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!,
      OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"),
      ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"),
      WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"),
      IdentityTokenProvider = new EntraFederationTokenProvider(),
  });
  using var client = new AnthropicClient(new ClientOptions { Credentials = credentials });

  var message = await client.Messages.Create(new()
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello from Azure" }],
  });
  foreach (var block in message.Content)
  {
      if (block.Value is TextBlock textBlock)
      {
          Console.WriteLine(textBlock.Text);
      }
  }

  class EntraFederationTokenProvider : IIdentityTokenProvider
  {
      private static readonly HttpClient Http = new();

      public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
      {
          var federatedToken = await File.ReadAllTextAsync(
              Environment.GetEnvironmentVariable("AZURE_FEDERATED_TOKEN_FILE")!, ct);
          var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
          var form = new FormUrlEncodedContent(new Dictionary<string, string>
          {
              ["client_id"] = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")!,
              ["grant_type"] = "client_credentials",
              ["scope"] = "api://<APP_ID>/.default",
              ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
              ["client_assertion"] = federatedToken,
          });
          var response = await Http.PostAsync(
              $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token", form, ct);
          response.EnsureSuccessStatusCode();
          using var json = await JsonDocument.ParseAsync(
              await response.Content.ReadAsStreamAsync(ct), default, ct);
          return json.RootElement.GetProperty("access_token").GetString()!;
      }
  }
  ```

  ```php PHP
  use Anthropic\Client;
  use Anthropic\Lib\Credentials\CredentialResult;
  use Anthropic\Lib\Credentials\IdentityTokenProvider;
  use Anthropic\Lib\Credentials\TokenCache;
  use Anthropic\Lib\Credentials\WorkloadIdentityCredentials;

  class EntraIdentityTokenViaFederation implements IdentityTokenProvider
  {
      public function getIdentityToken(): string
      {
          $ch = curl_init('https://login.microsoftonline.com/' . getenv('AZURE_TENANT_ID') . '/oauth2/v2.0/token');
          curl_setopt_array($ch, [
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_POSTFIELDS => http_build_query([
                  'client_id' => getenv('AZURE_CLIENT_ID'),
                  'grant_type' => 'client_credentials',
                  'scope' => 'api://<APP_ID>/.default',
                  'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
                  'client_assertion' => file_get_contents(getenv('AZURE_FEDERATED_TOKEN_FILE')),
              ]),
          ]);
          $body = json_decode(curl_exec($ch), true);
          curl_close($ch);
          return $body['access_token'];
      }
  }

  $client = new Client(
      credentials: new CredentialResult(
          provider: new TokenCache(new WorkloadIdentityCredentials(
              identityProvider: new EntraIdentityTokenViaFederation(),
              federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'),
              organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'),
              serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'),
              workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null,
          )),
      ),
  );

  $message = $client->messages->create(
      model: 'claude-opus-5-5',
      maxTokens: 1024,
      messages: [['role' => 'user', 'content' => 'Hello from Azure']],
  );
  $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
  echo $textBlock->text, PHP_EOL;
  ```

  ```ruby Ruby
  require "anthropic"
  require "json"
  require "net/http"

  def fetch_entra_token_via_federation
    tenant_id = ENV.fetch("AZURE_TENANT_ID")
    federated_token = File.read(ENV.fetch("AZURE_FEDERATED_TOKEN_FILE"))
    response = Net::HTTP.post_form(
      URI("https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token"),
      "client_id" => ENV.fetch("AZURE_CLIENT_ID"),
      "grant_type" => "client_credentials",
      "scope" => "api://<APP_ID>/.default",
      "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "client_assertion" => federated_token
    )
    JSON.parse(response.body).fetch("access_token")
  end

  client = Anthropic::Client.new(
    credentials: Anthropic::WorkloadIdentityCredentials.new(
      identity_token_provider: -> { fetch_entra_token_via_federation },
      federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"),
      organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"),
      service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"),
      workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"]
    )
  )

  message = client.messages.create(
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello from Azure"}]
  )
  puts message.content.find { it.type == :text }.text
  ```

  ```bash CLI
  # 1. Kubernetes가 프로젝션한 토큰을 Entra에서 발급한 액세스
  # 토큰으로 교환하고, CLI가 읽을 수 있는 임시 파일에 기록합니다.
  ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp)
  trap 'rm -f "$ANTHROPIC_IDENTITY_TOKEN_FILE"' EXIT
  curl -sS "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \
    -d client_id="$AZURE_CLIENT_ID" \
    -d grant_type=client_credentials \
    --data-urlencode "scope=api://<APP_ID>/.default" \
    -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
    --data-urlencode client_assertion@"$AZURE_FEDERATED_TOKEN_FILE" \
    | jq -r .access_token > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
  export ANTHROPIC_IDENTITY_TOKEN_FILE

  # 2. Claude API를 호출합니다. ANTHROPIC_FEDERATION_RULE_ID,
  # ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID는
  # 환경 변수에서 읽어옵니다.
  ant messages create \
    --model claude-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: "Hello from Azure"}'
  ```
</CodeGroup>

### 설정 확인

레이블이 지정된 파드 내부에서 [토큰 획득 및 사용](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#acquire-and-use-the-token-2)에 표시된 cURL 교환을 실행하고, `POST /v1/oauth/token`이 `sk-ant-oat01-`로 시작하는 `access_token`과 초 단위의 `expires_in` 값을 포함한 `200`을 반환하는지 확인하세요. 교환이 불투명한 `401` `authentication_error` 응답(메시지 `Authentication failed`)과 함께 실패하면, Claude Console의 [인증 기록 페이지](https://platform.claude.com/settings/workload-identity-federation?tab=history)에서 거부 사유를 확인한 다음 1단계의 Entra 발급 토큰을 디코딩하고(명령은 [실패한 교환 문제 해결](https://platform.claude.com/docs/ko/manage-claude/wif-reference#troubleshoot-a-failed-exchange) 참조) 가장 일반적인 Azure 측 원인을 확인하세요:

* **발급자 불일치:** 등록된 `issuer_url`은 토큰의 `iss` 클레임과 정확히 일치해야 합니다. v2.0 토큰은 `https://login.microsoftonline.com/<TENANT_ID>/v2.0`을 포함합니다. 디코딩된 `ver` 클레임이 `1.0`이면 [토큰이 v1.0인 경우](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0)를 참조하세요.
* **토큰 수명:** 테넌트 토큰 수명 정책 또는 CAE가 `client_credentials` 토큰을 7500초 이상으로 연장하는 경우, [Anthropic 구성](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#configure-anthropic-2)에 설명된 대로 발급자의 `max_jwt_lifetime_seconds`를 올리세요.
* **대상 불일치:** 규칙의 `audience`는 토큰의 `aud`와 정확히 일치해야 합니다. 이 가이드가 구성하는 v2.0 토큰의 경우 대상 앱 등록의 클라이언트 ID입니다.
* **클레임 이름 불일치:** 토큰에 없는 클레임과 일치시키는 규칙은 절대 통과하지 않습니다. v1.0 토큰은 클라이언트 ID를 `azp`가 아닌 `appid`에 담습니다. [토큰이 v1.0인 경우](https://platform.claude.com/docs/ko/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0)를 참조하세요.

## 토큰이 v1.0인 경우

이 가이드는 대상 앱 등록을 `api.requestedAccessTokenVersion: 2`로 구성하므로 표시되는 모든 토큰은 v2.0입니다. `requestedAccessTokenVersion`을 설정하지 않은 기존 등록을 재사용하면 Entra는 대신 v1.0 토큰을 발급합니다. 샘플 토큰을 디코딩하고 `ver` 클레임을 확인하세요. `1.0`이면 네 가지가 달라집니다:

* **발급자:** `iss` 클레임은 `https://login.microsoftonline.com/<TENANT_ID>/v2.0` 대신 `https://sts.windows.net/<TENANT_ID>/`입니다. 토큰의 `iss` 클레임에 담긴 그대로 발급자 URL을 등록하세요. 두 URL은 동일한 JWKS를 공유하므로 디스커버리 모드는 어느 쪽에서든 작동합니다.
* **마법사 선택기:** Connect workload 마법사의 **Token issuer** 선택기에서 **v2.0 (login.microsoftonline.com)** 대신 **v1 (sts.windows.net)**&#xC744; 선택하세요.
* **대상:** `aud` 클레임은 등록의 클라이언트 ID가 아니라 `resource`로 전달한 식별자 URI(예: `api://<APP_ID>`)입니다. 페더레이션 규칙의 `audience`를 디코딩된 토큰의 정확한 `aud` 값으로 설정하세요.
* **클라이언트 ID 클레임:** 호출하는 ID의 클라이언트 ID는 `azp`가 아닌 `appid`에 나타납니다. 두 클레임은 같은 토큰에 함께 나타나지 않으므로, `azp`와 일치시키는 규칙은 v1.0 토큰에 대해 절대 통과하지 않습니다.

`oid`, `sub`, `tid` 클레임은 두 버전에서 동일한 값을 가지므로 이 가이드의 나머지 부분은 변경 없이 적용됩니다.

## 규칙 범위 지정

페더레이션 규칙은 `claims` 맵에 추가로(또는 대신) `subject_prefix`로 토큰의 주체(subject)를 일치시킬 수 있습니다. 필드가 결합되는 방식은 [규칙 일치 의미론](https://platform.claude.com/docs/ko/manage-claude/wif-reference#rule-matching-semantics)을 참조하세요. 이러한 ID에 대한 Entra `sub` 값은 고정 길이의 정규 GUID이므로, 전체 36자 개체 ID를 포함하는 `subject_prefix`는 해당 주체와만 일치합니다. 이는 일반적인 `subject_prefix`의 속성이 아니라 Entra 주체 형식의 속성입니다.

<Warning>
  테넌트의 모든 ID가 등록된 대상에 대한 토큰을 요청할 수 있으므로, `audience`와 `tid`만으로는 특정 워크로드를 식별하지 못합니다. `oid` (또는 `azp`/`appid`) 일치를 생략하거나 와일드카드 또는 부분 GUID `subject_prefix`를 사용하는 규칙은 테넌트의 모든 관리 ID와 서비스 주체를 승인합니다.
</Warning>

규칙의 `match` 블록을 사용 사례에 맞는 가장 좁은 범위로 고정하세요:

* **`oid`를 정확한 값으로 일치:** `claims.oid`를 관리 ID의 전체 개체 ID로 설정하세요. 해당 전체 개체 ID로 설정된 `subject_prefix`는 동등합니다(Console 마법사는 둘 다 설정합니다). 의도한 것보다 더 많은 ID와 일치하는 와일드카드 또는 부분 GUID `subject_prefix`는 절대 사용하지 마세요.
* **심층 방어로 `tid` 고정:** 발급자 URL이 이미 테넌트를 고정하지만, `claims.tid`를 추가하면 나중에 발급자 레코드가 편집될 경우의 구성 드리프트를 방지합니다.
* **대상 고정:** 다른 애플리케이션용으로 발급된 토큰이 거부되도록 `audience`를 디코딩된 토큰의 정확한 `aud` 값으로 설정하세요.
* **관리 ID마다 별도의 규칙 사용:** 여러 ID를 승인하는 하나의 규칙 대신 각 ID마다 하나의 규칙을 생성하여, 다른 워크로드에 영향을 주지 않고 단일 워크로드의 액세스를 취소할 수 있도록 하세요.

## 다음 단계

* [Workload Identity Federation](https://platform.claude.com/docs/ko/manage-claude/workload-identity-federation)에서 전체 구성 모델을 검토하세요.
* AWS, Google Cloud, GitHub Actions, Kubernetes에 대한 [제공자 가이드](https://platform.claude.com/docs/ko/manage-claude/workload-identity-federation#identity-providers)를 참조하세요.
* 환경 변수, 프로필 파일, 자격 증명 우선순위는 [WIF 참조](https://platform.claude.com/docs/ko/manage-claude/wif-reference)를 참조하세요.
