Github API を使って簡易アプリケーションを作ってみました。その成果物を紹介すると同時に、Github API そのものについても少しずつ紹介する内容を書きたいと思ってブログエントリを書き始めています。アプリケーションの紹介までは長くなりそうなのでテーマを分割して、今回はとりあえず準備段階を含めた OAuth ログインを紹介します。


【もともとやりたかったこと】
もともとは「ファイルサーバー的なものを作りたい」と思っていました。ファイルサーバー自体はアプリケーション開発テーマとしては(わざわざ新たに作る必要があるものとも思わず、Box とかを使えばいいので)既に枯れたテーマだと思っていますが、ファイルのバージョン管理機能など細かな使い勝手を意識して実装しようとすると既存のものだけでは難しそうだと判断しました。その技術検討の中で(本来の目的とは違う使い方であることを理解した上で)Git のリポジトリをファイルサーバーとみなすとバージョン管理ははじめからついているし、個人ごとのフォルダも個人ごとにブランチを作ればいけそうだし、管理者は管理者用ブランチにマージすればまとめて見れるし、自分の希望に近いことを実現できるのではないか? と思いついたのでした。

要するにファイルサーバーのバックエンドとして Git を使い、そのバックエンド部分を API で読み書きするようなフロントエンドをアプリケーションとして実装すればよい、ということになります。実現するための Git の API として上述の Github API の存在を知ったことでなんかできそうな目処がたったので、実際に作って検証してみた、という経緯です。


【Github API を使う準備】
これから作るアプリケーションでは Github API を使うため、まずは Github に API を利用するアプリケーションを登録して、OAuth 認証用の各種 ID を取得したり、コールバック URL を設定しておく、という準備段階を済ませておく必要があります。以下、その手順を紹介します。

ウェブブラウザで Github にログインし、右上のアイコンメニューから Settings を選択します:
2021051601


Settings 画面の左で Developer settings を選択します:
2021051602


今回作るアプリケーションは Github に外部ログインするウェブアプリケーションです。この外部ログインを実現するため OAuth App として登録する必要があります。左メニューで OAuth Apps を選択して、"New OAuth App" ボタン(初めて登録する場合は "Register a new application" ボタン)をクリックします:
2021051603


アプリケーション名(適当)とアプリケーション URL (適当、http://localhost:8080/ など)を指定後、コールバック URL を指定します。この値は OAuth 認証後にリダイレクトする先の URL となり、アプリケーション側ではこの URL にアクセスされた際のパラメータを見て(API 実行時に必要な)アクセストークンを取得・保管する処理が必要になります。なので、アプリケーション毎にこの処理を行う URL を定義する必要がありますが、後述のサンプルアプリケーションでは "http://localhost:8080/api/callback" に GET リクエストがあった時にこの処理を行う想定で作っています。そのためアプリケーション URL には http://localhost:8080/api/callback と入力してください。他の値はオプションなので適当に入力し、最後に "Register Application" ボタンをクリックします:
2021051604


※注意点として、Github API ではこのコールバック URL は OAuth アプリケーションごとに1つだけしか登録できないようです。今回は localhost 環境で動かす想定でコールバック URL を http://localhost:8080/api/callback と登録しましたが、実際に公開するサービスとして利用する場合は、その本番サーバーのホスト名やポート番号を使って指定する必要があります。本番利用時にはこの値を書き換えるか、あるいは本番サーバー用の OAuth アプリケーションを新たに登録し、そこで取得した値(後述)を使ってアプリケーションを動かしてください。


アプリケーションの登録が完了した直後の画面に、外部アプリケーションからの認証時に指定する必要のある各種情報が表示されます(client_secret は "Generate a new client secret" ボタンをクリックすることで表示されます)。このうちの client_idclient_secret の値を(後で使うので)メモしておきます。なお、client_id の値は忘れてしまった場合でも改めて OAuth Apps 一覧から選択することで再び参照することができますが、client_secret の値はこの画面を閉じてしまうと2度と参照することができません(client_id ごとリセットして取得し直す必要があります)。間違えないように正しく記録を残しておくように気をつけてください:
2021051605


これで Github API を使うための、OAuth App を登録する手順が完了しました。次はこの OAuth App を実際に作って動かす段階となりますが、その前に Github API を使って操作する Github リポジトリを用意しておきます。


【Github API で操作するリポジトリを用意する】
後述のサンプルアプリケーションでは「指定した特定の Github リポジトリ内の main ブランチに属しているファイルの一覧を取得する」ことが可能な実装をしています。そのための「特定の Github リポジトリ」を用意します。

単にファイル一覧を読み取るだけなので(変更を加えるわけではないので)、既存の Github リポジトリがあればそれを使ってもいいし、新規に Github リポジトリを作成した上で指定しても構いません。今回は dotnsf/my_githubapi_test という動作確認用リポジトリを作り、この中の main ブランチのファイル一覧を取得するようなアプリケーションとして以下の説明を続けます(実際にはみなさんも独自のリポジトリを作って、README.md 他を main ブランチに入れておいてください):
2021051606


ただ一点注意が必要です。上述の client_id / client_secret を取得した時に使った Github ユーザー ID と同じ ID でログインする場合はこのままでいいのですが、別の Github ユーザー ID で使いたい場合や、友人など別の Github ユーザー ID からも同アプリケーションを使わせたい場合など、(client_id / client_secret を取得した時とは)別の Github ユーザーにもこのアプリケーションを使わせたい場合、そのユーザーが対象のリポジトリを読み取るための Collaborator 権限設定が必要になります。 その場合は同リポジトリのオーナーでブラウザログインし、対象リポジトリを開いてから Settings メニューを選択します:
2021051607


そして画面左から Manage Access メニューを選択し(パスワードを聞かれます)、画面右の Manage Access 内の "Invite a collaborator" ボタンをクリックします:
2021051608


そして後述のアプリケーションを使わせたい Github ユーザーを指定して "Add **** to this repository" ボタンをクリックします:
2021051609


すると指定されたユーザーに招待メールが送信され、メール内の "View invitations" リンクから遷移して accept することで該当リポジトリに対する Collaborator 権限が付与され、リポジトリのオーナー以外のユーザーでも操作できるようになります:
2021051601



ここまでの作業で外部アプリケーションからログインするための設定と、操作対象リポジトリの準備ができました。ではサンプルのアプリケーションを使って、実際に Github API が動作する様子を体験してみます。


【Github の OAuth を使って外部アプリケーションから Github にログインする】
Node.js を使って Github API を実際に動かすサンプルアプリケーションを用意しました。Node.js 導入環境を使って、こちらから git clone するかダウンロード&展開してください:
https://github.com/dotnsf/github_oauth_sample


ソースコード内の settings.js ファイルを編集して、上述の準備段階で集めた情報を指定します。git clone 後かダウンロード後、ローカルシステムにある同ファイルをテキストエディタで開き、以下のように値を入力します(青字はコメント):
//. settings.js
exports.client_id = 'xxxxxxx'; OAuth App 作成時に取得した client_id の値
exports.client_secret = 'xxxxxxx'; OAuth App 作成時に取得した client_secret の値
exports.callback_url = 'http://localhost:8080/api/callback'; OAuth App 作成時に指定したコールバック URL の値
exports.repo_name = 'dotnsf/my_githubapi_test';  操作対象リポジトリ名

この状態で npm install を指定して依存ライブラリを導入してから node app でアプリケーションを起動します:
$ npm install

$ node app

起動に成功すると、このサンプルアプリケーションは 8080 番ポートで HTTP リクエストを待ち受けます。ウェブブラウザで http://localhost:8080/ にアクセスします:
2021051601


最初はログイン前なので "login" ボタンが表示されています。このボタンをクリックすると Github API を使った OAuth ログインの処理がスタートします。一度もログインしたことがない場合は以下のような同意画面が表示されるので、ユーザー名を確認後に "Authorize ***" をクリックしてください:
E1ewH_vVkAAwHHE


すると再度 http://localhost:8080/ に転送されますが、今度はログイン後なのでユーザー情報を取得することができ、ログインした Github ユーザーのアバターアイコンやユーザー ID が画面右上に表示されます。このアイコンが自分の Github ユーザーアイコンであることを確認してください。このアイコンをクリックしてログアウトすることもでき、ログアウトすると再度 login ボタンが表示されます:
2021051602


とりあえず、Github API を使ったログイン処理を実装することができました。以下、アプリケーションのログイン部分の仕組みを解説します。


【Github の OAuth ログインの仕組み】
詳しくはサンプルアプリケーションのソースコード内 api/github.js を見ていただきたいのですが、正確には「ログイン認証の仕組み」というよりは「ログイン認証してアクセストークンを取得する仕組み」です。

まずログイン認証の仕組みは OAuth を使っています。アプリケーションで "login" ボタンをクリックすると、https://github.com/login/oauth/authorize にリダイレクトしています。その際に URL パラメータに付与する形で settings.js 内に記載した client_id の値と callback_url の値を指定しています:
router.get( '/login', function( req, res ){
  //. GitHub API V3
  //. https://docs.github.com/en/developers/apps/authorizing-oauth-apps
  res.redirect( 'https://github.com/login/oauth/authorize?client_id=' + settings.client_id + '&redirect_uri=' + settings.callback_url + '&scope=repo' );
});


このリダイレクト先で上述の Github の認証を行います:
E1ewH_vVkAAwHHE


ここで Authorize するとコールバック URL に転送されます。その際に一時的なアクセストークンが code パラメータに指定される形で送付されてくるので、これを取り出した上で client_id や client_secrent などの値と一緒に https://github.com/login/oauth/access_token に POST リクエストを発行します。その実行結果(form encoded形式)から access_token 変数としてアクセストークンを取り出すことができるので、(本サンプルアプリケーションではセッション内に記録する形で)保存して、この後の Github API 実行時に指定できるようにしています。その上で改めて GetMyInfo() 関数を実行してログイン情報を取得し(詳しくは次回に)トップページにリダイレクトしています:
router.get( '/callback', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var code = req.query.code;
  var option = {
    url: 'https://github.com/login/oauth/access_token',
    form: { client_id: settings.client_id, client_secret: settings.client_secret, code: code, redirect_uri: settings.callback_url },
    method: 'POST'
  };
  request( option, async function( err, res0, body ){
    if( err ){
      console.log( { err } );
    }else{
      //. body = 'access_token=XXXXX&scope=YYYY&token_type=ZZZZ';
      var tmp1 = body.split( '&' );
      for( var i = 0; i < tmp1.length; i ++ ){
        var tmp2 = tmp1[i].split( '=' );
        if( tmp2.length == 2 && tmp2[0] == 'access_token' ){
          var access_token = tmp2[1];

          req.session.oauth = {};
          req.session.oauth.token = access_token;

          var r = await GetMyInfo( access_token );
          if( r ){
            req.session.oauth.id = r.id;
            req.session.oauth.login = r.login;
            req.session.oauth.name = r.name;
            req.session.oauth.email = r.email;
            req.session.oauth.avatar_url = r.avatar_url;
          }
        }
      }
    }
    //console.log( 'redirecting...' );
    res.redirect( '/' );
  });
});

トップページでは画面ロード直後に GET /api/isLoggedIn という API が実行されます。この API はセッション内にアクセストークンがあるかどうかを調べ、含まれていた場合はその情報(ログイン時に GetMyInfo() 関数によって取り出したユーザー情報)をレスポンスの値として返します。つまりログインしていない場合は false 値が、ログインしている場合はユーザー情報を返すという関数です。これによって、未ログイン時のトップページでは login ボタンを、ログイン時のトップページでは logout ボタンをそれぞれ表示するようにしています:
router.get( '/isLoggedIn', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var status = false;
  if( req.session && req.session.oauth && req.session.oauth.token ){
    status = JSON.parse( JSON.stringify( ( req.session.oauth ) ) );
  }

  if( !status ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false }, null, 2 ) );
  }else{
    res.write( JSON.stringify( { status: true, user: status }, null, 2 ) );
  }
  res.end();
});

なお、logout ボタンが押された場合はセッションの中身を空にする、という処理が実行されます。これによって取得したアクセストークンは無効になり、(再ログインによって)再びアクセストークンを取得するまで Github API は実行できなくなります:
router.post( '/logout', function( req, res ){
  if( req.session.oauth ){
    req.session.oauth = {};
  }
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

と、Github API を使ったログイン時の処理内容を中心に紹介しました。次回は GetMyInfo() 関数でユーザー情報を取り出す仕組みと、ログイン後に main ブランチ内のファイル一覧を取り出す仕組みを紹介する予定です。


(2021/05/18 追記 続きはこちらこちら