메인
투자 노트

AWS Cognito 동시 접속 제한하기

@8/28/2023
AWS Cognito로 유저 인증 시스템을 구현하면서 하나의 계정이 동시에 사용되지 못하도록 차단하는 방법
유료 서비스를 개발할 때 하나의 계정이 여러곳에서 사용되지 못하도록 동시 접속을 제한하는건 흔한 케이스이면서 경우에 따라 매우 어려운 문제이기도 하다. 유저의 로그인 상황을 추적하고 동시 접속을 제한하려면 실시간으로 로그인 상태를 관리해야 한다. 정도의 차이는 있겠지만 MSA 아키텍쳐가 주를 이루는 요즘 서버가 메모리 수준으로 간단하게 세션 DB를 관리하기 불가능한 경우가 많다. AWS Lambda를 사용한 서버리스 아키텍쳐를 차용하면 영구적인 메모리를 사용할 수 없고, ECS를 활용해 수평 확장(오토스케일링)가능한 서버 아키텍처를 구현하면 컨테이너간 메모리 공간이 격리되어 상태 공유가 불가능해진다. 따라서 상태를 통합하기 위한 외부 인프라를 도입해야 하는데, 그 예시로는 일반적인 Database 부터 Redis (AWS ElastiCache), 외부 파일시스템(ex:EFS) 마운트 등 다양한 당법이 있는데 하나같이 비용과 복잡성을 증가시킨다.
AWS Cognito(이하 Cognito)는 유저 관리와 인증 시스템을 제공한다. 유저 시스템을 구현하는것은 요구사항에 따라 매우 복잡하고 민감한 문제이지만 Cognito를 별도의 인증 서버로 사용하면 많은 부분의 개발을 생략할 수 있다. 하지만 Cognito는 JWT기반의 인증 방법만을 제공한다. 그래서 “세션”이라는 상태관리가 불가능하다.
만약 Cognito의 유저 인증 시스템을 사용하고 있는데 “동시 접속을 막아달라”는 요구사항이 접수된다면 Cognito를 걷어내고 모든 유저 시스템을 손수 구현한 뒤 Redis를 사용한 세션 관리까지 개발해야 한다.
동시 접속 차단은 Cognito에서 지원하지 않는 기능이다. 하지만 Cognito에서는 모든 기기에서 로그아웃을 지원한다. 이 기능을 활용해서 로그인 동작 이전에 모든 기기에서 로그아웃이 선행되도록 만들면 동시 접속 차단을 구현할 수 있다.

개요

백엔드 코드가 AWS에서 제공하는 SDK인 boto3를 사용해 Cognito를 조작한다고 상정하고 쓴 글이다.
Python으로 boto3를 사용해서 Cognito를 사용할 때 로그인을 구현하려면 initiate_auth 메서드를 사용해야 한다. initiate_auth를 사용하면 아래 3가지 토큰을 얻을 수 있다.
[JWT]유저 속성정보가 들어있는 IdToken
[JWT]유저 권한정보가 들어있는 AccessToken
IdToken과 AccessToken을 갱신할 수 있는 RefreshToken
백엔드 코드는 IdToken과 AccessToken을 사용해서 Cognuto를 통해 사용자를 인증한다. 이 두가지 토큰은 JWT이기 때문에 Cognito 설정으로 유효시간을 지정할 수 있다. JWT는 일단 발급되면 만료될 때까지 유효하기 때문에 유효시간을 5분으로 최대한 짧게 설정하면 된다.
유효시간이 5분이면 동시접속 가능한 최대 시간이 5분이다. A에서 로그인 한 뒤 동일한 계정으로 B에서 로그인하면 A는 5분 안에 로그아웃이 된다.
IdToken과 AccessToken이 만료되면 RefreshToken을 사용해서 갱신해야 한다. RefreshToken이 만료되면 다시 로그인해서 RefreshToken을 다시 발급받아야 한다.
Cognito에는 admin_user_global_sign_out 메서드가 있다. 이걸 사용하면 특정 유저에게 발급된 모든 RefreshToken을 만료시킬 수 있다 (정확히는 취소시키는거다.)
예를 들어서 유저 A가 10개의 기기에서 로그인했어도 admin_user_global_sign_out을 사용하면 5분 뒤 모든 기기에서 JWT 재발급이 불가능해진다.(=로그아웃된다)
따라서 모든 로그인 동작 전에 admin_user_global_sign_out을 수행하면 해당 계정이 하나의 기기에서만 사용되도록 강제할 수 있다.
모든 RefreshToken 무효화 → 새로운 RefreshToken 발급 [ 유효한 RefreshToken이 단 하나만 존재하도록 한다 ]
단 로그인 동작을 이렇게 구현할때 주의해야 할 점이 있다. global_sign_out 기능은 “모든 기기에서 로그아웃”을 구현하기 위한 메서드이기 때문에 Cognito가 동작을 완료하기를 기다리지 않는다. 따라서 아래와 같은 오류가 발생한다.
1.
admin_user_global_sign_out 호출
2.
Cognito가 동작을 완료하기 전 initiate_auth 를 통해 RefreshToken 발급
3.
Cognito는 아직 global_sign_out를 수행중이기 때문에 발급된 RefreshToken을 바로 취소해버린다.
4.
사용 가능한 RefreshToken이 없는 상태가 되어버린다
initiate_auth가 비동기적으로 동작한다는 자료는 없지만 사용해보았을 때 실제로 그렇게 동작하더라 따라서 이 문제는 추후에 AWS가 해결해 줄 수도 있다.
해결책은 admin_user_global_sign_out 호출 후 initiate_auth를 통해 발급된 RefreshToken의 유효성을 검사하며 유효한 RefreshToken이 나올때까지 반복하는 것이다. ( Cognito가 동작을 완료할때까지 Polling하며 대기 )
import boto3 cognito = boto3.client("cognito-idp") COGNITO_USER_POOL_ID = "" COGNITO_APP_CLIENT_ID = "" def login(username, password): # Request cognito to invalidate all refresh tokens. cognito.admin_user_global_sign_out( UserPoolId=COGNITO_USER_POOL_ID, Username=username, ) while True: # Generates a new refresh token. auth = cognito.initiate_auth( AuthFlow="USER_PASSWORD_AUTH", AuthParameters={"USERNAME": username, "PASSWORD": password}, ClientId=COGNITO_APP_CLIENT_ID, ) id_token = auth["AuthenticationResult"]["IdToken"] access_token = auth["AuthenticationResult"]["AccessToken"] refresh_token = auth["AuthenticationResult"]["RefreshToken"] try: # Validate that the refresh token you generated is valid. cognito.initiate_auth( AuthFlow="REFRESH_TOKEN_AUTH", AuthParameters={"REFRESH_TOKEN": refresh_token}, ClientId=COGNITO_APP_CLIENT_ID, ) except cognito.exceptions.NotAuthorizedException: continue # If the refresh token is not valid, regenerate it. else: break # A valid and only refresh token has been created. return { "id_token": id_token, "access_token": access_token, "refresh_token": refresh_token, }
Python
복사