SharePoint Framework (SPFx) から Office 365 API を叩いてみる
はじめに
SharePoint Framework (SPFx) の Web パーツでは新しいエクスペリエンスが採用され、これまでの SharePoint アドインで使用されていた iframe
が廃止されました。SharePoint アドインでは iframe
内でリダイレクトして OAuth2 の暗黙的な許可フローを実現できたのですが、SPFx ではそれができなくなりました。さて困ったということで頑張ってなんとかしてみました。
念のため公式の対応方法を紹介しておきます。
adal.js
が Web パーツに対応していないから AuthenticationContext
の中身を書き換えろとかそれライブラリで対応する話じゃないんですかね。無理して adal.js
を使うこともないので今回は使わずに実装します。
サンプル コード
実行手順
アプリケーションの登録
Azure AD にアプリケーションを登録してアプリケーション ID を控えておきます。アクセス許可に Exchange Online の Mail.Read
を選択します。
プロジェクトの作成
SharePoint Framework を使うまでの流れは以下を参考にしてください。
yeoman
を使って適当な名前 (今回は SampleApplication
) でプロジェクトを作成します。現時点では SampleApplicationWebPart.manifest.json
にバグがあって $schema
のパスが通っていません。そこでファイル名を変更します。
変更前 | 変更後 |
---|---|
clientSideComponentManifestSchema.json |
client-side-component-manifest.schema.json |
Web パーツ プロパティの作成
別に定数でもいいのですがせっかくなのでプロパティを作成します。
ISampleApplicationWebPartProps.ts
export interface ISampleApplicationWebPartProps {
appId: string;
authUrl: string;
resourceUrl: string;
}
SampleApplicationWebPart.manifest.json
{
"preconfiguredEntries": [
{
"properties": {
"appId": "{{app-id}}",
"authUrl": "https://login.microsoftonline.com/common/oauth2/authorize",
"resourceUrl": "https://outlook.office.com"
}
}
]
}
ユーザー プロパティの作成
一度取得したアクセス トークンを sessionStorage
に保存しておくようにします。コンストラクターで Web パーツのインスタンス ID を受け取ることで、複数の Web パーツがあった場合でもアクセス トークンが競合しないようにします。
ISampleApplicationUserProps.ts
export class SampleApplicationUserProps {
private instanceId: string;
constructor(instanceId: string) {
this.instanceId = instanceId;
}
public get accessToken(): string {
return this._getValue("accessToken");
}
public set accessToken(value: string) {
this._setValue("accessToken", value);
}
private _getValue(key: string): string {
var stringValue = sessionStorage.getItem(this.instanceId);
if (stringValue == null) {
return null;
}
var jsonValue = JSON.parse(stringValue);
return jsonValue[key];
}
private _setValue(key: string, value: string): void {
var stringValue = sessionStorage.getItem(this.instanceId);
if (stringValue == null) {
stringValue = "{}";
}
var jsonValue = JSON.parse(stringValue);
jsonValue[key] = value;
stringValue = JSON.stringify(jsonValue);
sessionStorage.setItem(this.instanceId, stringValue);
}
}
OAuth フローの作成
上記にもある通り iframe
によるリダイレクトができないので window.open()
で新しいウィンドウを立ち上げるようにします。呼び出し元では setInterval()
でウィンドウの状態を監視し、フローが終わったら URL からアクセス トークンを取得します。取得が終わったら Outlook Mail REST API を叩いて未読件数を取得します。
SampleApplicationWebPart.ts
public render(): void {
this.userProperties = new SampleApplicationUserProps(this.context.instanceId);
if (window.location.hash == "") {
var loginName = this.context.pageContext.user.loginName;
if (this.userProperties.loginName != loginName) {
this.userProperties.clear();
this.userProperties.loginName = loginName;
}
if (this.userProperties.accessToken == null) {
var redirectUrl = window.location.href.split("?")[0];
var requestUrl = this.properties.authUrl + "?" +
"response_type=token" + "&" +
"client_id=" + encodeURI(this.properties.appId) + "&" +
"resource=" + encodeURI(this.properties.resourceUrl) + "&" +
"redirect_uri=" + encodeURI(redirectUrl);
var popupWindow = window.open(requestUrl, this.context.instanceId, "width=600px,height=400px");
var handle = setInterval((self) => {
if (popupWindow == null || popupWindow.closed == null || popupWindow.closed == true) {
clearInterval(handle);
}
try {
if (popupWindow.location.href.indexOf(redirectUrl) != -1) {
var hash = popupWindow.location.hash;
clearInterval(handle);
popupWindow.close();
var query = {};
var elements = hash.slice(1).split("&");
for (var index = 0; index < elements.length; index++) {
var pair = elements[index].split("=");
var key = decodeURIComponent(pair[0]);
var value = decodeURIComponent(pair[1]);
query[key] = value;
}
self.userProperties.accessToken = query["access_token"];
self.userProperties.expiresIn = query["expires_in"];
self._renderBody();
self._renderContent();
}
} catch (e) { }
}, 1, this);
} else {
this._renderBody();
this._renderContent();
}
}
}
private _renderBody(): void {
this.domElement.innerHTML = `
<div id="${this.context.manifest.id}" class="${styles.container}">
</div>`;
}
private _renderContent(): void {
var container = this.domElement.querySelector(`#${this.context.manifest.id}`);
var requestUrl = this.properties.resourceUrl + "/api/v2.0/me/mailfolders";
fetch(requestUrl, {
method: "GET",
headers: new Headers({
"Accept": "application/json",
"Authorization": `Bearer ${this.userProperties.accessToken}`
})
})
.then(response => response.json())
.then(data => {
var items = data.value;
for (var index = 0; index < items.length; index++) {
var displayName = items[index].DisplayName;
var unreadItemCount = items[index].UnreadItemCount;
container.innerHTML += `<div>${displayName}: ${unreadItemCount}</div>`;
}
});
}
実行結果
初回のみ新しいウィンドウが立ち上がります。次回以降は保存されたアクセス トークンが使われるためウィンドウは表示されません。
おわりに
動的に iframe
を作ってフローを処理する方法もありそうです。
Discussion