로그인 시 빼놓으면 섭섭한 passport.js의 github을 살펴보며 passport의 흐름에 대해 정리해보자.
Passport.js
`passport.js`는 인증을 도와주는 미들 웨어입니다. passport는 Oauth와 세션 쿠키 관리를 쉽게 도와줍니다. passportjs를 사용하며 공식문서가 조금 빈약하다고 느껴졌고 passport의 깃헙의 코드를 읽어봤습니다. 주석으로 설명이 너무 잘되어있어 깃헙을 읽게 된 과정과 내용을 정리해보려 합니다. 글이 다소 복잡할 수 있어 캡쳐와 그림만 봐도 내용을 이해하는데 큰 무리가 없도록 노력했습니다. 이 글에서는 제가 작성한 예시 코드로 설명합니다. 예시코드는 깃헙 repository에 공유해두었습니다.
이 글은 크게 두가지 내용을 담고 있습니다.
- passport의 작동 흐름
- passport 깃헙 살펴보는 저의 고군분투기
깃헙을 뜯어본 과정을 정리하고자 쓰는 글이지만 작동 흐름이 더 유익하실 것 같습니다.
Passport의 작동 흐름
먼저 세션과 쿠키를 포함한 로그인 과정을 생각해봅시다.
대략적인 과정은 위 흐름 정도가 될 거 같습니다. 에러 처리는 배제했습니다. 위 흐름을 따라가며 passport의 실행 순서를 설명드리겠습니다. passport의 실행 순서와 원리를 파악하기 위해 회원 가입, 로그인, 로그아웃, 마이 페이지를 기능을 구현해 봤습니다. 제가 작성한 코드를 토대로 설명드리겠습니다. 전체 소스코드는 깃헙에서 확인 가능합니다.
1. post 요청받기
이 부분은 passport의 역할은 아닙니다. 하지만 코드 상에서 post 요청에서 passport가 미들웨어로 사용되기에 추가했습니다. express로 설명드리겠습니다.
app.post('/auth/login',
passport.authenticate('local', {
// successRedirect: '/mypage',
failureRedirect: '/login',
}),
(req, res) => {
return res.redirect('/mypage');
}
);
/auth/login으로 post 요청을 보내면 위 라우터에서 passport.authenticate() 미들웨어를 실행합니다. 정확히 말하면 passport.authenticate()의 리턴 값이 미들웨어 입니다. 여기서 이 authenticate 함수 때문에 깃헙을 찾아보게 되었습니다. authenticate함수에 관해서는 로그인 과정을 따라가는 지금의 흐름에 방해되니 글 아래쪽에 따로 정리했습니다.
2. 회원 인증
authenticate 미들웨어가 실행되면 첫 번째 인자로 받은 strategy name(위 예시에서는 'local')으로 등록되어 있는 strategy를 실행시킵니다. localStrategy는 언제 등록했을까요?
passport.use(
new LocalStrategy({ usernameField: 'id', passwordField: 'pw' }, (username, password, done) => {
const user = userFinder('id', username);
if (user) {
if (user.pw === password) return done(null, user);
else return done(null, false, { message: 'Incorrected password' });
} else return done(null, false, { message: 'Incorrected ID' });
})
);
위 코드에서 passport에 'local' strategy name에 localStrategy를 등록했습니다.
const LocalStrategy = require('passport-local').Strategy; // 방법1
const LocalStrategy = require('passport-local'); // 방법2
localStrategy는 passport-local입니다. 여기서 많은 블로그와 강의에서 방법 1을 사용합니다. passport-local에서 Strategy를 꺼내오는데 공식문서에는 방법 2를 사용합니다. 둘의 차이를 비교해보기 위해 코드를 보니....
둘은 같습니다. 아마도 예전에는 방법 1로 쓰다가 방법 2를 추가한 것이 아닐까 싶습니다. 이제 그만 편하게 방법 2를 씁시다.
strategy에서는 options에 따라 설정을 바꾸고 콜백 함수(strategy의 콜백)에 username, password, 콜백(이하 done) 세 개의 인자를 주입해 회원 인증을 합니다. 그 뒤 strategy는 회원 인증 결과에 따라 done를 호출합니다. 공식 문서에는 cb로 done함수를 나타내고 있지만 done으로 자주 사용되어 done을 사용하겠습니다.
done은 3개의 인자를 받습니다. `done(error, 성공했을 때 넘길 데이터, 실패했을 때 넘길 데이터)` 회원 인증이 완료되면 done에 두 번째 인자로 user데이터를 넘겨줍니다.
3. 회원의 식별자와 쿠키 정보를 세션에 저장, 세션 쿠키 발급
passport.serializeUser(function (user, done) {
done(null, user.id);
});
인증이 성공되면 LocalStrategy에서 done(null, user)를 호출하고 이때 done의 인자로 넣어준 user를 serializeUser의 콜백이 받습니다. serializeUse의 콜백은 done을 호출해 세션에 저장할 유저의 식별자를 넘겨줍니다. user 전체를 넘겨줘도 되지만 저장공간을 위해 최소한의 식별자만 넘겨주는 것이 좋습니다. 즉 serializeUser에서는 어떤 식별자를 세션 쿠키에 저장할지를 선택만 하고 실제 세션 쿠키에 저장하는 부분은 passport에서 내부적으로 등록된 session strategy가 해결해줍니다. (passport/lib/authenticator.js의 229번째 줄과 passport/lib/strategies/session.js 참고)
4. 응답에 쿠키 추가
passport의 코드를 보면 내부적으로 authenticate -> req.login -> serializeUser을 순차적으로 호출합니다. serializeUser의 콜백 done은(sessionStrategy) 인자로 받은 object를 req.session에 유저 정보를 저장합니다.
여기서 req.login은 passport에서 req에 주입한 메서드이고 req.session은 express-session에서 req에 주입한 객체입니다. passport가 req.session에 로그인 된 사용자 정보를 넣어주면 응답에 쿠키를 추가해 주는 작업은 express-session에서 처리합니다.
express-session에서 req.session이 존재하면(빈 객체가 아니면) setcookie를 실행해 응답에 쿠키를 넣어주기때문에 passport의 serializeUser에서 req.session에 유저관련 정보를 넣어주면 응답에 쿠키를 추가해줍니다.
5. 응답
여기까지 에러가 발생하지 않았다면 passport.authenticate 미들웨어에서 next()를 호출해 app.post의 콜백으로 넘어가게 됩니다. passport.authenticate의 옵션으로 성공, 실패 시 redirect를 설정할 수도 있습니다.
app.post('/auth/login',
passport.authenticate('local', {
// successRedirect: '/mypage',
failureRedirect: '/login',
}),
(req, res) => {
return res.redirect('/mypage');
}
);
정리
passport의 진행 순서를 정리하면 아래와 같습니다.
authenticate()
authenicate의 콜백 함수, 내부 동작을 코드레벨에서 살펴보겠습니다.
1. authenticate의 callback함수는 어떤 인자를 받는가?
이 authenticate의 콜백 때문에 깃헙을 찾아보게 되었습니다. 공식문서에는 authenticate(strategy, options)에 대한 예시만 나와있습니다. 하지만 제가 듣는 강의에서는 authenticate(strategy, callback)의 형태로 옵션없이 사용했습니다. 여기서 저 authenticate의 콜백 함수가 인자로 무엇을 받는지 알아보기 위해 깃헙을 찾아보게 되었습니다.
먼저 index.d.ts 파일에 override 된 authenticate의 선언부부터 확인해 보겠습니다.
`authenticate(strategy, callback?)`, `authenticate(strategy, options, callback?)`으로 override 된 것을 확인할 수 있습니다. 하지만 제가 궁금한 callback의 인자는 나와있지 않습니다. 그래서 깃헙의 authenticate 정의 부분을 확인해 봤습니다.
authenticate는 passport, name, options, callback총 4개의 인자를 받고 세 번째 자리(options)에 함수가 오면 그 함수를 콜백으로 처리한 뒤 options는 빈 객체로 만듭니다. 즉 3개(passport, name, callback)의 인자와 4개의 인자로 override 되어있습니다. 이 부분에서 위 index.d.ts에서 (strategy, callback?)과 (strategy, options, callback?) 두 가지 경우의 override의 구현을 확인할 수 있습니다.
하지만 여기서 하나의 의문이 더 생깁니다. 첫 번째 인자 passport 뭘까요? 여기서 "typescript에서 함수를 정의할 때 첫번째 인자로 this를 주는 것과 비슷한 게 아닐까?" 하는 합리적인 의심이 생깁니다. authenticate호출도 passport.authenticate로 호출하니 실제로 passport가 this입니다. 확인하기 위해 authenticate.js를 호출하는 곳을 찾아봤습니다. authenticator.js에서 답을 찾을 수 있었습니다.
authenticate.js에서 정의된 authenticate함수를 authenticator의 메서드로 넣어주고 이때 this를 첫 번째 인자(passport)로 넣어줬습니다. 예상이 맞았습니다. 하지만 또 다른 의문이 남습니다. 우리는 passport.authenticate로 호출했습니다. passport.prototype.authenticate이라면 완벽한데 Authenticator라는 게 등장했습니다. 이제 index.js로 갑시다. passport 깃헙에서는 마지막 여정입니다.
Passport가 Authenticator였습니다. 우리가 import 한 passport = require(passport)는 Authenticator의 인스턴스였습니다. 이제 모든 의문이 해결되었습니다. 그림으로 정리해 보겠습니다.
이제 우리는 우리가 사용한 authenticate의 본체를 찾았습니다. 이제 본론으로 들어가 callback의 인자를 살펴보겠습니다. 코드를 통해 확인해도 되지만 주석으로 아주 친절하게 나와있습니다.
결론: callback은 4개의 인자를 받습니다. callback(error, user, info, status)
2. authenticate는 내부적으로 어떻게 작동하나?
authenticate 미들웨어가 실행되면 첫 번째 인자로 받은 strategy name(제 예시 코드에서는 'local')로 등록되어 있는 strategy 불러옵니다.
꺼내온 strategy에 메서드를 장착하는 부분은 196번째 줄 아래에 있습니다.
192번째 줄에서 strategy를 꺼내오는 부분을 살펴보겠습니다. authenticate.js의 passport는 위에서 callback함수의 인자를 알아볼 때 살펴봤듯 Authenticator의 인스턴스입니다.
passport.use를 이용해 등록한 strategy를 꺼내옵니다.
53번째 줄에서 passport-local은 자신의 이름을 'local'로 등록해놨기 때문에 passport.use에서 이름을 따로 등록하지 않아도 됩니다.
마치며
passport를 처음 사용해봐 사용법이 복잡하고 공식문서도 빈약하다는 생각이 들어 깃헙을 읽어봤고 깃헙에 주석이 공식문서보다 자세했고 코드와 파일 구조도 읽기 쉽게 작성되어 있었습니다. passport를 어떻게 사용해야 하는지 알게 되었고 passport-local을 읽으며 직접 strategy를 만드는 방법도 알게 되었습니다. 그 보다 더 좋았던 점은 다른 사람의 코드를 읽어보니 왜 코드를 readable 하게 작성해야 하고 주석도 열심히 달아야 하는지 알게 되었습니다. passport에 대한 지식을 떠나 좋은 경험이었습니다.
'Node.js' 카테고리의 다른 글
[Node] __dirname is not defined in ES module scope (0) | 2022.03.17 |
---|---|
[Web Socket] ws 모듈을 이용한 웹 채팅방 구현(without socket.io) (0) | 2022.03.12 |