데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      PKCE:OAuth를 더욱더 안전하게 만드는 방법

      youngwon 24.07.10
      8,777 9 2
      DEVOTEE 요약
      OAuth 2.0은 널리 사용되는 인증 프로토콜이지만, 인증 코드 가로채기와 같은 보안 취약점이 있습니다. 이를 해결하기 위해 PKCE(Proof Key for Code Exchange)를 사용하여, 클라이언트가 코드 교환 과정에서 무작위 문자열(code verifier)을 생성하여 보안을 강화합니다. PKCE는 모든 클라이언트에서 적용 가능하며, OAuth 2.1에서는 필수로 권고되는 보안 확장입니다.

      오늘은 OAuth의 보안성을 한층 더 향상시켜줄 수 있는 PKCE에 대해 이야기해보겠습니다.

      OAuth 2.0은 가장 널리 사용되는 인증 프로토콜 중 하나이지만, 기본 구현에는 몇 가지 보안상의 약점이 존재합니다.

      그 중에서도 '인증 코드 가로채기'(Authorization Code Interception Attack) 공격은 큰 위협으로 작용합니다.

      이번 글에서는 이러한 인증 코드 가로채기 공격에 대해 설명하고 이를 효과적으로 방어하기 위해 고안된

      PKCE (Proof Key for Code Exchange) 에 대해 알아보겠습니다.


      들어가기 전에

      들어가기 앞서, 몇 가지 기본 개념과 작동 방식에 대해 간단히 짚고 넘어가겠습니다.

      OAuth 2.0 인증 코드 흐름

      일반적으로 널리 사용되는 OAuth 2.0 인증 흐름입니다.

      1. 사용자 로그인: 사용자가 애플리케이션에서 로그인 버튼을 클릭합니다.

      2. 인증 요청: 애플리케이션이 인증 서버(예: Google)에 인증 요청을 보냅니다.

      3. 사용자 승인: 사용자가 인증 서버에서 자신의 자원에 접근할 수 있는 권한을 애플리케이션에 부여합니다.

      4. 인증 코드 반환: 인증 서버가 애플리케이션에 인증 코드를 반환합니다.

      5. 액세스 토큰 교환: 애플리케이션이 인증 코드를 인증 서버에 다시 보내고 액세스 토큰을 받습니다. 이 토큰을 사용하여 사용자의 자원에 접근할 수 있습니다.

      client_secret

      애플리케이션이 자신을 인증하기 위해 사용하는 비밀 키입니다. 서버는 이 값을 사용해 요청이 신뢰할 수 있는 애플리케이션에서 왔는지 확인합니다.

      공개 클라이언트 (Public Client)

      모바일 앱이나 브라우저 기반 애플리케이션처럼 client_secret을 안전하게 보관할 수 없는 애플리케이션입니다.

      Custom URI Scheme

      애플리케이션이 고유한 URI를 사용하여 다른 애플리케이션과 통신할 수 있게 해주는 방식입니다. 모바일 애플리케이션에서 흔히 사용되며, 브라우저에서 특정 앱을 여는 방식으로 사용됩니다.

      예를 들어, myapp://callback와 같은 URI를 사용할 수 있습니다.


      인증 코드 가로채기

      가로채기 위한 조건

      모든 상황이 위험에 노출되는 것은 아니지만, 특정 조건이 충족되었을 때 공격자가 인증 코드를 가로채는 위험이 발생할 수 있습니다.

      공격자가 인증 코드를 가로채기 위해서는 아래와 같은 조건이 필요합니다.

      • 공격자가 배포한 악성 애플리케이션이 사용자의 기기에 설치된 상태에서 OAuth 2.0 인증을 진행합니다.

      • 공격자는 이미 App Scheme을 변조해두었으며 Client Secret 값을 알아낸 상태여야 합니다.

      Custom URI Scheme

      Custom URI Scheme은 애플리케이션을 식별하기 위한 값이지만, 운영체제에서는 동일한 Custom URI Scheme을 여러 애플리케이션에서 등록할 수 있습니다.

      동일한 Scheme을 처리할 때 어떤 애플리케이션이 URI를 처리할지는 운영체제마다 다를 수 있습니다.

      예를 들어, 안드로이드에서는 사용자가 URI를 처리할 기본 애플리케이션을 선택할 수 있습니다.

      이로 인해 동일한 URI Scheme을 사용하는 여러 애플리케이션이 있을 때, 사용자는 어떤 애플리케이션을 사용할지 선택하게 됩니다.

      client_secret

      client_secret 값은 안전하게 관리되어야 합니다.

      하지만 안전하지 않은 저장소에서 관리되거나 소스코드에 하드코딩이 되어있는 경우, TLS를 사용하지 않는 경우 감청될 수 있는 등 다양한 부분에서 노출의 위험이 존재합니다.

      특히, 웹, 네이티브 등 여러 플랫폼에서 동일한 Client ID와 Client Secret을 사용할 경우 노출 가능성은 더욱 높아질 수 있습니다.

      인증 코드를 통해 토큰을 발급하는 OAuth Token API는 Server to Server 호출을 권장하며,

      이 과정에서 client_secret을 파라메터로 전달하기 때문에 높은 수준의 보안성을 제공하지만 client_secret이 유출된 경우에는 인증 코드 가로채기 공격의 대상이 될 수 있습니다.

      가로채기 과정

      +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
      | End Device (e.g., Smartphone)  |
      |                                |
      | +-------------+   +----------+ | (6) Access Token  +----------+
      | |Legitimate   |   | Malicious|<--------------------|          |
      | |OAuth 2.0 App|   | App      |-------------------->|          |
      | +-------------+   +----------+ | (5) Authorization |          |
      |        |    ^          ^       |        Grant      |          |
      |        |     \         |       |                   |          |
      |        |      \   (4)  |       |                   |          |
      |    (1) |       \  Authz|       |                   |  Authz   |
      |   Authz|        \ Code |       |                   |  Server  |
      | Request|         \     |       |                   |          |
      |        |          \    |       |                   |          |
      |        |           \   |       |                   |          |
      |        v            \  |       |                   |          |
      | +----------------------------+ |                   |          |
      | |                            | | (3) Authz Code    |          |
      | |     Operating System/      |<--------------------|          |
      | |         Browser            |-------------------->|          |
      | |                            | | (2) Authz Request |          |
      | +----------------------------+ |                   +----------+
      +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
      1. 스마트폰 등의 기기에서 네이티브 애플리케이션이 브라우저/운영 체제를 통해 OAuth 2.0 인증 요청을 보냅니다.

        • 이 때  리디렉션 엔드포인트 URI는 Custom URI Scheme 을 사용합니다. ( myapp://callback )

      2. 인증 서버로 인증을 요청합니다.

      3. 인증 서버가 인증 코드를 반환합니다.

      4. 제공된 리디렉션 엔드포인트 URI를 통해 인증 코드가 요청자에게 반환됩니다.

        • 리디렉션

          • 인증 서버가 인증 코드를 myapp://callback URI로 반환할 때, OS 는 이 URI를 처리할 수 있는 애플리케이션을 찾습니다.

        • 악성 앱이 실행

          • 만약 악성 애플리케이션이 이 URI를 먼저 핸들링하도록 설정되어 있다면, 인증 코드는 악성 애플리케이션으로 전달됩니다.

          • 만일 사용자가 실수로 'FakeBankApp'을 선택하면, 인증 코드는 'FakeBankApp'으로 전달됩니다.

        • 코드 가로채기

          • 악성 애플리케이션은 이 인증 코드를 사용하여 OAuth 2.0 인증 서버에 접근하고, 정당한 애플리케이션 대신 액세스 토큰을 요청할 수 있습니다.

      5. 액세스 토큰 요청

        • 악성 애플리케이션이 액세스 토큰을 획득하면, client secret 값을 포함하여 OAuth 2.0 서버에 엑세스 토큰을 요청합니다.

      6. 사용자의 액세스 토큰이 응답됩니다. 이제 공격자는 발급된 토큰을 통해 사용자의 자원에 접근할 수 있습니다.


      공격 방어하기

      이런 공격을 방지하기 위해 등장한 개념이 PKCE입니다.

      *URI 스킴 대신 앱 링크(App Links)나 유니버설 링크(Universal Links)를 사용하는 방법도 있습니다.

      PKCE는 "Proof Key for Code Exchange"의 약자로, '코드 교환을 위한 증명 키'라는 의미입니다.

      PKCE는 가로채기 공격을 완화하기 위해 "code verifier"라는 암호학적으로 안전한 무작위 문자열을 사용합니다.

      매 인증 요청마다 고유한 "code verifier"가 생성하며 이를 변환한 "code challenge" 값이 인증 서버로 전송되어 인증 코드를 얻습니다.

      그런 다음, 인증 코드를 "code verifier"와 함께 토큰 엔드포인트로 전송합니다.

      서버는 이전에 받은 "code challenge"와 비교하여 클라이언트의 코드 검증자 소유 증명을 수행합니다.

      이 과정은 TLS를 통해 전송되어 가로챌 수 없기 때문에, 공격자가 이 일회용 키를 알 수 없으므로 효과적인 방어책으로 작동합니다.


      PKCE

                                                       +-------------------+
                                                       |   Authz Server    |
             +--------+                                | +---------------+ |
             |        |--(A)- Authorization Request ---->|               | |
             |        |       + t(code_verifier), t_m  | | Authorization | |
             |        |                                | |    Endpoint   | |
             |        |<-(B)---- Authorization Code -----|               | |
             |        |                                | +---------------+ |
             | Client |                                |                   |
             |        |                                | +---------------+ |
             |        |--(C)-- Access Token Request ---->|               | |
             |        |          + code_verifier       | |    Token      | |
             |        |                                | |   Endpoint    | |
             |        |<-(D)------ Access Token ---------|               | |
             +--------+                                | +---------------+ |
                                                       +-------------------+ 

      PKCE 를 통한 인증 Flow 는 아래와 같습니다.

      1. 클라이언트:

        • "code_verifier" 를 생성합니다.

        • 이를 변환 (t, transformation) 하여 "code_challenge"를 만듭니다. 변환 방법은 "plain" 또는 "S256"입니다.

        • 변환 방법 (t_m, transformation method) 과 함께 OAuth 2.0 인증 요청에 "code_challenge"를 포함시켜 전송합니다.

      2. 인증 엔드포인트

        1. 요청을 받고 응답을 보내면서 "code_challenge"와 변환 방법을 세션에 기록합니다.

      3. 클라이언트:

      4. 인증서버

        1. 인증 코드를 받은 후, 액세스 토큰 요청에 "code_verifier"를 포함하여 보냅니다

        2. "code_verifier"를 수신하고, 이를 변환하여 기록된 "code_challenge"와 비교합니다.

        3. 값이 일치하지 않으면 접근을 거부합니다.


      Code Verifier

      PKCE 에서 클라이언트가 생성하는 암호학적으로 안전한 무작위 문자열입니다.

      Code Verifier는 예측할 수 없도록 최소 256비트의 충분한 엔트로피를 가져야 합니다.

      따라서 적절한 난수 생성기를 사용하여 32옥텟(32바이트) 시퀀스 를 생성한 후, 이를 base64url로 인코딩하여 43옥텟의 URL Safe String 생성해야 합니다.

      • 32바이트 데이터

        • 난수 생성기로 생성된 32바이트 데이터는 256 비트입니다.

      • base64url 인코딩

        • base64url 인코딩은 6비트 단위로 데이터를 인코딩합니다. (256비트 / 6비트 = 약 43문자)

      Code verifier를 해싱할 때 salt를 사용해야 할까?

      PKCE에서 사용되는 code verifier는 32바이트 길이의 임의 문자열로 높은 엔트로피를 가지고 있습니다. 

      높은 엔트로피를 가진 code verifier는 자체적으로 충분히 안전하므로 추가적인 공개 값(salt)을 포함시켜 해싱할 필요가 없습니다.

      과거에는 공격자가 미리 해시 값을 계산하여 디스크에 저장해두고, 이를 조회하는 방식으로 공격을 시도하곤 했습니다.

      이 방식은 저엔트로피 비밀번호의 경우 효과적이었지만, 높은 엔트로피를 가진 code verifier에는 적용하기 어렵습니다.

      높은 엔트로피를 가진 문자열은 가능한 조합이 너무 많아 미리 계산하고 저장하는 것이 비효율적이기 때문입니다.


      결론적으로 높은 엔트로피를 가진 code verifier는 추가적인 salt 없이도 충분히 안전합니다.

      생성예시

       public static String generateCodeVerifier() {
              SecureRandom secureRandom = new SecureRandom();
              byte[] codeVerifierBytes = new byte[32];
              secureRandom.nextBytes(codeVerifierBytes);
              
              Encoder encoder = Base64.getUrlEncoder().withoutPadding();
              return encoder.encodeToString(codeVerifierBytes);
          }


      Code Challenge

      Code Challenge는 "code verifier"를 변환한 값으로, 인증 요청 시 인증 서버에 전송되어 클라이언트의 소유 증명을 확인하는 데 사용됩니다.

      클라이언트는 code verifier 로부터 다음의 변환 중 하나를 사용하여 코드 챌린지를 생성합니다

      변환 방법

      code_challenge_method

      code_challenge 생성 방법

      설명

      plain

      code_challenge = code_verifier

      변환 없이 코드 검증자를 그대로 사용합니다.

      S256

      code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

      code verifier를 SHA-256으로 해시하고, 이를 base64url로 인코딩합니다.

      주의할 점

      "plain" 메소드는 "code verifier"를 변환하지 않고 그대로 사용하는 방법입니다. 이 메소드는 이미 보호된 요청 경로가 있는 구현과의 호환성을 위해 존재합니다.

      "plain" 메소드를 사용할 경우, Code Verifier 가 변환 없이 그대로 전송되기 때문에, Code Verifier가 애플리케이션과 서버 간의 통신 경로에서 노출될 가능성이 큽니다.

      따라서 새로운 구현에서는 기술적 이유로 "S256"을 지원할 수 없는 경우를 제외하고 "plain"을 사용해서는 안 됩니다.

      생성예시

      public static String generateCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
              // ASCII 인코딩
              byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
      
              // SHA-256 해시 계산
              MessageDigest md = MessageDigest.getInstance("SHA-256");
              byte[] digest = md.digest(bytes);
      
              // BASE64URL 인코딩
              Encoder encoder = Base64.getUrlEncoder().withoutPadding();
              return encoder.encodeToString(digest);
          }

      생성규칙

      Code Verifier 와 Code Challenge 는 아래와 같은 규칙을 따라 생성되어야 합니다.

      요소

      설명

      예시 문자

      ALPHA

      대문자 A-Z 및 소문자 a-z

      A, B, C, ..., Z, a, b, c, ..., z

      DIGIT

      숫자 0-9

      0, 1, 2, ..., 9

      예약되지 않은 문자

      Section 2.3 of [RFC3986]에 정의된 특수한 의미로 사용되지 않는 문자

      "-", ".", "_", "~"


      교환, 증명 과정

      1. [클라이언트]  인증 요청과 함께 Code Challenge 전송

      클라이언트는 OAuth 2.0 Authorize 요청을 보낼 때 아래 파라메터를 포함합니다.

      필드 이름

      필수 여부

      설명

      기본값

      사용 가능한 값

      code_challenge

      필수

      클라이언트가 생성한 코드 챌린지.

      없음

      임의의 문자열

      code_challenge_method

      선택 사항

      코드 챌린지 생성 방법을 지정. 이 매개변수가 없으면 기본값은 "plain".

      "plain"

      "S256", "plain"

      2. [서버] 코드 반환

      서버는 클라이언트가 보낸 인증 요청을 처리하고 인증이 성공하면 인증 코드를 반환합니다.

      "code_challenge"와 "code_challenge_method" 값을 인증 세션에 저장합니다.

      보안을 위해 암호화된 형태로 저장할 수 있습니다.

      3. [클라이언트] 인증 코드와 code_verifier 를 OAuth Token 엔드포인트로 전송

      필드 이름

      필수 여부

      설명

      기본값

      사용 가능한 값

      code_verifier

      필수

      초기 코드 검증자.

      없음

      임의의 문자열

      클라이언트가 인증 코드를 받은 후, 액세스 토큰을 요청하기 위해 토큰 엔드포인트로 요청을 보냅니다. 아래 파라메터를 포함합니다.

      4. [서버] code_verifier 검증

      토큰 엔드포인트에서 요청을 받은 후, 서버는 받은 code_verifier로부터 code_challenge 를 계산하고, 세션에 저장된 code_challenge와 비교하여 검증합니다.

      클라이언트가 지정한 code_challenge_method에 따라 변환을 수행합니다.

      Method

      변환 방식

      검증 비교

      S256

      BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

      BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge

      plain

      plain

      code_verifier == code_challenge

      에제 코드

      // 요청에서 code_verifier 획득
      String codeVerifierParameter = request.getParameter("code_verifier");
      
      // 저장된 세션에서  code_challenge와 method 획득
      String storedCodeChallenge = session.get("code_challenge");
      String storedCodeChallengeMethod = session.getAttribute("code_challenge_method");
              
      // code_challenge 생성  
      String generatedCodeChallenge = generateCodeChallenge(codeVerifierParameter, storedCodeChallengeMethod);
      
      // 검증
      boolean isValid = generatedChallenge.equals(storedCodeChallenge); 
      
      
      public static String generateCodeChallenge(String codeVerifier, String codeChallengeMethod) throws NoSuchAlgorithmException {
              if ("plain".equalsIgnoreCase(codeChallengeMethod)) {
                  return codeVerifier;
              } else if ("S256".equalsIgnoreCase(codeChallengeMethod)) {
                  byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
                  MessageDigest md = MessageDigest.getInstance("SHA-256");
                  byte[] digest = md.digest(bytes);
                  return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
              } else {
                  throw new IllegalArgumentException("invalid code_challenge_method");
              }
      }


      마치며

      PKCE(Proof Key for Code Exchange)는 OAuth 2.0 Authorization Code Grant Type의 보안성을 크게 향상시키는 중요한 확장 입니다.

      PKCE는 인증 코드 가로채기 공격을 효과적으로 방지하여 보안을 강화합니다. 또한, 웹, 모바일, 데스크탑 등 모든 클라이언트에서 사용할 수 있어 유연성을 제공합니다.

      대부분의 IDP(Identity Provider)도 PKCE를 지원하고 있어 외부 IDP 의 OAuth 를 이용하는 경우에도 적용이 용이하며

      대다수의 OAuth 라이브러리에서 PKCE를 지원하기 때문에 간단하게 구현할 수 있는 장점도 있습니다.

      현재 논의되고 있는 OAuth 2.1에서는 Code Authorization 방식의 인증을 사용할 때 PKCE를 필수로 사용하도록 권고하고 있습니다.

      이러한 점들을 종합해 볼 떄 OAuth 를 구현했거나 사용중이시라면 PKCE를 적용하여 하여 보다 안전한 인증 환경을 구축하면 서비스에 많은 도움이 될 것 같습니다 😀


      이 문서가 PKCE의 이해에 도움이 되었기를 바랍니다.


      감사합니다.


      Reference

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      youngwon 님의 최신 블로그

      더보기
      동영상 기고하기