[Node.js] 확장프로그램에서 JWT 토큰 인증 기능 구현하기

승우·2024년 11월 4일
post-thumbnail

작성 동기

이번 학기에는 구름톤 유니브 활동을 하게 되면서 스터디로 사이드 프로젝트 만들기를 진행하게 되었다.

우리 팀은 크롬에서 동작하는 확장 프로그램을 만들기로 하였다.

첫 번째 기능은 웹에 보여지는 원문을 클릭하면 원하는 언어의 번역본으로 바꿔주고 다시 한 번 더 클릭하면 원문으로 돌아오는 번역 기능이다.

두 번째 기능은 클릭한 원문을 클라이언트가 원하는 길이로 요약해주는 요약기능이다.

위 두 기능에 대한 내용은 나중에 따로 포스트할 예정이다.

본문

일반적인 구조

웹으로 구글 소셜 로그인을 구현해 본 사람들은 잘 알겠지만 일반적인 구조에서는 클라이언트에서 소셜 로그인 버튼을 누르면 우리 서버로 연결이 되고 우리 서버에서 구글 서버로 요청을 보내 사용자 인증을 한 뒤, redirect_url을 통해 클라이언트는 로그인된 상태로 서비스를 이용할 수 있게 된다. (정말 간단히 말해서..)

but,, 확장프로그램이라면?

웹에서 소셜 로그인을 구현할 때, 서버로 요청을 보내는 과정을 거치는 이유는 아무래도 보안때문인데 크롬 확장프로그램에서는 애초에 클라이언트 측에서 로그인을 하도록 메서드까지 제공해주는 것 같았다.

구글 소셜 로그인 기능을 구현해갔는데 프론트 분께서 본인이 구현 다 하셨다고,, 다 준비해갔는데 약간 허무했지만 그래도 다시 생각해보니 발급받은 액세스 토큰을 서버에 보내주지 않으면 보안상 문제가 생길 것이 분명해보였다. 그리고 어차피 액세스 토큰을 사용하여 JWT 토큰으로 바꿔줘야 하기 때문에 API를 약간 수정하기로 했다.

수정 시작

기존의 API는 일반적인 구글 소셜 로그인 API였지만 지금은 access_token을 받아 DB에 저장하고 jwt 토큰을 반환해주는 로직으로 바꿔주기로 했다.

우선 엔드포인트부터

GET /auth/google // 변경 전

GET /auth/convert-token // 변경 후

기존에는 google의 API를 호출하는 로직이었다면 수정 후에는 클라이언트로부터 access_token을 받아 그 값을 저장하고 jwt_token으로 전환해주므로 위와 같이 변경하였다.

그러다 문득 생각이 든 점은 한 API에서 user의 데이터 저장과 jwt 토큰생성을 한 번에 처리하다보니 api엔드포인트를 어떻게 정의해야할지가 애매하다는 것이었다.

GET /auth/convert-token // 토큰 변환에 집중

GET /auth/save-user // User 정보 저장에 집중

아무래도 가장 좋은 방법은 API 분기인 것 같긴 하지만 분기를 하게되면 로직의 복잡성을 낮출 수는 있지만 그만큼 api 호출량이 늘어나기도 해서 일단 api 엔드포인트만 변경하고 이 문제는 차후에 생각해보기로 했다.

로직 생각해보기

  1. 클라이언트에서 넘겨준 토큰을 토대로 구글 서버에 사용자 정보 요청
  2. 응답으로 온 사용자 정보 DB에 저장
  3. access_token을 jwt_token으로 변환
  4. user 정보, jwt_token 값 클라이언트에 반환

위와 같은 흐름으로 생각해보았고 이제 컨트롤러부터 구현을 해보겠다.

참고로 이 포스트는 jwt로 변환하는 과정에 대한 기능에 대한 것이므로 해당 기능 위주로 설명을 할 예정이다.

controller 구현

const authService = require('../services/auth-service');
const { StatusCodes } = require('http-status-codes');

exports.saveGoogleUser = async (req, res) => {
  try {
    const { accessToken } = req.body;	// 클라이언트로부터 토큰 받아오기
    if (!accessToken) {	// 토큰이 없다면 BAD_REQUEST 처리
      res.status(StatusCodes.BAD_REQUEST).json({
        success: false,
        message: 'AccessToken is required',
      });
    }

    const { user, jwtToken } =
      await authService.getGoogleUserDataFromAccessToken(accessToken);

    // 클라이언트에 토큰 반환
    return res.status(StatusCodes.OK).json({	
      success: true,
      user,
      token: jwtToken,
    });
  } catch (err) {
    return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
      success: false,
      message: err.message,
    });
  }
};

우선 클라이언트에서 구글 로그인을 실행하여 받은 accessToken을 Request Body에 담아 서버에 보내줘야 한다. authService의 getGoogleUserDataFromAccessToken의 user와 jwtToken을 json형식으로 return하도록 해주었다. 아래 코드는 service 레이어에서 작성된 함수들이다.

service 구현

exports.getGoogleUserDataFromAccessToken = async (accessToken) => {
  try {
    const response = await axios.get(
      'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      }
    );

    const userData = response.data;

    let user = await User.findOne({
      where: { googleId: userData.id },
    });

    if (!user) {
      user = User.create({
        googleId: userData.id,
        email: userData.email,
        name: userData.name,
        accessToken: accessToken,
      });
    } else {
      await user.update({ accessToken });
    }

    const jwtToken = exports.createJwtToken(user);

    return {
      user,
      jwtToken,
    };
  } catch (err) {
    throw new Error(err.message);
  }
};

exports.createJwtToken = (user) => {
  const payload = { userId: user.id, email: user.email };
  const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
  return token;
};

로직 설명

  1. 클라이언트에서 요청 헤더에 담은 accessToken을 사용하여 구글 api에 사용자 정보 요청
  2. 구글로부터 받은 정보를 토대로 신규 회원이라면 user 테이블에 저장, 기존 회원이면 accessToken 업데이트
  3. accessToken을 jwtToken으로 변환
  4. user정보와 jwtToken값 반환

위와 같이 모든 코드를 작성하니 api가 잘 돌아가는 것을 확인할 수 있다.

다음 포스트에서는 우리 서비스를 EB에 어떻게 배포했는지를 작성하게 될 것 같다!

profile
이것저것 해볼래요

0개의 댓글