저번에 이어서 어떤 방식으로 구현했는지 좀 더 코드의 중점을 맞췄다.
나의 방식에 대한 코드이다.
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로 사용자에게 보낸다.
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의 값과 비교 후 처리하도록 했다.
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차적으로 제한을 뒀다는 점에서 이전과는 비교할 수 없을 정도로 발전했다고 생각한다.
배우지 않았고 너무도 생소한 보안의 영역이었지만 빈틈을 계속 메워가며 발전시켜야 할 것 같다.
안녕하세요 글 잘봤습니다 ^^
저 질문이있는데요
passport 전략 여러개 세우기 Refresh Token이 오느냐 Access Token이 오느냐를 확인하기위해 passprot-jwt의 전략을 2가지로 나눴다.
이부분에서 프론트 상태값에 저장된 값 + 쿠키 값 을 서버에 같이 요청보내주고
AccessToken 이 verify 했을때 유효하지않거나 변조되었을경우 다른전략으로 분기처리를 해준다는 말씀이신가요?
아니면 프론드단에서 Refresh Token 이나 Access Token 을 따로 구별해서 보내는 방법이 있는건가요?
또 토큰을 검증하는 verify 로직이 없는거같아서요 이걸 안쓰면 굳이 jwt 를 쓰는 이유가 없다고 생각이돼서요 ..제가놓친부분이 있는걸까요?