security-express
Version:
security-express
410 lines (288 loc) • 7.85 kB
Markdown
# security-express
A lightweight authorization middleware for **Express** applications.
`security-express` provides a simple, flexible authorization layer based on middleware and dependency injection. It does not dictate how users authenticate or where permissions are stored, allowing it to integrate seamlessly with existing authentication systems and data sources.
## Features
* Express-native middleware
* Dependency injection for permission resolution
* Supports bitmask permissions
* Supports hierarchical permission levels
* Database-agnostic
* Framework-independent permission loading
* Zero runtime dependencies (other than Express)
* Small, composable API
## Installation
```bash
npm install security-express
```
or
```bash
yarn add security-express
```
## Request flow
```text
Auth middleware → sets res.locals.userId
↓
authorize(\"invoice\", write)
↓
calls privilege(userId, \"invoice\")
↓
returns permission bitmask (e.g. 3 = read|write)
↓
If (action & p) === action → next(),
else 403
```
## Concepts
The library separates authorization into two responsibilities:
* **Authorizer** — Express middleware that enforces permissions.
* **PrivilegeLoader** — Helper that loads user permissions from a data source.
Authentication is intentionally outside the scope of this library.
```
Authentication
│
▼
res.locals.userId
│
▼
Authorizer
│
▼
privilege(userId, privilege)
│
▼
Database / API / Redis / LDAP / ...
```
## Examples
- [cms-backoffice](https://github.com/content-system/cms-backoffice): A backoffice microservice for a CMS (user, role, audit-log, category, content, job)
- [admin-service](https://github.com/fintech-product/admin-service): A backoffice microservice for a common fintech product (user, role, audit-log, currency, country, locale)
## Basic Usage
```typescript
import express from "express";
import { Authorizer, PrivilegeLoader, read, write} from "security-express";
const app = express();
const loader = new PrivilegeLoader(
`
SELECT permission
FROM user_privileges
WHERE user_id = ?
AND privilege = ?
`,
query
);
const authorizer = new Authorizer(loader.privilege, console.error);
app.get(
"/users",
authorizer.authorize("user", read),
(req, res) => {
res.send("Allowed");
}
);
app.post(
"/users",
authorizer.authorize("user", write),
(req, res) => {
res.send("Created");
}
);
```
# Authorizer
`Authorizer` creates Express middleware that verifies whether the current user has sufficient permissions.
```typescript
const authorizer = new Authorizer(privilege, logError);
```
### Constructor
```typescript
new Authorizer(
privilege,
logError,
exact?,
userId?,
permissions?
)
```
| Parameter | Description |
| ------------- | ---------------------------------------------------------------------------- |
| `privilege` | Function that returns a user's permission value. |
| `logError` | Error logging callback. |
| `exact` | Permission comparison mode. Defaults to `true`. |
| `userId` | Key used in `res.locals` for the authenticated user id. Default: `"userId"`. |
| `permissions` | Key used to store resolved permissions. Default: `"permissions"`. |
## authorize()
```typescript
authorizer.authorize(privilege, action?)
```
Returns an Express middleware.
```typescript
app.get(
"/orders",
authorizer.authorize("order", read),
handler
);
```
If `action` is omitted, the middleware only verifies that the user possesses the specified privilege.
# Permission Models
## Bitmask Permissions (Default)
The default mode uses bitwise permission flags.
```typescript
import { read, write, approve } from "security-express";
```
| Permission | Value |
| ---------- | ---------: |
| `none` | 0 |
| `read` | 1 |
| `write` | 2 |
| `approve` | 4 |
| `all` | 2147483647 |
Permissions may be combined.
```typescript
const permission = read | write;
```
Checking permissions:
```typescript
authorizer.authorize("invoice", write);
```
A user with
```
read | write
```
is authorized.
## Hierarchical Permissions
If permissions represent levels instead of flags, set `exact` to `false`.
```typescript
const authorizer = new Authorizer(privilege, logError, false);
```
Example:
| Level | Meaning |
| ----: | ------------- |
| 1 | Viewer |
| 2 | Editor |
| 3 | Manager |
| 4 | Administrator |
A user with level `4` automatically satisfies checks for levels `1`, `2`, and `3`.
# PrivilegeLoader
`PrivilegeLoader` is a helper that converts database results into permission values.
```typescript
const loader = new PrivilegeLoader(sql, query);
```
### Constructor
```typescript
new PrivilegeLoader(sql, query)
```
Where
```typescript
query<T>(sql, args): Promise<T[]>
```
can be implemented using any database library.
Example:
```typescript
const loader = new PrivilegeLoader(
`
SELECT permission
FROM user_privileges
WHERE user_id = ?
AND privilege = ?
`,
db.query
);
```
The loader combines multiple rows using bitwise OR.
For example:
| Row |
| --: |
| 1 |
| 2 |
| 4 |
becomes
```
7
```
# Custom Permission Provider
Instead of using SQL, permissions may come from any source.
```typescript
const authorizer = new Authorizer(
async (userId, privilege) => {
return permissionService.getPermission(
userId,
privilege
);
},
console.error
);
```
Possible sources include:
* SQL databases
* MongoDB
* Redis
* REST APIs
* GraphQL
* LDAP
* Active Directory
# Authentication
Authentication is intentionally not included.
Populate `res.locals.userId` before calling the authorization middleware.
Example:
```typescript
app.use(jwtAuthentication);
app.use(authorizer.authorize("invoice", read));
```
# Error Responses
The middleware returns:
| Status | Description |
| -----: | ------------------------------------------- |
| 401 | User is not authenticated. |
| 403 | User does not have the required permission. |
# API
## Types
```typescript
type Handle
```
Express middleware.
```typescript
type Authorize
```
Authorization middleware factory.
```typescript
interface SimpleMap
```
Simple key/value map used for logging.
## Constants
```typescript
none
read
write
approve
all
```
## Exports
```typescript
Authorizer
PrivilegeLoader
none
read
write
approve
all
toString
Handle
Authorize
SimpleMap
```
# Design Philosophy
`security-express` focuses on one responsibility:
> Determine whether a request is authorized.
It deliberately avoids implementing authentication, session management, JWT validation, OAuth, or user management. Those concerns belong to the application.
By keeping authorization independent from authentication and storage, the library remains lightweight, composable, and easy to integrate into existing Express applications.
# License
MIT