Passport js와 그럴듯한 refresh, access token 만들기

개발자 왜?전·2020년 11월 10일
3

저번에 이어서 어떤 방식으로 구현했는지 좀 더 코드의 중점을 맞췄다.


나의 방식에 대한 코드이다.

로그인

router.post('/login', isNotLoggedIn, async (req, res, next) => {
  passport.authenticate('local', { session: false }, (err, user, info) => {
    if (err) {
      console.error(err);
      return next(err);
    }
    if (info) {
      return res.status(401).send(info.reason);
    }
    return req.login(user, { session: false }, async (loginErr) => {
      if (loginErr) {
        console.error(loginErr);
        return next(loginErr);
      }
      const fullUserWithoutPwd = // 각 db에 맞춰서..
      const refreshToken = jwt.sign({ /* 원하는 내용 */ }, "JWT_SECRET", { expiresIn:'14d'});
{
	/*사용중인 DB에 refreshToken 저장*/
}
      const accessToken = jwt.sign({/* 원하는 내용 */}, "JWT_SECRET", { expiresIn: '30m' });
      res.cookie('RefreshToken', refreshToken, { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 14});
      return res.status(200).json({ me: fullUserWithoutPwd, token: accessToken });
    });
  })(req, res, next);
});

로그인이 진행되면 먼저 passport-local을 통해 수립한 local전략을 처리 후 로그인을 진행한다. 이후 각기 다른 내용으로 RefreshToken과 AccesToken을 만들어 전자는 쿠키로 후자는 JSON Payload로 사용자에게 보낸다.

AccessToken에 필요한 페이지 접속

Next 기반의 SSR을 기반으로 하기에 페이지 전환시 값 유지에 어려움이 있다. Redux Persist를 사용하지 않았다. 따라서 AccessToken이 필요한 지점에 때 맞춰 해당 토큰을 서버에서 보내기로 했다.

사용자

export const getServerSideProps = wrapper.getServerSideProps(async (context) => {
  const cookie = context.req ? context.req.headers.cookie : '';
    if (cookie) {
      axios.defaults.headers.common.Authorization = `Bearer ${cookie}`;
      context.store.dispatch({ type: LOAD_MY_INFO_REQUEST });
    }
  }
  context.store.dispatch(END);
  await context.store.sagaTask.toPromise();
});

SSR을 하기 위해 next의 getServerSideProps를 사용해 정보를 가져왔다. 이 때 refreshToken을 사용하여 사용자의 기본적인 정보와 AccessToken을 가져오도록 했다.

서버

router.get('/myinfo', passport.authenticate('refresh-jwt', { session: false }), async (req, res, next) => {
  try {
    const fullUserWithoutPwd = await db.User.findOne({
	// 원하는 내용
    });
    if (req.headers.authorization !== fullUserWithoutPwd.token) {
      return res.status(403).send("CSRF Attacked");
    }
    const accessToken = jwt.sign({ /* 원하는 내용 */}, process.env.JWT_SECRET, { expiresIn: '30m' });
    res.status(200).json({ me: fullUserWithoutPwd, token: accessToken });
  } catch (error) {
    console.error(error);
    next(error);
  }
});

서버에서는 해당 Authorization header를 확인하고 DB의 값과 비교 후 처리하도록 했다.

passport 전략 여러개 세우기

Refresh Token이 오느냐 Access Token이 오느냐를 확인하기위해 passprot-jwt의 전략을 2가지로 나눴다. 출처

    passport.use('admin-rule',
	    new JwtStrategy(opts, (...........) => {.........
    }));
    passport.use('user-rule',
    	new JwtStrategy(opts, (...........) => {.........
    }));

단순히 이름을 붙여 전략을 나눌 수 있었다.

마무리

여전히 XSS공격이 도사리고 있다. 또한 AccessToken을 재발급 받는 방법이 뚫린다면 여전히 CSRF공격 또한 가능하다. 물론 XSS취약점이 없다는 상황에서, 재발급 받은 AccessToken을 JS에서 다룰 수 없다는 점과 Referer Check를 통해 1차적으로 제한을 뒀다는 점에서 이전과는 비교할 수 없을 정도로 발전했다고 생각한다.

배우지 않았고 너무도 생소한 보안의 영역이었지만 빈틈을 계속 메워가며 발전시켜야 할 것 같다.

profile
하고 싶어 개발하는, 능동개발자

2개의 댓글

comment-user-thumbnail
2022년 2월 24일

안녕하세요 글 잘봤습니다 ^^
저 질문이있는데요
passport 전략 여러개 세우기 Refresh Token이 오느냐 Access Token이 오느냐를 확인하기위해 passprot-jwt의 전략을 2가지로 나눴다.
이부분에서 프론트 상태값에 저장된 값 + 쿠키 값 을 서버에 같이 요청보내주고
AccessToken 이 verify 했을때 유효하지않거나 변조되었을경우 다른전략으로 분기처리를 해준다는 말씀이신가요?
아니면 프론드단에서 Refresh Token 이나 Access Token 을 따로 구별해서 보내는 방법이 있는건가요?

또 토큰을 검증하는 verify 로직이 없는거같아서요 이걸 안쓰면 굳이 jwt 를 쓰는 이유가 없다고 생각이돼서요 ..제가놓친부분이 있는걸까요?

1개의 답글