[Node.js/Express] passport.js로 SNS 로그인 구현(카카오톡, 트위터, 페이스북)

유진·2021년 1월 18일
3
post-thumbnail

1. passport-kakao로 카카오톡 로그인 구현하기

1) SNS 로그인을 할때는, auth/kakao 라우터로 사용자의 로그인 요청을 받고, 카카오 서버에 로그인 요청을 보낸다. auth/kakao/callback으로 카카오 서버로부터 로그인 정보를 받고, 사용자에게 로그인 여부를 알려준다.

passport-local과는 다르게, passport.authenticate('kakao')에서는 두번째 인자로 콜백 함수를 넣지 않는다. (passport-kakao는 req.login을 자체적으로 호출하므로 따로 적을 필요가 없기 때문이다.) 대신, 로그인에 실패했을 때 리다이렉트할 url을 failureRedirect 속성에 적는다. 로그인에 성공했을 경우에는 다음 라우터 함수를 실행하도록 한다.

// routes/auth.js

router.get('/kakao', passport.authenticate('kakao')); // 요청이 들어온다.

router.get('/kakao/callback', passport.authenticate('kakao', {
    failureRedirect: '/auth/login',  // 로그인에 실패했을 경우 해당 라우터로 이동한다
}), (req, res) => { // 로그인에 성공했을 경우, 다음 라우터가 실행된다
    res.redirect(`/${req.user.user_id}`);
});

2) (패스포트 내부에서) callbackURL(=redirect uri)과 clientID를 카카오 서버에 전송하면, 카카오 서버는 인증을 진행하고, callbackURL로 액세스 토큰, 리프레시 토큰, 사용자 정보(profile 객체)를 전송한다. passport는 이걸 캐치해서 verify 콜백함수에 넘겨준다.

// passport/kakaoStrategy.js
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy;

const User = require('../models/user');

module.exports = () => {
    passport.use(new KakaoStrategy({ // 첫번째 인자에는 인증에 필요한 값들을 적어넣는다. (카카오 서버에 전송하여 인증받을 내용.)
        callbackURL: '/auth/kakao/callback', // 카카오 디벨로퍼에 적어놓은 redirect uri와 같아야 한다
        clientID: process.env.KAKAO_ID, // 내 앱의 REST API
    }, 
    async(accessToken, refreshToken, profile, done) => { // 사용자가 유효한지 확인하는 verify 콜백함수 
        // accessToken과 refreshToken: 인증을 유지시켜주는 토큰
        // profile: 사용자 정보 객체
        // done(error, user): passport-twitter가 자체적으로 req.login와 serializeUser 호출하여 req.session에 사용자 아이디를 저장한다
        
        const exUser = await User.findOne({where: {sns_id: profile.id, provider:'kakao'}});
        done(null, exUser);
            
    }));
};

3) verify 콜백함수에서 done() 함수를 호출한다. done() 함수를 호출하면 로그인 전략을 마친다. 로그인 성공 여부에 따라 failureRedirect로 리다이렉트되거나 (실패), 다음 라우터 함수가 실행된다 (성공).

// passport/kakaoStrategy.js
// 중략...
module.exports = () => {
    passport.use(new KakaoStrategy({
        callbackURL: '/auth/kakao/callback',
        clientID: process.env.KAKAO_ID, 
    }, 
    async(accessToken, refreshToken, profile, done) => {  // verify 콜백함수
        try {
            const exUser = await User.findOne({where: {sns_id: profile.id, provider:'kakao'}});
            done(null, exUser); // 사용자 정보를 넘긴다
        } catch (err){
            done(err); // 에러가 발생하면 에러 정보를 넘긴다
        }
    }));
};
// routes/auth.js

router.get('/kakao', passport.authenticate('kakao'));

router.get('/kakao/callback', passport.authenticate('kakao', {
    failureRedirect: '/auth/login',
}), (req, res) => {  // kakaoStrategy에서 done함수가 호출되면 다음 라우터 함수가 실행된다
    res.redirect(`/${req.user.user_id}`);
});

2. passport-twitter로 트위터 로그인 구현하기

흐름은 passport-kakao와 똑같다. developer.twitter.com에서 프로젝트와 앱을 만들고 초기 설정을 해주고, 코드는 TwitterStrategy에서 필요한 변수에 맞춰 작성하면 된다.

다만, 이메일을 수집할 때는 Strategy에 includeEmail 속성을 true로 설정해야 한다.

// routes/auth.js

router.get('/twitter', passport.authenticate('twitter'));

router.get('/twitter/callback', passport.authenticate('twitter', {
    failureRedirect: '/auth/login', // 로그인 실패 시 여기로 리다이렉트
}), (req, res) => {
    res.redirect(`/${req.user.user_id}`);  // 로그인 성공 시
});
// passport/twitterStrategy.js
const passport = require('passport');
const TwitterStrategy = require('passport-twitter').Strategy;

const User = require('../models/user');

module.exports = () => {
    passport.use(new TwitterStrategy({
        // api key와 api secret key는 노출되면 안되기 때문에 루트 디렉터리의 .env 파일에 넣어두고 사용한다.
        consumerKey: process.env.TWITTER_API_KEY,
        consumerSecret: process.env.TWITTER_SECRET_KEY,
        // callbackURL은 Twitter Developer에서 설정한 callback url과 같아야 한다
        callbackURL: '/auth/twitter/callback',
        includeEmail: true  // 사용자 정보 객체에 이메일이 포함되도록 하는 옵션
    }, async(token, tokenSecret, profile, cb) => {
        // token과 tokenSecret: 트위터 authentication API
        // profile: 사용자 정보가 들어있는 객체
        // cb(error, user): passport-twitter가 자체적으로 req.login와 serializeUser 호출하여 req.session에 사용자 아이디를 저장한다
        try{
            const exUser = await User.findOne({where: {email: profile.emails[0].value}});
            if (exUser) {
                cb(null, exUser);
            } else {
                const newUser = await User.create({
                    email: profile.emails[0].value,
                    nickname: profile.displayName,
                    sns_id: profile.id,
                    provider: 'twitter',
                });
                cb(null, newUser);
            }
        } catch (err) {
            console.error(err);
            cb(err);
        }
    }));
};

3. passport-facebook로 페이스북 로그인 구현하기

마찬가지로 흐름은 똑같다. developers.facebook.com에서 앱을 만들고 초기설정을 해준다.

email 권한을 가져오기 위해서는 passport.authenticate()의 두번째 인자로 {scope: 'email'}을 추가해주어야 하며, FacebookStrategy 생성자에 profileFields: ['id', 'displayName', 'email'] 속성으로 어떤 사용자 정보를 가져올지 지정해야 한다.

// routes/auth.js

router.get('/facebook', passport.authenticate('facebook', {scope: 'email'}));  // 페이스북 서버에 이메일 권한 요청

router.get('/facebook/callback', passport.authenticate('facebook', {
    failureRedirect: '/auth/login',
}), (req, res) => {
    res.redirect(`/${req.user.user_id}`);
});
// passport/facebookStrategy.js

const passport = require('passport');
const FacebookStrategy = require('passport-facebook').Strategy;

const User = require('../models/user');

module.exports = () => {
    passport.use(new FacebookStrategy({
        clientID: process.env.FACEBOOK_ID,
        clientSecret: process.env.FACEBOOK_SECRET_CODE,
        callbackURL: '/auth/facebook/callback',
        profileFields: ['id', 'displayName', 'email']  //id, 이름, email을 가져올 것이라고 명시
    }, async(accessToken, refreshToken, profile, cb) => {
        try{
            const exUser = await User.findOne({where: {email: profile.emails[0].value, provider: 'facebook'}});
            if (exUser) {
                cb(null, exUser);
            } else {
                const newUser = await User.create({
                    email: profile.emails[0].value,
                    nickname: profile.displayName,
                    sns_id: profile.id,
                    provider: 'facebook',
                });
                cb(null, newUser);
            }
        } catch (err) {
            console.error(err);
            cb(err);
        }
    }));
};

4. 참고

profile
제가 또 기가막힌 한 줌의 트러플 소금 같은 존재그등요

0개의 댓글