API 보안 설정

이제 사용자 풀Identity Pool 및 인증 역할을 생성했으므로, 이를 사용해 API 접근을 보호할 준비가 되었습니다.

Serverless IAM 인증

Change indicator 먼저 serverless.yml 파일의 functions: 블록을 수정해 보겠습니다.

functions:
  # create.js의 main 함수를 호출하는 HTTP API 엔드포인트 정의
  # - path: URL 경로는 /notes
  # - method: POST 요청
  # - authorizer: AWS IAM 역할을 사용하여 인증
  create:
    handler: create.main
    events:
      - http:
          path: notes
          method: post
          authorizer: aws_iam

  get:
    # get.js의 main 함수를 호출하는 HTTP API 엔드포인트 정의
    # - path: URL 경로는 /notes/{id}
    # - method: GET 요청
    handler: get.main
    events:
      - http:
          path: notes/{id}
          method: get
          authorizer: aws_iam

  list:
    # list.js의 main 함수를 호출하는 HTTP API 엔드포인트 정의
    # - path: URL 경로는 /notes
    # - method: GET 요청
    handler: list.main
    events:
      - http:
          path: notes
          method: get
          authorizer: aws_iam

  update:
    # update.js의 main 함수를 호출하는 HTTP API 엔드포인트 정의
    # - path: URL 경로는 /notes/{id}
    # - method: PUT 요청
    handler: update.main
    events:
      - http:
          path: notes/{id}
          method: put
          authorizer: aws_iam

  delete:
    # delete.js의 main 함수를 호출하는 HTTP API 엔드포인트 정의
    # - path: URL 경로는 /notes/{id}
    # - method: DELETE 요청
    handler: delete.main
    events:
      - http:
          path: notes/{id}
          method: delete
          authorizer: aws_iam

여기서 중요한 변경 사항은 각 함수에 다음 줄을 추가한 것입니다.

authorizer: aws_iam

이 줄은 Serverless Framework에게 우리의 API가 Identity Pool을 사용하여 보안되고 있음을 알려줍니다. 이 과정은 대략 다음과 같이 동작합니다:

  1. 서명된 인증 헤더가 포함된 요청이 API로 전송됩니다.
  2. AWS는 헤더를 사용하여 어떤 Identity Pool이 연결되어 있는지 확인합니다.
  3. Identity Pool은 요청이 우리의 User Pool로 인증된 사용자에 의해 서명되었는지 확인합니다.
  4. 그렇다면, 이 요청에 Auth IAM 역할을 할당합니다.
  5. 마지막으로, IAM은 이 역할이 우리의 API에 접근할 수 있는 권한이 있는지 확인합니다.

모든 과정이 성공적으로 완료되면, Lambda 함수가 호출됩니다. 그리고 함수 핸들러의 event 매개변수에는 API를 호출한 사용자에 대한 정보가 포함됩니다.

Cognito Identity Id

우리의 Lambda 함수 시그니처를 다시 떠올려 보겠습니다.

export async function main(event, context) {}

또는 현재 사용 중인 리팩토링된 버전:

export const main = handler(async (event, context) => {});

지금까지 event 객체를 사용하여 경로 매개변수(event.pathParameters)와 요청 본문(event.body)을 가져왔습니다.

이제 인증된 사용자의 ID를 가져오겠습니다.

event.requestContext.identity.cognitoIdentityId;

이 ID는 Cognito Identity Pool이 우리 사용자에게 할당한 ID입니다.

지금까지 모든 API가 단일 사용자(사용자 ID 123)와 상호작용하도록 하드코딩되어 있음을 기억하실 겁니다.

userId: "123", // 작성자의 ID

이제 이를 변경해 보겠습니다.

Change indicator create.js에서 위의 줄을 다음과 같이 바꿉니다.

userId: event.requestContext.identity.cognitoIdentityId, // 작성자의 ID

Change indicator get.js에서도 동일하게 변경합니다.

userId: event.requestContext.identity.cognitoIdentityId, // 작성자의 ID

Change indicator update.js에서도 마찬가지로 변경합니다.

userId: event.requestContext.identity.cognitoIdentityId, // 작성자의 ID

Change indicator delete.js에서도 변경합니다.

userId: event.requestContext.identity.cognitoIdentityId, // 작성자의 ID

Change indicator list.js에서는 다음 줄을 찾습니다.

":userId": "123",

Change indicator 그리고 이를 다음과 같이 바꿉니다.

":userId": event.requestContext.identity.cognitoIdentityId,

위의 userId는 Federated Identity ID(또는 Identity Pool 사용자 ID)임을 기억하세요. 이는 User Pool에서 할당된 사용자 ID가 아닙니다. User Pool 사용자 ID를 사용하고 싶다면, Cognito Identity Id와 User Pool Id 매핑 챕터를 참고하세요.

로컬에서 테스트하기

이전에 API 엔드포인트를 처음 생성했던 챕터를 떠올려보면, Lambda 함수를 테스트하기 위해 모의 이벤트(mock events)를 사용했습니다. 이 파일들은 mocks/ 디렉토리에 저장되어 있습니다.

예를 들어, create-event.json 파일은 다음과 같습니다.

{
  "body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}"
}

이제 이 파일을 수정하여 event.requestContext.identity.cognitoIdentityId를 전달해야 합니다. 이제 이를 수행해 보겠습니다.

Change indicator create-event.json을 다음과 같이 변경합니다.

{
  "body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}",
  "requestContext": {
    "identity": {
      "cognitoIdentityId": "USER-SUB-1234"
    }
  }
}

여기서는 테스트 목적으로 cognitoIdentityId에 더미 값을 전달합니다.

이제 프로젝트 루트에서 다음 명령어를 실행합니다.

$ serverless invoke local --function create --path mocks/create-event.json

테스트 사용자를 위한 새로운 노트 객체가 생성된 것을 확인할 수 있습니다.

{
    "statusCode": 200,
    "body": "{\"userId\":\"USER-SUB-1234\",\"noteId\":\"0101be80-18b9-11eb-893d-b7fc3f6c5167\",\"content\":\"hello world\",\"attachment\":\"hello.jpg\",\"createdAt\":1603846842984}"
}

이제 다른 모의 이벤트 파일도 업데이트합니다.

Change indicator get-event.json을 다음과 같이 변경합니다.

{
  "pathParameters": {
    "id": "cf6a83b0-1314-11eb-9506-9133509a950f"
  },
  "requestContext": {
    "identity": {
      "cognitoIdentityId": "USER-SUB-1234"
    }
  }
}

Change indicator update-event.json을 다음과 같이 변경합니다.

{
  "body": "{\"content\":\"new world\",\"attachment\":\"new.jpg\"}",
  "pathParameters": {
    "id": "cf6a83b0-1314-11eb-9506-9133509a950f"
  },
  "requestContext": {
    "identity": {
      "cognitoIdentityId": "USER-SUB-1234"
    }
  }
}

Change indicator delete-event.json을 다음과 같이 변경합니다.

{
  "pathParameters": {
    "id": "a63c5450-1274-11eb-81db-b9d1e2c85f15"
  },
  "requestContext": {
    "identity": {
      "cognitoIdentityId": "USER-SUB-1234"
    }
  }
}

Change indicator 마지막으로, list-event.json을 다음과 같이 변경합니다.

{
  "requestContext": {
    "identity": {
      "cognitoIdentityId": "USER-SUB-1234"
    }
  }
}

이제 로컬에서 사용자와 연결된 Lambda 함수를 테스트할 수 있습니다.

변경 사항 배포

지금까지 만든 변경 사항을 빠르게 배포해 보겠습니다.

Change indicator 프로젝트 루트에서 다음 명령어를 실행하세요.

$ serverless deploy

배포가 완료되면 배포된 엔드포인트와 함수를 확인할 수 있습니다.

Service Information
service: notes-api
stage: prod
region: us-east-1
stack: notes-api-prod
resources: 32
api keys:
  None
endpoints:
  POST - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes
  GET - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id}
  GET - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes
  PUT - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id}
  DELETE - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id}
functions:
  create: notes-api-prod-create
  get: notes-api-prod-get
  list: notes-api-prod-list
  update: notes-api-prod-update
  delete: notes-api-prod-delete
layers:
  None

다음으로, 새로 보안이 적용된 API를 테스트해 보겠습니다.