新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Passport.js の使い方メモ

Passport.jsを使う機会があったので、また必要になった時用のPassport.jsの使い方個人メモです。

Passport.jsとは

  • Passport.jsとは、Node.js+Expressで作ったWebサイトに、ユーザーがログインできる、ユーザー認証を入れるためのライブラリ
  • ユーザー認証の種類は、自分でユーザー管理する「ユーザーID+パスワード認証」だけでなく、GoogleやTwitterなどといったSNS認証など、様々な種類がある
  • 各種の認証周りの実装はライブラリ化されていて、認証の仕組みを意識しないでユーザー認証を導入することができる

導入

  • ライブラリはメイン部分と、Strategyと呼ぶ認証部分に分かれている
  • 認証の種類毎にStrategyが分かれていて、使いたい認証のStrategyを都度インストールして使用する

例)Google認証

インストール

npm install --save passport
npm install --save passport-google-oauth20

ライブラリ読み込み

import * as passport from 'passport';
import * as GoogleStrategy from 'passport-google-oauth20';

ログイン状態

Passport.jsは一般的に、セッションと組み合わせて使用する。

セッションと組み合わせて使用すると、一度認証が完了すると、ログイン状態はセッションとして保存され、 2回目以降のアクセスでは、セッションIDからユーザーデータが復元され、ログイン状態が維持されるようになる。

2回目以降の、アクセスからユーザーデータ取得までの流れ

  • passportをExpressのミドルウェアで使用する。すると、ユーザーがログインしていれば「req.user」にユーザーデータが格納されるようになる
  • 「req.user」が存在するかをチェックして、ユーザーがログインしているかを判断し、ログインしていないならログインページにリダイレクトするなどの処理を書く
  • 都度ユーザーがログインしているかをチェックするのが面倒なら、ミドルウェア化してもよい
// ログイン状態を直接「req.user」から調べる
app.get('/',
    (req: any, res) => {

        if (!req.user) {
            return res.redirect('/login');
        }

        res.render('home.html', { user: req.user });
    }
);

// ログイン状態を調べるミドルウェア
const checkLogin = (req, res, next) => {
    if (!req.user) {
        return res.redirect('/login');
    }

    return next();
};

// ログイン状態チェックをミドルウェアに任せる
app.get('/userinfo',
    checkLogin,
    (req: any, res) => {
        res.render('userinfo.html', { user: req.user });
    }
);

passportとStrategyの紐づけ

  • passportとStrategyの紐づけは、passport.use(new Strategy(~))で行う
  • new Stragety()の2番目の引数に、認証が成功した後に呼ばれる関数を定義する
    • ログインしたユーザーが、妥当なユーザーなら、callback(null, <そのユーザーのユーザーデータ>)を呼び出す
    • <そのユーザーのユーザーデータ>は、req.userに格納される値となる
    • ログインしたユーザーが、妥当なユーザーでないなら、callback(null, false)を呼び出す
    • ログインしたユーザーが、妥当なユーザーかの検証中にエラーが発生したなら、callback(err)を呼び出す
  • new strategy()の具体的な使い方は、認証方法(Strategyの種類)によってマチマチなので、詳しくは各Strategyのドキュメントを参照する
//////////////////////////////
// passportとStrategyの紐づけ
passport.use(new LocalStrategy(
    (username, password, cb: any) => {
        try {
            const user = DB_USER.find((v) => {
                return v.username === username;
            });

            // 妥当なログインではない
            if (!user) {
                return cb(null, false);
            }

            // 妥当なログインではない
            if (user.password !== password) {
                return cb(null, false);
            }

            // 妥当なログイン
            return cb(null, user);

        } catch (err) {
            // エラー発生
            return cb(err);
        }
    }
));

passportとセッションの紐づけ

基本方針

  • セッションIDとユニークユーザー識別子を紐づけて、セッションデータとして保存する
  • ユーザーデータから、ユニークユーザー識別子を取り出して、セッションIDと紐づけする方法を、passport.serializerUser()で定義する
  • サイトにアクセスがあると、下記の流れで「req.user」にユーザーデータがセットされる
    • 1: セッションIDからセッションデータを参照して、ユニークユーザー識別子を取り出す
    • 2 : ユニークユーザー識別子から、ユーザーデータを取り出す
    • 3 : ユーザーデータを「req.user」に設定する
  • ユーザー識別子からユーザーデータを取り出す方法は、passport.deserializeUser()で定義する
//////////////////////////////
// passportとセッションの紐づけ
// ユーザーデータからユニークユーザー識別子を取り出す
passport.serializeUser( (user, cb) => {
    cb(null, user.username);
});

// ユニークユーザー識別子からユーザーデータを取り出す
passport.deserializeUser( (username, cb) => {
    const user = DB_USER.find((v) => {
        return v.username === username;
    });

    if (!user) {
        return cb(`ERROR : NO USERNAME -> ${username}`);
    }

    return cb(null, user);
});

認証

  • 認証の際に使用するページを、サブディレクトリで用意する
  • どのStrategyを使用するかはpassport.authenticate()の1つ目の引数で指定する
  • 認証が失敗した時のリダイレクト先を、passport.authenticate()の2つ目の引数の「failureRedirect」で指定する
  • ページの設定は認証方法(Strategyの種類)によってマチマチなので、詳しくは各Strategyのドキュメントを参照する

例)ユーザーID・パスワード認証

// 認証
app.post('/login',
    passport.authenticate('local', {
        failureRedirect: '/login', // 認証失敗した場合の飛び先
        failureFlash: true
    }),
    (req, res) => {
        // 認証成功した場合の処理
        res.redirect('/');
    }
);

ログイン・ログアウト

  • ログインページのURLは、サブディレクトリで用意する
  • ログインページの設定は、認証方法(Strategyの種類)によってマチマチなので、詳しくは各Strategyのドキュメントを参照する
  • ログアウトはreq.logout()で行う。req.logout()はセッションデータを削除するので、それ以降のアクセスはログアウト状態となる
// ログアウト
app.get('/logout',
    (req: any, res) => {
        req.logout();    // セッション削除
        res.redirect('/login');
    }
);

サンプル

passport-local:ユーザー名・パスワード認証

server.ts

import * as path from 'path';
import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as session from 'express-session';

//////////////////////////////
// Passport.js
import * as passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';

//////////////////////////////
// sample user DB
const DB_USER = [
    { username: "test01", password: "pass", email: "test01@foo.bar.com" },
    { username: "test02", password: "pass", email: "test02@foo.bar.com" },
    { username: "test03", password: "pass", email: "test03@foo.bar.com" },
];

//////////////////////////////
// passportとStrategyの紐づけ
passport.use(new LocalStrategy(
    (username, password, cb: any) => {
        try {
            const user = DB_USER.find((v) => {
                return v.username === username;
            });

            // 妥当なログインではない
            if (!user) {
                return cb(null, false);
            }

            // 妥当なログインではない
            if (user.password !== password) {
                return cb(null, false);
            }

            // 妥当なログイン
            return cb(null, user);

        } catch (err) {
            // エラー発生
            return cb(err);
        }
    }
));

//////////////////////////////
// passportとセッションの紐づけ
// ユーザーデータからユニークユーザー識別子を取り出す
passport.serializeUser( (user, cb) => {
    cb(null, user.username);
});

// ユニークユーザー識別子からユーザーデータを取り出す
passport.deserializeUser( (username, cb) => {
    const user = DB_USER.find((v) => {
        return v.username === username;
    });

    if (!user) {
        return cb(`ERROR : NO USERNAME -> ${username}`);
    }

    return cb(null, user);
});

//////////////////////////////
// Express
const app = express();
const PORT_NO = 3000;

// etc
app.use(bodyParser.urlencoded({ extended: true }));
app.set('views', path.dirname(__dirname) + '/view');
app.set('view engine', 'ejs');
app.engine('html', require('ejs').renderFile);

// session
// Passport.jsでセッションを使うようにする
app.use(session({
    secret: 'keyboard_cat',
    resave: false,
    saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());

//////////////////////////////
// route

// ログインページ
app.get('/login',
    (req, res) => {
        res.render('login.html');
    }
);

// 認証
app.post('/login',
    passport.authenticate('local', {
        failureRedirect: '/login', // 認証失敗した場合の飛び先
        failureFlash: true
    }),
    (req, res) => {
        // 認証成功した場合の処理
        res.redirect('/');
    }
);

// ログイン状態を直接「req.user」から調べる
app.get('/',
    (req: any, res) => {

        if (!req.user) {
            return res.redirect('/login');
        }

        res.render('home.html', { user: req.user });
    }
);

// ログイン状態を調べるミドルウェア
const checkLogin = (req, res, next) => {
    if (!req.user) {
        return res.redirect('/login');
    }

    return next();
};

// ログイン状態チェックをミドルウェアに任せる
app.get('/userinfo',
    checkLogin,
    (req: any, res) => {
        res.render('userinfo.html', { user: req.user });
    }
);

// ログアウト
app.get('/logout',
    (req: any, res) => {
        req.logout();    // セッション削除
        res.redirect('/login');
    }
);

app.listen(PORT_NO);

home.html

<%- include('head.html'); %>
<body>
    <h3>username</h3>
    <div><%= user.username %></div>
    <h3>email</h3>
    <div><%= user.email %></div>
    <div><a href="/userinfo">userinfo</a></div>
    <div><a href="/logout">logout</a></div>
</body>
</html>

login.html

<%- include('head.html'); %>
<body>
    <form action="/login" method="post">
        <h3>username</h3>
        <div><input type="text" name="username" /></div>
        <h3>password</h3>
        <div><input type="password" name="password" /></div>
        <div>
        </div>
    </form>
</body>
</html>

感想など

セッション周りの実装が手間ですね。 しかし、ややこしい認証周りをやってくれるので助かります。

色々データ変換があって大変なのですが、まとめると、データは下記のような流れで変換されています。

  • 「セッションID」>「ユーザー識別子(ユーザーID等)」->「req.user(ユーザーデータ)」

「req.user」に何のユーザーデータを登録するかは実装次第です。

Passport.jsを利用するには、セッションの理解が不可欠なのですが、そもそもそのあたりがよく分かっていなかったので、セッションについてもまとめました。

www.kwbtblog.com

今はシングルページアプリケーションでサイトを作っているので、このようなユーザー認証するWebサイトを作る必要はなかったのですが、SNSのトークンを取得するのにPassport.jsを使おうと考えていて、まずはPassport.jsの一般的な使い方をまとめてみました。

参考記事