이 포스트는 Passport.js Docs을 기반으로 작성되었습니다.
Passport.js는 node.js의 인증용(authentication) 미들웨어입니다. Passport는 인증이라는 한가지 목적만을 위해서 설계되었습니다.
$ git clone https://github.com/passport/todos-express-starter.git username-password-tutorial
우선 Passport에서 제공하는 스타터팩을 클론하여 로컬에 저장합니다.
Passport 스타터팩은 템플릿 엔진으로 ejs를 사용하며 데이터베이스로 SQLite를 사용합니다.
$ npm i
$ npm start
이후 dependencies를 설치한 후 서버를 실행시키고 http://localhost:3000
에 접속합니다.
일반적인 경우라면 위 화면과 같은 페이지가 렌더링 될 것입니다.
사용자가 username과 password를 통한 인증 과정을 거칠 페이지를 라우팅해줍니다.
routes
디렉토리에 auth.js
파일명의 라우터 파일을 생성해줍니다.
//.../routes/auth.js
const express = require('express');
const router = express.Router();
router.get('/login', function(req, res, next) {
res.render('login');
});
module.exports = router;
이후 해당 라우터를 app.js
에 import하여 라우팅합니다.
app.use("/", authRouter);
이제 /login
경로에 대한 라우터가 설정되었습니다.
하지만 현재 화면에서는 사용자가 로그인 인증 과정을 거칠 방벙이 없으므로 스타터팩에서 제공하는 로그인 페이지를 렌더링하도록 구현합니다.
/views/login.ejs
템플릿을 다음과 같이 업데이트 합니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Express • TodoMVC</title>
<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/index.css">
<link rel="stylesheet" href="/css/login.css">
</head>
<body>
<section class="prompt">
<h3>todos</h3>
<h1>Sign in</h1>
<form action="/login/password" method="post">
<section>
<label for="username">Username</label>
<input id="username" name="username" type="text" autocomplete="username" required autofocus>
</section>
<section>
<label for="current-password">Password</label>
<input id="current-password" name="password" type="password" autocomplete="current-password" required>
</section>
<button type="submit">Sign in</button>
</form>
<hr>
<p class="help">Don't have an account? <a href="/signup">Sign up</a></p>
</section>
<footer class="info">
<p>Created by <a href="https://www.jaredhanson.me">Jared Hanson</a></p>
<p>Part of <a href="https://todomvc.com">TodoMVC</a></p>
<p>Authentication powered by <a href="https://www.passportjs.org">Passport</a></p>
</footer>
</body>
</html>
이후 Login 라우터에서 해당 템플릿을 렌더링 하도록 구현합니다.
router.get("/login", (req, res, next) => {
res.render("login");
});
템플릿 엔진의 세팅같은 경우에는 스타터팩에서 기본적으로 설정되어있습니다.
이제 해당 라우터에서 템플릿이 정상적으로 렌더링 되는걸 확인할 수 있습니다.
이제 사용자가 입력한 데이터의 유효성을 확인하는 과정을 구현할 것입니다.
다음과 같은 2개의 패키지를 설치합니다.
$ npm install passport
$ npm install passport-local
여기서 passport-local
은 Passport에서 전략(Strategy) 이라고 불리우는데 이는 아이디와 비밀번호를 검증하는 인증 매커니즘을 지정합니다.
LocalStrategy
는 id와 password를 이용한 전통적인 방법의 전략입니다.
이후auth.js
를 다음과 같이 작성하여 미들웨어를 import합니다.
const router = require("express").Router();
const passport = require("passport");
const LocalStrategy = require("passport-local");
const crypto = require("crypto");
const db = require("../db");
router.get("/login", (req, res, next) => {
res.render("login");
});
module.exports = router;
이때 crypto
는 암호화를 위한 미들웨어이며 db
는 스타터팩에서 제공되는 sqlite 형식의 데이터베이스입니다.
이후 사용자의 입력값을 검증하는 로직을 구현합니다.
const router = require("express").Router();
const passport = require("passport");
const LocalStrategy = require("passport-local");
const crypto = require("crypto");
const db = require("../db");
router.get("/login", (req, res, next) => {
res.render("login");
});
passport.use(
new LocalStrategy(function verify(username, password, cb) {
db.get("SELECT * FROM users WHERE username = ?", [username], function (err, row) {
if (err) {
return cb(err);
}
if (!row) {
return cb(null, false, { message: "Incorrect username or password." });
}
crypto.pbkdf2(password, row.salt, 310000, 32, "sha256", function (err, hashedPassword) {
if (err) {
return cb(err);
}
if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) {
return cb(null, false, { message: "Incorrect username or password." });
}
return cb(null, row);
});
});
})
);
module.exports = router;
passport.use()
를 사용하면 전략을 구성하게 됩니다. passport.use()
의 인자로는 사용할 passport의 전략(Strategy)을 전략의은 인자로서 콜백함수를 입력받습니다.
입력받는 콜백함수는 function(username, password, done or cb)
의 형식으로 작성됩니다.
username, password
는 입력된 값들을 의미하며 done
은 검증용 콜백함수로서 첫번째 인자로 에러, 두번째 인자로 유저 데이터를 인자로 가집니다.
예를 들어서 유효한 인증으로서 검증이 완료되었을때는 다음과 같이 실행합니다.
return done(null, user);
이때 첫번째 인자인 null
은 에러가 발생하지 않았음을 의미하고 두번째 인자인 user
는 유저 데이터를 의미합니다.
return done(null, false);
return done(null, false, { message: 'Incorrect username or password.' });
이에 따라 만약 유효하지 않은 유저일 경우에 위와 같이 두번째 인자값을 false
로 보내어 검증이 되지 않았음을 전달 할 수 있습니다.
또한 세번째 인자에 검증 과정에서의 유효성 판단 결과를 message
프로퍼티를 가진 객체로 전달 할 수도 있습니다.
return done(err);
에러 핸들링의 경우 위와 같이 처리합니다.
이제 다시 한번 전략 코드를 살펴봅시다.
const router = require("express").Router();
const passport = require("passport");
const LocalStrategy = require("passport-local");
const crypto = require("crypto");
const db = require("../db");
router.get("/login", (req, res, next) => {
res.render("login");
});
passport.use(
new LocalStrategy(function verify(username, password, done) {
db.get("SELECT * FROM users WHERE username = ?", [username], function (err, row) {
if (!row) {
return done(null, false, { message: "Incorrect username or password." });
}
crypto.pbkdf2(password, row.salt, 310000, 32, "sha256", function (err, hashedPassword) {
if (err) {
return done(err);
}
if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) {
return done(null, false, { message: "Incorrect username or password." });
}
return done(null, row);
});
});
})
);
module.exports = router;
위의 코드에서 데이터베이스와 암호화 관련된 부분은 제외하고 유효성 검증 로직만 살펴보겠습니다.
전략의 콜백 함수인 verify
는 유효성을 검증할 username, password
을 인자로 받습니다.
우선 데이터베이스에 입력받은 username
에 해당하는 유저 데이터가 존재하는지 확인합니다.
만약 존재하지 않는다면
return done(null, false, { message: "Incorrect username or password." });
를 통해 에러는 null
, 유저 데이터는 false(존재하지 않음)
, 유효성 검사 결과 메세지는 "Incorrect username or password."
로 반환을 하게 됩니다.
crypto.pbkdf2(password, row.salt, 310000, 32, "sha256", function (err, hashedPassword) {
if (err) {
return done(err);
}
if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) {
return done(null, false, { message: "Incorrect username or password." });
}
return done(null, row);
});
이제 username
이 존재하는 유저인지 확인했다면 다음으로 password를 확인합니다.
대부분의 경우 Password를 데이터베이스에 저장할때 암호화 과정을 거치므로 유효성 검사를 할때도 입력받은 password에 동일한 암호화를 진행합니다.
이후 에러 핸들링을 위한 로직을 작성하고
만약 입력받은 username
유저데이터가 가진 password와 동일하지 않다면
return done(null, false, { message: "Incorrect username or password." });
를 통해 유효하지 않음을 반환해줍니다.
최종적으로 모든 검증을 통과하였을시,
return done(null, row);
을 통해 유저 데이터를 반환합니다.
-이때 row는 데이터베이스에서 찾은 유저 데이터를 의미합니다-
이제 유효성 검사 전략이 완성돼었습니다.
완성한 전략을 사용할 라우팅에 연결해주면 됩니다.
//.../routes/auth.js
router.post(
"/login/password",
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/login",
})
);
"/login/password"
경로에 POST
요청 미들웨어로 passport.authenticate
를 할당합니다.
첫번째 인자로는 전략의 타입을 적고 두번째 인자로는 성공,실패시 리다이렉트될 path
가 담긴 객체를 지정합니다.
이제 Sign in 페이지에 요청을 보내봅시다.
예상과는 다른 결과가 나타납니다.
해당 에러를 간단하게 해석하자면 "세션 써라" 라고 할 수 있습니다.
지금까지 과정은 단순히 유효성을 검사할 뿐인 로직구현입니다. 우리는 사용자가 웹 페이지를 탐색할때 인증정보를 유지할 세션을 설정해주어야합니다.
$ npm install express-session
$ npm install connect-sqlite3
우선 세션을 위한 express-session
미들웨어와 세션을 저장할 connect-sqlite3
를 설치해줍니다.
이후 app.js
에 설치한 미들웨어를 추가해줍니다.
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var passport = require('passport');
var session = require('express-session');
var SQLiteStore = require("connect-sqlite3")(session);
그리고 세션 스토리지를 SQLite DB와 연결합니다.
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: false,
store: new SQLiteStore({ db: 'sessions.db', dir: './var/db' })
}));
app.use(passport.authenticate('session'));
그리고 app.use(passport.authenticate('session'));
로 Passport와 세션을 연결해줍니다.
이러면 SQLite 환경의 데이터베이스의 세션이 저장되고 Passport에서는 이를 활용합니다.
passport.serializeUser(function (user, cb) {
process.nextTick(function () {
cb(null, { id: user.id, username: user.username });
});
});
passport.deserializeUser(function (user, cb) {
process.nextTick(function () {
return cb(null, user);
});
최종적으로 로그인 세션을 완성시켜 정상적인 로그인이 가능토록 합니다.