← 목록으로
보안·개인정보
#DPoP#OAuth2.0#송신자제약토큰#접근토큰보안#API보안
최종 업데이트 · 2026-11-02

DPoP(OAuth 2.0 Demonstrating Proof of Possession)

1. 개요

정의: DPoP(Demonstrating Proof of Possession, RFC 9449)는 OAuth 2.0 클라이언트가 애플리케이션 계층에서 공개키-개인키 쌍을 사용해 "자신이 토큰에 묶인 키의 소유자임"을 매 요청마다 증명하게 함으로써, 탈취된 토큰의 재사용을 막는 송신자 제약(sender-constrained) 토큰 메커니즘이다.

OAuth 2.0이 널리 쓰이면서 가장 끈질긴 위협으로 남은 것이 Bearer(소지자) 토큰의 구조적 약점이다. Bearer 토큰은 이름 그대로 "가진 자가 곧 권한자"다. 즉 토큰을 손에 넣기만 하면 누구든 정당한 클라이언트인 것처럼 자원 서버(Resource Server)에 접근할 수 있다. HTTPS로 전송 구간을 보호하더라도 토큰이 로그·브라우저 저장소·프록시 캐시·악성 확장프로그램·중간자 등을 통해 유출되면 공격자는 유효기간 내내 그 토큰을 그대로 재생(replay)할 수 있다. 접근 토큰의 수명을 짧게 가져가는 완화책이 있지만, 이는 근본 해결이 아니라 노출 창(window)을 줄이는 대증요법에 가깝다.

이 문제를 정면으로 해결하는 방향이 토큰을 특정 키에 묶는 것이다. 토큰을 발급받을 때 클라이언트의 공개키 지문을 토큰 안에 각인해 두고, 이후 자원 접근 시마다 그 공개키에 대응하는 개인키로 서명한 증거(proof)를 함께 제시하게 하면, 토큰만 훔친 공격자는 개인키가 없어 증거를 만들 수 없으므로 토큰이 무력화된다. 이런 접근을 PoP(Proof of Possession) 또는 송신자 제약 토큰이라 부르며, 기존에는 mTLS 기반(RFC 8705) 방식이 표준으로 존재했다. 그러나 mTLS는 전송 계층에서 클라이언트 인증서를 요구하므로 TLS 종료 지점(로드밸런서·CDN·리버스 프록시)에 인증서 정보가 종속되고, 브라우저 기반 SPA(Single Page Application)나 모바일 공개 클라이언트에서는 적용이 어렵다는 현실적 제약이 컸다.

DPoP는 바로 이 공백을 메우기 위해 2023년 9월 IETF가 RFC 9449로 표준화한 애플리케이션 계층의 PoP 기법이다. 전송 계층이 아닌 HTTP 헤더에 JWT 형태의 증거를 실어 보내므로 TLS 종료 구조에 영향받지 않고, 브라우저에서도 Web Crypto API로 키를 비추출(non-extractable) 형태로 생성·보관할 수 있어 공개 클라이언트에 특히 적합하다. 기술사 관점에서 DPoP는 "토큰 탈취를 가정한(assume breach) 제로 트러스트 시대에, 공개 클라이언트까지 포함해 토큰 재사용을 차단하는 경량 표준"으로 자리매김한다.

2. 동작 원리와 구성요소

2.1 전체 구조

DPoP는 인가 서버(Authorization Server, AS)·클라이언트·자원 서버(Resource Server, RS) 3자 사이에서 공개키 지문을 매개로 토큰과 키를 결속한다. 핵심 아이디어는 두 가지다. 첫째, 클라이언트가 자신만 아는 개인키로 각 요청을 서명한 DPoP Proof JWT를 만든다. 둘째, 발급된 접근 토큰 안에 그 개인키에 대응하는 공개키의 지문(cnf.jkt)을 넣어 토큰과 키를 묶는다. 자원 서버는 요청마다 "토큰에 각인된 지문"과 "증거에 담긴 공개키의 지문"이 일치하는지를 대조한다.

flowchart LR
    C["클라이언트(개인키 보유)"]
    AS["인가 서버(AS)"]
    RS["자원 서버(RS)"]
    C -->|"① 토큰요청 + DPoP Proof(공개키 포함)"| AS
    AS -->|"② DPoP 바인딩 토큰(cnf.jkt=공개키 지문)"| C
    C -->|"③ 자원요청 + 접근토큰 + DPoP Proof(ath 포함)"| RS
    RS -->|"④ 지문 대조 성공 시 자원 응답"| C
    AS -.->|"키 지문 각인"| RS

이 구조에서 신뢰의 뿌리는 "개인키는 클라이언트만 가진다"는 단 하나의 가정이다. 공개키·토큰·증거는 모두 네트워크를 떠돌 수 있지만, 개인키로 서명을 만들 수 있는 주체는 정당한 클라이언트뿐이므로, 서버는 서명 검증만으로 "이 요청을 보낸 자가 토큰에 묶인 키의 소유자"임을 확신할 수 있다. 따라서 개인키의 안전한 보관(비추출 키·보안 하드웨어)이 DPoP 보안의 사실상 전부를 떠받친다.

클라이언트는 먼저 키쌍을 만들고(①), 토큰 요청에 공개키(JWK)를 담은 DPoP Proof를 붙여 보낸다. AS는 공개키의 RFC 7638 SHA-256 썸프린트(thumbprint)를 계산해 접근 토큰의 cnf(confirmation) 클레임에 jkt로 각인한 뒤 토큰을 돌려준다(②). 이후 클라이언트는 자원 접근 때마다 같은 개인키로 새 Proof를 서명해 함께 보내고(③), RS는 Proof의 공개키 썸프린트가 토큰의 cnf.jkt와 같은지 검증한 뒤에만 자원을 내준다(④). 토큰 타입도 Bearer가 아니라 DPoP로 바뀌며, 요청 헤더는 Authorization: DPoP <token> 과 DPoP: <proof-jwt> 두 개를 함께 싣는다.

2.2 DPoP Proof JWT의 구조

DPoP Proof는 매 요청마다 새로 생성되는 짧은 수명의 JWT다. JWT의 보호 헤더(protected header)에는 typ: "dpop+jwt"와 서명 알고리즘(alg), 그리고 공개키 전체(jwk)가 들어간다. 중요한 보안 규칙으로, 이 헤더에는 공개키의 공개 성분만 담아야 하며 개인키 성분이 섞여 있으면 검증자는 즉시 거부해야 한다(RFC 9449 §4.2). 공개키를 매 요청에 포함시키는 설계 덕분에 서버는 별도의 키 등록·조회 없이 증거 자체에서 키를 꺼내 검증할 수 있다.

페이로드(클레임)에는 요청을 특정 맥락에 결속하는 값들이 들어간다. 각 요소를 원리와 함께 보면 다음과 같다. htm(HTTP method)과 htu(HTTP URI)는 이 증거가 어떤 메서드로 어떤 엔드포인트를 향한 것인지를 못박아, 한 요청용 증거를 다른 엔드포인트로 돌려쓰지 못하게 한다. iat(issued at)는 발급 시각으로 서버가 허용 시간창을 벗어난 오래된 증거를 걸러내는 근거가 된다. jti(JWT ID)는 증거마다 유일한 식별자로, 서버가 최근 본 jti를 기억해 동일 증거의 재생(replay)을 탐지하게 한다. 자원 요청 단계에서는 ath(access token hash) 클레임이 추가되는데, 이는 함께 제시된 접근 토큰의 base64url(SHA-256(access_token)) 값으로, 증거와 특정 토큰을 추가로 결속한다. 서버가 nonce를 요구하면 nonce 클레임도 포함된다.

위치 필드 의미 보안적 역할
header typ dpop+jwt 고정 타입 혼동(JWT 혼용) 방지
header jwk 공개키(공개 성분만) 서버가 즉석에서 서명 검증
payload htm/htu HTTP 메서드/URI 엔드포인트 간 증거 전용화
payload iat 발급 시각 허용 시간창 초과 증거 차단
payload jti 증거 고유 ID 재생 탐지(서버 캐시 대조)
payload ath 접근 토큰 해시 증거↔토큰 결속(자원 요청 시)
payload nonce 서버 제공 난스 서버 주도 재생 방지·시계 동기 불필요

2.3 키 바인딩과 nonce 메커니즘

토큰과 키의 결속은 cnf.jkt라는 한 쌍의 값으로 이뤄진다. AS는 Proof의 jwk에 대해 RFC 7638 SHA-256 썸프린트를 산출하고, 이를 RFC 7800이 정의한 cnf(confirmation method) 클레임 안의 jkt(JWK SHA-256 Thumbprint) 필드로 접근 토큰에 심는다. 자원 서버는 들어온 Proof의 공개키로 같은 썸프린트를 다시 계산해 토큰의 cnf.jkt와 바이트 단위로 비교한다(RFC 9449 §7). 두 값이 다르면 그 토큰은 이 키의 소유자가 제시한 것이 아니므로 거부된다. 접근 토큰이 JWT가 아닌 불투명(opaque) 토큰이면, RS는 토큰 인트로스펙션(RFC 7662) 응답에서 같은 cnf.jkt를 받아 대조한다.

nonce는 재생 방지를 서버가 주도하게 만드는 장치다. 클라이언트가 iat만으로 신선도를 증명하면 서버는 클라이언트 시계를 신뢰해야 하고 허용 시간창만큼의 재생 여지가 남는다. 이를 없애기 위해 서버는 DPoP-Nonce 응답 헤더로 서버가 생성한 난스를 내려주고, 클라이언트는 다음 Proof의 nonce 클레임에 그 값을 그대로 넣어야 한다. 서버가 난스를 요구했는데 없거나 오래된 난스면 HTTP 401과 함께 use_dpop_nonce 오류를 돌려주고(RFC 9449 §8), 클라이언트는 새 난스로 즉시 재시도한다. 실무 구현은 보통 32바이트 base64url 난스를 5분 안팎의 짧은 TTL로 발급·회전시켜, 서버가 발급한 적 없는 난스를 품은 증거를 모두 걸러낸다.

2.4 구체 예시: 실제 요청이 실린 헤더

개념을 수치와 함께 보면 구조가 분명해진다. 클라이언트가 접근 토큰 eyJ... 로 GET /userinfo 를 호출하는 실제 HTTP 요청은 대략 다음과 같은 모습이다. Authorization 헤더의 스킴이 Bearer가 아니라 DPoP이고, 그 아래에 별도 DPoP 헤더로 증거 JWT가 함께 실린다는 점이 핵심이다.

GET /userinfo HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJhbGciOiJSUzI1NiIsImtpZCI6...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7...

여기서 DPoP 헤더를 디코딩하면 보호 헤더는 {"typ":"dpop+jwt","alg":"ES256","jwk":{...공개키...}} 이고, 페이로드는 {"jti":"-BwC3ESc6acc2lTc","htm":"GET","htu":"https://api.example.com/userinfo","iat":1762041600,"ath":"fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo","nonce":"eyJ7S_..."} 와 같다. 자원 서버는 jwk로 서명을 검증한 뒤, htm·htu가 실제 요청과 같은지, ath가 제시된 토큰의 SHA-256 해시와 같은지, 그리고 공개키 썸프린트가 토큰의 cnf.jkt와 같은지를 차례로 대조한다. jti는 가령 120초 허용창 동안 캐시에 보관해 같은 값이 다시 오면 재생으로 판단한다. 공개키를 매 요청마다 싣는 비용(ES256 기준 수백 바이트)이 의아할 수 있으나, 이는 서버가 사전 키 등록·조회 없이 무상태(stateless)로 검증할 수 있게 하는 설계적 교환이다.

3. 처리 절차(요청 수명주기)

DPoP의 보안은 각 단계에서 무엇을 검증하느냐에 달려 있다. 아래 순서도는 자원 요청 한 건이 통과해야 하는 검증 관문을 보여준다.

sequenceDiagram
    participant C as 클라이언트
    participant RS as 자원서버
    C->>C: "개인키로 Proof JWT 서명(htm,htu,iat,jti,ath,nonce)"
    C->>RS: "Authorization: DPoP token + DPoP: proof"
    RS->>RS: "① 서명검증(jwk로 Proof 서명 확인)"
    RS->>RS: "② htm/htu가 실제 요청과 일치하는가"
    RS->>RS: "③ iat 시간창·jti 중복(재생) 검사"
    RS->>RS: "④ ath=SHA-256(token) 일치 확인"
    RS->>RS: "⑤ jwk 썸프린트 == 토큰 cnf.jkt"
    alt 모든 검증 통과
        RS-->>C: "200 자원 반환"
    else 난스 필요/불일치
        RS-->>C: "401 use_dpop_nonce + DPoP-Nonce"
        C->>RS: "새 nonce로 Proof 재서명 후 재시도"
    end

서명 검증(①)은 증거 자체의 무결성과 키 소유 증명의 뿌리다. htm/htu 대조(②)는 증거가 지금 이 엔드포인트를 향한 것임을 보장해 엔드포인트 간 전용(cross-endpoint reuse)을 막는다. 시간창·jti 검사(③)는 같은 증거를 두 번 쓰는 재생을 차단하는데, 서버는 보통 수십 초~수 분의 허용창 동안 본 jti를 메모리 저장소(예: Redis)에 보관한다. ath 확인(④)은 증거가 바로 이 접근 토큰과 함께 쓰이도록 묶고, 최종적으로 썸프린트 대조(⑤)가 토큰과 키의 소유 일치를 확정한다. 다섯 관문 중 하나라도 실패하면 자원을 내주지 않는다.

한 가지 유의할 운영 포인트는 리프레시 토큰의 결속이다. 공개 클라이언트에서는 접근 토큰뿐 아니라 리프레시 토큰도 DPoP 키에 묶여, 리프레시 토큰이 유출되어도 키 없이는 새 접근 토큰을 받을 수 없다. 반면 기밀 클라이언트(백엔드)는 이미 클라이언트 인증 수단이 있으므로 리프레시 토큰은 클라이언트 자격으로, 접근 토큰은 DPoP 키로 결속되는 식으로 역할이 나뉜다.

4. 비교: Bearer · mTLS 결속 · DPoP

세 방식의 차이는 "무엇으로 토큰을 묶는가"와 "어느 계층에서 증명하는가"에서 갈린다. Bearer는 아무것도 묶지 않아 탈취에 무방비하고, mTLS 결속은 전송 계층 인증서로 강하게 묶지만 공개 클라이언트·TLS 종료 구조에서 다루기 어렵다. DPoP는 애플리케이션 계층 키로 묶어 공개 클라이언트까지 포괄하되, mTLS만큼 전송 구간 자체를 보호하지는 않는다는 점이 본질적 트레이드오프다. 즉 DPoP는 "탈취 후 재사용"을 막지만, 활성 세션을 실시간으로 중계하는 정교한 MITM까지 전부 막아주지는 않는다(그래서 HTTPS·nonce가 전제된다).

구분 Bearer mTLS 결속(RFC 8705) DPoP(RFC 9449)
결속 매개 없음 클라이언트 TLS 인증서 애플리케이션 계층 키쌍
증명 계층 - 전송(TLS) 애플리케이션(HTTP 헤더)
공개 클라이언트 적합성 높음 낮음 높음(브라우저·모바일)
TLS 종료 구조 영향 없음 큼(종료점에 인증서 종속) 없음
탈취 토큰 재사용 취약 차단 차단
주요 비용 - PKI·인프라 요청당 서명·nonce 왕복

실무적 함의는 명확하다. 서버 간(B2B) 통신처럼 PKI가 이미 갖춰지고 TLS 종료를 직접 통제하는 환경에서는 mTLS 결속이 견고하다. 반대로 수많은 브라우저·모바일 앱이 접속하는 공개 API, 특히 금융권의 오픈뱅킹·마이데이터처럼 고가치 토큰을 공개 클라이언트가 다루는 영역에서는 DPoP가 현실적 선택이다. 실제로 영국 오픈뱅킹 및 FAPI 2.0(Financial-grade API) 보안 프로파일은 송신자 제약 수단으로 mTLS와 DPoP를 모두 허용해, 환경에 맞춰 택하도록 한다.

4.1 구체 공격 시나리오로 보는 효과

차이를 수치가 있는 시나리오로 보면 설득력이 커진다. 접근 토큰 수명을 15분으로 설정한 서비스를 가정하자. Bearer 방식에서는 공격자가 로그나 XSS로 토큰을 반출해 자신의 서버로 보내면, 남은 유효기간(최대 15분) 동안 어느 기기에서든 그 토큰을 그대로 재생해 자원에 접근할 수 있다. 토큰 수명을 1분으로 줄이면 창이 좁아지지만 그만큼 토큰 재발급이 잦아져 AS 부하와 사용자 경험이 나빠진다. 즉 Bearer에서 수명 단축은 보안과 비용의 제로섬 교환이다.

같은 공격을 DPoP가 어떻게 무력화하는지 보자. 공격자가 토큰을 반출해도, 자원 서버는 요청에 딸린 Proof JWT의 서명을 공격자가 갖지 못한 개인키 없이는 만들 수 없음을 확인한다. 공격자가 피해자의 과거 Proof까지 함께 훔쳐 재사용하려 해도 htm/htu가 다른 엔드포인트면 거부되고, 같은 엔드포인트라도 jti 중복·iat 시간창·nonce 불일치로 재생이 막힌다. 결과적으로 DPoP는 토큰 수명을 억지로 줄이지 않고도 "반출 후 외부 재사용"이라는 가장 흔한 탈취 경로를 구조적으로 닫아, 보안과 사용성의 제로섬을 완화한다.

5. 심화: 표준화 동향과 적용 사례

DPoP는 2023년 9월 RFC 9449로 확정되면서 "제안"이 아니라 인용 가능한 표준이 되었고, 이후 보안 프로파일들이 빠르게 이를 품었다. 대표적으로 FAPI 2.0 Security Profile은 접근 토큰을 반드시 송신자 제약으로 발급하도록 요구하며 그 수단으로 mTLS 또는 DPoP를 지정한다. 이는 오픈뱅킹·마이데이터처럼 토큰 하나가 곧 계좌·개인정보 접근권인 고위험 도메인에서 Bearer를 사실상 금지하는 흐름으로, 국내 금융보안 가이드도 같은 방향을 따라가고 있다.

구현 생태계도 성숙했다. 서버 측에서는 Spring Security(6.x 이후 DPoP-bound Access Token 지원), Keycloak, Curity, Authlete 등이 발급·검증을 내장했고, 클라이언트 측에서는 브라우저의 Web Crypto API로 비추출 키를 만들어 IndexedDB에 보관하는 패턴이 정착했다. 비추출 키는 자바스크립트가 개인키 원문을 읽어낼 수 없게 하므로, XSS로 토큰이 유출되더라도 공격자가 키를 가져가 다른 기기에서 증거를 위조하기 어렵다. 다만 XSS가 같은 브라우저 안에서 증거 생성 함수를 호출해 요청을 대리 수행하는 공격까지는 막지 못하므로, DPoP는 XSS 대비책이 아니라 토큰 반출(exfiltration) 후 외부 재사용을 막는 수단으로 이해해야 한다.

국내 맥락에서도 DPoP의 적실성은 높다. 마이데이터(본인신용정보관리업)는 정보주체의 위임을 받아 금융기관의 개인신용정보를 API로 전송받는 구조여서, 전송 토큰 하나가 곧 광범위한 개인정보 접근권이 된다. 이런 토큰이 공개 클라이언트(모바일 앱)에서 다뤄질 때 Bearer를 그대로 쓰면 단말 탈취·앱 복제 시 토큰 재사용 위험이 크다. 송신자 제약을 적용하면 토큰이 유출되어도 키가 없는 제3자는 전송 요청을 성사시키지 못하므로, 국내 금융권이 고가치 API에 PoP 계열을 요구하는 흐름은 DPoP의 도입 근거를 강화한다.

예상 출제 방향으로는 "Bearer 토큰의 한계와 송신자 제약 토큰 비교", "DPoP와 mTLS 결속의 장단점 및 선택 기준", "오픈뱅킹/마이데이터 등 고위험 API에서의 토큰 보호 설계", "제로 트러스트 관점의 토큰 재사용 차단" 등이 유력하다. 답안 구성 전략으로는 ① Bearer의 구조적 약점을 먼저 제시하고 ② DPoP의 키 바인딩·증거·nonce 메커니즘을 그림과 함께 전개한 뒤 ③ mTLS 결속과의 비교표로 선택 기준을 정리하고 ④ FAPI 2.0·오픈뱅킹 적용과 공개 클라이언트 적합성으로 마무리하는 구성이 설득력 있다.

6. 고려사항 및 시사점

  • 적용 전략(점진적 도입): 기존 Bearer 기반 시스템을 한 번에 바꾸기 어렵다면, 고위험 엔드포인트(송금·개인정보 조회)부터 DPoP를 적용하고 토큰 타입을 DPoP로 분리해 자원 서버가 엔드포인트별로 결속 요구 수준을 달리하는 점진적 전환이 현실적이다. AS·RS·클라이언트 SDK의 동시 지원이 전제이므로, API 게이트웨이에서 DPoP 검증을 공통 처리해 백엔드 서비스의 변경을 최소화하는 설계가 유효하다.

  • 트레이드오프(보안 vs 성능·복잡도): DPoP는 요청마다 개인키 서명과 서버의 다단계 검증, 그리고 nonce 왕복(최초 1회 추가 라운드트립)을 요구한다. 고TPS 환경에서는 jti 재생 캐시의 저장소 비용과 서명 검증 CPU 부하가 실질적 병목이 될 수 있으므로, nonce TTL·허용 시간창·캐시 만료 정책을 지연과 보안 사이에서 조율해야 한다. 키 서명을 비추출 키·하드웨어(TPM·Secure Enclave)에 위임하면 안전성은 오르지만 처리량은 더 제약된다.

  • 전망(제로 트러스트·PoP의 기본값화): "네트워크 경계를 믿지 않는" 제로 트러스트가 확산되면서 "토큰 소지=권한"이라는 Bearer 가정은 점차 퇴출되고, 송신자 제약이 기본값(secure by default)이 되는 흐름이다. FAPI 2.0이 이를 의무화한 것처럼, 향후 고가치 API 표준은 PoP를 선택이 아닌 필수로 요구할 가능성이 높다.

  • 연계 기술(생태계 통합): DPoP는 단독으로 완결되지 않고 PKCE(인가코드 가로채기 방지), BFF 패턴(브라우저에서 토큰 자체를 숨기는 대안), 토큰 인트로스펙션(불투명 토큰의 cnf 확인), 짧은 토큰 수명과 결합할 때 방어가 중첩된다. 특히 SPA 보안에서는 "토큰을 아예 브라우저에 두지 않는 BFF"와 "토큰을 두되 키로 묶는 DPoP"가 상호 보완적 선택지이므로, 서비스의 아키텍처·운영 역량에 맞춰 조합을 설계해야 한다.

  • 상호운용성과 운영 성숙도: DPoP는 AS·RS·클라이언트가 동일한 알고리즘(ES256 등)과 nonce 정책에 합의해야 동작하므로, 다자간 생태계에서는 지원 알고리즘·DPoP-Nonce 요구 여부·시계 오차 허용창을 상호운용 프로파일로 명문화해 두어야 현장 장애를 줄인다. 또 키 회전(rotation) 시 기존 토큰의 cnf.jkt가 무효화되는 점을 고려해, 재인증·재바인딩 절차와 오류(use_dpop_nonce, invalid_dpop_proof) 처리 로직을 클라이언트 SDK에 표준화해 두는 것이 운영 안정성의 관건이다.

  • 한계의 명확한 인식: DPoP는 토큰의 외부 재사용을 막는 도구이지 XSS·악성 기기 내부에서의 대리 호출, 실시간 중계형 MITM, 사용자 단말 자체의 완전 장악까지 막는 만능 수단이 아니다. 따라서 콘텐츠 보안 정책(CSP)·입력 검증·단말 무결성 점검 등과 다층 방어(defense in depth)로 결합해야 그 가치가 온전해진다.

참고자료


한 줄 요약: DPoP(RFC 9449)는 클라이언트가 개인키로 서명한 Proof JWT와 토큰에 각인된 공개키 지문(cnf.jkt)을 매 요청마다 대조하게 하여, 전송 계층에 의존하지 않고 공개 클라이언트에서도 탈취된 OAuth 토큰의 재사용을 차단하는 송신자 제약 토큰 표준이다.