@elegantys/polisafe
v0.1.0
Published
SDK NestJS para integrar @Permission/@Scope y autenticacion JWT/OpenID contra un servidor Polisafe (Polizei).
Maintainers
Readme
@elegantys/polisafe
NestJS client SDK for Polisafe IAM — protect your REST APIs with the authentication and authorization power of Polisafe, the multi-tenant Identity & Access Management platform (formerly Polizei).
Drop-in decorators (@Permission, @Scope, @UserToken, …), guards and Passport strategies that validate the RS256 JWTs issued by a Polisafe IAM server and resolve authorization — locally from token claims or remotely against the IAM server — so your services never re-implement auth.
Table of contents
- What it does
- How it works
- Requirements
- Installation
- Quick start
- Configuration options
- Decorators
- Authorization model
- Token types & auth flows
- Guards & strategies
- TypeScript
- Server API contract
- Troubleshooting
- Development
- License
What it does
- Authenticates every request by validating the
Authorization: BearerJWT (or a session cookie) against the Polisafe IAM server's per-tenant JWKS (RS256). - Resolves the tenant of the request from the token and the optional
X-Tenantheader. - Authorizes the route: the permission is checked either remotely (the IAM server decides,
POST …/polizei/io/authorization) or locally against thepermissionsclaims embedded in the access token. - Speaks machine-to-machine: it obtains and automatically renews a
client_credentialsaccess token so it can call the IAM server's authorization API on behalf of your service. - Keeps everything typed:
TokenPayload, config options and decorators ship with full TypeScript declarations.
Polisafe IAM is a technology of Elegantys. This package is the client-side utility: it consumes the APIs published by a Polisafe IAM server. You still need a running Polisafe IAM instance and an OAuth client registered on it.
How it works
┌──────────────────────┐ ┌────────────────────────────┐
Browser/App ──▶ Your NestJS service │ HTTP │ Polisafe IAM server │
(Bearer JWT) │ @elegantys/polisafe├───────▶│ • OAuth2 / OIDC token │
│ • jwt-header │ │ • per-tenant JWKS (/certs)│
│ • tenant guard │ │ • io/authorization check │
│ • privilege guard │ └────────────────────────────┘
└──────────────────────┘- A user signs in against Polisafe IAM and receives an RS256 JWT (OIDC) carrying claims such as
sub,tid/tenantId,tenants, optionallypermissions,scope,client_id. - Your route declares what it needs with a decorator (
@Permission('orders/list')). - The SDK's Passport strategy decodes the token, extracts the tenant, downloads the matching public key from
…/polisafe/openid/{tenant}/certsand verifies the signature. - Guards resolve the active tenant and decide allow/deny.
- When remote checks are enabled, the SDK authenticates itself with
client_credentialsand asks the IAM server whethersubmay perform{url, method}in its tenant.
Requirements
- Node.js ≥ 18
- NestJS 10 (
@nestjs/common,@nestjs/core,@nestjs/platform-express) - A reachable Polisafe IAM server and an OAuth client registered on it with:
- the
client_credentialsgrant, and - the scopes
security:io:authorization(remote checks) and optionallysecurity:io:tenants.
- the
peerDependencies are declared in package.json; npm ≥ 7 installs them automatically in the consuming project.
Installation
# from a registry
npm install @elegantys/polisafe
# or from a local checkout while developing
npm install ../sdk-polisafeQuick start
Register the module once in your root module:
// app.module.ts
import { Module } from '@nestjs/common';
import { PolisafeSdkModule } from '@elegantys/polisafe';
@Module({
imports: [
PolisafeSdkModule.register({
serviceUrl: process.env.POLISAFE_URL, // e.g. https://iam.example.com/api
issuer: process.env.POLISAFE_ISSUER, // e.g. https://iam.example.com
client_id: process.env.POLISAFE_CLIENT_ID, // your client_credentials client
client_secret: process.env.POLISAFE_CLIENT_SECRET,
scope: 'security:io:authorization', // enables remote permission checks
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
}),
],
})
export class AppModule {}Protect a REST controller:
// orders.controller.ts
import { Body, Controller, Get, Post } from '@nestjs/common';
import { Permission, TokenPayload, UserToken } from '@elegantys/polisafe';
@Controller('orders')
@Permission() // class level: every route in the controller is protected
export class OrdersController {
@Get()
list(@UserToken() user: TokenPayload) {
return { user: user.sub, tenant: user.tenant, items: [] };
}
@Post()
@Permission('orders/create') // more specific, overrides nothing: metadata is additive
create(@Body() body: unknown, @UserToken() user: TokenPayload) {
return { createdBy: user.id };
}
}The SDK registers JwtModule, HttpModule and ScheduleModule internally — you don't have to import anything else.
Configuration options
| Option | Type | Default | Description |
|---|---|---|---|
| serviceUrl | string | — (required) | Base URL of the Polisafe IAM server (public URL + path prefix), e.g. https://iam.example.com/api |
| issuer | string | — (required) | Expected iss claim of incoming tokens, usually the IAM public URL |
| client_id | string | — | OAuth client_credentials client id (needed for remote checks) |
| client_secret | string | — | OAuth client_credentials client secret (needed for remote checks) |
| scope | string | — | Scopes requested for the machine token, e.g. security:io:authorization |
| mode | 'production' \| 'development' | 'development' | development enables verbose error messages and debug logging |
| permissionCheck | boolean | true | Set to false to disable permission checks entirely — development only! |
| checkProviderPermissions | boolean | true | true: ask the IAM server (…/polizei/io/authorization); false: validate against user.permissions embedded in the token |
| audience | string | — | Expected audience (currently informational; not enforced by default) |
| logging | boolean | false | Reserved for future use |
| apiKey | string | — | Deprecated — legacy workspace key, do not use |
Options without defaults are merged over the SDK defaults at registration time, so you only need to pass what differs.
Decorators
@Permission(name?)
Authenticates (jwt-header strategy) → resolves the tenant (TenantGuard) → authorizes the route (PrivilegesGuard).
- Works at method or class level.
nameis the permission identifier registered in Polisafe IAM. When omitted, the guard falls back to the request's route path (/orders,POST /orders).
@Get('by-id/:id')
@Permission('orders:read')
read(@UserToken() user: TokenPayload) {
return user;
}@Scope(scopes, tokenType?)
Authenticates + tenant + validates OAuth scopes in the token's scope claim, optionally restricting the token type.
// only machine (client_credentials) tokens holding this scope may call this route
@Get('internal/tenants')
@Scope('security:io:tenants', 'client')
listTenants(@UserToken() root: TokenPayload) {
return { service: root.client_id };
}tokenType accepts 'user', 'client' or 'all' (default). The SDK tells them apart with sub === client_id.
@UserToken() / @TokenInfo()
Injects the verified token payload with convenience aliases:
@Get('me')
@Permission('profile:read')
me(@UserToken() user: TokenPayload) {
// user.sub — subject (string)
// user.id — alias of Number(user.sub)
// user.username — alias of preferred_username
// user.tid / .tenant — active tenant
// user.tenants — all tenants the principal belongs to
// user.is_client — sub === client_id
}@TokenInfo() is the same contract (originally used on OIDC userinfo-style endpoints).
@MapTenant(key)
Maps a request parameter (query or body) to one of the principal's tenants, returning 406 if the value is not inside user.tenants. Useful on multi-tenant admin routes to prevent cross-tenant access.
@Delete()
@Permission('orders:delete')
remove(@Body() body: Record<string, any>, @MapTenant('idScope') tenant: string) {
return { tenant }; // guaranteed to belong to the caller
}Authorization model
Permission names. In Polisafe IAM, permissions are stored per tenant and assigned to roles → users. The string you pass to @Permission() is matched by name (route.name), and optionally by HTTP method (route.type).
Two check modes (see PrivilegesGuard):
| checkProviderPermissions | Where the decision happens | Requirement |
|---|---|---|
| true (default) | IAM server — POST …/polizei/io/authorization with the machine token | client_id/client_secret/scope configured; IAM reachable |
| false | Your service — token's permissions claims | IAM client with access_token_include_permissions / openid_include_permissions enabled |
Tenant resolution. If the token lists several tenants (user.tenants), the active one comes from the X-Tenant request header and must be present in that list; otherwise the first tenant is used. A token cannot act outside its tenant boundaries.
Development mode. With mode: 'development' and permissionCheck: false, every request is allowed and guards return detailed error messages. Never ship this configuration to production — the SDK logs a warning when it detects it.
Token types & auth flows
User tokens — obtained from Polisafe IAM's OAuth 2.0 / OpenID Connect endpoints (authorization_code, password, implicit, refresh_token). Send them as:
Authorization: Bearer <access_token>Machine tokens — the SDK obtains its own via the OAuth 2.0 client_credentials grant:
POST {serviceUrl}/polisafe/oauth/tokenwith HTTP Basic authentication (client_id:client_secret) and the configuredscope.- The token is cached in the shared options object and renewed automatically: a scheduled job runs every minute and refreshes it when less than 120 seconds remain.
- Used as
Authorization: Bearer <machine-token>for remote permission checks.
Cookie sessions — for browser flows, the IAM server stores the token in a cookie named a-<client_id>. Use the cookie strategy:
import { JwtAuthCookieGuard } from '@elegantys/polisafe';
@Get('spa')
@UseGuards(JwtAuthCookieGuard)
spa(@UserToken() user: TokenPayload) {
return user;
}Remember to enable cookie-parser in your Nest app and to send client_id (query or body) so the SDK can locate the cookie.
Guards & strategies
Everything is exported for reuse or composition:
| Export | Kind | Notes |
|---|---|---|
| PolisafeSdkModule | module | register(options) returns a @Global dynamic module |
| JwtStrategyService | Passport strategy | name 'jwt-header', Bearer token, per-tenant JWKS |
| JwtCookieStrategyService | Passport strategy | name 'jwt-cookie', cookie a-<client_id> |
| PolizeiSdkService | service | machine client_credentials lifecycle |
| JwtAuthGuard / JwtAuthCookieGuard | guards | wrap the two strategies |
| TenantGuard, PrivilegesGuard, ScopesGuard | guards | authorization building blocks |
| ConfigPolizei, POLIZEI_CONFIG_OPTIONS | config | typed options + NestJS injection token |
| TokenPayload | type | claims of a verified Polisafe token |
Compose your own guards if the decorators are not enough:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { JwtAuthGuard, PrivilegesGuard, TenantGuard } from '@elegantys/polisafe';
@Injectable()
export class MyFullAuthGuard extends JwtAuthGuard implements CanActivate {
// e.g. run TenantGuard + PrivilegesGuard programmatically in canActivate()
}The strategies verify tokens only against Polisafe IAM's per-tenant JWKS — no shared secrets, no hardcoded keys, and key rotation is handled by jwks-rsa caching.
TypeScript
The package ships declarations. TokenPayload reflects the standard OIDC claims plus Polisafe's tenant/RBAC claims:
interface TokenPayload {
sub: string; // subject
iss: string; aud: string | string[];
exp: number; iat: number; jti?: string;
client_id?: string;
name?: string; given_name?: string; family_name?: string;
nickname?: string; preferred_username?: string;
picture?: string; email?: string; email_verified?: boolean;
roles?: string[]; permissions?: string[];
tid?: string; tenant?: string; tenants?: string[];
id: number; // Number(sub)
is_client?: boolean; // sub === client_id
}Need the options elsewhere? Inject them with full typing:
import { Inject } from '@nestjs/common';
import { ConfigPolizei, POLIZEI_CONFIG_OPTIONS } from '@elegantys/polisafe';
export class SomeService {
constructor(@Inject(POLIZEI_CONFIG_OPTIONS) private readonly options: ConfigPolizei) {}
}Server API contract
The SDK consumes the following Polisafe IAM endpoints. They are part of the public IAM surface; a future version of the SDK may pin an explicit API version.
| Endpoint | Purpose |
|---|---|
| POST {serviceUrl}/polisafe/oauth/token (client_credentials) | obtain / renew the machine token |
| GET {serviceUrl}/polisafe/openid/{tenant}/certs | per-tenant JWKS used to verify RS256 signatures |
| POST {serviceUrl}/polizei/io/authorization | remote permission decision: body { url, method, sub }, 200 = allowed |
| GET {serviceUrl}/polizei/io/tenants | (optional) tenant listing for machine clients |
The IAM server must issue tokens containing tid/tenantId and tenants; without them the strategies refuse the token (No tenant identifier found in token).
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| 401 on every protected route | Token missing, expired, wrong issuer, or the IAM server is unreachable for JWKS |
| No tenant identifier found in token | Token lacks tid/tenantId — check the IAM client scopes/config |
| 401 Tenant boundaries out of scope | X-Tenant not included in user.tenants — omit the header or send a valid tenant |
| 403 Forbidden (remote mode) | IAM says the user has no {url, method} permission — grant it in the IAM admin, or client_id/client_secret/scope are not configured |
| 403 Forbidden (local mode) | Token has no matching permissions entry — enable access_token_include_permissions on the client |
| Everything is allowed | permissionCheck: false (dev) is active — never enable in production |
| Errors show only in development | By design: production hides internals; guard messages depend on options.mode |
Development
npm install # install dev/peer dependencies
npm run build # tsc -> dist/
npm pack --dry-run # inspect the publishable tarballNotes for maintainers:
peerDependenciestarget NestJS 10.- The package is published from
dist/only (files: ["dist", "README.md"]). - If
npm installfails withEALLOWSCRIPTSunder npm ≥ 12, unset the injectednpm_config_allow_scriptsenvironment variable for the command, or use--ignore-scripts(none of the runtime dependencies require lifecycle scripts).
License
MIT — Polisafe IAM is a technology of Elegantys. This package is an independent client utility and does not include the IAM server.
