SharePoint Framework (SPFx) から Office 365 API を叩いてみる
はじめに
SharePoint Framework (SPFx) の Web パーツでは新しいエクスペリエンスが採用され、これまでの SharePoint アドインで使用されていた iframe が廃止されました。SharePoint アドインでは iframe 内でリダイレクトして OAuth2 の暗黙的な許可フローを実現できましたが、SPFx ではそれができなくなりました。今回はその対応方法についてご紹介します。
adal.js は Web パーツに対応していないため、AuthenticationContext の中身を書き換える必要があります。無理に adal.js を使う必要はありませんので、今回は adal.js を使用せずに実装します。
サンプル コード
実行手順
アプリケーションの登録
Azure AD でアプリケーションを登録し、アプリケーション ID を控えておきます。アクセス許可には Exchange Online の Mail.Read を選択してください。
プロジェクトの作成
SharePoint Framework を利用するまでの流れについては、以下のドキュメントを参考にしてください。
yeoman を使ってプロジェクトを作成します。現時点では WebPart.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>;
}
});
}
実行結果
初回のみ新しいウィンドウが表示されます。2 回目以降は保存されたアクセストークンが利用されるため、ウィンドウは表示されません。

おわりに
動的に iframe を作成してフローを処理する方法も考えられます。
Discussion