※ v2 endpoint に関する内容です。(Azure AD (v1) の場合は、こちら を参照してください。)
Azure AD v2.0 endpoint を使った Azure AD & MSA 対応アプリ開発
- OAuth による Client の開発
- JavaScript Client (AngularJS 等) の開発
- バックエンド連携 (Daemon, Service 等) 開発 (※英語)
- OAuth Token (JWT) の検証
- カスタム Web API の登録とカスタム scope の使用 (※英語)
こんにちは。
AngularJS など、JavaScript を主体としたフロント エンドのアプリケーション (バックエンドのサーバー サイドを使わないアプリケーション) では、「v2.0 endpoint の OAuth を使った Client 開発」で紹介したフローは使用できません。
例えば、JavaScript では、そのページ自身がロードされた元のドメインに対する Request (処理要求) しかできないため、AJAX を使って Azure AD に POST 要求を出すことは普通のやり方では不可能です。(JavaScript のこの制限については「クロス ドメイン (Cross-Domain) 問題の回避と諸注意」を参照してください。)
今回は、こうした JavaScript のみを使って、Azure Active Directory (Azure AD) と Microsoft Account (MSA) の双方に対応した v2.0 endpoint の OAuth フローを処理する Implicit Grant と呼ばれるフローを紹介します。(Azure AD v1 の頃と基本的な考え方は同様ですが、いくつか独自な注意点などもあるので記載します。)
準備 (Application の登録)
「v2.0 endpoint の OAuth を使った Client 開発」で紹介したように、あらかじめ、Application Registration Portal (https://apps.dev.microsoft.com) に Application を登録して Redirect Uri 等を設定しますが、この際、下図の [Allow Implicit Flow] にチェックを付けてください。
このチェックを付けないと、このあと解説するフローで Error (unsupported_response_type) が発生します。
HTTP Flow と Programming – 基本
OAuth の Implicit Grant Flow は以下の通り処理します。
まず、Web ブラウザーを使って、以下の URL に Redirect します。(ここで指定している client_id
, scope
等については「v2.0 endpoint の OAuth を使った Client 開発」を参照してください。)
「v2.0 endpoint の OAuth を使った Client 開発」では、response_type=code
でしたが、今回は response_type=token
としている点に注意してください。
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=token&client_id=7822587c-fed4-4dd3-8e68-165334eb7c92&redirect_uri=https%3A%2F%2Ftestsite.com%2Ftestapp01&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read
上記にアクセスすると、下図のようなログイン画面 (SignIn UI) が表示されます。
ユーザーがこのログイン画面 (SignIn UI) にユーザー ID とパスワードを入力してログインすると、ブラウザーは上記で指定した redirect_uri (今回の場合、https://testsite.com/testapp01) に Redirect し、URI フラグメント (ハッシュ) に access_token を設定して戻ってきます。下記のような URI です。
access token は URI のフラグメント (ハッシュ, #) として含まれるので、ブラウザーがこの URI を処理する際、access token が無意味にネットワーク上に流れることはなく、アクセス URI としてサーバー上のログなどの形で残ることもありません。(この access token は、Response の Location ヘッダーとして Server 側から送られて、クライアントのみで処理されます。)
https://testsite.com/testapp01#access_token=EwCQAsl6BA...&token_type=bearer&expires_in=3600&scope=https://graph.microsoft.com/mail.read
例えば、下記は、ログインをおこなって access token を取得する簡単な JavaScript のサンプル コードです。(上記のフローを実装したサンプル コードです。)
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Test page</title> </head> <body> <button id="btnLog">Login !</button> <div id="txtMsg"></div> <script> (function () { if(location.hash) { var hasharr = location.hash.substr(1).split("&"); hasharr.forEach(function(hashelem) { var elemarr = hashelem.split("="); if(elemarr[0] == "access_token") { document.getElementById('txtMsg').innerHTML = 'Access Token: ' + elemarr[1]; } }, this); } document.getElementById('btnLog').onclick = function() { location.href = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=token&response_mode=fragment&client_id=7822587c-fed4-4dd3-8e68-165334eb7c92&redirect_uri=https%3A%2F%2Ftestsite.com%2Ftest.html&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read'; } }()); </script> </body> </html>
あとは、「v2.0 endpoint の OAuth を使った Client 開発」で紹介したように、この access token の値を Authorization ヘッダーに設定して、API サービス (Microsoft Graph など) を呼び出します。
ここでは、この Microsoft Graph の呼び出しの処理については説明しませんが、Microsoft Graph は CORS に対応しているため、JavaScript からそのまま呼び出せます。(もし、自作の custom の API を呼び出す場合は、「クロス ドメイン (Cross-Domain) 問題の回避と諸注意」で紹介した方法で、CORS や XDM が使用できるように構成しておきましょう。)
HTTP Flow と Programming – 応用
上記はページの Redirect をおこなっていますが、Login 画面を Popup させて access token を取得し、その結果を元の Page (window) に返しても構いません。
この方法だと、使用している Page を遷移させずに token を取得できます。(つまり、画面の状態などが維持できます。)
以下は、Popup でログインをおこない、元の画面に access token を返す簡単なサンプルです。(popup.html がポップアップされる画面であり、ここにログイン画面が表示されます。)
main.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Test page</title> </head> <body> <button id="btnLog">Login !</button> <div id="txtMsg"></div> <script> (function () { document.getElementById('btnLog').onclick = function() { var popup = window.open('popup.html', 'oauth', 'width=500,height=400,status=no,toolbar=no,menubar=no,scrollbars=yes'); popup.focus(); } window.onmessage = function(e){ document.getElementById('txtMsg').innerHTML = 'Access Token: ' + e.data; }; }()); </script> </body> </html>
popup.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Test page</title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <script> (function () { if(location.hash) { var hasharr = location.hash.substr(1).split("&"); hasharr.forEach(function(hashelem) { var elemarr = hashelem.split('='); if(elemarr[0] == 'access_token') { window.opener.postMessage(elemarr[1], 'https://testsite.com/main.html'); window.close(); } }, this); } else { location.href = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=token&response_mode=fragment&client_id=7822587c-fed4-4dd3-8e68-165334eb7c92&redirect_uri=https%3A%2F%2Ftestsite.com%2Fpopup.html&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read'; } }()); </script> </body> </html>
access token には 1 時間という有効期限があります。
期限切れになった場合、「v2.0 endpoint の OAuth を使った Client 開発」では refresh token を使いましたが、OAuth Implicit Grant では、下記の通り、hidden の iframe を使って、再度、上記と同じフローを呼び出して access token を取得します。この際、下記のサンプルのように、prompt=none
を付与して呼び出すことで、ifarme 内でログイン画面 (SignIn UI) の表示を強制的に抑制できます。(ブラウザーの Cookie などがおぼえているため、ログイン済の状態で SSO されて access token が返ってきます。)
補足 : 逆に、prompt=login
にすると、必ず ログイン画面 (SignIn UI) が表示されます。
このフローにより、iframe では、access token が URI のフラグメントに付与されて同じページ (test.html) に戻され、このページ内の sessionStorage.setItem()
により access token が Session Storage に保存されます。以降、このドメイン内では sessionStorage.getItem()
関数で access token を取り出せます。
test.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Test page</title> </head> <body> <button id="btnLog">Login !</button> <button id="btnNew">Renew Token</button> <button id="btnGet">Display Token</button> <div id="txtMsg"></div> <script> (function () { if(location.hash) { var hasharr = location.hash.substr(1).split('&'); hasharr.forEach(function(hashelem) { var elemarr = hashelem.split('='); if(elemarr[0] == 'access_token') { sessionStorage.setItem('token_value', elemarr[1]); } }, this); }; document.getElementById('btnLog').onclick = function() { location.href = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=token&response_mode=fragment&client_id=7822587c-fed4-4dd3-8e68-165334eb7c92&redirect_uri=https%3A%2F%2Ftestsite.com%2Ftest.html&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read'; }; document.getElementById('btnGet').onclick = function() { var token = sessionStorage.getItem('token_value'); document.getElementById('txtMsg').innerHTML = 'Access Token: ' + token; }; // renew access token using hidden iframe document.getElementById('btnNew').onclick = function() { var ifr = document.createElement('iframe'); ifr.style.visibility = 'hidden'; ifr.style.position = 'absolute'; ifr.style.width = ifr.style.height = ifr.borderWidth = '0px'; var frame = document.getElementsByTagName('body')[0].appendChild(ifr); frame.src = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=token&response_mode=fragment&client_id=7822587c-fed4-4dd3-8e68-165334eb7c92&redirect_uri=https%3A%2F%2Ftestsite.com%2Ftest.html&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read&prompt=none&login_hint=tsuyoshi.matsuzaki@hotmail.com'; }; }()); </script> </body> </html>
上記では、redirect_uri を使って同じページに戻していますが、もし、redirect_uri として別ドメインのページを使用する場合は、window.parent.postMessage でこのページに access token を渡すなどすると良いでしょう。(別ドメインのページとの情報交換については「クロス ドメイン (Cross-Domain) 問題の回避と諸注意」を参照してください。)
また、v2.0 endpoint では、上記の通り、必ず login_hint
が必要なので注意してください。
上記では、この login_hint
として固定の値を設定していますが、login_hint
に指定する値は、通常、Id Token の Basic Profile (Claim) に含まれる preferred_username
を使います。(特に、Microsoft Account では、Access Token から Claim 情報の取得はできないので、この Basic Profile の取得は必須です。)
このため、現実のフローでは、上述のような access token の取得だけなく、下記の通り
response_type=id_token+token
、scope=openid%20profile
を指定して basic profile も取得してください。
test.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Test page</title> </head> <body> <button id="btnLog">Login !</button> <button id="btnNew">Renew Token</button> <button id="btnGet">Display Token</button> <div id="txtMsg"></div> <script> (function () { if(location.hash) { var hasharr = location.hash.substr(1).split('&'); hasharr.forEach(function(hashelem) { var elemarr = hashelem.split('='); if(elemarr[0] == 'access_token') { sessionStorage.setItem('token_value', elemarr[1]); } }, this); }; document.getElementById('btnLog').onclick = function() { location.href = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=id_token+token&response_mode=fragment&client_id=7822587c-fed4-4dd3-8e68-165334eb7c92&redirect_uri=https%3A%2F%2Ftestsite.com%2Ftest.html&scope=openid%20profile%20https%3A%2F%2Fgraph.microsoft.com%2Fmail.read&nonce=a1b2c3d4e5f'; }; . . . }()); </script> </body> </html>
補足 : 上述の通り token は https によりネットワーク上で暗号化されますが、token が URI のフラグメントとして含まれるため、悪意あるプログラム (例えば Plug-in など) によって簡単に呼び出せる点に注意してください。(再呼び出しや、別のトークンを使った「なりすまし」などがやりやすくなります。) こうした繰り返しの攻撃 (replay attacks) を防ぐために、上述の通り nonce を使用します。
この nonce についての詳細は、「JavaScript による Azure AD 連携」を参照してください。
なお、返ってきた Id Token から Basic Profile (または Claim) を取得する方法については、次回 細かく解説しますので、今日はここまで !
次回は、v2.0 endpoint における Token の検証 (Validation, Verification) について解説します。
Categories: Uncategorized