LoginSignup
30

More than 5 years have passed since last update.

Node.js+Passport+Google 認証を SPA から使う

Last updated at Posted at 2017-12-28

はじめに

ウェブアプリでユーザ認証を実装するのに、Node.js+Passport を使ってみました。

これを通常のウェブアプリでなく、SPA(シングルページアプリ)で使いたいと思いました。

Node.js でウェブ API を用意する

クライアント環境のウェブブラウザで、いわゆるフロントエンドアプリが動きます。
サーバアプリでウェブ API が提供され、クライアントアプリからコールされます。
まず、ウェブ API を提供するサーバアプリを Node.js で用意します。

ワークスペースを作る

まず、Node.js+Express のワークスペースを作ります。

app.js
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));

var server = app.listen(3000, function(){
    console.log("Node.js is listening to PORT " + server.address().port);
});

認証不要なウェブ API を用意する

まず、認証不要でアクセスできる API を作ります。単純な文字列を返すようにします。

app.js
app.get('/api/insecure', function(req, res){
    res.send("Insecure response.");
});

クライアントアプリからアクセスしてみます。API から返された文字列を表示します。

app.html
<button id='insecure'>Insecure Request</button>
<div id='status'></div>
<div id='output'></div>

<script>
    document.querySelector('#insecure').addEventListener('click', function(){
        fetch('/api/insecure')
        .then(function(res){
            document.querySelector('#status').innerHTML = res.statusText;
            res.text()
            .then(function(text){
                document.querySelector('#output').innerHTML = text;
            });
        });
    });
</script>

ID とパスワードで都度認証する API を用意する

Passport を使う

ワークスペースに Passport を追加します。

$ npm install --save passport
app.js
var passport = require('passport');
app.use(passport.initialize());

ID とパスワードで都度認証する API を用意する

前述の API を ID とパスワードで都度認証するようにします。

app.js
var LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy(function(username, password, done){
    if (/* username と password を確認する */) {
        return done(null, username);
    }
    else {
        return done(null, false);
    }
}));

app.post('/api/secure/local', passport.authenticate('local', { session: false }), function(req, res){
    res.send("Secure response from " + JSON.stringify(req.user));
});

クライアントアプリからアクセスします。

app.html
<button id='secure_local'>Local Auth Request</button>

<script>
    document.querySelector('#secure_local').addEventListener('click', function(){
        var data = new FormData();
        data.append('username', '◆◆◆◆');
        data.append('password', '◆◆◆◆');
        fetch('/api/secure/local',{
            method: 'POST',
            body: data
        })
        .then(function(res){
            document.querySelector('#status').innerHTML = res.statusText;
            res.text()
            .then(function(text){
                document.querySelector('#output').innerHTML = text;
            });
        });
    });
</script>

ID とパスワードの代わりに Google 認証を使う

ID とパスワードの代わりに Google を使って認証するようにします。

前述の API を Google を使って認証するようにしてみましょう。

app.js
app.get('/api/secure/google', passport.authenticate('google', { session: false,
    scope: ['https://www.googleapis.com/auth/plus.login'] }), function(req, res){
    res.send("Secure response from " + JSON.stringify(req.user));
});
/* 以下略 */

でも、API にアクセスするたびに Google のサイトに飛んで認証するとか、ナンセンスですね。
そこで、JWT を使います。

JWT で認証する

JWT とは

JWT(ジョット)とは、JSON Web Token の略で、電子署名付きの JSON で、改ざんをチェックできるようになっているものです。
以下のように使います。

①クライアントアプリは認証情報(ID とパスワードとか)をサーバに送る
②サーバアプリは送られた認証情報を確認する
(①②は OAUTH を使ってもいい。)
③サーバアプリは ID と有効期限を含む情報を暗号化して JSON(これが JWT)にして返す
④クライアントアプリは ID とパスワードの代わりに③で貰った JSON を送るようにする
⑤サーバアプリは送られた JSON の改竄検証した上で認証する

JWT で都度認証する API を用意する

ワークスペースで JWT を使った認証できるようにします。

$ npm install --save passport-jwt

前述の API を JWT で都度認証するようにします。

app.js
var JwtStrategy = require('passport-jwt').Strategy;
var ExtractJwt = require('passport-jwt').ExtractJwt;
passport.use(new JwtStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'),
    secretOrKey: '◆◆◆◆',
    issuer: '◆◆◆◆',
    audience: '◆◆◆◆'
}, function(payload, done){
    if (/* payload.sub を確認する */) {
        return done(null, user, payload);
    }
    else {
        return done();
    }
}));

app.get('/api/secure/jwt', passport.authenticate('jwt', { session: false }), function(req, res){
    res.send("Secure response from " + JSON.stringify(req.user));
});

クライアントアプリからアクセスします。

app.html
<script>
    var accessToken;
    document.querySelector('#secure_jwt').addEventListener('click', function(){
        var headers = {};
        if (accessToken) {
            headers['Authorization'] = 'JWT ' + accessToken;
        }
        fetch('/api/secure/jwt', { headers: headers })
        .then(function(res){
            document.querySelector('#status').innerHTML = res.statusText;
            res.text()
            .then(function(text){
                document.querySelector('#output').innerHTML = text;
            });
        });
    });
</script>

認証に使うトークンを予め貰っておかないといけませんね。

Google 認証してトークンを返す

Google 認証できるようにする

改めて Google 認証できるようにします。

app.js
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
passport.use(new GoogleStrategy({
    clientID: "◆◆◆◆◆◆◆◆.apps.googleusercontent.com",
    clientSecret: "◆◆◆◆",
    callbackURL: "http://◆◆◆◆/auth/google/callback"
}, function(accessToken, refreshToken, profile, done){
    if (/* profile.id を確認する */) {
        return done(null, user);
    }
    else {
        return done(null, false);
    }
}));

app.get('/auth/google', passport.authenticate('google', {
    scope: ['https://www.googleapis.com/auth/plus.login']
}));

app.get('/auth/google/callback', passport.authenticate('google', { session: false }), function(req, res){
    /* ここでトークンを生成して返す */
});

トークンを生成する

トークンを生成して返すためこうします。

npm install --save jsonwebtoken
app.js
var jwt = require('jsonwebtoken');

function generateToken(userId) {
    const token = jwt.sign({}, '◆◆◆◆', {
        expiresIn: '◆◆◆◆',
        audience: '◆◆◆◆',
        issuer: '◆◆◆◆',
        subject: userId.toString()
    });
    return token;
}

app.get('/auth/google/callback', passport.authenticate('google', { session: false }), function(req, res){
    var token = generateToken(req.user.id);
    res.JSON({ token: token });
});

クライアントアプリからアクセスしてみます。

app.html
<button id='auth_google'>Google Auth Login</button>

<script>
    document.querySelector('#auth_google').addEventListener('click', function(){
        fetch('/auth/google')
        .then(function(res){
        /* 以下略 */
</script>

上手く行きません。Google 認証するにはページ遷移しないといけないようです。

app.html
<a href="/auth/google">Google Auth Login</a>

Google 認証できますが、ページ遷移してしまうと、クライアントアプリ自体がいなくなってしまいます。

別ウィンドウを開いて Google 認証する

そこで、クライアントアプリでは別ウィンドウを開いて Google 認証を済ませるようにします。

app.html
<button id='auth_google'>Google Auth Login</button>

<script>
    document.querySelector('#auth_google').addEventListener('click', function(){
        window.open('/auth/google');
    });
</script>

クライアントアプリがサーバアプリからトークンを受取る

クライアントアプリがサーバアプリからトークンを受取るには工夫が必要です。

まず、クライアントアプリ側の仕掛け。
window オブジェクトにコールバック関数を仕掛けておきます。

app.html
<script>
    document.querySelector('#auth_google').addEventListener('click', function(){
        window.authenticateCallback = function(token){
            accessToken = token;
        }
        window.open('/auth/google');
    });
</script>

次に、サーバアプリ側の仕掛け。
こんなページテンプレートを用意しておきます。

authenticated.html
<script>
    window.opener.authenticateCallback("{{token}}");
    window.close();
</script>

このページが開かれると、事前に仕掛けておいたコールバック関数で、引数に渡したトークンがクライアントアプリに渡ります。

Google 認証からコールバックされトークンを生成したところで、上記のテンプレートをレンダリングします。
今回はレンダリングエンジンに Handlebars を使います。

npm install --save express-handlebars
app.js
var handlebars = require('express-handlebars');
app.engine('html', handlebars());
app.set('view engine', 'handlebars');
app.set('views', __dirname + '/●●●●');

app.get('/auth/google/callback', passport.authenticate('google', { session: false }), function(req, res){
    var token = generateToken(req.user.id);
    res.render('authenticated.html', { token: token });
});

おわりに

自作の SPA で Google 認証できるようにしたいと思い試してみましたが、上手く行きませんでした。
そこで以下の記事を見つけました。

これはいい記事ですが、記事に記載されているコードだけでは、自分のアプリに応用しても動きませんでした。
Github にあるコード全体を読みましたが、自分のスタイルと違っていて、自分のアプリに応用するのはつらかった。
そこで、自分のコードにするため、記事のコードを書換していきました。その経緯を、この記事にしておきました。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30