Passport.js 기초 튜토리얼

김민규·2023년 4월 14일
0
post-thumbnail

이 포스트는 Passport.js Docs을 기반으로 작성되었습니다.

Passport.js란

Passport.js는 node.js의 인증용(authentication) 미들웨어입니다. Passport는 인증이라는 한가지 목적만을 위해서 설계되었습니다.

Passport Tutorial

환경 구성

$ 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을 인자로 받습니다.

ID 유효성 검사

우선 데이터베이스에 입력받은 username에 해당하는 유저 데이터가 존재하는지 확인합니다.
만약 존재하지 않는다면

return done(null, false, { message: "Incorrect username or password." });

를 통해 에러는 null, 유저 데이터는 false(존재하지 않음), 유효성 검사 결과 메세지는 "Incorrect username or password."로 반환을 하게 됩니다.

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는 데이터베이스에서 찾은 유저 데이터를 의미합니다-

Passport 연결하기

이제 유효성 검사 전략이 완성돼었습니다.

완성한 전략을 사용할 라우팅에 연결해주면 됩니다.

//.../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);
  });

최종적으로 로그인 세션을 완성시켜 정상적인 로그인이 가능토록 합니다.

profile
Error Driven Development

0개의 댓글