【OAuth2.0】GoogleのアクセストークンをAuthorizationCodeGrantフローで取得する

# 概要
OAuth2.0のAuthorizationCodeGrantという認可方式でGoogleの認可サーバからトークンを取得してみた

# 今回作るアプリの構成
## 構成図
![oauth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/fa8f2f0f-73b5-9724-133d-1698e110fe4d.png)
ExpressとHTMLでクライアントアプリを作成しGoogleCloudPlatformからアクセストークンを取得してGmailAPIを叩けるようにする

## シーケンス図
↓はアクセストークン取得~GmailAPI叩くまでの処理の概要をシーケンス図に書き起こしたもの
![シーケンス図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/976ff80c-6826-f5e5-54cf-d23788294956.png)

# GoogleAPIの設定
## クライアントIDの発行
[GoogleAPI](https://console.cloud.google.com/apis/)にログインして適当なプロジェクトを作成する

メニューの`APIとサービス` > `認証情報`をクリックして↓のようなページへ
![1.jfif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/55a4d705-ef68-c563-dd19-765faafd6c8b.jpeg)


`認証情報を作成` > `OAuthクライアントIDの作成`をクリックし、クライアントIDを発行する
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/5e5508fc-32c0-bd92-acc9-042f1f8d1bb6.png)
`アプリケーションの種類`: ウェブアプリケーション
`承認済みのリダイレクトURI`: http://localhost:3000/auth/login/callback

作成したらOAuthクライアントのJSONファイルをダウンロードしておく

## テストユーザーの発行
外部に公開していないクライアントの場合テストユーザーとして登録したGoogleアカウントじゃないとアクセスできないみたいなので追加する
`OAuth同意画面`をクリックして`テストユーザー`ステップまで適当に進めて追加する
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/b11a2d2c-8fda-3d1d-fc96-1b8b592aa63f.png)

# クライアントアプリの実装
今回はフロントエンドをHTMLで、バックエンドをexpressで実装する
フロントエンドについてはバックエンドの各種エンドポイントにアクセスできればなんでもOK
バックエンドの実装については↓の項目を参照

## バックエンドの実装
### アプリの初期化
適当なディレクトリで↓を実行

```bash
npm init -y
npm i express cookie-parser uuid node-fetch
npm i -D webpack webpack-cli typescript ts-loader @types/express @types/cookie-parser @types/uuid @types/node-fetch
```

インストールが完了したら各種設定ファイルを作成/編集

```tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "target": "ES2021",
    "module": "ES2020",
    "moduleResolution": "node"
  }
}
```

```webpack.config.js
const path = require("path");

module.exports = {
  mode: "development",
  entry: {
    handler: path.resolve(__dirname, "src/index.ts"),
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
      },
    ],
  },
  target: "node",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
    libraryTarget: "commonjs",
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
};
```

```package.json
// 省略
"scripts": {
  "build": "webpack"
},
// 省略
```

### 静的ファイル配信の実装
最初にHTMLを返す処理を実装する
`src/index.ts`を作成して編集

```src/index.ts
import * as express from 'express';
import * as cookieParser from 'cookie-parser';
import { rootRouter } from './router/root-router';

const app = express();
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('static')); // ※静的ファイル等があるディレクトリを指定

app.use('/', rootRouter);

app.listen(3000, () => {
  console.log('Listening on port 3000');
});
```

`src/router/root-router.ts`を作成して編集

```src/router/root-router.ts
import * as express from 'express';

export const rootRouter = express.Router();

rootRouter.get('/', (req, res) => {
  res.sendFile('static/index.html'); // ※HTMLファイルのパスを指定
});
```

#### 動作確認

以下のコマンドでビルド＆アプリ実行し、ブラウザでhttp://localhost:3000 にアクセスしてHTMLで書いたページが表示されたら成功👍

```bash
npm run build
node dist/index.js
```

### アクセストークン取得処理の実装

```src/router/auth-router.ts
import * as express from 'express';
import fetch from 'node-fetch';
import { v4 } from 'uuid';
import { credentials } from '../../credentials'; // OAuthクライアントのJSONファイル

export const authRouter = express.Router();

// Googleのサインイン画面へリダイレクト
authRouter.get('/login', (_, res) => {
  const state = v4();
  const params = {
    client_id: credentials.client_id,
    redirect_uri: credentials.redirect_uris[0],
    response_type: 'code',
    scope: 'https://www.googleapis.com/auth/gmail.readonly',
    state: state,
  };
  const encodedParams = new URLSearchParams(params);
  const endpoint =
    'https://accounts.google.com/o/oauth2/v2/auth?' + encodedParams;

  // cookieにstateをセット
  res.cookie('state', state);
  res.redirect(endpoint);
});

// 認可サーバからのリダイレクト先
authRouter.get('/login/callback', async (req, res) => {
  // cookieのstateとクエリのstateが一致するか検証
  if (req.query.state !== req.cookies.state) {
    res.status(400).send(`state is invalid.
    Received state: ${req.query.state}
    Cookie state: ${req.cookies.state}`);
    return;
  }
  const params = {
    client_id: credentials.client_id,
    client_secret: credentials.client_secret,
    code: req.query.code as string,
    grant_type: 'authorization_code',
    redirect_uri: credentials.redirect_uris[0],
  };

  const encodedParams = new URLSearchParams(params);

  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: encodedParams,
  });
  const responseBody = (await response.json()) as any;

  res.cookie('access_token', responseBody.access_token);
  res.redirect('http://localhost:3000');
});
```

#### 動作確認

ビルド＆実行→ブラウザでhttp://localhost:3000 にアクセス

```bash
npm run build
node dist/index.js
```

クライアントが表示されるのでイベント発火→http://localhost:3000/auth/login へリダイレクト
![スクリーンショット 2021-09-24 184107.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/760ea75f-81aa-9f04-23c2-a196cd62675a.png)

Googleのログイン画面へ移動するのでメールアドレス、パスワードを入力してログイン
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/4a8ba0ab-192f-f9e9-f0a8-6abccf679d56.png)

クライアントへリダイレクトするので開発者ツールでCookieを確認
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/b7e441ae-4377-feb5-8682-3fa91b0d89af.png)
`access_token`が入っていれば成功👍

### GmailAPI実行処理の実装
Cookieに入れたアクセストークンを使ってGmailAPIを叩く処理を実装する
今回は↓のAPIを叩けるようにしてみる
https://developers.google.com/gmail/api/reference/rest/v1/users/getProfile

```src/router/gmailapi-router.ts
import * as express from 'express';
import fetch from 'node-fetch';

export const gmailRouter = express.Router();

gmailRouter.get('/user/profile', async (req, res) => {
  const mailaddress = req.query.mailaddress;

  const params = {
    emailAddress: mailaddress as string,
  };
  const encodedParams = new URLSearchParams(params);

  const response = await fetch(
    `https://gmail.googleapis.com/gmail/v1/users/${mailaddress}/profile?` +
      encodedParams,
    {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${req.cookies.access_token}`, // Authorizationヘッダーにアクセストークンを挿入
      },
    }
  );
  const responseBody = (await response.json()) as any;

  res.send(responseBody);
});
```

クエリからメールアドレスを取り出してそれに紐づくアカウントの情報を取得するAPI
OAuth2.0のアクセストークンは`Authorization`ヘッダーに入れてリソースサーバへ送信することになる
↓`Authorization`ヘッダーのフォーマット

```
Authorization: Bearer ${ACCESS_TOKEN} 
# "Bearer"の後ろに半角スペースが必要
# 地味に引っ掛かりやすいので注意
```

#### 動作確認
アクセストークンの取得後上のGmailAPIへリクエストを投げる
↓のようにプロフィールを取得できればOK👍
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1981935/291dcaa0-9967-968f-f0d3-e7df70ff0287.png)