AWS Cognito + Amplify UI + React

0. 들어가기

이 포스팅에서는 AWS의 Cognito user pool을 사용해서 React app에서 사용자 인증 과정(sign-up, sign-in, sign-out)을 구현하고자 한다.

Cognito란?

  • 웹 및 모바일 앱에 대한 인증, 권한 부여 및 사용자 관리를 제공함.
  • 사용자는 사용자 이름과 암호를 사용해 직접 로그인 하거나 Facebook, Amazon, Google, Apple 같은 타사를 통해 로그인 가능.
  • 더 자세한 정보는 공식 문서 참조.

User Pools

  • 사용자 풀은 Amazon Cognito의 사용자 디렉터리.
  • 사용자 풀이 있으면 사용자는 Cognito를 통해 웹 또는 모바일 앱에 로그인할 수 있음.
  • 사용자 풀 제공 사항
    • 가입 및 로그인 서비스.
    • 사용자 로그인을 위한 내장 사용자 지정 웹 UI.
    • Facebook, Google, Login with Amazon 및 Sign in with Apple을 통한 소셜 로그인 및 사용자 풀의 SAML 자격 증명 공급자를 통한 로그인.
    • 사용자 디렉터리 관리 및 사용자 프로필.
    • 멀티 팩터 인증(MFA), 이상 있는 자격 증명 확인, 계정 탈취 보호, 전화 및 이메일 확인과 같은 보안 기능.
    • AWS Lambda 트리거를 통한 사용자 지정 워크플로우 및 사용자 마이그레이션.

1. Cognito User Pool 생성

로그인 환경 구성

여기서 연동 자격 증명 공급자를 선택하면 소셜 로그인도 설정할 수 있다.

보안 요구 사항 구성

암호 정책은 기본값으로 사용.
사용자 지정으로 하면 암호 요구 사항을 custom 할 수 있음.

멀티 팩터 인증은 없음으로 설정하고, 사용자 계정 복구는 default로 사용.

가입 환경 구성

default로 설정

필수 속성은 name만 선택. 필수 속성 아래에는 사용자 지정 속성이 있는데, 여기에서 필수 속성에 있지 않은 custom 속성을 설정해서 사용할 수 있다.

메시지 전송 구성

Cognito를 사용하여 이메일 전송.

앱 통합

사용자 풀 이름을 설정하고, 앱 클라이언트를 설정한다.
앱 클라이언트가 있어야 react에서 사용할 수 있으니 꼭 생성한다.

2. React에서 user pool 설정

Amplify 사용하기

Amplify 패키지 설치

npm install aws-amplify @aws-amplify/ui-react
npm install aws-amplify

Auth 설정

리액트 앱에서 amplify auth를 설정하려면 user pool의 ID, ARN, 클라이언트 ID가 필요하다.
Cognito 콘솔에서 찾아서 복사.

리액트 앱에서 awsconfig.js로 만들어서 사용.

export default {
  Auth: {
    region: "YOUR-REGION",
    userPoolId: "YOUR-POOL-ID",
    userPoolWebClientId: "YOUR-CLIENT-ID"
  }
}

3. Sign-Up, Sign-In, Sign-Out 구현

전체적인 리액트 파일의 구조는 이렇게 되어 있다.

App.js

import "./App.css";
import React from "react";
import AuthView from "./views/AuthView";

export default function App() {
  return (
    <>
      <AuthView />
    </>
  );
}

AuthView.js

Auth process 관련 함수를 정의하고, Authenticator 를 사용해서 UI를 구현했다.
Authenticator를 사용하면 바로 로그인 UI가 구현된다.
하지만, 백엔드 코드가 작성되어 있지 않아서 제대로 동작하지 않는다. 위에서 로그인 관련 함수를 작성해둬야 한다. 이 함수들은 공식 문서에 코드가 나와 있으며, 자신의 user pool에 맞춰서 변경하면 된다.
return에서 main 코드 안에는 로그인 후에 볼 수 있는 화면을 설정한다.

import React from "react";
import Amplify, { Auth } from "aws-amplify";
import { Authenticator, Button } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";

import components from "../theme/components";
import formFields from "../theme/formFields";

import awsconfig from "../auth/awsconfig";
Amplify.configure(awsconfig);

export default class AuthView extends React.Component {
  constructor() {
    super();
    this.state = {
      username: "",
      email: "",
      password: "",
      new_password: "",
      code: "",
      stage: "SIGNIN",
      cognito_username: "",
    };
  }
  componentDidMount() {
    Auth.currentAuthenticatedUser()
      .then((user) => {
        console.log(user);
        this.setState({ stage: "SIGNEDIN", cognito_username: user.username });
        // console.log(user.signInUserSession.accessToken.jwtToken);
      })
      .catch(() => {
        console.log("Not signed in");
      });
  }
  signUp = async () => {
    let self = this;
    try {
      const user = await Auth.signUp({
        username: self.state.email,
        password: self.state.password,
        attributes: {
          email: self.state.email, // optional
          name: self.state.username,
        },
      });
      self.setState({ stage: "VERIFYING" });
    } catch (error) {
      console.log("error signing up:", error);
    }
  };
  signOut = async () => {
    try {
      await Auth.signOut();
      window.location.href = "/";
    } catch (error) {
      console.log("error signing out: ", error);
    }
  };
  signIn = async () => {
    let self = this;
    try {
      const user = await Auth.signIn({
        username: self.state.email,
        password: self.state.password,
      });
      this.setState({ stage: "SIGNEDIN", cognito_username: user.username });
    } catch (error) {
      console.log("error signing in", error);
      if (error.code === "UserNotConfirmedException") {
        await Auth.resendSignUp(self.state.email);
        this.setState({ stage: "VERIFYING" });
      }
    }
  };

  confirm = async () => {
    let self = this;
    console.log(self.state.code);
    let username = self.state.email;
    let code = self.state.code;
    try {
      await Auth.confirmSignUp(username, code);
      //바로로그인?
      this.signIn();
    } catch (error) {
      console.log("error confirming sign up", error);
    }
  };
  changePassword = async () => {
    let self = this;
    Auth.currentAuthenticatedUser()
      .then((user) => {
        return Auth.changePassword(
          user,
          self.state.password,
          self.state.new_password
        );
      })
      .then((data) => console.log(data))
      .catch((error) => console.log(error));
  };
  changePasswordForgot = async () => {
    let self = this;
    Auth.forgotPasswordSubmit(
      self.state.email,
      self.state.code,
      self.state.new_password
    )
      .then((data) => {
        console.log("SUCCESS");
      })
      .catch((error) => {
        console.log("err", error);
      });
  };
  resendCode = async () => {
    let self = this;
    try {
      await Auth.resendSignUp(self.state.email);
      console.log("code resent succesfully");
    } catch (error) {
      console.log("error resending code: ", error);
    }
  };
  sendCode = async () => {
    let self = this;
    Auth.forgotPassword(self.state.email)
      .then((data) => {
        console.log(data);
      })
      .catch((error) => console.log(error));
  };

  render() {
    return (
      <Authenticator
        loginMechanisms={["email"]}
        variation="modal"
        formFields={formFields}
        components={components}
      >
        {({ signOut, user }) => (
          <main>
            <h1>Hello {user.username}</h1>
            <Button onClick={signOut}>Sign out</Button>
          </main>
        )}
      </Authenticator>
    );
  }
}

여기 코드를 보면 formFields와 components를 정의해두었다. 이 옵션은 필수는 아니고 UI를 커스텀한 것이다. 공식 문서를 참조하면 된다.한 파일 안에 작성해도 되지만 길어져서 분리해서 두었다.

<Authenticator
        loginMechanisms={["email"]}
        variation="modal"
        formFields={formFields}
        components={components}
>

components.js

components는 로그인 화면의 전체적인 배치를 custom 할 수 있게 한다. Header, Footer 등을 정의할 수 있다.

import {
  useAuthenticator,
  useTheme,
  View,
  Image,
  Text,
  Heading,
  Button,
} from "@aws-amplify/ui-react";

import logo from "../assets/images/_logo_white.png";

const components = {
  Header() {
    const { tokens } = useTheme();

    return (
      <View textAlign="center" padding={tokens.space.large}>
        <Image alt="Amplify logo" src={logo} />
      </View>
    );
  },

  Footer() {
    const { tokens } = useTheme();

    return (
      <View textAlign="center" padding={tokens.space.large}>
        <Text color={`${tokens.colors.neutral["80"]}`}>
          &copy; All Rights Reserved
        </Text>
      </View>
    );
  },

  SignIn: {
    Header() {
      const { tokens } = useTheme();

      return (
        <Heading
          padding={`${tokens.space.xl} 0 0 ${tokens.space.xl}`}
          level={3}
        >
          Sign in to your account
        </Heading>
      );
    },
    Footer() {
      const { toResetPassword } = useAuthenticator();

      return (
        <View textAlign="center">
          <Button
            fontWeight="normal"
            onClick={toResetPassword}
            size="small"
            variation="link"
          >
            Reset Password
          </Button>
        </View>
      );
    },
  },

  SignUp: {
    Header() {
      const { tokens } = useTheme();

      return (
        <Heading
          padding={`${tokens.space.xl} 0 0 ${tokens.space.xl}`}
          level={3}
        >
          Create a new account
        </Heading>
      );
    },
    Footer() {
      const { toSignIn } = useAuthenticator();

      return (
        <View textAlign="center">
          <Button
            fontWeight="normal"
            onClick={toSignIn}
            size="small"
            variation="link"
          >
            Back to Sign In
          </Button>
        </View>
      );
    },
  },
  ConfirmSignUp: {
    Header() {
      const { tokens } = useTheme();
      return (
        <Heading
          padding={`${tokens.space.xl} 0 0 ${tokens.space.xl}`}
          level={3}
        >
          Enter Information:
        </Heading>
      );
    },
    Footer() {
      return <Text>Footer Information</Text>;
    },
  },
  SetupTOTP: {
    Header() {
      const { tokens } = useTheme();
      return (
        <Heading
          padding={`${tokens.space.xl} 0 0 ${tokens.space.xl}`}
          level={3}
        >
          Enter Information:
        </Heading>
      );
    },
    Footer() {
      return <Text>Footer Information</Text>;
    },
  },
  ConfirmSignIn: {
    Header() {
      const { tokens } = useTheme();
      return (
        <Heading
          padding={`${tokens.space.xl} 0 0 ${tokens.space.xl}`}
          level={3}
        >
          Enter Information:
        </Heading>
      );
    },
    Footer() {
      return <Text>Footer Information</Text>;
    },
  },
  ResetPassword: {
    Header() {
      const { tokens } = useTheme();
      return (
        <Heading
          padding={`${tokens.space.xl} 0 0 ${tokens.space.xl}`}
          level={3}
        >
          Enter Information:
        </Heading>
      );
    },
    Footer() {
      return <Text>Footer Information</Text>;
    },
  },
  ConfirmResetPassword: {
    Header() {
      const { tokens } = useTheme();
      return (
        <Heading
          padding={`${tokens.space.xl} 0 0 ${tokens.space.xl}`}
          level={3}
        >
          Enter Information:
        </Heading>
      );
    },
    Footer() {
      return <Text>Footer Information</Text>;
    },
  },
};

export default components;

formFields.js

formFiels는 각 화면에서 textfield 같은 부분을 커스텀 할 수 있는데, label이나 placeholder 등을 설정할 수 있다.

const formFields = {
  signIn: {
    username: {
      labelHidden: false,
      placeholder: "Enter your email",
    },
  },
  signUp: {
    email: {
      // label: "Email",
      // labelHidden: false,
      placeholder: "Enter your email",
      order: 1,
    },
    name: {
      // label: "Username:",
      // labelHidden: false,
      placeholder: "Enter your name",
      order: 2,
    },
    password: {
      // labelHidden: false,
      // label: "Password:",
      placeholder: "Enter your Password:",
      isRequired: false,
      order: 3,
    },
    confirm_password: {
      // labelHidden: false,
      // label: "Confirm Password:",
      order: 4,
    },
  },
  forceNewPassword: {
    password: {
      labelHidden: false,
      placeholder: "Enter your Password:",
    },
  },
  resetPassword: {
    username: {
      labelHidden: false,
      placeholder: "Enter your email:",
    },
  },
  confirmResetPassword: {
    confirmation_code: {
      labelHidden: false,
      placeholder: "Enter your Confirmation Code:",
      label: "New Label",
      isRequired: false,
    },
    confirm_password: {
      labelHidden: false,
      placeholder: "Enter your Password Please:",
    },
  },
  setupTOTP: {
    QR: {
      totpIssuer: "test issuer",
      totpUsername: "amplify_qr_test_user",
    },
    confirmation_code: {
      labelHidden: false,
      label: "New Label",
      placeholder: "Enter your Confirmation Code:",
      isRequired: false,
    },
  },
  confirmSignIn: {
    confirmation_code: {
      labelHidden: false,
      label: "New Label",
      placeholder: "Enter your Confirmation Code:",
      isRequired: false,
    },
  },
};

export default formFields;

4. 구현 화면

색은 커스텀하지 않았고, 로고만 변경해주었다.
Create Account 버튼을 누르면, 입력한 이메일 주소로 verification code가 발송되고, 코드를 입력하면 user pool에서 확인된 사용자로 바뀌고 계정 생성이 완료된다.계정을 생성하고 cognito 콘솔에서 생성된 사용자를 확인할 수 있다.

계정 생성이 완료되면 자동으로 로그인이 되고, user name과 sign-out 버튼을 볼 수 있다.

참고

cognito-auth-react

AWS Cognito와 Amplify로 로그인 기능 구현하기 (React)

좋은 웹페이지 즐겨찾기