LabHub
배우기 러닝패스 코스

Keycloakと企業認証

FastAPIアプリにトークン検証ミドルウェアを付ける

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 을 적는다.

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