본문 바로가기

AWS/Lambda

[인스타그램 클론코딩] Cognito Pre-SignUp 람다 함수

서버리스 설치 후 서버리스 프로젝트를 생성해 줍니다.

참고: 서버리스 프레임워크 초기화 및 AWS에 연결

npx serverless create --template aws-nodejs --path beforSignUp

인라인 편집기를 사용하지 않는 이유는 무엇인가요?

Cognito와 GraphQL 서버인 백엔드에 접속하기 위해서 aws-sdkgraphql-request 모듈을 설치하기 때문입니다.

GraphQL 요청은 모듈을 사용하지 않고 어떻게든 보낼 수 있는데, aws-sdk는 Cognito User Pool에 접속하기 위해서 필수입니다.

그래서 GraphQL도 그냥 모듈을 사용했습니다.

외부 모듈을 설치하려면 인라인 편집기는 사용할 수 없습니다.

yarn add aws-sdk graphql-request

serverless.yml

서버리스 프로젝트에는 serverless.yml 파일이 있습니다.

서버리스가 생성하고 배포할 AWS 서비스들을 설정하는 파일입니다.

양이 매우 많아보이는데 주석을 제거하면 별거 없습니다.

service: beforesignup

provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-2

functions:
  hello:
    handler: handler.verifier

function 밑에 hello는 람다 함수 이름이 됩니다. 원하는 이름으로 바꿔주세요. 전 실수로 못바꿨네요.

함수 이름 아래 handler는 람다 함수가 실행할 함수를 나타냅니다. handler.verifier는 handler.js의 verifier 함수를 실행한다는 것을 의미합니다. 원래는 handler.hello인데 전 handler.verifier로 변경했습니다.

region은 한국에서 서비스한다면 ap-northeast-2(서울)이 좋겠죠.

handler.js

서버리스 프로젝트 생성시 serverless.js와 같이 생성되는 파일입니다.

handler.js 파일에 있다고 무조건 실행되는 것이 아니라, serverless.yml에 지정을 해 주어야 실행됩니다.

제가 임시로 사용하는 Pre-SignUp 함수입니다.

'use strict';

const { GraphQLClient } = require('graphql-request');
const AWS = require('aws-sdk');

module.exports.verifier = async (event, context, callback) => {
  console.log("event.request: ", event.request);
  
  const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider({
    apiVersion: '2016-04-19', 
    region: 'ap-northeast-2'
  });

  const graphQLClient = new GraphQLClient(process.env.ENDPOINT);

  let firstName = "";
  let lastName = "";
  let name = ""
  let picture = "";

  if (event.request.userAttributes.hasOwnProperty("given_name")) {
    firstName = event.request.userAttributes.given_name;
  }
  if (event.request.userAttributes.hasOwnProperty("family_name")) {
    lastName = event.request.userAttributes.family_name;
  }
  if (event.request.userAttributes.hasOwnProperty("name")) {
    name = event.request.userAttributes.name;
  }
  if (event.request.userAttributes.hasOwnProperty("picture")) {
    picture = event.request.userAttributes.picture;
  } else {
    picture = process.env.DEFAULT_IMAGE
  }
  

  // Set the email as verified if it is in the request
  if (event.request.userAttributes.hasOwnProperty("email")) {
      const email = event.request.userAttributes.email;

      const params = {
        "Filter": `email = \"${email}\"`,
        "Limit": 1,
        "UserPoolId": process.env.USER_POOL_ID
      };

      const { Users } = await cognitoidentityserviceprovider.listUsers(params).promise()
      console.log("cog_data", Users)

      if(Users.length > 0) {
        console.log("Cognito에 이미 동일한 이메일을 사용하는 사용자가 존재합니다.")
        callback("EmailExistError_Cognito");
      } else {
        const userName = event.userName;

        const createAccountMutation = 
        `mutation{
          createAccount(userName: "${userName}", email: "${email}", firstName: "${firstName}", lastName: "${lastName}", name: "${name}", picture: "${picture}")
        }`;
        console.log("Prisma에 계정 생성 요청")
        const { createAccount } = await graphQLClient.request(createAccountMutation);
        console.log("Prisma에 계정 생성")
        console.log(createAccount);
        if(createAccount === "ExistEmailError") {
          callback("EmailExistError_Prisma");
        } else if(createAccount === "ExistUserNameError") {
          callback("UserNameExistError_Prisma");
        }
      }

      //event.response.autoVerifyEmail = true;
  }

  // Return to Amazon Cognito
  callback(null, event);
};

하나 하나 알아봅시다.

먼저 매개값으로 전달받은 event.request 객체를 출력해 볼까요?

Cognito 소셜 로그인 추가 (실전) - 구글에서 속성을 매핑해 주었기 때문에 Cognito에 직접 가입한 경우나 구글을 통해서 가입한 경우나 event의 속성 이름은 동일합니다.

Cognito에 직접 가입한 경우

직접 가입할 때 성, 이름, 이메일, 패스워드만 입력받도록 코딩했기 때문에 전송된 정보가 더 적습니다.

event.request:  {
  userAttributes: { 
    last_name: '권', 
    first_name: '준동', 
    email: 'mark8125@naver.com' 
  },
  validationData: null
}

구글을 통해서 가입한 경우

Cognito 소셜 로그인 추가 (실전) - 구글에서 인증 범위를 profile email openid로 설정해 주었기 때문에 더 많은 정보가 넘어옵니다.

특히 picture를 데이터베이스에 저장해 놓으면 사용자가 느끼기에 더 친숙할 것입니다.

event.request:  {
  userAttributes: {
    email_verified: 'true',
    'cognito:email_alias': '9wonjoondong@gmail.com',
    name: '권준동',
    'cognito:phone_number_alias': '',
    given_name: '준동',
    family_name: '권',
    email: '9wonjoondong@gmail.com',
    picture: 'https://lh6.googleusercontent.com/-tax6FzeL4HI/AAAAAAAAAAI/AAAAAAAAAAA/AAKWJJMy9-KUijHTV48EwH2hQI8NpEfxHw/s96-c/photo.jpg'
  },
  validationData: {}
}

Cognito User Pool 객체 얻기

const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider({
  apiVersion: '2016-04-19', 
  region: 'ap-northeast-2'
});

GraphQL 클라이언트 객체 얻기

ENDPOINT는 람다 환경변수로 정해 놓은 GrpahQL 백앤드 접속 경로입니다.

인스타그램 클론코딩에선 백앤드가 연결된 API 게이트웨이 주소로 설정했습니다.

참조: 서버리스 GraphQL 백앤드 구축

const graphQLClient = new GraphQLClient(process.env.ENDPOINT);

evnet 객체에서 사용자 정보 추출

let firstName = "";
let lastName = "";
let name = ""
let picture = "";

if (event.request.userAttributes.hasOwnProperty("given_name")) {
  firstName = event.request.userAttributes.given_name;
}
if (event.request.userAttributes.hasOwnProperty("family_name")) {
  lastName = event.request.userAttributes.family_name;
}
if (event.request.userAttributes.hasOwnProperty("name")) {
  name = event.request.userAttributes.name;
}
if (event.request.userAttributes.hasOwnProperty("picture")) {
  picture = event.request.userAttributes.picture;
} else {
  picture = process.env.DEFAULT_IMAGE
}

event 객체에 email 속성이 있다면,

if (event.request.userAttributes.hasOwnProperty("email")) {
    ...
}

event 객체에서 이메일 추출

const email = event.request.userAttributes.email;

Cognito User Pool 객체를 사용하여 해당 User Pool에 동일한 이메일을 사용하는 사용자가 있는지 확인

const params = {
  "Filter": `email = \"${email}\"`,
  "Limit": 1,
  "UserPoolId": process.env.USER_POOL_ID
};

const { Users } = await cognitoidentityserviceprovider.listUsers(params).promise()

참조 : https://docs.aws.amazon.com/ko_kr/cognito/latest/developerguide/how-to-manage-user-accounts.html

백엔드에 계정 복제 요청

위에서 event.request만 출력했는데, userName은 request와 같은 레벨로 전송되는 속성입니다.

사용자가 로그인할 때 사용하는 ID라고 보면 됩니다.

그리고 mutation.createAccount GrapqQL 요청식은 GraqpQL API에 대한 이해가 필요합니다.

하지만 언뜻 보기에 GraphQL 서버인 백엔드에 event에서 추출한 사용자 정보를 사용하여 계정 생성을 요청하는 것 같지 않나요?  

다음에 포스팅하겠지만 백엔드의 mutation.createAccount Resolver는 내부적으로 데이터베이스에서 동일한 이메일이 있는지를 체크합니다.

만약 동일한 userName이나 이메일을 사용하는 사용자가 있다면 "EmailExistError_Prisma" 또는 "UserNameExistError_Prisma" 문자열을 반환할 것입니다.

const userName = event.userName;

const createAccountMutation = 
`mutation{
  createAccount(userName: "${userName}", email: "${email}", firstName: "${firstName}", lastName: "${lastName}", name: "${name}", picture: "${picture}")
}`;

const { createAccount } = await graphQLClient.request(createAccountMutation);

데이터베이스에 동일한 userName이나 이메일이 있다면 오류 반환

if(createAccount === "ExistEmailError") {
  callback("EmailExistError_Prisma");
} else if(createAccount === "ExistUserNameError") {
  callback("UserNameExistError_Prisma");
}

이상없다면 다음 단계로 진행

callback(null, event);

참고 : AWS Lambda 핸들러 구조

배포

그냥 아래 명령어를 입력하면 AWS 람다가 생성됩니다.

그리고 Cognito에서 사전 가입 트리거로 선택해 주면 끝납니다.

npx serverless deploy

'AWS > Lambda' 카테고리의 다른 글

AWS Lambda 핸들러 구조  (0) 2020.05.13