@primus-saas/identity-validator
v1.3.3
Published
Multi-issuer JWT/OIDC token validator for Node.js. Supports Azure AD, custom JWT issuers, and any OIDC-compliant identity provider. Works with Express and NestJS.
Maintainers
Readme
Primus SaaS Identity Validator - Node.js SDK
Version: 1.3.3
Library-only validator for JWT/OIDC tokens from your configured issuers (Azure AD, any OIDC authority, or JWT with secret/JWKS). No Primus-hosted login or Primus-issued tokens.
Module quick starts live in
docs-site/docs/modules(Identity, Logging, Notifications). Use those instead of the removed integration guide.
Features
- Multi-Issuer Support: Configure multiple identity providers (Azure AD, any OIDC authority, Local/JWKS JWT)
- Express middleware for seamless integration
- Role-based access control
- OIDC: Discovery + JWKS fetching, RS256/RS384/RS512/ES256/ES384/ES512 validation; Azure path retains tenant verification
- Full TypeScript support with type definitions
- 74 tests passing (core logic covered)
- Intelligent JWKS caching (24-hour TTL)
Installation
npm install @primus-saas/identity-validatorQuick Start
Multi-Issuer Configuration (Recommended)
import express from 'express';
import { primusIdentityMiddleware } from '@primus-saas/identity-validator';
const app = express();
// Configure multiple trusted issuers
const primusAuth = primusIdentityMiddleware({
issuers: [
{
name: 'AzureAD',
type: 'oidc',
issuer: 'https://login.microsoftonline.com/<YOUR_TENANT_ID>/v2.0',
authority: 'https://login.microsoftonline.com/<YOUR_TENANT_ID>/v2.0',
audiences: ['api://your-app-id']
},
{
name: 'LocalAuth',
type: 'jwt',
issuer: 'https://auth.yourcompany.com',
secret: process.env.LOCAL_SECRET,
audiences: ['api://your-app-id']
}
]
});
// Protected routes
app.get('/api/protected', primusAuth, (req, res) => {
res.json({ user: req.primusUser });
});
app.listen(3000);Configuration
IssuerConfig Options
Each issuer in the issuers array accepts:
| Property | Type | Required | Description | |----------|------|----------|-------------| | name | string | Yes | Friendly name for this issuer | | type | 'oidc' | 'jwt' | Yes | Type of identity provider | | issuer | string | Yes | Expected iss claim value (used for routing) | | authority | string | OIDC only | Authority URL for OIDC discovery | | audiences | string[] | Yes | Valid audience values (aud claim) | | secret | string | JWT only | Shared secret for HMAC validation | | jwksUrl | string | Optional | JWKS endpoint (alternative to secret) |
Global Options
| Property | Type | Default | Description | |----------|------|---------|-------------| | clockSkew | number | 300 | Clock tolerance in seconds | | validateLifetime | boolean | true | Validate token expiration | | requireHttpsMetadata | boolean | true | Require HTTPS for OIDC metadata/JWKS (set false for local dev only) | | jwksCacheTtl | number | 24 | JWKS cache TTL in hours | | rateLimiting | object | disabled | Throttle repeated failed validations (maxFailuresPerWindow, windowSeconds, maxGlobalFailuresPerWindow) |
Usage Examples
Single Azure AD Issuer
const primusAuth = primusIdentityMiddleware({
issuers: [
{
name: 'AzureAD',
type: 'oidc',
issuer: 'https://login.microsoftonline.com/cbd15a9b-cd52-4ccc-916a-00e2edb13043/v2.0',
authority: 'https://login.microsoftonline.com/cbd15a9b-cd52-4ccc-916a-00e2edb13043/v2.0',
audiences: ['e2760fbd-f134-42f4-bcda-f44306fc3fe2']
}
],
clockSkew: 300
});Single Local JWT Issuer
const primusAuth = primusIdentityMiddleware({
issuers: [
{
name: 'LocalAuth',
type: 'jwt',
issuer: 'http://localhost:4000',
secret: 'your-secret-key-min-32-chars',
audiences: ['api://my-app']
}
]
});Multi-Issuer (Hybrid)
const primusAuth = primusIdentityMiddleware({
issuers: [
{
name: 'AzureAD-Production',
type: 'oidc',
issuer: 'https://login.microsoftonline.com/<PROD_TENANT>/v2.0',
authority: 'https://login.microsoftonline.com/<PROD_TENANT>/v2.0',
audiences: ['api://prod-app']
},
{
name: 'AzureAD-Development',
type: 'oidc',
issuer: 'https://login.microsoftonline.com/<DEV_TENANT>/v2.0',
authority: 'https://login.microsoftonline.com/<DEV_TENANT>/v2.0',
audiences: ['api://dev-app']
},
{
name: 'LocalAuth',
type: 'jwt',
issuer: 'http://localhost:4000',
secret: process.env.LOCAL_SECRET,
audiences: ['api://dev-app']
}
]
});Role-Based Access Control
import { primusIdentityMiddleware, requireRoles } from '@primus-saas/identity-validator';
// Multiple roles (user needs at least one)
app.get('/api/admin', primusAuth, requireRoles('Admin', 'SuperAdmin'), (req, res) => {
res.json({ message: 'Admin access granted' });
});
// Single role
app.get('/api/manager', primusAuth, requireRoles('Manager'), (req, res) => {
res.json({ message: 'Manager access' });
});Using the Validator Directly
import { PrimusIdentityValidator } from '@primus-saas/identity-validator';
const validator = new PrimusIdentityValidator({
issuers: [
{
name: 'AzureAD',
type: 'oidc',
issuer: 'https://login.microsoftonline.com/<TENANT>/v2.0',
authority: 'https://login.microsoftonline.com/<TENANT>/v2.0',
audiences: ['api://my-app']
}
]
});
// Validate token manually (without "Bearer " prefix)
const token = 'eyJ0eXAiOiJKV1QiLCJhbGc...';
const result = await validator.validateToken(token);
if (result.isValid) {
console.log('Token validated successfully');
console.log('Claims:', result.claims);
} else {
console.error('Validation failed:', result.error);
}How It Works
Token Routing
- Extract iss claim from JWT (without verifying signature)
- Match issuer against configured issuers array
- Route to appropriate validator:
- type: 'oidc' -> Fetch JWKS, validate with RS256
- type: 'jwt' -> Validate with shared secret (HS256)
- Verify signature, issuer, audience, expiration
- Return result with claims or error
OIDC Validation Flow
For type: 'oidc' issuers:
- Fetch OpenID configuration from {authority}/.well-known/openid-configuration
- Retrieve JWKS from jwks_uri
- Find public key matching token's kid (Key ID)
- Verify RS256 signature
- Validate issuer, audience, tenant, expiration
- Cache JWKS for 24 hours
JWT Validation Flow
For type: 'jwt' issuers:
- Verify HMAC signature using shared secret
- Validate issuer matches configured issuer
- Validate audience is in audiences array
- Verify token expiration
API Reference
primusIdentityMiddleware(options)
Creates Express middleware that validates tokens and attaches user to req.primusUser.
Behavior:
- Extracts token from Authorization: Bearer header
- Routes to correct validator based on iss claim
- Returns 401 if validation fails
- Attaches PrimusUser to req.primusUser on success
- Attaches tenant context to req.primusTenantContext (and req.tenantContext) when a tenantResolver is provided
- Optional rate limiting (429 + Retry-After) for repeated failures via
rateLimiting
requireRoles(...roles: string[])
Middleware that enforces role-based access control.
Returns:
- 401 if user not authenticated
- 403 if user lacks required role
- Calls next() if user has at least one required role
PrimusUser Interface
interface PrimusUser {
userId: string; // Subject (sub claim)
email: string; // User email
name: string; // Display name
roles: string[]; // Assigned roles
additionalClaims: Record<string, any>; // Other JWT claims
}Migration from v1.0.0
Breaking Changes
The configuration structure has changed from single-mode to multi-issuer:
OLD (v1.0.0):
// Legacy configuration (Removed)
const primusAuth = primusIdentityMiddleware({
portalUrl: '...',
clientId: '...',
clientSecret: '...',
mode: ValidationMode.AzureAd,
tenantId: '...'
});NEW (v1.1.0):
// New configuration
const primusAuth = primusIdentityMiddleware({
issuers: [
{
name: 'AzureAD',
type: 'oidc',
issuer: 'https://login.microsoftonline.com/<TENANT>/v2.0',
authority: 'https://login.microsoftonline.com/<TENANT>/v2.0',
audiences: ['<CLIENT_ID>']
}
]
});Migration Steps
- Remove ValidationMode imports (no longer exported)
- Replace single config object with issuers array
- For Azure AD: Use type: 'oidc' with authority
- For Local: Use type: 'jwt' with secret
- Map clientId -> audiences[0]
- Map tenantId -> extract from issuer URL
Generating Tokens for Local JWT Issuer
Important: The secret, issuer, and audiences values used when generating tokens MUST EXACTLY MATCH your validator configuration.
Quick Example
import { signLocalToken } from '@primus-saas/identity-validator';
const token = signLocalToken({
userId: '123',
email: '[email protected]',
roles: ['Admin'],
issuer: 'https://auth.example.com',
audience: 'api://my-app',
secret: process.env.LOCAL_SECRET || '',
expiresInSeconds: 3600
});Manual Example
const jwt = require('jsonwebtoken');
function generateLocalJwtToken(userId, email, name) {
// Critical: Load from same environment variables
const secret = process.env.JWT_SECRET;
const issuer = process.env.JWT_ISSUER;
const audience = process.env.JWT_AUDIENCE;
const payload = {
sub: userId,
email: email,
name: name,
aud: audience,
iss: issuer
};
const options = {
expiresIn: '1h',
issuer: issuer,
audience: audience,
algorithm: 'HS256'
};
return jwt.sign(payload, secret, options);
}For complete token generation examples including frontend integration, see TOKEN_GENERATION_GUIDE.md
Troubleshooting
Common Errors
| Error | Cause | Solution | |-------|-------|----------| | invalid signature | Secret key mismatch | Ensure token generation and validation use the same secret | | Untrusted issuer | Issuer format incorrect | Use full URL format (e.g., https://localhost:4000) not name | | jwt audience invalid | Audience mismatch | Use API identifier format (e.g., api://your-app-id) | | jwt expired | Token past expiration | Generate new token or increase clockSkew |
"Untrusted issuer: {url}"
Cause: Token's iss claim doesn't match any configured issuer.
Solution: Add issuer to issuers array with exact iss value:
issuers: [
{
name: '...',
type: '...',
issuer: '<EXACT_ISS_VALUE_FROM_TOKEN>', // Must match exactly
// ...
}
]"Authority URL required for OIDC issuer"
Cause: OIDC issuer missing authority property.
Solution: Add authority URL:
{
type: 'oidc',
authority: 'https://login.microsoftonline.com/<TENANT>/v2.0', // Required
issuer: 'https://login.microsoftonline.com/<TENANT>/v2.0',
// ...
}"Shared secret required for JWT issuer"
Cause: JWT issuer missing secret property.
Solution: Provide shared secret:
{
type: 'jwt',
secret: process.env.JWT_SECRET, // Required (min 32 chars recommended)
issuer: 'https://...',
// ...
}Token Expired
Cause: Token's exp claim is in the past.
Solution: Increase clockSkew for clock drift tolerance:
{
issuers: [...],
clockSkew: 600 // 10 minutes tolerance
}For detailed troubleshooting, see ERROR_REFERENCE.md
Production Deployment
Caution: Never commit secrets to source control! Use environment variables or secret management services.
Quick Checklist
- [ ] All secrets in environment variables or vault
- [ ] HTTPS enforced in production
- [ ] CORS configured for production domains
- [ ] Rate limiting enabled
- [ ] Logging and monitoring configured
For complete deployment guide, see PRODUCTION_DEPLOYMENT.md
Development
Build
npm run buildTest
npm test
npm run test:coverageLint
npm run lint
npm run formatRequirements
- Node.js 16.0.0+
- Express 4.18.0+ (for middleware usage)
Documentation
- TOKEN_GENERATION_GUIDE.md - Complete guide to generating JWT tokens
- ERROR_REFERENCE.md - Troubleshooting validation errors
- PRODUCTION_DEPLOYMENT.md - Production deployment best practices
License
MIT
Support
- Documentation: https://primussoft.github.io/Primus-SaaS-Framework/docs/modules/identity-validator
- Issues: https://github.com/primussoft/Primus-SaaS-Framework/issues
- Email: [email protected]
