Lambda를 활용한 서버리스 백엔드를 구현하면서 인가 고민이 정말정말 많았는데 Lambda Authorizer를 활용한 JWT 처리를 마쳐서 신나게 글을 써보려고한다.😆
API Gateway Authorizer에는 JWT Authorizer와 Lambda Authorizer가 있다.
두가지 모두 아래와 같이 동작한다.
API Gateway에 연결한 엔드포인트에 권한 부여자로 연결
→ 해당 엔드포인트 호출 시에 Authorizer 실행
→ Authroizer의 Allow/Deny 응답에 따라 엔드포인트 접근 허가/거부
확실히 이해하고 가야할 점은 백엔드에서 이야기하는 DB 데이터 접근 인가는 따로 구현해야하고 엔드포인트 접근에 관여한다는 것이다. 권한을 확인할 데이터 (ex. JWT 등)이 유효한지 등을 판별하여 엔드포인트 인가를 구현한다.
넘겨받은 JWT를 그대로 활용해서 리소스 접근 권한을 확인한다.
JWT 권한 부여자를 사용하여 HTTP API에 대한 액세스 제어 문서를 읽어보면 아래와 같은 내용을 확인하여 접근 권한을 확인한다.
따라서 JWT Authorizer가 요구하는 내용에 맞게 JWT를 구성해야한다. OAuth 2.0에서 정의한 JWT Access Token claim과 유사해보인다. RFC9068
필요한 데이터 포함 여부 등을 몇가지 설정을 거치면 확인해줘서 가장 간단하게 인가를 구현할 수 있다는 점이 장점이다.
Use API Gateway Lambda authorizers
Lambda Authorizer는 사용자가 구성한 Lambda로 권한 부여를 확인한다. 테스트가 번거롭지만 사용자의 편의에 따라 구현할 수 있는 점이 장점이다. Lambda Authorizer를 활용하면 token에 담긴 내용을 엔드포인트로 전달할 수도 있다.
기존 프로젝트에서 JWT를 사용자ID와 이름 정도로 가볍게 구성했고 인가 과정에서 사용자ID를 엔드포인트로 넘겨줘야해서 Lambda Authorizer를 활용했다. Labmda Authorizer를 활용하려면 HTTP API용 AWS Lambda 권한 부여자 작업에서 몇가지를 이해하고 넘어가야한다.
Lambda Authorizer가 연결된 엔드포인트를 호출했을 때, Lambda Authorizer가 앞서 호출되면서 넘어갈 요청과 Lambda 함수 응답 데이터 형식을 결정해야한다.
1.0과 2.0이 있고 요청에서는 큰 차이가 없지만 응답에서 차이가 크다.
Payload Response ver.1.0
{ "principalId": "abcdef", // The principal user identification associated with the token sent by the client. "policyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": "execute-api:Invoke", "Effect": "Allow|Deny", "Resource": "arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]" } ] }, "context": { "exampleKey": "exampleValue" } }
Payload Response ver.2.0 - Simple Response
{ "isAuthorized": true/false, "context": { "exampleKey": "exampleValue" } }
⇒ 비교적 간단한 응답 형식인 2.0을 선택했고
context에 엔드포인트로 넘길 데이터를 넣어준다.
권한을 확인할 데이터를 가리킨다. 인가를 확인할 수 있는 데이터를 가리키며 해당 데이터가 없으면 Lambda Authorizer를 호출하지 않고 바로 401 Unauthorized
를 반환한다.
헷갈리지 말아야할 점은 Lambda에서는 header값을 가져올 때 event.headers.~
를 활용하지만 자격 증명 포함 여부를 가리킬 때는 위의 표현식처럼 request.header.~
로 지정해야한다.
먼저 Lambda Authorizer로 사용할 Lambda 함수를 생성한다.
[구성>환경 변수]에서 JWT 비밀키를 등록하고 jsonwebtoken를 포함하는 레이어를 추가한다. 각 Lambda 런타임에 대한 계층 경로를 참고하여 꼭꼭 폴더 경로에 맞게 압축해서 layer를 등록해야한다.
그리고 Lambda 함수의코드를 작성한다. AWS Docs에서 제공하는 Lambda 권한 부여자 함수의 예를 참고하여 작성했다.
// index.js - CommonJS
"use strict"
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET;
module.exports.handler = async(event, context, callback) => {
try{
const token = event.headers.authorization;
const authorizationToken = jwt.verify(token, JWT_SECRET);
const userId = authorizationToken.id;
console.log("Allowed")
return generateResponse(true, userId);
} catch(err){
if(err.name === 'TokenExpiredError'){
console.log("TokenExpiredError");
callback("Unauthorized");
}
else{
console.log(err.name);
return generateResponse(false);
}
}
};
const generateResponse = (isAuthorized, userId) => {
const response = {
"isAuthorized": isAuthorized,
"context": {
"userId": userId
}
};
return response;
};
위 예시에서는 헤더에 Authroization으로 토큰을 담았으며 token의 만료 여부를 확인하고 JWT에 담겨있던 id를 꺼내서 context에 담아 엔드포인트로 넘겨준다.
테스트를 하고 싶다면 테스트 템플릿에서 API Gateway Request Authorizer를 선택해서 headers, queryStringParameters 등에 권한 부여에 활용할 데이터를 담고 실행하면된다.
API Gateway의 메뉴에서 [Authorization>권한 부여자 관리>생성]로 넘어가서 Lambda 함수를 연결하여 Lambda Authorizer를 생성한다.
그리고 [Authorization>경로에 권한 부여자 연결] 에서 Lambda Authorizer를 연결할 엔드포인트를 골라 권한 부여자로 연결한다.
context로 넘겨준 데이터는 Lambda 함수에서 event.requestContext.authorizer.lambda.<property>
로 활용할 수 있다.
위 예시에서는 Lambda Authorizer를 통해 넘어온 JWT의 id와 요청하는 데이터를 가리키는 pathParameter를 비교하여 인가를 구현하기 위해 아래와 같이 작성하였다.
const hostId = event.pathParameters.hostId;
const userId = event.requestContext.authorizer.lambda.userId;
if(hostId != userId)
return context.done(null, { "status" : 403, "message" : "User Not Allowed" });
// 리소스 접근 가능 확인 완료
위 코드 예시에 따라 여러 케이스를 확인한 결과는 다음과 같다.
// 만료된 토큰: 401
{
"message": "Unauthorized"
}
// JWT 형식을 지키지 않은 토큰: 403
{
"message": "Forbidden"
}
// 유효한 토큰과 JWT의 id & pathParameter hostID 일치
{
"Lambda 실행 결과 반환"
}
따로 에러 처리가 되어있지 않은데 문제가 발생하면 항상 발생하는 에러이다. 자주 보면 안되지만 헤매는 과정에서 자주 봤다...
{
"message": "Internal Server Error"
}
event.headers.Authorization
이 아닌 event.headers.authorization
POSTMAN으로 테스트할 때 Authorization으로 보내고 자격 증명 소스도 $request.header.Authorization
으로 설정해서 event.headers.Authorization
으로 확인했는데 undefined이 떠서 확인해보니 토큰이 event.headers.authorization
로 넘어오고 있었다.
event.requestContext.authorizer.lambda.<property>
context 데이터가 어디로 들어오는지 확인하느라 console.log로 하나하나 찍어보면서 확인했다.🥲 결과적으로 위 형식으로 넘어온다는 것을 확인했고 어느 글에서 context에 object를 넘기면 500 에러가 발생한다고 했는데 확인해본 결과 object를 넘겨도 잘 동작한다.
기타 에러 처리 되지 않은 문제 발생시
서버리스 구현은 해야겠고 자료는 많지 않고 Authorizer가 뭔지부터 이해하느라 꽤 오래 걸렸지만 덕분에 API Gateway와 Lambda, 그리고 둘의 통합까지 이해하는데 도움이 된 것 같다! 이제 함수 두개만 더 만들면 구현 끝이라 임시저장 목록에 밀려있는 패키징과 람다 함수 개별 호출 비교 대한 글도 곧 작성할 수 있을 것 같다.🥳