Using Python Decorators to Authenticate Google Cloud Functions

Cloud Functions are snippets of code that run in the cloud and triggered by a web request How do we stop just anyone from accessing (or worse, updating!) our database?...

2 months ago

Latest Post My Journey to MozFest by Rob Schaefer

Google Cloud Functions are a great way to glue together functionality between client apps and cloud based tools. Cloud Functions are snippets of code (in our case python) that are automatically spun up and run in the cloud and are triggered by a web request (among other methods). For example, say I wanted to update a database that lives in my Google Cloud Project, I could just send the data to a URL which would activate my python cloud function which could unpack and process the data.

No need to worry about managing servers or scaling, this is all handled by the Cloud Function service, you just POST to a URL and everything "magically" happens. They are super useful when it's not totally necessary to build out a complete REST API, but you still need a few endpoints to run server-side. This is great, but how do we stop just anyone from accessing (or worse, updating!) our database?

Google Cloud Functions

A google cloud functions are just a snippet of Python code that are passed a Flask Request object. This is purely for convenience as the the request object essentially just holds the data sent to the URL (e.g. JSON).

import json
def echo_payload(request):
    '''
        This function echoes the json payload
    '''
    data = request.get_json()
    return json.dumps(data)
A simple google cloud function

Conventionally, along with the payload, a token can be passed inside the header which can be used to validate users. We use the authentication functionality offered by Firebase (recently acquired by Google) to allow users to sign in using an email/password combination. For the sake of this post, assume that a user has logged in and they have access to their token (client side), which is just a string of characters (you can read more about and play with some tokens here).

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI
iOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiw
iaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fw
pMeJf36POk6yJV_adQssw5c
An example (fake) login token.

This token rides along with our HTTP request in the header section. We can modify our function to make sure that the user is valid by checking that header value:

import json

# Import some libraries to help validate tokens
from firebase_admin import auth 
firebase_admin.initialize_app()

def echo_authenticated_payload(request):
    '''
        This function echoes the json payload    
    '''
    # Get the token from the header
    # The token is in the "Authorization" field and starts with 
    # "Bearer "
    token = request.headers['Authrization'].replace('Bearer ','')
    # This will throw an exception if the token in invalid
    if auth.verify_id_token(token):
    	# if we get here, we are authorized to echo the payload
    	data = request.get_json()
    	return json.dumps(data)
    else:
    	# Return an "unauthorized" HTTP error code
    	return json.dumps('Unauthorized',401)
Adding some boilerplate code to make sure the user is valid

Thanks to some great library support, its pretty straightforward to check if a user has a valid token in the headers of the request object. However, we would need to copy and past this code every time we wanted to validate a user or would need to write a separate function to do do the validation, which can get a little messy.

What we are trying to express here is that there are certain functions that can only be executed by authorized users. This behavior is more a property of the function than a result, side effect, or product of it. Sounds like a perfect case for a python decorator (see this great review on python decorators).

Python Decorators

Python decorators are often used as a way to modify the behavior of a function. We can essentially break our echo_authenticated_payload function into two separate pieces echo and authenticated which we can then chain together to achieve the desired outcome. The behavior we are looking for is roughly: echo(authenticated(request)), where echo only gets called on authenticated requests. Let's write a decorator for our echo function:

from functools import wraps
def authenticated(fn):                                                          
    @wraps(fn)                                                                  
    def wrapped(request):                                             
        try:                                                         
            # Extract the firebase token from the HTTP header         
            token = request.headers['Authorization']
            token = token.replace('Bearer ','')      
            # Validate the token                                 
            verified = auth.verify_id_token(token)                   
        except Exception as e:
            # If an exception occured above, reject the request
            return abort(401,f'Invalid Credentials:{e}')             
        # Execute the authenticated function                         
        return fn(request)                                          
    # Return the input function "wrapped" with our
    # authentication check, i.e. fn(authenticated(request))
    return wrapped                                                   
We use the "wraps" function from the functools package

This authenticated function takes in another function (fn) and does one of two things. Either it is successful in extracting and authenitcating the token, in which case it returns the result of the original function (fn), or it returns the result of an abort call, which just returns an HTTP error code.

This decorator can be applied to any function that has the general function signature of def function_name(request), where the only parameter is a request object. We could make our decorator more generalized, but that is outside the scope of this blog post. For now, lets just decorate our original echo function to only accept authenticated requests.

import json

@authenticated
def echo_payload(request):
    '''
        This function echoes *AUTHENTICATED* json payloads
    '''
    data = request.get_json()
    return json.dumps(data)
A beautiful decorated python function!

All our boilerplate is gone! Well most of it. Our function is now "decorated" with @authenticated, which means that when the function is called, the code defined in the above def authenticated function gets evaluated first. This results in a clearly defined function that can only be called by a user with a valid token!

Rob Schaefer

Published 2 months ago