Node.jsにおいてGoogle認証機能の実装にチャレンジした話

#はじめに

私はプログラミング初学者です。
これから各記事はN予備校の「【2021年度】プログラミング入門 Webアプリ」を一通り終えて、今まで習ったことの復習と応用機能の実装を目的に開発を行っています。

この記事は、自身の行ったこと、躓いたところ、どうやって解決できたかを残すことで、同じ立ち位置の方に少しでも役に立てればと思って書いています。

内容、質ともに取るに足らない、何を言っているんだ？というところが殆どかもしれませんが、生温かい目で見守っていただけると幸いです。

#今回やろうと思ったこと

N予備校のプログラミング入門ではGithubの認証を用いてログインする機能を実装する。
ただ、今後開発することを考えると、Githubのアカウントもっている利用者はどれだけいるの？と思い、Googleでの認証機能を実装しようと思いました。

#まず…

今回はプログラミング入門を終えて間もなく、今まで学んだことの復習を兼ねているので教材に沿って進めていった。

内容に戻ること第４章「実践サーバーサイドプログラミング」

**17.【サービス開発2】プロジェクトの作成と認証の実装**
Dockerfileやらdb.Dockerfileやらdocker-compose.ymlやらを教材通り準備。
せっかくなので、ディレクトリ名やDB名などを自分なりにイジってみて関連性を体で覚えていこうと思いました。

ここで、Express.jsやhelmetモジュールを実装します。これも教材通り。

特に予定調整アプリをもう一度制作するつもりもないので、ここでGoogle認証機を用いたログイン機能を実装しようとしました。

#APIのIDとシークレットキーを取得

Githubでもやりましたが、IDとシークレットキーを発行してもらわなきゃならないので、下記サイトを参考にGoogle Cloud Platformで設定、発行を行いました。

https://photo-tea.com/p/17/nodejs-google-api-oauth/

#passport-google-oauth20

認証に関しては第四章の
**04.GitHub を使った外部認証**
を参考します。

Googleの認証機能を実装するには、
どうもpassport-google-oauth20というモジュールを使えばいけるらしい。

```
yarn add passport@0.3.2
yarn add passport-google-oauth20
yarn add express-session@1.13.0
```
以上の３つのモジュールを入れる。

さて、教材にはapp.jsに記入するコード、すなわち答えが記載されているが、passport-google-oauth20に対してどのようなコードをかけばよいのかわからない。
ググってみるとGithubのページがでました。

https://github.com/jaredhanson/passport-google-oauth2

ここには、

```Javascript:app.js
var GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
    clientID: GOOGLE_CLIENT_ID,
    clientSecret: GOOGLE_CLIENT_SECRET,
    callbackURL: "http://www.example.com/auth/google/callback"
  },
  function(accessToken, refreshToken, profile, cb) {
    User.findOrCreate({ googleId: profile.id }, function (err, user) {
      return cb(err, user);
    });
  }
));
```

```Javascript:app.js
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile'] }));

app.get('/auth/google/callback', 
  passport.authenticate('google', { failureRedirect: '/login' }),
  function(req, res) {
    // Successful authentication, redirect home.
    res.redirect('/');
  });

```
こんなコードを書けばよいよ的な事が書いてあります。

教材のGithub認証のコードと共通するところも多いし、このままコピペでええやろと思い、コピペ。
※callbackURL: "http://www.example.com/auth/google/callback"
→callbackURL: "http://localhost:8000/auth/google/callback"
に変えます

取り敢えずローカルで動けば良いので、IDとキーを変数に打ち込む。

```Javascript:app.js
var GOOGLE_CLIENT_ID = 'XXXXXXXXXXXXX';  // 発行されたID
var GOOGLE_CLIENT_SECRET ='XXXXXXXXXXXXX';  // 発行されたシークレットキー
```


ただexpress-session部分の記載は無いので、教材を参考にそのままコピペしました。

```Javascript:app.js
passport.serializeUser(function (user, done) {
  done(null, user);
});

passport.deserializeUser(function (obj, done) {
  done(null, obj);
});
```

```Javascript:app.js
app.use(session({ secret: 'XXXXXXXXXXXXX', resave: false, saveUninitialized: false })); //XXXXXXXXXXXXXは乱数
app.use(passport.initialize());
app.use(passport.session());
```

ここでサーバを起動してローカルホストにアクセスし、認証機能がうまく働いていればトップページにリダイレクトされます。しかし…

#User is not defined

![スクリーンショット 2021-05-20 1.12.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633152/3abb6263-8774-f3a7-3039-c80ba6614e42.png)

エラーです。リダイレクトされません。
Userが定義されていない…何の話だろう…

さっきコピペしたコードをよく見ると、

```Javascript:app.js
var GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
    clientID: GOOGLE_CLIENT_ID,
    clientSecret: GOOGLE_CLIENT_SECRET,
    callbackURL: "http://www.example.com/auth/google/callback"
  },
  function(accessToken, refreshToken, profile, cb) {
    User.findOrCreate({ googleId: profile.id }, function (err, user) {
      return cb(err, user);
    });
  }
));
```
ここにUser.findOrCreateの記載がありました。
ということはUserモジュールを読み込まなきゃいけませんね。
見た感じ、DBのUserテーブルに関する話なのかなーと思い、

**17.【サービス開発3】データモデルの実装とユーザーの保存**

を参考にsequalize と PostgreSQL 関連モジュールのインストールや、models/sequelize-loader.jsやmodels/User.jsなどを作成しました。

```
yarn add sequelize@6.5.0
yarn add pg@8.5.1
yarn add pg-hstore@2.3.3
```

```javascript:models/sequelize-loader.js
'use strict';
const {Sequelize, DataTypes} = require('sequelize');
const sequelize = new Sequelize(
  'postgres://postgres:postgres@db/XXXXXXXXXX' // XXXXXXXXXXはデータベース名
);

module.exports = {
  sequelize,
  DataTypes
};
```

```javascript:models/User.js
'use strict';
const {sequelize, DataTypes} = require('./sequelize-loader');

const User = sequelize.define(
  'users',
  {
    userId: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      allowNull: false
    }
  },
  {
    freezeTableName: true,
    timestamps: false
  }
);

module.exports = User;
```

app.jsにUserモジュールを読み込ませます。
そしてsync関数でデータベースのテーブルを作成します。

```javascript:app.js
var User = require('./models/user');
User.sync();
```
**※docker-compose exec db bashからデータベースの作成もお忘れなく**

そして、エラー対処にどうすればよいか右往左往している間に
**04.GitHub を使った外部認証**
のログインページなどの機能実装もドサクサに紛れて済ませてしまいました。

#エラー
![スクリーンショット 2021-05-20 1.37.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633152/2d9d35bb-235b-9a6e-1c1a-04d00476b960.png)

```
(node:387) UnhandledPromiseRejectionWarning: Error: Missing where attribute in the options parameter passed to findOrCreate. Please note that the API has changed, and is now options only (an object with where, defaults keys, transaction etc.)
    at Function.findOrCreate (/app/node_modules/sequelize/lib/model.js:2272:13)
    at Strategy._verify (/app/app.js:33:8)
    at /app/node_modules/passport-oauth2/lib/strategy.js:202:24
    at /app/node_modules/passport-google-oauth20/lib/strategy.js:122:5
    at passBackControl (/app/node_modules/oauth/lib/oauth2.js:134:9)
    at IncomingMessage.<anonymous> (/app/node_modules/oauth/lib/oauth2.js:157:7)
    at IncomingMessage.emit (events.js:327:22)
    at endReadableNT (internal/streams/readable.js:1327:12)
    at processTicksAndRejections (internal/process/task_queues.js:80:21)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:387) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:387) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
```

だめだ…このエラーでググってもよくわからん…

結局私はこのエラーで１日費やしてしまった結果、N予備校のフォーラムにGoogle認証の機能を実装する話が無いか検索したところ、
下記アプリケーションを作った方のGithubが公開されていたので、その中身を確認しました。

https://github.com/Takeharu7-51/Easy-Questionnaire

#process.nextTick()とdone()

ありました。テンプレのUser.findOrCreate云々の部分が下記の通りになってました。

```Javascript:app.js
passport.use(new GoogleStrategy({
  clientID: GOOGLE_CLIENT_ID,
  clientSecret: GOOGLE_CLIENT_SECRET,
  callbackURL: process.env.HEROKU_URL ? process.env.HEROKU_URL + 'auth/google/callback' : 'http://localhost:8000/auth/google/callback'
},
  function (accessToken, refreshToken, profile, done) {
    process.nextTick(function () {
      User.upsert({
        userId: profile.id,
        username: profile.displayName,
        numberofmyAnswers: profile.numberofmyAnswers,
        numberofmyQuestionnaire: profile.numberofmyQuestionnaire
      }).then(() => {
        done(null, profile);
      });
    });
  }
));
```

このprocess.nextTick()とdone()の下りはN予備校の教材でも解説されてました。
ここはGithub認証と同じ流れで良いのかー！！！

そして取り敢えず自分のコードには下記コードを実装しました。

```Javascript:app.js
passport.use(new GoogleStrategy({
  clientID: GOOGLE_CLIENT_ID,
  clientSecret: GOOGLE_CLIENT_SECRET,
  callbackURL: "http://localhost:8000/auth/google/callback"
},
function (accessToken, refreshToken, profile, done) {
  process.nextTick(function () {
    User.upsert({
      userId: profile.id
    }).then(() => {
      done(null, profile);
    });
  });
}
));
```

これでいけそう！

また再度サーバーを起動し、ローカルホストにアクセス。認証を行うと…

あれ？？進まない…コンソールを確認…

![スクリーンショット 2021-05-20 2.03.04aaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633152/09de1f3a-3fce-cbf1-c5f8-25c160f0e60e.png)

```
(node:534) UnhandledPromiseRejectionWarning: SequelizeDatabaseError: 値"104007363193274902089"は型integerの範囲外です
    at Query.formatError (/app/node_modules/sequelize/lib/dialects/postgres/query.js:386:16)
    at Query.run (/app/node_modules/sequelize/lib/dialects/postgres/query.js:87:18)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at async /app/node_modules/sequelize/lib/sequelize.js:619:16
    at async PostgresQueryInterface.upsert (/app/node_modules/sequelize/lib/dialects/abstract/query-interface.js:804:12)
    at async Function.upsert (/app/node_modules/sequelize/lib/model.js:2482:20)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:534) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:534) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
```
なんかuserIdのデータ型に問題がありそうですね。
また先程のTakeharuさんのGithubのmodels/user.jsを参照しました。

```javascript:models/User.js
'use strict';
const loader = require('./sequelize-loader');
const Sequelize = loader.Sequelize;

const User = loader.database.define(
  'users',
  {
    userId: {
      type: Sequelize.DECIMAL, //Githubアカウントの場合はINTEGERで良いがGoogleアカウントの場合IDが長いのでDECIMAL
      primaryKey: true,
      allowNull: false
    },
    username: { //アカウント名
      type: Sequelize.STRING,
      allowNull: false
    },
    numberofmyAnswers: { //自分の回答数
      type: Sequelize.INTEGER,
      allowNull: false,
      defaultValue: 0 //デフォルトで0を設定
    },
    numberofmyQuestionnaire: { //自分のアンケート数
      type: Sequelize.INTEGER,
      allowNull: false,
      defaultValue: 0
    }
  },
  {
    freezeTableName: true,
    timestamps: false,
    indexes: [
      {
        fields: ['userId']
      }
    ]
  }
);

module.exports = User;
```

ご丁寧に説明書きが…
「//Githubアカウントの場合はINTEGERで良いがGoogleアカウントの場合IDが長いのでDECIMAL」

最終的に以下の通りになりました。

```javascript:models/User.js
'use strict';
const {sequelize, DataTypes} = require('./sequelize-loader');

const User = sequelize.define(
  'users',
  {
    userId: {
      type: DataTypes.DECIMAL,
      primaryKey: true,
      allowNull: false
    }
  },
  {
    freezeTableName: true,
    timestamps: false
  }
);

module.exports = User;
```

**※※ここでdocker-compose exec db bashからデータベースのテーブルを一度削除しないと同じエラーが残ります**

#無事リダイレクトされた

ここで再度サーバーを起動し、ローカルホストにアクセスして、認証機能を確認したところ、無事リダイレクトされ、認証機能もしっかり動いていました。

#最後に

私が躓いたポイントは大きく２つ。

1. **process.nextTick()とdone()を使わなかったこと**
2. **Google認証の場合userIdのデータ型をDECIMALにしなければならないこと**

今回色々自分で調べ、イジり倒したら、少しだけコードを読みやすくなったり、基本操作に慣れた気がします。
みなさんも、どんどん調べてイジってみるといいかもしれません。無駄ではないと思いますよ。