express-session, passport.initialize, passport.session 미들웨어 간의 상호작용 in Passport Local Authentication

HAN·2021년 5월 31일
0

최근에 네이티브 앱에서 세션을 기반으로한 인증을 구현할 필요가 있었는데 브라우저와 달리 메뉴얼로 세션을 핸들링해야 했다. 서버에서 받은 쿠키를 저장하고 다시 이를 서버로 보내주는 로직이었는데 하다보니 좀 더 구체적인 모습으로 세션이 어떻게 핸들링 되는지 알 필요가 있었다. 그래서 경우의 수 별로 각각 flow를 찾아 정리해 보았다. 기존 많은 설명들이 serealizeUser와 deserializeUser이 간략하게 무엇을 하는지만 설명했었는데. 대체 이들 함수 실행 전후로 무슨 일이 일어나는지 알고 싶었다. 이 글은 각 상황에서 아래 3개의 미들웨어가 어떻게 상호작용하는지에 대한 설명이다.(틀린 부분이 있다면 지적 부탁드립니다.)

express-session : 세션 복원 및 쿠키 발급/조회

sid => req.session

passport.initialize : 인증유저 초기화

req.session.passport.user => user.id

passport.session : 유저객체 복원

user.id => req.user

첫 방문

  • 서버의 express-session 미들웨어는 쿠키에 아무것도 없는 걸 확인하고 세션을 형성한다.
  • passport.initialize는 req.session이 있는지 확인하고, req.session.passport를 추가한다.
  • => 응답헤더에 set-cookie : { sid : 해시된 세션id } 를 삽입하고 해시세션id => 세션 id => 세션id : 세션 객체{~~}를 세션 저장소에 저장한다.
  • 유저는 기존에 서버로 부터 응답받은 sid를 로컬 스토리지에 저장했다가, 다음번에 같은 도메인으로 요청할 경우 요청 헤더에 해당 쿠키를 삽입해준다.

첫 로그인

  • 유저가 id, pwd를 입력해서 /login 경로로 post 요청을 할 때, 요청 헤더에는 기존에 받았던 sid가 삽입되어 전달된다.

  • express-session 미들웨어는 요청 헤더의 쿠키를 매번 확인하는데 이번에는 sid를 확인할 수 있다.

  • 이를 세션저장소(mongostore or memoryStore)에서 조회한 뒤 해당 sid(해시값)와 일치하는 세션id(실제값)를 찾아 해당 세션객체를 req.session에 세팅한다.

  • 이후 passport.authenticate() 의 로직을 통과한 인증된 user 객체가 req.login() => serealizeUser에 의해 req.session에다가 passport : { user : user.id } } 를 추가한다.(아래 사진 참고)

  • 마지막으로, 전체 유저 오브젝트를 req.user에 붙인다.

  • 이후 서버 상에서 로그인한 유저의 user 객체를 프론트로 넘겨준다.

  • 자 이때, 우리가 위에서 붙였던 req.session.passport.user = user.id 는 세션 저장소에 저장된다. req.session.save() => 자동 처리된다.

  • 서버의 응답에는 sid가 여전히 전달된다.

*세션 저장소에 저장된 세션 객체의 모습은 아래와 같다.(json)

로그인 이후의 요청

  • 쿠키가 만료되기 전까지 모든 요청에는 sid가 지속적으로 전달된다.

  • express-session 미들웨어는 요청 헤더의 sid 쿠키를 확인하고 세션 저장소에서 꺼내 세션 객체를 복원시킨다.

  • 이때, req.session의 모습은 이전에 세션저장소에 저장했던 객체의 모습 그대로다.

  • 만약 쿠키가 만료됐다면, 해당 sid는 기존의 세션 객체를 복원하지 못할 것이다.

  • passport.initialize() 미들웨어는 req.session에서 req.session.passport.user 가 있는지 확인한다.

  • 그리고 아래 passport.session()미들웨어에 user.id를 넘긴다.

  • 저장된 세션 객체 내에 passport.user는, 해당 sid에 대응하는 인증된 유저를 의미한다.

  • 즉, 인증된 유저가 없다면 세션이 만료 되었거나 뭔가 문제가 있는 것. 다시 인증을 거쳐야 한다.

  • passport.session()의 실행과정에서 아래의 deserializeUser가 실행된다.

  • 이 내부에서 해당 user.id를 deserializeUser 의 2번째 인자로 넘겨준다.

  • deserializeUser는 db에서 user.id를 조회하여 user 객체 전체를 복원한다.

  • 이후 done(null, user)를 하면, req.user가 생긴다.

쿠키 만료 혹은 로그아웃 후 요청

  • express-session은 요청 헤더의 쿠키를 확인하고 만료된 쿠키임을 감지한다. (로그아웃 이후라면 쿠키가 없는 것을 확인한다.)
  • express-session은 새로운 세션(쿠키)를 생성하여 세션저장소에 저장하고 sid를 응답 헤더에 담는다.
  • passport.initialize는 세션이 만료되었으므로 기존 요청으로부터 req.session.passport.user를 확인할 수 없다.
  • 로그인 페이지로 돌려서 재 인증을 유도하든지, 아니면 새로운 로그인이 필요한 페이지라고 경고를 띄운다.

참고(사실상 아래 두 글을 상호 참조하여 연결고리를 이을 수 있었다..)

미들웨어의 순서가 포인트. 인증 요청이 언제 시작되고 어떻게 진행되는지 아는게 중요합니다.

  • 세션 미들웨어는 세션을 생성한다.(세션 스토어의 데이터를 사용)
  • passport.initialize는 request 객체에 _passport 객체를 할당하고, request 객체에 session 객체가 있는지, 그리고 있다면 그 하위에 passport 필드가 있는지(없다면 생성한다.) 확인한 뒤, _passport에 저장했던 request 객체 하위에도 session 객체를 할당한다. 그렇게 되면 아래와 같은 모습일 것이다.

    따라서, 세션 필드(req._passport.session)는 req.session.passport에 할당된 객체를 참조하게 된다.
  • passport.session은 req._passport.session 안에 있는 user필드를 찾는다. 그리고 만약 찾았다면, deserializeUser 함수를 실행하면서 deserializeUser 함수에 user를 인자로 전달한다. deserializeUser 함수는 req._passport.session.user를 request 객체의 user 필드에 할당한다.(req._passport.session.user가 있는 경우에만)

    만약 위와같이 serializeUser에서 user를 stringify를 했다면, deserializeUser 에서는 parse해주어야 한다.

  • deserializeUser 함수는 패스포트를 설치할 때 너의 콜백을 _deserializeUsers 함수 스택에 놓기 위해 처음 호출된다. 두번째로 실행되는 건, passport.session 미들웨어에서 request 객체에 user 필드를 할당하기 위해 호출된다. 우리가 deserializeUser 안에 정의한 콜백은 request 객체에 user필드가 할당되기 전에 실행된다.

  • serializeUser 역시 패스포트가 설치될 때 처음으로 호출된다. 하지만 이는 user 객체를 직렬화 해서 세션에 저장하기 위해서 이용될 뿐이다. 두번째로 실행되는 건, Passport에 의해 할당된 login / logIn (별칭) 메서드(역자 : req.login()을 말하는 듯)에서 호출되어 세션 내에 사용자 객체를 저장하는 데 사용됩니다. serializeUser 함수는 또한 _serializers 스택 안에 이미 내장된 함수( 우리가 패스포트를 설치할 때 할당 되어진 것)가 있는지도 확인하고 실행합니다.

  • 그러면 user 객체가 할당되거나, user.id가 req._passport.session.user에 할당됩니다. session 필드가 직접적으로 req.session.passport 필드를 참조한다는 잊으면 안됩니다. 그럼으로써, user가 세션 내에 저장됩니다.(왜냐하면 req._passport.session은 req.session.passport를 참조하고, req._passport.session은 매 요청마다 passport.initialize 미들웨어에 의해 수정되기 때문입니다. 요청이 끝나면, req.session 데이터는 세션 스토어에 저장될 것입니다.

  • 성공적으로 인증을 마친 뒤, 그 다음 요청이 시작될 때 어떤 일이 일어날까요?

  • 세션 미들웨어는 이미 유저 데이터가 저장된 세션 스토어로부터 세션을 얻는다.

  • passport.initialize는 어디에? 세션이 있는지 확인하고, req.session.passport를 req_passport.session에 할당한다.

  • passport.session은 req._passport.session.user 를 확인하고 deserialize 한다. 이 단계에서, 우리는 req.user를 얻고, req.isAuthenticated는 true를 리턴한다.(req._passport.session.user가 있다면)

profile
즐거운 배움이 되길

0개의 댓글