からめもぶろぐ。

俺たちは雰囲気で OAuth をやっている

SharePoint を使ったバッチ プログラムの認証/承認がなぜこんなにややこしいのか考えてみる

SharePoint を使ったバッチ プログラム開発、簡単なものでは手元で実行する簡単なデータ投入ツールだったり、複雑なものになると Azure Functions や Azure Automation を使ってやるものだったりすることがありますが、いずれにしてもこうした無人化されたプログラムでの認証/承認は避けられない問題です。SharePoint はオンプレミスからクラウドへと進化し続ける製品であり、その過程によってさまざまな試行錯誤があったため、結果として互換性の観点から複雑になってしまっているところがあります。大げさではありますが、歴史を振り返ることで、今は何がベストの方法なのかを考えてみたいと思います。

クラウド以前

SharePoint Online が世の中に広まるずっと以前、オンプレミスの SharePoint が全盛期だった時代、バッチ プログラムは SharePoint サーバーの中で動かすのが基本でした。SharePoint サーバー オブジェクト モデル (Microsoft.SharePoint.dll) を使って、ファーム アカウントで動作させることで、バッチ プログラムを非常に強い権限で動かすことができました。また、SharePoint 2010 からはクライアント オブジェクト モデル (Microsoft.SharePoint.Client.dll) が誕生し、外部のサーバーから SharePoint を操作することができるようになりましたが、ほとんどの場合は NTLM 認証で事足りたため、ファーム アカウント (またはそれに近い強い権限を持つアカウント) のユーザー名とパスワードでログインするということをしていました。

クラウド転換期

SharePoint 2013 から、それまでのファーム ソリューション開発やサンドボックス ソリューション開発に代わる新しい開発手法として、SharePoint アドイン (当時は SharePoint アプリと言っていました) が誕生しました。SharePoint アドインの特長として、アプリを SharePoint に登録することで、アプリに必要なアクセス許可を指定することができるという点がありました。これまでは「アプリ自体に何をさせることができるか」が指定できなかったため、悪意のある操作もできてしまったのですが、SharePoint アドイン モデルを採用することにより、アプリを制限された権限の範囲内で動作させることが可能になりました。SharePoint アドインの認証/承認には Azure ACS を使った OAuth の仕組みが取り入れられており、SharePoint Server でも SharePoint Online でも同じように動作させることができました。同じく SharePoint 2013 から導入された REST API と組み合わせて、これまで .NET でしか開発できなかったバッチ プログラムを他の言語でも開発できるようになりました。また、ユーザーがアプリに動作を委任するのではなく、アプリそのものがサービス アカウントとして動作する仕組み (Client Credentials Grant) もできました。

他方でクライアント オブジェクト モデルは進化をし続きます。SharePoint Online に対応するために SharePointOnlineCredentials (WS-Security) による認証により、ほぼ NTLM 認証と同じ感覚で開発をすることができました。

クラウド全盛期

Azure の発展や Office 365 によるサービスのクラウド化による「すべてがクラウドへ」という動きが加速するにつれ、SharePoint もクラウド ファーストという考えが入ってきます。SharePoint Server は SharePoint Online の過去のバージョンをパッケージ化したものであり、新しい機能は SharePoint Online にのみ取り入れられるようになります。Microsoft のクラウド サービス間の連携の機運が高まり、Azure Active Directory を基盤としてすべてのサービスに繋がる仕組みができます。これはのちに Microsoft Graph へと繋がっていきますが、SharePoint Online をはじめとした Microsoft のクラウド サービスの REST API は Azure Active Directory にアプリを登録して、OAuth による認証/承認のプロセスを経ることになります。SharePoint はここにきてまったく同じ仕組み (OAuth) を持つ異なる認証/承認方法を持つようになってしまいます。

SharePoint アドイン モデル Azure Active Directory
SharePoint Server 動作する 動作しない
SharePoint Online 動作する 動作する
他サービス連携 動作しない 動作する

SharePoint Server がある以上、互換性のために、SharePoint アドイン モデルによる OAuth をやめてしまうことはできません。Azure ACS は廃止されましたが、SharePoint アドイン モデルによる OAuth は生き残りました。SharePoint Online でバッチ プログラムを動かすときには、SharePoint アドインとしてアプリ登録することも、Azure Active Directory にアプリ登録することもでき、現時点ではどちらも有効な方法です。

クライアント オブジェクト モデルには .NET Core の波が押し寄せます。長らくクライアント オブジェクト モデルは .NET Standard に対応していなかったのですが、ついに .NET Standard に対応したライブラリが誕生しました。その際、もはや古い手法でありセキュリティにも問題がある SharePointOnlineCredentials を切り捨てています。今後、SharePointOnlineCredentials は使えなくなる流れにあると考えていいでしょう。

blog.karamem0.jp

まとめ

現時点で、SharePoint Online のバッチ プログラムを開発する上で、SharePoint アドイン モデルと Azure Active Directory のどちらの認証/承認を使っても問題はありません。ただし、SharePoint アドイン モデルはサポートされているとはいえ、すでに開発方法自体が古いものであり、今後何の前置きもなしに無効化されてしまう可能性を否定できません。ただし、SharePoint Online では Azure Active Directory のクライアント シークレットを使った Client Credentials Grant がサポートされないため、証明書を使う必要があります。クライアント シークレットを使うほうがお手軽なため、上記のリスクを承知したうえで、SharePoint アドイン モデルによる OAuth を使うという考え方はできると思います。

Office 365 管理 API を使って SharePoint Online の監査ログを取得する

いままでずっと SharePoint Online の監査ログの生データは取得できないと思っていたのですが、実は Office 365 管理 API というものを使えば取得できるのだそうです。Office 365 管理 API は SharePoint、Exchange、Teams、Power Platform などのさまざまな監査ログを取得できるのですが、特に利用状況の監視目的で SharePoint Online の監査ログを取りたいという要望は非常に多いため、この方法を使えば解決できるのはないかと思います。

なお今回は以下の記事を参考にしています。

qiita.com

サンプル コード

github.com

実行手順

Office 365 管理 API でログを取得するには以下の手順が必要になります。

  • アクセス トークンを取得する
  • サブスクリプションを作成する
  • コンテンツの URL を取得する
  • コンテンツを取得する

アクセス トークンを取得する

Office 365 管理 API を使用するためには、はじめに Azure Active Directory にアプリを登録する必要があります。[API のアクセス許可] では [Office 365 Management APIs] - [アプリケーションのアクセス許可] - [ActivityFeed.Read] を追加します。

f:id:karamem0:20200902135316p:plain

またアプリケーション シークレットも取得しておきます。

Office 365 管理 API は Azure Active Directory の v2.0 エンドポイントに対応していないので MSAL を使うことができません。ADAL を使ってもいいのですが、面倒なので、今回は直接 HttpClient で取りに行くようにします。といっても Client Credentials Grant なのでそれほど難しくはありません。

private static void AcquireToken()
{
    var httpRequestUrl = $"https://login.microsoftonline.com/{TenantId}/oauth2/token";
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, httpRequestUrl);
    var httpRequestContent = new FormUrlEncodedContent(new Dictionary<string, string>()
    {
        { "grant_type", "client_credentials" },
        { "resource", Resource },
        { "client_id", ClientId },
        { "client_secret", ClientSecret }
    });
    httpRequestMessage.Content = httpRequestContent;
    var httpResponseMessage = HttpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
    var httpResponseContent = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    var httpResponseJson = JsonConvert.DeserializeObject<JToken>(httpResponseContent);
    AccessToken = httpResponseJson.Value<string>("access_token");
}

サブスクリプションを作成する

Office 365 管理 API のコンテンツ タイプ (Audit.SharePoint) に対してサブスクリプションを作成します。Webhook も登録できるようなのですが、今回は省略します。PublisherIdentifier は任意の GUID を指定してください。

private static void CreateSubscription()
{
    var httpRequestUrl = $"https://manage.office.com/api/v1.0/{TenantId}/activity/feed/subscriptions/start?contentType=Audit.SharePoint&PublisherIdentifier={PublisherIdentifier}";
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, httpRequestUrl);
    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
    var httpResponseMessage = HttpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
    var httpResponseContent = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
}

コンテンツの URL を取得する

ログを取得する時間を指定してコンテンツ データのダウンロード先の URL を取得します。時間は 24 時間以内で過去 7 日以内である必要があります。今回は 1 日前のデータを取得するように指定します。

private static void GetContentUri()
{
    var startTime = DateTime.Today.AddDays(-1).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss");
    var endTime = DateTime.Today.AddSeconds(-1).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss");
    var httpRequestUrl = $"https://manage.office.com/api/v1.0/{TenantId}/activity/feed/subscriptions/content" +
        $"?contentType=Audit.SharePoint" +
        $"&PublisherIdentifier={PublisherIdentifier}" +
        $"&startTime={startTime}" +
        $"&endTime={endTime}";
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, httpRequestUrl);
    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
    var httpResponseMessage = HttpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
    var httpResponseContent = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    var httpResponseJson = JsonConvert.DeserializeObject<JArray>(httpResponseContent);
    ContentUri = httpResponseJson[0].Value<string>("contentUri");
}

コンテンツを取得する

取得した URL から JSON 形式のコンテンツを取得します。

private static void GetContents()
{
    var httpRequestUrl = ContentUri;
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, httpRequestUrl);
    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
    var httpResponseMessage = HttpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
    var httpResponseContent = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    var httpResponseJson = JsonConvert.DeserializeObject<JArray>(httpResponseContent);
    Console.WriteLine(JsonConvert.SerializeObject(httpResponseJson, Formatting.Indented));
}

実行結果

以下のような JSON が出力されます。サンプルはファイルのダウンロードですが、それだけではなく、いろいろな種類のログが取得できます。

[
  {
    "CreationTime": "2020-09-01T14:00:03",
    "Id": "bea6430c-9db6-400c-ebbf-08d84e7f532c",
    "Operation": "FileDownloaded",
    "OrganizationId": "92dbed3f-d37a-4f19-a392-f6970505cc6a",
    "RecordType": 6,
    "UserKey": "i:0h.f|membership|10033fffac7f4b34@live.com",
    "UserType": 0,
    "Version": 1,
    "Workload": "OneDrive",
    "ClientIP": "52.185.144.178",
    "ObjectId": "https://karamem0jp-my.sharepoint.com/personal/takashi_shinohara_karamem0_jp/Documents/fitbit.json",
    "UserId": "takashi.shinohara@karamem0.jp",
    "CorrelationId": "1aba0151-abda-4b69-a102-a9319a2a9e15",
    "EventSource": "SharePoint",
    "ItemType": "File",
    "ListId": "ccf377fa-0605-41b5-925a-224e62839884",
    "ListItemUniqueId": "3afd549b-4c58-4026-a7e2-d4fcc1a2ebe0",
    "Site": "0815189e-2f76-44f2-89fe-f2dd7260e20d",
    "WebId": "15cdf073-9e78-4667-94ea-96abe6aa860f",
    "HighPriorityMediaProcessing": false,
    "SourceFileExtension": "json",
    "SiteUrl": "https://karamem0jp-my.sharepoint.com/personal/takashi_shinohara_karamem0_jp/",
    "SourceFileName": "fitbit.json",
    "SourceRelativeUrl": "Documents"
  },
...
]

何が取得できるかについては以下が参考になります。

docs.microsoft.com

まとめ

Office 365 管理 API に関する情報は以下にまとまっています。

docs.microsoft.com

オンプレの SharePoint で監査ログを取っていろいろやっていたことを、SharePoint Online に移行しても同じことをやりたい、というときにぜひ使っていただきたいと思います。

Power Apps で複数選択可能なコンボ ボックスでフィルターをする

Power Apps のコンボ ボックスは複数の値を選択することができますが、これを使って複数の値でフィルターをしたいと思いました。

具体的には以下の感じです。初期状態は何も選択されていないのですべてが表示されます。

f:id:karamem0:20200826100159p:plain

コンボ ボックスで複数の値を選択するとその値にしたがってフィルターされます。

f:id:karamem0:20200826100215p:plain

どうやって実現しているのかというと、コンボ ボックスの SelectedItems を見て、値が選択されていれば、データ ソースに条件を追加するようにしています。

f:id:karamem0:20200826100932p:plain

わかりやすいようにテキストも貼っておきます。

Filter(
    'Test List 1',
    Or(
        Or(
            IsEmpty(ComboBox1.SelectedItems),
            IsBlank(ComboBox1.SelectedItems)
        ),
        And(
            Not(
                IsBlank(
                    LookUp(
                        ComboBox1.SelectedItems,
                        Value = "Test List Item 1"
                    )
                )
            ),
            Title = "Test List Item 1"
        ),
        And(
            Not(
                IsBlank(
                    LookUp(
                        ComboBox1.SelectedItems,
                        Value = "Test List Item 2"
                    )
                )
            ),
            Title = "Test List Item 2"
        ),
        And(
            Not(
                IsBlank(
                    LookUp(
                        ComboBox1.SelectedItems,
                        Value = "Test List Item 3"
                    )
                )
            ),
            Title = "Test List Item 3"
        )
    )
)

コンボ ボックスの選択肢についてそれぞれ条件を書かないといけないので、コンボ ボックスの選択肢は固定である必要があります。委任の警告は出ないので SharePoint リストでもうまく動作するはずです(試していません)。

第 1 回 Japan M365 Dev User Group 勉強会を開催しました

2020/08/19 に第 1 回 Japan M365 Dev User Group 勉強会を開催しました。Japan M365 Dev User Group は今年の 5 月に立ち上げた Microsoft 365 技術者向けのコミュニティで、今回がはじめての開催となります。

jpm365dev.connpass.com

今回のテーマは「Microsoft Teams 開発」ということで 2 人の方にご登壇いただきました。

武田さん: これで始める!Teams アプリケーション開発 101 *1

日本マイクロソフト株式会社の武田さんに de:code での登壇内容をベースにより踏み込んだ内容のデモを行っていただきました。de:code は 5 月時点でしたが、そこからのアップデート情報も紹介いただき、非常に熱いセッションとなりました。特に先日の Inspire で発表された Teams 会議にアプリを組み込めるというのは気になる内容で、詳細情報を期待したいところです。

小張さん: Teams を利用したメッセージ通知

アドバンスド・ソリューション株式会社の小張さんに Teams でのメッセージ送信について説明をしていただきました。Incoming Webhook は URL に対してリクエストを投げることでメッセージを投稿できる機能で、Postman から投稿するデモを見せていただきました。また、Teams Bot のプロアクティブ メッセージ送信では、実際の開発のコードを見ながらどのように動いているかを紹介いただきました。*2 Bot のほうから先にチャットでメッセージを送るという仕組みをあまり考えたことがなかったので参考になりました。

ライトニング トーク

ライトニング トークはテーマの縛りなく募集したので、SharePoint、Office Scripts、Microsoft Graph といろいろなお題でお話ししていただきました。どれも新しい気付きのある内容でとても参考になりました。

まとめ

Microsoft 365 開発という限られた分野のなかで大勢の方にご参加いただきまして本当にありがとうございます。Microsoft 365 開発に興味がある方がたくさんいらっしゃるということが実感できましたので、これからも継続的に開催していただければと思います。重ねてになりますが、登壇者や参加者のみなさま本当にありがとうございました!

*1:101 は「初級編」という意味だそうですよ。知らなかった!

*2:コードがいっぱい出てきてあまり追いつけていけませんでした。すみません。