DocumentDBの(マスター)キーとauthorizationトークンについて

最近 DocumentDB について調べたり、使ったりしております。
DocumentDB は (たしか)2015年にGAしたと思うのですが、GAから日が経っているのに、想像したよりもネット上のブログなどでの資料が少ない気がしています(特に日本語)。
一方 本家のドキュメントは日本語版も含めかなりしっかりしている印象です。

まあ、大抵のエンプラ系システムであれば、NoSQLなんて使わず従来通りのSQL Server(もしくはSQL Database)でOKですからね。
普通の開発現場では、ちょー早くて、無制限にスケール可能で、でもデータ構造設計やパーティション分割やトランザクションや検索にコツが必要な DocumentDB(NoSQL) を欲するケースは少ないのでしょう・・・

でも、私個人的には、DocumentDBを含むクラウドネイティブな技術が好きなんですよね^^

と長くなりましたが本題へ・・・

(マスター)キーとauthorizationトークンの関係

このお話、DocumentDBをREST APIから使用している(使用したことがある)方は分かっていると思いますが、ライブラリ経由でしか使ったことがない方の場合は認識できていないかもしれない、という内容のお話です。
だからDocumentDBを利用した開発においては必須というわけではありません。

DocumentDBを利用する場合、以下の2パターンの方法があります。

  • REST APIを直に呼び出す
  • 各言語用ライブラリを使用する

大抵、後者の各言語用ライブラリを使う事と思います。
Visual Studio & .NET だったらNugetで「Microsoft.Azure.DocumentDB(.NET CoreならMicrosoft.Azure.DocumentDB.Core)」を追加するだけですね。
ライブラリはバックエンドでREST APIを呼び出してくれます。接続プロトコル(ConnectionPolicy.ConnectionProtocol)を HTTPS にしていれば、Fiddlerなんかでその通信内容を確認することができます。

で、DocumentDBに接続する場合には、DocumentClientクラスを以下のようにインスタンス化します。

var client = new DocumentClient(uri, authKey);

認証・認可のために第2引数に認証キー(authKey)を渡します(第1引数uriは、利用するDocumentDBアカウントのURIです)。
インスタンス化した DocumentClient に対して、CreateDocumentQuery()だったりCreateStoredProcedureQuery()だったりのメソッド呼び出しを行うことで対象のDocumentDBに対する操作を行うことができます。
「認証キー(authKey)」というのは、Azureポータルで確認可能なプライマリキー(セカンダリキー)です。
以下の画面キャプチャ。

f:id:daigo-knowlbo:20170504125630p:plain

ではDocumentClientがDocumentDBに対して操作を行う際にはどのような通信を行っているのか確認してみます。
まず、以下のようなDocumentDBの操作を行うクラスを用意します(DocumentManagerクラス)。

public class DocumentManager {
  private const string EndpointUrl = "https://rddocdb.documents.azure.com:443/";

  private const string PrimaryKey = "[(マスター)プライマリキー]";
  
  private const string DatabaseID = "ExampleDB1";
  private const string CollectionID = "ExampleCollection1";
  
  private DocumentClient client;
  
  public async Task<bool> Init() {
    // DocumentClientオブジェクト 作成
    this.client = new DocumentClient(new Uri(DocumentManager.EndpointUrl), DocumentManager.PrimaryKey,
      new ConnectionPolicy
      {
        ConnectionMode = ConnectionMode.Gateway,
        ConnectionProtocol = Protocol.Https
      });
    
    // ExampleDB1 作成
    var db = await this.client.CreateDatabaseIfNotExistsAsync(
      new Database { Id = DocumentManager.DatabaseID });
    
    // ExampleCollection1 作成
    var collection = await this.client.CreateDocumentCollectionIfNotExistsAsync(
      UriFactory.CreateDatabaseUri(DocumentManager.DatabaseID),
      collection);
    
    return true;
  }
  
  public async Task<bool> ReadDocumentCollection() {
    // コレクション情報を読み込む
    var collection = await this.client.ReadDocumentCollectionAsync(
      UriFactory.CreateCollectionUri(DocumentManager.DatabaseID, DocumentManager.CollectionID));

    return true;
  }
}

Init()メソッドは、以下の処理を行います。

  • DocumentClientオブジェクトを作成しprivateメンバーに保持
  • ExampleDB1データベースを作成
  • ExampleCollection1コレクションを作成

ReadDocumentCollection()メソッドは、Init()で作成されたExampleCollection1コレクション情報を読み取ります。

そして、以下のようにdocumentManagerを呼び出しましょう。

var documentManager = new DocumentManager();
var initResult = await documentManager.Init();
var readResult = await documentManager.ReadDocumentCollection();

最後の ReadDocumentCollection() 呼び出しに注目します。
Fiddlerで抜き取ったHTTPS通信は以下の通りです。

f:id:daigo-knowlbo:20170504133809p:plain

「/dbs/ExampleDB1/colls/ExampleCollection1」にGETリクエストを行っています。
つまり↓↓↓のREST APIが呼び出されています。
Get a Collection | Microsoft Docs
HTTP GETのリクエストヘッダで注目すべきは authorization です。
DocumentClientオブジェクト作成時に渡した認証キー(プライマリキー)を元にして authorization 値が生成されています。
厳密には認証キー(プライマリキー)を含む以下の要素から生成されています。

  • HTTP Verb(GETとかPOSTとか)
  • リソースタイプ(CollectionとかDocumentとかのアクセスの対象リソースタイプ)
  • リソースリンク(こんなの・・/dbs/ExampleDB1/colls/ExampleCollection1)
  • 日時(トークン生成日)
  • キー
  • キーのタイプ(masterとかresource)
  • トークンバージョン

日時は注意が必要で、トークン生成時に指定した日時と HTTPヘッダ x-ms-date 値がマッチしている必要があります。

上記からauthorizationトークンを生成するロジックは Access Control on Azure Cosmos DB Resources | Microsoft Docs に書かれておりまして、コードの引用が以下になります。

// authorizationトークンを作成
static string GenerateAuthToken(
  string verb, 
  string resourceType, 
  string resourceLink, 
  string date, 
  string key, 
  string keyType, 
  string tokenVersion)
{
  var hmacSha256 = new System.Security.Cryptography.HMACSHA256 { Key = Convert.FromBase64String(key) };

  verb = verb ?? "";
  resourceType = resourceType ?? "";
  resourceLink = resourceLink ?? "";

  string payLoad = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}\n{1}\n{2}\n{3}\n{4}\n",
      verb.ToLowerInvariant(),
      resourceType.ToLowerInvariant(),
      resourceLink,
      date.ToLowerInvariant(),
      ""
  );

  byte[] hashPayLoad = hmacSha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payLoad));
  string signature = Convert.ToBase64String(hashPayLoad);

  return System.Web.HttpUtility.UrlEncode(String.Format(System.Globalization.CultureInfo.InvariantCulture, "type={0}&ver={1}&sig={2}",
    keyType,
    tokenVersion,
    signature));
}

以下のようなコードで認証トークンを生成することができます。

var authorizationToken = 
  GenerateAuthToken(
    "get", 
    "colls", 
    "dbs/ExampleDB1/colls/ExampleCollection1", 
    "Tue, 02 May 2017 10:20:01 GMT", 
    "[プライマリキー]", 
    "master", 
    "1.0");

認証トークン生成して使ってみる

では認証トークンを生成して実際にREST呼び出ししてみましょう。
Fiddlerを使ってHTTPリクエストを投げたいと思います。

1.正常なケース

以下のリクエストを行います。
トークン生成に使用した 日時 と x-ms-date は同一でないといけない点には注意。
f:id:daigo-knowlbo:20170504141751p:plain

いい感じにレスポンスが返ってきました。
f:id:daigo-knowlbo:20170504141832p:plain

2.不正なauthorizationトークンを利用したケース

リクエスト内容とマッチしないauthorizationトークンを使ってリクエストを行います。
f:id:daigo-knowlbo:20170504141857p:plain

401 Unauthorized が返ってきます。
f:id:daigo-knowlbo:20170504141923p:plain

3.期限切れのauthorizationトークンを利用したケース

authorizationトークンは有効期限があります。
次は、有効期限切れになったauthorizationトークンでリクエストを行ってみます。
f:id:daigo-knowlbo:20170504141947p:plain

403 Forbidden が返ってきます。 f:id:daigo-knowlbo:20170504142012p:plain

bodyを見ると親切に以下のようなメッセージが書かれています。

{
  "code":"Forbidden",
  "message":"The authorization token is not valid at the current time. 
             Please create another token and retry 
             (token start time: Thu, 04 May 2017 00:57:01 GMT, 
              token expiry time: Thu, 04 May 2017 01:12:01 GMT, 
              current server time: Thu, 04 May 2017 05:04:48 GMT).\r\n
              ActivityId: f7c026e7-ddea-4c89-9331-4c9b24d50d9e"
}

つまり・・・↓↓↓と言っていますね。DocumentDB、超親切!!

そのトークンは現時刻では無効だよ。
別のトークン作ってリトライしてね。
そのトークンの開始時刻は「Thu, 04 May 2017 00:57:01 GMT」。無効になるのは「Thu, 04 May 2017 01:12:01 GMT」、今のサーバー時刻は「Thu, 04 May 2017 05:04:48 GMT」。

うん、つまりauthorizationトークンの有効期限は15分ってことですね。

まとめ

内容的に まとめ ってこともないのですが、いつも思うことはライブラリにラップされた内容だけでなくその裏側の本当の姿まで理解しておく事は重要だと思います。
そうすると、何か問題が起きた時に色々解決の糸口への道のりが短くなるのだと思っています。