AWS Lambda: Basic Authentication support for OrangeHRM MySQL API
In this article, we will explore an experimental strategy to implement Basic Authentication support for a Lambda Function, by using a pair of Functions with AWS Signature Version 4 and AWS IAM Policy. While Basic Authentication is not as secure as HMAC or AWS Signature V4, it is more secure than having a public Lambda Function URL with Auth type set to NONE as our main Function.
The main reason why we are exploring this option: Many integration products including Identity & Access Management products do not support HMAC or AWS Signature for all Connector types. So instead of using a publicly available main Lambda Function, we can explore a 2-Function strategy that is a bit more secure.
We still use Auth type set to NONE for our gatekeeper Function, but our main Function uses Auth type set to AWS_IAM. We could say that we are leveraging layered security, to mitigate some of the risk.
This article is a follow up article to this one: How to :: MySQL to REST API with AWS Lambda and nodejs
To illustrate the strategy based on 2 Functions, we will be using OrangeHRM Open Source and a Function that provides a SQL Query REST API as described in the previous article linked above. For the MySQL function, you need a mysql2 layer.
For more information on how to configure the MySQL Function, refer to the linked article above.
Before we create our new Basic Authentication Function, we need 2 NodeJS module layers: One for axios and one to support AWS Signature V4. You can refer to this article for the steps: AWS Lambda: How to create a nodejs layer for axios
Now we need to create a new Lambda Function with NodeJS and add the layers.
This is the NodeJS code for our Basic Authentication Function:
import axios from 'axios';
import { SignatureV4 } from '@smithy/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-js';
export const handler = async (event) => {
console.log('####### event = '+JSON.stringify(event));
const API_URL = 'https:/'+event.rawPath;
const apiUrl = new URL(API_URL);
const method = event.requestContext.http.method;
// Extract values from Authorization Header
const base64token = event.headers.authorization.substring(6);
const base64payload = Buffer.from(base64token, 'base64').toString('ascii');
console.log('base64token = '+base64token+'. base64payload = '+base64payload);
const base64obj = JSON.parse(base64payload);
const AWS_ACCESS_KEY_ID = base64obj.AWS_ACCESS_KEY_ID;
const AWS_SECRET_ACCESS_KEY = base64obj.AWS_SECRET_ACCESS_KEY;
const AWS_SERVICE = base64obj.AWS_SERVICE;
const AWS_REGION = base64obj.AWS_REGION;
const sigv4 = new SignatureV4({
service: AWS_SERVICE,
region: AWS_REGION,
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY
},
sha256: Sha256,
});
if(event.body){
const signed = await sigv4.sign({
method: method,
hostname: apiUrl.host,
path: apiUrl.pathname,
protocol: apiUrl.protocol,
body: event.body,
headers: {
'Content-Type': event.headers['content-type'],
'Content-Length': event.headers['content-length'],
'host': apiUrl.hostname,
'mysql_host': event.headers.mysql_host,
'mysql_user': event.headers.mysql_user,
'mysql_password': event.headers.mysql_password,
'mysql_database': event.headers.mysql_database,
},
});
console.log('signed = '+JSON.stringify(signed));
try {
const { data } = await axios({
...signed,
data: event.body,
url: API_URL, // compulsory
});
console.log('Successfully received data: ', data);
return data;
} catch (error) {
console.log('An error occurred', error);
throw error;
}
} else{
const signed = await sigv4.sign({
method: method,
hostname: apiUrl.host,
path: apiUrl.pathname,
protocol: apiUrl.protocol,
headers: {
host: apiUrl.hostname, // compulsory
},
});
console.log('signed = '+JSON.stringify(signed));
try {
const { data } = await axios({
...signed,
url: API_URL,
});
console.log('Successfully received data: ', data);
return data;
} catch (error) {
console.log('An error occurred', error);
throw error;
}
}
};
The Function code is generic, except for the 4 HTTP Headers (signed) that needs to be submitted to our MySQL Lambda Function:
'mysql_host': event.headers.mysql_host,
'mysql_user': event.headers.mysql_user,
'mysql_password': event.headers.mysql_password,
'mysql_database': event.headers.mysql_database,
We also want to configure our MySQL Lambda Function for AWS_IAM Auth type:
The Basic Function extracts the AWS Signature values from a base64 encoded Basic Authentication token, that we need to submit via the Authorization HTTP Header.
We need to create an AWS User and assign a Policy to the User, e.g. via a Role. Here, we are assigning the Policy directly to the User:
The Authorization HTTP Header value is set to Basic {base64 encoded token}. To create the token using a similar string as shown below, you can use this free tool.
{"AWS_ACCESS_KEY_ID":"AQDSTBKABCDEFTXET","AWS_SECRET_ACCESS_KEY":"TGabcKZH5a2efg+SXqDk/nUABCDJLpl7ABcsbABC","AWS_SERVICE":"lambda","AWS_REGION":"us-east-1"}
We will use Postman to test our Function. In a real life scenario, we can use any product that can support an Authorization HTTP Header. The URL consists of a concatenation of the 2 Function URLs: We need to remove https:// for the 2nd MySQL Function URL:
https://abcdef....lambda-url.us-east-1.on.aws/zxcvb....lambda-url.us-east-1.on.aws
The body is a JSON document that contains a SQL Query:
{"Query":"select emp.employee_id as empid ... from ..."}
The next step would be to configure the product of your choice to query the MySQL database using our Basic Function. I tested this in my lab and it allowed me to successfully query MySQL using a Web Service connector with an Authorization HTTP Header, and the 4 MySQL Headers.
I hope that you found this article interesting, and that it will inspire you to explore and evolve your own strategy for adding security to Lambda Functions when AWS Signature is not available as an option.