Cognito에서 받은 JWT를 해석하여 로그인 한 유저의 정보 가져오기

728x90

 

서론


지난 글에서 API gateway에 권한 부여한 후 Cognito 인증하여 백엔드 리소스인 Lambda에 접근하는 것을 실습해봤다. 하지만 실제 어플리케이션이 사용자를 인식하여 동적으로 정보를 보여주기 위해서는 이것만으로는 충분하지 않을 것이다. 유저 맞춤형으로 정보를 제공해주기 위해서는 각 API 요청에서 헤더에 담겨있는 유저 정보를 꺼낼 수 있어야 한다. 이번 글에서는 Cognito로 생성한 JWT를 API gateway 뒤에 있는 백엔드 리소스 단에서 추출하여 사용자 정보를 인식하고 사용자 맞춤형 정보를 제공하는 실습을 진행한다.

시스템 구조 및 가정



현재 프로젝트의 시스템의 구조는 아래 그림과 같다. 




본 글에서는 이와 같은 연결 설정이 모두 완료되었다고 가정하고 글을 진행할 것이다. 만약 그림처럼 환경셋팅이 되지 않았다면, 그리고 셋팅하는 방법을 모른다면, 아래 글들을 참고해보자.

- Private Subnet에 Docker image 가져오기:
https://bascat-code.tistory.com/5

 

Private Subnet에서 ECS를 구동하기 위해 NAT gateway 사용하기

Private Subnet에서 배포하면서 생긴 문제 프로젝트 진행 중에 AWS ECS가 생성되지 않는 문제가 발생했다. 현재 프로젝트 설계 구조는 다음과 같이 VPC 내 프라이빗 리소스를 통해 API gateway - Load balancer

bascat-code.tistory.com

 

- HTTP API gateway 및 Cognito 설정하기
https://bascat-code.tistory.com/10

 

Cognito를 사용하여 API gateway 접근 권한 부여하기

서론 Cognito의 역할 Cognito는 아래 그림과 같이 동작한다. - 클라이언트 측에서 특정 경로로 접속하려고하는데, 그 경로가 권한이 부여되어있다면 접근이 제한된다. - 클라이언트는 Cognito의 앱 클

bascat-code.tistory.com

 

Cognito, API gateway, NLB 그리고 Private Subnet의 ECS 환경


본격적으로 JWT를 가져오는 어플리케이션 코드를 작성하기 전에 현 프로젝트의 AWS 환경을 간략하게 설명하겠다.

VPC & Subnet


단일 VPC에 public subnet 하나와 private subnet 하나씩 존재한다. public subnet에 nat gateway endpoint가 존재해서 private subnet의 요청이 인터넷으로 도달할 수 있다. 이렇게 함으로써 Docker hub의 image를 불러올 수 있다.



해당 VPC의 보안그룹은 인바운드 규칙에서 HTTP와 HTTPS의 모든 IP 트래픽을 허용하도록 설정해줘야한다.



 

ELB


ELB는 NLB를 선택하고, 내부 로드밸런서 유형을 선택한다. 



이전에 생성한 VPC를 선택하고 서브넷은 private subnet만 매핑해준다.



대상그룹도 생성하여 로드밸런서에 붙여준다. 대상그룹은 ip 유형을 선택한다. ip 주소 대상에 당장 무엇을 등록할 필요는 없지만 차후 ECS Task를 생성하고 나면 본 대상에 ip 주소가 등록된다.



ECS

Task Definition


Task 정의에서 Docker Hub에 있는 이미지를 불러온다. NAT gateway를 통해 가져온다. private subnet에서만 container를 구동한다.



서비스를 생성하여 Task를 자동적으로 구동하도록한다. Task의 Container instance는 해당 VPC에 있는 NLB와 연결된다.



Task 생성 후 ELB 대상그룹에 해당 Task의 IP 주소가 등록된 것을 확인 할 수 있다.

 

 

API gateway


http api gateway를 채택한다.


vpc link를 생성하여 이전에 생성한 VPC와 연결한다. 요청이 VPC의 NLB에 도달할 수 있게 해준다.



경로를 생성하고 모든 경로들에 방금 생성한 VPC Link를 이용하여 통합을 실행한다.


인증이 필요한 특정 경로에 권한 부여자를 설정한다. Cognito 사용자 풀과 클라이언트 앱이 있어야 설정을 완료할 수 있다. 아래 Cognito 설정까지 완료한 후 진행한다.

 

 

Cognito


사용자 풀을 생성하여 가입 환경과 유저 정보를 관리하게 한다. 글쓴이 본인은 이메일만 있으면 가입할 수 있도록 지정했다.


클라이언트 앱을 생성하고 호스팅 UI를 생성하게 하여 로그인 창을 AWS에게 맡긴다. 




콜백 URL을 입력하여 로그인 후 토큰을 받을 주소를 지정한다. 이 토큰을 프론트엔드 단에서 까서 id_token을 추출하면 다시 API gateway에 요청을 찔러 넣을 때 인증 토큰으로 활용할 수 있다. 


사용자풀과 클라이언트 앱까지 생성을 완료했으면 API gateway의 인증이 필요한 경로에 JWT 권한 부여자를 생성해서 인증이 완료된 사용자만 해당 경로에 접속할 수 있게 만든다.

발급자 URI에는 Cognito 사용자풀 ID가 들어가고 대상에는 클라이언트 앱 ID가 들어간다.

어플리케이션 코드


이제 클라우드 환경 설정은 마무리되었으므로 어플리케이션 코드를 작성하여 JWT를 통해 사용자 정보를 추출하도록 하겠다. 

헤더 전체 가져오기


우선 아래와 같이 헤더 전체를 꺼내는 코드를 작성해보자



도커 이미지로 빌드하여 업로드 후 ECS 서비스를 업데이트하여 Task에 반영한다.

Cognito 클라이언트 앱에 로그인 후 콜백 URL에 담긴 id 토큰을 추출하여 가져온다. 



id 토큰을 추출하여 POSTMAN을 실행하고 Authorization 헤더에 끼워넣은 후 id 토큰 값을 넣어서 권한 부여 설정을 한 경로 주소로 GET 요청을 보낸다. 아래처럼 HTTP 요청의 header 전체가 응답으로 나오게 된다.



이 어플리케이션 코드에서는 모든 헤더를 가져와서 출력했다. 여기 헤더에는 HTTP 요청을 보낼 때 함께 보낸 JWT가 들어있다. 이것이 사용자의 정보를 식별하는 핵심인 것이다. JWT에서 사용자 정보가 들어있는 Claims를 꺼내서 곧바로 DB에서 비교하여 사용자 정보를 가져오면 된다. 여기서 잠깐, 백엔드에서 이렇게 곧바로 JWT를 꺼내써도 괜찮은걸까? 백엔드에서 signature를 해석할 필요는 없는 것일까?

JWT의 성질, 곧바로 Payload의 정보를 사용할 수 있는 이유


JWT는 크게 '헤더', '페이로드', '시그니처' 세 부분으로 구성된다. 헤더는 토큰의 타입이나 서명 생성에 사용된 알고리즘이 표시된다. 페이로드에는 Claim이라 불리는 정보 조각들이 들어있다. Claim에는 토큰 발급자, 토큰 제목, 토큰 대상자, 토큰 만료시간, 토큰 발급시간 등의 토큰 관련 내용도 들어있지만, 해당 토큰을 사용하는 사용자가 누구인지 식별할 수 있는 정보도 담겨있다. (예를 들면 이메일 또는 닉네임) 벡엔드 리소스는 이 식별 정보를 가지고 DB에 접근해서 사용자에 따른 내용을 처리할 수 있는 것이다. 

헤더와 페이로드는 Base64방식으로 인코딩된다. Base64는 공통 ASCII 문자로 표현하기 위해 인코딩하는 기법으로 사실상 암호화라고 부를 순 없다. Base64 encoding 된 문자는 사실 보안 상의 기능은 없다고 볼 수 있는 것이다. 그렇다면 헤더와 페이로드의 정보가 사실상 감추어져있지 않으므로 JWT는 보안상의 역할을 못 하는 것일까? 그렇지않다. JWT에는 마지막으로 시그니처라는 부분이 남아있다. 

Signature

HMACSHA512(
base64UrlEncode(header) + "." + 
    base64UrlEncode(payload),
    256-bit-secret
)



시그니처에는 Base64로 encoding된 헤더와 페이로드가 포함되어있고 거기에 Base64 encoding된 256bit의 secret이 포함되어 HMAC 해시 알고리즘에 의해 암호화된다. (secret은 일종의 임의 문자열이며 개발자가 지정할 수도 있다. 다만 Signature 들어가기 전에 base64로 encoding된다.) Base64로 encoding된 세가지 정보가 합쳐져서 HMAC 알고리즘에 의해 암호화되는 것이다.

시그니처의 역할은 인증 처리 로직에서 본래의 헤더, 페이로드와의 내용과 Signature에서 HMAC decode된 내용이 서로 일치하는지 비교하기 위해 존재한다. 이렇게 시그니처를 사용하면 누군가 임의로 헤더와 페이로드를 작성하여 권한 설정된 경로로 요청을 보내도 Signature에 있는 정보가 헤더와 페이로드가 일치하지 않으므로 서버는 "Unauthorized"를 내뱉게 되는 것이다.

JWT 성질 이용하기


Cognito는 어플리케이션 유저가 로그인 했을 때 JWT를 클라이언트 측으로 돌려준다. 클라이언트에 권한이 필요한 경로로 요청을 보낼 때 이 JWT를 함께 보내면 API gateway는 이 JWT에 포함된 Secret을 해석하기 위해 Cognito로 전달하게 된다. Cognito에서 정상적인 JWT로 판명되어 API gateway에 알려주면 APi gateway는 요청을 백엔드 리소스로 보내어 로직이 동작하도록 허용해준다.

이렇게 인증이 완료되어 API gateway를 통과한 모든 요청의 JWT는 신뢰할 수 있는 정보이며, 백엔드 리소스에서 Authorization 헤더에 담긴 JWT의 페이로드 부분을 권한 있는 사용자의 정보를 담고 있는 격이다. 이미 Cognito에 의해 검증된 정보이므로 믿고 사용해도 되는 것이다. 그러므로 백엔드 어플리케이션 코드에서는 signature는 신경 쓸 필요 없이 그냥 JWT를 가져와서 페이로드를 Base64 Decode하여 사용하면 된다.

JWT로부터 사용자 정보 꺼내오기

HTTP 요청에서 JWT만 꺼내기


모든 헤더의 정보를 꺼낼 필요는 없다. 아래처럼 JWT만 꺼내도록 코드를 수정하고 이미지 업로드 후 POSTMAN으로 테스트 해보자

스프링을 사용하면 HttpHeaders 상수를 제공한다. 아래와 같이 작성하면 Authorization 헤더만 추출할 수 있다.



POSTMAN으로 Authorization에 JWT를 담아 보내면 백엔드에서 JWT만 추출하는 것을 확인할 수 있다.


 

 

JWT 페이로드만 복호화하기


일반적으로 JWT를 복호화 하기 위해서는 아래처럼 jjwt와 같은 라이브러리를 사용하여 파싱할 것이다. 



하지만 우리는 secret을 가지고 있지 않아서 jwt를 decode 할 수 없고, 사실 굳이 jwt 전체를 decode할 필요도 없다. 우리가 필요로 하는 정보는 payload에 있고 이것은 base64로 인코드 되어 사실상 그냥 꺼내쓰기만 하면 되기 때문이다.

그러므로 특별히 다른 dependency 설정 필요 없이 java가 제공하는 Base64 라이브러리를 사용하면 된다.

 



아래와 같이 함수를 생성하여 Base64를 decode하는 코드를 작성한다.  여기서 Base64의 decode 결과는 Byte 형태이므로 반드시 new Striong()으로 감싸서 문자열 형태로 반환해주도록 한다. 그렇지 않으면 알수 없는 형태의 숫자 byte가 나열되어 반환되기 때문이다.

 


방금 생성한 함수를 컨트롤러에서 활용하여 GET 요청에 대한 응답을 Payload만 빼도록 한다.

 



POSTMAN으로 테스트하면 아래와 같이 나온다.

 

 


마지막 Claims에 사용자의 email 정보가 뜬 것을 확인할 수 있다.

복호화한 페이로드에서 필요한 Claim만 추출하기


이렇게 복호화를 했어도 사용자 인식에 필요한 정보는 email만 있으면 되므로 해당 문자열에서 email 정보만 추출할 수 있어야 한다. 현재 반환된 값 형태는 String이므로 이 String을 JSON으로 바꿔서 email을 추출해야 한다. String 값을 JSON으로 변환하는 작업은 JSONObject 객체를 이용한다. (JSON으로 변환하지 않고 문자열 상태에서 정규식을 사용하거나, split 함수를 사용하여 email 등 정보를 추출할 수도 있으나, 작업이 매우 번거롭다. JSONObject를 사용하는 것이 훨씬 간편하고 직관적인 코드를 짤 수 있다.)

JSONObject를 사용할 수 있도록 아래와 같이 dependency 설정을 넣어준다.

implementation 'org.json:json:20230227'



String으로 주어진 Payload를 JSON으로 바꿔주고 JSON 객체에서 getString() 메서드를 사용해서 원하는 정보를 추출한다.



함수를 호출하여 복호화된 Payload를 JSON형태로 바꾸고 거기서 email 정보를 꺼내온다.

 




POSTMAN으로 Authorization 헤더에 JWT를 넣어서 GET 요청을 하면 다음과 같이 email 정보만 나오는 것을 확인할 수 있다. 

 

 

결론



JWT의 payload 부분에서 사용자의 정보를 추출해보았다. 이제 이를 이용하여 DB에서 특정 유저 정보(이메일 등)를 가진 데이터를 select하여 사용자 별 데이터를 렌더링할 수 있다.