Problem Overview
When calling REST services from a browser based application the handling authentication can be challenging. The web application, which runs in the browser, shouldn't store a user's password or private key which are often required to authenticate REST service calls. Anything in the browser can be inspected and these "secrets" are not safe on the client side. The solution is to store these secrets on the server side where the access can be restricted and they can be stored securely. We leverage the server to either act as a proxy and make the REST calls using provided secrets directly, or return to the client a short lived bearer token which represents the authenticated user and which browser based code can use to call the REST service.
That is exactly what Service Connections and the REST Service Proxy in Visual Builder do. Out of the box, Visual Builder supports a variety of authentication types: Basic Authentication, OAuth User Assertion, Oracle Cloud Account, Oracle Cloud Infrastructure API Signature, etc.
In this post we will look into how to handle cases where the REST service does not support one of above standards, and a custom authentication is required instead. The solution will leverage Oracle Cloud Infrastructure Functions. Specifically we'll show an example of getting a short lived authentication token and passing it with your REST calls.
The prerequisites for implementing the solution below are:
- standalone Visual Builder instance
- Oracle Cloud Infrastructure account
Implementing Google Auth support in Visual Builder application using OCI Functions
Let's use Google Auth as an example. In Push Notifications in Visual Builder PWA application blog the Google FireBase was used as an example of service delivering push notifications. The example was built using Legacy API which expires in Feb 2024. The replacement API requires an authentication token generated using Google Auth library and service account private key. That is, in order to send message using API REST call
https://fcm.googleapis.com/v1/projects/<your-fb-project>/messages:send
the REST call needs to be accompanied with authorization request header:
Authorization: Bearer <your-token>
where the bearer token is generated using a private key and Google Auth library call.
While this is a complex problem to solve, it is fairly easy to put it together using features provided by OCI platform. We create an OCI function, which is serveless "function as a service" compute service – no server management, just write a code ("function") and execute it on demand by calling an URL endpoint. The function will call Google Auth library and it will store google service private key in OCI Vault to keep it safe. The function can be written in any language which OCI supports – Java, Python, Ruby, Go, NodeJS, C#. Google documentation provides examples on how to generate Bearer token in NodeJS, Python or Java, which means all three language versions can be used. When the function is implemented we will call it from the Visual Builder app using Oracle Cloud Infrastructure API Signature authentication type and then pass returned token into Push Notification API REST call.
Create OCI Function
Three things are needed before a function can be created. Open your OCI account and:
- open Containers & Artifacts -> Container Registry and in the root compartment create a new private docker registry, named for example "custom-auth-for-vb"
- open Identity -> Compartments and create a new compartment in which the function will be created
- open Networking -> Virtual Cloud Networks and in your newly created compartment create a new VCN using "Start VCN Wizard" and VCN with Internet Connectivity option (with default options, but uncheck the "Use the DNS Hostnames" as it is not needed)
Functions live within an application. Open Functions -> Application, change to the new compartment we created and create a new application (named for example "google-auth-support-app") in the newly created VCN and using the public subnet, and open the created app. Getting Started is the first resource in a new application which was just created. It provides a summary on how to create, deploy and invoke functions. At first this may look overwhelming, but once you go through these steps it will become clear and obvious how this works. Perform the first 7 steps – these are all one-off setup. Keep the Cloud Shell open and open also Code Editor – both of these are accessible through the "Developer Tools" menu bar at the top right corner of your OCI page (close to your Profile icon).
We will create a NodeJS version of the Google Auth function. This is an arbitrary choice and Python or Java can be used instead. In Cloud Shell execute:
fn init --runtime node fcm-auth-js cd fcm-auth-js
Above commands created skeleton of OCI function in NodeJS called "fcm-auth-js" which we can now edit in Code Editor:
The screenshot already shows the final function code and all files required to implement an OCI NodeJS function. The code is based on this Google example but instead of using service-account.json file (which can be generated here and downloaded) it uses only two required properties from this file: private_key and client_email. client_email value is not sensitive and can be used in the code as is, but private_key value must be stored in the OCI Vault. To do that open Identity & Security -> Vault and switch to the compartment we created earlier, and create a new Vault using your name of preference. Open new vault and use Create Key to generate a new encryption key. When that's done we can switch to Secrets under Resources and using Create Secret we can persist private key value from service-account.json – give it name, select plain-text option and your master encryption key, and paste the key, that is "—–BEGIN PRIVATE[…]\n—–END PRIVATE KEY—–\n", into Secret Contents. Press Create Secret and copy OICD value (which represents this private key) and paste it into the function code:
const fdk = require('@fnproject/fdk');
const https = require('https');
const { google } = require('googleapis');
const common = require("oci-common");
const secrets = require("oci-secrets");
const MESSAGING_SCOPE = 'https://www.googleapis.com/auth/firebase.messaging';
const SCOPES = [MESSAGING_SCOPE];
async function getSecret(secret_ocid) {
const provider = common.ResourcePrincipalAuthenticationDetailsProvider.builder();
const client = new secrets.SecretsClient({authenticationDetailsProvider: provider});
const secret_content = await client.getSecretBundle({ secretId: secret_ocid});
return Buffer.from(secret_content.secretBundle.secretBundleContent.content, 'base64').toString()
}
async function getAccessToken() {
const secret = await getSecret("ocid1.vaultsecret.oc1.iad.<your-id>");
// restore new line characters in the private key:
const private_key = secret.replace(/\\n/g, "\n");
const client_email = "firebase-adminsdk-xxxxx@<your-project>.iam.gserviceaccount.com";
const jwtClient = new google.auth.JWT(
client_email,
null,
private_key,
SCOPES,
null
);
const res = await jwtClient.authorize();
return res.access_token;
}
fdk.handle(async function (input, ctx) {
const token = await getAccessToken();
return { 'token': token }
})
After "secret" and "client_email" references are updated in above code, the last two steps are to update package.json file with new dependencies:
{
"name": "fcm-auth-js",
"version": "1.0.0",
"description": "google auth function",
"main": "func.js",
"author": "",
"license": "Apache-2.0",
"dependencies": {
"oci-common": "^2.61.0",
"oci-secrets": "^2.61.0",
"google-auth-library": "^8.8.0",
"googleapis": "^118.0.0",
"@fnproject/fdk": ">=0.0.55"
}
}
and add line
memory: 512
to func.yaml to avoid running out of memory during function execution.
The function is complete now and before we can test it we need to grant it permissions to read values from the vault. To do that we first go to Identity -> Dynamic Group and create a group which will represent our function. Create new dynamic rule of name of your choice and give it this rule:
ALL {resource.type = 'fnfunc', resource.compartment.id = 'ocid1.compartment.oc1..<your-compartment-id>'}
This group will now represent all functions in this compartment. If that is too broad, make the rule more narrow and enumerate just the new function we are creating in this blog.
To grant access to the vault, switch to Identity -> Policies and in your compartment create new policy:
Allow dynamic-group <your-dynamic-group-name> to use secret-family in compartment <your-compartment-name-or-ocid>
With this in place the function can be deployed and executed. Switch to the Cloud Shell and in the fcm-auth-js directory execute:
fn -v deploy --app google-auth-support-app fn invoke google-auth-support-app fcm-auth-js
where "google-auth-support-app" corresponds to your application name we created earlier. The second command executes the function and result should be:
{"token": "ya29.c.b0A......."}
This token can be taken and tested from a REST service client like Postman to verify that calling Push Notification API REST call works and the token is valid. If function execution failed see last chapter of this blog for debugging tips.
Call OCI Function
Each OCI function is exposed via URL endpoint. And calling POST on such endpoint will execute the function and response will be function's return value. But in order to call the function the request needs to be signed with OCI user's private key. Which can be easily achieved by setting up service connection in VB and using authentication type "Oracle Cloud Infrastructure API Signature". Two things needs to be done in the OCI account before we move to VB:
- while still at the functions page, copy the "Invoke Endpoint" value representing our fcm-auth-js function. It will have form of https://<your-oci-instance>.oraclecloud.com/<id>/functions/ocid1.fnfunc.oc1.iad.<ocid>/actions/invoke
- in the Profile menu (top right corner) click on your email (a shortcut to Identity -> Users -> User Detail) and then API Keys under Resources. Select Add API Key (assuming you do not have one yet otherwise you can skip this step and use your existing one) and using the "Generate API Key Pair" option click on Download Private Key, and on Download Public Key, followed by Add button. From the Configuration File Preview copy following values: tenancy, user and fingerprint. And turn these values into single string in form: <tenancy>/<user>/<fingerprint> – that will be user identification we will need in the VB
Now we can register the function into VB. Switch to Services tab in VB and click + to create a new service connection. We will use the Define By Endpoint option, change Method to POST, paste the OCI Invoke Endpoint value and change action hint to Get One. In the next step change Service Name to getToken, and select Server tab and change Authentication Type to Oracle Cloud Infrastructure API Signature 1.0, and click the pencil icon next to Key ID. In this dialog we paste the user identification we created earlier into the Key Id field, and content from generate private key file we downloaded a moment ago will go into the Private Key field. Save, switch to Test, in Request->Body enter {}, and press Send Request.
Response should be OK 200, and body will be a fresh token.
Create the service connection and we are done.
Updating Push Notification application
We started with reference to Push Notifications in Visual Builder PWA application blog which is using legacy API. If the above OCI Function was registered in that application, all that remains to be done is to switch to a new REST API call. Open JavaScript section of main-events page and replace notifyClientAboutNewEvent function with this new code:
async notifyClientAboutNewEvent(event, clients, thisClientId, thisUserEmail, userPreferences) {
const getToken = await Rest.get("getToken/invoke").body({}).fetch();
if (getToken.response.status !== 200) {
// report error
}
clients.forEach(cl => {
if (cl.email === thisUserEmail && cl.client === thisClientId) {
// do not notify user creating this event on this device;
// this user will still be notified on other devices they are using
// which is handy for testing but may not be desirable in real app - simply remove
// the client part from the condition to NOT notify this user on any device
return;
}
if (!this.userInterestedInEvent(thisUserEmail, userPreferences, event.type1)) {
return;
}
const data = {
message: {
token: cl.token,
data: {
eventId: ""+event.id,
eventName: event.name
}
}
};
fetch('https://fcm.googleapis.com/v1/projects/<your-firebase-project>/messages:send', {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer "+getToken.body.token
},
body: JSON.stringify(data)
});
});
}
Things to note are:
- OCI function call to fetch new token
- changes to data payload passed into the new REST call API
- new REST call API URL with your project name
- token passed in Authorization header
Appendix
Performance
OCI Functions when not used will get offloaded and their first start, so called cold start, will be significantly slower. 20-60 seconds. To mitigate this in production time, open your function via Function ->
Applications -> google-auth-support-app -> fcm-auth-js and click Edit and tick "Enable provisioned concurrency". Be aware that such change will incur ongoing charges as OCI keeps the function initialized and running.
Troubleshooting function failures
Troubleshooting failures in a function can be challenging and tediously slow at the same time. Any code change needs to be deployed and executed and there is delay before failure shows in logs. If possible, write function code locally and run it locally too to resolve both compilation errors and coding errors. If your OCI command line support is installed properly it is possible to create local provider from your local private key and used it instead of provider returned from ResourcePrincipalAuthenticationDetailsProvider, ie.:
const provider = new common.ConfigFileAuthenticationDetailsProvider( "./oci.config", "my-dev-profile" );
That way both getSecret and getAccessToken functions can be developed, tested and fine-tuned locally.
To diagnose why function is failing you need to first enable logging. In Functions -> Application -> your-application-name select Logs under Resources and Enable Log. After it becomes active you can click on the Log Name to see its content. There is often slight delay (up to few minutes) between function failure and function execution logs appearance, so keep refreshing the page. Some high level logs (eg function execution failed) show up earlier and it can take more time for more detailed logs to appear (eg exception stack trace). Hence the tediousness mentioned earlier.
It is also helpful to add logging into your function to get a better understanding of what passed and which values were correct or not in your function. Logging can be via:
console.log("some message")
After any code change, the Code Editor automatically saves changes, but to see the changes live the function has to be deployed and invoked using the above two commands.
This page has other troubleshooting tips and explains common errors. For example facing (common) error:
504: Container initialization timed out, please ensure you are using the latest fdk and check the logs FunctionInvokeContainerInitTimeout
is documented to mean not enough memory and provides steps to mend it.
Other Considerations
The function returns a Google Auth short lived bearer token. But if the only reason to fetch the token is to call Push Notification REST call with it, then even better solution would be to call the Push Notification REST call directly from the OCI function and never exposing the bearer token to the client.
The code shared does not have any error handling in place. Add it accordingly.
