"AI 에이전트에 구글 계정 접근 권한을 줬는데, 서버에 저장된 리프레시 토큰이 새면 어떻게 되지?" MCP 서버나 업무 자동화 봇을 운영하는 분들이 늘 안고 있는 걱정입니다. 리프레시 토큰은 오래 살아 있고, 가진 사람이 누구든 새 액세스 토큰을 받아 낼 수 있기 때문입니다. 구글은 2026년 10월 15일 OAuth 플랫폼에서 DPoP(RFC 9449) 지원을 발표했습니다.
결론부터 말하면, DPoP를 쓰면 리프레시 토큰이 클라이언트가 만든 키쌍에 묶여, 토큰만 훔쳐서는 재사용할 수 없습니다. 다만 구글은 리프레시 토큰만 바인딩하고 액세스 토큰은 기존 Bearer 형식을 유지합니다. 구현할 때는 nonce 처리와 use_dpop_nonce 오류 재시도를 반드시 넣어야 합니다.
✅ 이 글에서 얻을 수 있는 것
1. DPoP가 유출된 리프레시 토큰을 무력화하는 원리
2. 구글 구현의 특징(리프레시 토큰만 바인딩, ES256 권장)
3. 구현 4단계와 nonce 오류 처리, 에이전트·MCP 환경 주의점
DPoP란? Bearer 토큰과의 차이

DPoP(Demonstrating Proof of Possession)는 "이 토큰을 쓰는 사람이 정말 그 키를 가진 클라이언트인가"를 매 요청 증명하게 하는 방식입니다. 클라이언트가 비대칭 키쌍(공개 키·개인 키)을 만들고, 토큰을 그 키에 묶습니다. 이후 토큰을 쓸 때마다 개인 키로 서명한 증명 JWT를 함께 보내야 합니다.
| 구분 | Bearer (기존) | DPoP 바인딩 |
|---|---|---|
| 토큰이 유출되면 | 가진 사람 누구나 사용 | 개인 키가 없으면 사용 불가 |
| 요청에 함께 보내는 것 | 토큰 | 토큰 + DPoP 증명 JWT |
| 구글 적용 범위 | 액세스 토큰 (그대로 Bearer) | 리프레시 토큰만 |
| 키 알고리즘 | - | EC P-256(ES256) 권장, RSA는 지원하지만 비권장 |
구글은 왜 리프레시 토큰만 묶을까
구글 개발자 블로그는 이유를 이렇게 설명합니다. 액세스 토큰은 API 호출마다 쓰이는데, 처리량이 많은 API에서 호출마다 서명을 검증하면 연산과 지연 부담이 커집니다. 그래서 수명이 길어 탈취 피해가 큰 리프레시 토큰만 DPoP로 보호하는 쪽을 택했습니다. 가이드의 토큰 응답 예시에서도 token_type은 Bearer로 나옵니다.
즉 액세스 토큰이 새면 그 토큰의 수명 동안은 여전히 쓰일 수 있습니다. DPoP는 "한 번 털리면 계속 새 토큰을 뽑아 가는" 리프레시 토큰 피해를 끊는 장치로 이해하면 됩니다.
📌 함께 보면 좋은 가이드: AI 에이전트 가드레일 & 거버넌스 실전 세미나 — 에이전트 권한 관리를 조직 차원에서 고민한다면
구글 OAuth DPoP 구현 4단계

구글 DPoP 도입 가이드 기준으로 흐름을 정리하면 다음과 같습니다.
1단계: 키쌍 만들기
클라이언트(서버)가 ES256(EC P-256) 키쌍을 만듭니다. 개인 키는 밖으로 나가지 않도록 보관합니다.
2단계: 인가 코드 교환에 DPoP 헤더 넣기
인가 코드를 토큰으로 바꾸는 요청을 토큰 엔드포인트 https://oauth2.googleapis.com/token에 보낼 때 DPoP 헤더(증명 JWT)를 함께 넣습니다. 리프레시 토큰을 받아 묶으려면 인가 요청에 access_type=offline이 있어야 합니다.
3단계: DPoP-Nonce 저장
토큰 응답에 담겨 오는 DPoP-Nonce 값을 저장해 둡니다. 다음 갱신 요청에 이 값이 필요합니다.
4단계: 토큰 갱신 때 nonce를 넣은 증명 보내기
리프레시 토큰으로 새 액세스 토큰을 받을 때, 직전 응답의 DPoP-Nonce를 nonce 클레임에 넣은 새 증명 JWT를 DPoP 헤더로 보냅니다.
증명 JWT에 들어가는 클레임
| 클레임 | 인가 코드 교환 | 토큰 갱신 |
|---|---|---|
htm |
필수 | 필수 |
htu |
필수 | 필수 |
iat |
필수 | 필수 |
jti |
인가 코드의 SHA256 해시(Base64URL) | 무작위 문자열 |
nonce |
- | 직전 응답의 DPoP-Nonce 값 |
htm은 요청의 HTTP 메서드, htu는 요청 대상 URL, iat은 발급 시각, jti는 증명마다 다른 고유값입니다. 인가 코드 교환과 갱신에서 jti를 만드는 방법이 다르다는 점을 놓치기 쉽습니다.
토큰 갱신 때 보내는 증명 JWT의 페이로드는 대략 이런 모양이 됩니다(값은 예시).
{
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"iat": 1792200000,
"jti": "무작위-고유-문자열",
"nonce": "직전 응답의 DPoP-Nonce 값"
}
JWT 헤더 구성과 서명 방법은 RFC 9449와 구글 가이드를 그대로 따르세요. 구글 블로그는 구현을 돕는 DPoP 도입 SKILL.md를 github.com/google/skills 저장소에 올려 두었다고 안내합니다. AI 코딩 도구에 이 파일을 참고로 주고 구현을 맡기는 방법도 생각해 볼 수 있습니다.
nonce 오류(use_dpop_nonce) 처리
DPoP 구현에서 운영 중 가장 자주 마주칠 부분이 nonce입니다. 구글 가이드는 nonce를 다음과 같이 다룹니다.
- nonce는 한 번만 쓸 수 있습니다.
- nonce가 없거나, 잘못됐거나, 만료됐거나, 재사용되면 요청이 거부됩니다.
- 이때 서버는 HTTP 400
use_dpop_nonce오류를 돌려주며, 클라이언트는 이에 대한 재시도 로직을 갖춰야 합니다.
재시도 흐름을 의사 코드로 정리하면 다음과 같습니다.
def refresh_access_token(state):
for attempt in range(2):
proof = make_dpop_proof( # 개인 키로 서명한 증명 JWT
htm="POST",
htu="https://oauth2.googleapis.com/token",
jti=random_string(),
nonce=state.last_dpop_nonce, # 직전 응답의 DPoP-Nonce
)
res = post_token_endpoint(state.refresh_token, dpop_header=proof)
if res.headers.get("DPoP-Nonce"):
state.last_dpop_nonce = res.headers["DPoP-Nonce"] # 항상 최신 값으로 저장
if res.status == 400 and res.error == "use_dpop_nonce":
continue # 새 nonce로 증명을 다시 만들어 한 번 더
return res
raise RuntimeError("DPoP nonce 재시도 실패")
여러 작업자(워커)가 같은 리프레시 토큰을 동시에 갱신하면, nonce가 한 번만 쓰이는 특성 때문에 서로의 nonce를 소모할 수 있습니다. 토큰 갱신은 한 곳에서 순서대로 처리하도록 설계해 두는 편이 안전해 보입니다.
💡 재시도 횟수는 작게 두세요. nonce 문제가 아니라 키나 토큰 자체가 잘못된 경우에는 재시도해도 계속 실패합니다.
브라우저 앱은 BFF로
구글 가이드는 SPA(브라우저에서만 동작하는 단일 페이지 앱)가 DPoP를 직접 쓸 수 없다고 보고, 기밀 클라이언트 역할을 하는 BFF(Backend-for-Frontend)를 거치라고 안내합니다. 브라우저는 BFF와만 통신하고, 리프레시 토큰과 DPoP 키는 서버 쪽 BFF가 보관하는 구조입니다.
한편 구글 개발자 블로그는 BFF, 토큰 중개 백엔드, 브라우저 기반 OAuth 클라이언트 세 가지 모델을 함께 언급합니다. 블로그와 가이드의 표현이 조금 다르므로, 실제 구현은 가이드 기준(BFF 권장)으로 잡는 것이 무난합니다.
AI 에이전트·MCP 환경에서 주의할 점
| 상황 | 구글 안내 |
|---|---|
| MCP·에이전트 같은 다중 홉 환경 | DPoP는 HTTP 헤더 수준에서 동작해, 클라우드 프록시에서 끊기기 쉬운 전송 계층 바인딩보다 유리하다고 설명 |
| 멀티 에이전트 위임 | 소비자 OAuth 엔드포인트는 토큰 교환(RFC 8693)을 지원하지 않으므로 Google Cloud STS 사용 권장 |
| 브라우저 쿠키 탈취 | 쿠키를 기기 하드웨어에 묶는 DBSC(기기 결합 세션 자격 증명)가 DPoP를 보완 |
에이전트가 요청을 여러 단계(프록시, 게이트웨이, MCP 서버)를 거쳐 보내는 구조라면, 전송 계층에 토큰을 묶는 방식은 중간에서 연결이 끊기며 바인딩이 깨질 수 있습니다. 구글은 DPoP가 헤더에 증명을 싣기 때문에 이런 환경에 강하다고 설명합니다. 이 부분은 구글 블로그의 설명이라는 점은 감안해 두세요.
에이전트 A가 받은 권한을 에이전트 B에게 넘겨야 하는 구조라면, 리프레시 토큰을 복사해 넘기는 대신 Google Cloud STS로 위임을 설계하라는 것이 구글의 권장입니다.
📌 함께 보면 좋은 가이드: 회사 정보 유출 방지! 안전한 ChatGPT 보안 설정 및 기업용 Teams 플랜 가이드 — AI 도구의 데이터 보안 설정을 함께 점검하고 싶다면
도입 체크리스트
- 인가 요청에
access_type=offline이 있는지 확인합니다. - ES256 키쌍을 만들고 개인 키 보관 위치를 정합니다.
- 인가 코드 교환 요청에 DPoP 헤더를 넣고,
jti는 인가 코드의 SHA256 해시(Base64URL)로 만듭니다. - 응답의
DPoP-Nonce를 저장하고, 갱신 때nonce클레임에 넣습니다. - HTTP 400
use_dpop_nonce재시도 로직을 넣습니다. - SPA라면 BFF를, 멀티 에이전트 위임이라면 Google Cloud STS를 검토합니다.
운영자 코멘트
발표문과 가이드를 정리하면서 느낀 점은, DPoP가 "액세스 토큰까지 완전히 막는 장치"는 아니라는 것입니다. 구글은 성능 때문에 리프레시 토큰만 묶었으니, 액세스 토큰은 여전히 짧은 수명과 최소 권한(scope)으로 관리해야 합니다. 그래도 에이전트·MCP 서버처럼 리프레시 토큰을 서버에 오래 쥐고 있는 구조라면, 토큰 저장소가 새더라도 재사용을 막을 수 있다는 점에서 도입 가치가 커 보입니다. 구현에서 가장 손이 많이 갈 부분은 nonce 처리이니, 동시 갱신이 일어나는 구조인지부터 먼저 점검해 보시길 권합니다.
맺음말
- 구글은 2026년 10월 15일 OAuth에 DPoP(RFC 9449) 지원을 발표했고, 리프레시 토큰만 키에 묶습니다.
- 키는 ES256, 갱신 요청에는 직전 응답의 DPoP-Nonce를 넣고,
use_dpop_nonce(400) 재시도를 갖춰야 합니다. - SPA는 BFF를 거치고, 멀티 에이전트 위임은 Google Cloud STS를 쓰라는 것이 구글의 안내입니다.
공식 자료
- Announcing DPoP support for Google OAuth: Securing refresh tokens (Google Developers Blog, 2026-10-15)
- DPoP adoption guide (Google Identity, 2026년 10월 확인)
👉 다음에 읽을 글
- AI 에이전트 가드레일 & 거버넌스 실전 세미나 9월 16일 개최 — 에이전트 운영 거버넌스 사례가 궁금하다면
- 챗GPT 해킹 가능할까? GPT-6 아스트라 첫 'Critical' 보안 등급 분석 — AI 모델 보안 위험을 더 알고 싶다면