AWS Cognito를 사용하여 로그인과 회원가입 구현하기

부루베릐·2024년 2월 12일
1

TIL

목록 보기
16/20

개요

AWS Cognito란?

AWS Cognito는 어플리케이션을 위한 자격 증명 플랫폼이다. 사용자를 등록하고 회원 가입을 진행하며, 어플리케이션에 접근할 수 있는 자격을 증명하는 등 여러 역할을 담당한다. 이에 더해 OAuth2.0이나 OIDC(OpenID Connect) 등 기본적인 IdP(Identity Providers)도 지원하며 사용자에게 S3와 같은 AWS 리소스에 접근할 수 있는 한시적인 권한도 부여할 수 있다. 이렇듯 인증에 대한 작업들을 편하게 처리해줄 수 있는 서비스이므로 사이드 프로젝트에 이 서비스를 적용해보려 한다.

Cognito의 구성 요소

Cognito는 사용자 풀과 자격증명 풀이라는 크게 두 가지 구성 요소를 가진다.

사용자 풀

사용자 풀은 어플리케이션 사용자를 저장하고 관리하는 사용자 디렉토리이다. 사용자 풀은 특정 어플리케이션에 로그인 기능을 제공해줄 수 있다. 어플리케이션에서 Cognito의 사용자 풀에 접근하면 사용자 풀은 해당 사용자를 어플리케이션에 등록하고 인증하는 역할뿐만 아니라 마치 OAuth2.0처럼 사용자의 신원을 확인하고 리소스에 접근할 수 있는 권한을 부여하는 JWT(JSON Web Token)을 발급하여 어플리케이션과 소통하는 작업 역시 해 준다. 즉 Cognito는 그 자체로써 IdP(Identity Provider) 역할을 담당하는 서비스이다.

또한 직접 AWS Cognito를 통하지 않고서도 소셜 로그인 제공자(네이버, 카카오 등)와 같은 서드 파티 IdP(Identity Provider)를 사용할 수도 있다. 이렇게 다양한 시스템과 어플리케이션 간 신원 정보를 공유하는 프로세스를 페더레이션(Federation)이라 한다. 그러므로 사용자 풀은 직접 코그니토를 통해 인증하는 로컬 사용자와 서드 파티 IdP를 통해 인증된 페더레이션 사용자 총 두 종류의 사용자 프로필을 가질 수 있다.

자격증명 풀(Identity Pools)

자격증명 풀은 어플리케이션 사용자가 AWS 리소스에 접근할 수 있도록 권한을 부여해주는 역할을 한다. 이를 통해서 사용자는 직접 S3나 DynamoDB 같은 AWS 서비스에 접근할 수 있다. 이 때 위의 그림과 같이 사용자 각각에게 AWS 리소스에 접근할 수 있는 IAM 역할(role)을 부여하여 사용자마다 허용된 접근 리소스 범위를 설정하게 된다. 예를 들어 인증 정보가 없는 게스트 사용자의 경우 아주 제한적인 권한만 허용해주고, 어드민 계정에는 좀 더 널널한 권한을 열어주는 경우가 있을 수 있다.

시작하기

우리는 어플리케이션의 회원가입과 로그인 기능을 구현하기 위해 Cognito를 사용하므로, 일단은 사용자 풀을 생성하고 이를 어플리케이션과 연동하는 작업을 진행할 예정이다.

User pool 생성

IdP(Identity Provider)에 대한 항목들이다. 우리는 자체적인 회원가입/로그인 기능뿐만 아니라 서드 파티 IdP로 구글 소셜 로그인도 구현할 것이기 때문에 Federated IdP 항목도 체크해준다. 또한 이메일을 통해 회원가입과 로그인을 진행할 것이기 때문에 sign-in option에 이메일 선택.

Configure security requirements

보안과 관련된 항목들이다. 다음과 같이 비밀번호 관련 validation 정책을 정할수도 있고, 만약 MFA(Multi-Factor Authentication) 기능을 원한다면 직접 설정해 줄 수 있다!

우리 서비스에서는 일단은 MFA 기능은 사용하지 않도록 하였다.

Configure sign-up experience
회원가입 시 설정에 대한 항목들이다.

self registration 설정을 켜 놓음으로써 관리자가 사용자를 위해 매번 계정을 생성해주지 않아도 사용자가 직접 회원가입을 할 수 있도록 한다.

또한 회원가입을 했을 때 사용자가 잘못된 이메일을 입력했을 때를 대비하여 다음과 같이 등록한 이메일을 인증할 수 있도록 메시지 설정을 해 준다.

Integrate your app

이제 어플리케이션과 통합하는 과정을 진행한다. Cognito를 통해 로그인과 회원가입 기능을 만들 때 개발자는 크게 두 가지 선택지를 가지고 있다.

첫 번째 방법은 사용자 풀 엔드포인트를 사용하여 이미 만들어진 빌트인 로그인 및 회원가입 UI를 사용하는 것이다. 이를 사용자 풀 Hosted UI라 한다. 만약 소셜 로그인을 추가한다면 해당 소셜 서비스에 로그인할 수 있는 버튼 또한 생성되고, 비밀번호 정책에 따라 validation을 진행할 수도 있다. MFA(Multi-Factor Authentication)를 위한 페이지도 마찬가지이다. 물론, 우리 서비스는 자체적인 회원가입과 로그인 페이지가 있으므로 사용자 풀 Hosted UI를 사용하지는 않겠지만 해당 UI를 만들 리소스가 없는 상황에서는 매력적인 선택지가 될 것 같다.

두 번째 방법은 사용자 풀 API를 이용하는 것이다. 우리 서비스와 같이 커스텀하게 회원가입 관련 UI를 만들어야 하는 경우 회원가입에 필요한 정보들을 모두 받아 API를 통해 요청하고 사용자 인증을 받으면 된다. 사용자 풀 API를 통해 인증을 진행할 때 반환되는 토큰을 가지고 인증 절차를 진행하면 된다. 이 때 서드 파티 IdP를 통한 소셜 로그인의 경우 사용자 풀 API로는 구현할 수 없고 사용자 풀 엔드포인트를 사용해야 하므로, 이 점은 주의하여야 한다.

다음과 같이 사용자 풀의 이름을 정하고 도메인 주소를 정하여야 한다. 도메인 주소는 Hosted UI에 접근하기 위해 사용될 뿐만 아니라 OAuth2.0의 엔드포인트 용도로도 사용되기 때문에 Hosted UI를 사용하지 않더라도 소셜 로그인을 등록할 예정이라면 잘 설정해주는 것이 좋다.

Initial App Client와 Advanced app client settings

앱 클라이언트는 어플리케이션과 사용자 풀을 이어주는 역할을 하는 플랫폼이다. 앱 클라이언트를 통해서 어플리케이션이 접근 가능한 사용자의 데이터와 속성을 설정할 수 있다. 앱은 이 데이터를 사용하여 리소스에 대한 액세스를 인증하고 권한을 부여하게 된다.

Advanced app client settings에서 해당 app client의 엑세스 토큰과 리프레시 토큰의 만료 기간 등 인증에 관한 설정들을 할 수 있다.

생성 완료

이렇게 이것저것 설정을 진행하면 다음과 같이 사용자 풀이 생성된 것을 볼 수 있다.

사용자 풀에 접속하면 다음과 같이 App Client 설정 정보가 나오는데, 위의 사용자 풀 ID와 아래의 클라이언트 앱 ID는 개발자가 만들 어플리케이션에서 사용할 사용자 풀을 특정지어주는 역할을 하므로 중요하다.

사용자 풀 API 사용

시나리오

앞서 이야기한 것처럼 우리는 사용자 풀 API를 통해 회원가입과 로그인 로직을 작성하여야 한다. 이에 다음과 같은 시나리오로 접근하려 한다.

  1. 회원가입 페이지로 이동 후 이메일과 비밀번호를 입력한다.
  2. 클라이언트에서 SignUp API 요청을 보내어 사용자 풀에 새 사용자를 등록한다.
  3. 사용자가 로그인하였을 때 클라이언트에서 InitiateAuth API 요청을 보내어 ID 토큰, 엑세스 토큰 그리고 리프레시 토큰을 받아 저장한다.

사용자 풀 인증 및 미인증 API 작업

사용자 풀 API는 인증이 필요한 버전과 인증이 필요하지 않은 버전으로 나눌 수 있다. 세부적으로는 IAM 인증 관리 작업, IAM 인증 사용자 작업, 미인증 사용자 작업, 토큰 권한 부여 사용자 작업 총 4 종류의 작업으로 나뉜다.

예를 들어 IAM 인증 관리 작업으로 분류되는 UpdateUserPool이라는 API는 사용자 풀을 업데이트할 때 사용하는데, 직접 사용자 풀에 접근하여 수정하여야 하므로 AWS 보안 인증 정보와 IAM(Identity and Access Management, AWS 리소스에 대한 접근을 안전하게 제어할 수 있는 웹 서비스) 권한이 필요하다.

이에 반해 SignUp API는 인증하지 않은 임의의 사용자가 어플리케이션에 가입할 때 사용하는 API이므로 미인증 사용자 작업 API로 분류된다.

토큰 권한 부여 사용자 작업은 사용자가 로그인된 상태에서 로그아웃이나 인증 정보를 관리할 때 사용한다. 이 때는 로그인 완료 시 반환되는 액세스 토큰을 요청 Authorization 헤더에 Bearer <token>의 형태로 포함하여 API를 요청하여야 한다.

AWS SDK를 사용하여 사용자 풀 API 작업하기

라이브러리 설치

Cognito를 프로젝트에서 사용하기 위해서 Cognito SDK인 amazon-cognito-identity-js를 사용하려 한다.

npm install --save amazon-cognito-identity-js

사용자 풀을 어플리케이션에 연결

라이브러리를 설치하고 내가 사용하려는 사용자 풀을 어플리케이션에 가지고 오는 작업을 진행한다. 이 때 어떤 사용자 풀을 가져올지를 특정하기 위해서 사용자 풀 ID클라이언트 앱 ID가 필요하다. 이 두 키 값은 사용자 인증을 진행하는 과정에서 클라이언트 사이드에서 사용해야 하는 데이터이므로 환경 변수를 작성할 때 NEXT_PUBLIC_ 접두사를 붙여 클라이언트에서 사용할 수 있도록 한다.

// .env.local
NEXT_PUBLIC_COGNITO_USER_POOL_ID=ap-northeast-2_xxxxxxx
NEXT_PUBLIC_COGNITO_CLIENT_ID=xxx.....

다음과 같이 CognitoUserPool 생성자를 통해 사용자 풀을 가져올 수 있다.

// lib/user-pool.ts
import {
  CognitoUserPool,
  ICognitoUserPoolData,
} from 'amazon-cognito-identity-js';

const userPoolData: ICognitoUserPoolData = {
  UserPoolId: process.env.NEXT_PUBLIC_COGNITO_USER_POOL_ID,
  ClientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID,
};

const userPool = new CognitoUserPool(userPoolData);
export default userPool;

회원가입

우리가 초기화한 사용자 풀을 import하여 SignUp API를 호출한다.

// pages/sign-up/index.tsx

import userPool from '@/lib/user-pool';

function handleSignUp({ email, newPassword }: SignUpFormValues) {
  userPool.signUp(email, newPassword, [], [], (error, data) => {
    if (error) {
      return console.error(error);
    }
    alert('회원가입 완료! 이메일을 확인하여 인증 바랍니다.');
    router.push('/login');
  });
}

이 때 AWS 사용자 풀을 확인해 보면 다음과 같이 인증되지 않은 사용자라 뜬다. 이는 Cognito에서 사용자가 회원가입을 했을 때 유효한 이메일을 입력했는지 여부를 검증해야 하기 때문이다.

회원가입 후 가입한 이메일을 인증하기 위해서 이메일 인증 메세지를 설정해 줄 수 있다. 사용자가 6자리 코드를 받아 이를 직접 인증할 수도 있는데, 일단 과정을 좀 더 간편하게 하기 위해서 인증용 링크를 보낼 수 있도록 설정했다.

이제 사용자가 회원가입을 완료하면 다음과 같이 이메일을 받게 된다.

이 링크를 클릭하면 다음과 같이 회원가입이 완료되었다는 페이지로 이동하면서 회원가입이 마무리된다.

이제 사용자 계정이 verified 되었다.

로그인

로그인 로직은 다음과 같다.

// pages/login/index.tsx
import { CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
import userPool from '@/lib/user-pool';

const USER_NOT_CONFIRMED_EXCEPTION = 'UserNotConfirmedException';
const NOT_AUTHORIZED_EXCEPTION = 'NotAuthorizedException';

function handleLogin({ email, password }) {
  const cognitoUser = new CognitoUser({ Username: email, Pool: userPool });
  const authenticationDetails = new AuthenticationDetails({
    Username: email,
    Password: password,
  });

  cognitoUser.authenticateUser(authenticationDetails, {
    onSuccess: function (result) {
      const accessToken = result.getAccessToken().getJwtToken();
      console.log('accessToken', accessToken);
    },
    onFailure: function (err) {
        if (err.code === USER_NOT_CONFIRMED_EXCEPTION) {
          setError('root.serverError', {
            type: USER_NOT_CONFIRMED_EXCEPTION,
            message: '가입한 이메일을 인증해주세요.',
          });
        } else if (err.code === NOT_AUTHORIZED_EXCEPTION) {
          setError('root.serverError', {
            type: NOT_AUTHORIZED_EXCEPTION,
            message: '이메일 또는 비밀번호가 잘못되었습니다.',
          });
        }
      },
  });
}

성공하였을 때 인자로 받는 result는 크게 엑세스 토큰, 리프레시 토큰, 그리고 ID 토큰 총 세 가지 토큰을 반환한다. 엑세스 토큰과 리프레시 토큰은 우리가 서버와 API를 통해 소통할 때 사용자의 인증 여부를 판단할 때 사용하는 토큰이고, ID 토큰은 해당 토큰의 소유자가 이메일 인증 여부나 이메일 주소처럼 사용자 인증에 관한 메타데이터가 들어간 토큰이다.

이제 여기서 받은 엑세스 토큰을 서버에 보낼 HTTP 요청 Authorization 헤더에 Bearer <token>의 형태로 넣어주어 통신하면 된다.

결론

AWS Cognito는 사용자 회원가입, 인증, 로그인 등에 사용될 수 있는 강력한 서비스라 생각한다. 내가 포함하지 않았을 뿐이지, MFA나 소셜 로그인과 같은 부가적인 기능들도 쉽게 사용할 수 있다. 현재 사이드 프로젝트로 만들고 있는 어플리케이션은 로그인과 회원가입 등의 기능은 메인 기능이 아니므로, 이럴 때 Cognito를 사용하여 간편하게 구현하면 좋을 것 같다는 생각이 들었다.

0개의 댓글