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

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

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の一般的な使い方をまとめてみました。

参考記事

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com