npm package: @jcdubs/janus
Description: Open source Serverless authentication: A Cedar-based authorisation engine for deterministic, deny-by-default access decisions through a CDK construct and SDK libraries.
Latest version: 1.2.0

# Janus


[![CI](https://github.com/JCDubs/Janus/actions/workflows/main.yaml/badge.svg?branch=main)](https://github.com/JCDubs/Janus/actions/workflows/main.yaml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node.js Version](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
[![pnpm](https://img.shields.io/badge/pnpm-9.x-orange)](https://pnpm.io)
[![Dependabot](https://github.com/JCDubs/Janus/actions/workflows/dependabot-pull-request.yaml/badge.svg)](https://github.com/JCDubs/Janus/actions/workflows/dependabot-pull-request.yaml)

<p align="center">
  <img src="./images/janus.jpg" alt="Janus" width="300"/>
</p>

Open source serverless authentication: A Cedar-based authorization engine for deterministic, deny-by-default access decisions through a CDK construct and SDK libraries.

## Overview

Janus is a TypeScript library that provides fine-grained, policy-based authorization for AWS Lambda functions using [Cedar](https://www.cedarpolicy.com/). It enables you to define complex authorization rules and evaluate them efficiently within your serverless applications.

Janus is based on the pattern discussed in this blog post: [Serverless: Granular Authorisation with Cedar — High control, minimal cost](https://medium.com/@jcdubs/serverless-granular-authorisation-with-cedar-high-control-minimal-cost-1149640f8cd9).

### Key Features

- 🔐 **Cedar Policy Engine** - Leverage Amazon's Cedar policy language for authorization
- ⚡ **Serverless Optimised** - Designed for AWS Lambda with singleton caching
- 🎯 **Type-Safe** - Full TypeScript support with comprehensive type definitions
- 🔄 **Fluent API** - Intuitive method chaining for building authorization requests
- 🧪 **Well Tested** - Comprehensive test coverage with real-world examples
- 📦 **Zero Config** - Easy integration with minimal setup

## Installation

### Install the Package

```bash
npm install @jcdubs/janus
```

or

```bash
pnpm add @jcdubs/janus
```

or

```bash
yarn add @jcdubs/janus
```

### Install Peer Dependencies

Janus requires the following peer dependencies to be installed in your project:

```bash
# For AWS Lambda PowerTools (logging)
npm install @aws-lambda-powertools/logger@2.15.0

# For AWS CDK (if using CDK constructs)
npm install aws-cdk-lib@2.219.0 constructs@10.4.2
```

With pnpm:

```bash
pnpm add @aws-lambda-powertools/logger@2.15.0
pnpm add aws-cdk-lib@2.219.0 constructs@10.4.2
```

With yarn:

```bash
yarn add @aws-lambda-powertools/logger@2.15.0
yarn add aws-cdk-lib@2.219.0 constructs@10.4.2
```

> **Note**: The CDK dependencies (`aws-cdk-lib` and `constructs`) are only required if you're using Janus in a CDK application. For Lambda runtime usage only, you just need `@aws-lambda-powertools/logger`.

## Quick Start

### 1. Define Your Cedar Policy

Create a `policies.cedar` file:

```cedar
// Allow users to view their own orders
permit (
  principal,
  action == Action::"viewOrder",
  resource
) when {
  principal.id == resource.customerId
};
```

### 2. Define Your Cedar Schema

Create a `schema.cedarschema` file:

```cedar
namespace OrderService {
  entity User = {
    id: String,
    roles: Set<Role>
  };
  
  entity Order = {
    customerId: String,
    status: String
  };
  
  entity Role;
  
  action viewOrder appliesTo {
    principal: User,
    resource: Order
  };
}
```

### Implement the Auth Lambda

The following example demonstrates a simple AWS Lambda handler that uses the middleware
to load Cedar authorization and then performs an authorization check inside the handler.

```typescript
import middy from '@middy/core';
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import {
  loadCedarAuthorization,
  AuthorizationService,
  EntityBuilder,
  getUserName,
} from '@jcdubs/janus';

const authorizationConfig = {
  namespace: 'OrderService::',
  principleType: 'User',
  resourceType: 'Order',
  roleType: 'Role',
};

const handler = async (
  event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
  // AuthorizationService is cached by the middleware, but retrieving it here is safe
  // and inexpensive (cached) and makes the intent explicit in the handler.
  const authService = await AuthorizationService.getService(authorizationConfig);

  const resourceId = event.pathParameters?.orderId ?? 'order-123';

  const isAuthorized = authService
    .setAction('viewOrder')
    .setResource(resourceId)
    .addEntity(
      new EntityBuilder(resourceId, authorizationConfig)
        .withStringAttr('customerId', getUserName())
        .build(),
    )
    .isAuthorized();

  return {
    statusCode: isAuthorized ? 200 : 403,
    body: JSON.stringify({ allowed: isAuthorized }),
  };
};

export const main = middy(handler).use(loadCedarAuthorization(authorizationConfig));
```

### 3. Use the Authorization Service

```typescript
import { AuthorizationService, EntityBuilder } from '@jcdubs/janus';

// Define the authorization configuration and initialize the service (cached as a singleton)
const authorizationConfig = {
  namespace: 'OrderService::',
  principleType: 'User',
  resourceType: 'Order',
  roleType: 'Role'
};

const authService = await AuthorizationService.getService(authorizationConfig);

// Examples showing varied `EntityBuilder` usage patterns

// Minimal: build an entity with only UID
const isAuthorizedMinimal = authService
  .setAction('viewOrder')
  .setResource('order-123')
  .addEntity(new EntityBuilder('order-123', authorizationConfig).build())
  .isAuthorized();

// Typical: add a few simple attributes
const isAuthorizedTypical = authService
  .setAction('viewOrder')
  .setResource('order-123')
  .addEntity(
    new EntityBuilder('order-123', authorizationConfig)
      .withStringAttr('customerId', 'user-456')
      .withStringAttr('status', 'PENDING')
      .withNumberAttr('items', 3)
      .build()
  )
  .isAuthorized();

// Full: include sets, references, extension attrs, parents and tags
const isAuthorizedFull = authService
  .setAction('viewOrder')
  .setResource('order-123')
  .addEntity(
    new EntityBuilder('order-123', authorizationConfig)
      .withStringAttr('customerId', 'user-456')
      .withBooleanAttr('active', true)
      .withNumberAttr('items', 5)
      .withSetAttr('flags', ['flagA', 'flagB'])
      .withAttr('owner', 'u1', authorizationConfig.principleType)
      .withExtnAttr('ip', 'ipaddr', '192.168.1.10')
      .withParent('role-1', 'Role')
      .withTag('label', 'lbl1', 'Label')
      .build()
  )
  .isAuthorized();

logger.info('Create authorisation requests', isAuthorizedMinimal, isAuthorizedTypical, isAuthorizedFull);
```

## API Reference

### AuthorizationService

#### Static Methods

##### `getService(config, refresh?)`

Retrieves or creates a cached instance of the Authorization Service.

**Parameters:**
- `config: AuthorizationConfigType` - Configuration object
  - `namespace: string` - Cedar namespace (e.g., `'OrderService::'`)
  - `principleType: string` - Principal entity type (e.g., `'User'`)
  - `resourceType: string` - Resource entity type (e.g., `'Order'`)
  - `roleType: string` - Role entity type (e.g., `'Role'`)
- `refresh?: boolean` - Force reload policies and schemas (default: `false`)

**Returns:** `Promise<AuthorizationService>`

#### Instance Methods

##### `setAction(action)`

Sets the action to be authorized.

**Parameters:**
- `action: string` - The action name

**Returns:** `this` (for chaining)

##### `setResource(resource)`

Sets the resource identifier.

**Parameters:**
- `resource: string` - The resource ID

**Returns:** `this` (for chaining)

##### `setContext(context)`

Sets the authorization context.

**Parameters:**
- `context: Record<string, cedar.CedarValueJson>` - Context data

**Returns:** `this` (for chaining)

##### `addEntity(entity)`

Adds an entity to the authorization request.

**Parameters:**
- `entity: cedar.EntityJson` - Entity definition

**Returns:** `this` (for chaining)

##### `addEntities(entities)`

Adds multiple entities to the authorization request.

**Parameters:**
- `entities: cedar.EntityJson[]` - Array of entity definitions

**Returns:** `this` (for chaining)

##### `isAuthorized()`

Evaluates the authorization request.

**Returns:** `boolean` - `true` if authorized, `false` otherwise

**Throws:**
- `MissingAuthenticatedUserDetailsError` - If user details are missing
- `MissingAuthorizationActionError` - If action is not set
- `MissingAuthorizationResourceError` - If resource is not set
- `MissingAuthorizationPolicyError` - If policies cannot be loaded
- `MissingAuthorizationSchemaError` - If schema cannot be loaded

### Middleware

#### `authorizationMiddleware`

Middy middleware for automatic authorization in Lambda handlers.

```typescript
import { authorizationMiddleware } from '@jcdubs/janus';
import middy from '@middy/core';

const handler = middy(async (event) => {
  // Your handler logic
})
  .use(authorizationMiddleware({
    namespace: 'OrderService::',
    principleType: 'User',
    resourceType: 'Order',
    roleType: 'Role',
  }));
```

### Auth Lambda Construct

Provides a CDK construct to bundle a Node.js Lambda with Cedar policy and schema files and the Cedar WASM runtime.

- **Export:** `AuthLambda` (class)
- **Props:** `AuthLambdaProps` — extends `NodejsFunctionProps` and adds `authorisation: { policyFilePath: string; schemaFilePath: string }`.

Usage: Use `AuthLambda` in CDK stacks to ensure Cedar policies and schema are bundled with the Lambda package and the Cedar WASM runtime copied into `node_modules/@cedar-policy/cedar-wasm`. In particular, `AuthLambda` makes sure the `@cedar-policy/cedar-wasm` package, your Cedar policy file (for example `policies.cedar`) and your Cedar schema file (for example `schema.cedarschema`) are included in the Lambda deployment package so they are available at runtime.

### EntityBuilder

Fluent builder for creating Cedar entity JSON objects used in authorization requests.

- **Export:** `EntityBuilder` (class)
- **Constructor:** `new EntityBuilder(id: string, authorizationConfig: AuthorizationConfigType, type?: string)`
- **Common Methods:** `withAttr(name, id, type)`, `withExtnAttr(name, fn, arg)`, `withBooleanAttr(name, value)`, `withNumberAttr(name, value)`, `withStringAttr(name, value)`, `withSetAttr(name, value)`, `withParent(id, type)`, `withTag(name, id, type)`, `build()` — returns `EntityJson`.

Example usage is shown in the Quick Start section above.

### File Loader

Small utility to read bundled files (Cedar policy and schema) from the Lambda package.

- **Export:** `loadFileAsString(fileName: string): string`

Throws an `Error` if the file cannot be read. Typically used by the `AuthorizationService` to load `policies.cedar` and `schema.cedarschema`.

### Types

Shared TypeScript types used across the library.

- `TypeAndId` — `{ type: string; id: string }`
- `EntityUidJson` — `{ __entity: TypeAndId } | TypeAndId`
- `CedarValueJson` — union of entity refs, extn values, primitives, arrays, objects, or null
- `FnAndArg` — `{ fn: string; arg: CedarValueJson }`
- `EntityJson` — `{ uid: EntityUidJson; attrs: Record<string, CedarValueJson>; parents: EntityUidJson[]; tags?: Record<string, CedarValueJson> }`

### Errors

The library exports a set of specific error classes used by the authorization flow.

- `MissingAuthenticatedUserDetailsError`
- `MissingAuthorizationActionError`
- `MissingAuthorizationPolicyError`
- `MissingAuthorizationResourceError`
- `MissingAuthorizationSchemaError`
- `UnauthorizedError`

These are exported from the `errors` module and are thrown by the `AuthorizationService` and middleware where applicable.

## User Details

The library provides utilities to extract user information from Lambda events:

```typescript
import { getUserName, getRoles } from '@jcdubs/janus';

const username = getUserName(event);
const roles = getRoles(event);
```

## Error Handling

The library provides specific error classes for different authorization failures:

- `MissingAuthenticatedUserDetailsError`
- `MissingAuthorizationActionError`
- `MissingAuthorizationPolicyError`
- `MissingAuthorizationResourceError`
- `MissingAuthorizationSchemaError`
- `UnauthorizedError`

## Examples

### Order Service Example

The `examples/order-service` project demonstrates a complete integration of Janus in a real-world serverless service. It shows how the Janus CDK construct, middleware and SDK are used together to provide Cedar-based authorization for AWS Lambda CRUD handlers.

- **Janus Integration**: The example uses the provided `Auth` Lambda construct and the `authorizationMiddleware` to bundle and load Cedar policy and schema files. The authorization checks inside the order CRUD Lambdas use the `AuthorizationService` from the Janus SDK (via the auth secondary adapter) to evaluate requests against the deployed Cedar policies and schema.
- **Full CRUD API**: The example implements a full Create/Read/Update/Delete API for `orders` backed by the included lambda handlers.
- **Scripts**: See the `examples/order-service/scripts` directory — it contains scripts to hydrate the database, create users and groups in the Cognito user pool, and login scripts for individual users associated with specific groups.
- **Postman Collection**: A Postman collection (`Auth.postman_collection.json`) is included in the example. It contains requests that exercise each user and group against the Cedar policy and schema files deployed with the order CRUD Lambdas.

See the [authorization-tests](./src/authorization-service/authorization-tests/) directory for comprehensive examples including:

- Customer role permissions
- Sales staff authorization
- Manager access controls
- Account manager restrictions
- Accountant read-only access

## Cedar Resources

- [Cedar Policy Tutorial](https://www.cedarpolicy.com/en/tutorial)
- [Cedar Policy Language Guide](https://docs.cedarpolicy.com/)
- [Cedar Policy Blog](https://www.cedarpolicy.com/blog)
- [Cedar SDK](https://github.com/cedar-policy)
- [Cedar Policy Playground](https://www.cedarpolicy.com/en/playground)
 - [Serverless: Granular Authorisation with Cedar — High control, minimal cost (blog post)](https://medium.com/@jcdubs/serverless-granular-authorisation-with-cedar-high-control-minimal-cost-1149640f8cd9)

## Development

### Prerequisites

- Node.js 20+ 
- pnpm 10+

### Peer Dependencies

This project uses peer dependencies to avoid version conflicts. The required peer dependencies are:

- `@aws-lambda-powertools/logger@2.15.0` - For structured logging
- `aws-cdk-lib@2.219.0` - For CDK constructs (optional)
- `constructs@10.4.2` - For CDK constructs (optional)

### Setup

```bash
# Install dependencies
pnpm install

# Run tests
pnpm test

# Run tests with coverage
pnpm test:coverage

# Build
pnpm build
```

### Testing

```bash
# Run all tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Generate coverage report
pnpm test:coverage
```

## Contributing

We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details on how to get started.

## License

This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.

## Security

For security concerns, please see [SECURITY.md](./SECURITY.md).

## Support

- 📫 [Open an issue](https://github.com/JCDubs/Janus/issues)
- 💬 [Discussions](https://github.com/JCDubs/Janus/discussions)

## Acknowledgments

- Built with [Cedar Policy](https://www.cedarpolicy.com/) by Amazon
- Powered by [@cedar-policy/cedar-wasm](https://www.npmjs.com/package/@cedar-policy/cedar-wasm)