Connecting to Cosmos DB Without Connection Strings
I'm a professional developer of applications for mobile devices.
As such, it is my solemn duty to test any application I develop to ensure it works in the field. My professional reputation is at stake after all!
This is why each and every day, hour after hour, unfailingly you can find me testing my apps.
I test them at hip cafes for breakfast.
I test them while taking long walks in the park.
I test them during 3 hour lunches at the new restaurant down the block.
But my favorite place to test apps is on the commute to the movies.
I don't have time to see garbage movies (I'm too busy testing my apps, lol 🤣) which is why I created a sweet app that shows me movie reviews!
In this article I'm going to show you how I built a Xamarin.Forms app that displays movie reviews downloaded as documents from Azure Cosmos DB.
Here's the fun part, this app also makes use of Cosmos DB permissions and an Azure Function to only show premium reviews to logged in users.
How We're Going To Pull This Off
Our app is going to display two types of movie reviews. One is a public review and can be viewed by anybody. The other is a premium review and is only viewable by people who have created an account and have logged in to the app.
There's going to be 3 pieces of the puzzle to display the reviews.
The first will be Azure Cosmos DB, which will hold the movie reviews as documents.
The second will be a Xamarin.Forms app (though this could be a web app too) which will directly query Cosmos for data and then display it (and also take care of logging users in).
Finally, there will an Azure Function that I'm going to call a Permission Broker. The app will invoke this HTTP triggered function to get a token (think of it as a credential), that will be used to access appropriate movie reviews in Azure Cosmos DB.
Dazed and Confused? Read on... read on...
Azure Cosmos DB
Let's start with Azure Cosmos DB.
We're going to use the magic of Cosmos DB permissions to ensure that non-logged in users can only read public reviews, while logged in users can read both public and premium. (For a quick refresher on Cosmos permissions, see this article.)
How is the movie review document structured? Glad you asked!
That document is going to have the same schema whether it's a premium review or a public one.
{
"id": "f58f93de-4f3c-464c-a72f-61c770c870ef",
"movieName": "Ant Man and the Wasp",
"starRating": 2,
"reviewText": "I'm embarassed I even watched the trailer.",
"isPremium": true
}
The isPremium
field will indicate whether a movie review is a premium one or not.
All of the reviews will reside in a single collection. There will be a partition on that collection - and that partition will be on the isPremium
field.
With the database setup out of the way, let's take a peek at the app.
The App
For now, our app is going to be bare bones and simple. It will display reviews in a list, and allow you to sign-up or sign-in to an Azure AD B2C account. (Check out this blog series for everything you ever wanted to know on AD B2C.)
We're going to use the Azure Cosmos DB client library to have a direct connection between the app and Cosmos DB.
Because the app will be talking directly to the database - it will need login credentials. But... hard coding login credentials is... kinda dumb.
Don't hard code login credentials in your app. It's a dumb idea.
Instead of holding credentials within the app to connect to the database. The app should ask another party - or a permission broker - for a login token.
It can then use that token to make a connection to the database, and that token will grant access to only the records that is needed.
The beauty of this method is - the app really doesn't have to do anything different on its end to handle the records for the public or premium scenario. The login credentials - or token - will signify to Cosmos to only return the records the app has access to.
Let's recap where we're at so far then.
We have an Azure Cosmos DB instance that holds movie review documents in a single collection that is partitioned based on a boolean field in those docs.
We have a Xamarin.Forms app (though it could be any app that displays data) that wants to get those movie review documents from Azure Cosmos DB - but doesn't want to maintain a hard coded connection string. Oh - and in this case it only wants access to the appropriate documents based on whether a user is logged in or not.
So that brings us to... the thing that will grant access to the database and the documents held within...
The Permissions Broker
The permissions broker holds the keys to all the movie reviews.
Literally holds the keys. Because it resides on the server we can feel safe having the Cosmos DB connection string hard coded (or at least in an app.config) within it. To be even safer, we should use Azure Keyvault to hold our secrets...
The broker will connect to Cosmos with superuser rights and generate tokens that have access to either public movie review documents or public + premium movie review documents, based on whether an incoming request is authenticated or not.
It returns that token to the caller - and then that caller is free to initiate its own connection directly to Azure Cosmos DB, using the token as its credential. Thus ensuring it only has access to the appropriate documents.
Show Me The Code!!
So remember, we have an Azure Cosmos DB with a single database and a single collection. It holds one type of document and is partitioned by a boolean field on that document.
We want our permission broker to return a token that gives access to either public or public + premium documents, based on whether the user is logged in or not.
The Permission Broker Function!!
Our permission broker is an Azure Function. It's going to check whether or not a request is authenticated. Based on that, it's going to generate an Azure Cosmos DB permission for the incoming user. That single permission is going to grant access to all the docs the user needs.
Let's see it in action!
The Function Definition
[FunctionName("MovieReviewPermission")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req,
[DocumentDB(databaseName: "moviereview-db", collectionName: "reviews", ConnectionStringSetting = "CosmosConnectionString")] DocumentClient client,
TraceWriter log)
This Azure Function is triggered by an HTTP request. That's cool - as you'd expect.
It also is using an Azure Cosmos DB input binding. This way we don't have to maintain the connection to Cosmos ourselves. We'll let the Function runtime do it for us, and it will hand us an initialized DocumentClient
object that's already attached to our database and collection.
The Function Body
string userId = GetUserId(log);
Permission token = await GetPartitionPermission(userId, client, dbName, collectionName);
return req.CreateResponse<string>(HttpStatusCode.OK, token.Token);
The actual body of the function is pretty straight-forward. (I'm saving all the hard stuff for the GetPartitionPermission
function.)
But what it does is first grab the user id associated with the incoming request. If the incoming request is not authenticated, I'm going to attach the name public to it. (This authentication / authorization part can be whatever you want it to be, which is why I'm not getting in too deep here. Check out the code in GitHub to see it in action.)
Then it generates the correct Cosmos DB Permission.
Finally it returns the token (a string) for that permission.
Generating the Permission
This is the heart of it all. It's here that we ask Cosmos to generate a permission object to certain resources (movie reviews).
Here's the definition of the function that we'll be using, so you can see the incoming variables and the return:
static async Task<Permission> GetPartitionPermission(string userId, DocumentClient client, string databaseId, string collectionId)
If you remember from the previous article on Cosmos Permissions - you can think of a Cosmos user as an abstraction for a set of permissions on a resource. In other words, a Cosmos user is a grouping of permissions, so it stands to reason that each permission for a user needs a unique ID.
So when we generate / retrieve the permission - that's the first thing we do. We decide on the name format and set it.
string permissionId = "";
bool isLimitedPartition = false;
Permission partitionPermission = new Permission();
if (userId == publicUserId)
{
permissionId = $"{userId}-partition-limited-{collectionId}";
isLimitedPartition = true;
}
else
{
permissionId = $"{userId}-partition-all-{collectionId}";
isLimitedPartition = false;
}
By having the permissionId
set - we can then ask Cosmos to retrieve it for us.
Uri permissionUri = UriFactory.CreatePermissionUri(databaseId, userId, permissionId);
partitionPermission = await client.ReadPermissionAsync(permissionUri);
Since every permission for a user in Azure Cosmos DB has a unique name, we can create an Uri
for it and then have the DocumentClient
try and read it out.
If the permission already exists - we're done. We can return it. Otherwise...
All operations against Cosmos are REST based, so if either the permission doesn't exist yet - or the user doesn't exist, we'll get a 404 error.
catch (DocumentClientException ex)
{
if (ex.StatusCode == HttpStatusCode.NotFound)
{
await CreateUserIfNotExistAsync(userId, client, databaseId);
After trying to read the permission, we want to catch a DocumentClientException
and then check its StatusCode
property to see if it's equal to a HttpStatusCode.NotFound
.
If the status code is NotFound
- we're going to try and create the user in Cosmos.
static async Task CreateUserIfNotExistAsync(string userId, DocumentClient client, string databaseId)
{
try
{
await client.ReadUserAsync(UriFactory.CreateUserUri(databaseId, userId));
}
catch (DocumentClientException e)
{
if (e.StatusCode == HttpStatusCode.NotFound)
{
await client.CreateUserAsync(UriFactory.CreateDatabaseUri(databaseId), new User { Id = userId });
}
}
}
The pattern here is the same. Use the DocumentClient
to see if the user already exists. If not, catch the 404 and create it.
Then after the CreateUserIfNotExistAsync
function returns, we finally get around to creating the permission!
var newPermission = new Permission
{
PermissionMode = PermissionMode.Read,
Id = permissionId,
ResourceLink = collectionUri.ToString()
};
This permission is for read-only. It has the Id
we created above. And the ResourceLink
is pointing to the entire collection where we're hosting all the movie reviews.
HOLD THE PHONE!!
We're about to return a permission for the entire collection?!? Granting whoever calls this function full read access to the entire collection?!?
Yes.
But ... this next line limits the permission to only a particular partition if the user isn't logged in:
if (isLimitedPartition)
newPermission.ResourcePartitionKey = new PartitionKey(false);
This is why I was making such a big deal about having the collection's partition key in place before!
All that's left to do then is create the permission in Cosmos
partitionPermission = await client.CreatePermissionAsync(userUri, newPermission);
And return it to the Function's calling code.
The Function then will return only the Token
portion of the Permission. Which in this case is all I need for...
Requesting and Consuming the Token
The hard part is over ... creating the permission broker Function.
Requesting the Token
All we need to do now is invoke it before we instantiate a DocumentClient
on the Xamarin.Forms (or any client) side.
The code to request the token...
public async Task<string> GetPermissionToken(string accessToken)
{
var baseUri = new Uri(APIKeys.BrokerUrlBase);
var client = new HttpClient { BaseAddress = baseUri };
var brokerUrl = new Uri(baseUri, APIKeys.BrokerUrlPath);
var request = new HttpRequestMessage(HttpMethod.Get, brokerUrl);
// Here check if there's a token or not
if (!string.IsNullOrEmpty(accessToken))
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var token = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
return token;
}
...is code you would use to make any normal HTTP call. Note that I'm passing in an accessToken
parameter. This is an access token obtained by calling out to Azure AD B2C (beyond the scope here, but check out this series and the code to find out more).
Consuming the Token
Once the Cosmos DB token is returned, we use it to instantiate the DocumentClient
object.
var token = await functionService.GetPermissionToken(accessToken);
docClient = new DocumentClient(new Uri(APIKeys.CosmosUrl), token);
This then limits our client's permissions to whatever that token will allow it to do.
Then finally, a query to pull down all the movie review records:
var feedOptions = new FeedOptions() { MaxItemCount = -1 };
if (notAuthenticated)
feedOptions.PartitionKey = new PartitionKey(false);
var collectionUri = UriFactory.CreateDocumentCollectionUri(APIKeys.MovieReviewDB, APIKeys.MovieReviewCollection);
var query = docClient.CreateDocumentQuery<MovieReview>(collectionUri, feedOptions).AsDocumentQuery();
Note that the FeedOptions
object has been primed to have a PartitionKey
in it should the user not be authenticated. This way it only is looking at public reviews.
Otherwise, the code handles public or premium reviews in exactly the same way!
Summing It Up
It was quite the journey we went on to get here ... Azure Cosmos DB, Permissions, Azure Functions, watching movies when we should be working, Xamarin.Forms, authentication... wow!
What it all comes down to though - you do not want to have a connection string to a database in any client application ever.
Rather, leave the connection strings up in the server where they can be properly guarded (and ideally in Azure Key Vault to be even safer).
But not having the connection string on the client doesn't mean you can't do direct communication to the database however ... you can use tokens!
And you generate those tokens through a permission broker.
The permission broker gets asked by a client for some credentials to Azure Cosmos DB. It then talks to Cosmos DB, gets the exact permissions to what it needs to, then returns those permissions to the calling client.
And there you have it - a movie review app that only returns premium reviews if the user is logged in. And it doesn't have to maintain any connection strings or do anything else special to do so!