[AWS] Lambda에서 Cognito 사용하기

nayonso·2024년 1월 1일
0

aws rookie championship

목록 보기
4/4
post-thumbnail

🤸‍♀️ 학습 배경


음성 감정 일기 서비스 '아니 있잖아'를 만들고 있습니다. 아니 있잖아는 일기 서비스이므로 사용자마다 다른 정보를 보여줘야합니다. 그래서 '로그인'기능이 반드시 필요했습니다. 저희 팀은 완전한 서버리스 서비스를 만들고자 하여, aws에서 제공하는 사용자 관리 및 인증 서비스인 aws cognito를 사용하기로 했습니다.


📍 구글 OAuth + Cognito = ☠️


처음에는 구글 OAuth를 사용해서 간단하게 회원가입하는 것을 생각했습니다. 왜냐하면 일반적인 회원가입 과정, 즉 이메일을 인증하고, 비밀번호 두개가 일치하는지 확인해야하는 회원가입이 다소 귀찮다고 느껴졌기 때문이었습니다😥 그냥 '구글로 회원가입하기' 버튼을 누르면 뿅!🪄 하고 회원가입이 되는 것을 목표로 삼았습니다.

하지만 아무리 시도해도 회원가입이 되지 않고, 로그인도 되지 않았습니다.. 열심히 튜토리얼을 찾아가며 시도했지만 aws의 UI가 바뀌었는지 튜토리얼이랑 다른 부분이 너무 많았고, 이틀을 꼬박 투자해도 감조차 잡히지 않자, 다른 방법을 찾아보게 되었습니다.


📍 Cognito + Lambda = 😇


이후 lambda에서 cognito를 사용하는 방법을 적용해보았는데, 정말 허무할 정도로 간단히도 해결되었습니다! 저는 스프링 백엔드를 공부하는 사람으로서, [회원가입 + 로그인 + 메일 인증]이 삼종 세트를 자바로도 구현을 해봤는데요, 다소 복잡하게 느껴질 때가 많았습니다. 그런데 람다에서 cognito를 사용하니 너무나도 간단하게 해결되어서 이 과정을 꼭 공유드리고 싶었습니다!


⚒️ 실습


📍 사용자 풀 생성

사용자 풀이란, 사용자들의 프로필 데이터를 저장하고 관리하는 곳입니다. 사용자 풀을 사용하여 회원가입 및 로그인, 비밀번호 재설정, MFA(Multi-Factor Authentication) 등의 기능을 쉽게 구현할 수 있습니다.


▲ 인증 공급자의 [공급자 유형]은 Cognito 사용자 풀로 설정해줍니다. 인증 공급자는 인증을 수행하고 토큰을 발급하는 주체를 의미하는데, 연동 자격 증명 공급자를 선택하면 OAuth를 사용할 수 있습니다. 하지만, 이 실습에서는 cognito만 사용하므로 Cognito 사용자 풀로 설정합니다! 그리고 로그인 옵션으로는 이메일을 선택해줍니다. 사용자는 이메일을 ID 삼아서 로그인할 수 있게 됩니다.


▲ 암호 정책은 Cognito 기본값으로 설정합니다. 8자 이상 영문 대소문자, 숫자, 특수 문자로 구성되어야 하는 정책입니다. 회원가입시 입력하는 비밀번호가 위 조건을 만족하지 않으면 400에러가 발생합니다.


▲ MFA는 없이, 사용자 계정 복구는 활성화를 해줍니다. 위 설정대로 하면 사용자는 이메일을 통해 비밀번호를 초기화할 수 있는 링크를 수신받을 수 있습니다.


▲ SES는 사용하지 않을 것이므로 Cognito를 사용한 이메일 전송을 선택합니다. 이후 설저은 모두 디폴트로 한 후 진행합니다.


📍 회원가입 람다 테스트 형식


API Gateway에서 프록시 통합 설정을 염두에 둔 테스트 형식입니다!

{
  "resource": "/auth/sign-up",
  "path": "/auth/sign-up",
  "httpMethod": "POST",
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate, br",
    "Access-Control-Allow-Origin": "*",
    "Cache-Control": "no-cache",
    "Host": "sl6qp09xxc.execute-api.us-east-2.amazonaws.com",
    "Postman-Token": "99a10b62-1e56-4021-88e7-c7989754a64b",
    "User-Agent": "PostmanRuntime/7.35.0",
    "X-Amzn-Trace-Id": "Root=1-6576e8f2-5154ea582e4ab0335f52fefe",
    "X-Forwarded-For": "220.121.121.193"
  },
  "X-Forwarded-Port": "443",
  "X-Forwarded-Proto": "https",
  "body": "{\"email\":\"sunny100percent@naver.com\", \"password\":\"Sunny100%\"}"
}

📍 회원가입 람다 코드


import boto3
import json

def lambda_handler(event, context):
    client_id = "앱 클라이언트 아이디"
    
    # Cognito 클라이언트 생성
    cognito = boto3.client('cognito-idp', region_name='us-east-2')
    
    # 해당 프로젝트에서 어떤 요청 형식을 따르는지에 따라 이 부분만 바꿔 적용
    # requestBody 추출
    requestBody = json.loads(event['body'])
    email = requestBody['email']
    password = requestBody['password']
    print("email: ", email)
    print("password: ", password)

    # 사용자 등록 요청
    try : 
        response = cognito.sign_up(
            ClientId = client_id,
            Username = email,
            Password = password,
            UserAttributes=[
                {
                    'Name': 'email',
                    'Value': email
                }
            ]
        )
        
        # 정상 응답 형식 포매팅
        statusCode = 200
        body = {'statusCode': statusCode}

    # 에러 응답 형식 포매팅    
    except cognito.exceptions.UsernameExistsException as e:
        statusCode = 409
        message = 'An account with the given email already exists.'
        body = {'statusCode': statusCode, 'message': message}

    except cognito.exceptions.InvalidPasswordException as e:
        statusCode = 400
        message = 'Password did not conform with policy.'
        body = {'statusCode': statusCode, 'message': message}
        
    except cognito.exceptions.InvalidParameterException as e:
        statusCode = 400
        message = 'Invalid email address format.'
        body = {'statusCode': statusCode, 'message': message}

    # 반환
    return {
        'statusCode': statusCode,
        'headers': {"Access-Control-Allow-Origin" : "*"},
        'body': json.dumps(body)
    }

📍 회원가입 람다 테스트 결과


코그니토의 사용자를 확인하면, 제대로 동록 된 것을 볼 수 있다.

입력한 메일의 메일함에 가면, 인증 번호가 수신된 것을 확인할 수 있다.

📍 이메일 인증 람다 테스트 형식


{
  "resource": "/auth/sign-up",
  "path": "/auth/sign-up",
  "httpMethod": "POST",
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate, br",
    "Access-Control-Allow-Origin": "*",
    "Cache-Control": "no-cache",
    "Host": "sl6qp09xxc.execute-api.us-east-2.amazonaws.com",
    "Postman-Token": "99a10b62-1e56-4021-88e7-c7989754a64b",
    "User-Agent": "PostmanRuntime/7.35.0",
    "X-Amzn-Trace-Id": "Root=1-6576e8f2-5154ea582e4ab0335f52fefe",
    "X-Forwarded-For": "220.121.121.193"
  },
  "X-Forwarded-Port": "443",
  "X-Forwarded-Proto": "https",
  "body": "{\"email\":\"sunny100percent@naver.com\", \"code\":\"564258\"}"
}

📍 이메일 인증 람다 코드


import boto3
import json

def lambda_handler(event, context):
    # 앱 클라이언트 아이디
    client_id = "앱 클라이언트 아이디" 

    # Cognito 클라이언트 생성
    cognito = boto3.client('cognito-idp', region_name='us-east-2')
    
    # requestBody 추출
    requestBody = json.loads(event['body'])
    email = requestBody['email']
    code = requestBody['code']
    print("email: ", email)
    print("code: ", code)

    # 사용자 메일 인증
    try:
        response = cognito.confirm_sign_up(
            ClientId = client_id,
            Username = email,
            ConfirmationCode = code
        )
        
        # 정상 응답 형식 포매팅
        statusCode = 200
        body = {'statusCode': statusCode}
    
    # 에러 응답 형식 포매팅  
    except cognito.exceptions.ExpiredCodeException as e:
        statusCode = 410
        message = 'Invalid verification code.'
        body = {'statusCode': statusCode, 'message': message}

    except cognito.exceptions.CodeMismatchException as e:
        statusCode = 400
        message = 'Expired verification code.'
        body = {'statusCode': statusCode, 'message': message}
        
    # 반환
    return {
        'statusCode': statusCode,
        'headers': {"Access-Control-Allow-Origin" : "*"},
        'body': json.dumps(body)
    }

📍 메일 인증 람다 테스트 결과


코그니토의 사용자를 확인하면, 확인 상태가 '확인됨'으로 바뀌었음을 확인할 수 있습니다.


📍 로그인 람다 테스트 형식

{
  "resource": "/auth/sign-in",
  "path": "/auth/sign-in",
  "httpMethod": "POST",
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate, br",
    "Access-Control-Allow-Origin": "*",
    "Cache-Control": "no-cache",
    "Host": "sl6qp09xxc.execute-api.us-east-2.amazonaws.com",
    "Postman-Token": "99a10b62-1e56-4021-88e7-c7989754a64b",
    "User-Agent": "PostmanRuntime/7.35.0",
    "X-Amzn-Trace-Id": "Root=1-6576e8f2-5154ea582e4ab0335f52fefe",
    "X-Forwarded-For": "220.121.121.193"
  },
  "X-Forwarded-Port": "443",
  "X-Forwarded-Proto": "https",
  "body": "{\"email\":\"sunny100percent@naver.com\", \"password\":\"Sunny100%\"}"
}

📍 로그인 람다 코드


import json
import boto3

def lambda_handler(event, context):
    # 앱 클라이언트 아이디
    client_id = "앱 클라이언트 아이디" 

    # Cognito 클라이언트 생성
    cognito = boto3.client('cognito-idp', region_name='us-east-2')
    
    # requestBody 추출
    requestBody = json.loads(event['body'])
    email = requestBody['email']
    password = requestBody['password']
    print("email: ", email)
    print("password: ", password)
    
    try:
        response = cognito.initiate_auth(
            AuthFlow='USER_PASSWORD_AUTH',
            AuthParameters={'USERNAME': email, 'PASSWORD': password },
            ClientId=client_id)

        # 정상 응답 형식 포매팅
        statusCode = 200
        wrapped_item = response['AuthenticationResult']
        body = {'statusCode': statusCode, 'body': wrapped_item}

    # 에러 응답 형식 포매팅
    except cognito.exceptions.NotAuthorizedException as e:
        statusCode = 400
        message = 'Incorrect username or password.'
        body = {'statusCode': statusCode, 'message': message}
            
    except cognito.exceptions.UserNotConfirmedException as e:
        statusCode = 403
        message = 'User is not confirmed.'
        body = {'statusCode': statusCode, 'message': message}
        
    # 반환
    return {
        'statusCode': statusCode,
        # '*' 대신 도메인 주소 넣어주는 걸로 바꿔야 함. 이거 빼면 CORS 에러 발생
        'headers': {"Access-Control-Allow-Origin" : "*"},
        'body': json.dumps(body, ensure_ascii=False)
    }

📍 로그인 람다 테스트 결과


{
  "statusCode": 200,
  "headers": {
    "Access-Control-Allow-Origin": "*"
  },
  "body": "{
  		\"statusCode\": 200, 
        \"body\": {\"AccessToken\": \"액세스 토큰\", 
        \"ExpiresIn\": 10800, 
        \"TokenType\": \"Bearer\", 
        \"RefreshToken\": \"리프레시 토큰\", 
        \"IdToken\": \"아이디 토큰\"}
        }"
}

🎯 Trouble Shooting

🤨 문제 상황
위의 [로그인 람다 테스트 결과] 처럼 람다의 반환에 슬래시가 자꾸 슬래시가 포함되어서 잘못 반환되고 있는 줄 알았습니다. 왜 body 부분만 JSON 형식이 아닌지에 대해 고민하다가 해결책을 찾았습니다!

😇 해결
슬래시는 body 자체를 문자열 형식으로 보내는 과정에서 발생하는 것인데
최종적으로 클라이언트에 전달될 때는 자동으로 문자열을 JSON으로 파싱하므로
성능에는 아무런 문제가 없다고 한다고 합니다! 실제로 api gateway와 연결해서 응답을 확인해보니 깔끔하게 응답되는 것을 확인할 수 있었습니다.


사진 출처 : https://dev.classmethod.jp/articles/using-azure-ad-as-an-external-idp-for-amazon-cognito/

0개의 댓글