- "기밀 VM의 빈틈을 메운다" 마이크로소프트의 오픈소스 파라바이저 '오픈HCL'란?
- The best early Black Friday AirPods deals: Shop early deals
- The 19 best Black Friday headphone deals 2024: Early sales live now
- I tested the iPad Mini 7 for a week, and its the ultraportable tablet to beat at $100 off
- The best Black Friday deals 2024: Early sales live now
How to Build Your First Node.js gRPC API
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: