shonen.hateblo.jp

やったこと,しらべたことを書く.

nodejs サーバのWebサイトに Google IDによる 認証機能を実装する

参考

developers.google.com

developers.google.com

やること

Googleアカウントを使って、Webアプリにログインしたりログアウトしたり出来るようにしたい。

やる

基本

まずは、認証も何もないサーバを構築する。npm init しよう。

package.jsonscripts"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>