Develop Functions

Before you deploy an API Gateway deployment, you'll need to develop and deploy your functions.

About Business Logic

The crux of the implementation is to put some code into a function. This code could be as complex as needed, calling multiple endpoints or perhaps performing some aggregation. The business logic function is the code that will be called when needed by Oracle Cloud Infrastructure API Gateway.

In this example architecture, the API Gateway calls an Oracle Function, which in turn queries some data from Oracle Fusion Applications Cloud Service via the REST API, manipulates it, and returns it to the user. When writing the function itself you can use any suitable framework you desire, but you need to be aware of the impact of a framework for serverless functions. In this example we have used Java as the language and the Apache HttpClient library to connect to the REST Service. The Apache library was chosen because it was easy to use and easy to implement; however in Java 11 we now have the new HTTP client which could also be used.

You should also avoid frameworks which instantiate a lot of in-memory objects when calling REST APIs, because these objects will be discarded on each call and therefore slow down the function’s execution.

About Getting Usernames and Roles

When a function is called by Oracle Cloud Infrastructure API Gateway, the gateway injects some metadata into the call using HTTP Headers. These headers are accessible by using the Functions SDK.

For example, in the following snippet of Java code (excerpted from the class JWTUtils in the example code provided with this solution playbook), we extract out the authentication token, decode it, and then return it as part of the body to the caller.

public OutputEvent handleRequest(InputEvent rawInput) {
        Optional<string> optionalToken=rawInput.getHeaders().get("Fn-Http-H-Authorization");
        if (!optionalToken.isPresent())
        {
            throw new Exception("No Authentication Bearer token found");
        }
        String jwtToken=optionalToken.get().substring(TOKEN_BEARER_PREFIX.length());
    String[] split_string = jwtToken.split("\\.");
        String base64EncodedHeader = split_string[0];
        String base64EncodedBody = split_string[1];
        String base64EncodedSignature = split_string[2];
        byte[] decodedJWT = Base64.getDecoder().decode(base64EncodedBody);
        try {
            String JSONbody = new String(decodedJWT, "utf-8");
            ObjectMapper mapper = new ObjectMapper();
            JsonNode root=mapper.readTree(JSONbody);
            username=root.get("sub").asText();
            System.out.println(“Username = “+username);
 
        } catch (Exception e)
        {
            Throw new Exception (e.getMessage());
        }
    Return OutputEvent.fromBytes(
            username.getBytes(), // Data
            OutputEvent.Status.Success,// Any numeric HTTP status code can be used here
            "text/plain");

The username can be used within the function to implement business logic as required. Roles are not available in the Authentication token, however they can be queried from Oracle Identity Cloud Service using REST APIs.

When Oracle API Gateway calls the function, a number of useful headers are also sent. These headers can be read using the function's API. Useful headers available from the Functions Developer Kit include:

For example, using a combination of Fn-Http-Method and Fn-Http-Request-Url you could implement a router within your code so that your function does different related things based on how it was called (such as PUT, PATCH, etc). This approach is often called the “Serverless Service” pattern and has the advantage that the developer has to maintain fewer functions and each function has a little router which determines what it needs to do.

public OutputEvent handleRequest(InputEvent rawInput, HTTPGatewayContext hctx) throws JsonProcessingException {
  String httpMethod = hctx.getMethod();
  String httpRequestURI = hctx.getRequestURL();
   // etc
}   

About Calling Oracle Fusion Applications Cloud Service Using a JWT Assertion Access Token

You need to implement the JWT Assertion from Oracle Functions by using the subject in the incoming Authorization header.

The sample Function provided with this solution playbook can perform this security process by using a helper library named idcsOAuthAsserter. The helper library performs the full OAuth Assertion flow by performing an exchange of bearer tokens before invoking Oracle Fusion Applications Cloud Service. This library is integrated with the sample Function.

The Function requires the private key and the public certificate in order to build the user and client assertions used to invoke Oracle Identity Cloud Service to create a new bearer access token using OAuth JWT Assertion.

The idcsOAuthAsserter library requires some properties that you can define in the Function Configuration. All of the variables in the following table are mandatory:

Config name Description Example
IDCS_URL Your Oracle Identity Cloud Service instance URL https://<your identity cloud hostname.identity.oraclecloud.com>
CLIENT_ID Your Oracle Identity Cloud Service Application Client ID associated with Oracle Functions and Oracle API Gateway 1a2b3c4d5e6f7g8h9i01j2k3l4m5o6p7
KEY_ID Alias of the certificates imported to the Trusted Oracle Identity Cloud Service Application fnassertionkey
SCOPE This scope should match with the target OAuth resource, which is the Oracle Identity Cloud Service Application associated with your Oracle Fusion Applications Cloud Service urn:opc:resource:fa:instanceid=xxxxxxxxxurn:opc:resource:consumer::all https://my_fusion_hostname:443/
AUDIENCE Audiences for the Assertion process. Separate multiple values with commas. myhostname.identity.oraclecloud.com, https://myfusionservice.dc1.oraclecloud.com
IDDOMAIN Name of the Oracle Fusion Applications Cloud Service Instance tenant mytenant

The function will also require configuration properties to access secrets for assertion related to idcsOAuthAsserter. The JWT Assertion requires a certificate and private key to generate the client and user assertions. The function retrieves the keystore with the OCID specified in V_KEYSTORE. The alias to retrieve that information should match with KEY_ID value in the configuration. The passphrase for both keystore and privatekey should be retrieved from the Oracle Cloud Infrastructure Vault Secrets Service using the OCIDs specified in V_KS_PASS and V_PK_PASS.

Config Name Description Example
V_KEYSTORE Secret that contains the secure stored content of the keystore ocid1.vaultsecret.oc1.dc1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
V_KS_PASS Secret that contains the secure stored password for the keystore ocid1.vaultsecret.oc1.dc1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
V_PK_PASS Secret that contains the secure stored password for the private key ocid1.vaultsecret.oc1.dc1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

An additional supported configuration value is the property USE_CACHE_TOKEN, which is set to True by default. This property allows you to store the generated Oracle Identity Cloud Service assertion token to be reused in future invocations of Oracle Fusion Applications Cloud Service, for as long as the token remains valid. The cached token is validated before use by comparing the Subject with the incoming token and verifying the expiry time to check if it is still valid. If not, a new access token is requested using the OAuth Assertion. This feature can be disabled by setting USE_CACHE_TOKEN to False.

The Function to be implemented can use SecurityHelper object from the idcsOAuthAsserter library to extract the subject from the access-token header, generate a new Bearer access token with OAuth JWT Assertion, and send a request to Oracle Fusion Applications Cloud Service using the new access token as Authorization header.

The Function saasopportunitiesfn in the source code sample is already integrated with the idcsOAuthAsserter library. The following code snippet is available in the handleRequest metdhod of the sample Function, which shows how to initialize the objects of idcsOAuthAsserter and perform an exhange of tokens using assertion with Oracle Identity Cloud Service:

// Full Oauth scenario Perform exchange of tokens
if(fullOAauth) {
    LOGGER.log(Level.INFO, "Full Oauth Assertion scenario - Perform exchange of tokens");
    SecurityHelper idcsSecurityHelper = new SecurityHelper(context)                   // Initialize SecurityHelper with RuntimeContext
                                        .setOciRegion(Region.US_PHOENIX_1)            // Specify the OCI region, used to retrieve Secrets.
                                        .extractSubFromJwtTokenHeader(rawInput);      // Extracts the subject from Token in Fn-Http-H-Authorization.
 
    // Get OAuth Access token with JWT Assertion using the principal extracted from Fn-Http-H-Access-Token Header
    jwttoken = idcsSecurityHelper.getAssertedAccessToken();
    LOGGER.log(Level.INFO, "Successfully token retrived with IDCS Assertion");
    LOGGER.log(Level.FINEST, "Access Token from assertion [" + jwttoken + "]");
}

Notice in this snippet that the object SecurityHelper is initialized with a context object of type RuntimeContext. This will be used to initialize the SecurityHelper with the configuration for idcsOAuthAsserter Helper Library.

The example saasopportunitiesfn Function is set to use the US_PHOENIX_1 region, so you should change the region in the method setOciRegion shown in the snippet to point to your region.

SecurityHelper also has the method extractSubFromJwtTokenHeader that takes the InputEvent object from the handleRequest Function method to extract the Bearer token that comes in the API Gateway Authorization header. Then you should be able to retrieve an Access Token as result of Oracle Identity Cloud Service Assertion.

For more information about the idcsOAuthAsserter usage and integration with a Function, review the README file for idcsOAuthAsserter in the code repository with the downloadable code sample accompanying this solution playbook.

Set Configuration Parameters

The Oracle Functions environment provides a very useful piece of functionality where you can define some parameters within the Oracle Cloud Infrastructure environment and then reference them from your code.

In this use case you will set the Fusion and an OverrideJWT token URLs, and a flag named full_oauth, as parameters which are used in your code. To add parameters:

  1. From the command line:

    fn config function cloudnativesaas saasopportunitiesfn fusionhost <value>

    fn config function cloudnativesaas saasopportunitiesfn overridejwt <value>

    fn config function cloudnativesaas saasopportunitiesfn full_oauth <value>

  2. From the console:
    1. Select Developer Services and select Functions.
    2. Under Applications, select your function.
    3. In the Resources section, select Configuration.
    4. Enter the key and value pairs for the following items and click the plus icon to add them as parameters.
      • the Fusion URL (key fusionhost)
      • OverrideJWT token URL (key overridejwt)
      • Use Full OAuth flag (key full_oauth)
  3. From the Function yaml file during deployment time:
    config:
      fusionhost: <value>
      overridejwt: <value>
      full_oauth: <value>

Within the code you can access these configuration variables using the Functions SDK by using a special Java annotation (@FnConfiguration) and accessing the parameters through the context variable:

private String jwtoverride = "";
    private String fusionHostname = "";
    private String fnURIBase ="/fnsaaspoc/opportunities";
    /**
     * @param ctx : Runtime context passed in by Fn
     */
    @FnConfiguration
    public void config(RuntimeContext ctx) {
        fusionHostname = ctx.getConfigurationByKey("fusionhost").orElse("NOTSET");
        jwtoverride = ctx.getConfigurationByKey("overridejwt").orElse("NOTSET");
        fullOAauth = Boolean.parseBoolean(ctx.getConfigurationByKey("full_oauth").orElse("false"));
        LOGGER.info("Configuration read : jwt=" + jwtoverride + " saasurl=" + fusionHostname);
    }

In addition, since this solution uses the idcsOAuthAsserter helper library, you need to provide in the Function Configuration the specific variables described in the previous section to use the Oracle Identity Cloud Service OAuth assertion to exchange access tokens. Because this process requires several mandatory configurations, we suggest that you set your Function configuration using the yaml file approach. For example:

config:
  AUDIENCE: <AUDIENCE_VALUES>
  CLIENT_ID: <YOUR_CLIENT_ID>
  IDCS_URL: <YOUR_IDCS_URL>
  IDDOMAIN: <YOUR_FA_TENANT_NAME>
  KEY_ID: <YOUR_IDCS_URL>
  SCOPE: <FA_RESOURCE_SCOPE>
  V_KEYSTORE: <YOUR_KS_OCID>
  V_KS_PASS: <YOUR_KSPASS_OCID>
  V_PK_PASS: <YOUR_PKPASS_OCID>
  fusionhost: <value>
  overridejwt: <value>
  full_oauth: <value>

Pass User Authentication Token to Fusion Applications

Your function needs to handle the user authentication token for secure REST API interactions.

When the function is called it will also have been sent an “authorization” header variable which would contain an authentication token. This token is generated by the calling application's identity server which is the same identity server that your Oracle Cloud Infrastructure API gateway function is using to validate the request.

The approach described in this solution playbook encorages you to use the Oracle Identity Cloud Service OAuth JWT Assertion to perform the exchange of tokens using the idcsOAuthAsserter helper library. This provides an additional security layer for the invocation from Oracle Functions to Oracle Fusion Applications Cloud Service. As shown in the architecture diagram, the Function with OAuth JWT Assertion queries for the token that comes in the inbound call from API Gateway header and uses it to be exhanged for another token during the Assertion process with Oracle Identity Cloud Service. This new token will be used in the outbound call to your destination server (Oracle Fusion Applications Cloud Service) and it (Oracle Fusion Applications Cloud Service) will execute the call as the user in Oracle Visual Builder.

In the provided sample function (saasopportunitiesfn), there is a configuration named full_oauth, which by default is set to True and the behavior will be the described above.

Optionally, you can set full_oauth to False. In this case, the Function queries the token that comes in the inbound call from API Gateway header and reuses it in the outbound call to Oracle Fusion Applications Cloud Service. No second token is generated.

The following code snippet uses the Apache HTTP library and calls the Oracle Fusion Applications Cloud Service REST API using the authentication token passed in.

String fusionURL=”<yourfusionresturl>”;
HttpClient client = HttpClients.custom().build();
HttpUriRequest request = RequestBuilder.get().setUri(fusionURL).
     setHeader(HttpHeaders.CONTENT_TYPE, "application/json").
     setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + jwtToken).
     build();
HttpResponse response = client.execute(request);
responseJson = EntityUtils.toString(response.getEntity());
status = response.getStatusLine().getStatusCode();

Compile and Deploy Your Function

In Oracle Functions, create an application for your function, and then deploy the function.

  1. Compile idcsOAuthAsserter code before your function to generate the required dependency in your local repo. For example:
    $ cd <PATH_TO>/idcsOAuthAsserter
    $ mvn clean install
  2. Ensure the lib directory exists in your function at the same level as yaml.func. For example, in this case the function name is saasopportunitiesfn. Compile your code using Maven commands in order to generate needed files, idcsOAuthAsserter-1.0.0.jar and idcsOAuthAsserter-1.0.0.pom, in the lib/ directory:
    $ cd <PATH_TO>/saasopportunitiesfn
    $ mkdir -pv lib/
    $ mvn clean install
     
    $ ls lib/
    idcsOAuthAsserter-1.0.0.jar  idcsOAuthAsserter-1.0.0.pom
  3. You need to use a Dockerfile to integrate the idcsOAuthAsserter library for your Function. The sample Function saasopportunitiesfn already has a Dockerfile in the following example. If you are behind a proxy, you may need to update the Dockerfile properties with the needed proxy properties by uncommenting the ENV MAVEN_OPTS line. Also, modify the proxy properties MAVEN_OPTS variable in the Dockerfile if needed.
    
    FROM fnproject/fn-java-fdk-build:jdk11-1.0.107 as build-stage
    WORKDIR /function
     
    COPY lib /function/lib
     
    # Uncomment this line and populate if you are behind a proxy
    # ENV MAVEN_OPTS -Dhttp.proxyHost=<ProxyHost> -Dhttp.proxyPort=<ProxyPort> -Dhttps.proxyHost=<ProxyHost> -Dhttps.proxyPort=<ProxyPort>
     
    ADD lib/idcsOAuthAsserter*.pom /function/pom.xml
    RUN ["mvn", "org.apache.maven.plugins:maven-install-plugin:2.5.2:install-file", "-Dfile=/function/lib/idcsOAuthAsserter-1.0.0.jar", "dependency:copy-dependencies", "-DincludeScope=runtime", "-DskipTests=false", "-Dmdep.prependGroupId=true", "-DoutputDirectory=target"]
     
    ADD pom.xml /function/pom.xml
    RUN ["mvn", "package", "dependency:copy-dependencies", "-DincludeScope=runtime", "-DskipTests=false", "-Dmdep.prependGroupId=true", "-DoutputDirectory=target"]
     
    ADD src /function/src
    RUN ["mvn", "package"]
    FROM fnproject/fn-java-fdk:jre11-1.0.107
    WORKDIR /function
    COPY --from=build-stage /function/target/*.jar /function/app/
     
    CMD ["<INSERT_FUNCTION_PACKAGE_AND_CLASSNAME>::handleRequest"]
  4. Create an application in your compartment in Oracle Functions:
    1. In a terminal window, create a new application by entering:
      $ fn create app <app-name> --annotation oracle.com/oci/subnetIds='["<subnet-ocid>"]'
      where:
      • <app-name> is the name of the new application. Avoid entering confidential information.
      • <subnet-ocid> is the OCID of the subnet (or subnets, up to a maximum of three) in which to run functions. If a regional subnet has been defined, best practice is to select that subnet to make failover across availability domains simpler to implement. If a regional subnet has not been defined and you need to meet high availability requirements, specify multiple subnets (enclose each OCID in double quotes separated by commas, in the format '["<subnet-ocid>","<subnet-ocid>"]'). Oracle recommends that the subnets are in the same region as the Docker registry that's specified in the Fn Project CLI context.
    2. Verify that the new application has been created by entering:
      $ fn list apps
  5. Deploy your function to your application. For example:
    $ cd <PATH_TO>/saasopportunitiesfn 
    $ fn deploy --app myapplication --verbose

Optionally Define an Authentication Function in Oracle Cloud Infrastructure

Oracle Cloud Infrastructure API gateway natively supports IDCS as an authentication provider. However, the gateway also allows the definition of a function that it can call. Optionally, you can create a custom authentication function using this feature.

The custom authentication function receives a call from the gateway, passing it the incoming authorization header. If the function returns true then the function call is allowed; conversely if it returns false then the request is rejected with a HTTP 401 error code. The function is provided in source code format and is deployed within Functions, which is then referenced in the OCI API GATEWAY deployment file via its OCID. You can discover the OCID by navigating to the deployed function in the console and expanding its OCID column. Before the function can be deployed you need to edit the file ResourceServerConfig.java; this defines how the function connects to Oracle Identity Cloud Service and which Oracle Identity Cloud Service OAuth application is used.

The function example below avoids hardcoding sensitive values such as the client secret. In the four lines beneath the comment // KMS Key for IDCS Client Secret, the code decrypts the OAuth secret using the Oracle Cloud Infrastructure key management feature, Oracle Cloud Infrastructure Vault. You enter these values in the Oracle Cloud Infrastructure console before deploying the function.

  1. Write a custom authentication function. For example:
    package com.example.fn.idcs_ocigw.utils;
    import com.fnproject.fn.api.RuntimeContext;
    import java.util.logging.Logger;
    
    /**
     * It contains the resource server configuration and constants
     * Like a properties file, but simpler
     */
    public class ResourceServerConfig {
    
        public  final String CLIENT_ID;
        public  final String CLIENT_SECRET;
        public  final String IDCS_URL ;
        public  final String SCOPE_ID;
    
        //INFORMATION ABOUT IDENTITY CLOUD SERVICES
        public  final String JWK_URL;
        public  final String TOKEN_URL;
        public final String KMS_ENDPOINT;
        public final String KMS_IDCS_SECRET_KEY;
    
        //PROXY
        public  final boolean HAS_PROXY ;
        public  final String PROXY_HOST;
        public  final int PROXY_PORT;
        public  final String DEBUG_LEVEL;
        private static final String NOT_SET_DEFAULT="NOTSET";
    
        /**
         * Gets defaults out of Oracle Functions Configuration
         */
        private  static final Logger LOGGER = Logger.getLogger("IDCS_GTW_LOGGER");
    
        public ResourceServerConfig(RuntimeContext ctx)   {
            // Get config variables from Functions Configuration
            HAS_PROXY = Boolean.parseBoolean(ctx.getConfigurationByKey("idcs_proxy").orElse("false"));
            PROXY_HOST = ctx.getConfigurationByKey("idcs_proxy_host").orElse("");
            PROXY_PORT = Integer.parseInt(ctx.getConfigurationByKey("idcs_proxy_port").orElse("80"));
    
            IDCS_URL = ctx.getConfigurationByKey("idcs_app_url").orElse(NOT_SET_DEFAULT);
            SCOPE_ID = ctx.getConfigurationByKey("idcs_app_scopeid").orElse(NOT_SET_DEFAULT);
            CLIENT_ID = ctx.getConfigurationByKey("idcs_app_clientid").orElse(NOT_SET_DEFAULT);
    
            DEBUG_LEVEL = ctx.getConfigurationByKey("debug_level").orElse("INFO");
            JWK_URL = IDCS_URL+"/admin/v1/SigningCert/jwk";
            TOKEN_URL=IDCS_URL+"/oauth2/v1/token";
    
            // KMS Key for IDCS Client Secret
            KMS_ENDPOINT = ctx.getConfigurationByKey("kms_endpoint").orElse(NOT_SET_DEFAULT);
            KMS_IDCS_SECRET_KEY= ctx.getConfigurationByKey("kms_idcs_secret_key").orElse(NOT_SET_DEFAULT);
    
            String decodedClientSecret="";
    
            // Decode the client Secret using KMS
            decodedClientSecret=DecryptKMS.decodeKMSString(KMS_ENDPOINT,KMS_IDCS_SECRET_KEY,ctx.getConfigurationByKey("idcs_app_secret").orElse(NOT_SET_DEFAULT));
            decodedClientSecret=decodedClientSecret.trim();
    
            CLIENT_SECRET = decodedClientSecret;
    
            LOGGER.info("IDCS Configuration Data read : IDCS_URL=[" + IDCS_URL + "] SCOPE_AUD=[" + SCOPE_ID +"] CLIENT_ID=["+CLIENT_ID+"], DEBUG_LEVEL=["+DEBUG_LEVEL+"]");
        }
    }
    You need to add some configuration values, such as the Oracle Identity Cloud Service endpoint and secret. These configuration parameters are entered in the Oracle Functions configuration.
  2. In the Oracle Cloud Infrastructure console, navigate to Developer Services, and click Functions.
  3. Select the function, and then under Resources, click Configuration.
    You can enter values for any of the configuration variables defined in the function. The sensitive values are secured by Oracle Cloud Infrastructure Vault.
  4. Deploy the function. For example:
    fn deploy -app fnsaaspoc