Monday, February 18, 2019

Cosmos DB REST API with POSTMAN



Cosmos DB Security & REST API

       Connecting to Cosmos DB and running queries with REST API can be tricky. In this post, I want to show you how to call Azure Cosmos DB by using its REST API. I am going to use the Cosmos DB Emulator and POSTMAN. Both applications are free.

       To connect to SQL Server, usually you need to have a username and password. If you have the right username and password combination, you should be set to run your queries. You need to do more than that to connect and run queries in CosmosDB. For example, you can call CosmosDB from web browsers and retrieve data without hitting to any web server. To be able to do that, you are going to need much more than username and password.

       Let’s look at the Keys page of Cosmos DB.  There are two types of keys, one set can Read and Write. Other set can only Read.  It is a good practice to have different connection strings in your applications. Read queries should use Read connection string, Write queries should use the Write connection string. This applies to SQL Server too.


        Try to use the Secondary Key as much as you can in your applications. Sometimes basic connection string can give all kind of troubles to me in SQL Server. This setup of CosmosDB can be tricky.  It’s very important for developers to understand how the authentication works. Before I continue, I want to be sure that you understand the following CosmosDB options.

Master Keys
These keys are created during the creation of your Cosmos DB Account. You can click on refresh icon to regenerate them at Cosmos DB Portal. You cannot regenerate master keys in Emulator. These keys provide access to the administrative resources and you should be very careful when you need to store them. Use Read-Only Keys as much as you can.

Resource Tokens
Resource Tokens are responsible for providing access to specific containers, documents, attachments, stored procedures, triggers, and UDFs. Each user must have a resource token. Your application needs to use a resource token to call Cosmos DB.

Users
Users are specific for Cosmos DB databases. You can attach specific permissions or roles to each user.

REST API

        You have many options to access to CosmosDB. Rest API is one of these options and it is the low level access way to Cosmos DB. You can customize all options of CosmosDB by using REST API. To customize the calls, and pass the required authorization information, you need to use http headers. There are many headers you can set depending on the operation you want to run in CosmosDB.  I am going to cover only the required headers here.

      In the following example, I am going to try to create a database in CosmosDB emulator by using the REST API. First let’s look at the required header fields for this request. These requirement applies to all other REST API calls too.

x-ms-version : This is the version of the REST API. You can find the available versions here. Which one to use? Try to use the latest one.

x-ms-date : This is the date of your request. It must be formatted by Coordinated Universal Time. (ex: Mon, 18 Feb 2019 05:00:23 GMT)

x-ms-session-token: This is required if you want to use session consistency. For each of your new write request in Session consistency, CosmosDB assings a new SessionToken to the calls. You need to track the right session token and use it in this header property to keep usng the same session. SDK does this for you in the background, if you want to use the REST API, you need to do this manually. For this example, I am not going to worry about if I have the right session so I will ignore this one for the following example.

Authorization: This one is the most important and tricky one. This needs to get generated for each of your call to Cosmos DB. It must be in the following format

type={typeoftoken}&ver={tokenversion}&sig={hashedsignature}

      You need to generate the hashedsignature by using the HTTP Verb, Resource Type, Resource Link and the Date. New line character (\n) is required between each value.

HTTP Verb : GET, POST, PUT, etc.
Resource Type : Type of CosmosDB object (Databases, Collections, Documents, etc...)
ResourceLink : Which specific object type are your trying to access/modify (a specific db, collection, document, etc..)
Date : Same value of x-ms-date. It must be in Coordinated Universal Time.

sig = Verb.toLowerCase() + "\n" + ResourceType.toLowerCase() + "\n" + ResourceLink + "\n" + Date.toLowerCase() + "\n" + "" + "\n";

     I wasn't kidding when I said it was tricky :) We are not done yet. We still need to generate a hashed token from the signature. Authorization string must be Base64 encoded using MIME RFC2045.To decode the hashed token, we need to use master key using MIME RFC2045 as its Base64 encoded.

     To call Cosmos DB directly from POSTMAN, first we need to figure out what URL we need to use. Since I am using the emulator, I am going to get the first part of the URL from the Emulator. This information will be in QuickStart section. Mine is https://localhost:8081 as you can see in the following screenshot.



     That was the first part of URL, now we need to figure out the rest of the URL. For that, we need to look at the documentation of CosmosDB Rest API. You can find all url locations from this link. Since I am trying to create a database, I am going to use the following path.

     Also, documentation tells us that this must be a POST Http Action. In Postman, I pick the POST and type the URL to the URL section in the following example.


      Next, I want to create an environment in Postman to store some variables. I need to generate a token for CosmosDB and get the current date to fill the header named x-ms-date. I am going to use variables in Environment to store the values. To Create an environment. Click on gears icon and click on Add.





     Now, I want to create the following variables in the new Environment. I named the environment local.

masterKey : This is the key from the Emulator or Azure Cosmos DB Portal. I am using my Cosmos DB Emulator so I really don't care about the master key. Be very careful, If you want to do this in production. You don't want to save your production masterkey to Postman or any unsecured location!

authToken : This will be the token we will create later to pass CosmosDB

utcDate : This will be the required date in special format. We will generate its value later.


      We are ready to add values to headers section. Click on Headers link, and add the following headers. These are the required HTTP headers for all CosmosDB REST API calls.

x-ms-version : 2017-02-22
(This is the latest version. You can find the other versions here.)

x-ms-date : {{utcDate}}
(This is the parameter we just created in POSTMAN Environment. We are going to generate its value in script.)

authorization : {{authToken}}
(This is the other parameter we just created. We are going to generate its value in script.)

Content-Type: application/json.
(This is required since this is going to be a POST Http Action.)

Your screen should looks like this.



       Now, we need to generate an authorization token and the date in the required format. To do that, I am going to use the Pre-request Script section. POSTMAN will run this script before each request, This will be the part we will generate authToken and utcDate parameters. Copy and Paste the code to the Pre-Request Script tab.

// Get the current date in UTCFormat and save it in parameter
var now = new Date().toUTCString(); 
pm.environment.set("utcDate", now); 

// I need these to generate a token
var verb = 'POST'; 
var resourceType = "dbs"; 
var resourceId = ''; 
var text = (verb || "").toLowerCase() + "\n" + (resourceType || "").toLowerCase() + "\n" + (resourceId || "") + "\n" + now.toLowerCase() + "\n" + "" + "\n"; 

//Hash and Encode by using the masterkey.
var key = CryptoJS.enc.Base64.parse(pm.variables.get("masterKey")); 
var signature = CryptoJS.HmacSHA256(text, key).toString(CryptoJS.enc.Base64); 
var MasterToken = "master"; 
var TokenVersion = "1.0"; 
var authToken = encodeURIComponent("type=" + MasterToken + "&ver=" + TokenVersion + "&sig=" + signature);

// Save it to parameter to use in HTTP header
pm.environment.set("authToken", authToken);
   
     We are almost done, Only one thing left. I am trying to create a database, so I need to pass the name of the new database by using the body part of the request. You can find the required information in the documentation. In this example, all I need to pass is a JSON format document.

{ "id":"testdb" }
     
      I think we are ready to send this request to local CosmosDB to create a new database. We have the URL, required headers, encoded signature and the body of the Request. I am going to click to Send and cross my fingers :)

      Success! Look at all that information we received back in the body of the Response. Database named testDb information is returned to me. Also, there are a lot of great information in headers tab too. 



9 comments:

  1. I think REST API and its features should definitely be applied while creating end report for users.

    powerbi read rest

    ReplyDelete
  2. There was an error in evaluating the Pre-request Script: TypeError: Cannot read property 'length' of undefined

    ReplyDelete
    Replies
    1. you need to set the environment to "local" using the top right corner dropdown (which will say "No environment" if it's the same problem I had)

      Delete
    2. Also, the variable names are case sensitive :)

      Delete
    3. The problem in this line: var key = CryptoJS.enc.Base64.parse(pm.variables.get("masterKey"));

      Method "parse" get "undefined" from "pm.variables.get("masterKey")"

      It turns out you must set a value to your variable before you want to read it.
      pm.environment.set("masterKey", "Your primary ReadOnly(for example) key");

      Delete
    4. No, I've mistaken.
      The real problem is when you coping text from this page and inserting it to your environment when creating variables. You copy variable with EXCESS space, like this "masterKey " and trying to access it with the name "masterKey".

      Delete
  3. I tried same example but to query documents using select * from C. I see empty documents list retrieved. Container does have documents loaded. Am I missing something. Below are the headers am using

    x-ms-version:2018-12-31
    x-ms-date: {{utcDate}}
    authorization : {{authToken}}
    Content-Type : application/query+json

    Response recieved :
    {
    "_rid": "OkheAMiCxyY=",
    "Documents": [],
    "_count": 0
    }

    ReplyDelete
  4. Thank you for posting this. It helped me immensely!

    ReplyDelete