見出し画像

node.js からgoogleapisを利用しgoogleでログインを実装する。

node.jsのexpressを利用しプロトタイプのWebアプリを開発中ですが、googleでログインを実装したくその方法をまとめました。Webアプリ内で様々なgoogleサービスを利用する場合、ログインしてから時間を開けたら認証が切れるのでrefresh tokenを利用して認証を続ける必要があります。しかし、ネット上の情報を参考しても上手くいかない穴が存在します。ここではその解決策も書いてありますので良く読んでみてください。

googleログインはgoogle側に認証を要求し、認証に問題なければgoogleログイン画面が表示され、ログインしたユーザーのgoogle側の情報を返すのでその情報を信頼しログインを許可する仕組みです。
認証を行うためにはgoogleが開発者に提供する認証クライアントIDとシークレットキーを取得する必要があります。

Google Developers Consoleからプロジェクトを作成する

google認証に必要である認証クライアントIDとシークレットキーを取得するためGoogle Developers Consoleを開きます。

お持ちのgoogleアカウントでログインしてからロゴの右側にあるプロジェクト名を選択後、表示される「+」ボタンでプロジェクトを新規作成します。

画像1

プロジェクト名にgoogleログインを利用するウェブアプリなどの名前を入力し、「作成」をクリックします。

画像4

プロジェクトが作成できたら通知の「プロジェクトを選択」をクリックしプロジェクトを選択しておきましょう。

画像5

クライアントIDとシクレットキーを取得する

操作したいAPIを選択します。youtube analytics apiを利用する予定なので「youtube」で検索しYouTube Analytics APIを選択ます。

画像3

「有効にする」をクリックし、APIを有効にします。

画像4

「認証情報を作成」をクリックします。

画像6

「プロジェクトへの認証情報の追加」画面が表示されますので、「使用するAPI」と「APIを呼び出す場所」と「アクセスするデータの種類」を選択し「必要な認証情報」をクリックします。

画像7

「OAuth同意画面の設定」が表示されますので「同意画面を設定」をクリックします。

画像8

User Typeを選択し「作成」をクリックします。

画像9

アプリ登録の編集画面が表示されますので、「アプリ名」と「ユーザーサポートメール」と「デベロッパーの連絡先情報」の「メールアドレス」を入力し、「保存して次へ」をクリックします。他の項目は後で入力可能です。

画像10

画像11

「スコープを追加または削除」をクリックします。

画像12

必要なスコープを選択し、「更新」をクリックします。

画像13

画像14

「非機密スコープ」に選択したスコープが表示されます。問題なかったら「保存して次へ」をクリックします。

画像15

画像16

テストに参加するユーザーを追加し、「保存して次へ」をクリックします。

画像17

今まで入力した内容が表示されますので間違いがないか確認してから「ダッシュボードへ戻る」をクリックします。

画像18

左メニューの「認証情報」をクリックし認証情報画面を表示します。「+認証情報を作成」をクリックし、「OAuth クライアントID」を選択します。

画像19

アプリケーション種類には「ウェブアプリケーション」を名前には適切な名前を入力します。

画像20

下にスクロールして「承認済みのJavaScript生成元」にはウェブアプリケーションをユーザーに提供するurlを追加します。
「承認済みのリダイレクトurl」にはユーザーが認証時に表示されるgoogleログインフォームや許可フォームから認証済み後にリダイレクトされるウェブアプリケーションのurlを入力します。(例:https://www.example.com)
上記の「承認済みのJavaScript生成元」のurlのサブディレクトリになるはずです。(例:https://www.example.com/youtube_redirect)

画像21

作成完了メッセージが出ますので「クライアントID」と「クライアントシークレット」をメモ帳などにメモして置きます。

画像22

ウェブアプリケーション構築のためライブラリをインストールする

まずはウェブアプリケーションを構築するのに必要なライブラリをインストールします。

$ npm install express --save
$ npm install express-session --save
$ npm install ejs --save
$ npm install body-parser --save
$ npm install cors --save
$ npm install googleapis --save
$ npm install google-auth-library --save

ウェブアプリケーション起動スクリプトを作成する

ウェブアプリケーションの起動スクリプトserver.jsを作成します。
重要なコードの説明はコメント(//)のところを読んでください。

// アプリケーションに使われるライブラリーを呼び出す。
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var session = require('express-session');
var fs = require("fs");
var path = require('path');
var cors = require('cors');
var http = require('http').Server(app); 

// ウェブアプリケーションの初期環境の設定を行う。
// htmlテンプレートエンジンはejsを使い、ejsファイルはviewsフォルダーに保管する。
app.set('views',__dirname+'/views');
app.set('view engine','ejs');
app.engine('html', require('ejs').renderFile);
// フロント側のcss、javascript、イメージファイルはpublicフォルダーに保管する。
app.use(express.static('public'));
// 他のドメインへのリソース要請を許可する。
app.use(cors());
// リクエスト・レスポンス本文のjson及びurlencodeを行う
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:true}));
// セッション関連設定を行う
app.use(session({
secret: '@#@$MYSIGN#@$#$',
resave: false,
saveUninitialized: true
}));

// サーバを起動し待ち受ける
var server = http.listen(3005,function(){
   console.log("Express server has started on port 3005");
});

// あるフォルダーにあるファイルはルートファイルとして呼び出す
function recursiveRoutes(folderName) {
   fs.readdirSync(folderName).forEach(function(file) {
       var fullName = path.join(folderName, file);
       var stat = fs.lstatSync(fullName);
       if (stat.isDirectory()) {
           recursiveRoutes(fullName);
       } else if (file.toLowerCase().indexOf('.js')) {
           require('./' + fullName)(app, fs, passport);
           console.log("require('" + fullName + "')");
       }
   });
}

// routerフォルダーにあるファイルをルートファイルとして呼び出す
recursiveRoutes('router'); 

ルーターファイルを作成する

routerフォルダーを作成し、メインルーターファイルであるmain.jsを作成します。ここで注意することはgenerateAuthUrlのオプションでaccess_typeを'offline'にする場合refresh_tokenが取得できるようですが、一番最初の一回のみであり、テスト途中には後になってrefresh_tokenを利用したくてもその時には取得ができなくて困る可能性があります。その場合は、approval_prompt:'force'オプションを一緒に使うことで毎回refresh_tokenを取得することができます。

// googleapisライブラリを呼び出す
var google = require('googleapis');
// 認証後使えるgoogleサービススコープを決める
var SCOPES = ['https://www.googleapis.com/auth/youtube.readonly',
             'https://www.googleapis.com/auth/youtubepartner-channel-audit',
             'https://www.googleapis.com/auth/userinfo.profile',
             'https://www.googleapis.com/auth/userinfo.email',
           ];
// このファイルを外部から呼び出すためexport
module.exports = function(app, fs)
{
  var clientId = 'Google Developers Consoleから取得したクライアントID';
  var clientSecret = 'Google Deveplopers Coonsoleから取得したシクれっとキー';
  var redirectLoginUrl = 'Google Developers Consoleへ登録したリダイレクトurl';
  // oauth2インスタンスを生成
  var oauth2LoginClient = new google.auth.OAuth2(
                                  clientId, 
                                  clientSecret, 
                                  redirectLoginUrl);
  // このアプリのurl/loginへアクセスした時の処理
  app.get('/login',function(req,res){
   // login.ejsを探し、htmlをレンダーリングして返す
   res.render('login', {
     title: title
   })
  });
  
  // このアプリのurl/googleloginへPOSTアクセスした時の処理
  app.get('/gogooglelogin',function(req, res){
   // google認証ページをurlを取得する
   var url = oauth2LoginClient.generateAuthUrl({
     // 'online' (default) or 'offline' (gets refresh_token)
     access_type: 'offline',
     approval_prompt:'force', 
     // If you only need one scope you can pass it as a string
     scope: SCOPES,
     redirect_uri:redirectLoginUrl,
   });
   console.log('Authorize this app by visiting this url: ', url);
   // 取得したgoogle認証ページへリダイレクト
   res.redirect(url);
 });
 
 // google認証後こちらにリダイレクトされる
 app.get('/youtubeapi_redirect', function(req, res){
   // urlパラメターからcode情報を取得する
   var code = req.query.code;
   console.log('code:'+code);
   // code情報からアクセストークンを取得する
   oauth2LoginClient.getToken(code, function(err,token){
     if (err) {
       console.log('Error while trying to retrieve access token', err);
       return;
     }
     console.log("token : " + JSON.stringify(token));
     // oauthインスタンスにトークンをセットする
     oauth2LoginClient.credentials = token;
     // googleからユーザー情報を取得する
     var oauth2 = google.oauth2({
       auth:oauth2LoginClient,
       version:'v2'
     });
     oauth2.userinfo.get(
       function(err,resp){
         if(err){
           console.log(err);
         } else {
           console.log(resp);
           
           //ログイン成功時の処理を書く
           return res.send("ログイン成功");
         }
       }
     );
     
   });
 });
}

フロント側のhtmlテンプレートファイルを作成する

次はviewsフォルダーを作成し、その下にlogin.ejsを作成します。

<!DOCTYPE html>
<html>
<head>
 <meta charset="UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="generator" content="picktube.net">
 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
 <link rel="shortcut icon" href="/assets/images/group-1icon.png" type="image/x-icon">
 <meta name="description" content="">
 
 
 <title>Picktube</title>
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
 <link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">
 <style>
   body{
       font-family:'Meiryo UI','メイリオ', 'Meiryo', sans-serif;
   }
   #sidebar{
       width:100px;
   }
   @media (max-width:576px) {
       #sidebar{
           width:100%;
       }

       .navbar .input-group{
           width:50%;
       }

       .navbar-nav{
           display:block;
           flex-direction:unset;
       }

       
       
   }
   .btn-circle.btn-xl {
       width: 70px;
       height: 70px;
       padding: 10px 16px;
       border-radius: 35px;
       font-size: 24px;
       line-height: 1.33;
   }

   .btn-circle {
       width: 30px;
       height: 30px;
       padding: 6px 0px;
       border-radius: 15px;
       text-align: center;
       font-size: 12px;
       line-height: 1.42857;
   }
 </style>
 <style>
       .login-block {
           margin: 30px auto;
           min-height: 93.6vh
       }
       .login-block .auth-box {
           margin: 20px auto 0;
           max-width: 450px !important
       }
       .card {
           border-radius: 5px;
           -webkit-box-shadow: 0 0 5px 0 rgba(43, 43, 43, .1), 0 11px 6px -7px rgba(43, 43, 43, .1);
           box-shadow: 0 0 5px 0 rgba(43, 43, 43, .1), 0 11px 6px -7px rgba(43, 43, 43, .1);
           border: none;
           margin-bottom: 30px;
           -webkit-transition: all .3s ease-in-out;
           transition: all .3s ease-in-out;
           background-color: #fff
       }
       .card .card-block {
           padding: 1.25rem
       }
       .f-80 {
           font-size: 80px
       }
       .form-group {
           margin-bottom: 1.25em
       }
       .form-material .form-control {
           display: inline-block;
           height: 43px;
           width: 100%;
           border: none;
           border-radius: 0;
           font-size: 16px;
           font-weight: 400;
           padding: 9px;
           background-color: transparent;
           -webkit-box-shadow: none;
           box-shadow: none;
           border-bottom: 1px solid #ccc
       }
       .btn-md {
           padding: 10px 16px;
           font-size: 15px;
           line-height: 23px
       }
       .btn-primary {
           background-color: #4099ff;
           border-color: #4099ff;
           color: #fff;
           cursor: pointer;
           -webkit-transition: all ease-in .3s;
           transition: all ease-in .3s
       }
       .btn {
           border-radius: 2px;
           text-transform: capitalize;
           font-size: 15px;
           padding: 10px 19px;
           cursor: pointer
       }
       .m-b-20 {
           margin-bottom: 20px
       }
       .btn-md {
           padding: 10px 16px;
           font-size: 15px;
           line-height: 23px
       }
       .heading {
           font-size: 21px
       }
       #infoMessage p {
           color: red !important
       }
       .btn-google {
           color: #545454;
           background-color: #ffffff;
           box-shadow: 0 1px 2px 1px #ddd
       }
       .or-container {
           align-items: center;
           color: #ccc;
           display: flex;
           margin: 25px 0
       }
       .line-separator {
           background-color: #ccc;
           flex-grow: 5;
           height: 1px
       }
       .or-label {
           flex-grow: 1;
           margin: 0 15px;
           text-align: center
       }
 </style>
</head>
<body>
 <section class="login-block">
   <div class="container-fluid">
       <div class="row">
           <div class="col-sm-12">
               <form class="md-float-material form-material">
                   <div class="auth-box card">
                       <div class="card-block">
                           <div class="row">
                               <div class="col-md-12">
                                   <h3 class="text-center heading">ログインしてください。</h3>
                               </div>
                           </div>
                           <% if(typeof error !== 'undefined' && error){ %>
                               <div class="alert alert-success" role="alert">
                                   <%= error %>
                               </div>
                           <% } %>
                           <div class="row">
                               <div class="col-md-12"> <a class="btn btn-lg btn-google btn-block text-uppercase btn-outline w-100" href="/gogooglelogin"><img src="https://img.icons8.com/color/16/000000/google-logo.png"> Googleでログイン</a> </div>
                           </div>
                           
                           <div class="or-container">
                               <div class="line-separator"></div>
                               <div class="or-label">or</div>
                               <div class="line-separator"></div>
                           </div>
                           <div class="form-group form-primary"> <input type="text" class="form-control" name="email" value="" placeholder="メールアドレス" id="email"> </div>
                           <div class="form-group form-primary"> <input type="password" class="form-control" name="password" placeholder="パスワード" value="" id="password"> </div>
                           <div class="row">
                               <div class="col-md-12"> <input type="button" id="btn-login" class="btn btn-primary btn-md btn-block waves-effect text-center m-b-20 w-100" value="ログイン"> <!-- <button type="button" class="btn btn-primary btn-md btn-block waves-effect text-center m-b-20"><i class="fa fa-lock"></i> Signup Now </button> -->
                               </div>
                           </div>
                            <br>
                           <p class="text-inverse text-center">アカウントを持っていないですか? <a href="/signup" data-abc="true">会員登録</a></p>
                       </div>
                   </div>
               </form>
           </div>
       </div>
   </div>
</section>
<script
 src="https://code.jquery.com/jquery-3.6.0.min.js"
 integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
 crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.1/dist/umd/popper.min.js" integrity="sha384-SR1sx49pcuLnqZUnnPwx6FCym0wLsk5JZuNx2bPPENzswTNFaQU1RDvt3wT4gWFG" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.min.js" integrity="sha384-j0CNLUeiqtyaRmlzUHCPZ+Gy5fQu0dQ6eZ/xAww941Ai1SxSY+0EQqNXNE6DZiVc" crossorigin="anonymous"></script>
<script>
     $("#btn-login").click(function(){
       var email = $("#email").val();
       var password = $("#password").val();
       if(email === ""){
           alert("メールアドレスを入力してください。");
           return;
       }
       if(password === ""){
           alert("パスワードを入力してください。");
           return;
       }
       $.ajax({
             url:'/gologin',
             type:'POST',
             dataType:'json',
             data:{email:email, password:password}
         }).done(function(data){
             if(data.success == 0){
                 alert(data.error);
                 return;
             }else{
                 location.href="/";
                 return;
             }
         }).fail(function(XMLHttpRequest, textStatus, errorThrown){
             alert("予期しないエラーが発生しました。");
         });
     });
</script>
</body>
</html>

ウェブアプリケーションを起動し確認する

ここまで作成できたら、server.jsを起動します。

$ node server.js

起動出来たらhttps://アプリのドメイン/loginへアクセスします。

画像23

GOOGLEでログインボタンをクリックしてみましょう。自分のgoogleアカウントを選択します。

画像24

パスワードを入力し「次へ」をクリックます。

画像25

もし、以下のエラーが表示される場合は、

画像26

Google Developers Consoleから作成した今のプロジェクトの「OAuth同意画面」から「テストユーザー」を追加します。

画像27

アプリのログイン画面に戻り、もう一度GOOGLEでログインを実行します。
先のエラーの代わりに以下の画面が出たら、Continueをクリックします。この画面はテストの時のみ表示されるはずです。Google Developers Consoleからプロジェクトを公開したら表示されなくなるはずです。

画像28

権限付与の画面が出たら「許可」をクリックします。
許可が必要な数分表示されますので全て許可にします。

画像29

最終的に同意チェック画面が出るので「許可」をクリックします。

画像30

ブラウザーにログイン成功時の画面が表示されたら成功です。

画像31

これでgoogleログイン機能の実装は完了です。

この記事が気に入ったらサポートをしてみませんか?