JWT 권한 부여자로 API 라우트 보호

AWSBeginner
지금 연습하기

소개

주문 API는 상태 확인을 공개로 유지하면서 읽기와 쓰기에 로그인을 요구해야 합니다. 두 주문 라우트에 토큰 검사를 연결하고 허용되는 요청과 거부되는 요청을 테스트하며 거부된 호출이 비즈니스 데이터를 변경하지 않는지 확인합니다.

Cognito로 애플리케이션 로그인 추가와 주문 API 구축 및 검증을 먼저 완료하세요. 이 새 환경에는 동작하는 공개 백엔드와 테스트 신원 준비물이 제공됩니다. 이전 리소스와 토큰은 재사용하지 않습니다.

인증 시험 관련 주제

이 실습은 다음 시험 주제에 대한 실습 경험을 제공합니다.

공개 라우트 확인 및 로그인

이 단계에서는 현재 공개 라우트 상태를 확인하고 의도한 앱 클라이언트의 실제 액세스 토큰을 받습니다.

작업 공간으로 이동하고 새로 만드는 토큰 파일을 비공개로 유지하세요.

cd /home/labex/project
umask 077

Terminal 옆에서 AWS View를 열어 API 요청, 함수 실행과 저장된 주문을 비교하세요. 게이트웨이 요청 로그와 Lambda 실행 로그는 별개입니다. 거부된 토큰은 함수가 실행되기 전에 차단되어야 합니다.

안전한 준비물 인벤토리를 읽으세요. 의도한 풀/클라이언트, 다른 클라이언트, 다른 발급자와 관련 없는 참조 풀을 식별합니다.

cat scenario.json

jq -r로 기본 ID를 선택하고 $(...) 명령 치환으로 출력을 저장하세요.

POOL_ID=$(jq -r '.main.pool' scenario.json)
CLIENT_ID=$(jq -r '.main.client' scenario.json)

준비된 HTTP API를 이름으로 찾고 정리를 위해 생성된 ID를 기록하세요.

API_ID=$(aws apigatewayv2 get-apis \
  --query "Items[?Name=='labex-a05'].ApiId | [0]" \
  --output text)
printf '%s\n' "$API_ID" > api-id.txt

라우트의 권한 부여 유형을 확인하세요.

aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query 'Items[].{Route:RouteKey,Authorization:AuthorizationType}'

처음에는 세 라우트 모두 NONE을 사용합니다. 사용자 디렉터리만으로 API가 보호되지는 않았습니다. 작업 공간에서 사용할 주소를 만드세요.

API_URL="http://127.0.0.1:8081/api/$API_ID"
curl -i "$API_URL/health"

HTTP 200과 healthy: true가 나와야 하며 AWS View에는 실제 상태 확인 실행이 표시되고 주문은 없어야 합니다.

준비된 테스트 사용자를 의도한 공개 클라이언트를 통해 로그인시키고 실제 응답을 비공개로 저장하세요.

aws cognito-idp initiate-auth \
  --client-id "$CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters file://sign-in.json \
  --query AuthenticationResult \
  --output json > tokens.json

액세스 토큰과 ID 토큰을 출력하지 않고 추출하세요.

jq -r '.AccessToken' tokens.json > access-token.txt
jq -r '.IdToken' tokens.json > id-token.txt

액세스 토큰만 사용하여 실제 애플리케이션 사용자 이름을 읽으세요.

aws cognito-idp get-user \
  --access-token "$(cat access-token.txt)" \
  --query Username \
  --output text

labex-demo가 나와야 합니다. AWS View에는 준비된 확인된 사용자와 클라이언트가 표시됩니다. 독립적인 검사는 실제 발급된 토큰과 실제 신원을 검증하며 대신 다시 인증하지 않습니다.

두 주문 라우트에 권한 부여자 요구

이 단계에서는 의도한 발급자/대상을 설정하고 비공개 읽기와 쓰기에 동일한 JWT 권한 부여자를 적용합니다.

JWT 권한 부여자는 API Gateway가 보호된 라우트를 호출하기 전에 서명된 토큰을 검사합니다. **발급자 (issuer)**는 서명된 Cognito 풀 신원입니다. **대상 (audience)**은 의도한 앱 클라이언트입니다. API Gateway는 aud가 있으면 이를 검증하고 없으면 client_id를 검증합니다. 실제 풀 ID로 발급자를 구성하세요.

ISSUER="https://cognito-idp.us-east-1.amazonaws.com/$POOL_ID"

따옴표로 감싸지 않은 here-document 구분자를 사용하여 셸이 두 ID를 일반 설정 파일에 확장하도록 하세요.

cat > jwt-config.json <<JSON
{
  "Issuer": "$ISSUER",
  "Audience": ["$CLIENT_ID"]
}
JSON

JWT 권한 부여자를 만드세요. 작은따옴표는 $request.header.Authorization 신원 소스를 셸 변수로 확장하지 않고 리터럴 그대로 유지합니다.

AUTH_ID=$(aws apigatewayv2 create-authorizer \
  --api-id "$API_ID" \
  --name OrdersUsers \
  --authorizer-type JWT \
  --identity-source '$request.header.Authorization' \
  --jwt-configuration file://jwt-config.json \
  --query AuthorizerId \
  --output text)

각 주문 라우트의 생성된 ID를 찾으세요.

READ_ROUTE_ID=$(aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query "Items[?RouteKey=='GET /orders'].RouteId | [0]" \
  --output text)
WRITE_ROUTE_ID=$(aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query "Items[?RouteKey=='POST /orders'].RouteId | [0]" \
  --output text)

API Gateway가 항상 액세스 토큰과 ID 토큰을 구별하는 것은 아닙니다. 이 네이티브 Cognito 비밀번호 흐름의 액세스 토큰에는 aws.cognito.signin.user.admin이 포함됩니다. 이 범위를 요구하면 해당 범위가 없는 ID 토큰을 거부합니다. 이 범위는 Cognito 사용자 셀프 서비스 접근을 나타내며 AWS 관리나 주문 소유권을 나타내지 않습니다. 이 실습에서는 사용자 지정 비즈니스 범위를 만들지 않습니다.

읽기 라우트에 JWT와 해당 범위를 요구하세요.

aws apigatewayv2 update-route \
  --api-id "$API_ID" \
  --route-id "$READ_ROUTE_ID" \
  --authorization-type JWT \
  --authorizer-id "$AUTH_ID" \
  --authorization-scopes aws.cognito.signin.user.admin \
  --query '{Route:RouteKey,Authorization:AuthorizationType,Scopes:AuthorizationScopes}'

쓰기 라우트에도 같은 요구 사항을 적용하세요.

aws apigatewayv2 update-route \
  --api-id "$API_ID" \
  --route-id "$WRITE_ROUTE_ID" \
  --authorization-type JWT \
  --authorizer-id "$AUTH_ID" \
  --authorization-scopes aws.cognito.signin.user.admin \
  --query '{Route:RouteKey,Authorization:AuthorizationType,Scopes:AuthorizationScopes}'

준비된 $default 스테이지는 AutoDeploy를 사용하므로 라우트 변경이 자동으로 적용됩니다. 모든 라우트 유형을 다시 읽으세요.

aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query 'Items[].{Route:RouteKey,Authorization:AuthorizationType}'

두 주문 라우트는 JWT, GET /health는 NONE이어야 합니다. AWS View에서 실제 발급자, 의도한 대상, 라우트의 권한 부여자 ID와 필수 범위를 비교하세요. 권한 부여자를 만들기만 하고 각 비공개 라우트에 연결하지 않으면 해당 라우트는 보호되지 않습니다.

공개 상태 확인과 JWT로 보호된 주문 라우트가 표시된 AWS View 예시

예시: 두 주문 라우트는 의도한 권한 부여자와 범위를 공유하며 상태 확인은 공개로 유지됩니다. VM에서 생성되는 리소스 ID는 다릅니다.

권한 부여자는 핸들러 실행 전에 비공개 요청을 허용하거나 거부합니다.

허용되는 요청과 거부되는 요청 테스트

이 단계에서는 의도한 사용자의 실제 비즈니스 접근과 토큰이 없거나 적합하지 않을 때 Lambda 실행 전에 거부되는 것을 증명합니다.

베어러 토큰은 이를 제시하는 사람을 인증합니다. Authorization 헤더 안에서 비공개 전달 파일의 토큰을 읽으세요. 응답 헤더/본문만 표시하도록 계속 curl -i를 사용하고 상세 요청 헤더 로깅은 사용하지 마세요. 수량이 4인 유효한 주문을 보내세요.

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat access-token.txt)" -H 'Content-Type: application/json' --data '{"id":"signed-order","quantity":4}'

HTTP 201과 합계 1100 (4 × 250 + 100)이 나와야 합니다. 액세스 토큰으로 같은 저장된 주문을 조회하세요.

curl -i "$API_URL/orders?id=signed-order" -H "Authorization: Bearer $(cat access-token.txt)"

같은 ID, 수량과 계산된 합계를 포함한 HTTP 200이 나와야 합니다. 네이티브 항목을 독립적으로 확인하세요.

aws dynamodb get-item \
  --table-name labex-a05-orders \
  --key '{"id":{"S":"signed-order"}}' \
  --consistent-read \
  --query Item

이제 토큰 없이 읽기를 반복하세요.

curl -i "$API_URL/orders?id=signed-order"

HTTP 401 Unauthorized가 나와야 하며 응답에 비공개 레코드가 없어야 합니다. 익명 쓰기도 테스트하세요.

curl -i -X POST "$API_URL/orders" -H 'Content-Type: application/json' --data '{"id":"reject-missing","quantity":9}'

이 요청도 401을 반환하고 항목을 만들지 않아야 합니다. 비공개 읽기와 쓰기 모두 권한 부여를 요구합니다.

제공된 잘못된 서명 준비물은 서명의 한 바이트가 변경되어 있습니다. 눈에 보이는 클레임 데이터가 그럴듯해 보여도 신뢰할 수 없습니다.

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat bad-signature-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-signature","quantity":9}'

401이 나와야 합니다. 다른 앱 클라이언트에 실제로 발급된 토큰 역시 이 API에 적합하지 않습니다. 안전한 해당 클라이언트 ID를 읽고 이를 통해 로그인한 다음 액세스 토큰을 비공개로 추출하세요.

WRONG_CLIENT_ID=$(jq -r '.wrong_client' scenario.json)
aws cognito-idp initiate-auth \
  --client-id "$WRONG_CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters file://sign-in.json \
  --query AuthenticationResult \
  --output json > wrong-client.json
jq -r '.AccessToken' wrong-client.json > wrong-client-token.txt
curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat wrong-client-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-client","quantity":9}'

401이 나와야 합니다. 유효한 서명만으로 설정된 대상과 일치하지는 않습니다. 다음으로 별도의 테스트 사용자와 공개 클라이언트를 가진, 준비된 다른 Cognito 발급자를 사용하세요.

OTHER_POOL_ID=$(jq -r '.other.pool' scenario.json)
OTHER_CLIENT_ID=$(jq -r '.other.client' scenario.json)
aws cognito-idp initiate-auth \
  --client-id "$OTHER_CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters file://sign-in.json \
  --query AuthenticationResult \
  --output json > wrong-issuer.json
jq -r '.AccessToken' wrong-issuer.json > wrong-issuer-token.txt
curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat wrong-issuer-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-issuer","quantity":9}'

서명된 발급자가 예상한 풀과 다르므로 401이 나와야 합니다. 제공된 만료 토큰 준비물은 이미 과거인 exp 값으로 서명되어 있습니다. 실제 세션이 만료될 때까지 기다리지 않고 시간 규칙을 테스트합니다.

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat expired-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-expired","quantity":9}'

401이 나와야 합니다. ID 토큰은 이 클라이언트에 대해 서명되었지만 라우트가 요구하는 액세스 토큰 범위가 없습니다.

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat id-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-id-token","quantity":9}'

범위가 부족하므로 403이 나와야 합니다. 서명, 발급자, 대상과 토큰 수명은 필요하지만 없는 권한을 제공하지는 않습니다.

상태 확인이 공개로 유지되고 유효한 비즈니스 쓰기만 저장되었는지 확인하세요.

curl -i "$API_URL/health"
aws dynamodb scan --table-name labex-a05-orders --query Items

상태 확인은 200이어야 하며 합계가 1100인 signed-order만 정확히 하나 있어야 합니다. AWS View에서 실제 API requests와 CloudWatch Logs를 비교하세요. 권한 부여가 거부되면 게이트웨이 결과는 401/403이고 일치하는 함수 실행은 없습니다. 성공한 비공개 요청에는 실제 Lambda 이벤트와 응답이 있으며 자격 증명 헤더는 제외됩니다. 평가 증거는 스크린샷이나 로컬 성공 표시가 아니라 이러한 런타임/API 결과와 네이티브 데이터입니다.

실제 허용된 API 요청과 거부된 API 요청 예시

예시: 유효한 주문 쓰기와 읽기는 201/200을 반환했고 적합하지 않은 토큰은 401/403을 반환했습니다. 함수 실행 목록과 네이티브 테이블은 거부된 요청이 비즈니스 쓰기를 하지 않았다는 사실을 독립적으로 확인합니다.

보호된 라우트와 소유한 리소스 제거

이 단계에서는 관련 없는 참조 리소스만 유지하면서 소유한 API, 신원 준비물과 데이터를 제거합니다.

인벤토리는 이 API와 권한 부여자, 함수/로그 그룹, 게이트웨이 요청 로그 그룹, 주문 테이블, OrdersData 역할 정책, 기본 풀 (클라이언트 둘과 사용자 하나), 다른 발급자 풀 (클라이언트 하나와 사용자 하나), 비공개 입력/응답 파일로 구성됩니다. 참조 풀, 테이블, 참조 로그와 FunctionLogs 역할 정책은 관련이 없으므로 남겨야 합니다.

권한 부여자, 라우트, 통합과 스테이지를 포함하여 API를 삭제하세요.

aws apigatewayv2 delete-api --api-id "$API_ID"
aws lambda delete-function --function-name labex-a05-orders-api
aws logs delete-log-group --log-group-name /aws/lambda/labex-a05-orders-api
aws logs delete-log-group --log-group-name /aws/apigateway/labex-a05
aws dynamodb delete-table \
  --table-name labex-a05-orders \
  --query TableDescription.TableName \
  --output text
aws iam delete-role-policy --role-name labex-a05-execution --policy-name OrdersData

테스트 사용자와 소유한 각 앱 클라이언트를 제거한 다음 풀을 제거하세요.

aws cognito-idp admin-delete-user --user-pool-id "$POOL_ID" --username labex-demo
aws cognito-idp delete-user-pool-client --user-pool-id "$POOL_ID" --client-id "$CLIENT_ID"
aws cognito-idp delete-user-pool-client \
  --user-pool-id "$POOL_ID" \
  --client-id "$WRONG_CLIENT_ID"
aws cognito-idp delete-user-pool --user-pool-id "$POOL_ID"
aws cognito-idp admin-delete-user --user-pool-id "$OTHER_POOL_ID" --username labex-demo
aws cognito-idp delete-user-pool-client \
  --user-pool-id "$OTHER_POOL_ID" \
  --client-id "$OTHER_CLIENT_ID"
aws cognito-idp delete-user-pool --user-pool-id "$OTHER_POOL_ID"

리소스가 없음을 증명하려면 인벤토리 쿼리가 성공해야 합니다. 네트워크/인증 오류는 정리 증거가 아닙니다.

aws apigatewayv2 get-apis --query 'Items[].Name'
aws lambda list-functions --query 'Functions[].FunctionName'
aws cognito-idp list-user-pools --max-results 60 --query 'UserPools[].Name'
aws dynamodb list-tables --query TableNames

API/함수 목록은 비어 있고 풀과 테이블 목록에는 labex-a05-reference만 있어야 하며 AWS View에서 참조 데이터/로그가 유지되어야 합니다. 나열된 비공개 파일만 제거하세요.

rm -f tokens.json access-token.txt id-token.txt wrong-client.json wrong-client-token.txt wrong-issuer.json wrong-issuer-token.txt expired-token.txt future-iat-token.txt future-nbf-token.txt bad-signature-token.txt sign-in.json

로컬 파일만 삭제하는 것은 일반적인 서버 측 JWT 취소가 아닙니다. 소유한 테스트 사용자/클라이언트/풀과 API도 삭제했습니다. 이 읽기 전용 리소스 검사 이후 참조 준비물 정리, 최종 자격 증명 취소와 VM 종료는 작성자의 정리 작업으로 남아 있습니다.

요약

Cognito JWT 권한 부여자를 설정하고 공개 상태 확인을 유지하면서 비공개 읽기와 쓰기에 이를 요구했습니다. 실제로 허용되는 요청과 토큰 누락, 변경, 잘못된 클라이언트, 잘못된 발급자, 만료, ID 토큰에 대한 거부를 테스트했습니다. 실제 게이트웨이 결과를 네이티브 Lambda 실행과 저장된 데이터에 연결한 다음 참조 리소스를 유지하면서 소유한 API/신원/데이터 리소스와 비공개 파일을 제거했습니다. JWT 권한 부여는 주문별 소유권을 구현하지 않았습니다.