Securing Nodejs(Express) REST API with Role-based access control(RBAC) using Keycloak
REST API could contain sensitive data(user information, financial details, or proprietary business data) and needs to be protected from unauthorized access. In this article, we will explore steps to secure a Nodejs(Express) REST API using RBAC(Role-based access control) setup in Keycloak.
Pre-requisite
Spin up Keycloak server — If you have Docker setup in your local system, you can use one of the existing GitHub repositories I created to spin up the Keycloak server — https://github.com/saurav-samantray/custom-auth-service
Setup Keycloak
Step1: Access the Local Keycloak Server
Access the local Keycloak server setup in the previous setup at http://localhost:8080
Step2: Create a new client
- Go to Clients in the left navigation.
- Click on Import Client
- Upload the file -> secure-express-service.json
- Click on save
Step3: Create Realm Roles
- Navigate to the Realm roles in the left navigation menu and click on Create role
- Create user role — express-user
- Repeat the steps to create an admin role — express-admin
Step4: Create Users
Navigate to the Users section from the left navigation menu. Fill out the new user form and click on Create.
- Navigate to the details of the newly created user — user@nerdcore.com, go to the Credentials tab and Click on Set Password
- Fill out the Set password form with the appropriate password. Make sure to mark the Temporary toggle button Off
- Create another user admin@nerdcode.com
Step5: Assign roles to users
Navigate to the Role Mapping Tab on the user details page and click on the Assign Role button
- Select express-user and click on the Assign button
- Similarly, assign the express-admin role to user admin@nerdcore.com
Basic Express JS application to serve REST endpoints
Initialize a Node JS application
run the below set of commands to initialize a NodeJS application as our express server will reside inside the Node application.
mkdir secure-express-service
cd secure-express-service
npm init
The npm init command will initiate a series of prompts to create the base package.json. Sample input below
package name: (secure-express-service)
version: (1.0.0)
description: Express JS based REST APIs secured using Keycloak auth server
entry point: (index.js) app.js
test command:
git repository:
keywords:
author: Saurav Samantray
license: (ISC)
Install dependencies
npm install express express-session keycloak-connect nodemon
Define main execution file — app.js
Add a file named app.js at the root level of your secure-express-service project.
Define imports and express app
// file - app.js
const express = require('express');
const session = require("express-session");
const Keycloak = require("keycloak-connect");
const app = express();
const PORT = 3000;
...
Setup Keycloak Middleware
// file - app.js
...
const USER_ROLE = process.env.USER_ROLE || 'express-user';
const ADMIN_ROLE = process.env.ADMIN_ROLE || 'express-admin';
const kcConfig = {
clientId: process.env.AUTH_CLIENT_ID || 'secure-express-service',
bearerOnly: true,
serverUrl: process.env.AUTH_SERVER || 'http://localhost:8080',
realm: process.env.AUTH_REALM || 'master'
};
const memoryStore = new session.MemoryStore();
Keycloak.prototype.accessDenied = function (request, response) {
response.status(401)
response.setHeader('Content-Type', 'application/json')
response.end(JSON.stringify({ status: 401, message: 'Unauthorized/Forbidden', result: { errorCode: 'ERR-401', errorMessage: 'Unauthorized/Forbidden' } }))
}
const keycloak = new Keycloak({ store: memoryStore }, kcConfig);
function adminOnly(token, request) {
return token.hasRole(`realm:${ADMIN_ROLE}`);
}
function isAuthenticated(token, request) {
return token.hasRole(`realm:${ADMIN_ROLE}`) || token.hasRole(`realm:${USER_ROLE}`);
}
app.use(session({
secret: process.env.APP_SECRET || 'BV&%R*BD66JH',
resave: false,
saveUninitialized: true,
store: memoryStore
}));
app.use( keycloak.middleware() );
...
Setup REST endpoint and server
app.get('/public', (req, res) => {
res.status(200).send({
'message': "This is a public enpoint which can be accessed by anonymous users",
});
})
app.get('/secured', [keycloak.protect(isAuthenticated)], (req, res) => {
res.status(200).send({
'message': "This is a secured enpoint which can be accessed by any authenticated user",
});
})
app.get('/secured-admin', [keycloak.protect(adminOnly)], (req, res) => {
res.status(200).send({
'message': "This is a secured enpoint which can be accessed only by any authenticated user with role admin",
});
})
app.listen(PORT, (error) => {
if (!error) {
console.log("Server is Successfully Running, and App is listening on port " + PORT)
}
else {
console.log("Error occurred, server can't start", error);
}
}
);
Update package.json scripts to run server
- Update the content in the scripts section of package.json as below. It will help you run your express server in both development and production mode
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
- Start your express server in development mode(auto-reload on file change) using the below command
npm run dev
Testing your REST APIs
Public Endpoint— http://localhost:3000/public
- Even without an authentication token, this endpoint will fetch a response.
Secured Endpoint — http://localhost:3000/secured
- Without a token, you should get an authentication error
- Let’s set up the Authorization tab to generate a token using the Configure New Token section. You can use the values below.
Token Name - secure-express-service
Grant type - Password Credentials
Access Token URL - http://localhost:8080/realms/master/protocol/openid-connect/token
Client ID - secure-express-service
Username - user@nerdcore.com
Password -
Client Authentication - Send as basic auth header
- Click on Get New Access Token, then Proceed, then Use Token
- Now fire the /secured rest endpoint again, and you will get a successful response.
Secured Admin Endpoint — http://localhost:3000/secured-admin
- If you try to access the endpoint without any token — Auth error
- If you try to access the endpoint with the token from user@nerdcore.com — Auth error
- If you try to access the endpoint with the token from admin@nerdcore.com — Success
You can find the GitHub repository here for reference.
Happy learning and happy coding!