LabHub
배우기 러닝패스 코스

Keycloak and Enterprise Identity

Adding Token Verification Middleware to a FastAPI App

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

FastAPI 앱에 JWT 검증 미들웨어를 직접 붙여, 토큰 없음·만료·잘못된 대상·권한 부족을 각각 올바른 상태 코드로 구분해 처리한다.

왜 중요한가

토큰 검증 미들웨어는 대부분 라이브러리로 해결하지만, 옵션 하나를 잘못 주면 조용히 뚫립니다. 그리고 상태 코드 구분도 자주 틀립니다 — 토큰이 없거나 유효하지 않으면 401 이고, 토큰은 유효한데 권한이 부족하면 403 입니다. 이 둘을 뭉개면 클라이언트가 "다시 로그인해야 하는가"와 "권한을 요청해야 하는가"를 구별하지 못합니다. 3번 스텝의 JWKS 캐시도 실무에서 중요합니다. 매 요청 인증 서버에 공개키를 물으면 로컬 검증의 이점이 사라집니다. 하지만 무한정 캐시하면 키 회전 직후 모든 검증이 실패합니다. 캐시하되 kid 를 못 찾으면 한 번 갱신하는 것이 표준적 구현이고, 이 실습에서 그것을 직접 만듭니다.

단계

  1. /root/app/main.py 를 127.0.0.1:8160 에 띄운다. 검증 대상은 사전 임포트된 렐름 labhub 이고 기대 audlab-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 /adminorder-admin 롤이 있어야 200 이고, 없으면 403 이다. 401 이 아니라 403 이어야 한다. 8. /root/app/e2e.sh 로 다섯 케이스(토큰 없음, 유효, 만료, 잘못된 aud, 권한 부족)를 순서대로 시험해 /root/app/e2e.outno_token=401 valid=200 expired=401 bad_aud=401 forbidden=403 을 적는다.

참고

앱 띄우고 공개 엔드포인트 확인하기

/root/app/main.py 를 127.0.0.1:8160 에 띄운다. 검증 대상은 사전 임포트된 렐름 labhub 이고 기대 audlab-api 이다. 사용자는 dev1(order-reader)과 admin1(order-admin)이며 비밀번호는 /opt/fixtures/kc/realm-info.env 에 있다. GET /public 이 인증 없이 200 을 준다.

인증이 필요 없는 경로도 하나 있어야 헬스체크가 됩니다.

토큰 없으면 401 주기

GET /me 를 토큰 없이 호출하면 401 이고 응답에 WWW-Authenticate 헤더가 있어야 한다.

401 에는 어떤 인증이 필요한지 알려 주는 헤더를 함께 줘야 합니다.

JWKS 캐시 구현하기

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 에 한 줄씩 저장해 둔다.

매 요청 인증 서버에 물으면 그것이 병목입니다. 캐시하되 키를 못 찾으면 갱신하세요.

유효 토큰 통과시키기

유효한 토큰으로 GET /me 를 호출하면 200 과 {"sub":"<값>","username":"dev1"} 를 준다.

서명 검증과 클레임 검사를 모두 해야 합니다. 응답에 주체 식별자를 담으세요.

만료 토큰 거부하기

만료된 토큰으로 호출하면 401 이고 본문에 expired 가 들어 있어야 한다. 만료 토큰은 /opt/fixtures/kc/expired.jwt 에 있다.

만료 시각이 지난 토큰으로 시험합니다. 시계 오차를 위한 여유는 아주 작게 두세요.

잘못된 대상 토큰 거부하기

aud 가 다른 토큰으로 호출하면 401 이고 본문에 audience 가 들어 있어야 한다. 그런 토큰은 /opt/fixtures/kc/wrongaud.jwt 에 있다.

같은 서버가 발급했어도 우리 API 를 위한 것이 아니면 거부해야 합니다.

롤 기반 인가 붙이기

GET /adminorder-admin 롤이 있어야 200 이고, 없으면 403 이다. 401 이 아니라 403 이어야 한다.

인증은 진입점에서, 인가는 자원 지점에서 합니다. 권한이 없으면 401 이 아니라 403 입니다.

다섯 가지 케이스 한 번에 검증하기

/root/app/e2e.sh 로 다섯 케이스(토큰 없음, 유효, 만료, 잘못된 aud, 권한 부족)를 순서대로 시험해 /root/app/e2e.outno_token=401 valid=200 expired=401 bad_aud=401 forbidden=403 을 적는다.

앞의 케이스들을 한 스크립트로 묶어 기대 상태 코드와 비교합니다.