もう多分怖くないサービスプリンシパル

★★★ Live配信告知 ★★★ぜひお申込みください!
◆◇世界一わかりみの深いクラウドネイティブ on Azure 第21回◆◇
〜 AppServiceでコンテナ〜 App Serviceでコンテナを動かすことができる「Web App for Containers」をご紹介します。10/11(火) 19:00~

こんにちは、サイオステクノロジー武井です。今回はAzureのサービスプリンシパルについて書こうかと思います。

なんかよくわかんないサービスプリンシパル

Azureで一番最初につまずくのはサービスプリンシパルかと思います。サービスプリンシパルはAzure上でアプリつくったり、Azure ADで認証を行う際には必須の概念なのですが、多分C言語のポインタと同じくらいなわかりづらさで有名かと想像します。サービスプリンシパルワカンナイヨーから脱却するための情報を本記事で提供出来たらなと思い一筆したためました。後は自分の備忘録のためです(´・ω・`)

ということでまずはじめにサービスプリンシパルについての概念的な説明をします。この概念だけの説明ではなかなかわからないので、それを補足する形でいくつかの利用例を交えながら、より詳細に説明していきたいと思います。

サービスプリンシパルってなに?

ややこしいサービスプリンシパルですが、なんでややこしいかというと色んな用途に使われるからです。Azure CLIでAzureリソースにアクセスするための認証とかOAuthとかOpenID Connectでも登場します。でも、どんな利用方法でも、共通して以下の2つの性質を持っています。

  • サービスプリンシパルは、アプリケーション用のIDである。
  • サービスプリンシパルは、アプリケーションの具現化インスタンスである。

サービスプリンシパルは、アプリケーション用のIDである

サービスプリンシパルは、アプリケーションが何かのリソースにアクセスするためのIDです。例えばOffice365にアクセスするためにはAzure ADにユーザーというものを作って、人間であるユーザーはOffice365にユーザー名とパスワードを渡してアクセスします。同じように、アプリケーションがAzureのリソースにアクセスしてなにかの情報(例えば仮想マシンの情報など)を取得する場合、Azure ADにサービスプリンシパルを作成して、アプリケーションはAzure(あくまで例です)にクライアントIDとシークレットを渡してAzure のリソースにアクセスします。以下のようなイメージです。

つまり、人間がなにかにアクセスするときにはAzure ADにユーザーを作る、アプリケーションがなにかにアクセスするときにはAzure ADにサービスプリンシパルを作るのです。

なんでわざわざサービスプリンシパルなんてもんを作らなければいけないのか、今までみたいにユーザーで認証でもいいじゃないかと思いますが、色々と不都合があります。

まず、アプリケーションが何かのサービスにアクセスするときにユーザー名でアクセスするようにしてしまうと、そのユーザーが退職などの理由で削除されたときにアプリケーションはサービスにアクセスできなくなってしまいます。

じゃぁ、そのユーザーをアプリケーション専用のユーザーにしてしまえばいいではないかと思いますが、それもあんまりよくありません。サービスプリンシパルにはアプリケーションがサービスにアクセスするために便利な機能がたくさんあります。シークレットに簡単に有効期限(1年、2年、無期限)をつけられたり、証明書認証にすることも簡単に出来ます。便利なSDKもサービスプリンシパル利用前提であるものが多いです。また、マネージドIDなどでもサービスプリンシパルを利用できるのです。マネージドIDの説明は今回省きますが、簡単にいいますと、例えば仮想マシンのマネージドIDを有効にすると、その仮想マシンからしかアクセスできない認証エンドポイントみたいなのができます。その認証エンドポイントにアクセスすると、Azureのリソースにアクセスするためのトークンをゲット出来ます。この認証エンドポイントにアクセスする際は資格情報は不要です。つまり、ソースコード中にAzureリソースにアクセスするための資格情報を書く必要がなくなるのです。この説明だけではよくわからないと思いますが、とにかくサービスプリンシパルにはアプリケーションがサービスにアクセスするための便利な機能がたくさんあると思って頂ければOKです。

サービスプリンシパルは、アプリケーションの具現化インスタンスである

抽象的な説明ですみません。これはつまり、サービスプリンシパルはそれ単体では存在できないということになります。必ず「アプリケーション」なるものと1対1もしくは1対多の関係になります。つまり、サービスプリンシパルを作るには、まず「アプリケーション」というものを作ります。するとそれに対応したサービスプリンシパルが自動的にAzure AD内に出来上がります。アプリケーションはクライアントIDやシークレット(ユーザーでいうところのユーザー名やパスワード)などの基本的な情報を持っていて、その他の多機能な情報はサービスプリンシパル側に持ちます。ちょうどオブジェクト指向でいうところのクラスとインスタンスの関係みたいですね。なので「アプリケーションの具象化インスタンス」って表現しました。アプリケーションの具現化インスタンスであるサービスプリンシパルは、アプリケーションで持っているクライアントIDやシークレットなどの基本情報を引き継いで持っているのです。ちょうどこの関係を図にしたいのが以下になります。

アプリケーションとサービスプリンシパルの関係については、なかなか一言では言い表しにくいので、ここではまだふわっとした理解で問題ありません。ただ、ちょっと上の図をもとに軽く説明させて下さい。

Azure ADの中に先程説明したアプリケーションというものがあります。これは後ほど説明しますが、Azureポータルの「アプリの登録」から登録出来るものです。ここではクライアントIDやシークレット情報(パスワードとか証明書とか)を定義します。

アプリケーションを作成すると、同時にサービスプリンシパルも作られます。これが先程作成したアプリケーションの実体となるもので、基本的にアプリケーションだけでは何も機能しなくて、実際に利用するのは、このサービスプリンシパルです。これも後ほど説明しますが、Azureポータルの「エンタープライズアプリケーション」から確認ができます。

「サービスプリンシパルはアプリケーションの実態で、サービスプリンシパルがないとIDとして機能しない」というのはAzureのルールなのでそのまま覚えてもいいと思います。なんともややこしい概念ですね(´・ω・`)

話が長くなりましたが、サービスプリンシパルが持つ基本的な性質はこの2つだけで、この性質がアプリケーション用のIDとしてはかなり大きな意味を持つことになります。

概念的な話ばかりなのもアレなので、次から実際の利用例も交えて説明してみたいとおもいます。

Azureのリソースにサービスプリンシパルでアクセスする

おそらく最も多い利用例であるかと思います。サービスプリンシパルで仮想マシンの情報を取得してみます。

アプリケーションの登録

では、早速実践してみましょう!!流れとしては以下のような感じになります。

 

まずは、アプリケーションの登録から始めます。サービスプリンシパルを作成するにはアプリケーショが必須なので、まずはアプリケーションの登録から始めます。Azureポータルの一番上のテキストボックスに「アプリの登録」と入力して表示される「アプリの登録」をクリックします。

 

「新規登録」をクリックします。

 

「名前」には任意の名称、「サポートされているアカウントの種類」は、「この組織ディレクトリのみに含まれるアカウント (既定のディレクトリ のみ – シングル テナント)」にチェックをして、「登録」をクリックします。

 

「アプリの登録」の一覧で見ますと、作成されていることが確認出来ます。このとき表示されるクライアントIDは後で使うのでメモしておいて下さい。

 

 

クリックすると詳細画面が現れます。ディレクトリ(テナントID)は後ほど使いますので、メモって下さい。

Azureポータルからアプリケーションを作成すると、サービスプリンシパルも自動で作成されます。確認してみましょう。Azureポータルの一番上のテキストボックスに「エンタープライズアプリケーション」と入力して表示される「エンタープライズアプリケーション」をクリックします。

 

先程作成した「vmsp」というサービスプリンシパルがあることが確認できます。しかしこの「エンタープライズアプリケーション」ってわかりにくい名前ですね。。。MSのドキュメントにはほとんど「サービスプリンシパル」って書いてあるので「サービスプリンシパル」でいいんじゃないかなぁと思っております(´・ω・`)なにはともあれ、ここでサービスプリンシパルはアプリケーションと対の関係であり、アプリケーションの具現化インスタンスであることが確認できたかと思います。

 

シークレットを作成します。ログインするためのパスワードのようなものです。先程作成したアプリケーションの詳細画面にて、「証明書とシークレット」をクリックします。認証方式は証明書とパスワードがあります。今回はパスワードでやりますので、「新しいクライアントシークレット」をクリックします。

 

「説明」にはこのシークレットの説明、「有効期限」はまさしくこのシークレットの有効期限で、今回は検証用途なので適用でよいです(´・ω・`) 最後に「追加」をクリックします。

 

シークレットが作成されます。最初の一度しか表示されませんし、後で使いますので、忘れずにめもってください(`・ω・´)シャキーン

 

現段階で、ここまで出来ました。

ロールの作成

Azureのリソースにアクセスする場合は、ロールの作成が必要になります。適宜ロールを作成しないと、何のリソースにもアクセスできません。ということで先程のサービスプリンシパルにロールを付与します。今回は仮想マシンの詳細を表示したいので、そのためのロールを付与します。

ということで、対象の仮想マシンをAzureポータルにて表示して、「アクセス制御(IAM)」→「+追加」→「ロールの割り当ての追加」の順にクリックします。

 

「役割」は「仮想マシン共同作成者」、「アクセスの割り当て先」は「ユーザー、グループ、またはサービスプリンシパル」、「選択」は「vmsp」(先程作成したサービスプリンシパルの名前)を入力します。すると、先程作成したサービスプリンシパルが表示されますので、選択をして「保存」をクリックします。

これで準備は整いました。今の状態は以下のような感じです。

サービスプリンシパルでアクセス

では、サービスプリンシパルでアクセスしてみましょう。以下のコマンドを実行して下さい。

$ az login --service-principal --username [先程メモしたクライアントID] --password [先程メモしたシークレット] --tenant [先程メモしたテナントID]
[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "id": "XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "isDefault": true,
    "managedByTenants": [],
    "name": "MVP Extended Azure Benefit適用サブスクリプション",
    "state": "Enabled",
    "tenantId": "XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "user": {
      "name": "XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
      "type": "servicePrincipal"
    }
  }
]

 

先程権限を与えた仮想マシンを見てみます。

$ az vm show -g spotvm_test -n spotvm
{
"additionalCapabilities": null,
"availabilitySet": null,
"billingProfile": {
"maxPrice": -1.0
},
…

 

おー、ちゃんと見れますね。では次に権限を与えていない仮想マシンを見てみましょう。

$ az vm show -g fileserver -n bastion
The client 'XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' with object id 'XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' does not have authorization to perform action 'Microsoft.Compute/virtualMachines/read' over scope '/subscriptions/XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/fileserver/providers/Microsoft.Compute/virtualMachines/bastion' or the scope is invalid. If access was recently granted, please refresh your credentials.

予想通り、ロールがきちんと付与されていないリソースは見ることが出来ませんでした。

 

先の2つのコマンドを実行したとき、以下のような流れのことが起きています。

①で、az loginコマンドでサービスプリンシパルで定義したクライアントIDとシークレットの情報をAzure ADに対して提示しています。ここで、サービスプリンシパルはアプリケーションのIDであることが確認できたかと思います。

②では、①で認証がOKとなるとトークンが返ってきます。これはJWT(JSON Web Token)形式のアクセストークンです。

③で、そのトークンをAzureのリソースに提示します。Azureのリソースとは、Azureの仮想マシンの情報を提供してくれるサービースのことです。これと同じようなものとして、Office365の各種情報を提供してくれるサービスとか、SharePointの情報を提供してくれるサービスがあります。

④で、Azureのリソースを提供してくれるサービスは、Azureサブスクリプション内で定義したロールの情報を参照します。

⑤では、④で十分な認可権限を持っていると判断した場合にのみ、クライアントにAzureに関する情報(今回であれば仮想マシンに関する情報)を返します。

いかがでしたでしょうか?これまでの過程で、サービスプリンシパルの2つの基本的性質である「サービスプリンシパルは、アプリケーション用のIDである」「サービスプリンシパルは、アプリケーションの具現化インスタンスである」ということが確認できたかと思います。

【ちょい補足】サービスプリンシパルを削除してみる

ちなみにちょっとここでサービスプリンシパルだけを削除してみたいと思います。エンタープライズアプリケーションの一覧画面から該当のサービスプリンシパルのプロパティにアクセスして「削除」をクリックします。

 

先ほどと同じように仮想マシンの情報を取得しようとしてみると、、、

$ az vm show -g spotvm_test -n spotvm
The received access token is not valid: at least one of the claims 'puid' or 'altsecid' or 'oid' should be present. If you are accessing as application please make sure service principal is properly created in the tenant.

あら、エラーになりました。やはり、アプリケーションだけあってもサービスプリンシパルがないとダメのようです(´・ω・`)

ということでやっぱり「サービスプリンシパルは、アプリケーションの具現化インスタンスである」なのですね(๑•̀ㅂ•́)و✧

サービスプリンシパルをOAuthの認証認可で使ってみる

これも結構利用する使い方だと思います。サービスプリンシパルは、Azure AD上でOAuthを実現するときにも利用されます。

OAuthの詳細の説明は割愛しますが、おさらいの意味も込めて、認可コードフローの概要を軽く説明したいと思います。イメージとしては以下のとおりです。

 

これをAzure上の利用に置き換えると以下のようになります。認証サーバーはAzure ADになって、リソースサーバーはマイクロソフトが提供する各種サービス(Microsoft Graph)だったり、自分で作成したアプリもリソースサーバーにもちろん出来ます。今回は以下のようなことをサービスプリンシパルを使ってやってみます。

 

より詳細なイメージ図は以下のとおりです。今回は、Azure ADを認証サーバーとして、自作アプリをリソースサーバー、クライアント(Webアプリ)も自作アプリという想定で、このフローを実現するためにサービスプリンシパルがどのように使われるかを説明していきます。

上図のクライアントと書いてあるのは、OAuthの文脈で言うところのクライアントです。今回は自作Webアプリを想定しています。そしてAzure ADにはクライアントに対応したアプリケーションおよびサービスプリンシパルを作成する必要があります。これはOAuthでいうところのクライアントID、シークレット、認証エンドポイントやコールバックURLを定義するためのものです。でも自作アプリといっても今回作成するわけではありません。

リソースサーバーも同じく自作とします。こちらも実際に自作アプリを作成するわけではありません。あくまでもその想定として、Azure側に設定を行うといった意味合いです。Microsoft Graphなどのマイクロソフトが提供するリソースサーバーでも良かったのですが、自作したほうが理解がすすむためになります。同じくリソースサーバーもアプリケーションおよびサービスプリンシパルを作成する必要があります。これをやらないとうまくスコープが登録出来ません。

クライアントの登録

では早速実践してみましょう。まずはクライアントに対応するアプリケーションとサービスプリンシパルを作成します。名前は任意のもので大丈夫です。「サポートされているアカウントの種類」は今回認証で使うAzure ADは、このアプリケーションを作成するAzure ADと同じものなので、「この組織ディレクトリのみに含まれるアカウント (既定のディレクトリ のみ – シングル テナント)」にします。「リダイレクトURI」はOAuthでいうところのコールバックURLです。とりあえず「https://localhost/」とします。

 

シークレットを作成します。OAuthの文脈でいうところのシークレットです。先程作成したアプリケーションの詳細画面にて、「証明書とシークレット」→「新しいクライアントシークレット」の順にクリックします。

 

「説明」にはこのシークレットの説明、「有効期限」はまさしくこのシークレットの有効期限で、今回は検証用途なので適用でよいです(´・ω・`) 最後に「追加」をクリックします。

 

シークレットが作成されます。最初の一度しか表示されませんし、後で使いますので、忘れずにめもってください(`・ω・´)シャキーン

今、ここまで出来ました。

リソースサーバーの登録

リソースサーバーをAzure ADに登録します。今回は自分でリソースサーバーを作ったと仮定します。Azure ADではリソースサーバーも「アプリケーション」として登録する必要があります。これはAzure ADから返されるアクセストークンのスコープに、リソースサーバーの権限を含めるためです。

Azure ADから返されるアクセストークンは以下のような形式になっています。Azure ADから返されるアクセストークンはJWT(JSON Web Token)形式になっていて、ペイロードの部分のみ抜粋しました。下記のscpフィールドにOAuthのスコープが入ってきます。スコープの名称はリソースサーバーが解釈できる任意の名称でOKなのですが、例えばリソースサーバーのファイルを読み込みたいのであればfile.read、書き込みたいのであればfile.writeなどがあります。このスコープの値をもとにして、クライアントのリソースサーバーに対する権限が決まります。アクセストークン内に、リソースサーバーが解釈できるスコープを指定するために、リソースサーバーをAzure ADにアプリケーションとして登録する必要があるのです。

{
  "aud": "api://2b2d079f-5380-4e54-a6ac-b9d72254be6f",
  "iss": "https://sts.windows.net/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/",
  "iat": 1610677022,
  "nbf": 1610677022,
  "exp": 1610680922,
  "acr": "1",
  "aio": "E2JgYLhQxOsbYf2JTcZOP2HBgYlz/zCFSR1Tmxv7Xq+peuVR07cA",
  "amr": [
    "pwd"
  ],
  "appid": "b1282d85-f769-475f-a73a-0cb63b69b350",
  "appidacr": "1",
  "ipaddr": "61.192.198.153",
  "name": "testuser01",
  "oid": "5e4db348-231e-4ddb-8728-8c640e6ad6ea",
  "rh": "0.AAAAqrv-zpxef0qwXcCtm4Y0IoUtKLFp919HpzoMtjtps1BTACA.",
  "scp": "file.read",
  "sub": "ua8ITLObsnlYj__YAK4zWjM9mBL8kMNSHqxg4K-zejc",
  "tid": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "unique_name": "testuser01@XXXX.onmicrosoft.com",
  "upn": "testuser01@XXXX.onmicrosoft.com",
  "uti": "yB6qL0OdY022Vcz4UM0HAA",
  "ver": "1.0"
}

 

では早速実践してみましょう!!クライアントのときと同様、アプリケーションを登録します。名前は任意のもので大丈夫です。「サポートされているアカウントの種類」は今回認証で使うAzure ADは、このアプリケーションを作成するAzure ADと同じものなので、「この組織ディレクトリのみに含まれるアカウント (既定のディレクトリ のみ – シングル テナント)」にします。「リダイレクトURI」は空っぽで結構です。

 

先程ご説明したスコープを追加します。「APIの公開」→「Scopeの追加」の順にクリックします。

 

アプリケーションIDのURIを定義します。どうやらこれは世界中のAzure AD全体でスコープを一意に識別するためのプレフィックスのようなものらしいです。アプリケーションIDは世界中のAzure AD全体一意の値でなければいけないようで、デフォルトで表示されているものは既にその要件を満たしているので、そのまま「保存してから続ける」をクリックします。

 

リソースサーバー内のファイルを読み取る権限をクライアントに与えると想定します。「スコープ名」は「files.read」とします。「同意できるのはだれですか?」は、今回はAzure ADに管理権限を持たないユーザーも対象ですので「管理者とユーザー」を選択します。「管理者の同意の表示名」「管理者の同意の説明」は後にご説明する同意画面に表示されるので、とりあえず以下のように入力しておいて下さい。最後に「スコープの追加」をクリックします。

 

ここまで出来ました。

APIのアクセス許可

リソースサーバーに送るアクセストークンのスコープに、先程定義した「files.read」を加えるためにはもうひと手間必要で、それが「APIのアクセス許可」という作業です。「クライアントの登録」で登録したアプリケーションを開いて、「APIのアクセス許可」→「+アクセス許可の追加」の順にクリックします。

 

「自分のAPI」をクリックすると先程登録したresourceというアプリケーションが表示されますので、クリックします。

 

「委任されたアクセス許可」を選択して、「files.read」にチェックを入れて、最後に「アクセス許可の追加」をクリックします。

ところでここで「委任されたアクセス許可」「アプリケーションのアクセス許可」の違いについて説明します。

「委任されたアクセス許可」とは、ユーザーが登場するようなOAuthのフロー(認可コードフローとか)において、クライアント(アクセストークンを受け取るWebアプリとか)がアクセストークンをAzure ADからもらうためには、必ずユーザーの同意が必要なアクセス許可のことをいいます。よくあるユースケースとしては、他のサービスにFacebookのユーザーでログインできるケースなのですが、あのときって、「Facebookログイン」のボタンをクリックすると、一旦Facebookの認証画面が表示されて、認証後に「XXXというサービスに以下の情報を送っていいですか?」という画面が出ます。あれが同意画面で、同意するとFacebookはアクセストークンをクライアントに発行します。

「アプリケーションの許可」とは、ユーザーの介在しない処理で使います。例えばデーモンとかバッチ処理とかですね。この場合、デーモンやバッチ処理が一人で勝手に色々やるので、ユーザーの同意もへったくれもありません。このスコープをAzure ADに要求する場合は「アプリケーションの許可」を選択します。そして、別の画面で、管理権限を持ったユーザーの同意が必要になります。ちなみにこの場合、アクセストークンを取得するときはClient Credentialフロー(クライアントIDとシークレットを指定して、直接アクセストークンを取得する方法)で行います。

ちょっと話がそれてしまいましたが、これで準備は整いました。

アクセストークンを取得してみる

準備は整ったので、アクセストークを取得してみましょう。

まずAzure ADの認証エンドポイントにアクセスしてみましょう。ブラウザに以下のURLを打ち込んで見て下さい。[]で囲んでいるところはこれから説明します。

https://login.microsoftonline.com/[テナントID]/oauth2/v2.0/authorize
?client_id=[クライアントID]
&response_type=code
&redirect_uri=https://localhost/
&state=12345
&scope=[アプリケーションIDを含んだスコープ]

[テナントID]は「クライアントの登録」で登録したアプリケーションの属するAzure ADのテナントのIDです(下図の「ディレクトリ (テナント) ID」)。

クライアントIDは同様に下図の「アプリケーション (クライアント) ID」になります。

response_typeは今回認可コードフローを表すcodeとします。

stateは任意の値で結構です(stateの詳細についてはこちらを御覧ください)。

 

scopeについては、新しく追加したスコープをクリックして表示される以下の値(アプリケーションIDを含むスコープ)になります。

 

先のURLを叩くと、ログイン画面が表示されます。Azure ADに登録しているユーザーでログインすると、以下の画面が表示されます。こちらが先程ご説明した同意画面になります。これに同意すると、クライアントはリソースサーバーにアクセスするためのトークンを取得して、ユーザーに成り代わり、ユーザーの情報をリソースサーバーから取得出来ます。ちゃんと信頼できるクライアントじゃないと、自分に関する情報をクライアントに渡すのって怖いですよね。だからこういう同意画面があるのです。「Accept」をクリックします。

 

すると以下のようなURLにリダイレクトされます。codeで指定された部分が認可コードになります。

https://localhost/?code=XXXXXX&state=12345&session_state=XXXXX

 

ここまでで以下の①〜③までのフローが進んだことになります。ユーザーはAzure ADの認証エンドポイントにアクセスし、認証して同意画面にて同意、その後、クライアントのアプリケーションで登録したリダイレクトURLにリダイレクトされた形です。そのクエリパラメータ−に認可コード(code=xxxxxxの部分)が付与されて、クライアントに認可コードが渡った形になります。

 

さて、次はいよいよAzure ADからアクセストークンをゲットしてみます。以下のコマンドを実行して下さい。これは、Azure ADのトークンエンドポイントからアクセストークンを取得するためのcurlコマンドなのです。ここでは人(この記事を読んでいるアナタ)がこのコマンドを叩く想定ですが、実際はアプリケーションがこれを行います。今回、テスト用のアプリケーション作るのがちょっと手間だったので、アナタが叩きます。

$ curl -X POST https://login.microsoftonline.com/[テナントID]/oauth2/v2.0/token \
-F grant_type=authorization_code \
-F code=[先程取得した認可コード] \
-F scope=[アプリケーションIDを含んだスコープ] \
-F client_id=[クライアントID] \
-F client_secret=[シークレット] \
-F redirect_uri=https://localhost/

 

[テナントID]は、先程認証エンドポイントにアクセスしたときに指定したのと同じものです。

grant_typeは、認可コードフローを表すauthorization_codeを指定します。

[先程取得した認可コード]は、まさに先程取得した認可コードを指定します。

[アプリケーションIDを含んだスコープ]は、先程認証エンドポイントにアクセスしたときに指定したのと同じものです。

[クライアントID]は、先程認証エンドポイントにアクセスしたときに指定したのと同じものです。

[シークレット]は、指定したクライアントIDに対応したシークレットです。

redirect_uriは、先程認証エンドポイントにアクセスしたときに指定したのと同じものです。

上記のコマンドを実行すると以下のようなレスポンスが返ってきます。このaccess_tokenのフィールドに入っている値こそがアクセストークンそのものになります。

{"token_type":"Bearer","scope":"api://76ff1cc7-67d8-41df-938e-ba82184456e0/files.read","expires_in":3599,"ext_expires_in":3599,"access_token":"XXXXXX"}

 

ここまでの処理で、以下の④〜⑤まで終わったことになります。

アプリケーションであるクライアントが、サービスプリンシパルでAzure ADに認証して、アクセストークンを受け取っています。まさに最初にお話したサービスプリンシパルの重要な2つの性質の1つである「サービスプリンシパルは、アプリケーション用のIDである」を満たしていることとなります。

さて、先程取得したアクセストークンの中身を見てみましょう。アクセストークンはヘッダー部とペイロード部がbase64でエンコードされた上でその他色々複雑な処理でエンコードされている(詳細はこちら参照)ので、それをデコードします。でもコマンドでやるのがめんどくさいので、以下のサイトを使うと簡単にデコードできます。

https://jwt.io/

アクセストークンの中身は以下のようになっています。ペイロードの部分のみ抜粋します。

{
  "aud": "api://76ff1cc7-67d8-41df-938e-ba82184456e0",
  "iss": "https://sts.windows.net/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/",
  "iat": 1610712411,
  "nbf": 1610712411,
  "exp": 1610716311,
  "acr": "1",
  "aio": "E2JgYEhbnRwwPyYiN/a3wzmbhjL/mGl/T0nqbOVo6M9per6o1BAA",
  "amr": [
    "pwd"
  ],
  "appid": "b1282d85-f769-475f-a73a-0cb63b69b350",
  "appidacr": "1",
  "ipaddr": "61.192.198.153",
  "name": "testuser01",
  "oid": "5e4db348-231e-4ddb-8728-8c640e6ad6ea",
  "rh": "0.AAAAqrv-zpxef0qwXcCtm4Y0IoUtKLFp919HpzoMtjtps1BTACA.",
  "scp": "files.read",
  "sub": "3YK3qXXav1nKT9mGdHfL7DbEVtrzpUl2VFqyKtDf4SI",
  "tid": "cefebbaa-5e9c-4a7f-b05d-c0ad9b863422",
  "unique_name": "testuser01@mailnoriyukitakei.onmicrosoft.com",
  "upn": "testuser01@mailnoriyukitakei.onmicrosoft.com",
  "uti": "zUbG6ukNR0ed2SXP8o0NAA",
  "ver": "1.0"
}

scpの部分がスコープです。このアクセストークンを渡されたリソースサーバーは、このscpで指定されたスコープを見て、このアクセストークンと引き換えにクライアントに渡していい情報の範囲を判断します。このケースではファイルの読み取り権限だけを与えます。まさしく認可ですね。

 

さて、次はリソースサーバーにアクセストークンを渡して、情報を取得するという処理(下図⑥〜⑦)なのですが、この部分は今回のお話の本質ではないので割愛させていただきます。

 

ここまでの流れでおわかりかと思いますが、アプリケーションのIDであるサービスプリンシパルは、ユーザーから権限(このケースで言うところのファイルの読み取り権限)を委譲されて、ユーザーの代わりにリソースサーバーにアクセスをしております。このようにサービスプリンシパルは、ユーザーから権限を委譲されることができるという機能も持っております。

その証拠として、クライアントのサービスプリンシパルにアクセスして、「アクセス許可」→「ユーザーの同意」の順にクリックしてみます。先程ユーザーが同意した内容が表示されています。

 

「XX名の合計ユーザー」をクリックすると、同意したユーザーが表示されます。

 

マルチテナントアプリケーションでサービスプリンシパルを使う

これで最後の利用例なのですが、マルチテナントアプリケーションでサービスプリンシパルを使う例を考えてみたいと思います。この利用例で、サービスプリンシパルの重要な性質の1つである「サービスプリンシパルは、アプリケーションの具現化インスタンスである」がやっとこさ有効活用されます。

ここで以下の要件を満たすアプリケーションを考えてみます。

  • Graph API(Azure ADの様々な情報を取得出来るAPI)を使ってAzure ADにログインしたユーザーの情報を取得するアプリケーションを作る。
  • このアプリケーションは、あらゆるAzure ADテナントの情報を取得できる。つまり、Azure ADテナントAにいるユーザーhogeの情報も取得出来るし、Azure ADテナントBにいるユーザーfugaの情報も取得出来る。

この要件を満たすアプリケーションをパッと考えると以下のような構成がまず思いつきます。下図の「クライアント」はGraph APIにアクセスしてAzure AD内のユーザーを取得するWebアプリケーションの想定です。

 

サービスプリンシパルをOAuthの認証認可で使ってみる」で説明したように、サービスプリンシパルはユーザーの同意を得ることによって、ユーザーに成り代わってリソースサーバーにアクセスするためのアクセストークンを取得することが出来ます。ただしこれは、サービスプリンシパルと同じAzure ADに所属するユーザーに限ります。

なので、上図のクライアントがAzure ADにアクセスしてアクセストークンを取得するためのサービスプリンシパルは各Azure ADテナントに必要なこととなり、もちろん先程から申し上げているように、サービスプリンシパルはそれ単体では存在出来ないので、アプリケーションも各テナントに作成することとなります。

ただし、これは現実的な運用ではありません。このクライアントを利用するお客様が増えるたびに、そのお客様のAzure ADテナントにお邪魔して、アプリケーションを作るのか、それともお客様自身にこの複雑な手順を実施してもらうのか、どちらにしても簡単ではありませんよね。

実はAzure ADには、こんな労力をかけなくても、このようなマルチテナントアプリケーション(と呼びます)を実現する方法があり、以下のような構成を取ることが可能なのです。

上図の構成をざっくり説明しますと、テナントAのAzure ADにクライアントに対応したアプリケーションを作るのですが、その際に「このアプリケーションはマルチテナント対応ですよー」っていうチェックボックスにチェックを入れるだけです。

テナントBのAzure ADのユーザーがこのクライアントを利用すると、Azure ADログイン後に同意画面が表示され、同意すると、なんとテナントAのAzure ADにあるアプリケーションの具現化インスタンスであるサービスプリンシパルがテナントBのAzure ADに作成されるのです。あとは、クライアントはテナントBのAzure ADにサービスプリンシパルを提示して、アクセストークンを取得すればOKです。

先程みたいにわざわざ各Azure ADテナントにアプリケーションを作成して回る必要はまったくないのです。

今までにご説明したシングルテナントのアプリケーションでは、「サービスプリンシパルは、アプリケーションの具現化インスタンスである」という性質の恩恵を受けることはあまりなかったですが、マルチテナントアプリケーションでは真価を発揮するのです!!

またサービスプリンシパルにだけ設定できる固有の機能に「条件付きアクセス」というのがあります。これは、いわゆる多要素認証を定義するもので、ワンタイムパスワードや生体認証ができます。つまりマルチテナントアプリケーションへの認証方法を各Azure ADテナントごとに定義できるのです。下図の例では、テナントBのAzure ADではプッシュ通知による認証、テナントCのAzure ADでは生体認証を行っています。クライアントIDやシークレットなど全てのサービスプリンシパルに必須の共通機能はアプリケーションから引き継ぎつつ、各サービスプリンシパル固有の機能を定義できます。

上図のような使い方を考えてみると「サービスプリンシパルは、アプリケーションの具現化インスタンスである」という性質は、最初はなんだかややこしいように思えましたが、今はとても合理的な機能のように思えてきませんでしょうか。私は非常によく考えられたアーキテクチャだと思います。

ちなみにOffice365やTeamsなどAzure ADを認証基盤としたコンシューマー向けのサービスは、上記の仕組みを使っています。試しにご自分のAzure ADにあるサービスプリンシパルを見てみますと、色々なサービスのものがあるはずです。これは、上記の仕組みを使って作成されたサービスプリンシパルなのです。「Office365用のサービスプリンシパル」とかあるはずです(多分)。

説明ばかりではあれですので、いつものように実際に手を動かしてマルチテナントアプリケーションを作ってみましょう!!以下のような構成のものをつくってみます。

ざっくり説明しますと、上図のクライアントはGraph APIにアクセスするためのWebアプリケーションです。Azure AD上にアプリケーションとして登録します。

また別のAzure ADテナントを作成して、そのユーザーでログイン、同意後(①)、サービスプリンシパルがAzureADテナントに作成されます(②)。認可コードを取得して(③、④)、その認可コードとサービスプリンシパル(クライントID、シークレット)をAzure ADに提示し(⑤)、その結果としてアクセストークンを取得し(⑥)、そのアクセストークンを持ってGraph APIにアクセスするという流れです(⑦、⑧)。

どうして今回は「サービスプリンシパルをOAuthの認証認可で使ってみる」のときのようにリソースサーバーはGraph APIなのかというと、どうやら自分で作成したリソースサーバーは、きちんとマイクロソフト側の認可を受けたクライアントでないと、そのスコープを追加出来ないような仕様になっているっぽいです。

ざっくりし過ぎしましたが、詳細は以降の手順にて説明します。

クライアントの登録

サービスプリンシパルをOAuthの認証認可で使ってみる」のときと同じように、クライアントに対応したアプリケーションを作成します。名前は任意のもので大丈夫です。「サポートされているアカウントの種類」は今回はマルチテナントアプリケーションなので、「任意の組織ディレクトリ内のアカウント (任意の Azure AD ディレクトリ – マルチテナント)」にします。「リダイレクトURI」はOAuthでいうところのコールバックURLです。とりあえず「https://localhost/」とします。最後に「登録」をクリックします。

このクライアントからGraph APIにアクセス出来るようにします。正確にいいますと、Azure ADから返されるアクセストークンのスコープに、Graph APIのスコープが含まれるようにします。と言いつつ実はデフォルトでその構成が出来ております。「APIのアクセス許可」をクリックして下さい。「Microsoft Graph」の「Users.Read」というのが、Azure ADのユーザー情報を取得するGraph APIのスコープです。

 

シークレットを作成します。OAuthの文脈でいうところのシークレットです。先程作成したアプリケーションの詳細画面にて、「証明書とシークレット」→「新しいクライアントシークレット」の順にクリックします。

 

「説明」にはこのシークレットの説明、「有効期限」はまさしくこのシークレットの有効期限で、今回は検証用途なので適用でよいです(´・ω・`) 最後に「追加」をクリックします。

 

シークレットが作成されます。最初の一度しか表示されませんし、後で使いますので、忘れずにめもってください(`・ω・´)シャキーン

実はマルチテナントアプリケーションはこれで準備完了なのです。あっけないですね。今、ここまで出来ました。

アクセストークンを取得してみる

では、アクセストークンを取得してみましょう。

まずAzure ADの認証エンドポイントにアクセスしてみましょう。ブラウザに以下のURLを打ち込んで見て下さい。[]で囲んでいるところはこれから説明します。

https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize
?client_id=[クライアントID]
&response_type=code
&redirect_uri=https://localhost/
&state=12345
&scope=https://graph.microsoft.com/User.Read

[テナントID]は「クライアントの登録」で登録したアプリケーションの属するAzure ADのテナントのIDです(下図の「ディレクトリ (テナント) ID」)。

クライアントIDは同様に下図の「アプリケーション (クライアント) ID」になります。

response_typeは今回認可コードフローを表すcodeとします。

stateは任意の値で結構です(stateの詳細についてはこちらを御覧ください)。

scopeは、Graph APIのユーザー情報取得権限を表すhttps://graph.microsoft.com/User.Readを指定します。

 

ところで、マルチテナントの場合は、認証エンドポイントのURLが「https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize」です。「サービスプリンシパルをOAuthの認証認可で使ってみる」のときのようなシングルテナントでは、organizationsのところはテナントIDでしたね。おそらく入力したユーザー名(ユーザープリンシパル名)のドメインの部分で、認証するAzure ADテナントを判断しているのだと思います。

先のURLを叩くと、ログイン画面が表示されます。先ほどクライアントを登録したAzure ADとは全くベルのAzure ADに登録しているユーザーでログインすると、以下の画面が表示されます。「サービスプリンシパルをOAuthの認証認可で使ってみる」のときと同じような同意画面です。「Accept」をクリックします。

 

すると以下のようなURLにリダイレクトされます。codeで指定された部分が認可コードになります。

https://localhost/?code=XXXXXX&state=12345&session_state=XXXXX

 

ここまでで以下の①〜④までのフローが進んだことになります。ユーザーはAzure ADの認証エンドポイントにアクセスし、認証して同意画面にて同意しました(①)。その後、認証したユーザーが所属するAzure ADにサービスプリンシパルが作成されます(②)。次に、クライアントのアプリケーションで登録したリダイレクトURLにリダイレクトされ、そのクエリパラメータ−に認可コード(code=xxxxxxの部分)が付与されて、クライアントに認可コードが渡ります。ほぼほぼ「サービスプリンシパルをOAuthの認証認可で使ってみる」のシングルテナントのときと同じなのですが、決定的に違うのは②ですね。別のテナントにサービスプリンシパルを作成しています。本当に作成されているか見てみましょう。

 

認証したユーザーが所属するAzure ADのサービスプリンシパルの一覧を見てみますと、たしかに作成されていますね。確認するとわかりますが、このサービスプリンシパルのアプリケーションIDは、「クライアントの登録」で作成したアプリケーションのアプリケーションIDと同じです。

 

次はいよいよAzure ADからアクセストークンをゲットしてみます。以下のコマンドを実行して下さい。このあたりの手順も、「サービスプリンシパルをOAuthの認証認可で使ってみる」のシングルテナントのときと同じです。

$ curl -X POST https://login.microsoftonline.com/organizations/oauth2/v2.0/token \
-F grant_type=authorization_code \
-F code=[先程取得した認可コード] \
-F scope=https://graph.microsoft.com/User.Read \
-F client_id=[クライアントID] \
-F client_secret=[シークレット] \
-F redirect_uri=https://localhost/

grant_typeは、認可コードフローを表すauthorization_codeを指定します。

[先程取得した認可コード]は、まさに先程取得した認可コードを指定します。

scopeは、先程認証エンドポイントにアクセスしたときに指定したのと同じもの(https://graph.microsoft.com/User.Read)です。

[クライアントID]は、先程認証エンドポイントにアクセスしたときに指定したのと同じものです。

[シークレット]は、指定したクライアントIDに対応したシークレットです。

redirect_uriは、先程認証エンドポイントにアクセスしたときに指定したのと同じものです。

上記のコマンドを実行すると以下のようなレスポンスが返ってきます。このaccess_tokenのフィールドに入っている値こそがアクセストークンそのものになります。

{"token_type":"Bearer","scope":"api://76ff1cc7-67d8-41df-938e-ba82184456e0/files.read","expires_in":3599,"ext_expires_in":3599,"access_token":"XXXXXX"}

 

ここまでで以下の処理が終わったことになります。②で別のAzure ADテナントに作成したサービスプリンシパルに、⑤でクライアントがアクセスしています。

 

さて、先程取得したアクセストークンの中身を見てみましょう。アクセストークンはヘッダー部とペイロード部がbase64でエンコードされた上でその他色々複雑な処理でエンコードされている(詳細はこちら参照)ので、それをデコードします。でもコマンドでやるのがめんどくさいので、以下のサイトを使うと簡単にデコードできます。

https://jwt.io/

アクセストークンの中身は以下のようになっています。ペイロードの部分のみ抜粋します。

{
"aud": "https://graph.microsoft.com",
"iss": "https://sts.windows.net/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/",
"iat": 1610731394,
"nbf": 1610731394,
"exp": 1610735294,
"acct": 0,
"acr": "1",
"acrs": [
"urn:user:registersecurityinfo",
"urn:microsoft:req1",
"urn:microsoft:req2",
"urn:microsoft:req3",
"c1",
"c2",
"c3",
"c4",
"c5",
"c6",
"c7",
"c8",
"c9",
"c10",
"c11",
"c12",
"c13",
"c14",
"c15",
"c16",
"c17",
"c18",
"c19",
"c20",
"c21",
"c22",
"c23",
"c24",
"c25"
],
"aio": "E2JgYAhgXjTzbkfnDO/0nc/N1pnGsDKpWEUI597oWOpvl9CQkgIA",
"amr": [
"pwd"
],
"app_displayname": "multitenantapp",
"appid": "f6cc8827-354f-45ac-82a4-a393e0a14298",
"appidacr": "1",
"idtyp": "user",
"ipaddr": "61.192.198.153",
"name": "fuga piyo",
"oid": "4f1bbb32-dca4-4add-92f0-3aa64b99ab5e",
"platf": "5",
"puid": "100320010CA7748E",
"rh": "0.AAAAG_n4MshvW0Sbsho3hZ2VLCeIzPZPNaxFgqSjk-ChQphxAEI.",
"scp": "User.Read profile openid email",
"sub": "hlm6hvj-1VCAudm_XDb8rwtCV5z9CcQJTpXjTPM0FNQ",
"tenant_region_scope": "AS",
"tid": "32f8f91b-6fc8-445b-9bb2-1a37859d952c",
"unique_name": "fuga@hogehogeorg.onmicrosoft.com",
"upn": "fuga@hogehogeorg.onmicrosoft.com",
"uti": "vXd3wp7Z-ESPO09WoEEeAA",
"ver": "1.0",
"wids": [
"b79fbf4d-3ef9-4689-8143-76b194e85509"
],
"xms_st": {
"sub": "Hj1iGvBNKcAjlrmBsbuZQYhXP_Frlx4yE8j2ftqJYoA"
},
"xms_tcdt": 1610716041
}

確かにスコープを示すscpの部分に、Azure ADのユーザー情報を取得するGraph APIのスコープであるUser.Readが指定されています。

さて、次はGraph APIにアクセストークンを渡して、情報を取得するという処理(下図⑦〜⑧)なのですが、この部分は今回のお話の本質ではないので割愛させていただきます。きっとうまくいくでしょう。

まとめ

いかがでしたでしょうか?本記事が、非常に概念のつかみにくいサービスプリンシパルの理解の一助になれば幸いです。うーん、ちょっと長くなっちゃいました(´・ω・`)ショボーン。。。つかれたー。





ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!


ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

6人がこの投稿は役に立ったと言っています。

Be the first to comment

Leave a Reply

Your email address will not be published.


*