Google Apps ScriptでGitHub Appsの認証をする


はじめに

Google Apps Script(GAS)でGitHub Appsの認証をする方法について説明します。

だいぶ前にやったことなんですが、役に立つ人がいるかもしれないのでメモとして残しておきます。

準備

GitHub Appsの設定画面でpemファイルをダウンロードして、 openssl pkey -in pemファイル の出力をGASのユーザープロパティとかスクリプトプロパティとかに設定しておく1

コード

少し長いのですが、GitHub Appsの認証を行う関数です。

function getAccessToken() {
  const props = PropertiesService.getUserProperties();
  const token = props.getProperty('token');

  // プロパティが存在しなかったら new Date(null) になって1970/1/1 9:00になる
  const expiration = new Date(props.getProperty('expiration'));

  if (token !== null && new Date().valueOf() < expiration.valueOf()) {
    return token;
  }

  const privateKey = props.getProperty('privateKey');

  const header = { alg: 'RS256', typ: 'JWT' };
  const payload = {
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 5 * 60,
    iss: 0 /* App IDを入れる */,
  };

  const toSign = Utilities.base64EncodeWebSafe(JSON.stringify(header)) + '.' + Utilities.base64EncodeWebSafe(JSON.stringify(payload)).replace(/=+$/, '');
  const signatureBytes = Utilities.computeRsaSha256Signature(toSign, privateKey);
  const signature = Utilities.base64EncodeWebSafe(signatureBytes).replace(/=+$/, '');

  const jwt = `${toSign}.${signature}`;

  const installationsResponse = UrlFetchApp.fetch('https://api.github.com/app/installations', {
    method: 'get',
    headers: {
      accept: 'application/vnd.github.machine-man-preview+json',
      authorization: `Bearer ${jwt}`,
    },
  });
  const installations = JSON.parse(installationsResponse.getContentText());
  const installationId = installations.find(v => v.account.login === 'XXX' /* 適当な条件で判定する */).id;

  const accessTokenResponse = UrlFetchApp.fetch(
    `https://api.github.com/app/installations/${installationId}/access_tokens`, {
    method: 'post',
    headers: {
      accept: 'application/vnd.github.machine-man-preview+json',
      authorization: `Bearer ${jwt}`,
    },
  });
  const accessTokenJson = JSON.parse(accessTokenResponse.getContentText());
  const accessToken = accessTokenJson.token;

  props.setProperties({
    expiration: accessTokenJson.expires_at,
    token: accessToken,
  });

  return accessToken;
}

この関数は

  • アクセストークンを1度も取得したことがなかったらアクセストークンを取得して返す
  • アクセストークンが取得済みで、期限が切れていなかったらそのトークンを返す
  • アクセストークンが取得済みで、期限が切れていたら、アクセストークンを取得し直して返す

というのをしているので、アクセストークンが欲しい場面で毎回この関数を呼ぶようにするといい感じになります。こんな感じです。

UrlFetchApp.fetch('https://api.github.com/graphql', {
  method: 'post',
  contentType: 'application/json',
  headers: {
    Authorization: `Bearer ${getAccessToken()}`,
  },
  payload: ...
});

どういう流れでアクセストークンを取得しているのかは、調べると出てくると思うので省略します。


  1. 他の人から見えない置き方をしていればなんでもいいです。