Have you ever had to call a complex REST call that required very specific signatures and programatically generated authentication headers, or needed to do extra processing or transformation on the expected payload? Sometimes, depending on your context, doing that from a client is impossible, extremely cumbersome or downright unnecessary. Alternatively, coding and deploying a microservice or a backend to handle all that can be time consuming. What if there was an easier way to do it?

In the guide below, we’ll be looking at how to quickly move all that complexity in the cloud, with the help of a Serverless function, and an API Gateway to keep our calls secured.

As an example, we’ll be using the Search Service API of Oracle Cloud Infrastructure, which is an API that requires REST calls to be signed respecting the IETF Standard.

We will learn how to quickly write a Serverless function in Python that handles the authorisation for us, and how to secure it behind an API Gateway in Oracle Cloud.

Prerequisites

Since we will be using OCI in our example, there are some prerequisites that need to be checked before we actually start. You can skip these steps if your policies are already in place, and if you already have a Virtual Cloud Network and a Subnet.

1. Dynamic Group and Policies

  • Create Dynamic Group for Oracle Functions

ALL {resource.type = 'fnfunc', resource.compartment.id = '<COMPARTMENT_OCID>'}
  • Allow API GW access to Oracle Functions
ALLOW any-user to use functions-family in compartment <COMPARTMENT_NAME> where ALL {request.principal.type= 'ApiGateway', request.resource.compartment.id = '<COMPARTMENT_OCID>'}
  • Allow Oracle Functions Dynamic Group access to OCI resources

Allow dynamicgroup <DYN_GRP_NAME> to manage all-resources in tenancy

2. Create a VCN and a subnet, and add port 443 (HTTPS) to the security list in the Ingress Rules. If you already have a VNC and subnet, you may skip this step.

Seting up Oracle Functions

After we’re done with all the prerequisites, it’s time to create our serverless function that will handle the complex API call for us. In order to do that, we will be creating an Application in Oracle Functions, then write our code and quickly deploy it.

An application is a collection of functions, each of which can be coded in a different programming language. For our example this time, we will be using Python to write our code.

Create your first application

  1. Sign in to the Console as a functions developer.

  2. In the Console, open the navigation menu and click Developer Services. Under Functions, click Applications.

  3. [OPTIONAL] Select the region you are using with Oracle Functions.

  4. Click Create Application.

This image shows the New Applicatoin dialog, with empty Name, VCN, and Subnets fields.

5. Specify:

  • oracleapi as the name for the new application. You’ll deploy your first function in this application, and specify this application when invoking the function.

  • The VCN and public subnet in which to run the function.

6. Click Create.

See detailed instructions for more information.

Now that our Application has been created, we can go ahead and write our function code. We can connect to the Functions environment using either the cloud shell or your local machine. For ease of use we will be using cloud shell in this guide.

Create the function from Cloud Shell

NB. This procedure will also appear in the Getting Started part of your Function Application, once it has been created.

Setup fn CLI on Cloud Shell

  1. Launch Cloud Shell in your OCI Gen2 dashboard

  2. Use the context for your region

    fn list context
    
    fn use context eu-frankfurt-1
  3. Update the context with the function’s compartment ID.
    fn update context oracle.compartment-id <COMPARTMENT_ID>
  4. Provide a unique repository name prefix to distinguish your function images from other people’s.

    fn update context registry fra.ocir.io/<tenancy_name>/[repo-name-prefix]
  5. Generate an Auth Token

  6. Log into the Registry using the Auth Token as your password.

    docker login -u '<tenancy_name>/<user_name>' fra.ocir.io
  7. Verify your setup by listing applications in the compartment.

    fn list apps

Verify your setup by listing applications in the compartment

Now that the Functions context is defined, we can create our function.

Create, deploy, and invoke your function

  1. Write the function code.

A Python serverless function in Oracle Functions contains 3 files

  • A file called func.py with your function code

  • A file called func.yaml which contains the function definition (name, version, how much RAM to use, etc)

  • A file called requirements.txt with the required packages

Let’s look at each one by one:

requirements.txt

fdk
oci
requests

The functions development kit (fdk) package is required for all functions, while we will be using the OCI package for signing the requests, and the requests package for the REST API invocation.

func.yaml

schema_version: 20180708
name: searchapi
version: 0.0.1
runtime: python
build_image: fnproject/python:3.8-dev
run_image: fnproject/python:3.8
entrypoint: /python/bin/fdk /function/func.py handler
memory: 64
timeout: 30

The yaml definition file contains the name of the function, runtime, the allocated memory (in MB), the timeout (in seconds) and the images that will be used to build the Docker container in the Functions environment.

func.py

import io
import json
import oci
import requests
from fdk import response


def handler(ctx, data: io.BytesIO = None):
    try:
        body = json.loads(data.getvalue())
    except Exception:
        raise Exception()
    resp = search(body)
    return response.Response(
        ctx,
        response_data=json.dumps(resp),
        headers={"Content-Type": "application/json"}
    )


def search(body):
    signer = oci.auth.signers.get_resource_principals_signer()
    endpoint = 'https://query.eu-frankfurt-1.oci.oraclecloud.com/20180409/resources'

    try:
        # Call the Search Service API and get the query data
        output = requests.post(endpoint, json=body, auth=signer)
    except Exception as e:
        output = "Failed: " + str(e)
    return output

We can see two functions defined in the code above. Firstly, handler is the entry point for the function code. We get the body sent by the client, and pass it as a parameter to the second function, called search, which will call the Search Service API with the proper authorization signature, and return the result to the handler, who will send it back to the client.

  1. Publish the code on github or any other repository, and clone it in your cloud shell. ALternatively, you can create the function directly within the cloud shell console.

    git clone [function repo]/search

  2. Switch into the generated directory.
    cd search

  3. Deploy the function to Oracle Functions.

    fn -v deploy --app oracleapi

After the deployment completes, you will see the function has been packaged in a Docker container and is available within the Application that you created previously.

At this point in time, we can call the function directly from the cloud shell to test it. Here is an example querying all ADB instances on our tenancy.

echo -n '{"type":"Structured", "query":"query autonomousdatabase resources"}' | fn invoke oracleapi search

Setting up API Gateway

After our function has been created and we’ve tested that it works properly, it’s time to set up a secure REST endpoint in API Gateway that we can call from a client application, and that will invoke the function above.

Create your API gateway

  1. . Under API Management, click Gateways.

  2. Click Create Gateway and specify:

  • a name for the new gateway, such as oracleapi-gw

  • the type of the new gateway as Public

  • the name of the compartment in which to create API Gateway resources

  • the name of the VCN to use with API Gateway

  • the name of the public regional subnet in the VCN

 

This image shows the Create Gateway dialog, with all fields empty by default, except for the Type field which is set to Public by default.

3. Click Create.

When the new API gateway has been created, it is shown as Active in the list on the Gateways page.

See detailed instructions for more information.

Create your API deployment

  1. On the Gateways page in the Console, click the name of the API gateway you created earlier.

  2. Deployments, and then click Create Deployment.

  3. Click From Scratch and in the Basic Information section, specify:

  • a name for the new API deployment, such as oracleapi

  • a path prefix to add to the path of every route contained in the API deployment, such as /oracleapi

  • the compartment in which to create the new API deployment

This image shows the Basic Information page of the Create Deployment workflow, with the From Scratch option selected. Other fields are empty by default.

Click Next and in the Route 1 section, specify:

  • a path, /search

  • a method accepted by the back-end service, POST

  • the type of the back-end service, and associated details:

  • Oracle Functions

  • Choose the function called search from the drop down list.

This image shows the Routes page of the Create Deployment workflow, with all fields empty by default.

Click Next to review the details you entered for the new API deployment, and click Create to create it.

When the new API deployment has been created, it is shown as Active in the list of API deployments.

See detailed instructions for more information.

[Optional] Setting up token authentication to API Gateway using Identity Cloud Service

API Gateway allows us to set up authentication using either a custom code that we can write in Oracle Functions, or an existing identity provider, such as IDCS, Auth0, and so on.

Let’s look at how we can set up an OAuth2 authentication flow in IDCS for our newly created endpoint.

Set up OAuth2 in IDCS

Prerequisites for using JWT Tokens

When enabling authentication and authorization using JWTs, you must consider the following:

  • You need an identity provider (Auth0, IDCS, etc) that can issue JWT tokens for client applications

  • In API Gateway, you’ll have a choice between using Remote JWKS (JSON Web Key Set) or Static Key in the authentication policy for the validation of JWTs:

  • Remote JWKS will be retrieving the public verification keys from the identity provider at runtime

  • Static Keys will be using public verification keys already issued by an identity provider and API Gateway will be able to verify JWTs locally without having to contact the identity provider

In this example, we’ll be using Static Keys

Obtain JWKS Static Key from IDCS

Permission required for this step only: IDCS Administrator

Before creating the Authentication Policy in API Gateway, we need to obtain the JWKS Static Key from IDCS>

  1. From the IDCS Console – Identity & Security Menu – FederationOracleIdentityCloudService, click on the Oracle Identity Cloud Service Console link:

  2. In IDCS, go to SettingsDefault Settings and Toggle ON the Access Signing Certificate option, in case it’s not activated already.

  3. Get the JWKS from IDCS

https://idcs-1234xxx.identity.oraclecloud.com/admin/v1/SigningCert/jwk

When accessing the URL above, you’ll get a JSON file with the key as a response – save the response somewhere as we’ll need it later.

Once you’ve retrieved the JWKS Key, go back to IDCS and Toggle OFF the Access Signing Certificate option to prevent unauthorized access.

Create the Authentication Policy

Edit the existing API Deployment

Edit the deployment created earlier.

Add an Authentication Policy

Authentication:

Configure Authentication Policy

Configure the policy as follows:

  • Authentication Type: JWT

  • Authentication Token: Header

  • Authentication Scheme: Bearer

  • https://identity.oraclecloud.com/

  • Audiences: Specify a value that is allowed in the audience (aud) claim of a JWT to identify the intended recipient of the token. For this example, we’ll be setting the audience as the API Gateway’s hostname

  • Type: Static Keys

  • KEY ID: SIGNING_KEY

  • Format: JSON Web Key

Example:

{ “kid”: “SIGNING_KEY”, “kty”: “RSA”, “use”: “sig”, “alg”: “RS256”, “n”: “abcd1234xxxx”, “e”: “AQAB” }

All the values for these parameters can be found in the JWKS we saved earlier. Replace the values with the ones for your instance.

For more info on what each field represents – please check the documentation.

Create IDCS applications to generate and validate JWTs

We need to create two confidential applications in IDCS to generate and then to validate the tokens:

  • A Resource Server – this application will be used to validate the tokens

  • A Client Application – this application will be used by a client to obtain the tokens

Create the Resource Server Application

The Resource Server Application will be used for JWT validation.

  1. From the IDCS dashboard, go to Applications, click on Add and select Confidential Application

  2. Give the Resource Server application a Name and click on Next

  3. Skip the Client configuration

  4. Select Configure this application as a resource server

  5. Set the Primary Audience with the same value as we set in the API Gateway deployment.

  6. Add a scope for OAuth. For this example, it can be whatever you want.

  7. Click on Next twice, and then on Finish

  8. Activate the Resource Server application.

Create the Client Application

The Client Application will be used by the API clients to obtain the tokens.

  1. From the IDCS dashboard, go to Applications and click on Add and select Confidential Application

  2. Give the Client Application a Name and click on Next

  3. Configure this application as a client, check Client Credentials and JWT Assertion

  4. Add the Scope defined in our Resource Server app

  5. Click on Next and skip the rest of the screens

    A Client ID and Client Secret for this client application will be generated for you at the end of the process. You will need to save them, as they will be used from the client application, but they can also be consulted afterwards, in the application’s definition, on the Configuration tab.

  6. Activate the application

Test the service

Using cURL, Postman or any other REST client, test out the services:

1. Get the JWT Token form IDCS:

curl --location --request POST 'https://idcs-1234xxxx.identity.oraclecloud.com/oauth2/v1/token' \
--header 'Authorization: Basic <BASE64 ENCODED CLIENT_ID:CLIENT_SECRET>' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=<scope>'

2. Call the API Gateway endpoint:

curl --location --request POST 'https://1234xxxx.apigateway.<region>.oci.customer-oci.com/oracleapi/search' \
--header 'Authorization: Bearer <TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"type":  "Structured",
"query":  "query autonomous resources"
}'