11편에서 언급했듯 로그인 인프라를 Flask 서버로 마이그레이션하고자 했음. Flask 서버의 역할들을 모두 APIGW와 Lambda로 옮기고, 세션 저장은 서버 대신 DynamoDB를 사용하는 초안을 설계했었지만 다음 두 가지 이유로 해당안은 폐기되었다.
거의 두 달을 이 문제에 시달리다가 결국 아키텍처를 재설계했다.
수정된 아키텍처는 이전과 마찬가지로 세션 + JWT 하이브리드 구조를 가진다.
세션 저장 위치는 우선 DynamoDB에서 로컬 스토리지로 변경했다. 이 경우 토큰이 탈취당할 위험이 높아지는 문제가 있기에 토큰 유효시간을 X분으로 짧게 저장하였다. 이후 이를 보안 쿠키를 활용하는 방법으로 개선하고자 한다.
프론트 코드가 긴 관계로 백엔드 부분만 먼저 다루겠다.
백엔드 아키텍처는 다음과 같다.

기존의 토큰 교환 로직은 모두 Cognito 엔드포인트로 바로 연결하고, 사용자 정보 교환을 위해서만 사용한다.
보안상의 이유로 별도의 REST API를 만들고, 엔드포인트는
/userinfo
GET
하나만 존재한다.

마찬가지로 프록시 통합을 수행하고 응답 헤더를 Lambda에서 정의하고자 한다.
구조도에는 나타나있지 않지만, 앞에서 말한 APIGW와 연결하여 사용자 정보를 교환하는 함수이다.
먼저 jwt 토큰의 payload 부분을 디코딩한다.
import json
import base64
import boto3
def decode_jwt_payload(token):
try:
payload = token.split('.')[1]
payload += '=' * (4 - len(payload) % 4) # base64 디코딩을 위한 padding
decoded = base64.b64decode(payload)
return json.loads(decoded)
except Exception as e:
raise ValueError(f"Invalid token format: {str(e)}")
이어서 CUP에서 사용자 정보를 가져오는 함수를 만들었다. boto3에서 cognito IdP 개체를 만들고 요청을 보내 사용자 정보를 추출, 딕셔너리 형태로 리턴한다.
def get_user_attributes(username, user_pool_id):
cognito = boto3.client('cognito-idp')
try:
response = cognito.admin_get_user(
UserPoolId=user_pool_id,
Username=username
)
attributes = {attr['Name']: attr['Value'] for attr in response['UserAttributes']}
return attributes
except Exception as e:
raise ValueError(f"Error fetching user attributes: {str(e)}")
이 함수들을 바탕으로 한 lambda handler의 동작은 주석으로 기술.
def lambda_handler(event, context):
try:
token = event['headers'].get('Authorization')
# 토큰이 없으면 401 반환
if not token:
return {
'statusCode': 401,
'body': json.dumps({'message': 'No authorization token provided'})
}
# jwt에서 payload를 디코딩
decoded = decode_jwt_payload(token)
# 디코딩된 토큰으로 사용자 정보(cognitot상에서의 username) 조회
username = decoded.get('cognito:username')
#CUP에서 해당 username의 속성(이름, 이메일, sub값) 로드
user_pool_id = 'us-east-1_XXXXXX'
user_attributes = get_user_attributes(username, user_pool_id)
user_info = {
'name': username,
'email': decoded.get('email'),
'nickname': user_attributes.get('nickname')
}
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': 'https://xxxxxx.cloudfront.net',
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
'Access-Control-Allow-Methods': 'GET,OPTIONS'
},
'body': json.dumps(user_info)
}
except Exception as e:
return {
'statusCode': 500,
'body': json.dumps({'message': str(e)})
}
해당 Lambda 함수에는 cloudfront를 통한 모니터링을 위한 권한, 그리고 cognito idp에 대해 사용자 정보를 얻을 수 있는 권한을 부여하기 위해 아래의 정책 구문을 연결하였다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:PutLogEvents"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"cognito-idp:AdminGetUser",
"cognito-idp:GetSigningCertificate"
],
"Resource": "*"
}
]
}
다음 편에서 계속