JavaScript カスタマイズで Microsoft 製品連携 - kintone編

著者名:竹内 能彦(サイボウズ株式会社)

目次

caution
警告

2020 年 8 月改訂のセキュアコーディング ガイドライン に抵触する内容が含まれています。認証情報が漏洩した場合の影響を考慮して慎重に検討してください。
該当箇所:外部ライブラリ(MSAL.js)内のアクセストークン保存部分

はじめに

kintone と Microsoft 製品を連携する場合は認証が必要で、認証方式はそのプラットフォームによって変わります。
プラットフォームを大きく分けると以下の 2 種類が考えられます。

  • Web ブラウザー
  • サーバーやアプリケーション

本記事では、Web ブラウザーで Microsoft 製品との連携に必要となる認証の方法を説明します。 認証方法は ユーザーのサインインを処理して、JavaScript シングルページアプリケーション から Microsoft Graph API を呼び出す (External link) で詳しく紹介されています。
本記事は kintone の JavaScript カスタマイズに特化した内容です。

また、認証を通過して Microsoft 製品と連携できることを確認するために、例として Entra ID のログイン情報を取得します。
取得には Microsoft Graph API (External link) を利用します。
Microsoft Graph API を使えば Microsoft 365 の Outlook や Entra ID、OneDrive などと連携できます。

サーバーやアプリケーション上での認証方法紹介も予定していますので、今後の記事にご期待ください。

Microsoft 連携の実践編として Outlook 連携 - kintone から Outlook メールの送受信をしよう を公開しています。
こちらも合わせて確認してください。

概要

やることは 2 つです。

Entra ID 認証

kintone の一覧画面にログインボタンを表示します。

ログインボタンをクリックすると、Entra ID の認証画面を表示します。
初回はアクセス許可の確認画面が表示されます。

Microsoft Graph API の実行

認証に成功すると、「ユーザー情報取得」ボタンと「ログアウト」ボタンを表示します。
「ユーザー情報取得」ボタンをクリックすると、 Microsoft Graph API のユーザー取得 (External link) を実行し、ユーザープリンシパル名を表示します。
ユーザープリンシパル名とは AD のユーザーを一意に識別するもので、形式は「アカウント名@ドメイン名」になります。

利用するライブラリ

Microsoft Authentication Library (External link)

Microsoft 製品の認証には OAuth 2.0 が利用されており、JavaScript で OAuth 2.0 認証を実現するライブラリです。
参考)認証ライブラリは Microsoft ID プラットフォームの認証ライブラリ (External link) にまとめられています。

SweetAlert 2 (External link)

スタイリッシュなポップアップを表示するライブラリです。
今回の認証には直接関係ありませんが、見た目をよくするために利用しています。

設定

kintone のアプリ ID が Microsoft の設定で必要になり、Microsoft のアプリケーション ID が kintone の設定で必要になります。
そのため、kintone → Micorsoft → kintone の順で設定します。

1. kintone のアプリ作成

まずは kintone アプリを作成します。
フィールドは利用しないのでフィールドなしの kintone アプリを作りましょう。

kintone アプリ ID が Microsoft の設定で必要になるのでメモします。
kintone アプリ ID は URL から確認できます。先ほど作成したアプリを開きます。
その URL が「https://{subdomain}.cybozu.com/k/944/」の場合、「944」が kintone アプリ ID になります。

2. Microsoft の設定

Microsoft ID プラットフォームにアプリケーションを登録する (External link) の手順に従い Microsoft アプリを登録します。

ポイントは以下になります。

  • プラットフォームの追加では Web を選択します。
  • 「暗黙的フローを許可する」にチェックします。
  • リダイレクト URL には「https://{subdomain}.cybozu.com/k/{kintone アプリ ID}/」を入力します。
  • Microsoft Graph のアクセス許可を設定する必要はありません。
    OAuth 2.0 ではログイン時にアクセス許可を要求できるからです。
    詳細は ユーザーの同意 (External link) を確認してください。
    テナント全体の同意を設定し、同意ページを表示させない方法も記載されています。

下記アプリケーション ID は kintone の JS 設定で利用します。メモしましょう。

3. kintone の設定

JavaScript / CSS を設定します。
「アプリの設定 > JavaScript / CSS でカスタマイズ」の「PC 用の CSS ファイル」に以下の URL を設定します。

  • https://js.cybozu.com/sweetalert2/v7.3.5/sweetalert2.min.css

「アプリの設定 > JavaScript / CSS でカスタマイズ」の「PC 用の JavaScript ファイル」に以下の URL/ファイルを設定します。

  • https://js.cybozu.com/jquery/3.2.1/jquery.min.js
  • https://secure.aadcdn.microsoftonline-p.com/lib/0.1.5/js/msal.min.js
  • https://js.cybozu.com/sweetalert2/v7.3.5/sweetalert2.min.js

以下のサンプルコードをエディタにコピーして、ファイル名を「sample.js」、文字コードを「UTF-8N」で保存します。
12 行目に「4.2 Microsoft の設定」でメモしたアプリケーション ID を設定し、アップロードします。
ファイル名は任意ですが、ファイルの拡張子は「js」にしてください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/**
 * MS製品との認証用のサンプルプログラム
 *
 * Copyright (c) 2018 Cybozu
 *
 * Licensed under the MIT License
 */
jQuery.noConflict();
(function($) {
  'use strict';

  const CLIENT_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; // アプリケーションID
  const API_SCOPES = ['User.Read'];

  const sampleCustomize = {
    setting: {
      ui: {
        buttons: {
          logIn: {
            id: 'sample-login',
            text: 'ログイン'
          },
          logOut: {
            id: 'sample-logout',
            text: 'ログアウト'
          },
          getUserInfo: {
            id: 'sample-getuserinfo',
            text: 'ユーザー情報取得'
          }
        }
      }
    },

    data: {
      ui: {}
    },

    uiCreate: function(kintoneHeaderSpace) {
      this.data.ui.headerLogIn = document.createElement('div');
      this.data.ui.headerLogOut = document.createElement('div');

      this.data.ui.btnLogIn = this.createButton(this.setting.ui.buttons.logIn);
      this.data.ui.btnLogOut = this.createButton(this.setting.ui.buttons.logOut);
      this.data.ui.btnGetUserInfo = this.createButton(this.setting.ui.buttons.getUserInfo);

      this.data.ui.headerLogIn.append(this.data.ui.btnLogIn);
      this.data.ui.headerLogOut.append(this.data.ui.btnGetUserInfo, this.data.ui.btnLogOut);
      kintoneHeaderSpace.append(this.data.ui.headerLogIn, this.data.ui.headerLogOut);
    },

    createButton: function(param) {
      const button = document.createElement('button');
      button.id = param.id;
      button.className = 'kintoneplugin-button-normal';
      button.innerHTML = param.text;
      button.style.margin = '0px 0px 10px 20px';
      return button;
    },

    showLoading: function() {
      swal({
        title: '処理中です。',
        type: 'info',
        allowOutsideClick: false,
        allowEscapeKey: false,
        showConfirmButton: false
      });
    },

    closeLoading: function() {
      swal.close();
    }
  };

  const graphApi = {
    clientId: CLIENT_ID,
    apiScopes: API_SCOPES,
    userAgentApplication: null,

    init: function() {
      this.userAgentApplication = new Msal.UserAgentApplication(this.clientId, null, (
        errorDes, token, error, tokenType) => {});

      const user = this.userAgentApplication.getUser();
      if (!user) {
        sampleCustomize.data.ui.headerLogIn.style.display = 'inline-block';
        sampleCustomize.data.ui.headerLogOut.style.display = 'none';
      } else {
        sampleCustomize.data.ui.headerLogIn.style.display = 'none';
        sampleCustomize.data.ui.headerLogOut.style.display = 'inline-block';
      }
    },

    logIn: function() {
      sampleCustomize.showLoading();
      const self = this;
      self.userAgentApplication.loginPopup(this.apiScopes).then((idToken) => {
        sampleCustomize.data.ui.headerLogIn.style.display = 'none';
        sampleCustomize.data.ui.headerLogOut.style.display = 'inline-block';
        sampleCustomize.closeLoading();
      }, (error) => {
        swal('ERROR!', 'ログインできませんでした。', 'error');
        sampleCustomize.closeLoading();
      });
    },

    logOut: function() {
      this.userAgentApplication.logout();
    },

    getAccessToken: function() {
      sampleCustomize.showLoading();
      const self = this;

      return self.userAgentApplication.acquireTokenSilent(self.apiScopes).then((accessToken) => {
        return accessToken;
      }, () => {
        return self.userAgentApplication.acquireTokenPopup(self.apiScopes).then((accessToken) => {
          return accessToken;
        }, (error) => {
          sampleCustomize.closeLoading();
          swal('ERROR!', error, 'error');
          return false;
        });
      }).catch(() => {
        sampleCustomize.closeLoading();
        swal('ERROR!', 'エラーが発生しました。', 'error');
        return false;
      });
    },

    getUserInfo: function(token) {
      if (!token) {
        return;
      }

      const url = 'https://graph.microsoft.com/v1.0/me';
      const header = {
        Authorization: 'Bearer ' + token,
        'Content-Type': 'application/json'
      };

      kintone.proxy(url, 'GET', header, {}).then((response) => {
        const responseJson = window.JSON.parse(!response[0] ? '{}' : response[0]);
        const userPrincipalName = responseJson.userPrincipalName;
        sampleCustomize.closeLoading();
        if (typeof responseJson.error !== 'undefined') {
          swal('ERROR!', responseJson.error.message, 'error');
        } else {
          swal('SUCCESS!', 'あなたのユーザープリンシパル名は ' + userPrincipalName + ' です。', 'success');
        }
      }).catch((error) => {
        swal('ERROR!', 'ユーザー情報の取得に失敗しました。', 'error');
      });
    }
  };


  // レコード一覧画面の表示時
  kintone.events.on('app.record.index.show', (event) => {
    // 増殖バグを防ぐ
    if (document.getElementById('sample-login') !== null) {
      return;
    }

    // ボタン表示
    sampleCustomize.uiCreate(kintone.app.getHeaderSpaceElement());

    graphApi.init();

    $('#sample-login').on('click', () => {
      graphApi.logIn();
    });

    $('#sample-logout').on('click', () => {
      graphApi.logOut();
    });

    $('#sample-getuserinfo').on('click', () => {
      graphApi.getAccessToken().then((token) => {
        graphApi.getUserInfo(token);
      });
    });
  });
})(jQuery);

動作確認

設定できたら、kintone の一覧画面に表示されるログインボタンをクリックして動作を確認してください。

解説

トークン

サンプルコードでは、ログイン時は id_token を取得し、リクエスト前に access_token を取得しています。
access_token は Microsoft Graph API を使ったリクエストの認証に必要です。

トークンの詳細は Entra ID の トークンと要求の概要 (External link) を参考にしてください。
上記 URL 内に記載されているとおり jwt.ms (External link) にトークンを貼り付けると内容を確認できます。

終わりに

Microsoft 製品との連携となると難しい印象をもつ方もいらっしゃいますが、今回の記事で「連携できそうだな」と思っていただける方がいらっしゃれば幸いです。

information

この Tips は、2018 年 4 月版 kintone で動作を確認しています。