Serverless Security
Tutorial: How to Build Your First Node.js gRPC API
Compared to other API technologies like REST and GraphQL, gRPC is lightweight and exceptionally robust, thanks in large part to its use of protobufs. Interested in exploring how to build your own API? Read on to see how easy it is to do so with Node.js and gRPC.
What is gRPC?
Google Remote Procedure Call (gRPC) is a remote procedure call framework that eases the communication process between client and server applications. It’s high-performing, robust, and lightweight.
These three qualities are due to its data exchange format and the interface definition language used by protocol buffers (protobufs). Protobufs are small and fast due to their data serialization format, which enables smaller packets. This makes them highly suitable for fast data flow and economical storage.
Before building the application program interface (API), it's important to understand the features that give gRPC an upper hand over other API technologies like REST and GraphQL.
What are the features of gRPC?
The distinguishing features of gRPC are:
- The use of protobufs. Developers must follow a specific format in defining the protobufs. This enforcement plays a significant role in clean code and error reduction.
- The presence of a compiler (protoc) to perform data serialization and deserialization. The protoc does this using automatic code generation that handles the functionality. This relieves developers from the overhead of parsing JSON or XML.
- The reduction of errors and increased efficiency because the protoc compiler parses the data. Developers only focus on other parts of the logic.
- gRPC uses HTTP/2, which is faster than HTTP/1 used by other technologies. Specifically, gRPC supports bidirectional streaming, enabling the multiplexed forms of communication between systems.
This article will demonstrate how to develop a basic Node.js gRPC API that salts and hashes a password received from a client endpoint. Also, it will give an overview of some of the potential security pitfalls of gRPC and the best practices for avoiding them.
Prerequisites
To best follow this walkthrough, the following prerequisites are required:
- A basic understanding of Node.js and the ability to create a basic app
- Node.js installed on your machine
While not mandatory, JavaScript knowledge is also quite helpful.
Creating the API
In the working directory, create a folder named node-grpc. Open the folder and your favorite terminal. Then start the node by running the command below:
npm init -y
Next, install the required libraries. Both grpc-js and protoc are two open-source libraries that enable you to use gRPC in Node.js. Install them by running these commands:
npm i @grpc/grpc-js
npm i @grpc/proto-loader
Next, create three files: one for the protobuf definition, one for the client stub, and one for the server code. You can do this using the following command:
touch server.js client-stub.js password.proto
As the names suggest, server.js contains the server code, client-stub.js, the client code, and the protobuf definition are in the password.proto file.
Writing the protobuf definition
In the password.proto file, paste the following code:
syntax = "proto3";
message PasswordDetails {
string id = 1;
string password = 2;
string hashValue = 3;
string saltValue = 4;
}
service PasswordService {
rpc RetrievePasswords (Empty) returns (PasswordList) {}
rpc AddNewDetails (PasswordDetails) returns (PasswordDetails) {}
rpc UpdatePasswordDetails (PasswordDetails) returns (PasswordDetails) {}
}
message Empty {}
message PasswordList {
repeated PasswordDetails passwords = 1;
}
The code starts by specifying the syntax version of the protocol buffers. Then, it creates a message definition for a password’s details. You must send all commands to a gRPC API contained in a service. So, in this case, PasswordService contains the commands needed for this tutorial.
The commands taking in and returning message replies are for adding a new password, editing, and reading passwords. The messages passed or replied to and from the endpoints can be empty (Empty) or contain some message. The rest of the message definitions added are for representing empty messages and a list of passwords.
The server code
Start by importing the required modules and your proto file:
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const PROTO_PATH = "./password.proto";
Then, initialize an object for storing the protobuf loader (protoLoader) options in this manner:
const loaderOptions = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
};
The above fields do the following:
- keepCase instructs the protoLoader to maintain protobuf field names.
- longs and enums store the data types that represent long and enum values.
- defaults, when set to true, sets default values for output objects.
- oneof sets virtual oneof properties to field names.
You can find more information about setting the options object here.
Next, initialize the package definition object by passing the protobuf file and the options object in the protobuf loader’s loadSync method:
// initializing the package definition
var packageDef = protoLoader.loadSync(PROTO_PATH, loaderOptions);
To create a gRPC object, call the grpc-js method, loadPackageDefinition while passing in the previously created package definition. Use the object to invoke the gRPC service and, eventually, the commands.
const grpcObj = grpc.loadPackageDefinition(packageDef);
But before that, you must invoke your Node.js server to add the gRPC services to it. Do that using this line:
const ourServer = new grpc.Server();
The first two default passwords are in a JavaScript object. Here, you can use your own logic and retrieve data from your storage engine. It can be a database, a serverless platform’s data retrieval logic, a file, and so on.
let dummyRecords = {
"passwords": [
{ id: "153642", password: "default1", hashValue: "default", saltValue: "default" },
{ id: "234654", password: "default2", hashValue: "default", saltValue: "default" }]
};
Add the service and the commands using the code below:
ourServer.addService(grpcObj.PasswordService.service, {
/*our protobuf message(passwordMessage) for the RetrievePasswords was Empty. */
retrievePasswords: (passwordMessage, callback) => {
callback(null, dummyRecords);
},
addNewDetails: (passwordMessage, callback) => {
const passwordDetails = { ...passwordMessage.request };
dummyRecords.passwords.push(passwordDetails);
callback(null, passwordDetails);
},
updatePasswordDetails: (passwordMessage, callback) => {
const detailsID = passwordMessage.request.id;
const targetDetails = dummyRecords.passwords.find(({ id }) => detailsID == id);
targetDetails.password = passwordMessage.request.password;
targetDetails.hashValue = passwordMessage.request.hashValue;
targetDetails.saltValue = passwordMessage.request.saltValue;
callback(null, targetDetails);
},
});
The addService method takes in two parameters: the service, and the commands. Each of the commands takes in a message argument (as defined in the proto file) and a callback function argument. The callback function passes the replies from the commands to the client.
The retrievePasswords method returns all passwords stored in the object. The addNewDetails method inserts new password details to the inner array of your object using the Array.prototype.push method from the message request.
To update a password’s details, the ID of the password comes from the request. The Array.prototype.find method retrieves the details to update using the request details passed in the command, updates the retrieved details, and returns the updated details to the client.
Finally, bind the server to a port and start it using the bindAsync method.
ourServer.bindAsync(
"127.0.0.1:50051",
grpc.ServerCredentials.createInsecure(),
(error, port) => {
console.log("Server running at http://127.0.0.1:50051");
ourServer.start();
}
);
The client code
Now you’re ready to salt and hash your passwords. To help achieve this, you need a library called bcrypt. Install it using this command:
npm i bcrypt
Just like the server code, start by importing the required modules. You create the package definitions and the gRPC object in the same way.
const grpc = require("@grpc/grpc-js");
var protoLoader = require("@grpc/proto-loader");
const PROTO_PATH = "./password.proto";
const bcrypt = require('bcrypt');
const options = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
};
var grpcObj = protoLoader.loadSync(PROTO_PATH, options);
const PasswordService = grpc.loadPackageDefinition(grpcObj).PasswordService;
You create the client stub by passing the server address and the server connection credentials to the service name constructor.
const clientStub = new PasswordService(
"localhost:50051",
grpc.credentials.createInsecure()
);
Invoke the commands by passing in the messages as the first parameter and the callback function as parameters. The retrievePasswords service command illustrates this.
clientStub.retrievePasswords({}, (error, passwords) => {
//implement your error logic here
console.log(passwords);
});
You generate a salt using the bcrypt.genSalt method by passing in the salt rounds you need. You generate a hash using the bcrypt.hash method by passing in the password and the salt values. In each method, there is a callback function containing any encountered error message, if found, and the result. Set the hash and salt values inside the method bodies as shown.
const saltRounds = 10;
let passwordToken = "5TgU76W&eRee!";
let updatePasswordToken = "H7hG%$Yh33"
bcrypt.genSalt(saltRounds, function (error, salt) {
bcrypt.hash(passwordToken, salt, function (error, hash) {
clientStub.addNewDetails(
{
id: Date.now(),
password: passwordToken,
hashValue: hash,
saltValue: salt,
},
(error, passwordDetails) => {
//implement your error logic here
console.log(passwordDetails);
}
);
});
});
bcrypt.genSalt(saltRounds, function (error, salt) {
//implement your error logic here
bcrypt.hash(updatePasswordToken, salt, function (error, hash) {
//implement your error logic here
clientStub.updatePasswordDetails(
{
/*
This is one of the defaultIDs of our dummy object's values.
You can change it to suit your needs
*/
id: 153642,
password: updatePasswordToken,
hashValue: hash,
saltValue: salt,
},
(error, passwordDetails) => {
//implement your error logic here
console.log(passwordDetails);
}
);
});
});
You should do the hashing and salting on the server. Here, you did the processes in the client just for demonstration purposes. You can learn more about salting and hashing here.
Then, run the application by starting the server using the command below:
node server.js
Add the client stub using node client-stub.js. Uncomment the ones you don’t want to fire at a specific time.
Below are screenshots produced when you run the commands.
retrievePasswords command:
updatePasswordDetails command:
addNewDetails command:
As you’ve seen, creating the protobuf definitions follows a format. There was no need for any parsing whatsoever. To check the bidirectional streaming capacities of gRPC, check this demo example on the official gRPC documentation page.
Now that you have created your gRPC API, it’s important to understand the security pitfalls of using gRPC and some possible workarounds.
Potential security pitfalls of gRPC and best practices for avoiding them
There are three primary problems with gRPC:
- Susceptibility to data leaks and man-in-the-middle attacks
- Vulnerability to denial-of-service attacks
- Memory bugs
Susceptibility to data leaks and man-in-the-middle attacks
Insecure credentials set in your code can give cyberattackers access to your data. This tutorial uses insecure client credentials in the client stub. Once your data leaks, the attackers may decide to inject malicious code into your data.
You can avoid this by setting up secure credentials outlined in the gRPC documentation. Also, avoid hard-coding or committing authorization details in your code. You should hide them and only access them when needed.
Vulnerability to denial-of-service attacks
For users with C/C++ implementations, ephemeral communications initiated within a short burst of time can trigger service denial calls until the service restarts.
You can prevent this by increasing the limit for opened file descriptors on Linux systems. Use this command:
sudo ulimit -n increasedNumber
Another possible workaround is to use load balancers to keep a single instance’s loads more manageable. Service watchdogs can also help because they monitor services’ statuses.
Memory bugs
Again, developers using gRPC C-core-based wrapper implementations don’t need to deal with memory management. This may lead to bugs in the system due to the memory handling code. Likewise, if you don’t handle memory management, occurrences such as system crashes are common.
To avoid this, beginner developers should use the language implementation, which handles memory management automatically.
You can find more information about this in this article.
This tutorial briefly introduced gRPC and the features of gRPC for APIs that differentiate it from other API technologies such as REST and GraphQL. Then, it provided instructions for creating a Node.js gRPC API from scratch, adding some basic functions to the API that demonstrate the unique features of gRPC, and finished by reviewing the potential security pitfalls of gRPC and the best practices for avoiding them. To successfully build a gRPC API, you must follow a specified structure according to the language you are using.
Trend Micro Solutions
Building with vulnerabilities in mind is crucial to cloud-native development. That’s why Trend Micro Cloud One™ Application Security is the solution for developers looking to work fast and build secure. Unlike signature-based tools, Application Security protects against code vulnerabilities, data exfiltration on the server, and other common vulnerability attacks at the application level. Plus, since all protection takes place inside the application directly, network latency is not a factor.
Experience it for yourself with a free 30-day trial. You can also watch serverless and container demos to learn more.