photo credit: suzyhazelwood DSC04299-02 via photopin (license)
コンソールから Node.jsを使って Google OAuth2 クライアントID による OAuth 認証のお試しコードを書いてみました。
日々の作業でGit BashやMSYS2を多用していますが、コンソールからGoogle Driveのスプレッドシートを参照したり、検索、定型データの追加などが軽くできれば、いちいちブラウザアプリを開いて「よっこらしょ」ってな感じより効率的な場合もあるかと思い、これをベースに「なんか作るか」と、思ったり思わなかったりして、いくつになってもお勉強です。
売り上げランキング: 1,632
目次
- Google OAuth2 に必要なもの
- そもそも OAuth の認証・認可とは
- 認可を求めるのは1度だけ
- 認可コード取得を自動化
- プロジェクトとクライアントIDを作成する
- サンプルコード
- 依存モジュールのインストール
- 実行例
- まとめ
- 参考サイト
関連記事:
Google OAuth2 に必要なもの
ここで示している認証では Google Developper ConsoleのAPIプロジェクト(=アプリケーション)に作成する「クライアントID」と「クライアントシークレット」を使用します(作成手順は後述します)。
これとは別に「クライアントシークレット」を使わず「API KEY」と「クライアントID」を使う方法もあるようですが、Node.js からできるかどうかわかりませんでした。
そもそも OAuth の認証・認可とは
このアプリケーションは、Google Drive API を使用して、ユーザー自身の Google Drive のファイルにアクセスします(とプロジェクトに設定している)。しかしユーザーファイルを勝手に触るわけにはいきません。 このため、実行時に認証ページを表示して、「このアプリケーションがユーザーファイルにアクセスして良い」と、ユーザーに認可してもらうのです。
ユーザーが認可すれば、認可コードがアプリケーションに通知され、アプリケーションは実際にユーザーデータにアクセスするためのアクセストークンを手に入れます。
認可を求めるのは1度だけ
認可を求める認証ページが開くのは初回の認可コードを取得するときだけです。
アクセストークンには有効期限が定められていますが、新たなアクセストークンを取得するためのリフレッシュトークンも含まれており、2回目以降は、これを使って新しいアクセストークンを取得できるというわけ。
(掲載しているプログラムでは、有効期限の判定は一切行わず、常にリフレッシュしています)
認可コード取得を自動化
初回の認証シーケンス(認証ページの表示から認可コードの取得まで)は一般的なウェブアプリと同じように自動化しています。 通常のサンプルコード等では「以下のURLをブラウザで表示して、表示された認証コードを入力してね」なコピペな感じですが、それよか格段に楽ですよ。
認可コードを得る部分のフローは。
- プロセス内にローカルウェブサーバーを起動。
- 認証ページをユーザーのデフォルトブラウザで表示。
- 認証時のリダイレクト先を上記ウェブサーバーのURLに設定。
- ユーザーが認証を完了させると、認可コードがリダイレクト。
認可コードはHTTP GET REQUEST の QUERY_STRINGに含まれています。 だからウェブサーバーがリクエストを受け付けた時点で認可コードを取得できるというわけです。
プロジェクトとクライアントIDを作成する
以下の手順に従って、Google Developper ConsoleのプロジェクトとクライアントIDを作成し、クライアントIDのキーファイルをダウンロードしておく必要があります。
- Google Developper Consoleで、新規プロジェクトを作成し、上のドロップダウンから作成したプロジェクトを選択。
- 「ライブラリ」タブで「Google Drive API」を使用できるように設定しておきます。検索ボックスに「Drive」と入力すればすぐ見つかる。認証をおこなうだけなら不要かもしれませんが、サンプルプログラムではGoogle Driveのルートにあるファイルの一覧を表示するために必要です。
- 「認証情報」タブで「OAuth 2.0 クライアント ID」を新規作成(「アプリケーションの種類」は「ウェブアプリケーション」か「その他」を選択。
- 作成したクライアントIDのJSONファイルをダウンロードします。
サンプルコード
以下のコードは、依存モジュールをインストールすれば単体で動作します。
ちょっと(いやかなり)長くなってしまい、かつコメント無しでスミマセン。
最初に定義されてるmain()
が本体。ざっくり以下の様なことをやっています。
- コマンドラインでクライアントIDのキーファイルを(
.json
を抜いて)指定して実行します。 - キーファイルを読み込んでOAuth2のクライアントを作成。
- 初回の認証ではブラウザを開き、認可コードを受け取ります。
- 2回目以降の認証ではアクセストークンをリフレッシュしています。
- 何れにせよアクセストークンが得られたら、ファイルに保存し、
- 認可したアカウントのGoogle Driveのルートフォルダのファイル一覧を表示します。
※ 実際に動かすための情報は後述。
google-oauth2.js
"use strict"; const google = require("googleapis"); const OAuth2 = google.auth.OAuth2; const drive = google.drive({ version: 'v3' }); const listit = require("list-it"); const opn = require('opn'); const server = require("node-http-server"); const fs = require("fs"); var REDIRECT_PORT = 8800; var clientName = process.argv[2]; function main() { var clientFn = clientName + ".json"; readJsonFile(clientFn).then(function(client) { console.log(clientFn + " Loaded:"); console.log("Client:"); console.log(JSON.stringify(client, null, " ")); return createAuth(client); }).then(function(auth) { var acctokFn = clientName + "-auth.json"; return readJsonFile(acctokFn).then(function(acctok) { console.log(acctokFn + " loaded:"); console.log("Access Token:"); console.log(JSON.stringify(acctok, null, " ")); console.log("Refresh Tokens:"); return refreshAccessToken(auth, acctok).then(function(acctok) { console.log(JSON.stringify(acctok, null, " ")); return writeJsonFile(acctokFn, acctok); }).then(function() { return auth; }); }).catch(function(err) { console.log("No Access Token:"); var authUrl = auth.generateAuthUrl({ scope: "https://www.googleapis.com/auth/drive" }); console.log("Auth URL:" + authUrl); return getAuthCode(authUrl, REDIRECT_PORT).then(function(code) { console.log("Auth Code:" + code); return getAccessToken(auth, code); }).then(function(acctok) { console.log("Access Token:"); console.log(JSON.stringify(acctok, null, " ")); return writeJsonFile(acctokFn, acctok); }).then(function() { return auth; }); }); }).then(function(auth) { return getFileList({ auth: auth, q: "parents='root' and trashed=false" }); }).then(function(resp) { var list = listit.buffer({ "autoAlign" : true }); list.d([ "name", "mimeType" ]); resp.files.forEach(function(file) { list.d([ file.name, file.mimeType ]); }); console.log(list.toString()); }).then(function() { process.exit(0); }).catch(function(err) { console.log("Error: ", err.message); process.exit(-1); }); } function createAuth(client) { var credential; if("installed" in client) { credential = client.installed; } else if("web" in client) { credential = client.web; } var auth = new OAuth2( credential.client_id, credential.client_secret, "http://localhost:" + REDIRECT_PORT + "/"); return auth; } function refreshAccessToken(auth, oldTokens) { return new Promise(function(resolve, reject) { auth.credentials = oldTokens; auth.refreshAccessToken(function(err, refreshedTokens) { if (err) { reject(err); return; } auth.credentials = refreshedTokens; resolve(refreshedTokens); }); }); } function getAccessToken(auth, code) { return new Promise(function(resolve, reject) { auth.getToken(code, function(err, tokens) { if (err) { reject(err); return; } auth.credentials = tokens; resolve(tokens); }); }); } function getFileList(params) { return new Promise(function(resolve, reject) { drive.files.list(params, function(err, resp) { if(err) { reject(err); return; } resolve(resp); }); }); }; function getAuthCode(url, redirect_port) { opn(url); return waitHttpRequest(redirect_port, "/").then(function(http) { var code = http.request.uri.query.code; http.serve(http.request, http.response, ["Auth Code:", code].join(" ")); return code; }); } function waitHttpRequest(port, pathname) { return new Promise(function(resolve, reject) { var config = new server.Config(); config.port = port; server.onRequest = function(request, response, serve) { if(request.uri.pathname == pathname) { resolve({ request: request, response: response, serve: serve }); return true; } else { reject(new Error("Illegal access.")); return false; } }; server.deploy(config); }); } function readJsonFile(fn) { return new Promise(function(resolve, reject) { fs.readFile(fn, function(err, data) { if(err) { reject(err); return; } resolve(JSON.parse(data)); }); }); } function writeJsonFile(fn, obj) { return new Promise(function(resolve, reject) { fs.writeFile( fn, JSON.stringify(obj, null, " "), function(err) { if(err) { reject(err); return; } resolve(); }); }); } main();
依存モジュールのインストール
依存モジュールは以下4つ。
これらが利用可能な状態でなければ動きません。npmでインストールして下さい。
npm install googleapis list-it opn node-http-server
をコピペでいけます。
実行例
コマンドライン引数に、クライアントIDのキーファイルを指定して実行します。
以下実行例で、キーファイルは ./client_id.json
として保存されています。
ファイル一覧の桁がエラくズレていますが、実際のコンソールでは問題なしよ。 (デバッグ出力が長くて申し訳ない)
初回の認証実行例
$ node google-oauth2.js ./client_id ./client_id.json Loaded: Client: { "installed": { "client_id": "<クライアントID>", "project_id": "<プロジェクト名>", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "<クライアントシークレット>", "redirect_uris": [ "urn:ietf:wg:oauth:2.0:oob", "http://localhost" ] } } No Access Token: Auth URL:<認証URL> Auth Code:<認可コード> Access Token: { "access_token": "<アクセストークン>", "refresh_token": "<リフレッシュトークン>", "token_type": "Bearer", "expiry_date": 1512041811335 } name mimeType ****** application/vnd.google-apps.spreadsheet ****** application/vnd.google-apps.folder ****** application/vnd.google-apps.script ****** application/vnd.google-apps.folder Document application/vnd.google-apps.folder Image application/vnd.google-apps.folder Google フォト application/vnd.google-apps.folder My Tracks application/vnd.google-apps.folder $
2回目以降のリフレッシュトークンを利用した実行例
$ node google-oauth2.js ./client_id ./client_id.json Loaded: Client: { "installed": { "client_id": "<クライアントID>", "project_id": "<プロジェクト名>", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "<クライアントシークレット>", "redirect_uris": [ "urn:ietf:wg:oauth:2.0:oob", "http://localhost" ] } } ./client_id-auth.json loaded: Access Token: { "access_token": "<アクセストークン>", "token_type": "Bearer", "expiry_date": 1512021213468, "refresh_token": "<リフレッシュトークン>" } Refresh Tokens: { "access_token": "<アクセストークン>", "token_type": "Bearer", "expiry_date": 1512040667695, "refresh_token": "<リフレッシュトークン>" } name mimeType ****** application/vnd.google-apps.spreadsheet ****** application/vnd.google-apps.folder ****** application/vnd.google-apps.script ****** application/vnd.google-apps.folder Document application/vnd.google-apps.folder Image application/vnd.google-apps.folder Google フォト application/vnd.google-apps.folder My Tracks application/vnd.google-apps.folder $
まとめ
とりあえず認証が通ったなら、あとは各種APIを利用して、いろんなことができるので、おらワクワクすっぞ!
もう少しキチンとライブラリ的にまとめられたら独立したnpmとしてpublishするつもりですが、今の段階ではこの状態でごめんなさい。