JavaScript による Azure AD 連携 (OAuth Implicit Grant)


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

開発者にとっての Microsoft Azure Active Directory

こんにちは。

クライアント リソースが充実してきた昨今、Web アプリケーションの世界では、以前のようなサーバー サイドの処理 (負荷) の集中を避け、JavaScript を主体にフロント側のリソースを活用して処理する SPA (Single Page Application) が使用される機会が増えてきました。(こうした開発を支える さまざまなライブラリーも提供されています。)

こうしたスタイルのアプリケーションでは、「Native Application (Mobile App) で Azure Active Directory に Login するプログラミング (Authentication)」で紹介したフローは使用できません。
例えば、JavaScript では、自分自身がロードされたサイトのドメインに対する Request (処理要求) しかできないため、AJAX などを使って Azure AD に POST 要求を出すことは、普通のやり方では不可能です。(JavaScript のこの制限については「JSONP などクロス ドメイン (Cross-Domain) 問題の回避と諸注意」を参照してください。)

今回は、こうした JavaScript のみを使って Azure Active Directory (Azure AD, すなわち Office 365 含む) と連携して OAuth のフローを処理する Implicit Grant と呼ばれるフローを紹介します。(数か月前に使えるようになりました。)

 

準備

まず、「Native Application (Mobile App) で Azure Active Directory に Login するプログラミング (Authentication)」で紹介した手順で、Azure Portal を使って、サービス側とクライアント側のアプリケーションを Azure AD に登録します。クライアント側からサービス側の呼び出しができるように、権限 (Permission) の設定もおこなってください。(設定後、クライアント側の client id をコピーしておいてください。以降で使用します。)
なお、クライアント側の登録の際には、[Native] を選択してください。(下図)

今回、サービス側を https://test-sv.azurewebsites.net、クライアント側 (今回作成する JavaScript のアプリケーション側) を https://test-cl.azurewebsites.net と仮定します。

Azure AD では、セキュリティ上の理由から、ここで解説する Implicit Grant Flow は既定で Disable (オフ) になっています。そこで、Manifest を直接編集して、この設定を Enable (オン) に切り替えます。

登録した Azure AD 上のアプリケーションの構成 (Configure) 画面で、[Manifest] ボタンを選択してマニフェスト エディターを表示します。

メモ帳などでダウンロードしたマニフェストを開き、下記の oauth2AllowImplicitFlow を true に変更します。

{
  "allowActAsForAllClients": null,
  "appId": "2ecc2d71-8fcc-4007-b18e-165413028cae",
  "appMetadata": {
    "version": 0,
    "data": []
  },
  "appRoles": [],
  "availableToOtherTenants": true,
  "displayName": "https://test-cl.azurewebsites.net",
  "errorUrl": null,
  "groupMembershipClaims": null,
  "homepage": null,
  "identifierUris": [],
  "keyCredentials": [],
  "knownClientApplications": [],
  "logoutUrl": null,
  "oauth2AllowImplicitFlow": true,
  "oauth2AllowUrlPathMatching": false,
  "oauth2Permissions": [],
  "oauth2RequirePostResponse": false,
  . . .

 

HTTP フローとプログラミング - 基本

Azure AD を使った Implicit Grant Flow は以下の通り処理します。

まず、以下の URL に Redirect します。
各 query string は、下記の内容を URL エンコードした文字列です。

  • client_id には、上記の事前準備で取得したクライアント アプリケーションの client id を設定します
  • redirect_uri には、上記で Azure AD 登録時に設定した Redirect Uri を設定します (ログイン後、このページに戻ってきます)
  • resource には、アクセス先のサーバーの ID (今回の場合、https://test-sv.azurewebsites.net) を指定します
GET https://login.microsoftonline.com/common/oauth2/authorize?response_type=token&client_id=2ecc2d71-8fcc-4007-b18e-165413028cae&resource=https%3A%2F%2Ftest-sv.azurewebsites.net&redirect_uri=https%3A%2F%2Ftest-cl.azurewebsites.net%2Ftest.html

上記にアクセスすると、下図のような Azure AD のログイン画面が表示されます。

ユーザーがログイン画面にユーザー ID とパスワードを入力してログインすると、ブラウザーは上記で指定した redirect_uri (今回の場合、https://test-cl.azurewebsites.net/test.html) に Redirect し、URI フラグメント (ハッシュ) に access_token を設定して戻ってきます。下記のような URI です。

access token は URI のフラグメント (ハッシュ, #) として含まれるので、ブラウザーがこの URI を処理する際、access token が無意味にネットワーク上に流れることはなく、アクセス URI としてサーバー上のログなどの形で残ることもありません。(この access token は、Response の Location ヘッダーとして Server 側から送られて、クライアントのみで処理されます。)

https://test-cl.azurewebsites.net/test.html#access_token=eyJ0eXAiOi...&token_type=Bearer&expires_in=3599&session_state=eb83c98a-9831-4b18-b1a1-bdf483104d66

補足 : 他のサービス (Web API) の呼び出しではなく、単に SignIn や、自身のバックエンド サービスを呼び出す際は、access token ではなく id token (アクセス先の scope などを含まない情報) を要求します。(通常は、この id token を使用する場合が多いでしょう。)
id token も「Azure AD : Service 開発 (access token の validation check)」で解説した方法と同じ手順で妥当性 (中身の正しさ) を確認できます。

https://login.microsoftonline.com/common/oauth2/authorize?response_type=id_token&client_id=2ecc2d71-8fcc-4007-b18e-165413028cae&redirect_uri=https%3A%2F%2Ftest-cl.azurewebsites.net%2Ftest.html&nonce=a1b2c3d4e5f

あとは、この access token の値を Authorization ヘッダーに設定して、サービス側を呼び出します。(サービス側は「Azure AD : Service 開発 (access token の validation check)」で解説した方法で実装します。サービスが Exchange Online、SharePoint Online などの既存のサービスの場合には、もちろん、実装する必要はありません。)
なお、サービス側は、フロントの JavaScript から呼び出すため、「JSONP などクロス ドメイン (Cross-Domain) 問題の回避と諸注意」で紹介したように、XDM (Cross Document Messaging) や CORS (Cross-Origin Resource Sharing) などを使用できるように構成し、Cross Domain の呼び出しができるようにすると良いでしょう。

GET https://test-sv.azurewebsites.net/testapi
Authorization: Bearer eyJ0eXAiOiJK...

なお、ページ遷移して戻ってきた後も、継続して何かの状態を維持しておきたい場合には (例えば、ユーザーが何か選択している状態を維持するなど)、下記の通り state を設定して Redirect します。こうすると、同じ state が付与されて戻ってくるので、アプリケーション側ではこの state を見て状態を復元できます。

GET https://login.microsoftonline.com/common/oauth2/authorize?response_type=token&client_id={client id}&resource={resource id}&redirect_uri={redirect url}&state={some string to keep state}

例えば、下記は、ログインをおこなって access token を取得する簡単な JavaScript のサンプル コードです。

<!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>
  <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/authorize?response_type=token&client_id=2ecc2d71-8fcc-4007-b18e-165413028cae&resource=https%3A%2F%2Ftest-sv.azurewebsites.net&redirect_uri=https%3A%2F%2Ftest-cl.azurewebsites.net%2Ftest.html';
      }
    }());
    
  </script>
</body>
</html>

 

HTTP フローとプログラミング - 応用

上記はページの Redirect をおこなっていますが、Login 画面を Popup させて access token を取得し、その結果を元の Page (window) に返しても構いません。
この方法だと、使用している Page は遷移せずに token を取得できます。(画面の状態などが維持できます。)

以下は、Popup でログインをおこない、元の画面に access token を返す簡単なサンプルです。

main.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>
  <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://test-cl.azurewebsites.net/main.html');
            window.close();
          }
        }, this);
      } else {
        location.href = 'https://login.microsoftonline.com/common/oauth2/authorize?response_type=token&client_id=2ecc2d71-8fcc-4007-b18e-165413028cae&resource=https%3A%2F%2Ftest-sv.azurewebsites.net&redirect_uri=https%3A%2F%2Ftest-cl.azurewebsites.net%2Fpopup.html';
      }
    }());
  </script>
</body>
</html>

Native Application (Mobile App) で Azure Active Directory に Login するプログラミング (Authentication)」で解説したように、access token には期限があります。
期限切れになった場合には、Implicit Grant Flow の場合、hidden の iframe を使って、再度、同じ処理を呼び出します。(この際、下記のサンプルのように、prompt=none を付与して呼び出します。)

下記では、iframe により access token が URI のフラグメントに付与されて同じページ  (下記の redirect_uri として設定されている https://test-cl.azurewebsites.net/test.html) に戻され、このページ (test.html) 内の sessionStorage.setItem() により access token が Session Storage に保存されます。以降、このドメイン内では sessionStorage.getItem() 関数で access token を取り出せます。
もし、redirect_uri として別ドメインのページに戻す必要がある場合は、window.parent.postMessage でこのページに access token を渡すと良いでしょう。(別ドメインのページとの情報交換については、以前記載した「JSONP などクロス ドメイン (Cross-Domain) 問題の回避と諸注意」を参照してください。)

こうすることで、ログイン画面は表示されずに (既にログイン済で Cookie がおぼえているため) access token だけを取り直すことができます。

補足 : 例えば、信頼済サイトで、別のゾーン (インターネット サイト) のページに iframe を使用して遷移するとエラーになります。ブラウザーのこれらの設定に注意してください。

<!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>
  <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/authorize?response_type=token&client_id=2ecc2d71-8fcc-4007-b18e-165413028cae&resource=https%3A%2F%2Ftest-sv.azurewebsites.net&redirect_uri=https%3A%2F%2Ftest-cl.azurewebsites.net%2Ftest.html';
      }

      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/authorize?response_type=token&client_id=2ecc2d71-8fcc-4007-b18e-165413028cae&login_hint=demouser01%40aadsample01.onmicrosoft.com&resource=https%3A%2F%2Ftest-sv.azurewebsites.net&redirect_uri=https%3A%2F%2Ftest-cl.azurewebsites.net%2Ftest.html&prompt=none';
      }
    }());
    
  </script>
</body>
</html>

補足 : 上記コードで login_hint は不要 (optional) ですが、利用環境によって User Account が識別できないケースもあるので、なるべく入れてください。なお、今回、固定の値 (demouser01@aadsample01.onmicrosoft.com) を設定していますが、この値は Token の Claim などから取得可能です。(Token の Claim の取得方法は「Azure AD を使った Service (API) 開発 (access token の verify)」を参照してください。)

 

ADAL.js (ライブラリーの使用)

上記の処理を、以下の JavaScript ライブラリーを使うことで、もっと簡易に実装できます。(後者は AngularJS 用です。)

https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.min.js
(https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.js)

https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal-angular.min.js
(https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal-angular.js)

例えば、下記は、adal.js を使って id token, access token を順番に取得する簡単なサンプルです。(access token を取得する前に、必ず id token を取得してください。)
内部では、上述した hidden iframe の処理などいろいろと複雑なことをしていますが、こうした内部動作を気にせずに直観的に使用できます。

<!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>
  <button id="btnLog">1. Login !</button>
  <button id="btnIdt">2. Get Id Token !</button>
  <button id="btnTkn">3. Get Access Token !</button>
  <button id="btnClr">4. Clear</button>
  <div id="txtMsg"></div>
  
  <script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.min.js"></script>
  <script>
    (function () {
      var config = {
        tenant: 'aadsample01.onmicrosoft.com',
        clientId: '2ecc2d71-8fcc-4007-b18e-165413028cae',
        endpoints: {'https://test-sv.azurewebsites.net' : 'https://test-sv.azurewebsites.net'},
        cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
      };
      var authContext = new AuthenticationContext(config);
      authContext.handleWindowCallback();

      document.getElementById('btnLog').onclick = function() {
        authContext.login();
      }

      document.getElementById('btnIdt').onclick = function() {
        authContext.acquireToken(authContext.config.clientId, function (error, token) {
          if (error) {
            document.getElementById('txtMsg').innerHTML = 'No token: ' + error;
          } else {
            document.getElementById('txtMsg').innerHTML = 'Id Token: ' + token;
            authContext.getCachedUser();
          }
        });
      }
 
      document.getElementById('btnTkn').onclick = function() {
        authContext.acquireToken('https://test-sv.azurewebsites.net', function (error, token) {
          if (error) {
            document.getElementById('txtMsg').innerHTML = 'No token: ' + error;
          } else {
            document.getElementById('txtMsg').innerHTML = 'Access Token: ' + token;
          }
        });        
      }
 
      document.getElementById('btnClr').onclick = function() {
        localStorage.clear();
        alert('Cleared cache !');
      }      
    }());

  </script>
</body>
</html>

[Login !] ボタンを押すと、ブラウザー全体が Redirect されて Azure AD のログイン画面が表示されます。

ユーザー ID / パスワードを入力すると SignIn がおこなわれ、順番に id token, access token が取得できます。(access token を使って、https://test-sv.azurewebsites.net のサービスにアクセスできます。)

ブラウザを一度閉じて、再度、画面を表示してみてください。今度は ログインをおこなわなくても、Id Token が取得できているのがわかります。
これは、取得した Token の情報が localStorage に保存され、再利用されているためです。

 

参考 : Replay Attacks と nonce

上述の通り token は SSL によりネットワーク上で暗号化されますが、token が URI のフラグメントとして含まれるため、悪意あるプログラム (例えば Plug-in など) によって簡単に呼び出せる点に注意してください。再呼び出しや、別のトークンを使った「なりすまし」などがやりやすくなります。

こうした繰り返しの攻撃 (replay attacks) を防ぐために、nonce を使います。
nonce は id_token を要求する際に付与する任意の文字列ですが、特徴として、返された token (id token) の中に含まれて送られてきます。このため、仮に nonce の内容が外部のプログラムで変更された場合でも、デジタル署名 (signature) が無効になり、渡された id token 自体が無効であると判断されます。(「Azure AD : Service 開発 (access token の validation check)」を参照してください。)

つまり、クライアント (JavaScript) では、まず最初のログインで nonce を渡して id_token を要求 (Request) し、Response の token に含まれる nonce が、送信した nonce と同じ値であるか確認をおこなって、もし同じなら、後始末をおこなって、2 回以上 同じ呼び出しが成功しないように実装すると良いでしょう。(こうして、上記の Replay Attack を防止できます。)

<!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>
  <button id="btnLog">Login !</button>
  <div id="txtMsg"></div>
  <script>    
    (function () {
      var nonce = null;
      
      if(location.hash) {
        var hasharr = location.hash.substr(1).split("&");
        hasharr.forEach(function(hashelem) {
          var elemarr = hashelem.split('=');
          if(elemarr[0] == 'id_token') {
            var idtoken = elemarr[1];
            var idtarr = idtoken.split('.');
            var encbody = idtarr[1];
            var body = base64_url_decode(encbody);
            if(body.nonce == nonce) {
              alert('Good !');
              nonce = null;  // initialize nonce !
            } else {
              alert('Bad !');              
            }
          }
        }, this);
      }
      
      function base64_url_decode(arg) {
        var res = arg;
        res = res.replace(/-/g, '+').replace(/_/g, '/');
        switch(res.length % 4) {
          case 2:
            res += '==';
          case 3:       
            res += '=';
          default:
            break;
        }
        // base64 decode
        return decodeURIComponent(escape(window.atob(res)));
      }
      
      function createRandomString() {
        . . .  (skip code)

        return result;
      }
      
      document.getElementById('btnLog').onclick = function() {
        nonce = createRandomString();
        location.href = 'https://login.microsoftonline.com/common/oauth2/authorize?response_type=id_token&client_id=2ecc2d71-8fcc-4007-b18e-165413028cae&resource=https%3A%2F%2Ftest-sv.azurewebsites.net&redirect_uri=https%3A%2F%2Ftest-cl.azurewebsites.net%2Ftest.html&nonce=' + nonce;
      }
    }());
  </script>
</body>
</html>

上記の id token の 2 番目の要素 (上記の body) には、下記のような内容が入っています。

{
  "aud": "2ecc2d71-8fcc-4007-b18e-165413028cae",
  "iss": "https://sts.windows.net/4f0e4227-0a28-4d2c-a8fd-8a74614d3f61/",
  "iat": 1425576146,
  "nbf": 1425576146,
  "exp": 1425580046,
  "ver": "1.0",
  "tid": "4f0e4227-0a28-4d2c-a8fd-8a74614d3f61",
  "amr": [
    "pwd"
  ],
  "oid": "5d08503d-506c-4412-864c-290c2c8cfaad",
  "upn": "demouser01@aadsample01.onmicrosoft.com",
  "sub": "tbx_4bwBU5uc7-ITKZqbZ9MG5vbDAaRRNZwEDbTpHYo",
  "given_name": "Tsuyoshi",
  "family_name": "Matsuzaki",
  "name": "Tsuyoshi Matsuzaki",
  "unique_name": "demouser01@aadsample01.onmicrosoft.com",
  "nonce": "c0fc21ad7199",
  "pwd_exp": "7730168",
  "pwd_url": "https://portal.microsoftonline.com/ChangePassword.aspx"
}

現状、この nonce は access token の要求で使用できないようなので、access token を使う際は、こうした攻撃に注意して実装 (プログラミング) してください。(あらかじめ取得した id token のユーザーとの同一性を確認するなど、ケアしてください。)

 

※ 変更履歴 :

2015/03/19 エンドポイントを https://login.windows.net から https://login.microsoftonline.com (新エンドポイント) に変更

2017/05/26  画面を新ポータル (https://portal.azure.com) に変更

Comments (2)

  1. SOhtsu says:

    現在、AAM(Azure Api Management)による弊社のAPIの有償公開について、以下のように大変苦労しており、断念を考えているところでした。

    https://social.msdn.microsoft.com/Forums/azure/en-US/89664714-2807-4f7a-8064-22925648c133/how-can-developers-get-access-token-by-javascript-in-aad-and-apim?forum=azureapimgmt

    https://azure.microsoft.com/en-us/documentation/articles/api-management-howto-oauth2/

    松崎さんのBLOGのおかげで、AADにおけるOAuth2.0を利用した認証の全体像が理解できたように感じます。特に、JWTにおけるaud(Audience)がAADにおける[resource]に該当するという情報は、大変価値があります。Microsoft社の方は、Defaultとして考える傾向があると思いますが、外部では、[resource]とは何だろうから始まります。問い合わせや自己開発などにより、解決する場合もありますが、上記のように情報不足のため最終的に断念してしまう場合も多いと思います。

    今後もこのような技術情報をぜひ発信していってください

    1. ありがとうございます。今後も、地道に お役に立てる情報発信に努めますので、よろしくお願いします。

Skip to main content