Working with sensitive data in custom elements

Custom elements give you the opportunity to expand your Kentico Kontent project beyond its built-in features. You can make your custom elements reusable and configurable in your project by entering JSON parameter properties. However, data entered in this field, including sensitive data such as API key or credentials, are able to be viewed by any of your contributors, including those who do not have the capabilities to view sensitive data.

Premium feature

Custom elements require a Professional plan or higher.

Table of contents

    To manage our products, we will use Magento's API. Because Magento exposes your management API key for your products, we want to provide a content editor with only a read-only product selector. In short, we don't want to allow a content editor to edit, create or delete products. In addition, Magento's API requires an access token while requesting their API which is also sensitive data and should not be shown in your custom element's properties. In this tutorial, you'll learn how to use and protect your sensitive data needed for custom elements by creating a server-side proxy solution to handle your sensitive information.

    Server-side proxy

    As a proxy, we will configure a simple AWS Lambda function which will receive requests from our custom element, add an authentication token, copy headers and query strings, send the request to Magento API, and then resend the response back to our custom element. Since the authentication token is stored within the AWS Lambda function, it's not accessible to contributors on your project.

    Communication diagram

    In this tutorial you will:

    • Create an AWS Lambda serverless function with API Gateway.
    • Configure the proper host and authentication token.
    • Send test request to our proxy.

    Step 1: Creating an AWS Lambda function

    First, you need an Amazon Web Services account. Visit Amazon Web Services and if you don't have an account, sign up. 

    Once you are logged in, select the data center that best fits your location or purpose in the upper-right corner. Expand All services and click Lambda.

    AWS Management Console

    On the next screen, click Create a function.

    Creating a new AWS Lambda function

    To create the function:

    • Select Author from scratch 
    • Name your function requestRepeater.
    • Under Runtime, select Node.js
    • Click Create function.

    Specify function name and runtime.

    On the function's Configuration page:

    1. Click API Gateway trigger from the left-side panel. 
    2. Navigate to Configure triggers
    3. Under API, select Create a new API.
    4. Under Security, select Open.
    5. Click Add in the lower right of the screen.
    6. Click Save in the upper-right corner.

    Note: For demonstration purposes, this endpoint will be publicly available and can be invoked by all users.

    Add API Gateway.

    Be sure to copy the API endpoint to your clipboard because you will need to enter it in Step 3. This URL is the endpoint of our proxy server. Click the requestRepeater Lambda function.

    API Gateway URL.

    Paste the following code in Function code.

    This code covers a scenario using GET requests returning a JSON response. If you are using a different endpoint, adjust the code to meet your needs. For example, if API is only able to serve XML content types, the Accept header would have to be changed to headers['Accept'] = 'application/xml';

    • JavaScript
    const https = require("https"); const querystring = require("querystring"); /* ========Config Section======== */ const host = process.env.HOST; const path = process.env.PATH; const accessControlAllowOriginValue = process.env.ACCESS_CONTROL_ALLOW_ORIGIN; const accessControlAllowHeadersValue = process.env.ACCESS_CONTROL_ALLOW_HEADERS; // Bearer token authentization const bearerToken = process.env.BEARER_TOKEN; // Basic authentication credentials const username = process.env.USERNAME; const password = process.env.PASSWORD; /* ========Config Section======== */ let authorizationHeaderValue; if (bearerToken || (username && password)) { authorizationHeaderValue = bearerToken ? `Bearer ${bearerToken}` : `Basic ${new Buffer(username + ":" + password).toString("base64")}`; } const request = (queryStringParameters, headers) => { const requestOptions = { host: host, path: path, port: 443, method: "GET", }; if (queryStringParameters) { requestOptions.path = `${requestOptions.path}?${querystring.stringify(queryStringParameters)}`; } if (authorizationHeaderValue) { headers["Authorization"] = authorizationHeaderValue; } headers["Accept"] = "application/json"; headers["accept-encoding"] = "identity"; headers["Host"] = host; requestOptions.headers = headers; return new Promise((resolve, reject) => { https.request(requestOptions, response => { let data = ""; response.on("data", chunk => { data += chunk; }); response.on("end", () => { const dataObject = JSON.parse(data); response.data = dataObject; resolve(response); }); }) .on("error", error => { reject(error); }) .end(); }); }; exports.handler = (event, context, callback) => { const corsHeaders = { "Access-Control-Allow-Origin": accessControlAllowOriginValue, "Access-Control-Allow-Headers": accessControlAllowHeadersValue }; const repeatResponse = (response) => { let multiValueHeaders = {}; for (const headerName in response.headers) { if (Array.isArray(response.headers[headerName])) { multiValueHeaders[headerName] = response.headers[headerName]; delete response.headers[headerName]; } } callback(null, { statusCode: response.statusCode, body: JSON.stringify(response.data), headers: { ...response.headers, ...corsHeaders }, multiValueHeaders: multiValueHeaders, }); }; const sendError = (error) => { callback(null, { statusCode: "400", body: JSON.stringify(error), headers: corsHeaders, }); }; switch (event.httpMethod) { case "GET": request(event.queryStringParameters, event.headers) .then((response) => { repeatResponse(response); }) .catch(error => { sendError(error); }); break; default: sendError(new Error(`Unsupported method "${event.httpMethod}"`)); } };
    const https = require("https"); const querystring = require("querystring"); /* ========Config Section======== */ const host = process.env.HOST; const path = process.env.PATH; const accessControlAllowOriginValue = process.env.ACCESS_CONTROL_ALLOW_ORIGIN; const accessControlAllowHeadersValue = process.env.ACCESS_CONTROL_ALLOW_HEADERS; // Bearer token authentization const bearerToken = process.env.BEARER_TOKEN; // Basic authentication credentials const username = process.env.USERNAME; const password = process.env.PASSWORD; /* ========Config Section======== */ let authorizationHeaderValue; if (bearerToken || (username && password)) { authorizationHeaderValue = bearerToken ? `Bearer ${bearerToken}` : `Basic ${new Buffer(username + ":" + password).toString("base64")}`; } const request = (queryStringParameters, headers) => { const requestOptions = { host: host, path: path, port: 443, method: "GET", }; if (queryStringParameters) { requestOptions.path = `${requestOptions.path}?${querystring.stringify(queryStringParameters)}`; } if (authorizationHeaderValue) { headers["Authorization"] = authorizationHeaderValue; } headers["Accept"] = "application/json"; headers["accept-encoding"] = "identity"; headers["Host"] = host; requestOptions.headers = headers; return new Promise((resolve, reject) => { https.request(requestOptions, response => { let data = ""; response.on("data", chunk => { data += chunk; }); response.on("end", () => { const dataObject = JSON.parse(data); response.data = dataObject; resolve(response); }); }) .on("error", error => { reject(error); }) .end(); }); }; exports.handler = (event, context, callback) => { const corsHeaders = { "Access-Control-Allow-Origin": accessControlAllowOriginValue, "Access-Control-Allow-Headers": accessControlAllowHeadersValue }; const repeatResponse = (response) => { let multiValueHeaders = {}; for (const headerName in response.headers) { if (Array.isArray(response.headers[headerName])) { multiValueHeaders[headerName] = response.headers[headerName]; delete response.headers[headerName]; } } callback(null, { statusCode: response.statusCode, body: JSON.stringify(response.data), headers: { ...response.headers, ...corsHeaders }, multiValueHeaders: multiValueHeaders, }); }; const sendError = (error) => { callback(null, { statusCode: "400", body: JSON.stringify(error), headers: corsHeaders, }); }; switch (event.httpMethod) { case "GET": request(event.queryStringParameters, event.headers) .then((response) => { repeatResponse(response); }) .catch(error => { sendError(error); }); break; default: sendError(new Error(`Unsupported method "${event.httpMethod}"`)); } };

    Click Create function and then Save.

    Step 2: Configuring your Lambda function

    In the Environment variables section, enter the following keys and values:

    • BEARER_TOKEN – {yourMagentoAccessToken}
    • HOST – demo1-m2.mage.direct
    • PATH – /index.php/rest/V1/products

    Note: For more info about Magento access tokens, see Integrating with e-commerce (Magento).

    AWS Lambda environment variables

    Step 3: Testing the proxy

    To test the Magento API via your new proxy (which is accessible through the API Gateway), enter your requestRepeater API endpoint as {Gateway API URL} and then enter the URL in your browser address bar to try the GET request:

    • HTTP
    {Gateway API URL}?searchCriteria[pageSize]=10&searchCriteria[filterGroups][0][filters][0][field]=name&searchCriteria[filterGroups][0][filters][0][conditionType]=like&searchCriteria[filterGroups][0][filters][0][value]=%25watch%25
    {Gateway API URL}?searchCriteria[pageSize]=10&searchCriteria[filterGroups][0][filters][0][field]=name&searchCriteria[filterGroups][0][filters][0][conditionType]=like&searchCriteria[filterGroups][0][filters][0][value]=%25watch%25

    In our example:

    • HTTP
    https://vpzvj1fspi.execute-api.eu-central-1.amazonaws.com/default/requestRepeater?searchCriteria[pageSize]=10&searchCriteria[filterGroups][0][filters][0][field]=name&searchCriteria[filterGroups][0][filters][0][conditionType]=like&searchCriteria[filterGroups][0][filters][0]
    https://vpzvj1fspi.execute-api.eu-central-1.amazonaws.com/default/requestRepeater?searchCriteria[pageSize]=10&searchCriteria[filterGroups][0][filters][0][field]=name&searchCriteria[filterGroups][0][filters][0][conditionType]=like&searchCriteria[filterGroups][0][filters][0]

    If entered correctly, you will receive a JSON response from the Magento server which was delivered via your new Lambda function.

    JSON response

    To learn more about Magento API, see Integrating Magento.

    Summarized

    When you need to enter sensitive data to your custom element configuration, use an external server-side service like AWS Lambda or Azure Function. After you complete the integration with the service, you get a server-side proxy to handle your sensitive data, request and response headers, and query parameters.