概要
OAuth2.0のAuthorizationCodeGrantという認可方式でGoogleの認可サーバからトークンを取得してみた
今回作るアプリの構成
構成図
ExpressとHTMLでクライアントアプリを作成しGoogleCloudPlatformからアクセストークンを取得してGmailAPIを叩けるようにする
シーケンス図
↓はアクセストークン取得~GmailAPI叩くまでの処理の概要をシーケンス図に書き起こしたもの
GoogleAPIの設定
クライアントIDの発行
GoogleAPIにログインして適当なプロジェクトを作成する
メニューのAPIとサービス
> 認証情報
をクリックして↓のようなページへ
認証情報を作成
> OAuthクライアントIDの作成
をクリックし、クライアントIDを発行する
アプリケーションの種類
: ウェブアプリケーション
承認済みのリダイレクトURI
: http://localhost:3000/auth/login/callback
作成したらOAuthクライアントのJSONファイルをダウンロードしておく
テストユーザーの発行
外部に公開していないクライアントの場合テストユーザーとして登録したGoogleアカウントじゃないとアクセスできないみたいなので追加する
OAuth同意画面
をクリックしてテストユーザー
ステップまで適当に進めて追加する
クライアントアプリの実装
今回はフロントエンドをHTMLで、バックエンドをexpressで実装する
フロントエンドについてはバックエンドの各種エンドポイントにアクセスできればなんでもOK
バックエンドの実装については↓の項目を参照
バックエンドの実装
アプリの初期化
適当なディレクトリで↓を実行
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
インストールが完了したら各種設定ファイルを作成/編集
{
"compilerOptions": {
"sourceMap": true,
"target": "ES2021",
"module": "ES2020",
"moduleResolution": "node"
}
}
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"],
},
};
// 省略
"scripts": {
"build": "webpack"
},
// 省略
静的ファイル配信の実装
最初にHTMLを返す処理を実装する
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
を作成して編集
import * as express from 'express';
export const rootRouter = express.Router();
rootRouter.get('/', (req, res) => {
res.sendFile('static/index.html'); // ※HTMLファイルのパスを指定
});
動作確認
以下のコマンドでビルド&アプリ実行し、ブラウザでhttp://localhost:3000 にアクセスしてHTMLで書いたページが表示されたら成功👍
npm run build
node dist/index.js
アクセストークン取得処理の実装
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 にアクセス
npm run build
node dist/index.js
クライアントが表示されるのでイベント発火→http://localhost:3000/auth/login へリダイレクト
Googleのログイン画面へ移動するのでメールアドレス、パスワードを入力してログイン
クライアントへリダイレクトするので開発者ツールでCookieを確認
access_token
が入っていれば成功👍
GmailAPI実行処理の実装
Cookieに入れたアクセストークンを使ってGmailAPIを叩く処理を実装する
今回は↓のAPIを叩けるようにしてみる
https://developers.google.com/gmail/api/reference/rest/v1/users/getProfile
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"の後ろに半角スペースが必要
# 地味に引っ掛かりやすいので注意