콘텐츠로 이동

Entra 앱 등록

FDAI에 필요한 세 개의 Entra ID 앱 등록을 만드는 방법을 알아봅니다. 등록 대상은 fdai-api(Operator API 대상), fdai-console-spa(SPA 로그인 클라이언트), fdai-approval-bot(Teams 승인 클라이언트)을 만들고, 로그인과 Teams OBO(on-behalf-of) 권한 부여에 필요한 App Role, 서비스 principal 및 역할 할당도 함께 생성합니다. 이 런북은 로컬 로그인 테스트 (console/README.md § 로컬 로그인 테스트)와 deploy-and-onboard.md 및 user-rbac-and-identity.md § 10 가 참조하는 배포 시점 설정을 모두 다룹니다.

고객 환경에 독립적인 예제입니다. 아래의 모든 ID는 셸 변수 또는 <placeholder>입니다. 실제 테넌트, 앱, 범위 GUID를 버전 관리 대상 파일에 붙여 넣지 마세요. Git 추적에서 제외된 .env.local이나 시크릿 저장소에만 보관하세요.

등록목적핵심 설정
fdai-api콘솔, ChatOps 백엔드, 내부 첨부 수집 엔드포인트가 사용하는 Web API의 대상(audience)입니다.애플리케이션 ID URI api://<api-app-id>, 위임된 권한 범위 access 하나, 사람용 App Role 다섯 개, 애플리케이션 전용 첨부 역할 하나, v2 액세스 토큰을 설정합니다.
fdai-console-spaSPA 로그인 클라이언트(MSAL, PKCE)입니다.SPA 리디렉션 URI와 fdai-api의 access 범위에 대한 위임된 권한을 설정합니다.
fdai-approval-botTeams 승인 클라이언트입니다.OBO 토큰용 fdai-api의 access 범위에 대한 위임된 권한을 설정합니다. 이 런북은 클라이언트 시크릿을 만들지 않습니다.

어느 앱 등록도 실행기 신원을 갖지 않습니다. 실행기에는 별도의 사용자 할당 Managed Identity를 사용합니다 (security-and-identity-ko.md).

  • az가 대상 테넌트에 로그인한 상태여야 합니다. 각 단계 전에 확인합니다.

    Terminal window
    az account show --query "{sub:id, tenant:tenantId, user:user.name}" -o json
  • 앱 등록을 만들고 관리자 동의를 부여할 수 있는 디렉터리 역할이 필요합니다. Application Administrator, Cloud Application Administrator 또는 Global Administrator가 해당합니다.

  • 자동 배포를 사용하는 경우 자체 호스팅 실행기 Managed Identity를 fdai-console-spa의 소유자로 지정하고, Microsoft Graph Application.ReadWrite.OwnedBy 애플리케이션 권한에 관리자 동의를 부여합니다. 그러면 작업 흐름은 해당 신원이 소유한 앱만 업데이트할 수 있습니다.

fdaictl provision entra는 사람 승인 증거와 제어 읽기를 분리합니다. 먼저 원래 Azure CLI 컨텍스트에서 로그인한 사람의 신원과 현재 aw-approvers 멤버십을 읽습니다. 프로세스 전체에 적용되는 직렬화 경계는 이 사람 단계와 그 안에 중첩된 실행기 단계가 다른 신원 작업과 섞이지 않도록 합니다. 그런 다음 target-profile v2 실행기 토큰과 현재 활성 테넌트 및 구독이 정확히 일치하는지 검증하고, Premium 플랜, 정확히 지정된 역할 그룹 5개, Conditional Access, Access Reviews, 인증 방법, Azure Policy 할당, 실행기 신원 및 모든 앱/그룹 계획 갱신을 읽습니다. 최초 관찰, 승인 재검증, claim 복구, 증적 재조회에도 같은 실행기 경계를 사용합니다. 실행기 컨텍스트, 대상, 권한 또는 읽기 결과가 없거나 일치하지 않으면 작업을 차단합니다. 이러한 읽기를 사람 컨텍스트에서 대신 수행하지 않습니다.

배포 소유자는 명령을 실행하기 전에 해당 읽기 엔드포인트에 필요한 최소 권한의 Microsoft Graph 애플리케이션 권한을 실행기 서비스 principal에 부여하고 테넌트 관리자 동의를 완료해야 합니다. 여기에는 Access Review 정의를 위한 AccessReview.Read.All과 인증 방법 정책을 위한 Policy.Read.AuthenticationMethod가 포함됩니다. 또한 실행기에는 프로필에 바인딩된 범위의 정책 할당을 읽을 수 있는 Azure RBAC 권한이 필요합니다.

이러한 애플리케이션 권한을 대화형 Azure CLI 범위로 다시 요청하지 마세요. 사람 로그인에 사용하는 Azure CLI는 Microsoft 자사 클라이언트이므로 이 권한을 동적으로 요청할 수 없으며 AADSTS65002를 반환합니다. 프로비저닝 명령은 미리 구성한 권한과 제어를 검증할 뿐입니다. Graph 권한 부여, 관리자 동의 또는 신원 제어 변경은 수행하지 않습니다.

Terminal window
# Single-tenant API app.
API_APPID=$(az ad app create \
--display-name "fdai-api" \
--sign-in-audience AzureADMyOrg \
--query appId -o tsv)
# Five human App Roles plus one application-only attachment role.
python3 - <<'PY' > /tmp/fdai_approles.json
import json, uuid
roles = [
("Reader", "View the operator console", ["User"]),
("Contributor", "Reader plus author draft governance PRs", ["User"]),
("Approver", "Contributor plus review and approve governance PRs and HIL", ["User"]),
("Owner", "Full administration of the fork's control plane", ["User"]),
("BreakGlass", "Segregated emergency access (never auto-activated)", ["User"]),
(
"Document.ChannelAttachment.Submit",
"Submit bounded channel attachments to the internal ingestion intake",
["Application"],
),
]
print(json.dumps([{
"allowedMemberTypes": members, "description": d, "displayName": n,
"id": str(uuid.uuid4()), "isEnabled": True, "value": n,
} for n, d, members in roles]))
PY
az ad app update --id "$API_APPID" --app-roles @/tmp/fdai_approles.json
az ad app update --id "$API_APPID" --identifier-uris "api://$API_APPID"

access 위임 권한 범위를 추가하고 v2 액세스 토큰을 사용하도록 설정합니다. 그러면 iss가 검증기의 기본 발급자인 .../v2.0이 됩니다.

Terminal window
API_OBJID=$(az ad app show --id "$API_APPID" --query id -o tsv)
SCOPE_GUID=$(python3 -c "import uuid; print(uuid.uuid4())")
python3 - "$SCOPE_GUID" <<'PY' > /tmp/fdai_api_scope.json
import json, sys
print(json.dumps({"api": {
"requestedAccessTokenVersion": 2,
"oauth2PermissionScopes": [{
"id": sys.argv[1],
"adminConsentDescription": "Allow the console to call the fdai Operator API on behalf of the signed-in operator",
"adminConsentDisplayName": "Access the fdai Operator API",
"userConsentDescription": "Allow the console to call the fdai Operator API on your behalf",
"userConsentDisplayName": "Access the fdai Operator API",
"isEnabled": True, "type": "User", "value": "access",
}],
}}))
PY
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/applications/$API_OBJID" \
--headers "Content-Type=application/json" \
--body @/tmp/fdai_api_scope.json
Terminal window
SPA_APPID=$(az ad app create \
--display-name "fdai-console-spa" \
--sign-in-audience AzureADMyOrg \
--query appId -o tsv)
SPA_OBJID=$(az ad app show --id "$SPA_APPID" --query id -o tsv)
# Seed local Vite origins here. The deploy workflow adds the deployed console
# HTTPS origin after Terraform creates the Static Web App.
SCOPE_GUID=$(az ad app show --id "$API_APPID" \
--query "api.oauth2PermissionScopes[?value=='access'].id | [0]" -o tsv)
python3 - "$API_APPID" "$SCOPE_GUID" <<'PY' > /tmp/fdai_spa.json
import json, sys
print(json.dumps({
"spa": {"redirectUris": ["http://localhost:5273", "http://127.0.0.1:5273"]},
"requiredResourceAccess": [{
"resourceAppId": sys.argv[1],
"resourceAccess": [{"id": sys.argv[2], "type": "Scope"}],
}],
}))
PY
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/applications/$SPA_OBJID" \
--headers "Content-Type=application/json" \
--body @/tmp/fdai_spa.json
Terminal window
APPROVAL_BOT_APPID=$(az ad app create \
--display-name "fdai-approval-bot" \
--sign-in-audience AzureADMyOrg \
--query appId -o tsv)
APPROVAL_BOT_OBJID=$(az ad app show --id "$APPROVAL_BOT_APPID" --query id -o tsv)
python3 - "$API_APPID" "$SCOPE_GUID" <<'PY' > /tmp/fdai_approval_bot.json
import json, sys
print(json.dumps({
"requiredResourceAccess": [{
"resourceAppId": sys.argv[1],
"resourceAccess": [{"id": sys.argv[2], "type": "Scope"}],
}],
}))
PY
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/applications/$APPROVAL_BOT_OBJID" \
--headers "Content-Type=application/json" \
--body @/tmp/fdai_approval_bot.json

보호된 부트스트랩은 관련 없는 위임된 권한을 보존하고 서비스 principal을 만든 다음 정확한 fdai-api 범위를 검증합니다. 테넌트 동의, Teams 설치, Bot 리소스와 해당 Managed Identity, 그룹에 연결된 승인 대상은 프로바이더가 호스팅하는 별도 단계에서 설정합니다.

console: prepare full stack 작업은 console/.env.local에서 로컬 테넌트와 SPA 클라이언트 값을 읽고 scripts/deployment/azure/sync-entra-spa-redirect.py를 통해 두 개의 고정 Vite 출처를 안전하게 재시도할 수 있는 방식으로 동기화합니다. 보조 도구는 기존 리디렉션을 모두 보존합니다. --allow-loopback-http 옵션은 localhost 또는 127.0.0.1에만 HTTP를 허용하며 기본 명령은 계속 HTTPS만 허용합니다. 테넌트가 다르거나 Microsoft Graph 권한이 부족하면 로컬 서비스가 시작되기 전에 준비 단계가 중단됩니다.

배포된 리디렉션 URI 동기화 유지

섹션 제목: “배포된 리디렉션 URI 동기화 유지”

각 배포 대상에 다음 GitHub Actions 저장소 변수를 설정합니다.

변수값
AZURE_TENANT_ID대상 Entra 테넌트 ID입니다.
ENTRA_CONSOLE_SPA_CLIENT_ID해당 테넌트의 fdai-console-spa 애플리케이션 클라이언트 ID입니다.

deploy-dev.yml을 apply=true 및 deploy_console=true로 실행하면 Terraform의 console_default_hostname을 읽고 scripts/deployment/azure/sync-entra-spa-redirect.py를 실행합니다. 이 보조 도구는 다음 작업을 수행합니다.

  1. 활성 Azure CLI 테넌트가 AZURE_TENANT_ID와 같은지 확인합니다.
  2. 기존 SPA 리디렉션 URI를 모두 보존하고, 배포된 HTTPS 출처가 없을 때만 추가합니다.
  3. 앱 등록을 다시 읽고 새 URI가 보이지 않으면 배포를 실패 처리합니다.

이 작업은 안전하게 재시도할 수 있습니다. 같은 테넌트의 다른 구독은 같은 해당 테넌트의 앱 등록을 사용합니다. 다른 테넌트에 배포하려면 해당 테넌트의 SPA 클라이언트 ID와 그 테넌트가 소유한 실행기 신원이 필요합니다. 변수가 없거나 테넌트가 일치하지 않거나 Graph 권한이 부족하면, 로그인 설정이 불완전한 채로 남지 않도록 배포가 중단됩니다.

Terminal window
# Enterprise apps (needed for App Role assignment + admin consent).
az ad sp create --id "$API_APPID"
az ad sp create --id "$SPA_APPID"
az ad sp create --id "$APPROVAL_BOT_APPID"
# Assign a user the Reader App Role on fdai-api (repeat per user/role).
USER_OBJID=$(az ad signed-in-user show --query id -o tsv) # or another user's id
API_SP_OBJID=$(az ad sp show --id "$API_APPID" --query id -o tsv)
READER_ROLE_ID=$(az ad app show --id "$API_APPID" \
--query "appRoles[?value=='Reader'].id | [0]" -o tsv)
python3 - "$USER_OBJID" "$API_SP_OBJID" "$READER_ROLE_ID" <<'PY' > /tmp/fdai_assign.json
import json, sys
print(json.dumps({"principalId": sys.argv[1], "resourceId": sys.argv[2], "appRoleId": sys.argv[3]}))
PY
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$API_SP_OBJID/appRoleAssignedTo" \
--headers "Content-Type=application/json" \
--body @/tmp/fdai_assign.json
# One-time admin consent so a signed-in user gets no consent prompt.
az ad app permission admin-consent --id "$SPA_APPID"
az ad app permission admin-consent --id "$APPROVAL_BOT_APPID"

실제 배포에서는 App Roles를 개별 사용자가 아니라 다섯 개의 aw-* Entra 보안 그룹에 할당하세요 (user-rbac-and-identity-ko.md § 4.4).

전용 Operator channel-edge Managed Identity가 생성되면 해당 서비스 principal에는 애플리케이션 전용 첨부 역할만 할당합니다. 이 신원에 사람용 FDAI 역할을 할당하지 마세요.

Terminal window
EDGE_CLIENT_ID=<channel-edge-managed-identity-client-id>
EDGE_SP_OBJID=$(az ad sp show --id "$EDGE_CLIENT_ID" --query id -o tsv)
ATTACHMENT_ROLE_ID=$(az ad app show --id "$API_APPID" \
--query "appRoles[?value=='Document.ChannelAttachment.Submit'].id | [0]" -o tsv)
python3 - "$EDGE_SP_OBJID" "$API_SP_OBJID" "$ATTACHMENT_ROLE_ID" <<'PY' \
> /tmp/fdai_channel_attachment_assign.json
import json, sys
print(json.dumps({"principalId": sys.argv[1], "resourceId": sys.argv[2], "appRoleId": sys.argv[3]}))
PY
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$API_SP_OBJID/appRoleAssignedTo" \
--headers "Content-Type=application/json" \
--body @/tmp/fdai_channel_attachment_assign.json

채널 경계 계층에서 첨부를 활성화하기 전에 내부 채널 수집 엔드포인트를 활성화합니다. 경계 계층은 시작 시 정확한 API 대상(audience)을 지정해 토큰을 요청하고 인증된 수집 엔드포인트의 상태 점검을 호출합니다. 역할 정의, 할당 또는 audience가 없으면 경계 계층은 준비 상태가 되지 않습니다.

위 단계에서 얻은 값은 런타임 구성에 사용합니다. 버전 관리 대상 파일 밖에 보관하세요.

값Operator API 환경 변수SPA 환경 변수(Vite)
테넌트 IDFDAI_ENTRA_TENANT_IDVITE_MSAL_TENANT_ID
api://$API_APPIDFDAI_API_AUDIENCE-
api://$API_APPID/access-VITE_MSAL_API_SCOPE
$SPA_APPID-VITE_MSAL_CLIENT_ID
$APPROVAL_BOT_APPIDFDAI_TEAMS_APPLICATION_ID-

Operator API 검증기의 환경 변수는 deploy-and-onboard.md를 참조하세요. FDAI_ENTRA_TENANT_ID, FDAI_API_AUDIENCE를 설정하며, FDAI_ENTRA_ISSUER, FDAI_ENTRA_JWKS_URI는 선택 사항입니다. SPA 환경 변수는 console/README.md § 포크 구성을 참조하세요.

Terminal window
az ad app show --id "$API_APPID" \
--query "{uri:identifierUris, tokenVer:api.requestedAccessTokenVersion, \
scopes:api.oauth2PermissionScopes[].value, roles:appRoles[].value}" -o json
az ad app show --id "$SPA_APPID" \
--query "{spa:spa.redirectUris, perms:requiredResourceAccess[].resourceAppId}" -o json
az ad app show --id "$APPROVAL_BOT_APPID" \
--query "{perms:requiredResourceAccess[].resourceAppId}" -o json

그런 다음 console/README.md의 로컬 로그인 테스트를 실행합니다. 토큰 없는 요청은 401, App Role이 없는 로그인 사용자의 요청은 403을 반환해야 합니다. Reader 역할이 있는 사용자는 콘솔을 열 수 있어야 합니다.

Terminal window
az ad app delete --id "$SPA_APPID"
az ad app delete --id "$APPROVAL_BOT_APPID"
az ad app delete --id "$API_APPID"

앱 등록을 삭제하면 해당 서비스 principal과 역할 할당도 함께 제거됩니다. 클라이언트 시크릿을 추가했다면 먼저 교체하세요. 위 절차에서는 시크릿을 추가하지 않습니다. SPA는 공용 클라이언트이고 API는 토큰을 검증하며, 승인 봇 인증은 배포가 소유하는 Managed Identity 바인딩으로 남습니다.