Authorization enforcement for Cloud Run

Neil Kolban
Jul 28, 2020 · 11 min read
Image for post
Image for post

Cloud Run allows us to expose REST based services implemented within Docker containers. When we consider a REST service, we find that it can consist of multiple callable operations. This article illustrates how we can secure each operation individually.

Let us work with an example service that handles social media posts.

We might have operations such as:

  • /list — List submissions
  • /get — Get a submission
  • /submit — Submit a new submission
  • /delete — Delete a submission

We quickly see that not all operations should be permitted for all users. For example, we may want to allow everyone to list and get submissions, authors to submit new submissions and moderators to delete submissions.

Image for post
Image for post

Generically, this concept is known as authorization. While the list of possible callable actions is known to all, only certain users are allowed (authorized) to perform certain requests. Google Cloud Platform (GCP) supplies built-in Identity and Access Management (IAM) for GCP provided services but these govern access to GCP exposed functions and not application exposed functions. For our Cloud Run story, the enforcement of authorizations must be performed within the implementation of the services themselves to cover application specific permissions.

In this article we are going to explore and demonstrate one possible design and implementation story.

When a request to perform an operation arrives at Cloud Run, we must be able to authenticate the caller. This means that we need to know who the caller claims to be and verify that they are indeed that person. If we don’t know who is calling or can’t trust that they are who they present to be, then we can’t begin to handle authorization requests. The identification of the caller is achieved through the notion of an HTTP header called “authorization”. The value of this header is supplied by the caller and is expected to be a “token” that identifies the caller and proves that they are who they claim to be. The high level story is that we presume that the caller has (at some time in the past) proven to a trusted third party that their identity is valid. This third party then attests to this fact and signs the claim using private information that only the third party has. This then becomes the token. When Cloud Run receives the token, it can then validate that the signature was minted from a trusted source and we have now proven that the caller is verified.

Now that Cloud Run knows the identity of the caller, we can start to think about authorization. At a generic level, we want to answer the question “Is user X allowed to perform operation Y?”. If yes, then the call is allowed to move forward and if not, the call is terminated. There are many schemes available that can answer this question. One example would be keeping a database table that maps user identity to allowed operations. When a request is received, the table is queried using the caller’s identity as a lookup key. Solutions such as this become difficult to manage and distract from the core development of a solution. Fortunately there is a story that covers both authentication and authorization.

GCP provides a service called “Identity Platform”. Identity Platform provides a service that allows a user to authenticate using credentials resulting in a token that can be passed with a REST request to Cloud Run. In addition to managing all aspects of token creation and user accounts, Identity Platform allows us to add “custom claims” to the managed attributes of a user. A custom claim is a name/value pair attached to the user account. An example might be:

forum-role: author

or

forum-role: moderator

The association between users and claim data is managed by Identity Platform. When a token for a user is generated as a result of them authenticating, the token itself contains the claim. This means that Cloud Run can immediately see the claims and can allow or deny an operation request based on directly available information. A bad actor can’t fake the claim as the claim is contained in the token and the token is signed by the trusted third party. Any attempt to manipulate the token would result in its signature becoming invalid and detectable. We can represent our story in the following diagram:

Image for post
Image for post
  1. A user opens a browser and navigates to our web-site and is shown the web page (HTML) that is then rendered in the browser. The user is presented with a sign-in screen and they enter their credentials (userid/password).
  2. The browser securely sends the credentials to the GCP Identity Platform where they are authenticated as valid. The account data of the user is retrieved and any custom claims for the user are found. The identity of the user plus the custom claims are encoded in a token that is then signed by “Identity Platform” to provide trust. The token can then be used for multiple subsequent operation calls.
  3. The browser now makes a call to Cloud Run to perform some functional requests. The token is passed with the REST request. Your logic in Cloud Run receives the token, validates it with Identity Platform and examines the custom claims. Depending on the claim values, the operation will either be allowed or denied based on your own authorization rules.

Enough of the theory, let’s look at this story in practice. We will split our discussion into two parts. The first will assume that when a call arrives at Cloud Run, the call contains a token which contains claims. First we need some logic to obtain the token. The token is passed as data in the “authorization” header in the incoming HTTP request. The format of the value of this header is:

Bearer [Json Web Token Data]

In other words, the value of the header is the fixed string “Bearer “ followed by the token encoded in Json Web Token (JWT) format. We must extract the JWT token from the header:

const jwtValue = req.headers['authorization'].substring(7);

With our hands on the JWT token, we can validate that it is genuine and obtain its content:

const admin = require('firebase-admin');
admin.initializeApp();
const decodedToken = await admin.auth().verifyIdToken(jwtValue);

If we now assume that there is a custom claim called “forum-role”, we can test the value of this before honoring the request:

if (decodedToken["forum-role"] != "moderator") {
res.send("Not Authorized");
return;
}

In the preceding code fragments, we saw reference to a package called “firebase-admin” and the admin objects. Now we need to explain this area of function.

Google provides a component of GCP it calls Identity Platform. We can think of Identity Platform as a user account management system. Loosely, it is a repository of known users and attributes about them. If we contemplate our social media example service, it would be Identity Platform that owns the knowledge of all the defined users that exist. Identity Platform also provides authentication and token generation services. An end user would thus authenticate with Identity Platform and be given a signed token. A Cloud Run hosted application could then receive that token and present the token back to Identity Platform to validate that it is genuine. In addition to the security features just mentioned, Identity Platform also provides the ability to attach Custom Claims to the information associated with users. These claims are then made available in the tokens when a user authenticates. The claims are managed by Identity Platform and included in the signing of the tokens. It would not be possible to tamper with the claims in the token after the token was created by Identity Platform without that being detected.

Let us now look at what it takes to set up the Identity Platform.

In a new project, find Identity Platform in the menu:

Image for post
Image for post

On first use, you will need to enable it.

Image for post
Image for post

We now need to tell our Identity Provider instance what methods users can use to sign-in. We click the Providers tab and click ADD A PROVIDER:

Image for post
Image for post

Select Email / Password.

Image for post
Image for post

Click on the User button in the menu and then click ADD USER:

Image for post
Image for post

Enter a sample user identity (email address). It does not need to be real. Also provide a password.

Image for post
Image for post

Eventually, we will be able to login as that user.

This concludes our setup of Identity Platform. Now we need to turn our attention to the user interaction components which will be where the end user will authenticate and eventually make the call to Cloud Run. Before we go on, we must clarify the relationship between Identity Platform and Firebase. Firebase is Google’s product for building mobile applications that manifest through Android, iOS and web. Firebase provides a rich set of services to present web hosting, storage, function calls and more for front-end developers. This includes authentication capabilities. Firebase gives us the ability to authenticate users and use that security model to secure access for applications. It would thus appear that Google has two end user authentication stories … namely Identity Platform and Firebase Authentication. The reality is that there is only one. The same underlying product is in both Identity Platform and Firebase Authentication. The technology and implementation happens (confusingly) to have two names. When we think in terms of Firebase Authentication, everything is named consistently, however, when we think in terms of Identity Platform we will find that a lot of the APIs we use to work with it have the Firebase name in their packages and are documented in the Firebase documentation. Since there is only one underlying implementation of Identity Platform/Firebase Authentication, the API used to work with it is identical. If you see references to using Firebase Authentication APIs in connections with Identity Platform, bear this in mind.

We are going to create a web page that will be presented to the user to perform a login and make a REST call to Cloud Run. Within the Cloud Console Identity Platform pages, at the top right, we see an entry that reads:

Image for post
Image for post

Click the “setup details” link. A popup window will appear that contains HTML to be copied and pasted into your web page in the <head> area. This boilerplate HTML initializes the Firebase environment in the browser and provides a linkage to the project which owns the Identity Platform accounts.

Our goal is to eventually make a REST call to our Cloud Run hosted application passing in a token that proves who we are. That token is issued by Identity Platform as a result of us proving to it that we are who we claim to be. One way to achieve that is to use the FirebaseUI package and present a login form. When the user enters their credentials, those are passed to Identity Platform and, if the credentials were correct, we are returned a token.

The following script fragment contained within our page will do the job:

<script>
var ui = new firebaseui.auth.AuthUI(firebase.auth());
ui.start('#firebaseui-auth-container', {
signInOptions: [
firebase.auth.EmailAuthProvider.PROVIDER_ID
],
callbacks: {
signInSuccessWithAuthResult: function(authResult) {
authResult.user.getIdToken().then((token) => {
globalToken = token;
});
return false;
}
}
});
</script>

The way to read this is that we are creating an instance of the FirebaseUI object and calling its start method passing in an HTML <div> id reference which is where the UI will appear in our web page. We are also saying that we will accept email ID for login. When the login succeeds, a callback is called and we save the returned token in a page wide variable.

Once the web page is in possession of the token, it can make a REST request to Cloud Run passing in the token. Here is an example of that using jQuery Ajax calls:

$.ajax({
method: "GET",
url: "https://[YOUR-CLOUD-RUN].a.run.app/operation",
headers: {
"Authorization": `Bearer ${globalToken}`
}
}).done((data) => {
// Process response
});

How you make the REST call is up to you but notice the inclusion of the “Authorization” HTTP header where we pass the token that was returned from Identity Platform as when we signed in.

Now we can turn our attention to our Cloud Run application. By definition, our application will be listening for incoming REST calls. In this example, we choose NodeJS as our implementation language and express.js as our REST processor. Here is an example fragment which handles a request:

const admin = require('firebase-admin');async function getToken(req) {
if (!req.headers['authorization'] || !req.headers['authorization'].startsWith('Bearer ')) {
throw "No or bad authorization header"
}
const jwtValue = req.headers['authorization'].substring(7);
const decodedToken = await admin.auth().verifyIdToken(jwtValue);
return decodedToken;
}
app.get('/operation', async (req, res) => {
try {
const token = await getToken(req);
// Work with token
res.send("Success");
}
catch(ex) {
console.log(ex);
res.send(`Failed: ${ex}`);
}
});

The core is a helper function called getToken(). This takes as input the REST request received from the caller. We validate that it includes a header called “authorization” and the header starts with “Bearer “. If it does, we retrieve the expected JWT token and make a call to the Firebase admin functions to verify the passed in token. This performs several tasks. The first is that the signature of the token is matched against “Identity Platform” to validate that it was issued by Identity Platform and has not been manipulated. If this passes, we are returned a JavaScript object that represents the trusted content of the token. This includes fields such as email which we can use as the identity of the caller.

We have one final task ahead of us that will bring our story to a close. Our original story was that we wanted to allow or deny the execution of an operation based on a permission that the caller may or may not possess. We want to do this “custom claims”. First let us imagine that the token actually does contain a custom claim, how can we access it?

Each custom claim is presented as a property in the decoded token object available after we verify it. For example, if we have a custom claim called “forum-role”, we could check that in our code with:

const token = await getToken(req);
if (token['forum-role'] == "moderator") {
// Perform (allow) operation
} else {
// Reject (disallow) operation
}

It is vital to realize that the enforcement of authorizations are performed explicitly in the code. Each operation that is exposed must thus perform its own validation that the caller is authorized.

The final question now becomes one of how do we manage custom claims associated with tokens? Unfortunately, Google doesn’t provide any command line tools or web applications to achieve this but does provide an API that can be called.

Here is a simple program that will set a custom claim for a user:

const admin = require('firebase-admin');
const app = admin.initializeApp();
async function setClaim() {
const user = await admin.auth().getUserByEmail('john@example.com');
console.log(`User: ${JSON.stringify(user)}`);
await admin.auth().setCustomUserClaims(user.uid, {
"forum-role": "moderator"
});
}
setClaim();

A fully baked sample is provided in this GitHub repository. The following video shows a walk through of working with the sample and illustrates further the story presented in this article.

See also:

Google Cloud - Community

Google Cloud community articles and blogs

Neil Kolban

Written by

IT specialist with 30+ years industry experience. I am also a Google Customer Engineer assisting users to get the most out of Google Cloud Platform.

Google Cloud - Community

A collection of technical articles and blogs published or curated by Google Cloud Developer Advocates. The views expressed are those of the authors and don't necessarily reflect those of Google.

Neil Kolban

Written by

IT specialist with 30+ years industry experience. I am also a Google Customer Engineer assisting users to get the most out of Google Cloud Platform.

Google Cloud - Community

A collection of technical articles and blogs published or curated by Google Cloud Developer Advocates. The views expressed are those of the authors and don't necessarily reflect those of Google.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store