Azure AD を使った API 開発 (access token の verify)


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

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

こんにちは。

Azure AD を使った API 連携の Client 開発 (OAuth 2.0 紹介) 」では、Azure AD で保護された Service (REST などの Web API) を呼び出す Client Application の構築方法を紹介しました。今回は、逆に、この Service (呼ばれる側) の構築方法を紹介します。(つまり、Office 365 API と同様のサービスを構築する方法を紹介します。)

Azure AD を使った API 連携の Client 開発 (OAuth 2.0 紹介) 」で紹介したように、Service 側では、Request と一緒に受け取った access token の検証 (validation) をおこない、問題なければ Response (API の実行結果) を返します。

GET http://contoso.com/yourapi

Authorization: Bearer eyJ0eXAiOiJKV1...

この投稿では、この検証の考え方とプログラミング方法を紹介します。

まずは、PHP によるプログラミングを例に紹介しますが、考え方を理解すれば、他のさまざまな言語から利用可能です。(.NET のプログラミングについては最後に言及します。)

 

渡された Access Token の中身は ? 検証方法は ?

かなり以前に「OAuth 2 Token の Decode」でも紹介しましたが、渡される access token がどのような中身になっているか理解することで、検証が可能です。
以降では、この中身を解説しながら、どのように解析するか紹介します。

補足 : OAuth の仕様では、ここで紹介する token のフォーマットは id token に関するもので (RFC 7519 参照)、access token のフォーマットについては自由です。(実際、Microsoft Account、Google Account など ほとんどの Provider で独自の ticket が使われています。)
Azure AD では独自に登録された custom api でも verify できるよう、このあと紹介するように id token と同じフォーマットの access token が使用されています。

Step 1)  Azure AD の access token の文字列は、. (dot) 区切りの 3 つのトークンで構成されています。
このため、まず、このトークンを分解します。

Step 2)  この 3 つトークンには、1 番目に「デジタル署名の header 情報(暗号化方式など)」、2 番目に「Credential の属性情報 (ID、氏名など)」、そして 3 番目に「それらから生成されたデジタル署名そのもの」が入っていて、これらは Base 64 URL エンコードされています。(Base64 エンコードされて、かつ、「+」、「/」、「=」などの URL 文字列の約束語は別の文字に変換されています。Wikipedia「Base64」の「変形版」の RFC 4648 を参照してください。)
このデコードをおこなうことで、1 番目と 2 番目のトークンとして、下記のような JWT (json フォーマット) が取得できます。(このうちの、alg、x5t、aud、nbf、exp, scp については後述します。その他の属性の解説は省略します。)

1st token

{
  "typ":"JWT",
  "alg":"RS256",
  "x5t":"MnC_VZcATfM5pOYiJHMba9goEKY"
}

2nd token

{
  "aud":"https://testcorp.onmicrosoft.com/testapi",
  "iss":"https://sts.windows.net/5597bcef-3944-476b-a4db-9970353bcf60/",
  "iat":1424231894,
  "nbf":1424231894,
  "exp":1424235794,
  "ver":"1.0",
  "tid":"5597bcef-3944-476b-a4db-9970353bcf60",
  "amr":["pwd"],
  "oid":"72b9ba89-c7fc-4d77-baed-58923247c483",
  "upn":"demouser01@testcorp.onmicrosoft.com",
  "sub":"XwgqNW4yjZcXj03Rx8EcZrPEedMxdY5428W1HqWGPPo",
  "given_name":"Taro",
  "family_name":"Demo",
  "name":"Demo Taro",
  "unique_name":"demouser01@testcorp.onmicrosoft.com",
  "appid":"e29d918e-4da6-4d42-aeb2-d949b73be432",
  "appidacr":"1",
  "scp":"user_impersonation",
  "acr":"1"
}

Step 3)  上記の情報をもとに、このあと述べる検証 (validation check) をおこないます。
まず最初の検証として、現在時刻が、上記の nbf (Not Before = Start Time) と exp (Expiration Time) の間かどうか確認します。(この期間に含まれない場合、この access token は期限切れであることを意味します。)

Step 4)  さらに、上記の aud (Audience) が、このサービス (呼ばれるサービス) と同一か確認します。この同一性を見ることで、access token が、このサービスのために発行されたものかどうか確認できます。(「Azure AD を使った API (Service) 連携の Client 開発 (OAuth 2.0 紹介) 」で解説した「resource」に該当します。)

Step 5)  さいごに、下記 URL から証明書 (key) の情報 (下記の json) を取得して、添付されたデジタル署名 (上記の 3 番目の token) が正しいか確認します。(この確認が最も重要です。)
これにより、渡された access token が不正に作成されたもの (あるいは、改竄されたもの) でないことを保証します。

https://login.microsoftonline.com/common/discovery/keys

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "kriMPdmBvx68skT8-mPAB3BseeA",
      "x5t": "kriMPdmBvx68skT8-mPAB3BseeA",
      "n": "kSCWg6q9iYxvJE2NIhSyOi ...",
      "e": "AQAB",
      "x5c": [
        "MIIDPjCCAiqgAwIBAgIQsR ..."
      ]
    },
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "MnC_VZcATfM5pOYiJHMba9goEKY",
      "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY",
      "n": "vIqz+4+ER/vNWLON9yv8hI ...",
      "e": "AQAB",
      "x5c": [
        "MIIC4jCCAcqgAwIBAgIQQN ..."
      ]
    }
    . . .

  ]
}
  • 取得した key 一覧 (上記の json 参照) のうち、access token の「デジタル署名の header 情報」 (上記の 1 番目のトークン) にある x5t (X.509 Certificate SHA-1 Thumbprint) の値と、x5t が一致する key (どれか 1 つ) が、今回使用する key です。
    上記の key の一覧から、この key を取得します。
  • 取得した key の x5c (X.509 Certificate Chain) を取り出して、この中に含まれる public key を取得します。(-----BEGIN PUBLIC KEY----- で始まり、-----END PUBLIC KEY----- で終わる文字列全体です。)
  • デジタル署名 (上記の 3 番目の token) が 1 番目のトークンと 2 番目のトークン (これを仮に「平文データ」と呼びます) から導出されることを述べましたが、上記で取得した public key を使って、この input となるデータ (つまり、上記の 1 番目の token と 2 番目の token から成る文字列) から正しく生成されたデジタル署名 (上記の 3 番目の token) であるか否かを確認できます。(この処理は、RSA を処理できる何某かのライブラリーを使用すると良いでしょう。一般的な仕様です。)

補足 : 検証では、scp (Scope) を使用した Permission の確認や、Role の確認もおこなってください。(ここでは、省略しています。)
詳細は「Azure Active Directory の Common Consent Framework (Service 側)」、「Azure AD : Application Role の使用」、「Azure AD : Backend Server-Side アプリの開発 (Deamon, Service など)」を参照してください。

public key を使って、このデジタル署名の正しさはチェックできても、private key を持っていなければデジタル署名そのものの作成はできない、もしくは、作成に天文学的時間を要します。(非可逆なアルゴリズムが使われています。これらの key は、非対称キー (Asymmetry key) と言われます。) つまり、private key を持っていない限り、このデジタル署名を作成することは事実上不可能であり、この private key は Azure AD のサーバーのみが持っています。
いま、例えば、悪意あるプログラムが、別のユーザーになりすますために、2 番目の token を書き換えたと仮定しましょう。この場合、上述の通り、デジタル署名は、token の 2 つの要素 (header 情報と属性情報) から成るバイト列を元に生成されているため、このデジタル署名も変更する必要がありますが、private key のない周辺のプログラムでは、この署名の作成は不可能です。(バイト列から生成されるため、改行コードや空白文字などを入れた場合も署名は無効になります。)

このようにして、token の改竄から Application を守ることができます。

補足 : この IETF の OAuth JWT の仕様詳細については、下記を参照してください。
http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html

一方、Access Token そのものが盗まれた場合には、一定時間 (Azure AD の場合、最長で約 1 時間)、不正にアクセスされることになるので注意してください。(このため、ほとんどの場合、https と組み合わせて使用することになるでしょう。)
なお、Refresh Token は、上記の Access Token と異なり、それ自身に意味を持っていません (上述のような情報は含まれていません)。

 

プログラミング例 (PHP)

下記に、上記の手続きを PHP でプログラミングした場合のサンプルを掲載します。手続きが分かれば、他の言語でも同様のライブラリーや関数 (今回の場合、RSA に対応したライブラリーが必要になります) を使ってプログラミングできます。

なお、上記の解説のどれに対応するかわかるようコメントに番号を付与しておきます。(上記の解説番号に対応しています。)

<?php
// return 1, if token is valid
// return 0, if token is invalid
function validate_token($access_token) {
  $res = 0;

  // 1 create array from token separated by dot (.)
  $token_arr = explode('.', $access_token);
  $header_enc = $token_arr[0];
  $attr_enc = $token_arr[1];
  $sig_enc = $token_arr[2];

  // 2 base 64 url decoding
  $header = json_decode(base64_url_decode($header_enc), TRUE);
  $attr = json_decode(base64_url_decode($attr_enc), TRUE);
  $sig = base64_url_decode($sig_enc);

  // 3 period check
  $dtnow = time();
  if($dtnow <= $attr['nbf'] or $dtnow >= $attr['exp'])
    return $res;

  // 4 audience check
  if (strcmp($attr['aud'], 'https://testcorp.onmicrosoft.com/testapi') !== 0)
    return $res;

  //
  // 5 check signature
  //

  // 5-a get key list
  $keylist = file_get_contents('https://login.microsoftonline.com/common/discovery/keys');
  $keylist_arr = json_decode($keylist, TRUE);
  foreach($keylist_arr['keys'] as $key => $value) {
    // 5-b select one key
    if($value['x5t'] == $header['x5t']) {
      // 5-c get public key from key info
      $cert_txt = '-----BEGIN CERTIFICATE-----' . "\n" . chunk_split($value['x5c'][0], 64) . '-----END CERTIFICATE-----';
      $cert_obj = openssl_x509_read($cert_txt);
      $pkey_obj = openssl_pkey_get_public($cert_obj);
      $pkey_arr = openssl_pkey_get_details($pkey_obj);
      $pkey_txt = $pkey_arr['key'];
      // 5-d validate signature
      $res = openssl_verify($header_enc . '.' . $attr_enc, $sig, $pkey_txt, OPENSSL_ALGO_SHA256);
    }
  }

  // others : permission check, etc
  // (This time, skip this code ...)

  return $res;
}

// Helper functions
function base64_url_decode($arg) {
  $res = $arg;
  $res = str_replace('-', '+', $res);
  $res = str_replace('_', '/', $res);
  switch (strlen($res) % 4) {
    case 0:
      break;
    case 2:
      $res .= "==";
      break;
    case 3:
      $res .= "=";
      break;
    default:
      break;
  }
  $res = base64_decode($res);
  return $res;
}
?>

Azure AD を使った API 連携の Client 開発 (OAuth 2.0 紹介) 」で解説したように、実際に呼び出される際には、access token は Authorization ヘッダーとして渡されるため、下記のような処理になるでしょう。(実際に PHP で REST API を構築する際には Laravel 等のフレームワークを使用すると思いますが、今回は Plan な PHP を使っています。Authorization 要求などのフローも省略しました。)

<?php
// get access token from Authorization header
if(!isset($_SERVER['HTTP_AUTHORIZATION'])) {
  http_response_code(401); // Unauthorized
  return; 
}
$matches = array();
preg_match('/((?i)Bearer(?-i)(\s)+)(.*)/', $_SERVER['HTTP_AUTHORIZATION'], $matches);
$access_token = $matches[3];

// check access token
if(validate_token($access_token) == 0) {
  http_response_code(401); // Unauthorized
  return;
}

// output result
header('Content-type: application/json');
echo '{ "greeting" : "hello" }';

function validate_token($access_token) {
  . . .  (same as above)

}

function base64_url_decode($arg) {
  . . .  (same as above)

}
?>

 

ここでは、PHP を使ったプログラミング サンプルですが、Node.js の場合は jwt-simple が使用できます。

 

プログラミング例 (.NET / C#)

ご存じの方も多いかと思いますが、.NET の場合には、実は上記のフローを知らなくても簡単に実装できてしまいます。

例えば、現在正式リリースされている Visual Studio 2013 では、プロジェクト作成時に [Web API] を選択し、[認証の変更] ボタンを押します。

表示されるダイアログ ボックス (下図) で、[組織アカウント] (Organization Account) を選択し、右ペインに、使用している Directory のドメインを入力します。(「組織アカウント」とは「Azure Active Directory または Windows Server Active Directory のアカウント」を意味しています。)
なお、Azure Active Directory へのログインが必要となりますが、使用しているテナントの全体管理者 (Global Administrator) でログインしてください。(Microsoft Account は NG です。)

この設定をおこなってプロジェクトを新規作成するだけで、以下の 2 つの設定がおこなわれます。(残念ながら、あとから Web API を追加した場合は構成してくれないので、あとから追加する場合は手動設定してください。)

  • Azure Active Directory (指定したテナント) へのアプリケーションの登録
  • Access token の Validation check をおこなうためのプログラム コードの記述

試しに、作成されたプロジェクトの App_Start/Startup.Auth.cs を見てみると、以下の通りプログラム コードが設定されているのが確認できます。たったこれだけの記述で、Access token の Validation check をおこない、必要な Principal 情報を設定します。(プログラマーは、Principal を見て処理を記述すれば OK です。)

. . .
public void ConfigureAuth(IAppBuilder app)
{
  app.UseWindowsAzureBearerToken(
    new WindowsAzureJwtBearerAuthenticationOptions
    {
      Audience = ConfigurationManager.AppSettings["ida:Audience"],
      Tenant = ConfigurationManager.AppSettings["ida:Tenant"]
    });
}
. . .

Visual Studio 2013 が使用できない方は、「JSON Web Token Handler For the Microsoft .Net Framework 4.5」を使用して下記の通り Access Token の validation check をおこなうこともできます。(下記の ValidateToken メソッドで検証しています。)

. . .
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.IdentityModel.Metadata;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Security;
using System.Threading;
using System.Threading.Tasks;
. . .

static void Main(string[] args)
{
  const string JwtToken = "eyJ0eXAiOi...";  // Access Token
  const string Audience = "https://testcorp.onmicrosoft.com/testapi";
  const string ScopeClaimType =
    "http://schemas.microsoft.com/identity/claims/scope";
  const string Tenant = "testcorp.onmicrosoft.com";

  string issuer;
  string stsMetadataAddress = string.Format(
    CultureInfo.InvariantCulture,
    "https://login.microsoftonline.com/{0}/federationmetadata/2007-06/federationmetadata.xml",
    Tenant);

  List signingTokens;

  GetTenantInformation(stsMetadataAddress, out issuer, out signingTokens);

  JwtSecurityTokenHandler tokenHandler =
    new JwtSecurityTokenHandler()
  {
    CertificateValidator = X509CertificateValidator.None
  };

  TokenValidationParameters validationParameters =
    new TokenValidationParameters
  {
    AllowedAudience = Audience,
    ValidIssuer = issuer,
    SigningTokens = signingTokens
  };

  //
  // 以下、Validation に失敗すると Exception が発生するので処理 !
  // (今回は省略)
  //

  ClaimsPrincipal claimsPrincipal =
    tokenHandler.ValidateToken(JwtToken, validationParameters);

  Thread.CurrentPrincipal = claimsPrincipal;

  if (ClaimsPrincipal.Current.FindFirst(ScopeClaimType) == null ||
    ClaimsPrincipal.Current.FindFirst(ScopeClaimType).Value != "user_impersonation")
  {
    throw new Exception("Unauthorized");
  }

  Console.WriteLine("Validation succeded !");
  Console.ReadLine();
  return;
}

private static void GetTenantInformation(
  string metadataAddress,
  out string issuer,
  out List signingTokens)
{
  signingTokens = new List();
  MetadataSerializer serializer = new MetadataSerializer()
  {
    // This is for demo. Don't use this in production code.
    CertificateValidationMode = X509CertificateValidationMode.None
  };
  using (XmlReader reader = XmlReader.Create(metadataAddress))
  {
    MetadataBase metadata = serializer.ReadMetadata(reader);
    EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;
    issuer = entityDescriptor.EntityId.Id;
    signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);
  }
}

static List ReadSigningCertsFromMetadata(
  EntityDescriptor entityDescriptor)
{
  List stsSigningTokens = new List();
  SecurityTokenServiceDescriptor stsd =
    entityDescriptor.RoleDescriptors.OfType().First();

  if (stsd != null && stsd.Keys != null)
  {
    IEnumerable x509DataClauses =
      stsd.Keys.Where(
        key => key.KeyInfo != null &&
          (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).
      Select(key => key.KeyInfo.OfType().First());
    stsSigningTokens.AddRange(
      x509DataClauses.Select(
        clause => new X509SecurityToken(
          new X509Certificate2(clause.GetX509RawData()))));
  }
  else
  {
    throw new Exception("There is no RoleDescriptor");
  }
  return stsSigningTokens;
}
. . .

 

※ 変更履歴 :

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

 

Comments (0)

Skip to main content