nodejs サーバのWebサイトに Google IDによる 認証機能を実装する
参考
やること
Googleアカウントを使って、Webアプリにログインしたりログアウトしたり出来るようにしたい。
やる
基本
まずは、認証も何もないサーバを構築する。npm init
しよう。
package.json
の scripts
に "start": "node index.js"
を追加。
file-type
というモジュールがあると嬉しいので、次のコマンドを実行してインストール。
npm i --save file-type@7.7.1
index.js
というファイルを作って、次のように記述。
const http = require('http'); const fs = require('fs'); const filetype = require('file-type'); const server = http.createServer((request, response) => { // エラー処理 request.on('error', (e) => { console.error(e); response.statusCode = 500; response.end("500"); }); // エラー処理 response.on('error', (e) => { console.error(e); response.statusCode = 500; response.end("500"); }); if (request.url == "/") { // index.html fs.readFile('./public/index.html', (err, data) => { if (err){ response.statusCode = 404; response.end("404"); } else{ response.writeHead(200, filetype(request.url)); response.end(data); } }); } else { response.statusCode = 404; response.end("404"); } }).listen(8888, "localhost"); console.log("listen localhost:8888");
さらに public/index.html
というファイルを作る。中身は今は適当でおk。
ここまでで、npm run start
して、 localhost:8888/
にアクセスすると、public/index.html
が表示されるはず。
これでサーバが出来た。
とりあえず認証
事前準備として、Google認証するための、GoogleAPIのクライアントIDが必要。 GoogleAPIs →認証情報に移動して、認証情報作成→OAuth2.0クライアント ID作成から作ることが出来る。
https://console.developers.google.com/apis?authuser=0
クライアント側のHTMLを書く。認証については、殆どボタンを貼るだけ。
認証が完了すると、onSignIn
関数(g-signin2
ボタンの data-onsuccess
パラメータで変更可能?)が呼ばれる。
googleUser
が手に入るので、これを使って、名前やアイコン等が取得出来る。
サインアウトする時は、googleAuth
インスタンスを持ってきて、signOut()
関数を呼ぶだけ。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title> <meta name="google-signin-client_id" content="クライアントIDです.apps.googleusercontent.com"> <!-- cookieに関する何か(単語から適当に推測しただけでよく調べてないです) --> <meta name="google-signin-cookiepolicy" content="single_host_origin"> <!-- profile閲覧の権限 --> <meta name="google-signin-scope" content="profile"> <!-- 認証に必要 --> <script src="https://apis.google.com/js/platform.js" async defer></script> <script> // nameとimageurlをを書き換える function updateUserData(name, imageurl) { document.getElementById("username").value = name; document.getElementById("userimage").src = imageurl; } // googleのサインインが完了した時に呼び出される function onSignIn(googleUser) { console.log('Google Auth Response', googleUser); const profile = googleUser.getBasicProfile(); console.log('ID: ' + profile.getId()); // Do not send to your backend! Use an ID token instead. console.log('Full Name: ' + profile.getName()); console.log('Given Name: ' + profile.getGivenName()); console.log('Family Name: ' + profile.getFamilyName()); console.log("Image URL: " + profile.getImageUrl()); console.log("Email: " + profile.getEmail()); // 権限を与えていないのでmailは取得できないはず updateUserData(profile.getName(), profile.getImageUrl()); } // "sign out"ボタンを押したときに呼び出される function signOut(){ const googleAuth = gapi.auth2.getAuthInstance(); // googleAuthインスタンスを取得 googleAuth.signOut().then(()=>{ // signOut。Promiseが返ってくるので、thenで成功時のコードを記述。 console.log("success signout"); updateUserData("", ""); }); } </script> </head> <body> <h1> index.html </h1> <!-- https://apis.google.com/js/platform.js によっていい感じのボタンを生成してくれる--> <div class="g-signin2" data-onsuccess="onSignIn" data-theme="dark"></div> <div><button onclick="signOut()">sign out</button></div> <dl> <dt>name</dt> <dd><output id="username"></output></dd> <dt>image</dt> <dd><img src="" id="userimage"></dd> </dl> </body> </html>
サーバ側の認証
クライアントが認証したかどうかをサーバが把握するには、次の手順を踏む。
- クライアントがGoogleで認証。(今実装した)
- 認証結果を使って、id_tokenを取得。
- クライアントからサーバへid_tokenを送る。
- サーバは受け取ったid_tokenを検証する。
onSignIn
を書き換えて、id_tokenを送るところまでクライアント側に実装。
// googleのサインインが完了した時に呼び出される function onSignIn(googleUser) { console.log('Google Auth Response', googleUser); const profile = googleUser.getBasicProfile(); updateUserData(profile.getName(), profile.getImageUrl()); // 認証結果を使って、id_tokenを取得。 const id_token = googleUser.getAuthResponse().id_token; // クライアントからサーバへid_tokenを送る。 postIdToken(id_token, (text) => { console.log("server response", text}); } function postIdToken(callback) { const xhr = new XMLHttpRequest(); xhr.open('POST', 'http://localhost:8888/tokensignin'); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onload = () => { console.log("server response", xhr.responseText}; xhr.send('idtoken=' + id_token); }
あとは、サーバ側の検証の実装だけ。その前に、新たにライブラリが必要なので、次のコマンドを実行する。
npm install google-auth-library --save
受け取ったトークンの検証の実装は次のようになる。
const {OAuth2Client} = require('google-auth-library'); const CLIENT_ID = "クライアントIDです.apps.googleusercontent.com"; const oauthClient = new OAuth2Client(CLIENT_ID); // トークンの認証 async function verify(idToken) { const ticket = await oauthClient.verifyIdToken({ idToken: idToken, audience: CLIENT_ID }); const payload = ticket.getPayload(); // payloadからいくつかの情報を取得。 return { "userid": payload["sub"], // ユーザを識別するために使う。userIDを外部に公開するのはNG。 "username": payload['name'], "picture" : payload["picture"] }; }
/tokensignin
のPOSTクエリの処理部分を書く。
const server = http.createServer((request, response) => { // ... if (request.url == "/") { // ... } else if (request.url.substr(0,12) == "/tokensignin") { let data = ""; request.on("data", (chunk) => { data += chunk; }); // bodyを読み込む request.on("end", () => { // body が読み込み終わった const token_id = data; // クライアント側では body に token_id を載せたはず。 verify(token_id).then((user) => { // 検証が成功した response.writeHead(200, {"Content-Type": "text/plain"}); response.end("ok. Hello, " + user.username); console.log("verified: " + user.username); }).catch((err) => { // 検証が失敗した console.error(err); }); }); } else { // ... } }).listen(8888, "localhost");
サーバを再起動して、サーバ側にもユーザ名が表示されたら成功。
まとめ
あらためて、コード全体を掲載。
ファイル構成
public / index.html index.js package.json その他npmによって生成されたファイル
必要なライブラリは下記の通りです。
npm i --save file-type@7.7.1 npm install google-auth-library --save
index.js
const http = require('http'); const fs = require('fs'); const filetype = require('file-type'); const {OAuth2Client} = require('google-auth-library'); const CLIENT_ID = "クライアントIDです.apps.googleusercontent.com"; const oauthClient = new OAuth2Client(CLIENT_ID); // トークンの認証 async function verify(idToken) { const ticket = await oauthClient.verifyIdToken({ idToken: idToken, audience: CLIENT_ID }); const payload = ticket.getPayload(); // payloadからいくつかの情報を取得。 return { "userid": payload["sub"], // userIDを外部に公開するのはNG "username": payload['name'], "picture" : payload["picture"] }; } const server = http.createServer((request, response) => { // エラー処理 request.on('error', (e) => { console.error(e); response.statusCode = 500; response.end("500"); }); // エラー処理 response.on('error', (e) => { console.error(e); response.statusCode = 500; response.end("500"); }); if (request.url == "/") { // index.html fs.readFile('./public/index.html', (err, data) => { if (err){ response.statusCode = 404; response.end("404"); } else{ response.writeHead(200, filetype(request.url)); response.end(data); } }); } else if (request.url.substr(0,12) == "/tokensignin") { // tokensignin の処理 let data = ""; request.on("data", (chunk) => { data += chunk; }); // bodyを読み込む request.on("end", () => { // body が読み込み終わった const token_id = data; // クライアント側では body に token_id を載せたはず。 verify(token_id).then((user) => { // 検証が成功した response.writeHead(200, {"Content-Type": "text/plain"}); response.end("ok. Hello, " + user.username); console.log("verified: " + user.username); }).catch((err) => { // 検証が失敗した console.error(err); }); }); } else { response.statusCode = 404; response.end("404"); } }).listen(8888, "localhost"); console.log("listen localhost:8888");
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title> <meta name="google-signin-client_id" content="クライアントIDです.apps.googleusercontent.com"> <meta name="google-signin-cookiepolicy" content="single_host_origin"><!-- くっきー --> <meta name="google-signin-scope" content="profile"><!-- profile閲覧の権限 --> <script src="https://apis.google.com/js/platform.js" async defer></script> <script> function updateUserData(name, imageurl) { document.getElementById("username").value = name; document.getElementById("userimage").src = imageurl; } // googleのサインインが完了した時に呼び出される function onSignIn(googleUser) { console.log('Google Auth Response', googleUser); const profile = googleUser.getBasicProfile(); updateUserData(profile.getName(), profile.getImageUrl()); // 認証結果を使って、id_tokenを取得。 const id_token = googleUser.getAuthResponse().id_token; // クライアントからサーバへid_tokenを送る。 postIdToken(id_token, (text) => { console.log("server response", text); }); } function postIdToken(id_token, callback) { const xhr = new XMLHttpRequest(); xhr.open('POST', 'http://localhost:8888/tokensignin/'); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onload = () => { callback(xhr.responseText) }; xhr.send(id_token); } function signOut(){ const googleAuth = gapi.auth2.getAuthInstance(); googleAuth.signOut().then(()=>{ console.log("success signout"); updateUserData("", ""); }); } </script> </head> <body> <h1> index.html </h1> <div class="g-signin2" data-onsuccess="onSignIn" data-theme="dark"></div> <div><button onclick="signOut()">sign out</button></div> <dl> <dt>name</dt> <dd><output id="username"></output></dd> <dt>image</dt> <dd><img src="" id="userimage"></dd> </dl> </body> </html>