@entur-partner/permission-client-node
v3.6.2
Published
Node client library for Permission Store
Downloads
6,241
Keywords
Readme
Permission Client for Node
Permission Client is a JavaScript module for Node.js, serving as an SDK for Entur’s Permission Store. It provides a local cache for user permissions, improving performance and reducing load on the backend.
A similar SDK is available for Java: Permission Client for Java.
Using Permission Client helps you comply with Entur’s Application Security Requirements for authorization.
For questions about using Permission Store, join the Slack channel #work-tilgangstyring. For general authentication or authorization discussions, visit #talk-sikkerhet.
Requirements
Minimum requirements
| Requirement | Functionality | Comment | |-------------|-----------------------------|---------| | ES2020 | All | | | Node21 | PermissionDeliverRepository | |
Optional requirements
| Requirement | Functionality | Comment | |------------------------------|-----------------|--------------------------------------------------------| | typescript | All | | | @stomp/stompjs + websocket | In Memory Cache | Used to save network traffic and faster refresh rates. |
Getting Started
In this getting started guide we will describe how to set up a Node application with authorisation delivered by Permission Client for Node.
Links to other frameworks and services that are mentioned/relevant for this guide:
- Express
- Entur Partner / Permission Admin
- Permission Store
- The DevOps Handbook
- Application Security Requirements
- Technical Platform Documentation
Setup
Permission Client for Node is a standalone JavaScript module for authorization.
Before using it, ensure the following:
- Your application is running.
- Your application validates JWTs:
- For Express, you can use packages like express-jwt, express-oauth2-jwt-bearer or another package of your choice.
- Alternatively, validate JWTs manually using jwks-rsa and jsonwebtoken.
- Your application has an internal/partner Auth0 M2M (Machine-to-Machine) client available. Internal clients can be provisioned as described in The DevOps Handbook and How to implement authentication.
- You have set up a test framework, such as Jest, for security testing.
[!IMPORTANT] Permission Store does not validate JWTs. To ensure a secure service, your application must handle JWT validation.
Add Permission Client package
yarn add @entur-partner/permission-client-nodeDefine Permissions
Application permissions will be defined in TypeScript and later used in this Getting Started section.
Permissions defined by applications must follow the guidelines outlined in Confluence here.
Example: myPermissions.ts
The following example defines application permissions to be used in your application:
import { ApplicationPermission, BusinessCapability } from '@entur-partner/permission-client-node'
export const APPLICATION_PERMISSIONS : ApplicationPermission[] = [];
// Define Business Capabilities, typically used to secure endpoints.
export const OPERATION1_CREATE : BusinessCapability = {operation: 'operation1', access: 'opprett', description: '<description>'};
APPLICATION_PERMISSIONS.push(OPERATION1_CREATE);
export const OPERATION1_READ : BusinessCapability = {operation: 'operation1', access: 'les', description: '<description>'};
APPLICATION_PERMISSIONS.push(OPERATION1_READ);
// Define Responsibility Sets, typically used to secure data.
export const OPERATION2_READ : ApplicationPermission = {operation: 'operation2', access: 'les', responsibilityType: 'name1.attributt1', description: '<description>'};
APPLICATION_PERMISSIONS.push(OPERATION2_READ);
Initialize Permission Client
Setting up the Permission Client in your Node.js application involves initializing a TokenFactory and an AuthorizeCache. The following example demonstrates how to configure before use:
Example: myPermissionClientFactory.ts
import { TokenOptions, TokenFactory } from '@entur-partner/permission-client-node'
import { AuthorizeCacheType, Application, PermissionDeliverRepository } from '@entur-partner/permission-client-node'
import PermissionClient from '@entur-partner/permission-client-node'
import { APPLICATION_PERMISSIONS } from './myPermissions'
import { logger } from './myLogger'
// Define values for Auth0 internal client to be used when calling Permission Store
const clientTokenOptions : TokenOptions = {
domain: 'internal.dev.entur.org', // Changes to match environment.
clientId: '<MNG_AUTH0_INT_CLIENT_ID>', // Client ID from Google Secret Manager.
clientSecret: '<MNG_AUTH0_INT_CLIENT_SECRET>', // Client secret from Google Secret Manager.
audience: 'https://api.dev.entur.io', // Changes to match environment.
refreshBeforeMinValidSeconds: 600 // Refresh token when valid to is less than 10 minutter.
}
const application : Application = {
name : 'MyApplication', // Name of this application.
refreshRate : 300 // Refresh rate in seconds for in memory cache.
}
let permissionClient: AuthorizeCache;
const initPermissionClient = async () => {
try {
// TokenFactory will be used to get access tokens from Auth0
const accessTokenFactory = new TokenFactory(clientTokenOptions);
const permissionStoreUrl = new URL('permission-store URL'); // for local development: https://api.dev.entur.io/permission-store/v1
const repository = new PermissionDeliverRepository(application, accessTokenFactory, permissionStoreUrl);
permissionClient = await PermissionClient(AuthorizeCacheType.IN_MEMORY, APPLICATION_PERMISSIONS, repository);
// Optional to use WebSocket when running in Google Kubernetes Engine (GKE)
// const permissionClient = await PermissionClient(AuthorizeCacheType.IN_MEMORY, APPLICATION_PERMISSIONS, repository, { communicationType: CommunicationType.SocketJS });
permissionClient.setScheduleErrorHandler((error) => {
logger.warning('PermissionClient scheduler encountered an error', error);
});
logger.info('PermissionClient initialized');
} catch (error) {
logger.error('Failed to initialize PermissionClient', error);
throw Error('Failed to initialize PermissionClient. Please make sure permission store cache configurations are correct and try again.');
}
};
export { initPermissionClient, permissionClient };Use Permission Client
Example: server.ts
Initializing the Permission Client on Application Startup. In your main application file (e.g., server.ts or app.ts):
import express from 'express';
import { initPermissionClient, permissionClient } from './myPermissionClientFactory';
const app = express();
// Initialize Permission Client before starting the server
initPermissionClient()
.then(() => {
console.log('Permission Client initialized successfully');
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
})
.catch((error) => {
console.error('Failed to initialize Permission Client:', error);
process.exit(1); // Exit if initialization fails
});Example: requirePermission.ts
Checking Permissions in an Express Middleware and use permissionClient to enforce access control on API routes:
import { Request, Response, NextFunction } from 'express';
import { permissionClient } from './myPermissionClientFactory';
import { BusinessCapability, JwtDecoder } from '@entur-partner/permission-client-node';
// Middleware to check if a user has the required permission
const requirePermission = (requiredPermission: BusinessCapability) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
if (!permissionClient) {
return res.status(500).json({ error: 'Permission Client is not initialized' });
}
const userJwt = req.headers.authorization?.split(' ')[1]; // Extract JWT from Authorization header
if (!userJwt) {
return res.status(401).json({ error: 'Unauthorized: Missing token' });
}
const authoritySubject = JwtDecoder.getAuthoritySubject(userJwt);
const hasAccess = await permissionClient.checkBusinessCapabilityPermission(authoritySubject, requiredPermission);
if (!hasAccess) {
return res.status(403).json({ error: 'Forbidden: Insufficient permissions' });
}
next(); // Proceed if authorized
} catch (error) {
console.error('Permission check failed:', error);
return res.status(500).json({ error: 'Internal Server Error' });
}
};
};
export default requirePermission;Example: myRouter.ts
Securing a REST Endpoint with Permissions. Use the requirePermission middleware to protect your Express routes:
import express from 'express';
import requirePermission from './requirePermission';
import { OPERATION1_READ } from './myPermissions';
const router = express.Router();
router.get('/secure-data', validateBearerToken(req, res), requirePermission(OPERATION1_READ), (req, res) => {
res.json({ message: 'You have access to this secure data' });
});
export default router;Permissions
Defining and implementing permissions checks in the application is a central part of using Permission Client. Permission Client supports two types of permissions:
- Business Capability
- Responsibility Set
Business Capability Permissions is typically used for securing REST endpoints. And Responsibility Permissions is a good candidate when you want you securely share data between partners.
Permissions can be:
- Restricted to a single or every organisation.
- Restricted with the superuser flag.
Business Capability
Business capability is a right typically used to control access to endpoints.
Business Capability are defined in code and used when creating PermissionClient. Example:
export const OPERATION_CREATE : BusinessCapability = {operation: 'operation', access: 'opprett', description: '<description>'};Responsibility Set
Responsibility Set is a right designed to control access to data through permissions and agreements. This permission works similar to Business Capability, but in addition an agreement must be registered that the users organisation is allowed to access target data element.
Responsibility Sets are defined in code and used when creating PermissionClient. Example:
export const OPERATION_READ : ApplicationPermission = {operation: 'operation', access: 'les', responsibilityType: 'name.attributt', description: '<description>'};Agreement
Agreements are used in tandem with responsibility sets to restrict access to data based on which organisation the user/client belongs to. I.e. an agreement is used to describe the relationship between an organisation and some data element.
An application using Responsibility Sets will also normally maintain Agreements in its UI or delegates this to a related UI.
Note: Agreements can only be changed by the same application that created it.
To administrate agreements using Permission Client, applications can use the following methods on PermissionDeliverRepository:
- async getAgreements(responsibilitySet: ResponsibilitySet): Promise<Agreement[]>
- async storeAgreement(agreement: Agreement): Promise
- async deleteAgreement(agreementId: bigint): Promise
- async deleteObject(objectKey: ObjectKey): Promise // Similar to deleteAgreement, difference that it will delete all Agreements related to one object.
An Agreement is defined from:
- operation
- access
- responsibilityType
- responsibilityKey
- organisationId
Cache types
Permission Client for Node only supports one cache type: IN_MEMORY cache.
IN_MEMORY
Cache type IN-MEMORY is for production.
Optional: Push notifications
When the application is running in one of the internal GKE environments (dev, tst/staging, prd), you can tell Permission Client to use Permission Store push notifications through websockets, rather than polling the Store directly at a regular interval. To do this, add websocket dependencies:
yarn add @stomp/stompjs websocketAnd when you create PermissionClient pass the parameter typescript{ communicationType: CommunicationType.SocketJS }
const permissionClient = await PermissionClient(AuthorizeCacheType.IN_MEMORY, APPLICATION_PERMISSIONS, repository, { communicationType: CommunicationType.SocketJS });LOCAL_TEST_CACHE
LOCAL_TEST_CACHE is a cache type for running automatic tests. LOCAL_TEST_CACHE is not implemented for Permission Client for Node. It's expected that mock functionality in Jest will be used in automatic tests.
Miscellaneous
Use Permission Store without downtime
Starting from version 3.0.0 of Permission Client, each instance of your running service will receive a unique instance ID. This ID is used to track the permissions required by your service, allowing Permission Store to calculate the total permission requirements for your application.
When introducing new permissions, it's sometimes necessary to add them to application.yaml before using them in your code. This ensures that role management can be performed before the corresponding restrictions are enforced during code execution.
Get source
Clone module from GitHub with:
git clone https://github.com/entur/permission-client.gitInstall dependencies:
yarn installRun Jest tests:
yarn testPublishing
Publishing is done through GitHub Actions when a new release is published on the repository.
GitHub Release
Publishing a new release on GitHub is easy and is done with a few clicks.
- Go to releases tab and click "Draft a new release" in top right corner
- Click on "Choose a tag"
- If a tag is already not made for the release version, create a new one.
- Press "Generate release notes" and change content if needed
- Select "Set as the latest release" checkbox if this is not a pre-release.
- Press Publish Release
