0

A resource token is created in cosmos db and when that is used to do REST API Call, CosmosDb gives 403 Unauthorized error. The same resource token when used in CosmosDb donot sdk, it works. So definitely something is wrong in the way i am doing REST API call. Here is the code.

using Microsoft.Azure.Cosmos;
using System.Net.Http.Formatting;

namespace CosmosDb_Rest_DotNet
{
    internal class Program
    {
        static readonly string endpoint = "https://localhost:8081/";
        static readonly string masterKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
        static readonly Uri baseUri = new Uri(endpoint);        

        static readonly string databaseId = "SampleDb";
        static readonly string collectionId = "Lists";
        static readonly string documentId = "cbd77fac-f7f0-46fb-a2f3-f6b48c11670c";
        static readonly string partitionKey = "MyPartitionKey";
        static readonly string userId = "userId";

        static readonly string utc_date = DateTime.UtcNow.ToString("r");

        static void Main(string[] args)
        {
            Console.WriteLine(DateTime.UtcNow.ToString("r"));
            string resourceToken = "";

            using (var cosmosClient = new CosmosClient(endpoint, masterKey))
            {
                Database cosmosdb = cosmosClient.GetDatabase(databaseId);
                var listsContainer = cosmosdb.GetContainer(collectionId);


                //Create user
                try
                {
                    var result = cosmosdb.CreateUserAsync(userId).Result;
                }
                catch (Exception)
                {
                    //user already exists
                }
                var cosmosUser = cosmosdb.GetUser(userId);

                Console.WriteLine($"Created Cosmos User: {cosmosUser.Id}");

                //create permission and get ResourceToken. Resource Token permission is restricted to Partition Key
                var listsContainerPermissionProps = new PermissionProperties($"{listsContainer.Id}-{cosmosUser.Id}",
                                                                            PermissionMode.All,
                                                                            listsContainer,
                                                                            new PartitionKey(partitionKey));

                var listsContainerPermissionResponse = cosmosUser.UpsertPermissionAsync(listsContainerPermissionProps, (int)TimeSpan.FromMinutes(30).TotalSeconds).Result;

                resourceToken = listsContainerPermissionResponse.Resource.Token;

                Console.WriteLine("Successfully received Resource Token.");
            }

            Console.WriteLine("Using Resource Token in CosmosClient SDK to query document.");

            //TEST resourceToken using Cosmos SDK
            using (CosmosClient resoureceTokenCosmosClient = new CosmosClient(endpoint, resourceToken))
            {
                Database resourceTokenCosmosDb = resoureceTokenCosmosClient.GetDatabase(databaseId);
                var resourceTokenListsContainer = resourceTokenCosmosDb.GetContainer(collectionId);

                var query = new QueryDefinition("SELECT * FROM c where c.Aid = 'MyPartitionKey'");
                var queryResponse = resourceTokenListsContainer.GetItemQueryIterator<dynamic>(query, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey("MyPartitionKey") });

                while (queryResponse.HasMoreResults)
                {
                    var items = queryResponse.ReadNextAsync().Result;

                    foreach (var item in items)
                    {
                        Console.WriteLine($"Received Item using CosmosClient with resource token: Name: {item.Name}, id: {item.id}");
                    }
                }
                Console.WriteLine("Resource Token Successfully worked and returned documents");
            }

            Console.WriteLine("Using same Resource Token in REST API Test");

            //REST Client TEST
            using (var client = new System.Net.Http.HttpClient())
            {
                string response = string.Empty;
                string authHeader = string.Empty;
                string verb = string.Empty;
                string resourceType = string.Empty;
                string resourceId = string.Empty;
                string resourceLink = string.Empty;

                client.DefaultRequestHeaders.Add("x-ms-date", DateTime.UtcNow.ToString("r"));
                client.DefaultRequestHeaders.Add("x-ms-version", "2020-07-15");
                client.DefaultRequestHeaders.Add("x-ms-documentdb-isquery", "True");

                try
                {
                    //EXECUTE a query
                    verb = "POST";
                    resourceType = "docs";
                    resourceLink = string.Format("dbs/{0}/colls/{1}/docs", databaseId, collectionId);

                    //url encode resource token
                    authHeader = Uri.EscapeDataString(resourceToken);

                    client.DefaultRequestHeaders.Remove("authorization");
                    client.DefaultRequestHeaders.Add("authorization", authHeader);                    

                    var qry = new SqlQuerySpec { query = "SELECT * FROM c where c.Aid = 'MyPartitionKey'" };
                    var r = client.PostWithNoCharSetAsync(new Uri(baseUri, resourceLink), qry).Result;

                    Console.WriteLine(r.Content.ReadAsStringAsync().Result);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            }
        }

        private static string GenerateMasterKeyAuthorizationSignature(string verb, string resourceId, string resourceType, string key, string keyType, string tokenVersion)
        {
            var hmacSha256 = new System.Security.Cryptography.HMACSHA256 { Key = Convert.FromBase64String(key) };

            string payLoad = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}\n{1}\n{2}\n{3}\n{4}\n",
                    verb.ToLowerInvariant(),
                    resourceType.ToLowerInvariant(),
                    resourceId,
                    utc_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));
        }
    }

    //This is used when executing a query via REST
    //DocumentDB expects a specific Content-Type for queries
    //When setting the Content-Type header it must not have a charset, currently. 
    //This custom class sets the correct Content-Type and clears the charset
    public class NoCharSetJsonMediaTypeFormatter : JsonMediaTypeFormatter
    {
        public override void SetDefaultContentHeaders(Type type, System.Net.Http.Headers.HttpContentHeaders headers, System.Net.Http.Headers.MediaTypeHeaderValue mediaType)
        {
            base.SetDefaultContentHeaders(type, headers, new System.Net.Http.Headers.MediaTypeHeaderValue("application/query+json"));
            headers.ContentType.CharSet = "";
        }
    }

    //A custom extension of HttpClient that adds a new PostWithNoCharSet async method
    //that uses the custom MediaTypeFormatter class to post with the correct Content-Type header
    public static class HttpClientExtensions
    {
        public static async Task<HttpResponseMessage> PostWithNoCharSetAsync<T>(this HttpClient client, Uri requestUri, T value) { return await client.PostAsync(requestUri, value, new NoCharSetJsonMediaTypeFormatter()); }
    }

    class SqlQuerySpec
    {
        public string query { get; set; }
    }
}

Here is the output

Created Cosmos User: userId
Successfully received Resource Token.
Using Resource Token in CosmosClient SDK to query document.
Received Item using CosmosClient with resource token: Name: Test Doc, id: cbd77fac-f7f0-46fb-a2f3-f6b48c11670c
Resource Token Successfully worked and returned documents
Using same Resource Token in REST API Test
{"code":"Forbidden","message":"Insufficient permissions provided in the authorization header for the corresponding request. Please retry with another authorization header.\r\nActivityId: 723c8bc1-2d11-463e-a5bc-7f3ef038e59f, Microsoft.Azure.Documents.Common/2.14.0"}

Anyone should be able to run the code for local cosmos db emulator.

1
  • The resource token should not be URL-encoded. Add it directly to the Authorization header. Make sure the resourceLink correctly identifies the resource. Verify the resource token has permissions for the query's partition key. Commented Jan 7 at 6:04

2 Answers 2

0

when calling the rest api endpoint, the authorization header is not just the resource token. refer to this link for details:

https://learn.microsoft.com/en-us/rest/api/cosmos-db/access-control-on-cosmosdb-resources#resource-tokens

it should be in this format: type=resource&ver=1.0&sig=5mDuQBYA0kb70WDJoTUzSBMTG3owkC0/cEN4fqa18/s=

Sign up to request clarification or add additional context in comments.

7 Comments

Yes it is in correct format. the same resource token works using CosmosClient .Net SDK. Here is the example token. type=resource&ver=2.0&sig=wyQ6frxgogvmbHR5gtebHw==;46DrmwwVrubHp+dhMA1f9K3zQ/cCdNAePX0O4FoCpfkQPSdJSDp8L1JVzREFjO/kEMjbDohla/pBrRab3syEQwNxwABfeMtPFusX6oj6LH7qZ9OGAAu32iXFCcmQOcJ8bnxyuZzcvXWfouP/tKT/CcqHU/e781rsfRmyivNO0agfMNrmFMTidz7mPRUQVvNprj3+UrWKG4bOnt+LOxJWpBZ648x5wK+p6ffNTRwPAwY=;i03R9CMnxl/RyyK6xfDE/ZeJQnXV1pSNNyAgal7SQag
just tried out my self. got same error first. if you remove the partitionkey in the PermissionProperties var listsContainerPermissionProps = new PermissionProperties($"{listsContainer.Id}-{cosmosUser.Id}", PermissionMode.All, listsContainer);, then add client.DefaultRequestHeaders.Add("x-ms-documentdb-query-enablecrosspartition", "True");. then it can query correctly so i think it does something with partitionkey. might also explore with client.DefaultRequestHeaders.Add("x-ms-documentdb-partitionkey", partitionKey); this header.
maybe access to a partitionkey does not get you full query permission ? stackoverflow.com/questions/50396780/…
Yes removing PartitionKey from permissions worked. But now the resource token has access to all the partitions. I want to restrict access to just 1 partition.
This did the trick. Created the Permission with PartitionKey and added following partitionkey in header as client.DefaultRequestHeaders.Add("x-ms-documentdb-partitionkey", "[\"MyPartitionKey\"]"); Now the Rest API works.
|
0

The error is resolved after adding partition key in request header.

client.DefaultRequestHeaders.Add("x-ms-documentdb-partitionkey", "[\"MyPartitionKey\"]");

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.