BTP

Destination 인증 뭘 골라야 하나? #shorts #SAP #BTP

▶ YouTube에서 보기

왜 Destination 인증 방식 선택이 중요한가

SAP BTP에서 외부 시스템을 호출할 때 Destination Service는 "어디로, 어떤 자격으로" 연결할지를 정의합니다. 문제는 인증 타입을 무엇으로 고르느냐에 따라 백엔드 감사 로그에 남는 주체가 완전히 달라진다는 점입니다. 고정 계정(BasicAuth)을 쓰면 모든 호출이 한 명의 사용자로 기록되고, Principal Propagation을 쓰면 실제 로그인한 사용자가 그대로 기록됩니다. 이 글을 읽고 나면 다음을 할 수 있습니다.

  • BasicAuth / OAuth2ClientCredentials / Principal Propagation의 동작 원리 구분
  • 배치·이벤트·사용자 대화형 시나리오별 인증 방식 판단 기준 수립
  • 감사 추적(audit trail)과 권한 통제 관점에서의 영향 평가
  • CAP(Node.js)와 Cloud SDK로 세 방식을 실제 코드에 적용

미리 알아두면 좋은 배경

OAuth 2.0의 기본 흐름(토큰 발급·전달), HTTP 헤더 구조, 그리고 BTP 서브어카운트·XSUAA의 역할을 대략 알고 있으면 좋습니다. CAP 프로젝트를 한 번이라도 배포해 봤다면 예제 코드를 바로 따라올 수 있습니다. S/4HANA 온프레미스 연동 파트는 Cloud Connector 개념(리버스 프록시 아님, 아웃바운드 터널)만 알면 충분합니다.

실습 환경과 준비물

이 글의 예제는 다음 환경을 기준으로 작성했습니다.

  • SAP BTP Cloud Foundry 환경 (Trial 또는 Enterprise 계정), Destination Service lite 플랜
  • Connectivity Service + Cloud Connector 2.16 이상 (온프레미스 Principal Propagation 파트)
  • Node.js 20, @sap/cds 8.x, @sap-cloud-sdk/connectivity·http-client 3.x
  • 백엔드: S/4HANA Cloud Public Edition 또는 S/4HANA 2023 온프레미스(OData V4 API 활성화)

서브어카운트에 Destination Service 인스턴스를 만들고, 애플리케이션에 XSUAA와 함께 바인딩되어 있다고 가정합니다. 사용자 주체 전파 예제에서는 BTP와 백엔드 간 신뢰 설정(트러스트 구성) 권한이 필요합니다.

세 가지 인증 방식, 동작 원리부터 구분하기

비유하자면 회사 건물 출입에 세 가지 방법이 있는 것과 같습니다. BasicAuth는 "팀 공용 출입카드" — 누가 들어갔는지 기록에는 항상 같은 카드 번호만 남습니다. OAuth2ClientCredentials는 "정식 등록된 로봇 사원증" — 여전히 한 명(기술 사용자)이지만, 비밀번호 대신 만료되는 토큰을 쓰고 발급 이력이 관리됩니다. Principal Propagation은 "방문자 각자의 신분증 확인" — 실제 사용자가 자기 신원으로 들어가며, 기록에도 본인이 남습니다.

기술적으로 정리하면 다음과 같습니다.

구분BasicAuthOAuth2ClientCredentialsPrincipal Propagation
백엔드가 보는 주체고정 계정 1개기술 사용자 1개실제 로그인 사용자
자격 증명ID/비밀번호 상시 보관클라이언트 시크릿 또는 X.509단기 토큰/단기 인증서
백엔드 감사 로그전부 동일 사용자로 기록전부 동일 기술 사용자사용자별로 구분 기록
백엔드 권한 체크고정 계정의 권한으로 일괄기술 사용자 권한으로 일괄사용자 개인 권한 적용
적합 시나리오레거시·개발 테스트배치, 이벤트, 시스템 간 통신사용자 대화형 앱

Principal Propagation은 구현 경로가 둘로 나뉩니다. 클라우드→클라우드는 Destination 타입 OAuth2SAMLBearerAssertion(또는 OAuth2UserTokenExchange, OAuth2JWTBearer)로, 사용자의 JWT를 SAML 어서션이나 다른 토큰으로 교환해 전달합니다. 클라우드→온프레미스는 Destination 타입 PrincipalPropagation + Cloud Connector 조합으로, Cloud Connector가 사용자마다 수명이 짧은 X.509 인증서를 발급해 백엔드에 제시합니다. 두 경로 모두 핵심은 같습니다. "앱이 아니라 사용자가 호출 주체"라는 사실이 백엔드까지 살아서 전달된다는 것입니다.

판단 기준은 세 가지 질문으로 압축됩니다.

  1. 호출 시점에 사용자가 존재하는가? 배치 잡·이벤트 핸들러처럼 사용자 세션이 없다면 Principal Propagation은 애초에 불가능 → 기술 사용자(OAuth2ClientCredentials)를 씁니다.
  2. 백엔드에서 사용자별 권한 차등이 필요한가? 영업사원 A는 자기 조직의 판매오더만 봐야 한다면, 그 통제를 백엔드 권한으로 수행하는 Principal Propagation이 일반적으로 권장됩니다.
  3. 감사·컴플라이언스 요건이 있는가? "누가 이 전표를 승인했나"를 백엔드 로그(S/4HANA의 Security Audit Log 등)에서 추적해야 한다면 고정 계정은 탈락입니다. 고정 계정 구조에서는 애플리케이션 로그와 백엔드 로그를 수동으로 대조해야 하고, 이 연결 고리는 감사 시 취약점으로 지적되기 쉽습니다.

BasicAuth는 OAuth를 지원하지 않는 레거시 시스템이나 개발 단계의 임시 연결 정도로 용도를 좁히는 것이 일반적입니다. 비밀번호가 Destination에 상시 저장되고, 주기적 비밀번호 변경 정책과 충돌해 장애 원인이 되기도 합니다.

실전 예제 3단계

1단계 — BasicAuth로 빠르게 연결해 보기 (개발용)

사내 레거시 구매요청(PurchaseRequisition) 시스템이 아직 OAuth를 지원하지 않는 상황을 가정합니다. Destination을 코드로 정의하면 다음과 같습니다(콕핏 UI 입력값과 동일한 구조).

{
  "Name": "LEGACY_PR_SYSTEM",
  "Type": "HTTP",
  "URL": "https://legacy-pr.internal.example.com/odata/v2",
  "ProxyType": "Internet",
  "Authentication": "BasicAuthentication",
  "User": "SVC_BTP_PRREAD",
  "Password": "<스토어에 저장, 코드에 절대 하드코딩 금지>"
}

CAP 서비스에서 이 Destination을 원격 서비스로 연결합니다.

// srv/pr-service.js — @sap/cds 8.x
const cds = require("@sap/cds");

module.exports = class PRService extends cds.ApplicationService {
  async init() {
    const legacyPR = await cds.connect.to("LegacyPR"); // package.json의 cds.requires 참조
    this.on("READ", "PurchaseRequisitions", (req) => legacyPR.run(req.query));
    return super.init();
  }
};
// package.json 발췌 — kind와 destination 매핑
{ "cds": { "requires": { "LegacyPR": {
  "kind": "odata-v2", "model": "srv/external/LegacyPR",
  "[production]": { "credentials": { "destination": "LEGACY_PR_SYSTEM" } }
} } } }

여기까지는 10분이면 됩니다. 하지만 이 순간부터 백엔드 로그에는 모든 조회가 SVC_BTP_PRREAD로만 남습니다. 개발·검증 단계 이상으로 가져가지 않는 것이 일반적으로 안전합니다.

2단계 — OAuth2ClientCredentials로 배치 동기화 (기술 사용자 + 에러 처리·로깅)

매일 새벽 판매오더(SalesOrder) 상태를 동기화하는 잡은 사용자 세션이 없으므로 기술 사용자가 정답입니다. Destination 타입만 바꾸면 토큰 발급·갱신은 서비스가 대신 처리합니다.

{
  "Name": "S4_SALESORDER_SYNC",
  "Type": "HTTP",
  "URL": "https://my300123-api.s4hana.cloud.sap",
  "Authentication": "OAuth2ClientCredentials",
  "tokenServiceURL": "https://my300123.s4hana.cloud.sap/oauth/token",
  "clientId": "sb-so-sync-client",
  "clientSecret": "<시크릿 스토어 관리>"
}
// jobs/sync-salesorders.js — Cloud SDK 3.x, 재시도와 구조화 로깅 포함
const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");
const log = cds.log("so-sync");

async function syncSalesOrders(changedSince) {
  try {
    const res = await executeHttpRequest(
      { destinationName: "S4_SALESORDER_SYNC" },
      { method: "GET",
        url: "/sap/opu/odata4/sap/api_salesorder/srvd_a2x/sap/salesorder/0001/SalesOrder",
        params: { $filter: `LastChangeDateTime gt ${changedSince}`, $top: "500" },
        timeout: 15000 });
    log.info("sync ok", { count: res.data.value.length });
    return res.data.value;
  } catch (e) {
    if (e.response?.status === 401)
      log.error("token/credential 문제 — 시크릿 만료 여부 확인", { dest: "S4_SALESORDER_SYNC" });
    else if (e.response?.status === 429)
      log.warn("rate limit — 백오프 후 재시도");
    else log.error("sync 실패", { status: e.response?.status, msg: e.message });
    throw e; // 잡 스케줄러가 재시도 판단
  }
}

포인트 두 가지입니다. 첫째, 401은 "코드 버그"가 아니라 시크릿 만료·회전 실패인 경우가 많으므로 로그에 원인 후보를 명시합니다. 둘째, 백엔드 감사 로그에는 여전히 기술 사용자 한 명만 남지만, 배치 잡은 원래 그게 정상입니다. 대신 잡 실행 이력(누가 잡을 트리거했는지)은 BTP Audit Log Service 쪽에서 추적합니다.

3단계 — Principal Propagation으로 사용자 신원까지 전달 (프로덕션)

영업사원이 화면에서 자기 판매오더를 조회·수정하는 대화형 앱이라면 사용자 주체 전파로 전환합니다. 클라우드 간 연동은 SAML Bearer 교환 방식 Destination을 씁니다.

{
  "Name": "S4_SALESORDER_USER",
  "Type": "HTTP",
  "URL": "https://my300123-api.s4hana.cloud.sap",
  "Authentication": "OAuth2SAMLBearerAssertion",
  "audience": "https://my300123.s4hana.cloud.sap",
  "tokenServiceURL": "https://my300123.s4hana.cloud.sap/sap/bc/sec/oauth2/token",
  "clientKey": "SO_APP_OAUTH_CLIENT",
  "nameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
  "userIdSource": "email"
}

CAP에서는 요청 사용자의 JWT가 원격 호출에 함께 전달되도록 요청 컨텍스트 안에서 호출하는 것이 핵심입니다.

// srv/salesorder-service.js — 사용자 컨텍스트 유지가 핵심
this.on("READ", "MySalesOrders", async (req) => {
  const s4 = await cds.connect.to("S4SalesOrder");
  // req를 통해 실행하면 사용자 JWT가 토큰 교환에 사용됨
  return s4.tx(req).run(
    SELECT.from("SalesOrder").where({ SoldToParty: req.user.attr.customerScope })
  );
});

온프레미스 S/4HANA라면 Destination을 "ProxyType": "OnPremise", "Authentication": "PrincipalPropagation"으로 바꾸고, Cloud Connector에서 (1) 서브어카운트 신뢰 등록, (2) 시스템 인증서·CA 설정, (3) 백엔드 CERTRULE 트랜잭션으로 인증서→사용자 매핑 규칙을 잡습니다. 배포 전 체크리스트는 세 가지입니다.

  • 보안: 전파 대상 사용자에게 백엔드 최소 권한만 부여했는지(기술 사용자 권한 복사 금지)
  • 테스트: 권한이 다른 두 사용자로 같은 API를 호출해 결과·감사 로그가 각각 본인으로 남는지 확인
  • 성능: 토큰 교환은 사용자별로 발생하므로 Destination Service의 토큰 캐시가 동작하는지(기본 캐시 사용) 부하 테스트로 검증

이 구성이 완료되면 S/4HANA의 Security Audit Log와 변경 이력에 실제 사용자 ID가 남아, "누가 언제 무엇을 했는가"라는 감사 질문에 백엔드 단독으로 답할 수 있게 됩니다.

자주 겪는 문제와 해결 (FAQ)

Q1. OAuth2SAMLBearerAssertion인데 401이 계속 납니다.

십중팔구 신뢰 설정 또는 사용자 매핑 문제입니다. BTP 서브어카운트와 백엔드 간 트러스트가 양방향으로 구성됐는지, 그리고 nameIdFormat·userIdSource가 백엔드의 사용자 식별자와 일치하는지 확인하세요. BTP는 이메일인데 백엔드는 로그온 ID인 경우가 대표적입니다.

Q2. 온프레미스 Principal Propagation에서 백엔드가 여전히 익명/거부 처리합니다.

Cloud Connector의 시스템 CA 인증서를 백엔드 ICM이 신뢰하는지, CERTRULE 매핑 규칙이 실제 인증서의 Subject 패턴과 맞는지 순서대로 점검하세요. Cloud Connector 로그 레벨을 올려 단기 인증서 발급 자체가 성공하는지 먼저 확인하는 것이 빠릅니다.

Q3. 백엔드에서 역할을 바꿨는데 앱에는 한동안 반영이 안 됩니다.

토큰·인증서 캐시 때문입니다. Destination Service와 백엔드 모두 유효기간 동안 이전 권한의 토큰을 재사용할 수 있으므로, 권한 변경 직후에는 캐시 만료(일반적으로 수 분~토큰 수명)까지 기다리거나 짧은 토큰 수명을 설계 단계에서 정해두세요.

Q4. BasicAuth Destination이 갑자기 전부 실패했습니다.

고정 계정의 비밀번호 만료·잠금이 단골 원인입니다. 반복 401이 계정 잠금을 유발해 장애를 키우기도 하므로, 불가피하게 BasicAuth를 쓴다면 비밀번호 무만료 정책의 전용 서비스 계정과 회전 절차를 함께 운영하는 것이 권장됩니다.

이 다음에 살펴볼 주제

여기까지 익혔다면 자연스러운 확장 방향은 세 갈래입니다. 첫째, Destination Service의 X.509(mTLS) 기반 클라이언트 인증 — 시크릿 없는 기술 사용자 구성으로 한 단계 더 안전해집니다. 둘째, SAP Cloud Identity Services(IAS)와의 신뢰 체인 — 사용자 전파의 전제인 ID 매핑을 체계화합니다. 셋째, BTP Audit Log Service로 클라우드 측 감사 기록까지 통합해 엔드투엔드 추적 체계를 완성하는 것입니다.

더 깊이 볼 자료

댓글 0

아직 댓글이 없습니다.