Azure AD v2.0 endpoint の JavaScript Client 開発 (OAuth Implicit Grant Flow)


※ v2 endpoint に関する内容です。(Azure AD (v1) の場合は、こちら を参照してください。)

Azure AD v2.0 endpoint を使った Azure AD & MSA 対応アプリ開発

こんにちは。

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+tokenscope=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) について解説します。

Comments (0)

Skip to main content