Passport.js + JWT Token 로그인 인증 구현

미루고 미뤘던 블로그 글쓰기를 시작하려고 합니다. 제 일생 블로그 첫 글이 될 꺼 같은데요. 미숙한 글쓰기 실력과 더불어 흐접한 개발 실력에 맞물려 언제 이 글이 포스팅 될 지는 모르겠지만 열심히 작성해보려고 합니다. 

개인적으로 진행하고 있는 Node.js 사이드 프로젝트를 진행하고 있던하던 중 로그인 인증 구현이 필요하게 되었습니다. Express 프레임워크에서 써드 파티 미들웨어로 추천하는 Passport.js를 통해 로그인 인증을 만들고 인증 방법은 기존에 구현해 본 전통적인 세션-쿠키 인증방식이 아닌 확장성이 있는 JWT Token 인증 방식으로 구현하려고 합니다. (토큰 인증방식과 JWT Token에 대한 완벽한 글을 찾았습니다. 토큰 인증 방식에 생소한 분들은 참고하시면 좋을 것 같습니다.)

이메일, 비밀번호 두 개의 정보를 입력받는 Local 인증으로 구현하며 인증 성공시 JWT Token를 발급하는 사용자 인증과 JWT token의 유효성을 검사하는 Token 인증을 구현하려고 합니다.

1. Passport.js

passort.js에 대해 알아보겠습니다. Express 공식 웹사이트에서 추천하고 있으며 Node 사용자가 대다수 사용하고 있는 인증 package 입니다. 다음은 공식 웹사이트에서 캡쳐한 화면인데요. passport.authenticate(‘local’) 이 코드 부분이 전체적인 passport.js의 컨셉을 말해주고 있습니다. 라우터의 미들웨어 형태로 인증하게 될 것이며 인증 방식은 ‘local’이 될 것입니다. passport.js 는 ‘local’ 인증 방식 외 다양한 인증 방식(‘auth’, ‘jwt’, ‘social’)을 미리 구현하고 있기에 알맞은 인증 방식 (Strategy)를 선택하여 사용하기만 하면 됩니다. 초보인 저는 디자인 패턴 책에서만 구경하던 Strategy Pattern 를 볼 수 있었습니다. (Strategy 패턴 참고 : https://en.wikipedia.org/wiki/Strategy_pattern)

passport.js의 컨셉을 알 수 있다!!!
Passport.js는 많은 Strategy를 제공한다! 골라서 쓰기만 하면된다

 passport.js Strategy에는 Local 인증 방식(id, password 전통적인 로그인 방식)부터 oauth, openID, 소셜 인증까지 다양한 Strategy가 제공됩니다. 이 중 저에게 필요한 local, jwt 를 선택해서 구현해 보겠습니다. 차후 소셜 부분도 구현할 예정인데 현재는 로컬 인증 부분만 구현하고 후에 포스팅 해보도록 하겠습니다. 

2. Install

node가 설치 되어 있다고 가정하고 진행하겠습니다. passort 메인 패키지와 선택한 local, jwt strategy를 설치하고 JWT token를 만들기 위한 jwtwebtoken 또한 설치해 줍시다.  

npm install passport // main package
npm install passport-local // 1. local strategy
npm install passport-jwt // 2. jwt strategy
npm install jsonwebtoken

3. Local 인증 구현

passport를 사용하기 위한 작업을 세분화해보았습니다. 아래 3가지 작업을 거치면 간단히 인증을 구현할 수 있습니다.

  1. passport 등록
  2. Strategy 정의
  3. Local 인증 구현
  4. JWT 인증 구현

3-1. passport 등록

우선 , app.js 파일에 passport 등록을 하겠습니다. passport.initaialize()가 등록 부분이 되겠는데요. 패키지 body-parser가 의존 관계에 있습니다. body-parser, cookie, session(필요하다면) 설정이 끝난 후 passport를 등록해야 합니다. Express 4.x 이상을 사용하고 계신 분들은  body-parser가 내장되어 있어서 express.json() 부분이 body-parser 설정을 대신합니다.

 app.js
const passport = require('passport');

const app = express();

// view engine setup
app.set('views', path.join(__dirname, 'public'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
//app.use(session({secret: 'secret'})); // session 방식 구현시 필요
app.use(passport.initialize());
//app.use(passport.session()); // session 방식 구현 시 필요

3-2. Strategy 정의

app.js 성공적으로 등록하였으면 Strategy 를 입맛에 맞게 정의해 봅시다. LocalStrategy의 역할은 request 에 넘겨져오는 form-data ({’email’: email, ‘password’: password}) 와 Local DB에 저장되어있는 User와 비교하는 것입니다. JWTStrategy 역할은 단순이 JWT 토큰을 읽어 해당 사용자를 인증합니다. Locarlstrategy는 로그인, JWT Strategy는 API 접근 인증이 될것입니다.

// config/passport.js

const passport = require('passport');
const passportJWT = require('passport-jwt');
const JWTStrategy   = passportJWT.Strategy;
const ExtractJWT = passportJWT.ExtractJwt;
const LocalStrategy = require('passport-local').Strategy;
let UserModel = require('../models').User;
require('dotenv').config();

module.exports = () => {
    // Local Strategy
    passport.use(new LocalStrategy({
            usernameField: 'email',
            passwordField: 'password'
        },
        function (email, password, done) {
            // 이 부분에선 저장되어 있는 User를 비교하면 된다. 
            return UserModel.findOne({where: {email: email, password: password}})
                .then(user => {
                    if (!user) {
                        return done(null, false, {message: 'Incorrect email or password.'});
                    }
                    return done(null, user, {message: 'Logged In Successfully'});
                })
                .catch(err => done(err));
        }
    ));
    
    //JWT Strategy
    passport.use(new JWTStrategy({
            jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
            secretOrKey   : process.env.JWT_SECRET
        },
        function (jwtPayload, done) {
            return UserModel.findOneById(jwtPayload.id)
                .then(user => {
                    return done(null, user);
                })
                .catch(err => {
                    return done(err);
                });
        }
    ));
}

설정 역할을 하는 config/passport.js 를 app.js 에 아래와 같이 등록해줍니다. (저는 config 디렉토리 아래에 두었는데 프로젝트 구조에 맞게 설정을 사용하시면 되겠습니다.)

// app.js
const passport = require('passport');
const passportConfig = require('./config/passport')
const app = express();

// view engine setup
app.set('views', path.join(__dirname, 'public'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(passport.initialize());
passportConfig();

3-3. Local 인증 구현

Local 인증을 통해 JWT Token 를 발급하는 API를 작성합니다. API endpoint는 /auth/tokens 로 생성하고 Method는 Post로 설정해줍니다. 

// api.js
// route 등록
const AuthTokenController = require('../controllers/api/v1/AuthTokenController');

route.post('/auth/tokens', AuthTokenController.create);

passport.js Documents 에 나온 코드처럼 passport.authenticate(‘local’) 만 작성해주면 Localstreatgy 에 따라 인증 프로세스가 적용되며며 login 에 성공할 경우 다음과 같이 jwtwebtoken 패키지를 이용하여 JWT token를 발급합니다. JWT token을 만드는데 사용하는 JWT SECRET KEY는 .env 파일을 사용하여 노출을 방지하였습니다. 

// controller/AuthTokenController.js

const jwt = require('jsonwebtoken');
const passport = require('passport');
require('dotenv').config();

exports.create = function (req, res) {
    passport.authenticate('local', {session: false}, (err, user) => {
        if (err || !user) {
            return res.status(400).json({
                message: 'Something is not right',
                user   : user
            });
        }
        req.login(user, {session: false}, (err) => {
            if (err) {
                res.send(err);
            }
            // jwt.sign('token내용', 'JWT secretkey')
            const token = jwt.sign(user.toJSON(), process.env.JWT_SECRET);
            return res.json({user, token});
        });
    })(req, res);
};

Local 인증 방식 구현이 모두 끝났습니다. 인증 API (/auth/tokens) 를 호출하여 결과를 확인해보겠습니다. DB, 파일 등 기존 저장소에 사용자가 존재하는 경우에는 아래와 같이 user 정보와 JWT 토큰을 정상적으로 가져올 수 있습니다. 클라이언트는 JWT Token 를 로컬 스토리지에 저장하게되며 이를 헤더에 포함시키는 형태로 API 호출을 하게 됩니다. 

3-4. JWT 인증 구현

자 마지막 과정이 남았습니다. 미리 구현한 JWT Strategy (JWT 토큰이 있는지 , 유효한 토큰인지 확인)를 가지고 Router에 미들웨어 형태로 사용해줍시다. local 인증을 구현한 것과 동일하게 jwt 를 사용해주기만 하면됩니다.

// api.js
const UserController = require('../controllers/api/v1/UserController');

// passport를 미들웨어로 장착해주기만 하면 된다.
router.get('/users', passport.authenticate('jwt', {session: false}), UserController.index);
해당 API 호출 시 – JWT Token 이  없는 경우 다음과 같이 401 error가 발생한다.
JWT Token 으로 정상적으로 API 호출한 경우

마무리

Node 환경에서 passport 패키지를 이용해 인증을 구현해 보았습니다. passport에는 이 포스팅에서 구현한 Local, JWT 인증 외 oauth, 소셜 로그인 등 다양한 인증을 구현할 수 있으며 Session, Cookie 등 인증 보관에 대한 부분도 지원하고 있으니 Node 환경에서 인증을 구현하고자 하시는 분들은 passport.js로 손쉽게 빠르게 구현해 보셨으면 합니다. 첫 블로그 글인만큼 부족한 부분들이 많은데 앞으로 재밌고 유익한 포스팅을 할 수 있도록 노력하겠습니다. 다양한 피드백 언제든지 환영합니다. 읽어주셔서 감사합니다.

Passport.js + JWT Token 로그인 인증 구현”의 7개의 댓글

  1. 오현아 댓글달기

    오타가 있는 것 같아서 댓글 남깁니다.
    npm install jwtwebtoken 가 ‘jsonwebtoken’ 모듈의 오타로 보이는데 맞나요!

    이와 별개로 글 잘 읽고 갑니다. 정리가 깔끔하네요!

  2. 정병태 댓글달기

    친절한 설명 많은 도움이 됩니다.
    github에서 구현하신 코드를 다운받을 수 있나요?

답글 남기기

이메일 주소는 공개되지 않습니다.