Keycloak and Enterprise Identity
Completing the Authorization Code + PKCE Flow
한국어 원문으로 표시합니다.
목표
authorization code + PKCE 흐름을 SDK 없이 처음부터 끝까지 수행하고, PKCE 와 state 가 각각 무엇을 막는지 실패 케이스로 확인한다.
왜 중요한가
OIDC 라이브러리를 쓰면 이 흐름은 함수 호출 두 번입니다. 그래서 무엇이 일어나는지 모르는 채로 쓰게 되고, 문제가 생기면 어디를 봐야 할지 모릅니다. 이 실습에서 각 단계를 손으로 하면 세 가지가 분명해집니다. 첫째, 인가 코드가 브라우저 URL 에 노출되지만 왜 그것만으로는 쓸모없는지. 둘째, code_verifier 를 모르면 왜 교환이 실패하는지 — 6번 스텝에서 직접 실패시켜 봅니다. 셋째, state 가 없으면 왜 공격자가 피해자를 자기 계정에 로그인시킬 수 있는지. 마지막 리프레시 회전까지 보면 실무에서 권장되는 조합(짧은 액세스 토큰 + 리프레시 회전)이 왜 블랙리스트 없이도 무효화 효과를 내는지 이해됩니다.
Keycloak 시작에 40~90초가 걸리므로 앞선 실습을 마치고 오면 대개 준비돼 있습니다.
단계
/root/pk/pkce.py로code_verifier(43~128자, URL 안전 문자)와code_challenge를 만들어/root/pk/verifier.txt와/root/pk/challenge.txt에 각각 한 줄씩 저장한다./root/pk/authz_url.txt에 인가 요청 URL 을 한 줄 적는다. 대상은 사전 임포트된 렐름labhub의 공개 클라이언트web-app이고 리다이렉트 URI 는http://127.0.0.1:8161/callback이다.response_type=code,client_id,redirect_uri,scope=openid,state,code_challenge,code_challenge_method=S256이 모두 있어야 한다.- 쿠키 항아리를 유지하며 그 URL 을 GET 해 로그인 폼의
actionURL 을/root/pk/action.txt에 저장한다.http로 시작해야 한다. - 사용자
dev1/ 비밀번호Dev1!pass를 POST 해 리다이렉트 응답의Location에서 인가 코드를 뽑아/root/pk/code.txt에 저장한다. 20자 이상이어야 한다. - 토큰 엔드포인트에
grant_type=authorization_code, 코드,redirect_uri,client_id,code_verifier를 보내 토큰을 받는다./root/pk/tokens.json에access_token과refresh_token이 있어야 한다. - 새 인가 코드를 받아 틀린
code_verifier로 교환을 시도한다./root/pk/pkce_fail.json의error가invalid_grant여야 한다. /root/pk/state_check.py는 인자 두 개(보낸 state, 받은 state)를 받아 같으면 종료 코드 0, 다르면 1 로 끝난다. 일치·불일치 두 경우의 결과를/root/pk/state.out에match=ok mismatch=rejected로 적는다.- 리프레시 토큰으로 갱신해
/root/pk/refresh.json을 만든다./root/pk/rotation.txt에rotated=<true|false>를 적는다. 판단 근거는 새 리프레시 토큰이 이전과 다른지이다.
참고
- challenge 계산:
base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=') - 쿠키 유지:
curl -c /root/pk/cookies -b /root/pk/cookies ... - 리다이렉트를 따라가지 않아야 Location 헤더를 볼 수 있습니다(
-i와 함께-L없이). - 흔한 실수 1: 해시 결과를 16진 문자열로 만들어 Base64 하는 것 — 이진 다이제스트를 인코딩해야 합니다.
- 흔한 실수 2: 토큰 교환의
redirect_uri가 인가 요청 때와 달라invalid_grant가 나는 것.
code_verifier 와 challenge 만들기
/root/pk/pkce.py 로 code_verifier(43~128자, URL 안전 문자)와 code_challenge 를 만들어 /root/pk/verifier.txt 와 /root/pk/challenge.txt 에 각각 한 줄씩 저장한다.
길이 제한과 문자 집합이 규격에 정해져 있습니다. 해시 결과를 그대로 Base64 하면 안 되고 URL 안전 인코딩에 패딩을 빼야 합니다.
인가 요청 URL 조립하기
/root/pk/authz_url.txt 에 인가 요청 URL 을 한 줄 적는다. 대상은 사전 임포트된 렐름 labhub 의 공개 클라이언트 web-app 이고 리다이렉트 URI 는 http://127.0.0.1:8161/callback 이다. response_type=code, client_id, redirect_uri, scope=openid, state, code_challenge, code_challenge_method=S256 이 모두 있어야 한다.
필수 파라미터가 여섯 개입니다. 하나라도 빠지면 인증 서버가 오류를 줍니다.
로그인 폼의 action 추출하기
쿠키 항아리를 유지하며 그 URL 을 GET 해 로그인 폼의 action URL 을 /root/pk/action.txt 에 저장한다. http 로 시작해야 한다.
인가 엔드포인트를 GET 하면 HTML 이 옵니다. 쿠키를 유지해야 다음 단계가 됩니다.
자격 증명 POST 해서 코드 받기
사용자 dev1 / 비밀번호 Dev1!pass 를 POST 해 리다이렉트 응답의 Location 에서 인가 코드를 뽑아 /root/pk/code.txt 에 저장한다. 20자 이상이어야 한다.
폼 필드 이름을 HTML 에서 확인하세요. 성공하면 리다이렉트 응답의 위치 헤더에 코드가 들어 있습니다.
코드를 토큰으로 교환하기
토큰 엔드포인트에 grant_type=authorization_code, 코드, redirect_uri, client_id, code_verifier 를 보내 토큰을 받는다. /root/pk/tokens.json 에 access_token 과 refresh_token 이 있어야 한다.
인가 코드와 원본 verifier 를 함께 보냅니다. 리다이렉트 URI 도 처음과 정확히 같아야 합니다.
틀린 verifier 로 실패 확인하기
새 인가 코드를 받아 틀린 code_verifier 로 교환을 시도한다. /root/pk/pkce_fail.json 의 error 가 invalid_grant 여야 한다.
이것이 PKCE 가 실제로 작동한다는 증거입니다. 오류 코드를 기록하세요.
state 불일치 검증 구현하기
/root/pk/state_check.py 는 인자 두 개(보낸 state, 받은 state)를 받아 같으면 종료 코드 0, 다르면 1 로 끝난다. 일치·불일치 두 경우의 결과를 /root/pk/state.out 에 match=ok mismatch=rejected 로 적는다.
인가 요청에 보낸 값과 콜백으로 돌아온 값을 비교합니다. 다르면 거부해야 합니다.
리프레시와 회전 확인하기
리프레시 토큰으로 갱신해 /root/pk/refresh.json 을 만든다. /root/pk/rotation.txt 에 rotated=<true|false> 를 적는다. 판단 근거는 새 리프레시 토큰이 이전과 다른지이다.
갱신하면 새 리프레시 토큰이 옵니다. 옛것과 같은지 다른지가 회전 여부입니다.