egain-ps-utils
v4.0.1
Published
eGain PS Utils. A utility package for eGain PS
Maintainers
Readme
egain-ps-utils
JWT authorizer utilities for eGain PS, built for AWS API Gateway Lambda authorizers and Azure AD (or any OpenID Connect issuer with JWKS). The package ships dual module builds (ESM + CommonJS) with full TypeScript types.
Package version: 4.0.0
Node.js: >=22.0.0 (native fetch + AbortSignal.timeout)
What changed in 4.0.0
| Area | Behavior |
|------|----------|
| Public API | validateToken, generateAuthResponse, getDecodedJwtToken, AuthError / AuthErrorCode, ValidateTokenOptions, PolicyDocument |
| Removed | validateTokenWithSecret (AWS Secrets Manager), isTokenIssuedForValidClaimTenantId |
| Config | Pass issuer + audience explicitly to validateToken — no Secrets Manager dependency |
| Errors | Failures throw AuthError with stable code, optional statusCode, and standard Error.cause |
| Crypto | JWKS JWK → PEM via Node crypto.createPublicKey (no third-party PEM converters) |
| Network | OpenID config + JWKS fetches use a 10s timeout |
| Modules | exports map: ESM import → dist/esm, CJS require → dist/cjs |
Requirements
- Node.js 22 or higher
- Network egress from the Lambda/runtime to the issuer’s OpenID discovery and JWKS endpoints
Install
npm install egain-ps-utilsyarn add egain-ps-utilsPackage entrypoints
| Condition | Path |
|-----------|------|
| Types | dist/cjs/index.d.ts |
| import (ESM) | dist/esm/index.js |
| require (CJS) | dist/cjs/index.js |
Built with:
npm run build # clean + CJS + ESM
npm run test:cjs # CommonJS smoke tests (after build)
npm run test:mjs # ESM smoke tests (after build)Features
validateToken(authorization, options)
Validates a JWT from an HTTP Authorization header against the issuer’s OpenID configuration and JWKS.
What it checks:
- Header is present and non-empty after stripping an optional
Bearerprefix - Token header contains
kid issuerandaudienceoptions are non-empty strings- Fetches
{issuer}/.well-known/openid-configuration(trailing slash normalized) - Fetches JWKS from
jwks_uri, matcheskid, builds RSA public key - Verifies signature and claims with
jsonwebtoken(algorithms: ['RS256'],issuer,audience)
Returns: { isTokenValid: true } on success
Throws: AuthError on any failure (does not return false)
type ValidateTokenOptions = {
issuer: string; // e.g. https://login.microsoftonline.com/<tenantId>/v2.0
audience: string; // app ID URI or client ID expected in `aud`
};generateAuthResponse(effect, methodArn)
Builds an API Gateway Lambda authorizer allow/deny response.
| Field | Value |
|-------|--------|
| principalId | 'apigateway.amazonaws.com' |
| policyDocument.Version | '2012-10-17' |
| policyDocument.Statement[0].Action | 'execute-api:Invoke' |
| policyDocument.Statement[0].Effect | your effect (Allow / Deny) |
| policyDocument.Statement[0].Resource | wildcard ARN for same API + stage (…/stage/*) when the input ARN has ≥4 /-separated parts |
Throws: AuthError (UNHANDLED, status 500) if effect or methodArn is missing.
getDecodedJwtToken(jwtString)
Decodes a JWT without verifying signature or claims. Uses jsonwebtoken.decode(jwt, { complete: true }).
Returns: { header, payload, signature } (Jwt from jsonwebtoken)
Throws: AuthError (TOKEN_DECODE_FAILED, status 401) if the token is malformed
Use only for claim inspection after (or separate from)
validateToken. Decoding alone is not authentication.
Error handling (AuthError)
import { AuthError, type AuthErrorCode } from 'egain-ps-utils';| Property | Description |
|----------|-------------|
| code | Stable machine-readable AuthErrorCode — branch on this |
| statusCode | Optional HTTP-ish status (401, 500, …) |
| message | Human-readable message |
| cause | Underlying error (standard Error cause), if any |
| name | Always 'AuthError' |
AuthErrorCode values
| Code | Typical meaning | Typical statusCode |
|------|-----------------|----------------------|
| INVALID_AUTH_HEADER | Missing/invalid Authorization header | 401 |
| EMPTY_TOKEN | Bearer token empty after strip | 401 |
| INVALID_ISSUER | Missing/invalid issuer option | 500 |
| INVALID_AUDIENCE | Missing/invalid audience option | 500 |
| TOKEN_DECODE_FAILED | JWT decode failed | 401 |
| MISSING_KID | header.kid missing | 401 |
| TOKEN_INVALID | Signature/claims verification failed (expired, wrong aud/iss, …) | 401 |
| OPEN_ID_CONFIG_FETCH_FAILED | OpenID discovery fetch/parse failed | 500 |
| SIGNING_CERT_FETCH_FAILED | JWKS fetch/parse failed | 500 |
| SIGNING_CERT_KEY_NOT_FOUND | No JWKS key for token kid | 401 |
| UNHANDLED | Unexpected internal error | 500 |
try {
await validateToken(authorization, { issuer, audience });
} catch (error) {
if (error instanceof AuthError) {
console.error(error.code, error.statusCode, error.message, error.cause);
}
throw error;
}How verification works
Authorization: Bearer <jwt>
│
▼
strip Bearer / validate header
│
▼
decode JWT → read header.kid
│
▼
GET {issuer}/.well-known/openid-configuration (10s timeout)
│
▼
GET jwks_uri → match kid → RSA PEM (crypto.createPublicKey)
│
▼
jsonwebtoken.verify (RS256, issuer, audience)
│
▼
{ isTokenValid: true } or throw AuthErrorIssuer URL: if issuer does not end with /, one is appended before resolving .well-known/openid-configuration.
Usage
API Gateway Lambda authorizer (ESM)
import {
validateToken,
generateAuthResponse,
getDecodedJwtToken,
AuthError,
} from 'egain-ps-utils';
const ISSUER = process.env.JWT_ISSUER; // e.g. https://login.microsoftonline.com/<tenant>/v2.0
const AUDIENCE = process.env.JWT_AUDIENCE;
export const handler = async (event) => {
const authorization =
event.headers?.Authorization || event.headers?.authorization;
try {
if (!authorization) {
throw new AuthError('INVALID_AUTH_HEADER', 'Authorization token is missing', {
statusCode: 401,
});
}
await validateToken(authorization, {
issuer: ISSUER,
audience: AUDIENCE,
});
return generateAuthResponse('Allow', event.methodArn);
} catch (error) {
if (error instanceof AuthError) {
console.error('Authorization failed', {
code: error.code,
statusCode: error.statusCode,
message: error.message,
});
} else {
console.error('Authorization error', error);
}
return generateAuthResponse('Deny', event.methodArn);
}
};CommonJS
const {
validateToken,
generateAuthResponse,
getDecodedJwtToken,
AuthError,
} = require('egain-ps-utils');
exports.handler = async (event) => {
const authorization =
event.headers?.Authorization || event.headers?.authorization;
try {
if (!authorization) {
throw new AuthError('INVALID_AUTH_HEADER', 'Authorization token is missing', {
statusCode: 401,
});
}
await validateToken(authorization, {
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE,
});
return generateAuthResponse('Allow', event.methodArn);
} catch (error) {
if (error instanceof AuthError) {
console.error('Authorization failed', {
code: error.code,
statusCode: error.statusCode,
message: error.message,
});
} else {
console.error('Authorization error', error);
}
return generateAuthResponse('Deny', event.methodArn);
}
};TypeScript
import {
validateToken,
generateAuthResponse,
getDecodedJwtToken,
AuthError,
type ValidateTokenOptions,
type PolicyDocument,
} from 'egain-ps-utils';
interface AuthorizerEvent {
headers?: {
Authorization?: string;
authorization?: string;
};
methodArn: string;
}
const options: ValidateTokenOptions = {
issuer: process.env.JWT_ISSUER!,
audience: process.env.JWT_AUDIENCE!,
};
export const handler = async (event: AuthorizerEvent) => {
const authorization =
event.headers?.Authorization || event.headers?.authorization;
try {
if (!authorization) {
throw new AuthError('INVALID_AUTH_HEADER', 'Authorization token is missing', {
statusCode: 401,
});
}
await validateToken(authorization, options);
return generateAuthResponse('Allow', event.methodArn);
} catch (error) {
if (error instanceof AuthError) {
console.error('Authorization failed', {
code: error.code,
statusCode: error.statusCode,
message: error.message,
});
}
return generateAuthResponse('Deny', event.methodArn);
}
};
/** Inspect claims only — does not prove the token is valid */
export const readClaims = (jwtString: string) => {
const decoded = getDecodedJwtToken(jwtString);
return {
sub: decoded.payload && typeof decoded.payload === 'object'
? (decoded.payload as Record<string, unknown>).sub
: undefined,
aud: decoded.payload && typeof decoded.payload === 'object'
? (decoded.payload as Record<string, unknown>).aud
: undefined,
exp:
decoded.payload &&
typeof decoded.payload === 'object' &&
typeof (decoded.payload as { exp?: number }).exp === 'number'
? new Date((decoded.payload as { exp: number }).exp * 1000)
: undefined,
};
};Decode then inspect (after validation)
import { validateToken, getDecodedJwtToken } from 'egain-ps-utils';
await validateToken(authorization, { issuer, audience });
const token = authorization.replace(/^Bearer\s+/i, '').trim();
const { header, payload } = getDecodedJwtToken(token);
// use payload claims (roles, oid, tid, …)Configuration (Azure AD example)
Register an app in Microsoft Entra ID (Azure AD).
Note the Application (client) ID or app ID URI used as token
aud→audience.Issuer is typically:
https://login.microsoftonline.com/<tenant-id>/v2.0Pass both into
validateTokenfrom env, SSM, your own secret layer, or config — this package no longer reads AWS Secrets Manager.
API reference
Exports
| Export | Kind | Description |
|--------|------|-------------|
| validateToken | async function | Validate Bearer JWT via issuer JWKS |
| generateAuthResponse | function | Lambda authorizer policy response |
| getDecodedJwtToken | function | Decode JWT (no verify) |
| AuthError | class | Structured auth failure |
| AuthErrorCode | type | Union of error codes |
| ValidateTokenOptions | type | { issuer, audience } |
| PolicyDocument | type | IAM policy shape on the authorizer response |
validateToken
validateToken(
authorization: string,
options: ValidateTokenOptions
): Promise<{ isTokenValid: true }>generateAuthResponse
generateAuthResponse(
effect: string,
methodArn: string
): {
principalId: string;
policyDocument: PolicyDocument;
}getDecodedJwtToken
getDecodedJwtToken(jwtString: string): Jwt // jsonwebtoken Jwt (complete)AuthError
new AuthError(
code: AuthErrorCode,
message: string,
opts?: { statusCode?: number; error?: unknown }
)
// opts.error is stored as Error.causePolicyDocument
interface PolicyDocument {
Version: string;
Statement: Array<{
Action: string;
Effect: string;
Resource: string;
}>;
}Dependency notes
| Package | Role |
|---------|------|
| jsonwebtoken | Decode + verify JWT (verify, decode) |
| Node built-ins | fetch, AbortSignal.timeout, crypto.createPublicKey |
Runtime peers for Secrets Manager are not required.
Notes
- Prefer
instanceof AuthErrorand branch oncode, not raw error strings. validateTokenalways throws on failure; there is no{ isTokenValid: false }success/failure tuple.- Expired tokens surface as
TOKEN_INVALID(or network codes if discovery/JWKS cannot be reached). - Resource ARNs from
generateAuthResponseare stage-wide wildcards when the method ARN parses as expected — fine for many authorizer setups; tighten separately if you need method-level scoping. - Dual package layout means the same import path works in both ESM and CommonJS consumers via the
exportsfield.
