Morning Girl

Web API, Windows, C#, .NET, Dynamics 365/CRM etc..

Office365 GraphAPI で認証アプローチ毎のアクセス範囲にハマった話 ユーザーの予定(Event)を横断的に取得したい! 

先週くらいに、Office365 のCalendar情報を横断的に取得したいという要望があって調べていたんですが、ちょっと変に先入観があって躓いていました。

https://twitter.com/sugimomoto/status/1113275622841737218?s=20

いろいろと試行錯誤を経て解決したんですが、改めて全体像を調べてスッキリ理解できたので、GraphAPIにおける認証アプローチとアクセス範囲に関する考え方をまとめておきたいと思います。

ちなみにわかり易さから予定表(イベント)の取得に焦点を当てていますが、考え方はファイルやメールなどにも共通するお話です。

なお、Exchange Online(EWS)ベースのアプローチであれば、以前取り組んでいたのをBlogにしています。

kageura.hatenadiary.jp

やりたかったこと

Office365の予定表では、以下のような感じにいろんな人の予定が見えるようになっていると思いますが、これをGraphAPI経由で横断的に取得したい、というのが今回の目的です。

こんな感じの予定データを

f:id:sugimomoto:20190422185231p:plain

横断的に取得して、分析活用したい、というイメージですね。

f:id:sugimomoto:20190422185236p:plain

データ連携的観点でも、例えば外部のCRMアプリケーションなどで作られた営業案件の再訪問スケジュールを連携して、結果を確認したい、みたいなことがあると思います。

それを実現するための、GraphAPIアクセス範囲・アクセス許可の考え方です。

今回のお話の焦点:Azure AD アプリにおけるGraphAPIのアクセス許可

今回とりあげるのは、Azure ADのアプリケーション登録で行う以下のアクセス許可の話です。

f:id:sugimomoto:20190422185247p:plain

以下のリファレンスがベースになりますが、私の解釈も踏まえてざっくりお話していきます。

docs.microsoft.com

ベースとなるアクセス許可は2種類

まず、大前提として抑えておきたいことは、ベースとなる「アクセス許可」は2種類存在するということです。

docs.microsoft.com

それが、「委任されたアクセス許可」と「アプリケーションのアクセス許可」です。

f:id:sugimomoto:20190422185322p:plain

なんのことを言っているんだ、となるかもしれませんが、これはOAuthのAuthorizationURLが2種類

「委任されたアクセス許可」の場合以下のようなURLで

https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?client_id=6731de76-14a6-49ae-97bc-6eba6914391e&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F&response_mode=query&scope=offline_access%20user.read%20mail.read&state=12345

このようなアクセス許可を求められますね。

f:id:sugimomoto:20190422185349p:plain

https://docs.microsoft.com/ja-jp/graph/auth-v2-user#2-get-authorization

「アプリケーションのアクセス許可」の場合は、URL構成が変わって、

https://login.microsoftonline.com/{tenant}/adminconsent?client_id=6731de76-14a6-49ae-97bc-6eba6914391e&state=12345&redirect_uri=https://localhost/myapp/permissions

ちょっとだけ、メッセージも変わったことがわかると思います。

f:id:sugimomoto:20190422185355p:plain

https://docs.microsoft.com/ja-jp/graph/auth-v2-service#3-get-administrator-consent

メッセージが示すとおりでもありますが、それぞれで役割が違います。

  • 委任されたアクセス許可

明示的なサインインユーザーが存在する場合に利用するアプローチです。 これは言ってしまえば、ユーザー自身がOffice365を利用する時と同様のアクセス範囲を想定した 自分の予定、自分のメール、自分のファイルをベースに、アクセス許可を行ったユーザーに共有されたgファイル、予定などにもアクセスすることを想定しています。

  • アプリケーションのアクセス許可

明示的なサインインユーザーが存在しないアプリで利用するアプローチです。たとえば、バックグラウンド でデータ連携をしたり、強力な権限で組織全体のリソースにアクセスしたい場合に使用します。そして注意したいのが、アプリケーションのアクセス許可は「管理者のみが同意」してはじめて利用できるようになるという点です。

GraphAPIリソースへのアクセス許可は「リソース:操作:制約」の3つで捉える

その上で、GraphAPIのアクセス許可は以下の図に示すように、「リソース:操作:制約」の3つで捉えるのが個人的にいいと思っています。

  • リソースは「Event、File」など、どんなデータにアクセスするのか
  • 操作は「Read、Write」など、データに対してどんな処理を行うのか
  • 制約は「All、Shared、None」など、どの範囲までアクセスするのか

f:id:sugimomoto:20190422185433p:plain

今回の図は趣旨を理解しやすくするために単純化していますが、「操作」と「制約」はこれ以外にもいくつかあります。

必要な操作とリソースの特定方法

おそらく、操作とリソースは比較的わかりやすいでしょう。

予定を取りたいなら、CalenderのRead

予定の書き込みも行いたいなら、CalenderのReadWrite

各必要なリソース名、操作はそれぞれのリソースページから確認することができます。

例えば予定(Event)の主得であれば、以下の通り。

docs.microsoft.com

f:id:sugimomoto:20190422185443p:plain

アクセス範囲の肝となる「制約」の考え方

そして、今回のポイントになるのは、この「制約」の部分になります。

基本的な制約の種類は以下の3種類。一番上が強い権限です。

  • All
  • Shared
  • None(制約の指定無し)

Noneだけちょっと分かりづらいですが、一番弱い(アクセス範囲が狭い)権限です。ドキュメントでは「制約の指定無し」と書かれていて、混乱を招きやすいですね・・・。一番弱い制約って言うと語弊がありますし。

これ以外に、AppFolderや各リソース固有のものが存在しますが、基本的には上記の3種類がベースになっていると思っていいと思います。

それぞれの制約の範囲イメージ

これらの制約をEventのリソースをベースに表したのが、以下のそれぞれの図になります。

Noneの場合は、アプリケーションにログインしたユーザー自身のリソースにしかアクセスできません。これは、予定が共有されていたとしても変わりません。

f:id:sugimomoto:20190422185507p:plain

そして、もう少しアクセス範囲を広げたい場合は、Sharedが必要になります。この場合は自分の予定に加えて、共有されている予定にもアクセスできます。

f:id:sugimomoto:20190422185515p:plain

そして、最後に共有されていない予定にもアクセスしたい場合は、「All」が必要になります。これが今回私が必要としていたアクセス範囲です。

一番注意したいのが、委任されたアクセス許可では、この制約範囲を利用できない、ということです。

f:id:sugimomoto:20190422185522p:plain

制約で「All」を使いたい場合は 2種類のアクセス許可に気をつける

ここが今回私が躓いたポイントでした。

アプリケーションの権限で「Calender.ReadAndWrite.All」を付与していたにも関わらず、アクセス許可は「委任されたアクセス許可」で実施していなかったので、他のユーザーのリソースにアクセスしようとしても「Access is denied.」になってしまいました。

f:id:sugimomoto:20190422185534p:plain

それぞれの「アクセス許可」方法の確認

ここまで来れば、あとはそれぞれの「アクセス許可」を使ってAccessTokenを取得するだけです。

アプリケーションの登録方法はどちらも同じですが、アクセス許可の方法は

「委任されたアクセス許可」の場合、「AuthorizationCode」

「アプリケーションのアクセス許可」の場合、「ClientCredential」

で実施します。

ちなみに、Graphエクスプローラーは現在「AuthorizationCode」にしか対応していないみたいなのでご注意ください。

「委任されたアクセス許可」を使ってユーザーの代わりにアクセスを取得する

ほとんどリファレンスに書かれている通りなので、要点だけ記載します。

docs.microsoft.com

1. AuthorizationURLを生成して、ユーザーへログイン要求を実施

この時ポイントになるのが、Scopeパラメータ。アプリ側で権限を許可していても、ここでスコープの要求をしない限り、操作を実施することはできないので要注意。

https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?client_id=6731de76-14a6-49ae-97bc-6eba6914391e&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F&response_mode=query&scope=offline_access%20user.read%20mail.read&state=12345

f:id:sugimomoto:20190422185554p:plain

2. アクセス許可後のリダイレクトURLに含まれる「Code」を使って、AccessTokenを取得

Callbackとして、以下のようなデータが返却されるので、Stateをチェックし、Codeを取得したら

https://localhost/myapp/?
code=M0ab92efe-b6fd-df08-87dc-2c6500a7f84d
&state=12345

そのCodeを利用して、TokenエンドポイントにAccessTokenを要求します。この時GrantTypeはAuthoricationCodeを指定します。

POST https://login.microsoftonline.com/common/oauth2/v2.0/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&scope=user.read%20mail.read
&code=OAAABAAAAiL9Kn2Z27UubvWFPbm0gLWQJVzCTE9UkP3pSx1aXxUjq3n8b2JRLk4OxVXr...
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&grant_type=authorization_code
&client_secret=JqQX2PNo9bpM0uEihUPzyrh

これで晴れてAccessTokenが取得できます。

{
    "token_type": "Bearer",
    "scope": "user.read%20Fmail.read",
    "expires_in": 3600,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...",
    "refresh_token": "AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4..."
}

「アプリケーションのアクセス許可」の仕方、ユーザーなしでアクセスを取得する

docs.microsoft.com

アプリケーションのアクセス許可も基本プロセスは似ていますが、要所要所でちょっと違います。

1. Adminconsentエンドポイントを使って、管理者の同意を実施

前述したとおり、一番最初に必要なことは、対象のアプリケーション(ClientID)に対して、管理者が同意するというプロセスです。「委任されたアクセス許可」とはURLやパラメータが違うことに注意してください。

GET https://login.microsoftonline.com/{tenant}/adminconsent
?client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&state=12345
&redirect_uri=https://localhost/myapp/permissions

f:id:sugimomoto:20190422185624p:plain

また、レスポンスも変わり、「委任されたアクセス許可」であったようなCodeは含まれません。つまり、「アプリケーションのアクセス許可」の場合、同意と後続のプロセスは独立した形になります。

GET https://localhost/myapp/permissions
?tenant=a8990e1f-ff32-408a-9f8e-78d3b9139b95&state=12345
&admin_consent=True

2. 同意された後、AccessTokenを取得する

同意が完了したら、あとはAccessTokenを取得するだけです。前述したとおり、Codeを渡す必要は無いので、同意さえ取っていれば、どのタイミングで実施しても問題ありません。

POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret=qWgdYAmab0YSkuL1qKv5bPX
&grant_type=client_credentials

あとは、同じようにAccessTokenがレスポンスで変えるだけですが、Scopeが含まれないので要注意。

{
  "token_type": "Bearer",
  "expires_in": 3599,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1uQ19WWmNBVGZNNXBP..."
}

※ちなみに、管理者の同意はAzure ADのアプリケーション管理画面で「アクセス許可の付与」をクリックすれば、特にURLの生成などは必要なく実施することができます。(逆に手軽に実施できてしまうので要注意)

f:id:sugimomoto:20190422185652p:plain

余談:そもそも「管理者」ってどんな「管理者」?

GraphAPIとAzure ADのアプリリファレンスを見ていると、たくさんの「管理者」というキーワードに遭遇します。

そして、今ひとつどの管理者設定を入れておけば、この管理者という要件を満たすのかが、いまいちわかりづらいですね。

管理者の同意に必要な権限は何か?

結論から言うと、Azure AD ロールの「アプリケーション管理者(全体管理者でももちろんOK)」が必要です。

アプリケーション管理者:このロールのユーザーは、エンタープライズ アプリケーション、アプリケーション登録、アプリケーション プロキシの設定の全側面を作成して管理できます。 さらに、このロールは、委任されたアクセス許可とアプリケーション アクセス許可 (Microsoft Graph と Azure AD Graph を除く) に同意する権限を付与します。 このロールに割り当てられたユーザーは、新しいアプリケーション登録またはエンタープライズ アプリケーションを作成する際に、所有者として追加されません。

https://docs.microsoft.com/ja-jp/azure/active-directory/users-groups-roles/directory-assign-admin-roles

以下の画面で付与することができます。

f:id:sugimomoto:20190422185718p:plain

なので、例えば、予定表を取得したいからといって、Exchangeの管理者権限が必要か? といえばそんなことは無いようです。アプリケーション管理者が同意してしまえば、そのClientIDはその権限にしたがって効力を発揮します。

ちなみに、私がはじめ勘違いしていたのですが、Office365 管理画面で表示できる、この画面の役割とさっきのAzure ADロール管理画面の役割は一緒です。同期されてます。

f:id:sugimomoto:20190422185708p:plain

ただし、アプリケーション管理者のロールはAzure ADロール管理画面でしか付与することができませんので要注意。

実際に予定を取得してみる

というわけで、実際に予定を取得してみます。せっかくなので「アプリケーションのアクセス許可」で試してみましょう。シェアをしていないユーザーの想定です。

docs.microsoft.com

1. Azure AD アプリケーション登録

まず、リファレンスにある通り、「Calendars.Read.All」の権限を持ったAzure ADアプリを作成。

CientIDとCientSecretを取得します。(アプリの作り方は以前Blogで書いているので割愛。)

kageura.hatenadiary.jp

2. 管理者の同意を取得

さくっと、Azure ADの画面から実施してしまいます。

f:id:sugimomoto:20190422185652p:plain

3. アクセストークンの取得

そして、アクセストークンを取得します。

POST /a67527d4-180e-4472-bcdd-bca82c56c70a/oauth2/token?api-version=1.0 HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
client_id=XXXXXXXXXXXXXXXXXXXXXXXXXX
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXX=
&grant_type=client_credentials

4. 指定ユーザーのイベントを取得

通常、自分自身のイベントを取得する場合は以下のようなURLですが

GET /me/events

他のユーザーのイベントを取得する場合はUsersの後にXXX@XXX.onmicrosoft.comといった形でユーザーを指定することで、予定を取得できます。

GET /users/{id | userPrincipalName}/events

実際にはこんな感じのリクエストですね。

GET /v1.0/users/user01@sugimomoto21.onmicrosoft.com/events HTTP/1.1
Host: graph.microsoft.com
Authorization: Bearer eyJ0XXXXXXXXX

CData Office365 Driverの対応状況

ちなみに、CData Office365 Driverを使って、予定やファイルの取得が可能なのですが、今の所「委任されたアクセス許可」のみに対応中でした。

でも、やっぱり管理権限でバッチ処理を動かしたり、予定を横断的に取得したいというシナリオはよく聞くので、現在USチームと実装に向けてディスカッション中です。

もし、そういった使い方

https://www.cdata.com/jp/drivers/office365/

ちなみに、Excel-Addinで予定を取得する場合は、Office365 Excel Add-inをインストールして

Excelのリボンから「From Office365」をクリック

f:id:sugimomoto:20190422191723p:plain

接続画面はDefaultのままでいいので、「Test Connection」をクリック

f:id:sugimomoto:20190422191756p:plain

同意画面が表示されるので、内容を確認して、承諾するだけ。

f:id:sugimomoto:20190422192313p:plain

接続が完了します。

f:id:sugimomoto:20190422192328p:plain

あとは、Eventsテーブルを選択して、OKをクリックすれば

f:id:sugimomoto:20190422192422p:plain

自分自身の予定データがずらっと取得できます。

f:id:sugimomoto:20190422192551p:plain

おわりに

結構権限周りを調べるのはめんどいことがわかった記事でした。

結構悩む人は多いのではなかろうか。。。