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で動作を確認しています。