diff --git a/functionsbasicauth/.classpath b/functionsbasicauth/.classpath new file mode 100644 index 0000000..e40ff91 --- /dev/null +++ b/functionsbasicauth/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/functionsbasicauth/.factorypath b/functionsbasicauth/.factorypath new file mode 100644 index 0000000..35743b5 --- /dev/null +++ b/functionsbasicauth/.factorypath @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/functionsbasicauth/.gitignore b/functionsbasicauth/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/functionsbasicauth/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/functionsbasicauth/.project b/functionsbasicauth/.project new file mode 100644 index 0000000..b314fc4 --- /dev/null +++ b/functionsbasicauth/.project @@ -0,0 +1,23 @@ + + + functionsbasicauth + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/functionsbasicauth/.settings/org.eclipse.core.resources.prefs b/functionsbasicauth/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..29abf99 --- /dev/null +++ b/functionsbasicauth/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding//src/test/resources=UTF-8 +encoding/=UTF-8 diff --git a/functionsbasicauth/.settings/org.eclipse.jdt.apt.core.prefs b/functionsbasicauth/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/functionsbasicauth/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/functionsbasicauth/.settings/org.eclipse.jdt.core.prefs b/functionsbasicauth/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..87f474b --- /dev/null +++ b/functionsbasicauth/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/functionsbasicauth/.settings/org.eclipse.m2e.core.prefs b/functionsbasicauth/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/functionsbasicauth/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/functionsbasicauth/README.md b/functionsbasicauth/README.md new file mode 100644 index 0000000..4d8d05a --- /dev/null +++ b/functionsbasicauth/README.md @@ -0,0 +1,93 @@ +# Basic Authentication function for API Gateway + +This is an example of how to use a function to authenticate an API Gateway Basic auth header. + +## Prequisites + +To compile and install this function you will need to have followed the relevant [instructions to setup the functions environment](https://docs.oracle.com/en-us/iaas/Content/Functions/home.htm) (chose if it's going to be cloud shell, local development etc. Personally I think the cloud shell is the easiest to setup) + +## Create the application and it's networking + +Identify the VCN and subnet to put your function, I'd suggest using the see the information in the [Creating application](https://docs.oracle.com/en-us/iaas/Content/Functions/Tasks/functionscreatingapps.htm#top) Then use those instructions to create a functions application (this holds the functions) If you already have an existing setup or API GW you can use the private subnet behind the API GW. + +I would also recommend that you create a log group (The "logs" section on the left side of the applications page) for the log information +Once the application is created you will need to update the NSG (if using them) to allow the API GW to talk to the function. Do this by going to the application page and wheere the NSG's are listed add the NSG being used by the API GET (look at the API GW page if needed to figure this out) + +## Create the function and upload it + +Assuming you've done the quick start instructions you can now upload your function. See the [Creating your function](https://docs.oracle.com/en-us/iaas/Content/Functions/Tasks/functionsuploading.htm) instructions, but as you have the functions code just make sure you are in this projects root directory and run compile and upload the function Remember to use the app name you just created. + +``` +fn -v deploy --app basic +``` + +## Setup the function configuration + +Use the function configuration to define the way the function will behave. This function supports the following configuration options : + + - `auth-source` Optional. The location of the actual values to check, if set to `vault` the code will assume the value in `auth-secret` is the OCID to be used to load the secret from the vault, any other value and it will try to load the contents from the `auth-secret` config property. The default is `config` (So loading from the function config) + + - `auth-secret` Required, must contain either the OCID of the secrets to use (if `auth-source` is set to `vault`) OR value to check (for any onther value of the `auth-source` config) There is no default. The value can be in plain text or base 64 encoded (see the `auth-source-format` config setting) + + - `auth-source-format` Indicates the format of the value to check, if set to `base64` then the retrieved value (from the config or the vault secret) will be decoded form base64 before being used for compartisson checks, if it's `plaintext` then it will be used cdirectly. The default is `plaintext` + + - `result-cache-seconds` Indicates how long to ask the API GW to cache the results for, if not a valid number or provided does not specify a valid cache time on the returned data and will use the API GFW default (60 seconds at the time of writing) If below 60 or above 3600 will be wrapped to fit within that range as per the API GW max / min valid responses. Defaults ot no value provided and thus to the API GW default. + + - `testing-mode` **MUST NOT BE SET ON PRODUCTION SYSTEMS.** If set to true will include the values for auth-secret and any provided values in the log data, this is of course a very bad thing for anything other than testing the function code and logic, and **MUST NOT BE SET ON PRODUCTION SYSTEMS.** Defaults to false. + +## Vault setup + +If you are storing the auth details in a vault (`auth-source`is set to `vault` above) then create a vault (if needed) then a master signing key (again if needed) then a secret containing either the plaintext auth details OR the base64 encoded version (if using the base 64 encoded feature in the create secret make sure you set the secret type to plaintext, if adding using the plain text feature AND you are pasting the base64 encoded version then you will need to ensure that the auth type is set to base 64) + +Get the OCID of the secret and put it into the `auth-secret` in the config and then set the `auth-source` to vault. + + +## Functions dynamic group and policies to access vault secrets + +To access the secrets in the vault (if you chose to store the password in the vault) Call the dynamic group `ExperientialFunctions` you will need to create a dynamic group to identify the function. The simplest version of this is + +``` +All {resource.type = 'fnfunc'} +``` + +This will match all functions, but you may want to restrict this further, for example adding a restriction for a specific compartment + +``` +All {resource.type = 'fnfunc', resource.compartment.id='compartment ocid'} +``` +Would only match functions in the compartment with the specified OCID. + +Once you have a dynamic group to identify the function then you need a policy that will let that group access the vault secret. This is a basic policy rule + +``` +allow dynamic-group ExperientialFunctions to manage secret-family in compartment experiential +``` + +Note that if the dynamic group was created using an identity someon (e.g. ) then the domain name needs ro be added to the DG name, for example + +``` +allow dynamic-group OracleIdentityCloudService/ExperientialFunctions to manage secret-family in compartment experiential +``` + +However you may want to restrict this further. + + +## Configure a policy for APIGW to call functions + +The API Gateway needs to be able to call functions. create a policy rule to allow API Gateways in the specified compartment to access functions. + +``` +ALLOW any-user to use functions-family in compartment experiential where ALL {request.principal.type= 'ApiGateway', request.resource.compartment.id = 'ocid1.compartment.oc1......ocid'} +``` + +## Configuring the API Gateway to authenticate using the function + +Follow the steps in the API Gateway [quick start documentation](https://docs.oracle.com/en-us/iaas/Content/APIGateway/Tasks/apigatewayquickstartsetupcreatedeploy.htm) to setup the appropriate networks and gateways + +Create a deployment and [add the function you created as single authentication multi argument authorizer function](https://docs.oracle.com/en-us/iaas/Content/APIGateway/Tasks/apigatewayusingauthorizerfunction.htm) Note that you don't need to create the function itself, that's what this code does. You just need to configure the gateway to use it. + +For the authorizer function setup the following function argument mapping + +`request.headers.authorization` -> `authorization` + +The argument name must be lower case \ No newline at end of file diff --git a/functionsbasicauth/body.txt b/functionsbasicauth/body.txt new file mode 100644 index 0000000..a73c04d --- /dev/null +++ b/functionsbasicauth/body.txt @@ -0,0 +1 @@ +Life, the universe and everything \ No newline at end of file diff --git a/functionsbasicauth/func.yaml b/functionsbasicauth/func.yaml new file mode 100644 index 0000000..f4add39 --- /dev/null +++ b/functionsbasicauth/func.yaml @@ -0,0 +1,7 @@ +schema_version: 20180708 +name: verify-basic-auth +version: 0.0.1 +runtime: java +build_image: fnproject/fn-java-fdk-build:jdk17-1.0.193 +run_image: fnproject/fn-java-fdk:jre17-1.0.193 +cmd: com.oracle.timg.demos.functions.basicauth.BasicAuthFunction::handleAPIGWAuthenticationRequest diff --git a/functionsbasicauth/oictest.txt b/functionsbasicauth/oictest.txt new file mode 100644 index 0000000..a88f28a --- /dev/null +++ b/functionsbasicauth/oictest.txt @@ -0,0 +1,18 @@ +{ + "customerCode": "808080", + "customerName": "LOGIPE FINTECH PRIVATE LIMITED - DEMO through APIGW", + "productCode": "UPICOLL", + "productDescription": "Regular", + "poolingAccountNumber": "10089295611", + "transactionType": "Collections", + "dataKey": "751 10204 28-01-2023L00147700340000000001", + "batchAmt": "100.00", + "batchAmtCcd": "INR", + "vaNumber": "8080809837148839", + "utrNo": "302876436829", + "creditGenerationTime": "28-JAN-23 04.59.33.488870 AM", + "remitterName": "BALVANTSINGHSOMAKKHANSINGH", + "remitterAccountNumber": "15900100013792", + "remittingBankName": "BANK OF BARODA", + "ifscCode": "BARB0JASPUR" +} \ No newline at end of file diff --git a/functionsbasicauth/pom.xml b/functionsbasicauth/pom.xml new file mode 100644 index 0000000..fbd942c --- /dev/null +++ b/functionsbasicauth/pom.xml @@ -0,0 +1,150 @@ + + + 4.0.0 + com.oracle.timg.demo + functionsbasicauth + 0.0.1 + BasicAuthImplementation + Imlements a basic authentication pulling from the OCI Vault + + UTF-8 + 1.5.0 + 1.0.193 + 1.18.30 + 3.17.0 + 1.7.33 + 1.15 + 4.13.1 + + + + + + com.oracle.oci.sdk + oci-java-sdk-bom + ${version.ocisdk} + pom + import + + + + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + junit + junit + ${version.junit} + test + + + + commons-codec + commons-codec + ${version.apache-commons} + + + + commons-cli + commons-cli + ${version.cliparser} + + + com.oracle.oci.sdk + oci-java-sdk-core + + + com.oracle.oci.sdk + oci-java-sdk-secrets + + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + + + org.slf4j + slf4j-jdk14 + ${version.slf4j} + + + org.projectlombok + lombok + ${version.lombok} + provided + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + false + + + + + \ No newline at end of file diff --git a/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/AuthRequest.java b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/AuthRequest.java new file mode 100644 index 0000000..4b633b5 --- /dev/null +++ b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/AuthRequest.java @@ -0,0 +1,49 @@ +/*Copyright (c) 2024 Oracle and/or its affiliates. + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any +person obtaining a copy of this software, associated documentation and/or data +(collectively the "Software"), free of charge and under any and all copyright +rights in the Software, and any and all patent rights owned or freely +licensable by each licensor hereunder covering either (i) the unmodified +Software as contributed to or provided by such licensor, or (ii) the Larger +Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +one is included with the Software (each a "Larger Work" to which the Software +is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, +use, sell, offer for sale, import, export, have made, and have sold the +Software and the Larger Work(s), and to sublicense the foregoing rights on +either these or other terms. + +This license is subject to the following condition: +The above copyright notice and either this complete permission notice or at +a minimum a reference to the UPL must be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package com.oracle.timg.demos.functions.basicauth; + +import java.util.HashMap; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class AuthRequest { + private String type; + private HashMap data = new HashMap<>(); +} diff --git a/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/AuthResponse.java b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/AuthResponse.java new file mode 100644 index 0000000..18315bc --- /dev/null +++ b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/AuthResponse.java @@ -0,0 +1,55 @@ +/*Copyright (c) 2024 Oracle and/or its affiliates. + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any +person obtaining a copy of this software, associated documentation and/or data +(collectively the "Software"), free of charge and under any and all copyright +rights in the Software, and any and all patent rights owned or freely +licensable by each licensor hereunder covering either (i) the unmodified +Software as contributed to or provided by such licensor, or (ii) the Larger +Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +one is included with the Software (each a "Larger Work" to which the Software +is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, +use, sell, offer for sale, import, export, have made, and have sold the +Software and the Larger Work(s), and to sublicense the foregoing rights on +either these or other terms. + +This license is subject to the following condition: +The above copyright notice and either this complete permission notice or at +a minimum a reference to the UPL must be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package com.oracle.timg.demos.functions.basicauth; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class AuthResponse { + + private Boolean active = false; + private List scope = new LinkedList<>(); + private String expiresAt = ""; + private Map context = new HashMap<>(); +} diff --git a/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/BasicAuthFunction.java b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/BasicAuthFunction.java new file mode 100644 index 0000000..5e7c2a5 --- /dev/null +++ b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/BasicAuthFunction.java @@ -0,0 +1,362 @@ +/*Copyright (c) 2024 Oracle and/or its affiliates. + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any +person obtaining a copy of this software, associated documentation and/or data +(collectively the "Software"), free of charge and under any and all copyright +rights in the Software, and any and all patent rights owned or freely +licensable by each licensor hereunder covering either (i) the unmodified +Software as contributed to or provided by such licensor, or (ii) the Larger +Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +one is included with the Software (each a "Larger Work" to which the Software +is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, +use, sell, offer for sale, import, export, have made, and have sold the +Software and the Larger Work(s), and to sublicense the foregoing rights on +either these or other terms. + +This license is subject to the following condition: +The above copyright notice and either this complete permission notice or at +a minimum a reference to the UPL must be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package com.oracle.timg.demos.functions.basicauth; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; +import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider; +import com.oracle.bmc.secrets.SecretsClient; +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails; +import com.oracle.bmc.secrets.model.SecretBundle; +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BasicAuthFunction { + public static final Integer HTTP_RESPONSE_OK = 200; + public static final Integer HTTP_RESPONSE_UNAUTHORIZED = 401; + + // set this to true if you are using it for testing, note that this MUST NOT be + // used in production as it may output base64 and clear text versions of the + // saved and requested passwords which would end up in the logs + public final static String CONFIG_TESTING_MODE = "testing-mode"; + public final static String DEFAULT_CONFIG_TESTING_MODE = "false"; + + // The names below refer to the variable names IN THIS CODE, to look at the + // actual + // config names used please refer to the variables below. + // CONFIG_AUTH_SOURCE must be set to the value if one of the constants + // AUTH_SOURCE_CONFIG or + // AUTH_SOURCE_VAULT (case ignored) + // if set to the value of AUTH_SOURCE_CONFIG the text in the config + // CONFIG_AUTH_SOURCE is used + // if set to the value of AUTH_SOURCE_VAULT then the config in + // CONFIG_AUTH_SOURCE should contain the OCID of the + // vault secret to retrieve + // default are to store the auth data in the config + // if not set or not a recognized option then AUTH_SOURCE_CONFIG is used + // the auth info (either directly in the config or retrieved form the vault + // must be in the form username:password + // if the auth info is plain text then set the variable in + // CONFIG_AUTH_SOURCE_FORMAT to the value of AUTH_SOURCE_FORMAT_PLAINTEXT + // if the auth info is base64 then set the variable in CONFIG_AUTH_SOURCE_FORMAT + // to the value of AUTH_SOURCE_FORMAT_BASE64 + // default is to store the auth data as plain text + // CONFIG_RESULT_CACHE_SECONDS is how long a successful response should be + // cached by the API GW + // it must be 60 or higher and 3599 or lower, if outside these bounds it will be + // warped to them + // if not provided then the cache duration will not be set and the API GW will + // use it's default. + // As the basic functions framework may on occasion stop a function if it's not + // been called for a + // while resulting in a cold start that may take longer than expected (this can + // be overridden + // using provisioned concurrency) it's important to configure an appropriate + // cache time that ensures + // that the function remains active but also balances unnecessary calls to the + // function + public final static String CONFIG_AUTH_SOURCE = "auth-source"; + public final static String AUTH_SOURCE_CONFIG = "config"; + public final static String AUTH_SOURCE_VAULT = "vault"; + public final static String CONFIG_AUTH_SECRET = "auth-secret"; + public final static String CONFIG_AUTH_SOURCE_FORMAT = "auth-source-format"; + public final static String AUTH_SOURCE_FORMAT_PLAINTEXT = "plaintext"; + public final static String AUTH_SOURCE_FORMAT_BASE64 = "base64"; + public final static String INCOMMING_AUTH_HEADER = "authorization"; + public final static String BASIC_AUTH_TYPE = "Basic"; + public final static String CONFIG_RESULT_CACHE_SECONDS = "result-cache-seconds"; + + public final static String DEFAULT_CONFIG_AUTH_SOURCE = AUTH_SOURCE_CONFIG; + public final static String DEFAULT_CONFIG_AUTH_SOURCE_FORMAT = AUTH_SOURCE_FORMAT_PLAINTEXT; + + public final static int RESULT_CACHE_SECONDS_MIN = 60; + public final static int RESULT_CACHE_SECONDS_MAX = 3600; + + public final static String OCID_VAULT_SECRET_PREFIX = "ocid1.vaultsecret"; + // these are used to track config errors. + private boolean errorFound = false; + private String error = ""; + private boolean testingMode = false; + private String AUTH_SOURCE; + private String AUTH_SECRET; + private String AUTH_SOURCE_FORMAT; + private Integer RESULT_CACHE_SECONDS; + + @FnConfiguration + public void config(final RuntimeContext ctx) { + log.info("Loading settings from function config"); + testingMode = Boolean + .parseBoolean(ctx.getConfigurationByKey(CONFIG_TESTING_MODE).orElse(DEFAULT_CONFIG_TESTING_MODE)); + AUTH_SOURCE = ctx.getConfigurationByKey(CONFIG_AUTH_SOURCE).orElse(DEFAULT_CONFIG_AUTH_SOURCE); + AUTH_SECRET = ctx.getConfigurationByKey(CONFIG_AUTH_SECRET).orElse(null); + AUTH_SOURCE_FORMAT = ctx.getConfigurationByKey(CONFIG_AUTH_SOURCE_FORMAT) + .orElse(DEFAULT_CONFIG_AUTH_SOURCE_FORMAT); + + String pendingResultCacheSeconds = ctx.getConfigurationByKey(CONFIG_RESULT_CACHE_SECONDS).orElse(null); + + log.info("Config start Auth secret source is " + AUTH_SOURCE); + if (testingMode) { + log.info("Config start Auth secret is " + AUTH_SECRET); + } + log.info("Config start Auth source format is " + AUTH_SOURCE_FORMAT); + log.info("Config result cache seconds is " + pendingResultCacheSeconds); + // only need to check for the vault as if it's config it's already been + // retrieved + if (AUTH_SOURCE.equalsIgnoreCase(AUTH_SOURCE_VAULT)) { + if (AUTH_SECRET == null) { + if (errorFound) { + error += "\n"; + } + String msg = "Config property AUTH_SECRET is not set, so no vault secret OCID is available"; + error += msg; + errorFound = true; + log.info(msg); + } else { + log.info("Loading auth from vault with potential OCID " + AUTH_SECRET); + // the value must start with ocid to be a secret + if (AUTH_SECRET.toLowerCase().startsWith(OCID_VAULT_SECRET_PREFIX.toLowerCase())) { + // we have something, try and get it + log.info("Attempting to create ResourcePrincipalAuthenticationDetailsProvider"); + var authProvider = ResourcePrincipalAuthenticationDetailsProvider.builder().build(); + log.info("Attempting to create SecretsClient"); + var secretsClient = SecretsClient.builder().build(authProvider); + log.info("Attempting to load auth secret from ocid '" + AUTH_SECRET + "'"); + var secretResp = secretsClient + .getSecretBundle(GetSecretBundleRequest.builder().secretId(AUTH_SECRET).build()); + if (secretResp.get__httpStatusCode__() != 200) { + if (errorFound) { + error += "\n"; + } + error += "Problem requesting vault secret - returned response is " + + secretResp.get__httpStatusCode__(); + errorFound = true; + } + SecretBundle secretBundle = secretResp.getSecretBundle(); + Base64SecretBundleContentDetails bundleContent; + try { + bundleContent = (Base64SecretBundleContentDetails) secretBundle.getSecretBundleContent(); + try { + AUTH_SECRET = new String(Base64.getDecoder().decode(bundleContent.getContent())); + } catch (IllegalArgumentException e) { + error += "Cannot base64 decode secret value from vault data because " + + e.getLocalizedMessage(); + errorFound = true; + } + } catch (ClassCastException e) { + if (errorFound) { + error += "\n"; + } + error += "Can't convert the secret content details to a Base64 version. origional type was " + + secretBundle.getSecretBundleContent().getClass().getCanonicalName(); + errorFound = true; + } + } else { + String msg = "The AUTH_SOURCE is vault, but the AUTH_SECRET does not start with " + + OCID_VAULT_SECRET_PREFIX; + error += msg; + errorFound = true; + log.info(msg); + } + } + } else { + log.info("Using secret from the config"); + if (AUTH_SECRET == null) { + if (errorFound) { + error += "\n"; + } + String msg = "No secret - is " + CONFIG_AUTH_SECRET + " set in the configuration ?"; + error += msg; + errorFound = true; + log.info(msg); + } + } + // is the auth secret is base 64 encoded ? (if it was in the vault then it might + // have been double encoded !) + log.info("AUTH_SOURCE_FORMAT is " + AUTH_SOURCE_FORMAT); + if ((AUTH_SOURCE_FORMAT != null) && (AUTH_SOURCE_FORMAT.equalsIgnoreCase(AUTH_SOURCE_FORMAT_BASE64))) { + log.info("AUTH_SOURCE_FORMAT is " + AUTH_SOURCE_FORMAT_BASE64 + " so it's being decoded from base 64"); + try { + AUTH_SECRET = new String(Base64.getDecoder().decode(AUTH_SECRET)); + } catch (IllegalArgumentException e) { + String msg = "Cannot base64 decode secret value because " + e.getLocalizedMessage(); + if (testingMode) { + msg += " input value " + AUTH_SECRET; + } + error += msg; + errorFound = true; + log.info(msg); + } + } else { + log.info( + "Auth secret format is not " + AUTH_SOURCE_FORMAT_BASE64 + " so it is being treated as plain text"); + } + RESULT_CACHE_SECONDS = null; + if (pendingResultCacheSeconds != null) { + try { + RESULT_CACHE_SECONDS = Integer.parseInt(pendingResultCacheSeconds); + if (RESULT_CACHE_SECONDS < RESULT_CACHE_SECONDS_MIN) { + log.warn("results cache seconds " + RESULT_CACHE_SECONDS + " is to low, reset to the minimum of " + + RESULT_CACHE_SECONDS_MIN); + RESULT_CACHE_SECONDS = RESULT_CACHE_SECONDS_MIN; + } + if (RESULT_CACHE_SECONDS > RESULT_CACHE_SECONDS_MAX) { + log.warn("results cache seconds " + RESULT_CACHE_SECONDS + " is to high, reset to the minimum of " + + RESULT_CACHE_SECONDS_MAX); + RESULT_CACHE_SECONDS = RESULT_CACHE_SECONDS_MAX; + } + } catch (NumberFormatException e) { + String msg = "Unable to parse provided results cache seconds (" + pendingResultCacheSeconds + + ") to a number"; + error += msg; + errorFound = true; + log.info(msg); + } + } + if (errorFound) { + log.error(error); + } + if (RESULT_CACHE_SECONDS == null) { + log.info("No results cache seconds set, API GW will apply it's default"); + } else { + log.info("Results cache seconds set to " + RESULT_CACHE_SECONDS); + } + if (testingMode) { + log.info("Final auth secret is '" + AUTH_SECRET + "'"); + } + log.info("Completed config"); + } + + /** + * IMPORTANT, The API Gateway must transfer the authorization across using names + * that matches the names in the function configuration. It can of course modify + * those names if the original ones do not match In the event that the body of + * the request is to be processed then it must be present in the request with a + * field named (all upper case) + * + * @param request + * @return + */ + public AuthResponse handleAPIGWAuthenticationRequest(AuthRequest request) { + if (testingMode) { + log.info("Recieved auth request is " + request); + } + AuthResponse response = new AuthResponse(); + // if there is an error in the configuration do not proceed and default to + // denying access. + if (errorFound) { + log.error(error); + response.setActive(false); + response.getContext().put("responseMessage", error); + return response; + } + String auth = request.getData().get(INCOMMING_AUTH_HEADER); + AuthResponse resp = processRequest(response, auth); + log.info("Proccessed auth request, returning resp " + resp.toString()); + return resp; + } + + public String handleHttpRequest(HTTPGatewayContext hctx, String body) { + AuthResponse response = new AuthResponse(); + // if there is an error in the configuration do not proceed + if (errorFound) { + log.error(error); + response.setActive(false); + response.getContext().put("responseMessage", error); + return error; + } + String auth = hctx.getHeaders().get(INCOMMING_AUTH_HEADER).orElse(null); + String resp = processRequest(response, auth).toString(); + log.info("Returning response " + resp); + return resp; + } + + public AuthResponse processRequest(AuthResponse response, String authHeader) { + if (authHeader == null) { + response.setActive(false); + response.getContext().put("responseMessage", "No " + INCOMMING_AUTH_HEADER + " present"); + return response; + } + // does the header start with Basic + if (!authHeader.startsWith(BASIC_AUTH_TYPE)) { + response.setActive(false); + response.getContext().put("responseMessage", + "Header" + INCOMMING_AUTH_HEADER + " is not authentication type " + BASIC_AUTH_TYPE); + return response; + } + // get the actual auth content + String authProvidedBase64 = authHeader.substring(BASIC_AUTH_TYPE.length()).trim(); + if (testingMode) { + log.info("Extracted header is " + authProvidedBase64); + } + // decode it + String authProvided; + try { + authProvided = new String(Base64.getDecoder().decode(authProvidedBase64)).trim(); + } catch (IllegalArgumentException e) { + String msgString = "Problem decoding incomming auth data " + e.getLocalizedMessage(); + log.error(msgString); + response.setActive(false); + response.getContext().put("responseMessage", msgString); + return response; + } + if (testingMode) { + log.info("Validating auth '" + authProvided + "' against specified secret of '" + AUTH_SECRET + "'"); + } + boolean codeOk = authProvided.equals(AUTH_SECRET); + response.setActive(codeOk); + if (codeOk) { + if (RESULT_CACHE_SECONDS != null) { + response.setExpiresAt( + DateTimeFormatter.ISO_DATE_TIME.format(ZonedDateTime.now().plusSeconds(RESULT_CACHE_SECONDS))); + } else { + response.setExpiresAt(""); + } + response.getContext().put("responseMessage", "Authenticated OK"); + } else { + response.getContext().put("responseMessage", + "Authentication failed due to invalid username and / or password"); + } + return response; + } +} \ No newline at end of file diff --git a/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/package-info.java b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/package-info.java new file mode 100644 index 0000000..cde458e --- /dev/null +++ b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/package-info.java @@ -0,0 +1 @@ +package com.oracle.timg.demos.functions.basicauth; \ No newline at end of file diff --git a/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/test/GenerateTestBasicAuth.java b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/test/GenerateTestBasicAuth.java new file mode 100644 index 0000000..55161d5 --- /dev/null +++ b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/test/GenerateTestBasicAuth.java @@ -0,0 +1,166 @@ +/*Copyright (c) 2023 Oracle and/or its affiliates. + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any +person obtaining a copy of this software, associated documentation and/or data +(collectively the "Software"), free of charge and under any and all copyright +rights in the Software, and any and all patent rights owned or freely +licensable by each licensor hereunder covering either (i) the unmodified +Software as contributed to or provided by such licensor, or (ii) the Larger +Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +one is included with the Software (each a "Larger Work" to which the Software +is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, +use, sell, offer for sale, import, export, have made, and have sold the +Software and the Larger Work(s), and to sublicense the foregoing rights on +either these or other terms. + +This license is subject to the following condition: +The above copyright notice and either this complete permission notice or at +a minimum a reference to the UPL must be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package com.oracle.timg.demos.functions.basicauth.test; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.http.HttpStatus; + +import com.oracle.timg.demos.functions.basicauth.AuthRequest; +import com.oracle.timg.demos.functions.basicauth.BasicAuthFunction; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class GenerateTestBasicAuth { + + private static Option urlOption; + private static Option usernameOption; + private static Option passwordOption; + + public static void main(String[] args) throws IOException { + // build the input string, the options are + // -p / --password the password to use for the request + // -r / --url the url to use as a test call if missing then no test call will be + // -n / --username the username to use for the test + // test request defaults to hmac + CommandLine commandLine = getCommandLine(args); + AuthRequest request = new AuthRequest(); + String authString = commandLine.getOptionValue("n") + ":" + commandLine.getOptionValue("p"); + String authBase64 = new String(Base64.getEncoder().encode(authString.getBytes())); + String authHeader = BasicAuthFunction.BASIC_AUTH_TYPE + " " + authBase64; + request.getData().put(BasicAuthFunction.INCOMMING_AUTH_HEADER, authHeader); + request.setType("USER_DEFINED"); + + String requestBody = "{\"type\":\"USER_DEFINED\", \"data\": {"; + requestBody += request.getData().entrySet().stream().map(e -> "\"" + e.getKey() + "\":\"" + e.getValue() + "\"") + .collect(Collectors.joining(",")); + requestBody += "}}"; + log.info("Generated data is " + requestBody); + if (commandLine.hasOption(urlOption)) { + // this must have a value to have got here + String url = commandLine.getOptionValue(urlOption); + callUrl("POST", url, new HashMap<>(), requestBody, 1, 1, true); + } + } + + private static void callUrl(String requestMethod, String url, Map headers, String body, int count, + int delay, boolean exitOnFail) { + log.info("Making " + requestMethod + "\nCalling " + url + "\nWith headers" + headers + "\nrequest body is\n" + + body); + for (int i = 1; i <= count; i++) { + HttpClient client = HttpClient.newBuilder().build(); + URI uri; + try { + uri = new URI(url); + } catch (URISyntaxException e) { + log.error("Provided URL " + url + " does not parse properly because " + e.getLocalizedMessage()); + return; + } + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .method(requestMethod, BodyPublishers.ofString(body)).uri(uri); + headers.entrySet().stream().forEach(entry -> requestBuilder.header(entry.getKey(), entry.getValue())); + HttpRequest request = requestBuilder.build(); + HttpResponse response; + try { + response = client.send(request, BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + log.error("Exception making the http call:" + e.getLocalizedMessage()); + return; + } + log.info("Try " + i + " Response status code is " + response.statusCode()); + log.info("Try " + i + " Response body is:\n" + response.body()); + if (exitOnFail && response.statusCode() != HttpStatus.SC_OK) { + log.info("Non OK response, stopping"); + return; + } + if (i < count) { + try { + log.info("Completed try " + i + " waitign " + delay + " seconds "); + Thread.sleep((long) (1000 * delay)); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + } + + private static CommandLine getCommandLine(String args[]) { + Options options = setupCommandLineOptions(); + DefaultParser parser = new DefaultParser(); + CommandLine commandLine = null; + try { + commandLine = parser.parse(options, args); + } catch (ParseException e) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp( + GenerateTestBasicAuth.class.getSimpleName() + " sends a request to the server for auth test", + options); + System.exit(-1); + } + return commandLine; + } + + private static Options setupCommandLineOptions() { + usernameOption = Option.builder("n").longOpt("username").required().hasArg().desc("username to use").build(); + passwordOption = Option.builder("p").longOpt("password").required().hasArg().desc("the password to use") + .build(); + urlOption = Option.builder("u").longOpt("url").optionalArg(true).hasArg().desc( + "URL To call to test request, if missing no test call will be made, just output what the request would be") + .build(); + Options options = new Options().addOption(usernameOption).addOption(passwordOption).addOption(urlOption); + return options; + } +} \ No newline at end of file diff --git a/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/test/package-info.java b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/test/package-info.java new file mode 100644 index 0000000..afae9d3 --- /dev/null +++ b/functionsbasicauth/src/main/java/com/oracle/timg/demos/functions/basicauth/test/package-info.java @@ -0,0 +1 @@ +package com.oracle.timg.demos.functions.basicauth.test; \ No newline at end of file