Keycloak 과 기업 인증 · 애플리케이션 연동(리다이렉트·PKCE) · 실습
FastAPI 앱에 토큰 검증 미들웨어 붙이기
목표
FastAPI 앱에 JWT 검증 미들웨어를 직접 붙여, 토큰 없음·만료·잘못된 대상·권한 부족을 각각 올바른 상태 코드로 구분해 처리한다.
왜 중요한가
토큰 검증 미들웨어는 대부분 라이브러리로 해결하지만, 옵션 하나를 잘못 주면 조용히 뚫립니다. 그리고 상태 코드 구분도 자주 틀립니다 — 토큰이 없거나 유효하지 않으면 401 이고, 토큰은 유효한데 권한이 부족하면 403 입니다. 이 둘을 뭉개면 클라이언트가 "다시 로그인해야 하는가"와 "권한을 요청해야 하는가"를 구별하지 못합니다. 3번 스텝의 JWKS 캐시도 실무에서 중요합니다. 매 요청 인증 서버에 공개키를 물으면 로컬 검증의 이점이 사라집니다. 하지만 무한정 캐시하면 키 회전 직후 모든 검증이 실패합니다. 캐시하되 kid 를 못 찾으면 한 번 갱신하는 것이 표준적 구현이고, 이 실습에서 그것을 직접 만듭니다.
단계
1. /root/app/main.py 를 127.0.0.1:8160 에 띄운다. 검증 대상은 사전 임포트된 렐름 labhub 이고 기대 aud 는 lab-api 이다. 사용자는 dev1(order-reader)과 admin1(order-admin)이며 비밀번호는 /opt/fixtures/kc/realm-info.env 에 있다. GET /public 이 인증 없이 200 을 준다.
2. GET /me 를 토큰 없이 호출하면 401 이고 응답에 WWW-Authenticate 헤더가 있어야 한다.
3. JWKS 를 메모리에 캐시한다. GET /_debug/jwks 가 {"cached":true,"keys":<n>,"fetches":<n>} 를 주고, /me 를 5번 호출한 뒤에도 fetches 가 2 이하여야 한다. 이 스텝부터 dev1 의 액세스 토큰을 /root/app/token.txt 에, admin1 의 것을 /root/app/admin_token.txt 에 한 줄씩 저장해 둔다.
4. 유효한 토큰으로 GET /me 를 호출하면 200 과 {"sub":"<값>","username":"dev1"} 를 준다.
5. 만료된 토큰으로 호출하면 401 이고 본문에 expired 가 들어 있어야 한다. 만료 토큰은 /opt/fixtures/kc/expired.jwt 에 있다.
6. aud 가 다른 토큰으로 호출하면 401 이고 본문에 audience 가 들어 있어야 한다. 그런 토큰은 /opt/fixtures/kc/wrongaud.jwt 에 있다.
5·6번의 실패 케이스 토큰은 파드가 뜰 때 /opt/fixtures/kc/gen-tokens.py 가 만듭니다. 두 파일이 안 보이면 그 스크립트를 직접 한 번 실행하세요 — 픽스처 디렉터리가 읽기 전용이면 /tmp/lab-kc/ 아래에 만들어집니다.
7. GET /admin 은 order-admin 롤이 있어야 200 이고, 없으면 403 이다. 401 이 아니라 403 이어야 한다.
8. /root/app/e2e.sh 로 다섯 케이스(토큰 없음, 유효, 만료, 잘못된 aud, 권한 부족)를 순서대로 시험해 /root/app/e2e.out 에 no_token=401 valid=200 expired=401 bad_aud=401 forbidden=403 을 적는다.
참고
- 401 은 '누구인지 모르겠다', 403 은 '누구인지는 알지만 권한이 없다'입니다.
- JWKS 캐시는 TTL 을 두되
kid미스 시 즉시 갱신하는 것이 표준입니다. - 시계 오차 허용(leeway)은 몇 초 이내로 아주 작게 둡니다.
- 흔한 실수 1: 권한 부족에 401 을 주는 것 — 클라이언트가 불필요하게 재로그인합니다.
- 흔한 실수 2: 검증 라이브러리에 기대
issuer와audience를 넘기지 않는 것 — 서명만 맞으면 통과합니다.
단계 8개
- 앱 띄우고 공개 엔드포인트 확인하기
- 토큰 없으면 401 주기
- JWKS 캐시 구현하기
- 유효 토큰 통과시키기
- 만료 토큰 거부하기
- 잘못된 대상 토큰 거부하기
- 롤 기반 인가 붙이기
- 다섯 가지 케이스 한 번에 검증하기