Spring/Oauth2

Oauth2 PKCE-enhanced Authorization Code Grant

hwanguu 2025. 1. 12. 23:13

PKCE(Proof Key for Code Exchange, RFC - 6749) 개요

 

  • 코드 교환을 위한 증명 키로서 CSRF 및 권한부여코드 삽입 공격을 방지하기 위한 Authorization Code Grant Flow 의 확장버전이다. 
  • 권한부여코드 요청시 Code Verifier와 Code Challenge 를 추가하여 만약 Authorization Code Grant Flow 에서 Authrozization Code 가 탈취당했을 때 Access Token을 발급하지 못하도록 차단한다.
  • PKCE는 원래 모바일 앱에서 Authorization Code Grant Flow 를 보호하도록 설계되었으며 나중에 단일 페이지 앱에서도 사용하도록 권장되으며 모든 유형의OAuth2 클라이언트, 심지어 클라이언트 암호를 사용하는 웹 서버에서 실행되는 앱에도 유용하다.

 

코드 생성

  • 1. Code Verifier
    • ① 권한부여코드 요청 전에 앱이 원래 생성한 PKCE 요청에 대한 코드 검증기
    • ② 48 ~ 128 글자수를 가진 무작위 문자열
    • ③ A-Z a-z 0-9 -._~ 의 ASCII 문자들로만 구성됨
  • 2. Code Challenge
    • ① 선택한 Hash 알고리즘으로 Code Verifier를 Hashing 한 후 Base64 인코딩을 한 값
    • ② ex) Base64Encode(Sha256(ASCII(Code Verifier)))
  • 3. Code Challenge Method
    • ① plain – Code Verifier 가 특정한 알고리즘을 사용하지 않도록 설정
    • ② S256 – Code Verifier 해시 알고리즘 사용하도록 설정

 

처리 흐름

  1. 클라이언트는 code_verifier를 생성하고, code_challenge_method를 사용하여 code_challenge를 계산한다.
  2. 클라이언트가 /authorize에 대한 요청을 작성한다.
  3. 권한 서버가 /authorize에 대한 표준 OAuth2 요청 유효성 검증을 수행한다.
  4. 권한 서버가 code_challenge 및 code_challenge_method의 존재를 확인한다.
  5. 권한 서버가 권한 코드에 대해 code_challenge 및 code_challenge_method를 저장한다.
  6. 권한 서버가 권한 코드 응답을 리턴한다.
  7. 클라이언트가 추가 code_verifier를 포함해 권한 코드를 /token에 제공한다.
  8. 권한 서버가 /token에 대한 표준 OAuth2 요청 유효성 검증을 수행한다.
  9. 권한 서버가 제공된 code_verifier 및 저장된 code_challenge_method를 사용하여 고유 code_challenge를 생성한다.
  10. 권한 서버가 생성된 code_challenge를 /authorize에 대한 초기 요청에 제공된 값과 비교한다.
  11. 두 값이 일치하면 액세스 토큰이 발행되고 일치하지 않으면 요청이 거부된다.

 

code_challenge_method 검증

 

  1. 권한 부여 코드 흐름에 있어 인가서버는 code_verifier를 검증하기 위해 code_challenge_method 을 이미 알고 있어야 한다
  2. 토큰 교환시 code_challenge_method 가 plain 이면 인가서버는 전달된 code_verifier 와 보관하고 있는 code_challenge 문자열과 단순히 일치하는지 확인만 하면 된다
  3. code_challenge_method 가 S256이면 인가서버는 전달된 code_verifier 를 가져와서 동일한 S256 해시 메소드를 사용하여 변환한 다음 보관된 code_challenge 문 자열과 비교해서 일치 여부를 판단한다

 

흐름

 

 

 

실습

기본세팅 : https://hwanguu.tistory.com/71

 

Oauth2 Keycloak Docker compose, 기본세팅

version: '3.9'services: postgres: image: postgres:latest container_name: postgres restart: always environment: POSTGRES_USER: keycloak POSTGRES_PASSWORD: keycloak POSTGRES_DB: keycloak ports: - "5432:5432" keycloak: image: quay.io/keycloak/keycloak:19.0.1

hwanguu.tistory.com

 

code_challenge_method : plain 방식

 

1. code_challenge 생성

https://tonyxu-io.github.io/pkce-generator/

Generate Code Verifier 클릭 후 Generate Code Challenge 클릭Code Challenge 를 복사한후 아래 url 중 code_challenge= 에 붙여넣는다.

 

localhost:8080/realms/oauth2/protocol/openid-connect/auth?response_type=code&client_id=oauth2-client-app&scope=profile email&redirect_uri=http://localhost:8081&code_challenge='여기'&code_challenge_method=plain

 

ex) localhost:8080/realms/oauth2/protocol/openid-connect/auth?response_type=code&client_id=oauth2-client-app&scope=profile email&redirect_uri=http://localhost:8081&code_challenge=-nwsnaOQV3nY8ikTvUSoB8Zm00Z-jfTE9w3bmoGeOqo&code_challenge_method=plain

 

 

2. 인증 서버 로그인

 

3. 로그인 성공시 return 값 확인

http://localhost:8081/?session_state=21b71827-9d9a-4390-93c8-050df234cc03&code=0ae64686-af05-49dc-b1bc-4f0e18367f1f.21b71827-9d9a-4390-93c8-050df234cc03.a86f1300-5de6-4970-b889-a72430531df5

 

 

4. code 값을 통해서 token 요청

Code Verifier 는 1번에서 생성된 Code Challenge 를 넣는다. (plain 이기때문에 Code Challenge와 Code Verifier가 같다)

 

5. return 값 확인

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqOXYyeTNJN0RzTy02aFhqanFGUi1YSURWd2RvWUwyemRfVjN3c05EREFZIn0.eyJleHAiOjE3MzY2OTA1MjgsImlhdCI6MTczNjY5MDIyOCwiYXV0aF90aW1lIjoxNzM2NjkwMjAzLCJqdGkiOiJhN2ZkNTk0ZS1iMGVlLTQ5YmQtODI4Yy03MGNmMTUwYWFhYjMiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL29hdXRoMiIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJiN2JmNzk5OC01Yzk4LTQ4NmItOTdiYi1iODMwMzc0MGRkYjciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJvYXV0aDItY2xpZW50LWFwcCIsInNlc3Npb25fc3RhdGUiOiIyMWI3MTgyNy05ZDlhLTQzOTAtOTNjOC0wNTBkZjIzNGNjMDMiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWZhdWx0LXJvbGVzLW9hdXRoMiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjIxYjcxODI3LTlkOWEtNDM5MC05M2M4LTA1MGRmMjM0Y2MwMyIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6ImNob2kgc2Vod2FuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlciIsImdpdmVuX25hbWUiOiJjaG9pIiwiZmFtaWx5X25hbWUiOiJzZWh3YW4iLCJlbWFpbCI6InVzZXJAbmF2ZXIuY29tIn0.ObuIvS3s33Q7Nffc2vs-Z2OgSHYdKcvjKGUV6KEdyouhCecNVzoI0OexuIjHKszmwj7mXMUogjY3aOduMg7HBo3cIQLmwoTzASXdGrGfODlIEmLYUr8emZc6RGO9MsXxBW9YhPlX7OBvGwTtj_Qwte1d-qPy3Q7H-bsK7mH0AEQa1vs9e4cYhGOiPQBkK9Tzh4dDSDDL98WzDs1sfjPYiBHSW9E1OQ3bQf1-M_4_yJSRikJUE0HLt9w-vg3cHyQkvvOshJ4Bd_aCecS_lzS6B8CtIIxTjE-jAfkNUEe44uA3WXOXo_HXPhCQZz2wSnbO6U6Fhl0d3BXD7bB4VJeU4w",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4NWNmMTZkNS04OWY1LTQxY2EtOWQ4ZS1hMjZiODFiMGVmNjgifQ.eyJleHAiOjE3MzY2OTIwMjgsImlhdCI6MTczNjY5MDIyOCwianRpIjoiYzg4NDk4ZTgtNThlYy00MGY0LTgyNmEtMjQ5NjEwNjkzNDMwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9vYXV0aDIiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL29hdXRoMiIsInN1YiI6ImI3YmY3OTk4LTVjOTgtNDg2Yi05N2JiLWI4MzAzNzQwZGRiNyIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJvYXV0aDItY2xpZW50LWFwcCIsInNlc3Npb25fc3RhdGUiOiIyMWI3MTgyNy05ZDlhLTQzOTAtOTNjOC0wNTBkZjIzNGNjMDMiLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIyMWI3MTgyNy05ZDlhLTQzOTAtOTNjOC0wNTBkZjIzNGNjMDMifQ.LBwnKxOWjKNxgLsi7AbvkimhJUps4YP8fc5dyhJ5zs8",
    "token_type": "Bearer",
    "not-before-policy": 0,
    "session_state": "21b71827-9d9a-4390-93c8-050df234cc03",
    "scope": "email profile"
}

 

 

 

code_challenge_method : s256 방식

https://tonyxu-io.github.io/pkce-generator/

 

Generate Code Verifier 클릭 후 Generate Code Challenge 클릭

Code Challenge 를 복사한후 아래 url 중 code_challenge= 에 붙여넣는다.

 

ex) localhost:8080/realms/oauth2/protocol/openid-connect/auth?response_type=code&client_id=oauth2-client-app&scope=profile email&redirect_uri=http://localhost:8081&code_challenge=YoHoXkH143cNcFIqGhSr2mMpLEEbu_cxOmS_6zwGmKY&code_challenge_method=S256

 

 

2. 인증 서버 로그인

 

3. 로그인 성공시 return 값 확인

http://localhost:8081/?session_state=21b71827-9d9a-4390-93c8-050df234cc03&code=8e1fc7d7-6f05-4919-9223-123ed6ba5bac.21b71827-9d9a-4390-93c8-050df234cc03.a86f1300-5de6-4970-b889-a72430531df5

 

4. code 값을 통해서 token 요청

Code Verifier 는 1번에서 생성된 code_verifier를 넣는다.

 

5. return 값 확인

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqOXYyeTNJN0RzTy02aFhqanFGUi1YSURWd2RvWUwyemRfVjN3c05EREFZIn0.eyJleHAiOjE3MzY2OTEzNzQsImlhdCI6MTczNjY5MTA3NCwiYXV0aF90aW1lIjoxNzM2NjkwMjAzLCJqdGkiOiI5ODFjMzMwYi0yMTUyLTRmM2ItOWViOC00NWQxNmRmM2NmZWIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL29hdXRoMiIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJiN2JmNzk5OC01Yzk4LTQ4NmItOTdiYi1iODMwMzc0MGRkYjciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJvYXV0aDItY2xpZW50LWFwcCIsInNlc3Npb25fc3RhdGUiOiIyMWI3MTgyNy05ZDlhLTQzOTAtOTNjOC0wNTBkZjIzNGNjMDMiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWZhdWx0LXJvbGVzLW9hdXRoMiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjIxYjcxODI3LTlkOWEtNDM5MC05M2M4LTA1MGRmMjM0Y2MwMyIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6ImNob2kgc2Vod2FuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlciIsImdpdmVuX25hbWUiOiJjaG9pIiwiZmFtaWx5X25hbWUiOiJzZWh3YW4iLCJlbWFpbCI6InVzZXJAbmF2ZXIuY29tIn0.M64EDsBoom-7jwbfduFdKBxSWtWLkM8bQk9L8kmuN1p-HLNNsPZX5FFatNenGDntUrHVRrUPKhKoaR3Epw8rKIdfIHS3jhSKD0mYE2OmSFSr9cLbAAkVgFPfdLbUzz83EeOb9u-U76fkxBW24RmBbrVN3-T21HZJTw_jhZUUker-cNbmZfocuB4MXykqgG50ZQXxzL8cY8p8usx7aSqCB6lgGicL4gTzknewuk_wnKir_m4n6jCmGz7WuEalhhwUiAv4pxZ_tTkdEpv7ibgQSC_-Ky0M8Ss3YVW3CGxy1KFvwHNtwPrSpOO2oEvju9-JcCKUVh6_1tMNlaXLTjxZ4g",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4NWNmMTZkNS04OWY1LTQxY2EtOWQ4ZS1hMjZiODFiMGVmNjgifQ.eyJleHAiOjE3MzY2OTI4NzQsImlhdCI6MTczNjY5MTA3NCwianRpIjoiNzRhNDI1N2ItOGI5Mi00MjJhLTg5MWMtNzdiMjNlZTFiOGQzIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9vYXV0aDIiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL29hdXRoMiIsInN1YiI6ImI3YmY3OTk4LTVjOTgtNDg2Yi05N2JiLWI4MzAzNzQwZGRiNyIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJvYXV0aDItY2xpZW50LWFwcCIsInNlc3Npb25fc3RhdGUiOiIyMWI3MTgyNy05ZDlhLTQzOTAtOTNjOC0wNTBkZjIzNGNjMDMiLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIyMWI3MTgyNy05ZDlhLTQzOTAtOTNjOC0wNTBkZjIzNGNjMDMifQ.SGVtJStd5d4Ak2_cDN1HIQ9o2X1fkVyRSN9SOvuYGCk",
    "token_type": "Bearer",
    "not-before-policy": 0,
    "session_state": "21b71827-9d9a-4390-93c8-050df234cc03",
    "scope": "email profile"
}

 

References 및 사진 출처

정수원 스프링 시큐리티 OAuth2

'Spring > Oauth2' 카테고리의 다른 글

ID Token  (0) 2025.01.17
Open ID Connect OIDC  (0) 2025.01.16
Oauth2 Refresh Token Grant  (0) 2025.01.12
Oauth2 Client Credentials Grant  (0) 2025.01.12
Oauth2 Password Grant  (0) 2025.01.12