passport
는 Node.js를 위한 인증 미들웨어 이다. 전략이라는 명칭으로 다양한 Federated Login
을 도와준다.
passport
는 사용하기엔 편할 수 있지만 사용하기 이전에 몇가지 개념
에 대해서 미리 숙지 하고 있지 않으면 사용하면서도 이해하기 어렵다고 생각한다. 개인적으로 본인은 이해가 어려웠음. 따라서 아래 관련 개념을 간략히 정리 해보자면:
쿠키
는 간단히 이야기하면 브라우저
가 가지고 있는 작은 저장소라고 생각하면 된다.
쿠키
는 서버쪽에서 응답에 Set-Cookie
라는 키값으로 헤더에 실어서 응답에 같이 보내는 정보이다.
서버로 온 요청에 쿠키를 등록해 응답에 되돌려보낸 후 이후 요청부터는 브라우저로부터 매 요청시마다 쿠키를 같이 보내준다. 이 쿠키로 인해 서버는 이 요청이 어떤 곳에서부터 온 요청인지 파악할 수 있게 된다. (이말은 HTTP의 stateless, connectionless의 성질을 알면 이해가 된다.)
❗따라서 쿠키는 사용자의 치명적인 정보가 절대로 담는 용도로 사용되어선 안된다.
❗쿠키자체가 설사 아무 정보를 담고 있지 않다 할지라도 쿠키자체만으로도 사용자의 동의없이 탈취되어선 안된다.
❗따라서 운영환경에서는 쿠키의 여러 옵션(httpOnly, secure 등)을 통해 보안에 유의해야한다.
ℹ️ 만약 프론트
와 백엔드
의 서버가 서로 다를 경우
쿠키를 전달하기 위해서는 각 프론트 백엔드 서버 모두 credentials
값이 true
되어야한다.
세션
은 서버측에서 저장하고 있는 데이터이다. 보통 아주 아주 아주 기본적인 사용자의 대한 정보를 담는다. 이메일
이나 사용자 id
가 아닌 사용자를 식별할 수 있는 유니크한 값.
쿠키의 세션ID ➡️ 세션 값 ➡️ DB의 사용자 정보
위와 같은 방식으로 서버는 매 요청으로부터 사용자를 식별할 수 있다.
OAuth
는 페이스북, 트위터, 네이버, 카카오톡 등과 같은 (provider
)가 제공하는 API를 사용할 수 있는 토큰발행을 받는 목적으로 한다.
이런 토큰들은 provider
가 제공하는 resource
를 사용할 수 있는 접근 권한도 포함되어 있다. 따라서 이런 토큰을 발행받기 위해서는 실제 소유자인 resource owner
에게 승인을 받는것이 필수이다. 승인을 받게 되면 provider
는 client(내 서버)
를 해당 권한을 사용할 수 있게 토큰을 발행해준다.
토큰을 발행받은 client
는 위임된 권한 내에서 provider
가 제공하는 resource
에 접근이 가능하다.
ℹ️ OAuth2.0은 OAuth1.0과 달리 토큰의 life-time을 지정할 수 있도록 함.
passport를 이해하는데 위에 개념이 어느정도 있다면 매우 쉽게 사용할 수 있을 것이라고 생각함.
passport는 크게 초기화
, 전략구현
, 라우트
파트로 나누어 구현하면 된다.
✅ passport는 세션
을 사용하기 때문에 세션
의 미들웨어 아래에 초기화를 해줘야한다.
app.use(
session(
process.env.NODE_ENV === 'production'
? sesseionProdOption
: sesseionDevOption
)
)
app.use(passport.initialize())
app.use(passport.session())
Top-level 폴더에 passport를 만들고 그 아래 전략들을 구현하여 모듈화하여 구현하면 관리하기 편하다.
✅ serializeUser
, deserializeUser
는 passport
폴더내 index.js에서 구현하고 이것을 app에 호출해준다.
// passport/index.js
module.exports = () => {
passport.serializeUser((user, done) => {
return done(null, user.id)
})
passport.deserializeUser(async (id, done) => {
try {
console.log('🪀🪀🪀 deserializeUser')
const user = await User.findOne({
where: { id }
})
return done(null, user)
} catch (error) {
if (error) {
logger.error(error)
return done(error, false)
}
}
})
local()
github()
google()
kakao()
facebook()
}
✅ 전략들은 아래그림과 같은 구조로 전략들을 각각 구현하고 그것들을 index에서 호출한다.
라우트 파트는 로그인버튼 클릭시 호출되는 라우트와 provider
로부터 받는 callback을 위한 라우트로 각 전략별로 2개씩이 필요하다.
✅ 로컬로그인으로 흐름을 정리하자면 아래와 같다.
auth/login
이라고 할 때 이 라우트의 미들웨어로 passport
를 사용하고 이 passport
의 전략으로 흐름 진행됨해당 사용자가 정상적으로 있는경우
로컬전략의 로직 구현부에서 done(null, user)
로 콜백이 발생한다.local
전략의 경우에는 req.login() 함수를 호출해야 session에 등록됨(serializeUser 호출됨😀😀)serializeUser(user, done)
에 user를 콜백하도록 약속되어있음. 이때 이 user
의 식별자값을 호출 done(null, user.id)
(여기선 id인데 어떤식별자값이던 상관 없음.)user.id로 할 경우
세션
에id
값이passport: {user: 아이디값}
의 형태로 등록됨.
serializeUser는 즉 로그인에 성공한 user의 식별자를 세션에 등록하는 역할을 하는 것이다.
따라서 로그인이 성공하면 딱 한번호출되는 것이 있다면 이 세션에 등록하는 과정인 serializeUser
이다.
deserializeUser
이 호출된다.deserializeUser
는 app.use(passport.session())
이 미들웨어가 호출하는 것이므로 이것에 아래 있는 라우팅경로에 호출들은 무조건 다 호출된다.
이렇게 호출된 deserializeUser는 세션에 등록된 id
값을 통해 user를 DB에서 조회할 수 있다. user가 조회되면 done(null, user)
의 콜백을 통해 전달해주고 전달된 user는 req.user
에 등록된다. req.user
는 passport에서만 사용가능하다.
따라서 passport를 사용하지 않으면 req.user라는 객체는 없다. (따로 만들어 넣지 않는 이상)