Cognito를 통한 인증 API Gateway (feat. Terraform)
이번에는 Lambda와 Integration된 API Gateway와 Cognito를 통해 JWT 토큰을 발급받아 API gateway의 API들에 접근하도록하는 아키텍처를 구성해보도록 한다.
- 사용자가 Cognito로 부터 인증 Token을 발급 받고,
- API Gateway에 연결된 각 리소스와 메소드에 접근할때에 인증 Token을 통해
- Cognito User pool에 있는 사용자인지 검증해서 접근제어를 수행하는 로직이 되는 것이다.
구성
1. API Gateway 생성
rest_api 타입으로 API Gateway를 생성한다.
# API Gateway REST API
resource "aws_api_gateway_rest_api" "mygw" {
name = "simple-auth"
}
2. Lambda 함수 생성과 API Gateway로 연결 (Integration)
API Gateway를 통해 접근할 Lambda 함수를 간단하게 생성한다.
import json
import boto3
import os
def lambda_handler(event, context):
try:
# 환경변수에서 버킷 이름 가져오기
bucket_name = os.environ.get('BUCKET_NAME')
if not bucket_name:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "BUCKET_NAME 환경변수가 설정되지 않았습니다."})
}
# S3 클라이언트 생성
s3_client = boto3.client('s3')
# 버킷의 객체 목록 가져오기
response = s3_client.list_objects_v2(Bucket=bucket_name)
# 객체 목록 추출
objects = []
if 'Contents' in response:
for obj in response['Contents']:
objects.append({
'key': obj['Key'],
'size': obj['Size'],
'last_modified': obj['LastModified'].isoformat()
})
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"bucket": bucket_name,
"objects": objects
})
}
except Exception as e:
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": str(e)})
}
# Lambda 실행 역할
resource "aws_iam_role" "lambda_exec" {
name = "lambda_exec_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
resource "aws_iam_policy" "lambda_bucket_policy" {
name = "lambda_bucket_policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
var.bucket_arn,
"${var.bucket_arn}/*"
]
}
]
})
}
resource "aws_iam_role_policy_attachment" "lambda_bucket" {
role = aws_iam_role.lambda_exec.name
policy_arn = aws_iam_policy.lambda_bucket_policy.arn
}
# Lambda 함수 - 기본/인증 확인용
resource "aws_lambda_function" "hello" {
function_name = "api_handler"
role = aws_iam_role.lambda_exec.arn
handler = "lambda_function.lambda_handler"
runtime = "python3.12"
filename = "lambda_function.zip"
timeout = 10
source_code_hash = data.archive_file.hello.output_base64sha256
environment {
variables = {
BUCKET_NAME = replace(var.bucket_arn, "arn:aws:s3:::", "")
}
}
}
data "archive_file" "hello" {
type = "zip"
source_file = "${path.module}/../lambda_function.py"
output_path = "${path.module}/lambda_function.zip"
}
+) `source_code_hash`는 Lambda 함수의 소스코드 해시값으로 Terraform이 Lambda 함수의 소스코드 변경이력을 추적할 수 있는 수단
다음으로는 Lambda와 API gateway를 연결(Integration)하는 과정을 제작하는데, 이 과정에서는 Lambda에 대한 API Gateway 리소스와 메소드를 생성해서 매핑해주어야한다.
즉, API Gateway 식당에 들어오는 손님(클라이언트)가 어떤 문으로 들어갈지(URL 경로), 어떻게 문으로 들어갈지(메소드)를 지정해서 손님을 맞도록 지정하는 것이다.
# /hello 리소스와 GET 메소드
resource "aws_api_gateway_resource" "hello" {
rest_api_id = aws_api_gateway_rest_api.mygw.id
parent_id = aws_api_gateway_rest_api.mygw.root_resource_id
path_part = "hello"
}
resource "aws_api_gateway_method" "hello_get" {
rest_api_id = aws_api_gateway_rest_api.mygw.id
resource_id = aws_api_gateway_resource.hello.id
http_method = "GET"
authorization = "NONE"
}
# Lambda 통합 (Lambda 프록시 모드)
resource "aws_api_gateway_integration" "hello_get" {
rest_api_id = aws_api_gateway_rest_api.mygw.id
resource_id = aws_api_gateway_resource.hello.id
http_method = aws_api_gateway_method.hello_get.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.hello.invoke_arn
}
그리고 API Gateway가 이 Lambda에 접근할 수 있도록 Policy를 설정해주어야한다.
# API Gateway → Lambda 호출 권한
resource "aws_lambda_permission" "apigw" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.mygw.execution_arn}/*/*"
}
이제 API Gateway URL을 통해 연결한 Lambda에 접근할 수 있는지 테스트해볼 수 있다.
인증단 추가
1. Cognito User pool 생성
이제 구성된 API들에 접근제어를 위한 수단으로 Cognito를 연결해본다.
Cognito는 사용자 인증을 위한 서비스로 'User Pool'에 등록된 사용자를
# 1) Cognito User Pool & Client
resource "aws_cognito_user_pool" "pool" {
name = "example-user-pool"
}
resource "aws_cognito_user_pool_client" "pool" {
name = "example-app-client"
user_pool_id = aws_cognito_user_pool.pool.id
generate_secret = false
explicit_auth_flows = [
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH"
]
}
resource "aws_cognito_user" "testuser" {
user_pool_id = aws_cognito_user_pool.pool.id
username = var.testuser-name
password = var.testuser-password
}
이렇게 Cognito User Pool ID와 Client ID를 생성하게 되었고, 이를 통해 인증을 수행하고 토큰을 발급해주는 엔드포인트를 만들어본다.
2. Cognito를 통해 인증토큰을 발급받는 Lambda 생성
아래와 같이 앞서 생성한 Cognito User pool에 등록된 사용자를 인증해서 토큰을 발급해주는 lambda를 구성한다.
import os
import json
import boto3
USER_POOL_ID = os.environ['COGNITO_USER_POOL_ID']
CLIENT_ID = os.environ['COGNITO_APP_CLIENT_ID']
cognito = boto3.client('cognito-idp')
def lambda_handler(event, context):
"""
요청 본문에 JSON으로 아래 필드를 담기
{
"username": "사용자이름",
"password": "비밀번호"
}
"""
body = json.loads(event.get('body') or '{}')
username = body.get('username')
password = body.get('password')
if not username or not password:
return {
"statusCode": 400,
"body": json.dumps({"error": "username and password are required"})
}
try:
resp = cognito.initiate_auth(
ClientId=CLIENT_ID,
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': username,
'PASSWORD': password
}
)
# AccessToken, IdToken, RefreshToken 등
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(resp['AuthenticationResult'])
}
except cognito.exceptions.NotAuthorizedException:
return {
"statusCode": 401,
"body": json.dumps({"error": "Invalid credentials"})
}
except Exception as e:
return {
"statusCode": 500,
"body": json.dumps({"error": str(e)})
}
3. Cognito Authorizer를 API Gateway와 연결
앞서 생성한 Cognito User pool로 사용자 인증을 위해 API gateway와 통합한다.
여기서는 `aws_api_gateway_authorizer` 리소스를 통해 연결할 수 있다.
# 앞서 구성했던 /hello 엔드포인트에 Cognito 인증로직을 추가
resource "aws_api_gateway_method" "hello_get" {
rest_api_id = aws_api_gateway_rest_api.mygw.id
resource_id = aws_api_gateway_resource.hello.id
http_method = "GET"
# Cognito User Pool을 이용해서 인증하도록 구성 (authorization: NONE -> COGNITO_USER_POOLS)
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.cognito.id
}
...
# /token 리소스 생성
resource "aws_api_gateway_resource" "token" {
rest_api_id = aws_api_gateway_rest_api.mygw.id
parent_id = aws_api_gateway_rest_api.mygw.root_resource_id
path_part = "token"
}
# POST 메소드
resource "aws_api_gateway_method" "token_post" {
rest_api_id = aws_api_gateway_rest_api.mygw.id
resource_id = aws_api_gateway_resource.token.id
http_method = "POST"
authorization = "NONE" # 로그인하려고할때에는 인증구간이 없어야함
}
# Lambda 프록시 통합
resource "aws_api_gateway_integration" "token_post" {
rest_api_id = aws_api_gateway_rest_api.mygw.id
resource_id = aws_api_gateway_resource.token.id
http_method = aws_api_gateway_method.token_post.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.token.invoke_arn
}
# Cognito Authorizer (인증 관문)
resource "aws_api_gateway_authorizer" "cognito" {
name = "cognito-authorizer"
rest_api_id = aws_api_gateway_rest_api.mygw.id
type = "COGNITO_USER_POOLS"
provider_arns = [aws_cognito_user_pool.pool.arn]
identity_source = "method.request.header.Authorization"
}
이러면 authorizer와 연결된 method로 유입되는 request들은 Cognito에서 발급받은 토큰을 통해 Cognito User pool에 존재하는 사용자인지를 검증하게 된다.
이 글에서 사용된 코드는 Repo에서 확인할 수 있다.