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

# はじめに

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

* [Node.js＋Express＋Passport を使ってみた]
(https://qiita.com/tinymouse/items/fa910bf80a038c7f9ccb)
* [Node.js＋Passport＋Google 認証を使ってみた]
(https://qiita.com/tinymouse/items/ab79a14173ebc7b75274)

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

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

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

## ワークスペースを作る

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

* [ゼロからはじめるExpress + Node.jsを使ったアプリ開発 - Qiita]
(https://qiita.com/nkjm/items/723990c518acfee6e473)

```javascript: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 を作ります。単純な文字列を返すようにします。

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

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

```html: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 を追加します。

* [Node.js＋Express＋Passport を使ってみた - Qiita]
(https://qiita.com/tinymouse/items/fa910bf80a038c7f9ccb)

```
$ npm install --save passport
```

```javascript:app.js
var passport = require('passport');
app.use(passport.initialize());
```

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

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

```javascript: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));
});
```

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

```html: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 を使って認証するようにします。

* [Node.js＋Passport＋Google 認証を使ってみた - Qiita]
(https://qiita.com/tinymouse/items/ab79a14173ebc7b75274)

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

```javascript: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 について調べた内容をまとめました。 - Qiita]
(https://qiita.com/nkjm/items/723990c518acfee6e473)

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

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

```
$ npm install --save passport-jwt
```

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

```javascript: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));
});
```

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

```html: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 認証できるようにします。

* [Node.js＋Passport＋Google 認証を使ってみた - Qiita]
(https://qiita.com/tinymouse/items/ab79a14173ebc7b75274)

```javascript: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
```

```javascript: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 });
});
```

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

```html: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 認証するにはページ遷移しないといけないようです。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```
npm install --save express-handlebars
```

```javascript: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 認証できるようにしたいと思い試してみましたが、上手く行きませんでした。
そこで以下の記事を見つけました。

* [手間のかかるSPAのソーシャルログインをPassport.jsで手軽に実装する方法 - WPJ]
(https://www.webprofessional.jp/spa-social-login-google-facebook/)

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