This blog picks up from where we left here.
In that blog we had discussed about an end-to-end usecase involving API Gateway, WAF and OCI Functions. We had showcased how we can implement security using a combination of WAF and API Gateway, with WAF handling the edge security usecases such as SQL injection protection, and the API Gateway handling authentication. In the API Gateway setup we had used a custom function that remotely introspected the JWT token in the Authorization header against an IDCS instance.
In this blog we will delve a little deeper into how we built this Authorization function using Node.js, and will discuss the following:
How to use the FDK (Fn Development Kit) to implement the Authorization function logic, interrogate the Fn input context and send back a chained asynchronous response.
How to manage parameters in Functions.
How to use Resource Principals and Signature Authentication to call into OCI platform APIs (in this case the Secrets service APIs).
How to manage vaults, secrets and extract secrets using OCI platform API calls.
Let first recap on the API Gateway setup and the overall view on the solution flow.
The client obtains a valid OAuth JWT Access Token from IDCS.
The client sends an API request to the API Gateway deployment, specifying the required route (eg /hello).
The API Gateway deployment authenticates the inbound request using the custom Authorization function.
The Authorization function uses the FDK object to interrogate the input and context that get passed to it from the Fn runtime.
The context is interrogated and the JWT token extracted.
The Authorization function fetches the IDCS credentials that it will need in order to be able to authenticate with IDCS before it can introspect the token. The Authorization function calls into the OCI secrets vault where the credentials are stored. The Authorization function will need to authenticate itself with OCI, and we'll explain below how it can do that using Signature Authentication.
The Authorization function then introspects the token remotely against the IDCS issuer.
Since these will be asynchronous calls we need to construct promises for each of these API calls and then chain the two promises together and return the chained result back to the FDK, as we’ll explain below.
The API Gateway routes the request to the appropriate route which calls the backend Fn function which implements our business logic and is deployed in the same tenancy. Note that In this case the API Gateway uses automatic Resource Principal-based Signature Authentication with the backend function. Also the JWT token intercepted by the API Gateway is forwarded to the backend function.
Below is the overall view of the structure of the Authorization function, and the handler function that we need to implement to pass back the chained promise result.
The inputs to the handler function are: input and ctx. The input contains the string input that is sent to the function. The ctx contains context information about the request such as ctx.config (environment configuration variables), ctx.headers (input headers), ctx.memory (amount of RAM in MB allocated to the function), etc.
We use two helper methods in our implementation: (introspect_token) and (get_secrets). Each of these methods returns a Promise. Obviously we need to fetch the secrets and pass the fetched IDCS credentials to the introspect token function. To do this, the two promises will need to be chained. To implement this chaining we use the (.then) handler approach. We chain the first promise to the second using the (.then) handler which passes the result from the first promise into the introspect-token function which returns the second promise. The chain, now setup, needs to be passed back to the FDK framework which in turn executes it, and returns the result as output: the output in this case determining whether the API Gateway trusts the inbound OAuth token or rejects it.
In our case we will need to manage quite a few static parameters in our code. For example the URLs of the secrets service endpoints, the username and other constant parameterised data. We can manage these either at Application or Function level (an OCI Function is packaged in an Application which can contain multiple Functions). In this case I will create function level parameters. You can use the following command to create the parameters:
fn config function test idcs-assert idcsClientId aedc15531bc8xxxxxxxxxxbd8a193
once you execute this command you’ll be able to see the parameters in the OCI console here:
The authentication function is IDCS-ASSERT. The parameters you see under ‘configuration’ are the configured parameters for this function. These parameters can be referenced in your code during runtime using the context that is passed on to the FDK handler that you use to implement the function logic in Node.js. The context ctx, which was passed into the handler function, has a config JSON object that you can use to reference these configured parameters. Like so:
Let paramterValue = ctx.config.<paramatername>;
In order for the Authorization function to be able to call into the OCI Secrets API, it will will need to to be able to authenticate itself. Now how do we do this authentication, and using what principal? As we mentioned before in my previous blog, we can use several types of principals in OCI (User, Instance and Resource). In this case we will use a Resource Principal for authentication since we're attempting to authenticate a resource of type function to access a vault of the OCI Secrets Service (which in turn is another type of resource). So basically our Authorization function, which is an OCI resource, is represented by this resource principal.
Using this Resource Principal we will sign the OCI API request. The Resource Principal comprises two artefacts: the private Key of a public private key pair, and a Resource Principal Session Token (RPST) signed using that private key. That private key and session token are injected/copied into the Fn docker container that's instantiated everytime a function resource call executes. Using Node, our Authorization function running in this docker container can access the key and token using the process.env.<VARIABLE_NAME> approach and use that information to do signature authentication. The diagram below illustrates the overall context of an typical Fn deployment during runtime.
At the time of working on this project (2 months ago) there was no Node.js SDK available for signature authentication for OCI APIs. So instead of using the now available SDK authentication providers to interrogate the injected key and token and sign our OCI api requests, we used a custom signature authenticator that implements the same signing logic. This will be a good chance for us to examine the internals of how signature authentication works, and capture a quick glimpse of how things work under the hood.
Now let’s see how we extract the RPST token and PEM private key which are essential ingredients required for signature authentication. As we mentioned they are copied into the instantiated Fn container during runtime, and global env variables are initialised that point to their locations. The variable names are: OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM and OCI_RESOURCE_PRINCIPAL_RPST. In our Node.js Authorization function we reference these variables as follows:
let pem = process.env.OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM;
let rpst = process.env.OCI_RESOURCE_PRINCIPAL_RPST;
The values of these values are OS file paths pointing to the locations of the respective artifacts. Once we get the variable values we then fetch the files from the corresponding locations and interrogate their content. The PEM key file will contain the private key value in PEM format and is the private key that we will use to sign our OCI requests. The RPST file will contain a string of this format XXX.YYY.ZZZ which is a JWT token string that encapsulates claims relating to the Fn resource. Let’s have a look at a the exploded/decoded RPST for our deployed Authroization Fn.
By looking at this RPST we see that it contains claims that fully describe our Fn resource. The sub claim is the OCID of the Authorization function. The res_tenant is the OCID of our tenancy. The ptype (principal type) claim is “resource”, indicating that this is a Resource Principal token, and you’ll notice that res_type is 'fnfunc', since our resource is a function. The jti claim is a replay prevention nonce. The jwk claim reference the JWKS set used to sign the token, including all the typical JWKS parameters like the 'n' key modulus, the 'kty' key type and 'kid' key id. The 'ttype' claim is res_sp, indicating it’s a resource principal session token, and notice the 'opc-dgs' claim. It references the OCID of the dynamic group which our function belongs to, and whose members we configured in our IAM policies to have read access privileges to the Secrets family resource type.
allow dynamic-group sphinx-functions to read secret-family in tenancy
Now let’s see how we can sign our OCI API request that we’re going to be sending to the Secrets Service endpoint to fetch our secrets. The OCI APIs expect the request to be signed as per RFC RC2617. Basically the client is responsible for signing the request and injecting an Authorization header carrying that signature. Now how is that request signed? A signing string (the composed string that will be signed) needs to be constructed out of the request and then signed. The signing string is composed by concatenating the headers and their values with spaces as delimiters, thus:
<header name> : <header value> <space> <header name> : <header value> ...
Now by default OCI APIs expect the following default headers irrespective of type of HTTP method: host, date, (request-type). The (request-type) header is a special header that can be generated by concatenating the lower-cased HTTP method, anASCII space and the URI path of the request. For POST and PUT methods, OCI APIs expect, in addition to the default headers, the following extra 3 headers: content-type, content-length, x-content-sha256. The content-type and content-length are the type and length of the HTTP body, and the x-conent-sha256 header is the SHA-256 hash of the TEXT input format of the HTTP body.
Once the signing string (the concatenated headers that are to be signed) is constructed, it is then signed using the private Key via the RSA-SHA256 algorithm. Now that the signing string has been signed we need to assemble the Signature Authorization header and then inject it into the request. The Authroization header has the following format:
Authorization: Signature keyId="<keyId>",algorithm="rsa-sha256",headers="<list of headers delimited by spaces>",signature="Base64(RSA-SHA256(<signing string>))"
The header is constructed using the Signature keyword followed by a series of parameters delimited by commas. You'll notice that the signature parameter has a value equal to the Base64 encoding of our signed signing string. And you'll also notice the KeyId parameter. It is a required string that the server (OCI API gateway) can use to look up the component/certificate/public that it needs to validate the signature. This opaque string is implementation specific and is out of scope of the RFC specification. The way that OCI implements this is that it prepends the XXX.YYY.ZZZ JWT string representing the Resource Principal Session Token (RPST) with 'ST$' and uses the concatenated string as a sort of fingerprint that maps to the public key that is used to validate/verify the Signature. Below is the actual Authorization header we're injecting in our OCI requests (with KeyId redacted):
Authorization: Signature version=\"1\",keyId=\"ST$xxx.yyy.zzz\",algorithm=\"rsa-sha256\",headers=\"host date (request-target)\",signature=\"XpNvvTB5SgMv2u3O/yHEeaLY9f9mIV0Q8SwSsOETAHfLyc1Xv6w/sbIyK+kgBKHp368MiHV2fU+8hDByHV7l8DuPDy3HxLVOKzedVe2/iMkZCr6sCcng/nrPJv3GX6tPEPLh49Y1U/GBrkjQpfkqnK0wbiDIrHV5a6HnOk2V4ztVcCFOpjXt0EnBprFU1KvrbARjJg9syrCw75Lrt2jlaq+hdAt35rBOK1z6zRS1X0d+UVYSlymqLo4tjmrIHjWihFd8L0Z1xgtkyo+LIjZwf6fMZWOJNOo4MMaDnwVnIZq7ncq3E5kYRU0Wlv6M5Kavf7GiK5M6OKD0d/zTblcymg==\"
Note the list of headers. In this case we only see the headers required for GET requests, since as we'll see below we will only need GET requests when fetching secrets. You'll also notice that there is an extra OCI specific parameter version which stipulates the signature scheme used. OCI currently uses only one default signature authentication scheme.
Below you'll find the SignatureAuthenticator Node.js class that implements this OCI signature authentication algorithm. It is reusable and self explanatory. We use 2 Node.js modules in this class: the http-signature module and the jssha. The http-signature class has a sign method which takes as input a request, the keyId, the private key, and a list of headers to be signed. Noteworthy to mention that lines 26-40 relate to the case when the request is either a POST or a PUT request, and hence we need to sign the body and calculate and inject the x-content-sha256 and content-length headers, which, along with all the other headers in the headers input array, are composed before overall request signature is calculated and injected in the returned request.
We need to setup the vault, create the secret and populate it’s value, before we can call the Secrets APIs to fetch the secret bundle. Once the vault and secret bundles are setup and configured we will show you how, using the OCI CLI raw requests, to send OCI APIs to fetch a secret. Finally we'll show you how to call into these same APIs from within our Authorization function using the Resource Principal Signature Authentication we just discussed. So let's proceed.
I’ll defer here to my colleague Tim Melander’s post that goes in detail about how to setup Vaults and create secrets etc. End result will be something like this:
One important thing to notice: that the secrets vault is in a different region (London, UK) from the IDCS-ASSERT function which is (Ashburn, US). That's quite a distance! But it's no problem!! As long as the function and secrets vault belong to the same tenancy (which can span multiple regions) then we can Resource Principal signature authentication to call from the function into the secrets endpoint.
Below is how a Secret configured in the OCI console looks like:
Here you can see the base64 encoded value of the Client Secret that we will use to connect to IDCS when we're introspecting our token. The secret has multiple versions. Only one version can be current at any single point in time. Let's see how we can fetch this secret using the OCI APIs.
Secrets service has the concept of a Secrets Bundle. A Secret Bundle consists of the secret contents, properties of the secret and secret version (such as version number or rotation state), and user-provided contextual metadata for the secret.
Let’s construct the endpoint we'll use to fetch the Secret Bundle that contains our secret. We will need to query the OCI /secretbundles REST resource. Let's test this API by calling it using the installed OCI CLI that’s on my machine. Using a raw request, the CLI authenticates using a User Principal Provider, and my user belongs to a group that has access to the secret.. So let’s see. Below is the what the raw request will look like:
oci raw-request --http-method GET --target-uri https://secrets.vaults.uk-london-1.oci.oraclecloud.com/20190301/secretbundles/<ocid1.vaultsecret.oc1.uk-london-1.ocid-id-of-secret>?compartmentId=<ocid1.compartment.oc1.ocid-of-compartment>
Notice that I have to specify the URI parameter which is the OCID of the secret; and a query parameter which is the id of the compartment in which my vault has been configured. Also note that the region id has to be specified in the URL. In my case the region id is uk-london-1. So you can see here that as long as we’re within the same tenancy we can call into region-based services using Resource Principal authentication from anywhere within that tenancy. My function is in Ashburn, US and my secrets are in London, UK.. Neat!
The response we get is like this:
This response is the Secret Bundle which always contains the Base64 encoded value of the CURRENT version of the secret. NB. A secret can have multiple versions (as in values basically). Only one value is designated as current.
So now we have all the elements in place. We know how sign an OCI API request using a Resource Principal from a Function resource. We know which OCI secrets APIs to call. We’re all set. All we need to do now is fire up our SignatureAuthenticator class and sign our request before sending it out from our Authorization function. See code snippet below from the (get_secret_async) helper method in our Authorization function. You'll notice it fetches the PEM key file location in line 3, constructs the RPST key id in line 46, instantiates an instance of the SignatureAuthenticator class (line 47), and then finally calls the sign method passing in the HTTP request as an input parameter. The request gets signed, the Authorization signature header gets injected and then the request gets sent (in line 50). And that's it!
In this blog we’ve shown you how to build a Node.js OCI API Gateway Authorization function that uses chained asynchronous calls to fetch an IDCS credential secret, and use fetched secret to call into IDCS and introspect an JWT token. We’ve also showcased how to do Resource Principal authentication (from within a Node.js Function) when calling into an OCI resource API (like the Secrets Vault discussed in this blog). We've also built a SignatureAuthenticator class in Node.js to help you do the Signature Authentication in your Node.js projects (although there is now a Typescript OCI SDK that's just been released, and which we encourage you to use in your future Node.js projects).