Why do I get an unauthorized exception when using a DocumentClient with permissions returned from my Azure Functions app?

Viewed 150

Inspired by Matt Soucoup's session on Stitching together a mobile backend for your app, I'm trying to do just that.

I have a Xamarin Forms app which uses AD B2C to authenticate a user. The app then requests a list of permissions for Cosmos DB in respect of that user from an Azure Function. The returned permissions are then passed to a DocumentClient constructor.

However, when I try to save a document into Cosmos DB, I'm getting a Microsoft.Azure.Documents.UnauthorizedException with the message 'The client does not have any valid token for the requested resource'.

Here's the relevant code from the Azure Function (abridged for brevity). Although at the moment I'm only requesting one permission (and so I could retrieve an access token rather than a list of permissions), the app will require to allow the user access to more than one collection, so this functionality will be required.

public static class CommDiaryBroker
    {
        [FunctionName("CommDiaryBroker")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            [CosmosDB(databaseName:"commdiarydb",collectionName:"roster",ConnectionStringSetting="CommDiaryCosmosConnectionString")] DocumentClient client,
            ClaimsPrincipal claims, 
            ILogger log)
        {
            log.LogInformation("Requesting permissions for the commdiarydb database.");

            if(claims == null)
            {
                return new NotFoundResult();
            }

            var azpClaim = claims.Claims.SingleOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");

            var userId = azpClaim?.Value;

            log.LogInformation($"User Id: {userId}");

            if(string.IsNullOrEmpty(userId))
            {
                log.LogInformation("Returning not found result");
                return new NotFoundResult();
            }

            var dbName = "commdiarydb";
            var collectionname = "roster";

            List<Permission> tokens = await GetPartitionPermission(userId, client, dbName, collectionname, log);
            log.LogInformation($"Returning {tokens.Count} permissions");

            return new OkObjectResult(tokens);
        }

        static async Task<List<Permission>> GetPartitionPermission(string userId, DocumentClient client, string databaseId, string collectionId, ILogger log)
        {
            var permissionId = userId;
            Permission partitionPermission;

            Uri permissionUri = UriFactory.CreatePermissionUri(databaseId, userId, permissionId);
            Uri collectionUri = UriFactory.CreateDocumentCollectionUri(databaseId, collectionId);
            Uri userUri = UriFactory.CreateUserUri(databaseId, userId);

            try
            {
                partitionPermission = await client.ReadPermissionAsync(permissionUri);
                log.LogInformation($"Existing roster partition permission found: {partitionPermission.Id}");
            }
            catch(DocumentClientException ex)
            {
                if(ex.StatusCode == System.Net.HttpStatusCode.NotFound)
                {
                    log.LogInformation($"There wasn't an existing permission, so we create a user if one does not exist");
                    await CreateUserIfNotExistAsync(userId, client, databaseId, log);
                    Permission newPermission = new Permission
                    {
                        PermissionMode = PermissionMode.All,
                        Id = permissionId,
                        ResourceLink = collectionUri.ToString(),
                        ResourcePartitionKey = new PartitionKey(userId)    
                    };
                    partitionPermission = await client.CreatePermissionAsync(userUri, newPermission);
                }
                else
                {
                    log.LogInformation($"Something went wrong attempting to read a permission: {ex.Message}");
                }
            }

            
            
            FeedResponse<Permission> permFeed = await client.ReadPermissionFeedAsync(userUri);

            return permFeed.ToList();
        }

        static async Task CreateUserIfNotExistAsync(string userId, DocumentClient client, string databaseId, ILogger log)
        {
            try
            {
                await client.ReadUserAsync(UriFactory.CreateUserUri(databaseId, userId));
                log.LogInformation("Found existing user");
            }
            catch(DocumentClientException ex)
            {
                if(ex.StatusCode == System.Net.HttpStatusCode.NotFound)
                {
                    await client.CreateUserAsync(UriFactory.CreateDatabaseUri(databaseId), new User {Id = userId});
                    log.LogInformation("Attempted to create a user");
                }
            }
        }

Here's code from my DataService class in my Xamarin Forms app, again abridged for brevity. It's the UpdateRosterEntry() method which throws the exception.

public class DataService
    {
        private static readonly string DatabaseName = "commdiarydb";
        private static readonly string RosterCollectionName = "roster";
        private static readonly Lazy<DataService> lazy = new Lazy<DataService>(() => new DataService());

        public static DataService Instance
        {
            get
            {
                return lazy.Value;
            }

        }

        private DataService()
        {

        }

        private async Task<DocumentClient> InitialiseAsync()
        {
            DocumentClient result;
            try
            {
                var accessToken = await GetAccessToken();
                if (accessToken.Any())
                {
                    result = new DocumentClient(new Uri(Constants.Azure.CosmosEndpointUrl), accessToken);
                    Debug.WriteLine($"DataService.cs: A new DocumentClient has been created");
                }
                else
                {
                    result = null;
                }
            }
            catch (Exception)
            {
                result = null;
                Debug.WriteLine("DataService.cs: Something went wrong while trying to initialise the DocumentClient");
            }
            return result;
        }

        

        public async Task<ResourceResponse<Document>> UpdateRosterEntry(RosterEntry entry)
        {
            DocumentClient client = await InitialiseAsync();
            ResourceResponse<Document> response = null;
            if (client != null && AuthenticationService.Instance.CurrentUser != null && AuthenticationService.Instance.CurrentUser.IsLoggedOn)
            {
                try
                {
                    Debug.WriteLine($"Attempting to upsert Roster Entry with Id {entry.Id} and userId {entry.UserId}");
                    response = await client.UpsertDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseName, RosterCollectionName),
                        entry,
                        disableAutomaticIdGeneration: true,
                        options: new RequestOptions
                        {
                            PartitionKey = new PartitionKey(entry.UserId)
                        });
                }
                catch (Exception ex)
                {
                    response = null;
                    Debug.WriteLine($"DataService.cs: A problem occurred upserting a roster entry, {ex.Message}");
                }
            }
            return response;
        }

        
        public async Task<List<Permission>> GetAccessToken()
        {
            List<Permission> results;
            try
            {
                var baseUri = new Uri(Constants.Azure.TokenEndpointBaseUrl);
                var httpClient = new HttpClient
                {
                    BaseAddress = baseUri
                };
                HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, new Uri(baseUri, Constants.Azure.TokenPath));
                message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AuthenticationService.Instance.CurrentUser.AccessToken);
                var tokenResponse = await httpClient.SendAsync(message);
                tokenResponse.EnsureSuccessStatusCode();
                string resultString = await tokenResponse.Content.ReadAsStringAsync();
                results = new List<Permission>(JsonConvert.DeserializeObject <List<Permission>>(resultString));
                Debug.WriteLine($"DataService.cs: {results.Count} permissions were returned");
                Debug.WriteLine($"DataService.cs: First permission is {results[0].Id}, has {results[0].Token} token, with resource link {results[0].ResourceId}");
            }
            catch(HttpRequestException hex)
            {
                results = new List<Permission>();
                Debug.WriteLine($"DataService.cs: Something went wrong while fetching an access token from the Azure Function, {hex.Message}");
            }
            catch (Exception ex)
            {
                results = new List<Permission>();
                Debug.WriteLine($"DataService.cs: Something went wrong while fetching an access token from the Azure Function, {ex.Message}");
            }
            return results;
        }


    }

The code has been heavily inspired by the MonkeyFinder and Azure Cosmos DB Resource Token Broker samples.

Due to logging, I know that a list with a single permission object is being returned to the app and the permission seems to have the correct values for its properties. It also seems as if the DocumentClient is being successfully created. In the Azure portal, I can see that the requests are reaching Cosmos DB.

Initially, in my Azure Function app, I was simply creating a list of permission instances and returning them. After referring to the Azure Cosmos DB documentation, I tried instead to retrieve the permissions from the permission feed from the user and return that. The error remains the same either way.

I have also made sure that I am using the same partition key value in the app code which inserts items into the Cosmos DB collection as I am using to create the permissions.

I have read the permissions documentation on the Azure Cosmos DB and Azure Cosmos DB .NET SDK pages, as well as other blogs here and here, and am still not sure how to resolve this issue.

Any advice or help is greatly appreciated.

EDIT: As it transpires, the resourcePartitionKey value in the Permission that is returned is null and is probably the source of this error. I still can't figure out why it is happening though.

EDIT 2: I've figured out a workaround.

As far as I can figure, and based on this GitHub issue on the Azure Cosmos Db .NET SDK v2 repo, the reason the resourcePartitionKey is null on my returned Permission instances is because of a problem with deserialisation from the string in the HTTP response.

So, based partly on this sample, I constructed a small PermissionSummary class in order to expedite the transmission across the serialisation boundary:

public class PermissionSummary
    {
        
        public string Id { get; set; }
        public string Token { get; set; }
        public string UserId { get; set; }
    }

In the Azure Function, I amended the GetPartitionPermission() method to return a Task<List<PermissionSummary>>.

At the end of the method, I compile a list of PermissionSummary instances from the user's permission feed:

List<PermissionSummary> results = new List<PermissionSummary>();
FeedResponse<Permission> permFeed = await client.ReadPermissionFeedAsync(userUri);
foreach(var permission in permFeed.ToList())
{
results.Add(new PermissionSummary
    {
        Id = permission.Id,
        Token = permission.Token,
        UserId = userId
    });
}

return results;

With the PermissionSummary class defined in both my Azure Function and in the Xamarin.Forms client project, these objects are now correctly serialised and deserialised.

However, I can't pass these PermissionSummary instances directly to the DocumentClient constructor; it's expecting either a list of Permission objects or an access token.

So, I've amended my InitialiseAsync() method to take a single string parameter named permissionId.

This method now retrieves all of the PermissionSummary instances, and simply selects the correct one based on the permissionId passed to it. It then passes the access token from that PermissionSummary to the DocumentClient constructor.

This means that in any method which attempts to access the Cosmos DB, such as my UpdateRosterEntry() method, I now need to construct the permission id for the permission which is required to allow the logged in user to access the container which the method will operate on. This is possible since I'm constructing the permission ids in a standard way anyway.

So, a workaround which seems to work. If anyone has any ideas though on being able to serialise and deserialise Permission objects, and therefore provide an answer to my original question, I'd still love to hear them!

0 Answers
Related