OAuth PKCE: authorization code와 verifier를 묶는 원리
요청별 verifier와 S256 challenge가 탈취된 code의 교환을 제한하는 과정과, PKCE가 액세스 토큰 자체를 보호하지 않는 경계를 설명한다.
발행·오류 제보: infostoa · 편집 기준
이 글의 개별 작성자와 전문 검토자 정보는 제공되지 않습니다.
OAuth PKCE는 authorization code를 토큰으로 바꿀 때, 그 로그인 요청을 시작한 클라이언트가 가진 verifier도 증명하도록 한다. code만 가로챈 공격자는 올바른 verifier 없이 교환을 완료할 수 없도록 설계된다. 이는 code 교환의 보호이며, 발급된 모든 토큰의 탈취나 잘못된 권한 설정까지 해결하는 장치는 아니다.
요청마다 다른 비밀을 준비한다
RFC 7636은 각 authorization 요청을 위해 높은 엔트로피의 code_verifier를 만들도록 정의한다. 허용 문자를 사용한 43~128자 문자열이 규격의 범위다. 길이만 맞춘 예측 가능한 값은 충분한 무작위 비밀과 다르다. 요청마다 새 값을 준비하고 해당 요청의 교환 단계까지 보관해야 한다.
하나의 값을 모든 로그인에 재사용하면 요청별 결합이라는 목적이 약해진다. 또한 여러 창에서 로그인을 시작할 때 마지막 값을 공통 저장 위치에 덮어쓰면 다른 요청의 code와 연결할 수 있다. 구현 검토에서는 난수 생성뿐 아니라 요청과 보관한 verifier가 정확히 짝을 이루는지도 확인해야 한다. 이 저장 방식의 점검은 규격을 적용하는 실무적 권고다.
authorization 요청에는 challenge를 보낸다
S256 방식은 verifier의 ASCII 표현에 SHA-256을 적용하고 결과를 base64url로 표현해 code_challenge를 만든다. authorization 요청에는 이 challenge와 방식 이름을 보낸다. 원래 verifier는 나중의 토큰 요청에 쓰므로 이 단계에 그대로 노출하지 않는다. 두 값의 이름이 비슷해도 전송 목적은 다르다. 패딩 없이 base64url로 표현한다. S256 변환 규정
RFC는 S256을 사용할 수 있는 클라이언트가 이를 사용해야 한다고 명시한다. 일반 base64와 base64url의 차이나 잘못된 문자 인코딩은 서버가 계산한 값과의 불일치를 만들 수 있다. 규격의 예제 입력과 결과로 변환을 확인하면 UI 로그인 전체를 반복하지 않고도 변환 부분의 오류를 분리할 수 있다.
토큰 서버가 연결을 확인한다
서버는 authorization 단계의 challenge를 code와 연결해 두고, 토큰 교환에서 받은 verifier를 같은 방식으로 변환해 비교한다. RFC 7636은 불일치할 때 invalid_grant 오류로 거부하도록 정의한다. 클라이언트가 challenge를 보냈다는 사실만으로 보호가 완성되는 것은 아니다. 교환 단계의 서버 검사가 필요하다. 일치는 다른 검사를 계속할 조건이며 토큰 발급 자체를 보장하지 않는다. 토큰 검증 규정
RFC 9700은 public client에 PKCE 사용을 MUST로 요구하고 confidential client에는 RECOMMENDED로 둔다. state는 응답을 시작한 세션에 연결하고, OpenID Connect의 nonce는 인증 요청과 ID Token을 묶어 반환 claim을 검증한다. 서버가 PKCE를 지원한다고 확인할 수 있을 때만 PKCE를 CSRF 방어로 신뢰할 수 있으며, 그렇지 않으면 안전하게 결합한 일회용 state를 사용한다. 서버는 요청에 challenge가 없었는데 토큰 요청에 verifier가 온 경우 거부해야 downgrade를 막을 수 있다. 이는 S256을 plain으로 낮추는 fallback과 다른 공격 경로다.
다음은 실행 결과가 아닌 거부 동작 테스트 제안입니다: verifier 누락·불일치, 다른 요청의 verifier, challenge 생략 후 verifier 제출, S256 실패 뒤 plain 재시도입니다. 정상 code 교환도 함께 시험하고, 실제 테스트 전에는 통과로 기록하지 마세요. 로그에는 code와 verifier 원문을 남기지 말고, 비밀 없이 성공·실패와 요청 결합 여부만 기록합니다.
실패 시 보호를 끄지 않고 원인을 찾는다
RFC는 S256을 시도한 뒤 plain으로 낮추지 않도록 명시한다. 불일치가 나면 먼저 요청별 verifier 보관, 인코딩, challenge 방식, 서버에 연결된 값을 점검하는 편이 맞다. 보호를 낮춰 성공시키는 것은 정상 교환을 확인하는 방법이 아니다.
도입 완료는 정상 code 교환과 잘못된 verifier 거부를 함께 확인한 상태로 판단할 수 있다. 여러 동시 로그인에서도 요청끼리 값이 섞이지 않는지 확인한다. PKCE를 로그인 전체의 보증으로 쓰기보다 요청 시작과 code 교환을 연결하는 한 통제로 이해하면, 남은 보안 검토의 범위도 명확해진다.
출처
- RFC 7636: verifier, S256, 서버 확인.
- OWASP OAuth2 안내: 요청 결합과 보호 범위.
- RFC 9700: public/confidential client 지침, CSRF 조건과 downgrade 방지.