{"id":51200326,"url":"https://github.com/softwarity/nestjs-granted","last_synced_at":"2026-06-28T00:01:36.526Z","repository":{"id":211494424,"uuid":"729319582","full_name":"softwarity/nestjs-granted","owner":"softwarity","description":"Security module for Nestjs","archived":false,"fork":false,"pushed_at":"2026-05-29T06:13:37.000Z","size":522,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-29T06:17:00.149Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/softwarity.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-12-08T22:22:42.000Z","updated_at":"2026-05-29T06:05:35.000Z","dependencies_parsed_at":null,"dependency_job_id":"3c8de5df-b4f7-4044-a436-f4fe85ca8061","html_url":"https://github.com/softwarity/nestjs-granted","commit_stats":null,"previous_names":["hhnest/granted","softwarity/nestjs-granted"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/softwarity/nestjs-granted","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-granted","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-granted/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-granted/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-granted/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/softwarity","download_url":"https://codeload.github.com/softwarity/nestjs-granted/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-granted/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34872279,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-28T00:01:35.580Z","updated_at":"2026-06-28T00:01:36.514Z","avatar_url":"https://github.com/softwarity.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @softwarity/nestjs-granted\n\n[![npm version](https://img.shields.io/npm/v/@softwarity/nestjs-granted.svg)](https://www.npmjs.com/package/@softwarity/nestjs-granted)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Node](https://img.shields.io/node/v/@softwarity/nestjs-granted.svg)](https://nodejs.org)\n[![Unit tests](https://github.com/softwarity/nestjs-granted/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/softwarity/nestjs-granted/actions/workflows/unit-tests.yml)\n\n**RBAC security for NestJS endpoints.** Declarative, decorator-based authorization built on a small algebra of composable boolean specifications — and a pluggable provider that reads the current user from HTTP headers or from a verified JWT.\n\n📚 **Full documentation:** [softwarity.github.io/nestjs-granted](https://softwarity.github.io/nestjs-granted/)\n\n---\n\n## Why?\n\nYou have endpoints behind an API gateway (or an OAuth2 proxy) that already authenticated the caller and forwards the identity — either as plain headers (`username`, `roles`) or as a `Bearer` JWT. You don't want another auth stack; you just want to **declare, per route, who is allowed in** and **inject the identity** into your handlers. That's exactly what this module does, and nothing more.\n\n```ts\n@Get('orders/:userId')\n@GrantedTo(and(isAuthenticated(), or(hasRole('ADMIN'), isUser('Param', 'userId'))))\nfindOrders(@Username() me: string, @Roles() roles: string[]) { /* ... */ }\n```\n\n## Features\n\n- 🛡️ **One decorator to secure a route** — `@GrantedTo(...specs)`, applied by a global guard\n- 🧩 **Composable boolean specifications** — `and`, `or`, `not`, `hasRole`, `isAuthenticated`, `isUser`, `isTenant`, `isTrue`, `isFalse`\n- 💉 **Parameter decorators** — `@Username()`, `@Roles()`, `@Tenant()`\n- 🪜 **Role hierarchy** — declare that one role implies others (`ADMIN ⇒ MANAGER ⇒ USER`); checks and injection see the expanded set\n- 🧹 **Known-roles filtering** — keep only the roles your module owns, ignoring those a shared token carries for other services\n- 🔌 **Pluggable principal provider** — HTTP headers (JSON or CSV roles) or a verified JWT\n- 🔑 **JWT verification** with **IdP presets** — RFC 9068/SCIM, Azure AD/Entra, Keycloak, Okta — or a fully custom claim mapping\n- 🏢 **Multi-tenant aware** — `@Tenant()` injection plus `isTenant` to block cross-tenant access\n- 🪶 **Tiny \u0026 dependency-light** — just `jsonwebtoken`; works on NestJS 10 \u0026 11\n\n## Installation\n\n```bash\nnpm install @softwarity/nestjs-granted\n# peer deps you probably already have\nnpm install @nestjs/common @nestjs/core @nestjs/platform-express rxjs reflect-metadata\n```\n\n### Peer dependencies\n\n| name | version |\n|---|---|\n| @nestjs/common | \u003e=10 \u003c12 |\n| @nestjs/core | \u003e=10 \u003c12 |\n| @nestjs/platform-express | \u003e=10 \u003c12 |\n| rxjs | ^7.5 |\n| reflect-metadata | ^0.1.13 \\|\\| ^0.2 |\n\n---\n\n## Getting started\n\n### 1. Register the module\n\n```ts\nimport { Module } from '@nestjs/common';\nimport { GrantedModule } from '@softwarity/nestjs-granted';\n\n@Module({\n  imports: [\n    // `apply: false` loads the module but disables enforcement (handy per environment).\n    GrantedModule.forRoot({ apply: true }),\n  ],\n})\nexport class AppModule {}\n```\n\nBy default the module reads the identity from HTTP headers (`username`, `roles`, `tenant`). To decode it from a JWT instead, pass a `GrantedJwtPrincipalProvider` (see below).\n\n### 2. Inject identity into your handlers\n\n```ts\n@Get('me')\nme(\n  @Username() username: string,\n  @Roles() roles: string[],\n  @Tenant() tenant: string | undefined,\n) {\n  return { username, roles, tenant };\n}\n```\n\n### 3. Secure endpoints\n\n```ts\n@Get('admin')\n@GrantedTo(and(isAuthenticated(), hasRole('ADMIN')))\nadminOnly() { /* ... */ }\n```\n\nA route with **no** `@GrantedTo` is open. A route with `@GrantedTo(...)` passes only if **every** spec returns `true`.\n\n`@GrantedTo` also applies at the **controller class** level — a baseline for every route inside it. Class and method specs are **merged**: all of them must pass (class = baseline, method tightens).\n\n```ts\n@Controller('admin')\n@GrantedTo(isAuthenticated())          // baseline: every route requires a logged-in caller\nexport class AdminController {\n  @Get('stats')\n  stats() { /* needs: isAuthenticated() */ }\n\n  @Get('config')\n  @GrantedTo(hasRole('ADMIN'))         // tightened: isAuthenticated() AND hasRole('ADMIN')\n  config() { /* ... */ }\n}\n```\n\n\u003e There is no \"opt-out\": a method can't loosen a class-level spec (specs are AND-merged). Leave a controller un-annotated and secure routes individually if some must stay open.\n\n---\n\n## Boolean specifications\n\n`@GrantedTo` takes one or more `BooleanSpec`. Combine them freely:\n\n```ts\nGrantedTo(...specs: BooleanSpec[])     // all must pass\n\nand(...specs)                          // every spec passes\nor(...specs)                           // at least one passes\nnot(spec)                              // inverts a spec\nisTrue()                               // always allow\nisFalse()                              // always deny\nhasRole(role: string)                  // role is in the user's roles (after hierarchy expansion)\nisAuthenticated()                      // username is set and not 'anonymous'\nisUser(type: 'Param'|'Query'|'Body', field: string)    // request value === username\nisTenant(type: 'Param'|'Query'|'Body', field: string)  // request value === caller's tenant\n```\n\n### Ownership checks — `isUser` / `isTenant`\n\n`isAuthenticated()` and `hasRole()` prove *who* the caller is. They do **not** prove that the record a request targets belongs to that caller — the classic **IDOR** hole, where a logged-in user just edits an id in the URL or body to hit someone else's data.\n\nConsider `POST /orders` protected only by `isAuthenticated()`. Mallory is a real, logged-in user; she forges the body so the order is booked on **Alice's** account:\n\n```bash\ncurl -X POST https://api.example.com/orders \\\n  -H 'authorization: Bearer \u003cmallory-valid-token\u003e' \\\n  -d '{ \"customer\": { \"id\": \"alice\" }, \"items\": [ ... ] }'\n```\n\nAuth passes — the token is valid. Nothing checks that `body.customer.id` is *her own* id. `isUser` reads that value **from the request** and requires it to equal the caller's `username`:\n\n```ts\n// the owner declared in the body must be the caller (admins excepted)\n@Post('orders')\n@GrantedTo(and(isAuthenticated(), or(hasRole('ADMIN'), isUser('Body', 'customer.id'))))\ncreateOrder() { /* body.customer.id === caller, or caller is ADMIN */ }\n\n// the resource owner in the URL must be the caller\n@Patch('users/:userId/profile')\n@GrantedTo(or(hasRole('ADMIN'), isUser('Param', 'userId')))\nupdate(@Param('userId') userId: string) { /* ... */ }\n```\n\nMallory's forged POST now returns `403` (`'alice'` ≠ `'mallory'`), and she can't read `/users/alice/profile` by swapping the id.\n\n`isTenant` is the same check one level up — for multi-tenant APIs. It matches the **requested** tenant (URL/query/body) against the caller's **claimed** tenant (from the token/headers, never the attacker-controlled payload), blocking cross-tenant access:\n\n```ts\n@Post('tenants/:tenantId/invoices')\n@GrantedTo(and(isAuthenticated(), or(hasRole('ADMIN'), isTenant('Param', 'tenantId'))))\ncreateInvoice() { /* a request for /tenants/globex/... from an acme token is rejected */ }\n```\n\n\u003e Authorization reads `username`, `roles` and (via `isTenant`) `tenant`. Note `isTenant` only checks that a *requested* tenant matches the *claimed* one — it does not replace data-layer scoping (`WHERE tenant_id = ?`), which you still apply with the injected `@Tenant()` value.\n\n---\n\n## Roles: known set \u0026 hierarchy\n\nTwo module-level options shape the roles before the guard and `@Roles()` ever see them. They work with any provider (header or JWT).\n\n**`knownRoles`** — a gateway often issues one token whose roles span several services. Declare the roles *this* module cares about and the rest are dropped, so your view isn't polluted:\n\n```ts\nGrantedModule.forRoot({\n  knownRoles: ['ORDER_READ', 'ORDER_WRITE', 'ORDER_ADMIN'],\n});\n// token roles ['ORDER_WRITE', 'BILLING_ADMIN', 'CRM_USER'] → seen as ['ORDER_WRITE']\n```\n\n**`roleHierarchy`** — map a role to the roles it implies. Expansion is transitive and cycle-safe, applied *before* `knownRoles` filtering, for both the guard and `@Roles()`:\n\n```ts\nGrantedModule.forRoot({\n  roleHierarchy: {\n    ORDER_ADMIN: ['ORDER_WRITE'],\n    ORDER_WRITE: ['ORDER_READ'],\n  },\n});\n// caller holds ['ORDER_ADMIN'] → hasRole('ORDER_READ') passes; @Roles() yields all three\n```\n\n---\n\n## Principal providers\n\nThe identity is resolved by an `IGrantedPrincipalProvider`. Two are shipped.\n\n### `GrantedPrincipalProvider` (default) — from headers\n\n| info | default header | parsing | fallback |\n|---|---|---|---|\n| `username` | `username` | raw string | `anonymous` |\n| `roles` | `roles` | JSON array, or CSV | `[]` |\n| `tenant` | `tenant` | raw string | `undefined` |\n\nBoth the **header names** and the **roles encoding** are configurable:\n\n```ts\nimport { GrantedModule, GrantedPrincipalProvider } from '@softwarity/nestjs-granted';\n\nGrantedModule.forRoot({\n  principalProvider: new GrantedPrincipalProvider({\n    usernameHeader: 'x-user',   // default 'username'\n    rolesHeader: 'x-roles',     // default 'roles'\n    tenantHeader: 'x-tenant',   // default 'tenant'\n    rolesFormat: 'csv',         // default 'json' — 'ROLE1, ROLE2' instead of [\"ROLE1\",\"ROLE2\"]\n  }),\n});\n```\n\n\u003e These options are specific to the header provider — JWT identity comes from configurable claims (`rolesClaim`, etc.), and roles there are already an array.\n\n### `GrantedJwtPrincipalProvider` — from a verified JWT\n\nReads the `Authorization: Bearer \u003ctoken\u003e` header, verifies the token with your public key, and maps the claims to `username` / `roles` / `tenant`. Claim names are configurable (dotted paths supported for nested claims), with presets for common IdPs:\n\n```ts\nimport { GrantedModule, GrantedJwtPrincipalProvider } from '@softwarity/nestjs-granted';\n\n@Module({\n  imports: [\n    GrantedModule.forRoot({\n      apply: true,\n      // Preset — you only provide the key material:\n      principalProvider: GrantedJwtPrincipalProvider.keycloak({\n        algorithm: 'RS256',\n        pemFile: 'config/jwt_public_key.pem',\n      }),\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n#### Presets\n\n| Factory | username | roles | tenant |\n|---|---|---|---|\n| `GrantedJwtPrincipalProvider.rfc9068(...)` | `sub` | `roles` | `tenant` |\n| `GrantedJwtPrincipalProvider.azureAd(...)` | `preferred_username` | `roles` | `tid` |\n| `GrantedJwtPrincipalProvider.keycloak(...)` | `preferred_username` | `realm_access.roles` | `tenant` |\n| `GrantedJwtPrincipalProvider.okta(...)` | `sub` | `groups` | `tenant` |\n\nEvery field is overridable, e.g. `GrantedJwtPrincipalProvider.okta({ pemFile, usernameClaim: 'email' })`.\n\n#### Custom claim mapping\n\n```ts\nnew GrantedJwtPrincipalProvider({\n  algorithm: 'RS256',\n  pemFile: 'config/jwt_public_key.pem',\n  // or base64Key: '-----BEGIN PUBLIC KEY-----\\n...\\n-----END PUBLIC KEY-----',\n  usernameClaim: 'sub',              // default 'sub'\n  rolesClaim: 'realm_access.roles',  // default 'roles' — dotted paths supported\n  tenantClaim: 'tid',                // default 'tenant'\n});\n```\n\n\u003e A token that is missing, malformed, or fails verification yields an **anonymous** request — it's then up to your specs (e.g. `isAuthenticated()`) to reject it. The provider never logs the token or the key material.\n\n### Custom provider\n\nImplement `IGrantedPrincipalProvider` to read the identity from anywhere. Handle both `Request` (route handlers) and `IncomingMessage` (param decorators run earlier in the pipeline):\n\n```ts\nexport class MyGrantedPrincipalProvider implements IGrantedPrincipalProvider {\n  getUsernameFromRequest(req: Request): string { return req.header('x-user') || 'anonymous'; }\n  getRolesFromRequest(req: Request): string[] { return JSON.parse(req.header('x-roles') || '[]'); }\n  getTenantFromRequest(req: Request): string | undefined { return req.header('x-tenant') || undefined; }\n\n  getUsernameFromIncomingMessage(msg: IncomingMessage): string { return (msg.headers['x-user'] as string) || 'anonymous'; }\n  getRolesFromIncomingMessage(msg: IncomingMessage): string[] { return JSON.parse((msg.headers['x-roles'] as string) || '[]'); }\n  getTenantFromIncomingMessage(msg: IncomingMessage): string | undefined { return (msg.headers['x-tenant'] as string) || undefined; }\n}\n```\n\n```ts\nGrantedModule.forRoot({ apply: true, principalProvider: new MyGrantedPrincipalProvider() })\n```\n\n---\n\n## License\n\nMIT © [Softwarity](https://www.softwarity.io/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoftwarity%2Fnestjs-granted","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoftwarity%2Fnestjs-granted","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoftwarity%2Fnestjs-granted/lists"}