OAuth 2.1의 PKCE 를 통해 AuthorizationCode 방식 개선하기

BLUECHEAT
10 min readDec 19, 2023

--

이번 시간에는 서비스에서 가장 중요한 인증/인가 부분을 처리하고 있는 OAuth2.1 PKCE 방식에 대해 알아보고자 합니다.

OAuth 2.0 인증 방식이 도입된 지 10년이 넘었습니다(RFC 6749). 이제까지 쌓인 정보가 방대하기 때문에, ‘oauth’라는 검색어로 구글에서 검색하면 다양한 블로그 글을 쉽게 찾아볼 수 있습니다.

하지만 이제는 OAuth 2.0이 아닌, 추가적인 사양이 포함된 OAuth 2.1을 중심으로 논의할 시기가 온 것 같습니다.

저 역시 최근에 OAuth에 대해 다시 공부하기 시작했고, 보안 측면의 개선과 기기 발전에 따른 새로운 인증 방식의 등장을 알게 되었습니다.

이번에는 특히 OAuth 2.1의 PKCE 방식에 대해 자세히 정리해보았습니다.

OAuth2.1이란?

OAuth2.1은 이미 버전에서 힌트를 주고 있듯이, 새로운 인증 방식이라기 보다는 2.0의 보안 및 사용 편의성을 보완하고 있는 프레임워크라고 생각하시면 좋을 것 같습니다.

자세한 내용은 The OAuth 2.1 Authorization Framework 라고도 불리는 스팩으로 정리된 문서가 있습니다.

OAuth2.1에서 달라진 점

  1. PKCE : 모든 OAuth 클라이언트가 Authorization Code Grant flow를 사용할 때 PKCE(Proof Key for Code Exchange)를 필수적으로 사용해야 합니다. 이는 코드 교환 과정의 보안을 강화하기 위함입니다.
  2. 리다이렉트 URI의 정확한 문자열 일치 비교: 리다이렉트 URI는 정확한 문자열 매칭을 사용하여 비교해야 합니다. 이는 보안을 강화하기 위한 조치입니다.
  3. Implicit, Resource Owner Password Credentials 제외: OAuth 2.1 사양에서 제외되었습니다. 이는 보안 취약점을 줄이기 위한 조치입니다.
  4. Bearer Token 사용의 제한: URI의 쿼리 문자열에서 베어러 토큰(bearer tokens) 사용을 금지합니다. 이는 보안 위험을 감소시키기 위함입니다.
  5. 공개 클라이언트의 Refresh token 제한: 공개 클라이언트(public clients)에 대한 리프레시 토큰은 발신자 제약(sender-constrained)이 있거나 일회용(one-time use)이어야 합니다.
  6. 공개 및 기밀 클라이언트의 정의 간소화: 공개 및 기밀 클라이언트의 정의가 클라이언트에 자격 증명이 있는지 여부로만 표시되도록 간소화되었습니다.

위에서 보듯 대개 보안을 위한 여러 변경점이 있었는데요, 그중 주목할 사항으로는 OAuth2.0에서 주로 사용되는 Authorization Code Grant flow 에서 PKCE 방식이 추가되었다는 것을 알 수 있습니다.

PKCE 란?

Proof Key for Code Exchange 의 약어로써 Authorization Code Grant Type의 확장 개념입니다.

SPA와 Native Application은 Reverse engineering에 취약합니다. SPA의 경우 애플리케이션의 소스 코드는 브라우저 내에서 사용되며, Native Application은 디컴파일할 수 있습니다. 이러한 이유로 SPA와Native Application은 클라이언트 자격증명(특히 클라이언트 비밀)을 안전하게 저장할 수 없으며 공용 클라이언트로 간주됩니다.

PKCE에서 추가되는 필드는 다음과 같습니다.

  • code_verifier: 인증 코드(code)를 가로채지 못하도록 하는 임의의 Random key 입니다.
  • code_challenge: code_verifier 값을 code_challenge_method 로 Hashing 한 값입니다.
  • code_challenge_method: code_challenge를 어떤 방식으로 변환할 것인지를 지정합니다.

code_challenge_method의 값으로는 보통 SHA256을 뜻하는 S256, 해시되지 않은 순수 값 plain 이 들어가게 됩니다.

PKCE는 어떻게 동작하는가?

기본적인 동작 방식은 Authorization Code 와 동일하며, code 교환 및 token 교환 시 추가되는 파라미터 존재합니다.

아래는 PKCE 의 전체적인 인증 프로세스입니다. 빨간색 텍스트와 Flow를 주목하면서 순차적으로 확인해 보겠습니다.

  1. 유저가 서비스 사용을 위해 Client application에 접근합니다.
  2. Client application 에서 인증이 필요함을 인지하고 PKCE 수행하기 전 code_verifier, code_challenge 값을 생성합니다. 아래는 예제입니다.
// verifier 생성 샘플코드
function generateVerifier(length) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

for (var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}

return text;
}

// challenge 생성 샘플 코드
async function generateCodeChallenge(codeVerifier, method) {
if (method === 'S256') {
// SHA-256 해시를 계산하고 Base64 URL 인코딩
const hashed = await sha256(codeVerifier);
return base64url(hashed);
} else {
// 'plain' 메소드를 사용할 경우, codeVerifier를 그대로 반환
return codeVerifier;
}
}

3. Authorization Server 측으로 code 인증 방식 요청과 challenge 값을 보냅니다.

  • https://example.com/authorize: 인증 서버의 /authorize 엔드포인트 URL
  • YOUR_CLIENT_ID: 클라이언트 ID
  • YOUR_REDIRECT_URI: 인증 후 사용자가 redirect될 URI 값
  • YOUR_CODE_CHALLENGE: PKCE를 사용하여 생성된 코드 챌린지
  • YOUR_STATE: CSRF 공격을 방지하기 위한 유니크한 상태 값
curl -X GET "https://example.com/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=YOUR_REDIRECT_URI
&code_challenge=YOUR_CODE_CHALLENGE
&code_challenge_method=S256
&state=YOUR_STATE"

4~5. 유저에게 Oauth 인증화면을 보여주고 인증을 시도합니다.

6. redirect_uri 에 code값을 담아서 전달합니다.

7.전달받은 code 값과 code_verifier 를 통해 token 교환을 요청합니다.

curl -X POST https://example.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "grant_type=authorization_code" \
-d "code=YOUR_AUTHORIZATION_CODE" \
-d "redirect_uri=YOUR_REDIRECT_URI" \
-d "code_verifier=YOUR_CODE_VERIFIER"
  • https://example.com/token: 토큰 교환을 위한 서버의 /token 엔드포인트 URL
  • YOUR_CLIENT_ID: 클라이언트 ID
  • YOUR_CLIENT_SECRET: 클라이언트 시크릿. (public 클라이언트인 경우 필요하지 않을 수 있음)
  • YOUR_AUTHORIZATION_CODE: /authorize 엔드포인트에서 받은 인증 코드
  • YOUR_REDIRECT_URI: 인증 후 사용자가 redirect될 URI 값 ( 해당 시점에서 redirect uri가 필요한 이유는 보안과 일관성 때문입니다. )
  • YOUR_CODE_VERIFIER: PKCE 과정에서 생성한 코드 검증

8. 전달받은 값을 통해서 Authorization Server 측에서 code_verifier를 사용하여 code_challenge를 다시 계산하고, 이것이 클라이언트가 처음에 보낸 code_challenge와 일치하는지 확인합니다.

9. 만약 일치하면, 서버는 클라이언트에게 accessToken을 발급합니다.

그렇다면 PKCE 방식이 어떻게 보안을 강화 시켜줄까요? 아래는 위 flow 에서 공격자에 대한 token 탈취 방어를 어떻게 할 수 있는지 작성하였습니다.

PKCE가 공격을 방지할 수 있는 이유는?

  1. code_challenge 전송: 클라이언트는 인증 요청(/authorize)을 보낼 때 code_challenge를 인증 서버에 전송합니다. 이 단계에서 code_challenge는 공개될 수 있으며, 이것 자체로는 보안 위험을 초래하지 않습니다.
  2. Callback code 시 : 사용자가 인증을 완료하면, Authorization Server는 클라이언트에게 code를 반환합니다. 이 코드는 일반적으로 리다이렉션을 통해 클라이언트에게 전달되며, 이 과정에서 공격자가 인증 코드를 가로챌 가능성이 있습니다.
  3. code_verifier의 역할: 공격자가 code를 가로챘다고 해도, code_verifier가 없다면 액세스 토큰을 얻을 수 없습니다. code_verifier는 클라이언트가 보유하고 있으며, /token 요청 시에만 인증 서버에 전송됩니다. 이 단계에서 code_verifier는 암호화되거나 보호되는 채널을 통해 전송되므로, 공격자가 이를 가로채기는 매우 어렵습니다.

결론적으로, 공격자가 code_challenge를 가로챈다 해도, 이 정보만으로는 액세스 토큰을 획득할 수 없습니다.

중요한 것은 code_verifier로, 이는 안전하게 관리되어야 하며, 인증 서버는 /token 요청 시 code_verifier를 사용하여 code_challenge를 검증합니다. 이를 통해 code가 가로채진 경우에도 보안을 유지할 수 있습니다.

참고

--

--